lpdf-mis.lua /size: 21 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['lpdf-mis'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Although we moved most pdf handling to the lua end, we didn't change
10-- the overall approach. For instance we share all resources i.e. we
11-- don't make subsets for each xform or page. The current approach is
12-- quite efficient. A big difference between MkII and MkIV is that we
13-- now use forward references. In this respect the MkII code shows that
14-- it evolved over a long period, when backends didn't provide forward
15-- referencing and references had to be tracked in multiple passes. Of
16-- course there are a couple of more changes.
17
18local next, tostring, type = next, tostring, type
19local format, gsub, formatters = string.format, string.gsub, string.formatters
20local concat, flattened = table.concat, table.flattened
21local settings_to_array = utilities.parsers.settings_to_array
22
23local backends, lpdf, nodes = backends, lpdf, nodes
24
25local nodeinjections       = backends.pdf.nodeinjections
26local codeinjections       = backends.pdf.codeinjections
27local registrations        = backends.pdf.registrations
28
29local nuts                 = nodes.nuts
30local copy_node            = nuts.copy
31
32local nodepool             = nuts.pool
33local pageliteral          = nodepool.pageliteral
34local register             = nodepool.register
35
36local pdfdictionary        = lpdf.dictionary
37local pdfarray             = lpdf.array
38local pdfconstant          = lpdf.constant
39local pdfreference         = lpdf.reference
40local pdfunicode           = lpdf.unicode
41local pdfverbose           = lpdf.verbose
42local pdfstring            = lpdf.string
43local pdfflushobject       = lpdf.flushobject
44local pdfflushstreamobject = lpdf.flushstreamobject
45local pdfaction            = lpdf.action
46local pdfminorversion      = lpdf.minorversion
47
48local formattedtimestamp   = lpdf.pdftimestamp
49local adddocumentextgstate = lpdf.adddocumentextgstate
50local addtocatalog         = lpdf.addtocatalog
51local addtoinfo            = lpdf.addtoinfo
52local addtopageattributes  = lpdf.addtopageattributes
53local addtonames           = lpdf.addtonames
54
55local pdfgetmetadata       = lpdf.getmetadata
56
57local texset               = tex.set
58
59local variables            = interfaces.variables
60
61local v_stop               = variables.stop
62local v_none               = variables.none
63local v_max                = variables.max
64local v_bookmark           = variables.bookmark
65local v_fit                = variables.fit
66local v_doublesided        = variables.doublesided
67local v_singlesided        = variables.singlesided
68local v_default            = variables.default
69local v_auto               = variables.auto
70local v_fixed              = variables.fixed
71local v_landscape          = variables.landscape
72local v_portrait           = variables.portrait
73local v_page               = variables.page
74local v_paper              = variables.paper
75local v_attachment         = variables.attachment
76local v_layer              = variables.layer
77local v_lefttoright        = variables.lefttoright
78local v_righttoleft        = variables.righttoleft
79local v_title              = variables.title
80local v_nomenubar          = variables.nomenubar
81
82local positive             = register(pageliteral("/GSpositive gs"))
83local negative             = register(pageliteral("/GSnegative gs"))
84local overprint            = register(pageliteral("/GSoverprint gs"))
85local knockout             = register(pageliteral("/GSknockout gs"))
86
87local omitextraboxes       = false
88
89directives.register("backend.omitextraboxes", function(v) omitextraboxes = v end)
90
91local function initializenegative()
92    local a = pdfarray { 0, 1 }
93    local g = pdfconstant("ExtGState")
94    local d = pdfdictionary {
95        FunctionType = 4,
96        Range        = a,
97        Domain       = a,
98    }
99    local negative = pdfdictionary { Type = g, TR = pdfreference(pdfflushstreamobject("{ 1 exch sub }",d)) } -- can be shared
100    local positive = pdfdictionary { Type = g, TR = pdfconstant("Identity") }
101    adddocumentextgstate("GSnegative", pdfreference(pdfflushobject(negative)))
102    adddocumentextgstate("GSpositive", pdfreference(pdfflushobject(positive)))
103    initializenegative = nil
104end
105
106local function initializeoverprint()
107    local g = pdfconstant("ExtGState")
108    local knockout  = pdfdictionary { Type = g, OP = false, OPM  = 0 }
109    local overprint = pdfdictionary { Type = g, OP = true,  OPM  = 1 }
110    adddocumentextgstate("GSknockout",  pdfreference(pdfflushobject(knockout)))
111    adddocumentextgstate("GSoverprint", pdfreference(pdfflushobject(overprint)))
112    initializeoverprint = nil
113end
114
115function nodeinjections.overprint()
116    if initializeoverprint then initializeoverprint() end
117    return copy_node(overprint)
118end
119function nodeinjections.knockout ()
120    if initializeoverprint then initializeoverprint() end
121    return copy_node(knockout)
122end
123
124function nodeinjections.positive()
125    if initializenegative then initializenegative() end
126    return copy_node(positive)
127end
128function nodeinjections.negative()
129    if initializenegative then initializenegative() end
130    return copy_node(negative)
131end
132
133-- function codeinjections.addtransparencygroup()
134--     -- png: /CS /DeviceRGB /I true
135--     local d = pdfdictionary {
136--         S = pdfconstant("Transparency"),
137--         I = true,
138--         K = true,
139--     }
140--     lpdf.registerpagefinalizer(function() addtopageattributes("Group",d) end) -- hm
141-- end
142
143-- actions (todo: store and update when changed)
144
145local openpage, closepage, opendocument, closedocument
146
147function codeinjections.registerdocumentopenaction(open)
148    opendocument = open
149end
150
151function codeinjections.registerdocumentcloseaction(close)
152    closedocument = close
153end
154
155function codeinjections.registerpageopenaction(open)
156    openpage = open
157end
158
159function codeinjections.registerpagecloseaction(close)
160    closepage = close
161end
162
163local function flushdocumentactions()
164    if opendocument then
165        addtocatalog("OpenAction",pdfaction(opendocument))
166    end
167    if closedocument then
168        addtocatalog("CloseAction",pdfaction(closedocument))
169    end
170end
171
172local function flushpageactions()
173    if openpage or closepage then
174        local d = pdfdictionary()
175        if openpage then
176            d.O = pdfaction(openpage)
177        end
178        if closepage then
179            d.C = pdfaction(closepage)
180        end
181        addtopageattributes("AA",d)
182    end
183end
184
185lpdf.registerpagefinalizer    (flushpageactions,    "page actions")
186lpdf.registerdocumentfinalizer(flushdocumentactions,"document actions")
187
188--- info : this can change and move elsewhere
189
190local identity = { }
191
192function codeinjections.setupidentity(specification)
193    for k, v in next, specification do
194        if v ~= "" then
195            identity[k] = v
196        end
197    end
198end
199
200function codeinjections.getidentityvariable(name)
201    return identity[name]
202end
203
204local done = false  -- using "setupidentity = function() end" fails as the meaning is frozen in register
205
206local function setupidentity()
207    if not done then
208        local metadata = pdfgetmetadata()
209        local creator  = metadata.creator
210        local version  = metadata.contextversion
211        local time     = metadata.time
212        local jobname  = environment.jobname or tex.jobname or "unknown"
213        --
214        local title = identity.title
215        if not title or title == "" then
216            title = tex.jobname
217        end
218        addtoinfo("Title", pdfunicode(title), title)
219        local subtitle = identity.subtitle or ""
220        if subtitle ~= "" then
221            addtoinfo("Subject", pdfunicode(subtitle), subtitle)
222        end
223        local author = identity.author or ""
224        if author ~= "" then
225            addtoinfo("Author",  pdfunicode(author), author) -- '/Author' in /Info, 'Creator' in XMP
226        end
227        addtoinfo("Creator", pdfunicode(creator), creator)
228        addtoinfo("CreationDate", pdfstring(formattedtimestamp(time)))
229        local date = identity.date or ""
230        local pdfdate = date and formattedtimestamp(date)
231        if pdfdate then
232            addtoinfo("ModDate", pdfstring(pdfdate), date)
233        else
234            -- users should enter the date in 2010-01-19T23:27:50+01:00 format
235            -- and if not provided that way we use the creation time instead
236            addtoinfo("ModDate", pdfstring(formattedtimestamp(time)),time)
237        end
238        local keywords = identity.keywords or ""
239        if keywords ~= "" then
240            keywords = concat(settings_to_array(keywords), " ")
241            addtoinfo("Keywords", pdfunicode(keywords), keywords)
242        end
243        local id = lpdf.id()
244        addtoinfo("ID", pdfstring(id), id) -- needed for pdf/x
245        --
246        addtoinfo("ConTeXt.Version",version)
247        addtoinfo("ConTeXt.Time",os.date("%Y-%m-%d %H:%M"))
248        addtoinfo("ConTeXt.Jobname",jobname)
249     -- addtoinfo("ConTeXt.Url","www.pragma-ade.com")
250        addtoinfo("ConTeXt.Url","github.com/contextgarden/context")
251        addtoinfo("ConTeXt.Support","contextgarden.net")
252        addtoinfo("TeX.Support","tug.org")
253        --
254        done = true
255    else
256        -- no need for a message
257    end
258end
259
260lpdf.registerpagefinalizer(setupidentity,"identity")
261
262-- or when we want to be able to set things after pag e1:
263--
264-- lpdf.registerdocumentfinalizer(setupidentity,1,"identity")
265
266local function flushjavascripts()
267    local t = interactions.javascripts.flushpreambles()
268    if #t > 0 then
269        local a = pdfarray()
270        local pdf_javascript = pdfconstant("JavaScript")
271        for i=1,#t do
272            local ti     = t[i]
273            local name   = ti[1]
274            local script = ti[2]
275            local j = pdfdictionary {
276                S  = pdf_javascript,
277                JS = pdfreference(pdfflushstreamobject(script)),
278            }
279            a[#a+1] = pdfstring(name)
280            a[#a+1] = pdfreference(pdfflushobject(j))
281        end
282        addtonames("JavaScript",pdfreference(pdfflushobject(pdfdictionary{ Names = a })))
283    end
284end
285
286lpdf.registerdocumentfinalizer(flushjavascripts,"javascripts")
287
288-- -- --
289
290local plusspecs = {
291    [v_max] = {
292        mode = "FullScreen",
293    },
294    [v_bookmark] = {
295        mode = "UseOutlines",
296    },
297    [v_attachment] = {
298        mode = "UseAttachments",
299    },
300    [v_layer] = {
301        mode = "UseOC",
302    },
303    [v_fit] = {
304        fit  = true,
305    },
306    [v_doublesided] = {
307        layout = "TwoColumnRight",
308    },
309    [v_fixed] = {
310        fixed  = true,
311    },
312    [v_landscape] = {
313        duplex = "DuplexFlipShortEdge",
314    },
315    [v_portrait] = {
316        duplex = "DuplexFlipLongEdge",
317    },
318    [v_page] = {
319        duplex = "Simplex" ,
320    },
321    [v_paper] = {
322        paper  = true,
323    },
324    [v_title] ={
325        title = true,
326    },
327    [v_lefttoright] ={
328        direction = "L2R",
329    },
330    [v_righttoleft] ={
331        direction = "R2L",
332    },
333    [v_nomenubar] ={
334        nomenubar = true,
335    },
336}
337
338local pagespecs = {
339    --
340    [v_max]         = plusspecs[v_max],
341    [v_bookmark]    = plusspecs[v_bookmark],
342    [v_attachment]  = plusspecs[v_attachment],
343    [v_layer]       = plusspecs[v_layer],
344    [v_lefttoright] = plusspecs[v_lefttoright],
345    [v_righttoleft] = plusspecs[v_righttoleft],
346    [v_title]       = plusspecs[v_title],
347    --
348    [v_none] = {
349    },
350    [v_fit] = {
351        mode = "UseNone",
352        fit  = true,
353    },
354    [v_doublesided] = {
355        mode   = "UseNone",
356        layout = "TwoColumnRight",
357        fit = true,
358    },
359    [v_singlesided] = {
360        mode   = "UseNone"
361    },
362    [v_default] = {
363        mode   = "UseNone",
364        layout = "auto",
365    },
366    [v_auto] = {
367        mode   = "UseNone",
368        layout = "auto",
369    },
370    [v_fixed] = {
371        mode   = "UseNone",
372        layout = "auto",
373        fixed  = true, -- noscale
374    },
375    [v_landscape] = {
376        mode   = "UseNone",
377        layout = "auto",
378        fixed  = true,
379        duplex = "DuplexFlipShortEdge",
380    },
381    [v_portrait] = {
382        mode   = "UseNone",
383        layout = "auto",
384        fixed  = true,
385        duplex = "DuplexFlipLongEdge",
386    },
387    [v_page] = {
388        mode   = "UseNone",
389        layout = "auto",
390        fixed  = true,
391        duplex = "Simplex",
392    },
393    [v_paper] = {
394        mode   = "UseNone",
395        layout = "auto",
396        fixed  = true,
397        duplex = "Simplex",
398        paper  = true,
399    },
400    [v_nomenubar] = {
401        mode      = "UseNone",
402        layout    = "auto",
403        nomenubar = true,
404    },
405}
406
407local pagespec, topoffset, leftoffset, height, width, doublesided = "default", 0, 0, 0, 0, false
408local cropoffset, bleedoffset, trimoffset, artoffset = 0, 0, 0, 0
409local marked = false
410local copies = false
411
412local getpagedimensions  getpagedimensions = function()
413    getpagedimensions = backends.codeinjections.getpagedimensions
414    return getpagedimensions()
415end
416
417function codeinjections.setupcanvas(specification)
418    local paperheight = specification.paperheight
419    local paperwidth  = specification.paperwidth
420    local paperdouble = specification.doublesided
421    --
422    paperwidth, paperheight = codeinjections.setpagedimensions(paperwidth,paperheight)
423    --
424    pagespec    = specification.mode       or pagespec
425    topoffset   = specification.topoffset  or 0
426    leftoffset  = specification.leftoffset or 0
427    height      = specification.height     or paperheight
428    width       = specification.width      or paperwidth
429    marked      = specification.print
430    --
431    copies      = specification.copies
432    if copies and copies < 2 then
433        copies = false
434    end
435    --
436    cropoffset  = specification.cropoffset  or 0
437    trimoffset  = cropoffset  - (specification.trimoffset  or 0)
438    bleedoffset = trimoffset  - (specification.bleedoffset or 0)
439    artoffset   = bleedoffset - (specification.artoffset   or 0)
440    --
441    if paperdouble ~= nil then
442        doublesided = paperdouble
443    end
444end
445
446local function documentspecification()
447    if not pagespec or pagespec == "" then
448        pagespec = v_default
449    end
450    local settings = settings_to_array(pagespec)
451    -- so the first one detemines the defaults
452    local first    = settings[1]
453    local defaults = pagespecs[first]
454    local spec     = defaults or pagespecs[v_default]
455    -- successive keys can modify this
456    if spec.layout == "auto" then
457        if doublesided then
458            local s = pagespecs[v_doublesided] -- to be checked voor interfaces
459            for k, v in next, s do
460                spec[k] = v
461            end
462        else
463            spec.layout = false
464        end
465    end
466    -- we start at 2 when we have a valid first default set
467    for i=defaults and 2 or 1,#settings do
468        local s = plusspecs[settings[i]]
469        if s then
470            for k, v in next, s do
471                spec[k] = v
472            end
473        end
474    end
475    -- maybe interfaces.variables
476    local layout    = spec.layout
477    local mode      = spec.mode
478    local fit       = spec.fit
479    local fixed     = spec.fixed
480    local duplex    = spec.duplex
481    local paper     = spec.paper
482    local title     = spec.title
483    local direction = spec.direction
484    local nomenubar = spec.nomenubar
485    if layout then
486        addtocatalog("PageLayout",pdfconstant(layout))
487    end
488    if mode then
489        addtocatalog("PageMode",pdfconstant(mode))
490    end
491    local prints = nil
492    if marked then
493        local pages     = structures.pages
494        local marked    = pages.allmarked(marked)
495        local nofmarked = marked and #marked or 0
496        if nofmarked > 0 then
497            -- the spec is wrong in saying that numbering starts at 1 which of course makes
498            -- sense as most real documents start with page 0 .. sigh
499            for i=1,#marked do marked[i] = marked[i] - 1 end
500            prints = pdfarray(flattened(pages.toranges(marked)))
501        end
502    end
503    if fit or fixed or duplex or copies or paper or prints or title or direction or nomenubar then
504        addtocatalog("ViewerPreferences",pdfdictionary {
505            FitWindow         = fit       and true                   or nil,
506            PrintScaling      = fixed     and pdfconstant("None")    or nil,
507            Duplex            = duplex    and pdfconstant(duplex)    or nil,
508            NumCopies         = copies    and copies                 or nil,
509            PickTrayByPDFSize = paper     and true                   or nil,
510            PrintPageRange    = prints                               or nil,
511            DisplayDocTitle   = title     and true                   or nil,
512            Direction         = direction and pdfconstant(direction) or nil,
513            HideMenubar       = nomenubar and true                   or nil,
514        })
515    end
516    addtoinfo   ("Trapped", pdfconstant("False")) -- '/Trapped' in /Info, 'Trapped' in XMP
517    addtocatalog("Version", pdfconstant(format("1.%s",pdfminorversion())))
518    addtocatalog("Lang",    pdfstring(tokens.getters.macro("currentmainlanguage")))
519end
520
521-- temp hack: the mediabox is not under our control and has a precision of 5 digits
522
523local factor  = number.dimenfactors.bp
524local f_value = formatters["%.6N"]
525
526local function boxvalue(n) -- we could share them
527    return pdfverbose(f_value(factor * n))
528end
529
530local function pagespecification()
531    local paperwidth, paperheight = codeinjections.getpagedimensions()
532    local llx = leftoffset
533    local lly = paperheight + topoffset - height
534    local urx = width - leftoffset
535    local ury = paperheight - topoffset
536    -- boxes can be cached
537    local function extrabox(WhatBox,offset,always)
538        if offset ~= 0 or always then
539            addtopageattributes(WhatBox, pdfarray {
540                boxvalue(llx + offset),
541                boxvalue(lly + offset),
542                boxvalue(urx - offset),
543                boxvalue(ury - offset),
544            })
545        end
546    end
547    if omitextraboxes then
548        -- only useful for testing / comparing
549    else
550        extrabox("CropBox",cropoffset,true) -- mandate for rendering
551        extrabox("TrimBox",trimoffset,true) -- mandate for pdf/x
552        extrabox("BleedBox",bleedoffset)    -- optional
553     -- extrabox("ArtBox",artoffset)        -- optional .. unclear what this is meant to do
554    end
555end
556
557lpdf.registerpagefinalizer(pagespecification,"page specification")
558lpdf.registerdocumentfinalizer(documentspecification,"document specification")
559
560-- Page Label support ...
561--
562-- In principle we can also support /P (prefix) as we can just use the verbose form
563-- and we can then forget about the /St (start) as we don't care about those few
564-- extra bytes due to lack of collapsing. Anyhow, for that we need a stupid prefix
565-- variant and that's not on the agenda now.
566
567local map = {
568    numbers       = "D",
569    Romannumerals = "R",
570    romannumerals = "r",
571    Characters    = "A",
572    characters    = "a",
573}
574
575-- local function featurecreep()
576--     local pages, lastconversion, list = structures.pages.tobesaved, nil, pdfarray()
577--     local getstructureset = structures.sets.get
578--     for i=1,#pages do
579--         local p = pages[i]
580--         if not p then
581--             return -- fatal error
582--         else
583--             local numberdata = p.numberdata
584--             if numberdata then
585--                 local conversionset = numberdata.conversionset
586--                 if conversionset then
587--                     local conversion = getstructureset("structure:conversions",p.block,conversionset,1,"numbers")
588--                     if conversion ~= lastconversion then
589--                         lastconversion = conversion
590--                         list[#list+1] = i - 1 -- pdf starts numbering at 0
591--                         list[#list+1] = pdfdictionary { S = pdfconstant(map[conversion] or map.numbers) }
592--                     end
593--                 end
594--             end
595--             if not lastconversion then
596--                 lastconversion = "numbers"
597--                 list[#list+1] = i - 1 -- pdf starts numbering at 0
598--                 list[#list+1] = pdfdictionary { S = pdfconstant(map.numbers) }
599--             end
600--         end
601--     end
602--     addtocatalog("PageLabels", pdfdictionary { Nums = list })
603-- end
604
605local function featurecreep()
606    local pages        = structures.pages.tobesaved
607    local list         = pdfarray()
608    local getset       = structures.sets.get
609    local stopped      = false
610    local oldlabel     = nil
611    local olconversion = nil
612    for i=1,#pages do
613        local p = pages[i]
614        if not p then
615            return -- fatal error
616        end
617        local label = p.viewerprefix or ""
618        if p.status == v_stop then
619            if not stopped then
620                list[#list+1] = i - 1 -- pdf starts numbering at 0
621                list[#list+1] = pdfdictionary {
622                    P = pdfunicode(label),
623                }
624                stopped = true
625            end
626            oldlabel      = nil
627            oldconversion = nil
628            stopped       = false
629        else
630            local numberdata = p.numberdata
631            local conversion = nil
632            local number     = p.number
633            if numberdata then
634                local conversionset = numberdata.conversionset
635                if conversionset then
636                    conversion = getset("structure:conversions",p.block,conversionset,1,"numbers")
637                end
638            end
639            conversion = conversion and map[conversion] or map.numbers
640            if number == 1 or oldlabel ~= label or oldconversion ~= conversion then
641                list[#list+1] = i - 1 -- pdf starts numbering at 0
642                list[#list+1] = pdfdictionary {
643                    S  = pdfconstant(conversion),
644                    St = number,
645                    P  = label ~= "" and pdfunicode(label) or nil,
646                }
647            end
648            oldlabel      = label
649            oldconversion = conversion
650            stopped       = false
651        end
652    end
653    addtocatalog("PageLabels", pdfdictionary { Nums = list })
654end
655
656lpdf.registerdocumentfinalizer(featurecreep,"featurecreep")
657