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