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