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