lpdf-mis.lmt /size: 18 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 pdfbackend           = backends and backends.registered.pdf or { }
24local nodeinjections       = pdfbackend.nodeinjections
25local codeinjections       = pdfbackend.codeinjections
26local registrations        = pdfbackend.registrations
27
28local getpagedimensions    = layouts.getpagedimensions
29local getcanvas            = layouts.getcanvas
30
31local nodes                = nodes
32local nuts                 = nodes.nuts
33local copy_node            = nuts.copy
34
35local nodepool             = nuts.pool
36local setstate             = nodepool.setstate
37local register             = nodepool.register
38
39local lpdf                 = lpdf
40local pdfdictionary        = lpdf.dictionary
41local pdfarray             = lpdf.array
42local pdfconstant          = lpdf.constant
43local pdfreference         = lpdf.reference
44local pdfunicode           = lpdf.unicode
45local pdfverbose           = lpdf.verbose
46local pdfstring            = lpdf.string
47local pdfaction            = lpdf.action
48local pdfflushobject       = lpdf.flushobject
49local pdfflushstreamobject = lpdf.flushstreamobject
50local pdfminorversion      = lpdf.minorversion
51
52local adddocumentextgstate = lpdf.adddocumentextgstate
53local addtocatalog         = lpdf.addtocatalog
54local addtoinfo            = lpdf.addtoinfo
55local addtopageattributes  = lpdf.addtopageattributes
56local addtonames           = lpdf.addtonames
57
58local texset               = tex.set
59
60local variables            = interfaces.variables
61
62local v_stop               = variables.stop
63local v_none               = variables.none
64local v_max                = variables.max
65local v_bookmark           = variables.bookmark
66local v_fit                = variables.fit
67local v_doublesided        = variables.doublesided
68local v_singlesided        = variables.singlesided
69local v_default            = variables.default
70local v_auto               = variables.auto
71local v_fixed              = variables.fixed
72local v_landscape          = variables.landscape
73local v_portrait           = variables.portrait
74local v_page               = variables.page
75local v_paper              = variables.paper
76local v_attachment         = variables.attachment
77local v_layer              = variables.layer
78local v_lefttoright        = variables.lefttoright
79local v_righttoleft        = variables.righttoleft
80local v_title              = variables.title
81local v_nomenubar          = variables.nomenubar
82
83local positive             = register(setstate("/GSpositive gs"))
84local negative             = register(setstate("/GSnegative gs"))
85local overprint            = register(setstate("/GSoverprint gs"))
86local knockout             = register(setstate("/GSknockout gs"))
87
88local omitextraboxes       = false
89
90directives.register("backend.omitextraboxes", function(v) omitextraboxes = v end)
91
92local function initializenegative()
93    local a = pdfarray { 0, 1 }
94    local g = pdfconstant("ExtGState")
95    local d = pdfdictionary {
96        FunctionType = 4,
97        Range        = a,
98        Domain       = a,
99    }
100    local negative = pdfdictionary { Type = g, TR = pdfreference(pdfflushstreamobject("{ 1 exch sub }",d)) } -- can be shared
101    local positive = pdfdictionary { Type = g, TR = pdfconstant("Identity") }
102    adddocumentextgstate("GSnegative", pdfreference(pdfflushobject(negative)))
103    adddocumentextgstate("GSpositive", pdfreference(pdfflushobject(positive)))
104    initializenegative = nil
105end
106
107local function initializeoverprint()
108    local g = pdfconstant("ExtGState")
109    local knockout  = pdfdictionary { Type = g, OP = false, OPM  = 0 }
110    local overprint = pdfdictionary { Type = g, OP = true,  OPM  = 1 }
111    adddocumentextgstate("GSknockout",  pdfreference(pdfflushobject(knockout)))
112    adddocumentextgstate("GSoverprint", pdfreference(pdfflushobject(overprint)))
113    initializeoverprint = nil
114end
115
116function nodeinjections.overprint()
117    if initializeoverprint then initializeoverprint() end
118    return copy_node(overprint)
119end
120function nodeinjections.knockout ()
121    if initializeoverprint then initializeoverprint() end
122    return copy_node(knockout)
123end
124
125function nodeinjections.positive()
126    if initializenegative then initializenegative() end
127    return copy_node(positive)
128end
129function nodeinjections.negative()
130    if initializenegative then initializenegative() end
131    return copy_node(negative)
132end
133
134-- function codeinjections.addtransparencygroup()
135--     -- png: /CS /DeviceRGB /I true
136--     local d = pdfdictionary {
137--         S = pdfconstant("Transparency"),
138--         I = true,
139--         K = true,
140--     }
141--     lpdf.registerpagefinalizer(function() addtopageattributes("Group",d) end) -- hm
142-- end
143
144-- actions (todo: store and update when changed)
145
146local openpage, closepage, opendocument, closedocument
147
148function codeinjections.registerdocumentopenaction(open)
149    opendocument = open
150end
151
152function codeinjections.registerdocumentcloseaction(close)
153    closedocument = close
154end
155
156function codeinjections.registerpageopenaction(open)
157    openpage = open
158end
159
160function codeinjections.registerpagecloseaction(close)
161    closepage = close
162end
163
164local function flushdocumentactions()
165    if opendocument then
166        addtocatalog("OpenAction",pdfaction(opendocument))
167    end
168    if closedocument then
169        addtocatalog("CloseAction",pdfaction(closedocument))
170    end
171end
172
173local function flushpageactions()
174    if openpage or closepage then
175        local d = pdfdictionary()
176        if openpage then
177            d.O = pdfaction(openpage)
178        end
179        if closepage then
180            d.C = pdfaction(closepage)
181        end
182        addtopageattributes("AA",d)
183    end
184end
185
186lpdf.registerpagefinalizer    (flushpageactions,    "page actions")
187lpdf.registerdocumentfinalizer(flushdocumentactions,"document actions")
188
189-- the code above will move to scrn-ini
190
191-- or when we want to be able to set things after page 1:
192--
193-- lpdf.registerdocumentfinalizer(setupidentity,1,"identity")
194
195local function flushjavascripts()
196    local t = interactions.javascripts.flushpreambles()
197    if #t > 0 then
198        local a = pdfarray()
199        local pdf_javascript = pdfconstant("JavaScript")
200        for i=1,#t do
201            local ti     = t[i]
202            local name   = ti[1]
203            local script = ti[2]
204            local j = pdfdictionary {
205                S  = pdf_javascript,
206                JS = pdfreference(pdfflushstreamobject(script)),
207            }
208            a[#a+1] = pdfstring(name)
209            a[#a+1] = pdfreference(pdfflushobject(j))
210        end
211        addtonames("JavaScript",pdfreference(pdfflushobject(pdfdictionary{ Names = a })))
212    end
213end
214
215lpdf.registerdocumentfinalizer(flushjavascripts,"javascripts")
216
217-- -- --
218
219local plusspecs = {
220    [v_max] = {
221        mode = "FullScreen",
222    },
223    [v_bookmark] = {
224        mode = "UseOutlines",
225    },
226    [v_attachment] = {
227        mode = "UseAttachments",
228    },
229    [v_layer] = {
230        mode = "UseOC",
231    },
232    [v_fit] = {
233        fit  = true,
234    },
235    [v_doublesided] = {
236        layout = "TwoColumnRight",
237    },
238    [v_fixed] = {
239        fixed  = true,
240    },
241    [v_landscape] = {
242        duplex = "DuplexFlipShortEdge",
243    },
244    [v_portrait] = {
245        duplex = "DuplexFlipLongEdge",
246    },
247    [v_page] = {
248        duplex = "Simplex" ,
249    },
250    [v_paper] = {
251        paper  = true,
252    },
253    [v_title] ={
254        title = true,
255    },
256    [v_lefttoright] ={
257        direction = "L2R",
258    },
259    [v_righttoleft] ={
260        direction = "R2L",
261    },
262    [v_nomenubar] ={
263        nomenubar = true,
264    },
265}
266
267local pagespecs = {
268    --
269    [v_max]         = plusspecs[v_max],
270    [v_bookmark]    = plusspecs[v_bookmark],
271    [v_attachment]  = plusspecs[v_attachment],
272    [v_layer]       = plusspecs[v_layer],
273    [v_lefttoright] = plusspecs[v_lefttoright],
274    [v_righttoleft] = plusspecs[v_righttoleft],
275    [v_title]       = plusspecs[v_title],
276    --
277    [v_none] = {
278    },
279    [v_fit] = {
280        mode = "UseNone",
281        fit  = true,
282    },
283    [v_doublesided] = {
284        mode   = "UseNone",
285        layout = "TwoColumnRight",
286        fit    = true,
287    },
288    [v_singlesided] = {
289        mode   = "UseNone"
290    },
291    [v_default] = {
292        mode   = "UseNone",
293        layout = "auto",
294    },
295    [v_auto] = {
296        mode   = "UseNone",
297        layout = "auto",
298    },
299    [v_fixed] = {
300        mode   = "UseNone",
301        layout = "auto",
302        fixed  = true, -- noscale
303    },
304    [v_landscape] = {
305        mode   = "UseNone",
306        layout = "auto",
307        fixed  = true,
308        duplex = "DuplexFlipShortEdge",
309    },
310    [v_portrait] = {
311        mode   = "UseNone",
312        layout = "auto",
313        fixed  = true,
314        duplex = "DuplexFlipLongEdge",
315    },
316    [v_page] = {
317        mode   = "UseNone",
318        layout = "auto",
319        fixed  = true,
320        duplex = "Simplex",
321    },
322    [v_paper] = {
323        mode   = "UseNone",
324        layout = "auto",
325        fixed  = true,
326        duplex = "Simplex",
327        paper  = true,
328    },
329    [v_nomenubar] = {
330        mode      = "UseNone",
331        layout    = "auto",
332        nomenubar = true,
333    },
334}
335
336local function documentspecification()
337    local canvas   = getcanvas()
338
339    -- move this to layo-ini ?
340
341    local pagespec = canvas.pagespec
342    if not pagespec or pagespec == "" then
343        pagespec = v_default
344    end
345    local settings = settings_to_array(pagespec)
346    -- so the first one detemines the defaults
347    local first    = settings[1]
348    local defaults = pagespecs[first]
349    local spec     = defaults or pagespecs[v_default]
350    -- successive keys can modify this
351    if spec.layout == "auto" then
352        if canvas.doublesided then
353            local s = pagespecs[v_doublesided] -- to be checked voor interfaces
354            for k, v in next, s do
355                spec[k] = v
356            end
357        else
358            spec.layout = false
359        end
360    end
361    -- we start at 2 when we have a valid first default set
362    for i=defaults and 2 or 1,#settings do
363        local s = plusspecs[settings[i]]
364        if s then
365            for k, v in next, s do
366                spec[k] = v
367            end
368        end
369    end
370    -- maybe interfaces.variables
371    local layout    = spec.layout
372    local mode      = spec.mode
373    local fit       = spec.fit
374    local fixed     = spec.fixed
375    local duplex    = spec.duplex
376    local paper     = spec.paper
377    local title     = spec.title
378    local direction = spec.direction
379    local nomenubar = spec.nomenubar
380    if layout then
381        addtocatalog("PageLayout",pdfconstant(layout))
382    end
383    if mode then
384        addtocatalog("PageMode",pdfconstant(mode))
385    end
386    local prints = nil
387    local marked = canvas.marked
388    local copies = canvas.copies
389    if marked then
390        local pages     = structures.pages
391        local marked    = pages.allmarked(marked)
392        local nofmarked = marked and #marked or 0
393        if nofmarked > 0 then
394            -- the spec is wrong in saying that numbering starts at 1 which of course makes
395            -- sense as most real documents start with page 0 .. sigh
396            for i=1,#marked do marked[i] = marked[i] - 1 end
397            prints = pdfarray(flattened(pages.toranges(marked)))
398        end
399    end
400    if fit or fixed or duplex or copies or paper or prints or title or direction or nomenubar then
401        addtocatalog("ViewerPreferences",pdfdictionary {
402            FitWindow         = fit       and true                   or nil,
403            PrintScaling      = fixed     and pdfconstant("None")    or nil,
404            Duplex            = duplex    and pdfconstant(duplex)    or nil,
405            NumCopies         = copies    and copies                 or nil,
406            PickTrayByPDFSize = paper     and true                   or nil,
407            PrintPageRange    = prints                               or nil,
408            DisplayDocTitle   = title     and true                   or nil,
409            Direction         = direction and pdfconstant(direction) or nil,
410            HideMenubar       = nomenubar and true                   or nil,
411        })
412    end
413    addtoinfo   ("Trapped", pdfconstant("False")) -- '/Trapped' in /Info, 'Trapped' in XMP
414    addtocatalog("Version", pdfconstant(format("1.%s",pdfminorversion())))
415    addtocatalog("Lang",    pdfstring(tokens.getters.macro("currentmainlanguage")))
416end
417
418local bpfactor = number.dimenfactors.bp
419
420local function pagespecification()
421    local canvas      = getcanvas()
422    local paperwidth  = canvas.paperwidth
423    local paperheight = canvas.paperheight
424    local leftoffset  = canvas.leftoffset
425    local topoffset   = canvas.topoffset
426    --
427    local llx = leftoffset
428    local urx = canvas.width - leftoffset
429    local lly = paperheight + topoffset - canvas.height
430    local ury = paperheight - topoffset
431    -- boxes can be cached
432    local function extrabox(WhatBox,offset,always)
433        if offset ~= 0 or always then
434            addtopageattributes(WhatBox, pdfarray {
435                (llx + offset) * bpfactor,
436                (lly + offset) * bpfactor,
437                (urx - offset) * bpfactor,
438                (ury - offset) * bpfactor,
439            })
440        end
441    end
442    if omitextraboxes then
443        -- only useful for testing / comparing
444    else
445        extrabox("CropBox",canvas.cropoffset,true) -- mandate for rendering
446        extrabox("TrimBox",canvas.trimoffset,true) -- mandate for pdf/x
447        extrabox("BleedBox",canvas.bleedoffset)    -- optional
448     -- extrabox("ArtBox",canvas.artoffset)        -- optional .. unclear what this is meant to do
449    end
450end
451
452lpdf.registerpagefinalizer(pagespecification,"page specification")
453lpdf.registerdocumentfinalizer(documentspecification,"document specification")
454
455-- Page Label support ...
456--
457-- In principle we can also support /P (prefix) as we can just use the verbose form
458-- and we can then forget about the /St (start) as we don't care about those few
459-- extra bytes due to lack of collapsing. Anyhow, for that we need a stupid prefix
460-- variant and that's not on the agenda now.
461
462local map = {
463    numbers       = "D",
464    Romannumerals = "R",
465    romannumerals = "r",
466    Characters    = "A",
467    characters    = "a",
468}
469
470-- local function featurecreep()
471--     local pages, lastconversion, list = structures.pages.tobesaved, nil, pdfarray()
472--     local getstructureset = structures.sets.get
473--     for i=1,#pages do
474--         local p = pages[i]
475--         if not p then
476--             return -- fatal error
477--         else
478--             local numberdata = p.numberdata
479--             if numberdata then
480--                 local conversionset = numberdata.conversionset
481--                 if conversionset then
482--                     local conversion = getstructureset("structure:conversions",p.block,conversionset,1,"numbers")
483--                     if conversion ~= lastconversion then
484--                         lastconversion = conversion
485--                         list[#list+1] = i - 1 -- pdf starts numbering at 0
486--                         list[#list+1] = pdfdictionary { S = pdfconstant(map[conversion] or map.numbers) }
487--                     end
488--                 end
489--             end
490--             if not lastconversion then
491--                 lastconversion = "numbers"
492--                 list[#list+1] = i - 1 -- pdf starts numbering at 0
493--                 list[#list+1] = pdfdictionary { S = pdfconstant(map.numbers) }
494--             end
495--         end
496--     end
497--     addtocatalog("PageLabels", pdfdictionary { Nums = list })
498-- end
499
500local function featurecreep()
501    local pages        = structures.pages.tobesaved
502    local list         = pdfarray()
503    local getset       = structures.sets.get
504    local stopped      = false
505    local oldlabel     = nil
506    local olconversion = nil
507    for i=1,#pages do
508        local p = pages[i]
509        if not p then
510            return -- fatal error
511        end
512        local label = p.viewerprefix or ""
513        if p.status == v_stop then
514            if not stopped then
515                list[#list+1] = i - 1 -- pdf starts numbering at 0
516                list[#list+1] = pdfdictionary {
517                    P = pdfunicode(label),
518                }
519                stopped = true
520            end
521            oldlabel      = nil
522            oldconversion = nil
523            stopped       = false
524        else
525            local numberdata = p.numberdata
526            local conversion = nil
527            local number     = p.number
528            if numberdata then
529                local conversionset = numberdata.conversionset
530                if conversionset then
531                    conversion = getset("structure:conversions",p.block,conversionset,1,"numbers")
532                end
533            end
534            -- If needed we can do some preroll on a prefix (label) but this is a rather useless
535            -- feature (creep) anyway so why bother.
536            conversion = conversion and map[conversion] or map.numbers
537            if number == 1 or oldlabel ~= label or oldconversion ~= conversion then
538                list[#list+1] = i - 1 -- pdf starts numbering at 0
539                list[#list+1] = pdfdictionary {
540                    S  = pdfconstant(conversion),
541                    St = number,
542                    P  = label ~= "" and pdfunicode(label) or nil,
543                }
544            end
545            oldlabel      = label
546            oldconversion = conversion
547            stopped       = false
548        end
549    end
550    addtocatalog("PageLabels", pdfdictionary { Nums = list })
551end
552
553lpdf.registerdocumentfinalizer(featurecreep,"featurecreep")
554