lpdf-mis.lua /size: 21 Kb    last modification: 2021-10-28 13:50
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.Support","contextgarden.net")
251        addtoinfo("TeX.Support","tug.org")
252        --
253        done = true
254    else
255        -- no need for a message
256    end
257end
258
259lpdf.registerpagefinalizer(setupidentity,"identity")
260
261-- or when we want to be able to set things after pag e1:
262--
263-- lpdf.registerdocumentfinalizer(setupidentity,1,"identity")
264
265local function flushjavascripts()
266    local t = interactions.javascripts.flushpreambles()
267    if #t > 0 then
268        local a = pdfarray()
269        local pdf_javascript = pdfconstant("JavaScript")
270        for i=1,#t do
271            local ti     = t[i]
272            local name   = ti[1]
273            local script = ti[2]
274            local j = pdfdictionary {
275                S  = pdf_javascript,
276                JS = pdfreference(pdfflushstreamobject(script)),
277            }
278            a[#a+1] = pdfstring(name)
279            a[#a+1] = pdfreference(pdfflushobject(j))
280        end
281        addtonames("JavaScript",pdfreference(pdfflushobject(pdfdictionary{ Names = a })))
282    end
283end
284
285lpdf.registerdocumentfinalizer(flushjavascripts,"javascripts")
286
287-- -- --
288
289local plusspecs = {
290    [v_max] = {
291        mode = "FullScreen",
292    },
293    [v_bookmark] = {
294        mode = "UseOutlines",
295    },
296    [v_attachment] = {
297        mode = "UseAttachments",
298    },
299    [v_layer] = {
300        mode = "UseOC",
301    },
302    [v_fit] = {
303        fit  = true,
304    },
305    [v_doublesided] = {
306        layout = "TwoColumnRight",
307    },
308    [v_fixed] = {
309        fixed  = true,
310    },
311    [v_landscape] = {
312        duplex = "DuplexFlipShortEdge",
313    },
314    [v_portrait] = {
315        duplex = "DuplexFlipLongEdge",
316    },
317    [v_page] = {
318        duplex = "Simplex" ,
319    },
320    [v_paper] = {
321        paper  = true,
322    },
323    [v_title] ={
324        title = true,
325    },
326    [v_lefttoright] ={
327        direction = "L2R",
328    },
329    [v_righttoleft] ={
330        direction = "R2L",
331    },
332    [v_nomenubar] ={
333        nomenubar = true,
334    },
335}
336
337local pagespecs = {
338    --
339    [v_max]         = plusspecs[v_max],
340    [v_bookmark]    = plusspecs[v_bookmark],
341    [v_attachment]  = plusspecs[v_attachment],
342    [v_layer]       = plusspecs[v_layer],
343    [v_lefttoright] = plusspecs[v_lefttoright],
344    [v_righttoleft] = plusspecs[v_righttoleft],
345    [v_title]       = plusspecs[v_title],
346    --
347    [v_none] = {
348    },
349    [v_fit] = {
350        mode = "UseNone",
351        fit  = true,
352    },
353    [v_doublesided] = {
354        mode   = "UseNone",
355        layout = "TwoColumnRight",
356        fit = true,
357    },
358    [v_singlesided] = {
359        mode   = "UseNone"
360    },
361    [v_default] = {
362        mode   = "UseNone",
363        layout = "auto",
364    },
365    [v_auto] = {
366        mode   = "UseNone",
367        layout = "auto",
368    },
369    [v_fixed] = {
370        mode   = "UseNone",
371        layout = "auto",
372        fixed  = true, -- noscale
373    },
374    [v_landscape] = {
375        mode   = "UseNone",
376        layout = "auto",
377        fixed  = true,
378        duplex = "DuplexFlipShortEdge",
379    },
380    [v_portrait] = {
381        mode   = "UseNone",
382        layout = "auto",
383        fixed  = true,
384        duplex = "DuplexFlipLongEdge",
385    },
386    [v_page] = {
387        mode   = "UseNone",
388        layout = "auto",
389        fixed  = true,
390        duplex = "Simplex",
391    },
392    [v_paper] = {
393        mode   = "UseNone",
394        layout = "auto",
395        fixed  = true,
396        duplex = "Simplex",
397        paper  = true,
398    },
399    [v_nomenubar] = {
400        mode      = "UseNone",
401        layout    = "auto",
402        nomenubar = true,
403    },
404}
405
406local pagespec, topoffset, leftoffset, height, width, doublesided = "default", 0, 0, 0, 0, false
407local cropoffset, bleedoffset, trimoffset, artoffset = 0, 0, 0, 0
408local marked = false
409local copies = false
410
411local getpagedimensions  getpagedimensions = function()
412    getpagedimensions = backends.codeinjections.getpagedimensions
413    return getpagedimensions()
414end
415
416function codeinjections.setupcanvas(specification)
417    local paperheight = specification.paperheight
418    local paperwidth  = specification.paperwidth
419    local paperdouble = specification.doublesided
420    --
421    paperwidth, paperheight = codeinjections.setpagedimensions(paperwidth,paperheight)
422    --
423    pagespec    = specification.mode       or pagespec
424    topoffset   = specification.topoffset  or 0
425    leftoffset  = specification.leftoffset or 0
426    height      = specification.height     or paperheight
427    width       = specification.width      or paperwidth
428    marked      = specification.print
429    --
430    copies      = specification.copies
431    if copies and copies < 2 then
432        copies = false
433    end
434    --
435    cropoffset  = specification.cropoffset  or 0
436    trimoffset  = cropoffset  - (specification.trimoffset  or 0)
437    bleedoffset = trimoffset  - (specification.bleedoffset or 0)
438    artoffset   = bleedoffset - (specification.artoffset   or 0)
439    --
440    if paperdouble ~= nil then
441        doublesided = paperdouble
442    end
443end
444
445local function documentspecification()
446    if not pagespec or pagespec == "" then
447        pagespec = v_default
448    end
449    local settings = settings_to_array(pagespec)
450    -- so the first one detemines the defaults
451    local first    = settings[1]
452    local defaults = pagespecs[first]
453    local spec     = defaults or pagespecs[v_default]
454    -- successive keys can modify this
455    if spec.layout == "auto" then
456        if doublesided then
457            local s = pagespecs[v_doublesided] -- to be checked voor interfaces
458            for k, v in next, s do
459                spec[k] = v
460            end
461        else
462            spec.layout = false
463        end
464    end
465    -- we start at 2 when we have a valid first default set
466    for i=defaults and 2 or 1,#settings do
467        local s = plusspecs[settings[i]]
468        if s then
469            for k, v in next, s do
470                spec[k] = v
471            end
472        end
473    end
474    -- maybe interfaces.variables
475    local layout    = spec.layout
476    local mode      = spec.mode
477    local fit       = spec.fit
478    local fixed     = spec.fixed
479    local duplex    = spec.duplex
480    local paper     = spec.paper
481    local title     = spec.title
482    local direction = spec.direction
483    local nomenubar = spec.nomenubar
484    if layout then
485        addtocatalog("PageLayout",pdfconstant(layout))
486    end
487    if mode then
488        addtocatalog("PageMode",pdfconstant(mode))
489    end
490    local prints = nil
491    if marked then
492        local pages     = structures.pages
493        local marked    = pages.allmarked(marked)
494        local nofmarked = marked and #marked or 0
495        if nofmarked > 0 then
496            -- the spec is wrong in saying that numbering starts at 1 which of course makes
497            -- sense as most real documents start with page 0 .. sigh
498            for i=1,#marked do marked[i] = marked[i] - 1 end
499            prints = pdfarray(flattened(pages.toranges(marked)))
500        end
501    end
502    if fit or fixed or duplex or copies or paper or prints or title or direction or nomenubar then
503        addtocatalog("ViewerPreferences",pdfdictionary {
504            FitWindow         = fit       and true                   or nil,
505            PrintScaling      = fixed     and pdfconstant("None")    or nil,
506            Duplex            = duplex    and pdfconstant(duplex)    or nil,
507            NumCopies         = copies    and copies                 or nil,
508            PickTrayByPDFSize = paper     and true                   or nil,
509            PrintPageRange    = prints                               or nil,
510            DisplayDocTitle   = title     and true                   or nil,
511            Direction         = direction and pdfconstant(direction) or nil,
512            HideMenubar       = nomenubar and true                   or nil,
513        })
514    end
515    addtoinfo   ("Trapped", pdfconstant("False")) -- '/Trapped' in /Info, 'Trapped' in XMP
516    addtocatalog("Version", pdfconstant(format("1.%s",pdfminorversion())))
517    addtocatalog("Lang",    pdfstring(tokens.getters.macro("currentmainlanguage")))
518end
519
520-- temp hack: the mediabox is not under our control and has a precision of 5 digits
521
522local factor  = number.dimenfactors.bp
523local f_value = formatters["%.6N"]
524
525local function boxvalue(n) -- we could share them
526    return pdfverbose(f_value(factor * n))
527end
528
529local function pagespecification()
530    local paperwidth, paperheight = codeinjections.getpagedimensions()
531    local llx = leftoffset
532    local lly = paperheight + topoffset - height
533    local urx = width - leftoffset
534    local ury = paperheight - topoffset
535    -- boxes can be cached
536    local function extrabox(WhatBox,offset,always)
537        if offset ~= 0 or always then
538            addtopageattributes(WhatBox, pdfarray {
539                boxvalue(llx + offset),
540                boxvalue(lly + offset),
541                boxvalue(urx - offset),
542                boxvalue(ury - offset),
543            })
544        end
545    end
546    if omitextraboxes then
547        -- only useful for testing / comparing
548    else
549        extrabox("CropBox",cropoffset,true) -- mandate for rendering
550        extrabox("TrimBox",trimoffset,true) -- mandate for pdf/x
551        extrabox("BleedBox",bleedoffset)    -- optional
552     -- extrabox("ArtBox",artoffset)        -- optional .. unclear what this is meant to do
553    end
554end
555
556lpdf.registerpagefinalizer(pagespecification,"page specification")
557lpdf.registerdocumentfinalizer(documentspecification,"document specification")
558
559-- Page Label support ...
560--
561-- In principle we can also support /P (prefix) as we can just use the verbose form
562-- and we can then forget about the /St (start) as we don't care about those few
563-- extra bytes due to lack of collapsing. Anyhow, for that we need a stupid prefix
564-- variant and that's not on the agenda now.
565
566local map = {
567    numbers       = "D",
568    Romannumerals = "R",
569    romannumerals = "r",
570    Characters    = "A",
571    characters    = "a",
572}
573
574-- local function featurecreep()
575--     local pages, lastconversion, list = structures.pages.tobesaved, nil, pdfarray()
576--     local getstructureset = structures.sets.get
577--     for i=1,#pages do
578--         local p = pages[i]
579--         if not p then
580--             return -- fatal error
581--         else
582--             local numberdata = p.numberdata
583--             if numberdata then
584--                 local conversionset = numberdata.conversionset
585--                 if conversionset then
586--                     local conversion = getstructureset("structure:conversions",p.block,conversionset,1,"numbers")
587--                     if conversion ~= lastconversion then
588--                         lastconversion = conversion
589--                         list[#list+1] = i - 1 -- pdf starts numbering at 0
590--                         list[#list+1] = pdfdictionary { S = pdfconstant(map[conversion] or map.numbers) }
591--                     end
592--                 end
593--             end
594--             if not lastconversion then
595--                 lastconversion = "numbers"
596--                 list[#list+1] = i - 1 -- pdf starts numbering at 0
597--                 list[#list+1] = pdfdictionary { S = pdfconstant(map.numbers) }
598--             end
599--         end
600--     end
601--     addtocatalog("PageLabels", pdfdictionary { Nums = list })
602-- end
603
604local function featurecreep()
605    local pages        = structures.pages.tobesaved
606    local list         = pdfarray()
607    local getset       = structures.sets.get
608    local stopped      = false
609    local oldlabel     = nil
610    local olconversion = nil
611    for i=1,#pages do
612        local p = pages[i]
613        if not p then
614            return -- fatal error
615        end
616        local label = p.viewerprefix or ""
617        if p.status == v_stop then
618            if not stopped then
619                list[#list+1] = i - 1 -- pdf starts numbering at 0
620                list[#list+1] = pdfdictionary {
621                    P = pdfunicode(label),
622                }
623                stopped = true
624            end
625            oldlabel      = nil
626            oldconversion = nil
627            stopped       = false
628        else
629            local numberdata = p.numberdata
630            local conversion = nil
631            local number     = p.number
632            if numberdata then
633                local conversionset = numberdata.conversionset
634                if conversionset then
635                    conversion = getset("structure:conversions",p.block,conversionset,1,"numbers")
636                end
637            end
638            conversion = conversion and map[conversion] or map.numbers
639            if number == 1 or oldlabel ~= label or oldconversion ~= conversion then
640                list[#list+1] = i - 1 -- pdf starts numbering at 0
641                list[#list+1] = pdfdictionary {
642                    S  = pdfconstant(conversion),
643                    St = number,
644                    P  = label ~= "" and pdfunicode(label) or nil,
645                }
646            end
647            oldlabel      = label
648            oldconversion = conversion
649            stopped       = false
650        end
651    end
652    addtocatalog("PageLabels", pdfdictionary { Nums = list })
653end
654
655lpdf.registerdocumentfinalizer(featurecreep,"featurecreep")
656