lpdf-epa.lmt /size: 47 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-epa'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-epa.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-- Links can also have quadpoint
10
11-- embedded files ... not bound to a page
12
13local type, tonumber, next = type, tonumber, next
14local format, gsub, lower, find, match = string.format, string.gsub, string.lower, string.find, string.match
15local formatters = string.formatters
16local concat, merged = table.concat, table.merged
17local abs = math.abs
18local expandname = file.expandname
19local allocate = utilities.storage.allocate
20local isfile = lfs.isfile
21
22local trace_links       = false  trackers.register("figures.links",    function(v) trace_links    = v end)
23local trace_comments    = false  trackers.register("figures.comments", function(v) trace_comments = v end)
24local trace_fields      = false  trackers.register("figures.fields",   function(v) trace_fields   = v end)
25local trace_outlines    = false  trackers.register("figures.outlines", function(v) trace_outlines = v end)
26
27local report_link       = logs.reporter("backend","link")
28local report_comment    = logs.reporter("backend","comment")
29local report_field      = logs.reporter("backend","field")
30local report_outline    = logs.reporter("backend","outline")
31
32local context           = context
33
34local pdfbackend        = backends.registered.pdf
35local nodeinjections    = pdfbackend.nodeinjections
36local codeinjections    = pdfbackend.codeinjections
37----- registrations     = pdfbackend.registrations
38
39local setmacro          = tokens.setters.macro
40
41local lpdf              = lpdf
42local pdfarray          = lpdf.array
43local pdfdictionary     = lpdf.dictionary
44local pdfconstant       = lpdf.constant
45local pdfreference      = lpdf.reference
46local pdfreserveobject  = lpdf.reserveobject
47local pdfgetpos         = lpdf.getpos
48local pdfcopyboolean    = lpdf.copyboolean
49local pdfcopyunicode    = lpdf.copyunicode
50local pdfcopyarray      = lpdf.copyarray
51local pdfcopydictionary = lpdf.copydictionary
52local pdfcopynumber     = lpdf.copynumber
53local pdfcopyinteger    = lpdf.copyinteger
54local pdfcopystring     = lpdf.copystring
55local pdfcopyconstant   = lpdf.copyconstant
56
57local createimage       = images.create
58local embedimage        = images.embed
59
60local hpack_node        = nodes.hpack
61
62local loadpdffile       = lpdf.epdf.load
63
64local nameonly          = file.nameonly
65
66local variables         = interfaces.variables
67----- urlescaper        = lpegpatterns.urlescaper
68----- utftohigh         = lpegpatterns.utftohigh
69local escapetex         = characters.filters.utf.private.escape
70
71local bookmarks         = structures.bookmarks
72
73local maxdimen          <const> = tex.magicconstants.maxdimen
74local bpfactor          <const> = number.dimenfactors.bp
75
76local layerspec = {
77    "epdfcontent"
78}
79
80local getpos = function() getpos = codeinjections.getpos return getpos() end
81
82local collected = allocate()
83local tobesaved = allocate()
84
85local jobembedded = {
86    collected = collected,
87    tobesaved = tobesaved,
88}
89
90job.embedded = jobembedded
91
92local function initializer()
93    tobesaved = jobembedded.tobesaved
94    collected = jobembedded.collected
95end
96
97job.register('job.embedded.collected',tobesaved,initializer)
98
99local function validdocument(specification)
100    if figures and not specification then
101        specification = figures and figures.current()
102        specification = specification and specification.status
103    end
104    if specification then
105        local fullname = specification.fullname
106        local expanded = lower(expandname(fullname))
107        -- we could add a check for duplicate page insertion
108        tobesaved[expanded] = true
109        --- but that is messy anyway so we forget about it
110        return specification, fullname, loadpdffile(fullname) -- costs time
111    end
112end
113
114local function getmediasize(specification,pagedata)
115    local xscale   = specification.xscale or 1
116    local yscale   = specification.yscale or 1
117    ----- size     = specification.size   or "crop" -- todo
118    local mediabox = pagedata.MediaBox
119    local llx      = mediabox[1]
120    local lly      = mediabox[2]
121    local urx      = mediabox[3]
122    local ury      = mediabox[4]
123    local width    = xscale * (urx - llx) -- \\overlaywidth, \\overlayheight
124    local height   = yscale * (ury - lly) -- \\overlaywidth, \\overlayheight
125    return llx, lly, urx, ury, width, height, xscale, yscale
126end
127
128local function getdimensions(annotation,llx,lly,xscale,yscale,width,height,report,minheight)
129    local rectangle = annotation.Rect
130    local a_llx     = rectangle[1]
131    local a_lly     = rectangle[2]
132    local a_urx     = rectangle[3]
133    local a_ury     = rectangle[4]
134    local x         = xscale * (a_llx -   llx)
135    local y         = yscale * (a_lly -   lly)
136    local w         = xscale * (a_urx - a_llx)
137    local h         = yscale * (a_ury - a_lly)
138    if w > width or h > height or w < 0 or h < 0 or abs(x) > (maxdimen/2) or abs(y) > (maxdimen/2) then
139        report("broken rectangle [%.6F %.6F %.6F %.6F] (max: %.6F)",a_llx,a_lly,a_urx,a_ury,maxdimen/2)
140        return
141    end
142    if minheight and minheight ~= 0 then
143        local dh = minheight - h
144        if dh > 0 then
145            y = y - dh/2
146            h = h + dh
147        end
148    end
149    return x, y, w, h, a_llx, a_lly, a_urx, a_ury
150end
151
152local layerused = false
153
154-- local function initializelayer(height,width)
155--     if not layerused then
156--         context.definelayer(layerspec, { height = height .. "bp", width = width .. "bp" })
157--         layerused = true
158--     end
159-- end
160
161local function initializelayer(height,width)
162--     if not layerused then
163        context.setuplayer(layerspec, { height = height .. "bp", width = width .. "bp" })
164        layerused = true
165--     end
166end
167
168function codeinjections.flushmergelayer()
169    if layerused then
170        context.flushlayer(layerspec)
171        layerused = false
172    end
173end
174
175local f_namespace = formatters["lpdf-epa-%s-"]
176
177local function makenamespace(filename)
178    filename = gsub(lower(nameonly(filename)),"[^%a%d]+","-")
179    return f_namespace(filename)
180end
181
182local function add_link(x,y,w,h,destination,what)
183    x = x .. "bp"
184    y = y .. "bp"
185    w = w .. "bp"
186    h = h .. "bp"
187    if trace_links then
188        report_link("destination %a, type %a, dx %s, dy %s, wd %s, ht %s",destination,what,x,y,w,h)
189    end
190    local locationspec = { -- predefining saves time
191        x      = x,
192        y      = y,
193        preset = "leftbottom",
194    }
195    local buttonspec = {
196        width  = w,
197        height = h,
198        offset = variables.overlay,
199        frame  = trace_links and variables.on or variables.off,
200    }
201    context.setlayer (
202        layerspec,
203        locationspec,
204        function() context.button ( buttonspec, "", { destination } ) end
205     -- context.nested.button(buttonspec, "", { destination }) -- time this
206    )
207end
208
209local function link_goto(x,y,w,h,document,annotation,pagedata,namespace,pageoffset)
210    local a = annotation.A
211    if a then
212        local destination = a.D -- [ 18 0 R /Fit ]
213        local what = "page"
214        if type(destination) == "string" then
215            local destinations = document.destinations
216            local wanted = destinations[destination]
217            destination = wanted and wanted.D -- is this ok? isn't it destination already a string?
218            if destination then what = "named" end
219        end
220        local pagedata = destination and destination[1]
221        if pagedata then
222            local destinationpage = pagedata.number
223            if destinationpage then
224                if pageoffset then
225                    destinationpage = "page(" .. (destinationpage+pageoffset) .. ")"
226                else
227                    destinationpage = namespace .. destinationpage
228                end
229                add_link(x,y,w,h,destinationpage,what)
230            end
231        end
232    end
233end
234
235local function link_uri(x,y,w,h,document,annotation)
236    local url = annotation.A.URI
237    if url then
238     -- url = lpegmatch(urlescaper,url)
239     -- url = lpegmatch(utftohigh,url)
240        url = escapetex(url)
241        add_link(x,y,w,h,formatters["url(%s)"](url),"url")
242    end
243end
244
245-- The rules in PDF on what a 'file specification' is, is in fact quite elaborate
246-- (see section 3.10 in the 1.7 reference) so we need to test for string as well
247-- as a table. TH/20140916
248
249-- When embedded is set then files need to have page references which is seldom the
250-- case but you can generate them with context:
251--
252-- \setupinteraction[state=start,page={page,page}]
253--
254-- see tests/mkiv/interaction/cross[1|2|3].tex for an example
255
256local embedded = false directives.register("figures.embedded", function(v) embedded = v end)
257local reported = { }
258
259local function link_file(x,y,w,h,document,annotation)
260    local a = annotation.A
261    if a then
262        local filename = a.F
263        if type(filename) == "table" then
264            filename = filename.F
265        end
266        if filename then
267            filename = escapetex(filename)
268            local destination = a.D
269            if not destination then
270                add_link(x,y,w,h,formatters["file(%s)"](filename),"file")
271            elseif type(destination) == "string" then
272                add_link(x,y,w,h,formatters["%s::%s"](filename,destination),"file (named)")
273            else
274                -- hm, zero offset so maybe: destination + 1
275                destination = tonumber(destination[1]) -- array
276                if destination then
277                    destination = destination + 1
278                    local loaded = collected[lower(expandname(filename))]
279                    if embedded and loaded then
280                        add_link(x,y,w,h,makenamespace(filename) .. destination,what)
281                    else
282                        if loaded and not reported[filename] then
283                            report_link("reference to an also loaded file %a, consider using directive: figures.embedded",filename)
284                            reported[filename] = true
285                        end
286                        add_link(x,y,w,h,formatters["%s::page(%s)"](filename,destination),"file (page)")
287                    end
288                else
289                    add_link(x,y,w,h,formatters["file(%s)"](filename),"file")
290                end
291            end
292        end
293    end
294end
295
296-- maybe handler per subtype and then one loop but then what about order ...
297--
298-- specification is normally nil
299
300local function getpageoffset(options)
301    if options then
302        for k in next, options do
303            local p = tonumber(match(k,"pageoffset:([%+%-]*%d+)"))
304            if p then
305                return p
306            end
307        end
308    end
309end
310
311local function getminheight(options)
312    if options then
313        for k in next, options do
314            local p = tonumber(match(k,"minheight:([%+%-]*%d+)"))
315            if p then
316                return p
317            end
318        end
319    end
320end
321
322function codeinjections.mergereferences(specification,options)
323    local specification, fullname, document = validdocument(specification)
324    if not document then
325        return ""
326    end
327    local pagenumber  = specification.page or 1
328    local pagedata    = document.pages[pagenumber]
329    local annotations = pagedata and pagedata.Annots
330    local namespace   = makenamespace(fullname)
331    local reference   = namespace .. pagenumber
332    local pageoffset  = getpageoffset(options)
333    local minheight   = getminheight(options)
334    if annotations and #annotations > 0 then
335        local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale)
336        initializelayer(height,width)
337        for i=1,#annotations do
338            local annotation = annotations[i]
339            if annotation then
340                if annotation.Subtype == "Link" then
341                    local a = annotation.A
342                    if not a then
343                        local d = annotation.Dest
344                        if d then
345                            a = { S = "GoTo", D = d } -- no need for a dict
346                            annotation.A = a
347                        end
348                    end
349                    if not a then
350                        report_link("missing link annotation")
351                    else
352                        local x, y, w, h = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_link,minheight)
353                        if x then
354                            local linktype = a.S
355                            if linktype == "GoTo" then
356                                link_goto(x,y,w,h,document,annotation,pagedata,namespace,pageoffset)
357                            elseif linktype == "GoToR" then
358                                link_file(x,y,w,h,document,annotation)
359                            elseif linktype == "URI" then
360                                link_uri(x,y,w,h,document,annotation)
361                            elseif trace_links then
362                                report_link("unsupported link annotation %a",linktype)
363                            end
364                        end
365                    end
366                end
367            elseif trace_links then
368                report_link("broken annotation, index %a",i)
369            end
370        end
371    end
372    -- moved outside previous test
373    setmacro("mergedfigurereference",reference,"global")
374    if trace_links then
375        report_link("setting figure reference to %a",reference)
376    end
377    specification.reference = reference
378    return namespace
379end
380
381function codeinjections.mergeviewerlayers(specification)
382    -- todo: parse included page for layers .. or only for whole document inclusion
383    if true then
384        return
385    end
386    local specification, fullname, document = validdocument(specification)
387    if not document then
388        return ""
389    end
390    local namespace = makenamespace(fullname)
391    local layers    = document.layers
392    if layers then
393        for i=1,#layers do
394            local layer = layers[i]
395            if layer then
396                local tag   = namespace .. gsub(layer," ",":")
397                local title = tag
398                if trace_links then
399                    report_link("using layer %a",tag)
400                end
401                attributes.viewerlayers.define { -- also does some cleaning
402                    tag       = tag, -- todo: #3A or so
403                    title     = title,
404                    visible   = variables.start,
405                    editable  = variables.yes,
406                    printable = variables.yes,
407                }
408                codeinjections.useviewerlayer(tag)
409            elseif trace_links then
410                report_link("broken layer, index %a",i)
411            end
412        end
413    end
414end
415
416-- It took a bit of puzzling and playing around to come to the following
417-- implementation. In the end it looks simple but as usual it takes a while
418-- to see what the specification (and implementation) boils down to. Lots of
419-- shared properties and such. The scaling took some trial and error as
420-- viewers differ. I had to extend some low level helpers to make it more
421-- comfortable. Hm, the specification is somewhat incomplete as some fields
422-- are permitted even if not mentioned so in the end we can share more code.
423--
424-- If all works ok, we can get rid of some copies which saves time and space.
425
426local commentlike = {
427    Text      = "text",
428    FreeText  = "freetext",
429    Line      = "line",
430    Square    = "shape",
431    Circle    = "shape",
432    Polygon   = "poly",
433    PolyLine  = "poly",
434    Highlight = "markup",
435    Underline = "markup",
436    Squiggly  = "markup",
437    StrikeOut = "markup",
438    Caret     = "text",
439    Stamp     = "stamp",
440    Ink       = "ink",
441    Popup     = "popup",
442}
443
444local function copyBS(v) -- dict can be shared
445    if v then
446     -- return pdfdictionary {
447     --     Type = copypdfconstant(V.Type),
448     --     W    = copypdfnumber  (V.W),
449     --     S    = copypdfstring  (V.S),
450     --     D    = copypdfarray   (V.D),
451     -- }
452        return copypdfdictionary(v)
453    end
454end
455
456local function copyBE(v) -- dict can be shared
457    if v then
458     -- return pdfdictionary {
459     --     S = copypdfstring(V.S),
460     --     I = copypdfnumber(V.I),
461     -- }
462        return copypdfdictionary(v)
463    end
464end
465
466local function copyBorder(v) -- dict can be shared
467    if v then
468        -- todo
469        return copypdfarray(v)
470    end
471end
472
473local function copyPopup(v,references)
474    if v then
475        local p = references[v]
476        if p then
477            return pdfreference(p)
478        end
479    end
480end
481
482local function copyParent(v,references)
483    if v then
484        local p = references[v]
485        if p then
486            return pdfreference(p)
487        end
488    end
489end
490
491local function copyIRT(v,references)
492    if v then
493        local p = references[v]
494        if p then
495            return pdfreference(p)
496        end
497    end
498end
499
500local function copyC(v)
501    if v then
502        -- todo: check color space
503        return pdfcopyarray(v)
504    end
505end
506
507local function finalizer(d,xscale,yscale,a_llx,a_ury)
508    local q = d.QuadPoints or d.Vertices or d.CL
509    if q then
510        return function()
511            local h, v = pdfgetpos() -- already scaled
512            for i=1,#q,2 do
513                q[i]   = xscale * q[i]   + (h*bpfactor - xscale * a_llx)
514                q[i+1] = yscale * q[i+1] + (v*bpfactor - yscale * a_ury)
515            end
516            return d()
517        end
518    end
519    q = d.InkList or d.Path
520    if q then
521        return function()
522            local h, v = pdfgetpos() -- already scaled
523            for i=1,#q do
524                local q = q[i]
525                for i=1,#q,2 do
526                    q[i]   = xscale * q[i]   + (h*bpfactor - xscale * a_llx)
527                    q[i+1] = yscale * q[i+1] + (v*bpfactor - yscale * a_ury)
528                end
529            end
530            return d()
531        end
532    end
533    return d()
534end
535
536local validstamps = {
537    Approved            = true,
538    Experimental        = true,
539    NotApproved         = true,
540    AsIs                = true,
541    Expired             = true,
542    NotForPublicRelease = true,
543    Confidential        = true,
544    Final               = true,
545    Sold                = true,
546    Departmental        = true,
547    ForComment          = true,
548    TopSecret           = true,
549    Draft               = true,
550    ForPublicRelease    = true,
551}
552
553-- todo: we can use runlocal instead of steps
554
555local function validStamp(v)
556    local name = "Stamped" -- fallback
557    if v then
558        local ok = validstamps[v]
559        if ok then
560            name = ok
561        else
562            for k in next, validstamps do
563                if find(v,k.."$") then
564                    name = k
565                    validstamps[v] = k
566                    break
567                end
568            end
569        end
570    end
571    -- we temporary return to \TEX:
572    context.predefinesymbol { name }
573    context.step()
574    -- beware, an error is not reported
575    return pdfconstant(name), codeinjections.analyzenormalsymbol(name)
576end
577
578local annotationflags = lpdf.flags.annotations
579
580local function copyF(v,lock) -- todo: bxor 24
581    if lock then
582        v = (v or 0) | (annotationflags.ReadOnly + annotationflags.Locked + annotationflags.LockedContents)
583    end
584    if v then
585        return pdfcopyinteger(v)
586    end
587end
588
589-- Speed is not really an issue so we don't optimize this code too much. In the end (after
590-- testing) we ended up with less code that we started with.
591
592function codeinjections.mergecomments(specification)
593    local specification, fullname, document = validdocument(specification)
594    if not document then
595        return ""
596    end
597    local pagenumber  = specification.page or 1
598    local pagedata    = document.pages[pagenumber]
599    local annotations = pagedata and pagedata.Annots
600    if annotations and #annotations > 0 then
601        local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale)
602        initializelayer(height,width)
603        --
604        local lockflags  = specification.lock -- todo: proper parameter
605        local references = { }
606        local usedpopups = { }
607        for i=1,#annotations do
608            local annotation = annotations[i]
609            if annotation then
610                local subtype = annotation.Subtype
611                if commentlike[subtype] then
612                    references[annotation] = pdfreserveobject()
613                    local p = annotation.Popup
614                    if p then
615                        usedpopups[p] = true
616                    end
617                end
618            end
619        end
620        --
621        for i=1,#annotations do
622            -- we keep the order
623            local annotation = annotations[i]
624            if annotation then
625                local reference = references[annotation]
626                if reference then
627                    local subtype = annotation.Subtype
628                    local kind    = commentlike[subtype]
629                    if kind ~= "popup" or usedpopups[annotation] then
630                        local x, y, w, h, a_llx, a_lly, a_urx, a_ury = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_comment)
631                        if x then
632                            local voffset    = h
633                            local dictionary = pdfdictionary {
634                                Subtype      = pdfconstant   (subtype),
635                                -- common (skipped: P AP AS OC AF BM StructParent)
636                                Contents     = pdfcopyunicode(annotation.Contents),
637                                NM           = pdfcopystring (annotation.NM),
638                                M            = pdfcopystring (annotation.M),
639                                F            = copyF         (annotation.F,lockflags),
640                                C            = copyC         (annotation.C),
641                                ca           = pdfcopynumber (annotation.ca),
642                                CA           = pdfcopynumber (annotation.CA),
643                                Lang         = pdfcopystring (annotation.Lang),
644                                -- also common
645                                CreationDate = pdfcopystring (annotation.CreationDate),
646                                T            = pdfcopyunicode(annotation.T),
647                                Subj         = pdfcopyunicode(annotation.Subj),
648                                -- border
649                                Border       = pdfcopyarray  (annotation.Border),
650                                BS           = copyBS        (annotation.BS),
651                                BE           = copyBE        (annotation.BE),
652                                -- sort of common
653                                Popup        = copyPopup     (annotation.Popup,references),
654                                RC           = pdfcopyunicode(annotation.RC) -- string or stream
655                            }
656                            if kind == "markup" then
657                                dictionary.IRT          = copyIRT          (annotation.IRT,references)
658                                dictionary.RT           = pdfconstant      (annotation.RT)
659                                dictionary.IT           = pdfcopyconstant  (annotation.IT)
660                                dictionary.QuadPoints   = pdfcopyarray     (annotation.QuadPoints)
661                             -- dictionary.RD           = pdfcopyarray     (annotation.RD)
662                            elseif kind == "text" then
663                                -- somehow F fails to view : /F 24 : bit4=nozoom bit5=norotate
664                                dictionary.F            = nil
665                                dictionary.Open         = pdfcopyboolean   (annotation.Open)
666                                dictionary.Name         = pdfcopyunicode   (annotation.Name)
667                                dictionary.State        = pdfcopystring    (annotation.State)
668                                dictionary.StateModel   = pdfcopystring    (annotation.StateModel)
669                                dictionary.IT           = pdfcopyconstant  (annotation.IT)
670                                dictionary.QuadPoints   = pdfcopyarray     (annotation.QuadPoints)
671                                dictionary.RD           = pdfcopyarray     (annotation.RD) -- caret
672                                dictionary.Sy           = pdfcopyconstant  (annotation.Sy) -- caret
673                                voffset = 0
674                            elseif kind == "freetext" then
675                                dictionary.DA           = pdfcopystring    (annotation.DA)
676                                dictionary.Q            = pdfcopyinteger   (annotation.Q)
677                                dictionary.DS           = pdfcopystring    (annotation.DS)
678                                dictionary.CL           = pdfcopyarray     (annotation.CL)
679                                dictionary.IT           = pdfcopyconstant  (annotation.IT)
680                                dictionary.LE           = pdfcopyconstant  (annotation.LE)
681                             -- dictionary.RC           = pdfcopystring    (annotation.RC)
682                            elseif kind == "line" then
683                                dictionary.LE           = pdfcopyarray     (annotation.LE)
684                                dictionary.IC           = pdfcopyarray     (annotation.IC)
685                                dictionary.LL           = pdfcopynumber    (annotation.LL)
686                                dictionary.LLE          = pdfcopynumber    (annotation.LLE)
687                                dictionary.Cap          = pdfcopyboolean   (annotation.Cap)
688                                dictionary.IT           = pdfcopyconstant  (annotation.IT)
689                                dictionary.LLO          = pdfcopynumber    (annotation.LLO)
690                                dictionary.CP           = pdfcopyconstant  (annotation.CP)
691                                dictionary.Measure      = pdfcopydictionary(annotation.Measure) -- names
692                                dictionary.CO           = pdfcopyarray     (annotation.CO)
693                                voffset = 0
694                            elseif kind == "shape" then
695                                dictionary.IC           = pdfcopyarray     (annotation.IC)
696                             -- dictionary.RD           = pdfcopyarray     (annotation.RD)
697                                voffset = 0
698                            elseif kind == "stamp" then
699                                local name, appearance  = validStamp(annotation.Name)
700                                dictionary.Name         = name
701                                dictionary.AP           = appearance
702                                voffset = 0
703                            elseif kind == "ink" then
704                                dictionary.InkList      = pdfcopyarray     (annotation.InkList)
705                            elseif kind == "poly" then
706                                dictionary.Vertices     = pdfcopyarray     (annotation.Vertices)
707                             -- dictionary.LE           = pdfcopyarray     (annotation.LE) -- todo: names in array
708                                dictionary.IC           = pdfcopyarray     (annotation.IC)
709                                dictionary.IT           = pdfcopyconstant  (annotation.IT)
710                                dictionary.Measure      = pdfcopydictionary(annotation.Measure)
711                                dictionary.Path         = pdfcopyarray     (annotation.Path)
712                             -- dictionary.RD           = pdfcopyarray     (annotation.RD)
713                            elseif kind == "popup" then
714                                dictionary.Open         = pdfcopyboolean   (annotation.Open)
715                                dictionary.Parent       = copyParent       (annotation.Parent,references)
716                                voffset = 0
717                            end
718                            if dictionary then
719                                local locationspec = {
720                                    x       = x .. "bp",
721                                    y       = y .. "bp",
722                                    voffset = voffset .. "bp",
723                                    preset  = "leftbottom",
724                                }
725                                local finalize = finalizer(dictionary,xscale,yscale,a_llx,a_ury)
726                                context.setlayer(layerspec,locationspec,function()
727                                    context(hpack_node(nodeinjections.annotation(w/bpfactor,h/bpfactor,0,finalize,reference)))
728                                end)
729                            end
730                        end
731                    else
732                     -- report_comment("skipping annotation, index %a",i)
733                    end
734                end
735            elseif trace_comments then
736                report_comment("broken annotation, index %a",i)
737            end
738        end
739    end
740    return namespace
741end
742
743local widgetflags = lpdf.flags.widgets
744
745local function flagstoset(flag,flags)
746    local t = { }
747    if flags then
748        for k, v in next, flags do
749            if (flag & v) ~= 0 then
750                t[k] = true
751            end
752        end
753    end
754    return t
755end
756
757-- BS : border style dict
758-- R  : rotation 0 90 180 270
759-- BG : background array
760-- CA : caption string
761-- RC : roll over caption
762-- AC : down caption
763-- I/RI/IX : icon streams
764-- IF      : fit dictionary
765-- TP      : text position number
766
767-- Opt : array of texts
768-- TI  : top index
769
770-- V  : value
771-- DV : default value
772-- DS : default string
773-- RV : rich
774-- Q  : quadding (0=left 1=middle 2=right)
775
776function codeinjections.mergefields(specification,dataonly)
777    local specification, fullname, document = validdocument(specification)
778    if not document then
779        return ""
780    end
781    local pagenumber  = specification.page or 1
782    local pagedata    = document.pages[pagenumber]
783    local annotations = pagedata and pagedata.Annots
784    if annotations and #annotations > 0 then
785        local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale)
786        initializelayer(height,width)
787        --
788        for i=1,#annotations do
789            -- we keep the order
790            local annotation = annotations[i]
791            if annotation then
792                local subtype = annotation.Subtype
793                if subtype == "Widget" then
794                    local parent = annotation.Parent or { }
795                    local name   = annotation.T or parent.T
796                    local what   = annotation.FT or parent.FT
797                    if name and what then
798                        local x, y, w, h, a_llx, a_lly, a_urx, a_ury = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_field)
799                        if x then
800                            x = x .. "bp"
801                            y = y .. "bp"
802                            local W, H = w, h
803                            w = w .. "bp"
804                            h = h .. "bp"
805                            if trace_fields then
806                                report_field("field %a, type %a, dx %s, dy %s, wd %s, ht %s",name,what,x,y,w,h)
807                            end
808                            local locationspec = {
809                                x      = x,
810                                y      = y,
811                                preset = "leftbottom",
812                            }
813                            --
814                            local aflags = flagstoset(annotation.F  or parent.F  or 0, annotationflags)
815                            local wflags = flagstoset(annotation.Ff or parent.Ff or 0, widgetflags)
816                            if what == "Tx" then
817                                -- DA DV F FT MaxLen MK Q T V | AA OC
818                                if wflags.MultiLine then
819                                    wflags.MultiLine = nil
820                                    what = "text"
821                                else
822                                    what = "line"
823                                end
824
825                                -- via context
826                                local frame   = trace_links and variables.on or variables.off
827                                local default = annotation.V or ""
828                                if default == "" then
829                                    local parent = annotation.Parent
830                                    if parent then
831                                        default = parent.V or ""
832                                    end
833                                end
834                                if dataonly then
835                                    if type(default) == "string" and default ~= "" then
836                                        local data = lpdf.epdf.parsecontent(tostring(annotation.DA or "") or "") or { }
837                                        local font = "Normal"
838                                        local size = "10bp"
839                                        for i=1,#data do
840                                            local d = data[i]
841                                            if d[3] == "Tf" then
842                                                font = d[1][2]
843                                                size = d[2][2] .. "bp"
844                                                break
845                                            end
846                                        end
847                                        local fieldspec = {
848                                            width  = w,
849                                            height = h,
850                                            frame  = frame,
851                                            --
852                                            ["epdf:font"] = font,
853                                            ["epdf:size"] = size,
854                                        }
855report_field("field %a, type %a, data %a",name,what,default)
856                                        context.setlayer (layerspec,locationspec,function()
857                                            context["epdf"..what.."field"](fieldspec,default)
858                                        end)
859                                    end
860                                else
861                                    local fieldspec = {
862                                        width   = w,
863                                        height  = h,
864                                        offset  = variables.overlay,
865                                        frame   = frame,
866                                        n       = annotation.MaxLen or (parent and parent.MaxLen),
867                                        type    = what,
868                                        option  = concat(merged(aflags,wflags),","),
869                                        default = default,
870                                    }
871                                    context.setlayer (layerspec,locationspec,function()
872                                        context.definefieldbody ( { name } , fieldspec )
873                                        context.fieldbody ( { name } )
874                                    end)
875                                end
876                            elseif what == "Btn" then
877                                if wflags.Radio or wflags.RadiosInUnison then
878                                    -- AP AS DA F Ff FT H MK T V | AA OC
879                                    wflags.Radio = nil
880                                    wflags.RadiosInUnison = nil
881                                    what = "radio"
882                                elseif wflags.PushButton then
883                                    -- AP DA F Ff FT H MK T | AA OC
884                                    --
885                                    -- Push buttons only have an appearance and some associated
886                                    -- actions so they are not worth copying.
887                                    --
888                                    wflags.PushButton = nil
889                                    what = "push"
890                                else
891                                    -- AP AS DA F Ff FT H MK T V | OC AA
892                                    what = "check"
893                                    -- direct
894                                    if dataonly then
895                                        report_field("field %a, type %a, todo: render value",name,what)
896                                    else
897                                        local AP = annotation.AP or (parent and parent.AP)
898                                        if AP then
899                                            local a = document.__xrefs__[AP]
900                                            if a and pdfe.copyappearance then
901                                                local o = pdfe.copyappearance(document,a)
902                                                if o then
903                                                    AP = pdfreference(o)
904                                                end
905                                            end
906                                        end
907                                        local dictionary = pdfdictionary {
908                                            Subtype = pdfconstant("Widget"),
909                                            FT      = pdfconstant("Btn"),
910                                            T       = pdfcopyunicode(annotation.T or parent.T),
911                                            F       = pdfcopyinteger(annotation.F or parent.F),
912                                            Ff      = pdfcopyinteger(annotation.Ff or parent.Ff),
913                                            AS      = pdfcopyconstant(annotation.AS or (parent and parent.AS)),
914                                            AP      = AP and pdfreference(AP),
915                                         -- V       = annotation.V,
916                                        }
917                                        local finalize = dictionary()
918                                        context.setlayer(layerspec,locationspec,function()
919                                            context(hpack_node(nodeinjections.annotation(W/bpfactor,H/bpfactor,0,finalize)))
920                                        end)
921                                    end
922                                end
923                            elseif what == "Ch" then
924                                -- F Ff FT Opt T | AA OC (rest follows)
925                                if wflags.PopUp then
926                                    wflags.PopUp = nil
927                                    if wflags.Edit then
928                                        wflags.Edit = nil
929                                        what = "combo"
930                                    else
931                                        what = "popup"
932                                    end
933                                else
934                                    what = "choice"
935                                end
936                            elseif what == "Sig" then
937                                what = "signature"
938                            else
939                                what = nil
940                            end
941                        end
942                    end
943                end
944            end
945        end
946    end
947end
948
949-- The crappy parts of pdf .. too unpredictable.
950
951function codeinjections.mergerenditions(specification)
952--  local specification, fullname, document = validdocument(specification)
953--  if not document then
954--      return ""
955--  end
956--  local pagenumber  = specification.page or 1
957--  local pagedata    = document.pages[pagenumber]
958--  local annotations = pagedata and pagedata.Annots
959--  if annotations and #annotations > 0 then
960--      local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale)
961--      initializelayer(height,width)
962--      --
963--      for i=1,#annotations do
964--          -- we keep the order
965--          local annotation = annotations[i]
966--          if annotation then
967--              local subtype = annotation.Subtype
968--              if subtype == "Screen" then
969--                  print("not done")
970--              end
971--          end
972--      end
973--  end
974end
975
976-- Beware, bookmarks can be in pdfdoc encoding or in unicode. However, in mkiv we
977-- write out the strings in unicode (hex). When we read them in, we check for a bom
978-- and convert to utf.
979
980function codeinjections.getbookmarks(filename)
981
982    -- The first version built a nested tree and flattened that afterwards ... but I decided
983    -- to keep it simple and flat.
984
985    local list = bookmarks.extras.get(filename)
986
987    if list then
988        return list
989    else
990        list = { }
991    end
992
993    local document = nil
994
995    if isfile(filename) then
996        document = loadpdffile(filename)
997    else
998        report_outline("unknown file %a",filename)
999        bookmarks.extras.register(filename,list)
1000        return list
1001    end
1002
1003    local outlines     = document.Catalog.Outlines
1004    local pages        = document.pages
1005    local nofpages     = document.nofpages
1006    local destinations = document.destinations
1007
1008    -- I need to check this destination analyzer with the one in annotations .. best share
1009    -- code (and not it's inconsistent). On the todo list ...
1010
1011    local function setdestination(current,entry)
1012        local destination = nil
1013        local action      = current.A
1014        if action then
1015            local subtype = action.S
1016            if subtype == "GoTo" then
1017                destination = action.D
1018                local kind = type(destination)
1019                if kind == "string" then
1020                    entry.destination = destination
1021                    destination = destinations[destination]
1022                    local pagedata = destination and destination[1]
1023                    if pagedata then
1024                        entry.realpage = pagedata.number
1025                    end
1026                elseif kind == "table" then
1027                    local pageref = #destination
1028                    if pageref then
1029                        local pagedata = pages[pageref]
1030                        if pagedata then
1031                            entry.realpage = pagedata.number
1032                        end
1033                    end
1034                end
1035         -- elseif subtype then
1036         --     report("unsupported bookmark action %a",subtype)
1037            end
1038        else
1039            local destination = current.Dest
1040            if destination then
1041                if type(destination) == "string" then
1042                    local wanted = destinations[destination]
1043                    destination = wanted and wanted.D
1044                    if destination then
1045                        entry.destination = destination
1046                    end
1047                else
1048                    local pagedata = destination and destination[1]
1049                    if pagedata and pagedata.Type == "Page" then
1050                        entry.realpage =  pagedata.number
1051                 -- else
1052                 --     report("unsupported bookmark destination (no page)")
1053                    end
1054                end
1055            end
1056        end
1057    end
1058
1059    local function traverse(current,depth)
1060        while current do
1061         -- local title = current.Title
1062            local title = current("Title") -- can be pdfdoc or unicode
1063            if title then
1064                if type(title) == "table" then
1065                    title = lpdf.frombytes(title[2],title[3]) -- hack, we have an indirect one
1066                end
1067                local entry = {
1068                    level = depth,
1069                    title = title,
1070                }
1071                list[#list+1] = entry
1072                setdestination(current,entry)
1073                if trace_outlines then
1074                    report_outline("%w%s",2*depth,title) -- not ok
1075                end
1076            end
1077            local first = current.First
1078            if first then
1079                local current = first
1080                while current do
1081                    local title = current.Title
1082                    if type(title) == "table" then
1083                        title = lpdf.frombytes(title[2],title[3]) -- hack, we have an indirect one
1084                    end
1085                    if title and trace_outlines then
1086                        report_outline("%w%s",2*depth,title) -- not ok
1087                    end
1088                    local entry = {
1089                        level = depth,
1090                        title = title,
1091                    }
1092                    setdestination(current,entry)
1093                    list[#list+1] = entry
1094                    traverse(current.First,depth+1)
1095                    current = current.Next
1096                end
1097            end
1098            current = current.Next
1099        end
1100    end
1101
1102    if outlines then
1103        if trace_outlines then
1104            report_outline("outline of %a:",document.filename)
1105            report_outline()
1106        end
1107        traverse(outlines,0)
1108        if trace_outlines then
1109            report_outline()
1110        end
1111    elseif trace_outlines then
1112        report_outline("no outline in %a",document.filename)
1113    end
1114
1115    bookmarks.extras.register(filename,list)
1116
1117    return list
1118
1119end
1120
1121function codeinjections.mergebookmarks(specification)
1122    -- codeinjections.getbookmarks(document)
1123    if not specification then
1124        specification = figures and figures.current()
1125        specification = specification and specification.status
1126    end
1127    if specification then
1128        local fullname  = specification.fullname
1129        local bookmarks = codeinjections.getbookmarks(fullname)
1130        local realpage  = tonumber(specification.page) or 1
1131        for i=1,#bookmarks do
1132            local b = bookmarks[i]
1133            if not b.usedpage then
1134                if b.realpage == realpage then
1135                    if trace_options then
1136                        report_outline("using %a at page %a of file %a",b.title,realpage,fullname)
1137                    end
1138                    b.usedpage  = true
1139                    b.section   = structures.sections.currentsectionindex()
1140                    b.pageindex = specification.pageindex
1141                end
1142            end
1143        end
1144    end
1145end
1146
1147-- A bit more than a placeholder but in the same perspective as
1148-- inclusion of comments and fields:
1149--
1150-- getinfo{ filename = "tt.pdf", metadata = true }
1151-- getinfo{ filename = "tt.pdf", page = 1, metadata = "xml" }
1152-- getinfo("tt.pdf")
1153
1154function codeinjections.getinfo(specification)
1155    if type(specification) == "string" then
1156        specification = { filename = specification }
1157    end
1158    local filename = specification.filename
1159    if type(filename) == "string" and isfile(filename) then
1160        local pdffile = loadpdffile(filename)
1161        if pdffile then
1162            local pagenumber = specification.page or 1
1163            local metadata   = specification.metadata
1164            local catalog    = pdffile.Catalog
1165            local info       = pdffile.Info
1166            local pages      = pdffile.pages
1167            local nofpages   = pdffile.nofpages
1168            if metadata then
1169                local m = catalog.Metadata
1170                if m then
1171                    m = m()
1172                    if metadata == "xml" then
1173                        metadata = xml.convert(m)
1174                    else
1175                        metadata = m
1176                    end
1177                else
1178                    metadata = nil
1179                end
1180            else
1181                metadata = nil
1182            end
1183            if pagenumber > nofpages then
1184                pagenumber = nofpages
1185            end
1186            local nobox = { 0, 0, 0, 0 }
1187            local crop  = nobox
1188            local media = nobox
1189            local page  = pages[pagenumber]
1190            if page then
1191                crop  = page.CropBox or nobox
1192                media = page.MediaBox or crop or nobox
1193            end
1194            local bbox = crop or media or nobox
1195            return {
1196                filename     = filename,
1197                pdfversion   = tonumber(catalog.Version),
1198                nofpages     = nofpages,
1199                title        = info.Title,
1200                creator      = info.Creator,
1201                producer     = info.Producer,
1202                creationdate = info.CreationDate,
1203                modification = info.ModDate,
1204                metadata     = metadata,
1205                width        = bbox[4] - bbox[2],
1206                height       = bbox[3] - bbox[1],
1207                cropbox      = { crop[1], crop[2], crop[3], crop[4] },      -- we need access
1208                mediabox     = { media[1], media[2], media[3], media[4] } , -- we need access
1209            }
1210        end
1211    end
1212end
1213