lpdf-ano.lua /size: 46 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['lpdf-ano'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- when using rotation: \disabledirectives[refences.sharelinks] (maybe flag links)
10
11-- todo: /AA << WC << ... >> >> : WillClose actions etc
12
13-- internal references are indicated by a number (and turned into <autoprefix><number>)
14-- we only flush internal destinations that are referred
15
16local next, tostring, tonumber, rawget, type = next, tostring, tonumber, rawget, type
17local rep, format, find = string.rep, string.format, string.find
18local min = math.min
19local lpegmatch = lpeg.match
20local formatters = string.formatters
21local sortedkeys, concat = table.sortedkeys, table.concat
22
23local backends, lpdf = backends, lpdf
24
25local trace_references        = false  trackers.register("references.references",   function(v) trace_references   = v end)
26local trace_destinations      = false  trackers.register("references.destinations", function(v) trace_destinations = v end)
27local trace_bookmarks         = false  trackers.register("references.bookmarks",    function(v) trace_bookmarks    = v end)
28
29local log_destinations        = false  directives.register("destinations.log",     function(v) log_destinations = v end)
30local untex_urls              = true   directives.register("references.untexurls", function(v) untex_urls       = v end)
31
32local report_references       = logs.reporter("backend","references")
33local report_destinations     = logs.reporter("backend","destinations")
34local report_bookmarks        = logs.reporter("backend","bookmarks")
35
36local variables               = interfaces.variables
37local v_auto                  = variables.auto
38local v_page                  = variables.page
39local v_name                  = variables.name
40
41local factor                  = number.dimenfactors.bp
42
43local settings_to_array       = utilities.parsers.settings_to_array
44
45local allocate                = utilities.storage.allocate
46local setmetatableindex       = table.setmetatableindex
47
48local nodeinjections          = backends.pdf.nodeinjections
49local codeinjections          = backends.pdf.codeinjections
50local registrations           = backends.pdf.registrations
51
52local javascriptcode          = interactions.javascripts.code
53
54local references              = structures.references
55local bookmarks               = structures.bookmarks
56
57local flaginternals           = references.flaginternals
58local usedinternals           = references.usedinternals
59local usedviews               = references.usedviews
60
61local runners                 = references.runners
62local specials                = references.specials
63local handlers                = references.handlers
64local executers               = references.executers
65
66local nodepool                = nodes.pool
67
68local new_latelua             = nodepool.latelua
69
70local texgetcount             = tex.getcount
71
72local jobpositions            = job.positions
73local getpos                  = jobpositions.getpos
74local gethpos                 = jobpositions.gethpos
75local getvpos                 = jobpositions.getvpos
76
77local pdfdictionary           = lpdf.dictionary
78local pdfarray                = lpdf.array
79local pdfreference            = lpdf.reference
80local pdfunicode              = lpdf.unicode
81local pdfconstant             = lpdf.constant
82local pdfflushobject          = lpdf.flushobject
83local pdfshareobjectreference = lpdf.shareobjectreference
84local pdfreserveobject        = lpdf.reserveobject
85local pdfpagereference        = lpdf.pagereference
86local pdfdelayedobject        = lpdf.delayedobject
87local pdfregisterannotation   = lpdf.registerannotation -- forward definition (for the moment)
88local pdfnull                 = lpdf.null
89local pdfaddtocatalog         = lpdf.addtocatalog
90local pdfaddtonames           = lpdf.addtonames
91local pdfaddtopageattributes  = lpdf.addtopageattributes
92local pdfrectangle            = lpdf.rectangle
93
94-- todo: 3dview
95
96----- pdf_annot               = pdfconstant("Annot")
97local pdf_uri                 = pdfconstant("URI")
98local pdf_gotor               = pdfconstant("GoToR")
99local pdf_goto                = pdfconstant("GoTo")
100local pdf_launch              = pdfconstant("Launch")
101local pdf_javascript          = pdfconstant("JavaScript")
102local pdf_link                = pdfconstant("Link")
103local pdf_n                   = pdfconstant("N")
104local pdf_t                   = pdfconstant("T")
105local pdf_fit                 = pdfconstant("Fit")
106local pdf_named               = pdfconstant("Named")
107
108local autoprefix              = "#"
109local usedautoprefixes        = { }
110
111function codeinjections.setautoprefix(prefix)
112    autoprefix = prefix ~= "" and prefix or autoprefix
113end
114
115local function registerautoprefix(name)
116    local internal = autoprefix .. name
117    if usedautoprefixes[internal] == nil then
118        usedautoprefixes[internal] = false
119    end
120    return internal
121end
122
123local function useautoprefix(name)
124    local internal = autoprefix .. name
125    usedautoprefixes[internal] = true
126    return internal
127end
128
129local function checkautoprefixes(destinations)
130    for k, v in next, usedautoprefixes do
131        if not v then
132            if trace_destinations then
133                report_destinations("flushing unused autoprefix %a",k)
134            end
135            destinations[k] = nil
136        end
137    end
138end
139
140local maxslice = 32 -- could be made configureable ... 64 is also ok
141
142local function pdfmakenametree(list,apply)
143    if not next(list) then
144        return
145    end
146    local slices   = { }
147    local sorted   = sortedkeys(list)
148    local size     = #sorted
149    local maxslice = maxslice
150    if size <= 1.5*maxslice then
151        maxslice = size
152    end
153    for i=1,size,maxslice do
154        local amount = min(i+maxslice-1,size)
155        local names  = pdfarray { }
156        local n      = 0
157        for j=i,amount do
158            local name   = sorted[j]
159            local target = list[name]
160            n = n + 1 ; names[n] = tostring(name)
161            n = n + 1 ; names[n] = apply and apply(target) or target
162        end
163        local first = sorted[i]
164        local last  = sorted[amount]
165        local limits = pdfarray {
166            first,
167            last,
168        }
169        local d = pdfdictionary {
170            Names  = names,
171            Limits = limits,
172        }
173        slices[#slices+1] = {
174            reference = pdfreference(pdfflushobject(d)),
175            limits    = limits,
176        }
177    end
178    local function collectkids(slices,first,last)
179        local f = slices[first]
180        local l = slices[last]
181        if f and l then
182            local k = pdfarray()
183            local n = 0
184            local d = pdfdictionary {
185                Kids   = k,
186                Limits = pdfarray {
187                    f.limits[1],
188                    l.limits[2],
189                },
190            }
191            for i=first,last do
192                n = n + 1 ; k[n] = slices[i].reference
193            end
194            return d
195        end
196    end
197    if #slices == 1 then
198        return slices[1].reference
199    else
200        while true do
201            local size = #slices
202            if size > maxslice then
203                local temp = { }
204                local n    = 0
205                for i=1,size,maxslice do
206                    local kids = collectkids(slices,i,min(i+maxslice-1,size))
207                    if kids then
208                        n = n + 1
209                        temp[n] = {
210                            reference = pdfreference(pdfflushobject(kids)),
211                            limits    = kids.Limits,
212                        }
213                    else
214                        -- error
215                    end
216                end
217                slices = temp
218            else
219                local kids = collectkids(slices,1,size)
220                if kids then
221                    return pdfreference(pdfflushobject(kids))
222                else
223                    -- error
224                    return
225                end
226            end
227        end
228    end
229end
230
231lpdf.makenametree = pdfmakenametree
232
233-- Bah, I hate this kind of features .. anyway, as we have delayed resolving we
234-- only support a document-wide setup and it has to be set before the first one
235-- is used. Also, we default to a non-intrusive gray and the outline is kept
236-- thin without dashing lines. This is as far as I'm prepared to go. This way
237-- it can also be used as a debug feature.
238
239local pdf_border_style = pdfarray { 0, 0, 0 } -- radius radius linewidth
240local pdf_border_color = nil
241local set_border       = false
242
243local function pdfborder()
244    set_border = true
245    return pdf_border_style, pdf_border_color
246end
247
248lpdf.border = pdfborder
249
250directives.register("references.border",function(v)
251    if v and not set_border then
252        if type(v) == "string" then
253            local m = attributes.list[attributes.private('color')] or { }
254            local c = m and m[v]
255            local v = c and attributes.colors.value(c)
256            if v then
257                local r = v[3]
258                local g = v[4]
259                local b = v[5]
260             -- if r == g and g == b then
261             --     pdf_border_color = pdfarray { r }       -- reduced, not not ... bugged viewers
262             -- else
263                    pdf_border_color = pdfarray { r, g, b } -- always rgb
264             -- end
265            end
266        end
267        if not pdf_border_color then
268            pdf_border_color = pdfarray { .6, .6, .6 } -- no reduce to { 0.6 } as there are buggy viewers out there
269        end
270        pdf_border_style = pdfarray { 0, 0, .5 } -- < 0.5 is not show by acrobat (at least not in my version)
271    end
272end)
273
274-- the used and flag code here is somewhat messy in the sense
275-- that it belongs in strc-ref but at the same time depends on
276-- the backend so we keep it here
277
278-- the caching is somewhat memory intense on the one hand but
279-- it saves many small temporary tables so it might pay off
280
281local pagedestinations = setmetatableindex(function(t,k)
282    k = tonumber(k)
283    if not k or k <= 0 then
284        return pdfnull()
285    end
286    local v = rawget(t,k)
287    if v then
288     -- report_references("page number expected, got %s: %a",type(k),k)
289        return v
290    end
291    local v = k > 0 and pdfarray {
292        pdfreference(pdfpagereference(k)),
293        pdf_fit,
294    } or pdfnull()
295    t[k] = v
296    return v
297end)
298
299local pagereferences = setmetatableindex(function(t,k)
300    k = tonumber(k)
301    if not k or k <= 0 then
302        return nil
303    end
304    local v = rawget(t,k)
305    if v then
306        return v
307    end
308    local v = pdfdictionary { -- can be cached
309        S = pdf_goto,
310        D = pagedestinations[k],
311    }
312    t[k] = v
313    return v
314end)
315
316local defaultdestination = pdfarray { 0, pdf_fit }
317
318-- fit is default (see lpdf-nod)
319
320local destinations = { }
321local reported     = setmetatableindex("table")
322
323local function pdfregisterdestination(name,reference)
324    local d = destinations[name]
325    if d then
326        if not reported[name][reference] then
327            report_destinations("ignoring duplicate destination %a with reference %a",name,reference)
328            reported[name][reference] = true
329        end
330    else
331        destinations[name] = reference
332    end
333end
334
335lpdf.registerdestination = pdfregisterdestination
336
337logs.registerfinalactions(function()
338    if log_destinations and next(destinations) then
339        local report = logs.startfilelogging("references","used destinations")
340        local n = 0
341        for destination, pagenumber in table.sortedhash(destinations) do
342            report("% 4i : %-5s : %s",pagenumber,usedviews[destination] or defaultview,destination)
343            n = n + 1
344        end
345        logs.stopfilelogging()
346        report_destinations("%s destinations saved in log file",n)
347    end
348end)
349
350local function pdfdestinationspecification()
351    if next(destinations) then -- safeguard
352        checkautoprefixes(destinations)
353        local r = pdfmakenametree(destinations,pdfreference)
354        if r then
355            pdfaddtonames("Dests",r)
356        end
357        if not log_destinations then
358            destinations = nil
359        end
360    end
361end
362
363lpdf.destinationspecification = pdfdestinationspecification
364
365lpdf.registerdocumentfinalizer(pdfdestinationspecification,"collect destinations")
366
367-- todo
368
369local destinations = { }
370
371local f_xyz   = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] >>"]
372local f_fit   = formatters["<< /D [ %i 0 R /Fit ] >>"]
373local f_fitb  = formatters["<< /D [ %i 0 R /FitB ] >>"]
374local f_fith  = formatters["<< /D [ %i 0 R /FitH %.6N ] >>"]
375local f_fitv  = formatters["<< /D [ %i 0 R /FitV %.6N ] >>"]
376local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] >>"]
377local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] >>"]
378local f_fitr  = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"]
379
380local v_standard  = variables.standard
381local v_frame     = variables.frame
382local v_width     = variables.width
383local v_minwidth  = variables.minwidth
384local v_height    = variables.height
385local v_minheight = variables.minheight
386local v_fit       = variables.fit
387local v_tight     = variables.tight
388
389-- nicer is to create dictionaries and set properties but it's a bit overkill
390
391-- The problem with the following settings is that they are guesses: we never know
392-- if a box is part of something larger that needs to be in view, or that we are
393-- dealing with a vbox or vtop so the used h/d values cannot be trusted in a tight
394-- view. Of course some decent additional offset would be nice so maybe i'll add
395-- that some day. I never use anything else than 'fit' anyway as I think that the
396-- document should fit the device (and vice versa). In fact, with todays swipe
397-- and finger zooming this whole view is rather useless and as with any zooming
398-- one looses the overview and keeps zooming.
399
400-- todo: scaling
401
402-- local destinationactions = {
403--     [v_standard]  = function(r,w,h,d) return f_xyz  (r,gethpos()*factor,(getvpos()+h)*factor) end, -- local left,top with no zoom
404--     [v_frame]     = function(r,w,h,d) return f_fitr (r,pdfrectangle(w,h,d)) end,                   -- fit rectangle in window
405--     [v_width]     = function(r,w,h,d) return f_fith (r,(getvpos()+h)*factor) end,                  -- top coordinate, fit width of page in window
406--     [v_minwidth]  = function(r,w,h,d) return f_fitbh(r,(getvpos()+h)*factor) end,                  -- top coordinate, fit width of content in window
407--     [v_height]    = function(r,w,h,d) return f_fitv (r,gethpos()*factor) end,                      -- left coordinate, fit height of page in window
408--     [v_minheight] = function(r,w,h,d) return f_fitbv(r,gethpos()*factor) end,                      -- left coordinate, fit height of content in window    [v_fit]       =                          f_fit,                                                 -- fit page in window
409--     [v_tight]     =                          f_fitb,                                               -- fit content in window
410--     [v_fit]       =                          f_fit,
411-- }
412
413local destinationactions = {
414    [v_standard]  = function(r,w,h,d,o)        -- local left,top with no zoom
415        local tx, ty = getpos()
416        return f_xyz(r,tx*factor,(ty+h+2*o)*factor) -- we can assume margins
417    end,
418    [v_frame]     = function(r,w,h,d,o)        -- fit rectangle in window
419        return f_fitr(r,pdfrectangle(w,h,d,o))
420    end,
421    [v_width]     = function(r,w,h,d,o)        -- top coordinate, fit width of page in window
422        return f_fith(r,(getvpos()+h+o)*factor)
423    end,
424    [v_minwidth]  = function(r,w,h,d,o)        -- top coordinate, fit width of content in window
425        return f_fitbh(r,(getvpos()+h+o)*factor)
426    end,
427    [v_height]    = function(r,w,h,d,o)        -- left coordinate, fit height of page in window
428        return f_fitv(r,(gethpos())*factor)
429    end,
430    [v_minheight] = function(r,w,h,d,o)        -- left coordinate, fit height of content in window
431        return f_fitbv(r,(gethpos())*factor)
432    end,
433    [v_tight]     = f_fitb,                    -- fit content in window
434    [v_fit]       = f_fit,                     -- fit content in window
435}
436
437local mapping = {
438    [v_standard]  = v_standard,  xyz   = v_standard,
439    [v_frame]     = v_frame,     fitr  = v_frame,
440    [v_width]     = v_width,     fith  = v_width,
441    [v_minwidth]  = v_minwidth,  fitbh = v_minwidth,
442    [v_height]    = v_height,    fitv  = v_height,
443    [v_minheight] = v_minheight, fitbv = v_minheight,
444    [v_fit]       = v_fit,       fit   = v_fit,
445    [v_tight]     = v_tight,     fitb  = v_tight,
446}
447
448local defaultview   = v_fit
449local defaultaction = destinationactions[defaultview]
450local offset        = 0 -- 65536*5
451
452directives.register("destinations.offset", function(v)
453    offset = string.todimen(v) or 0
454end)
455
456-- A complication is that we need to use named destinations when we have views so we
457-- end up with a mix. A previous versions just output multiple destinations but now
458-- that we moved all to here we can be more sparse.
459
460local pagedestinations = setmetatableindex(function(t,k) -- not the same as the one above!
461    local v = pdfdelayedobject(f_fit(k))
462    t[k] = v
463    return v
464end)
465
466local function flushdestination(specification)
467    local names = specification.names
468    local view  = specification.view
469    local r     = pdfpagereference(texgetcount("realpageno"))
470    if (references.innermethod ~= v_name) and (view == defaultview or not view or view == "") then
471        r = pagedestinations[r]
472    else
473        local action = view and destinationactions[view] or defaultaction
474        r = pdfdelayedobject(action(r,specification.width,specification.height,specification.depth,offset))
475    end
476    for n=1,#names do
477        local name = names[n]
478        if name then
479            pdfregisterdestination(name,r)
480        end
481    end
482end
483
484function nodeinjections.destination(width,height,depth,names,view)
485    -- todo check if begin end node / was comment
486    view = view and mapping[view] or defaultview
487    if trace_destinations then
488        report_destinations("width %p, height %p, depth %p, names %|t, view %a",width,height,depth,names,view)
489    end
490    local method = references.innermethod
491    local noview = view == defaultview
492    local doview = false
493    -- we could save some aut's by using a name when given but it doesn't pay off apart
494    -- from making the code messy and tracing hard .. we only save some destinations
495    -- which we already share anyway
496    if method == v_page then
497        for n=1,#names do
498            local name = names[n]
499            local used = usedviews[name]
500            if used and used ~= true then
501                -- already done, maybe a warning
502            elseif type(name) == "number" then
503             -- if noview then
504             --     usedviews[name] = view
505             --     names[n] = false
506             -- else
507                    usedviews[name] = view
508                    names[n] = false
509             -- end
510            else
511                usedviews[name] = view
512            end
513        end
514    elseif method == v_name then
515        for n=1,#names do
516            local name = names[n]
517            local used = usedviews[name]
518            if used and used ~= true then
519                -- already done, maybe a warning
520            elseif type(name) == "number" then
521                local used = usedinternals[name]
522                usedviews[name] = view
523                names[n] = registerautoprefix(name)
524                doview = true
525            else
526                usedviews[name] = view
527                doview = true
528            end
529        end
530    else
531        for n=1,#names do
532            local name = names[n]
533            if usedviews[name] then
534                -- already done, maybe a warning
535            elseif type(name) == "number" then
536                if noview then
537                    usedviews[name] = view
538                    names[n] = false
539                else
540                    local used = usedinternals[name]
541                    if used and used ~= defaultview then
542                        usedviews[name] = view
543                        names[n] = registerautoprefix(name)
544                        doview = true
545                    else
546                        names[n] = false
547                    end
548                end
549            else
550                usedviews[name] = view
551                doview = true
552            end
553        end
554    end
555    if doview then
556        return new_latelua {
557            action = flushdestination,
558            width  = width,
559            height = height,
560            depth  = depth,
561            names  = names,
562            view   = view,
563        }
564    end
565end
566
567-- we could share dictionaries ... todo
568
569local function pdflinkpage(page)
570    return pagereferences[page]
571end
572
573local function pdflinkinternal(internal,page)
574 -- local method = references.innermethod
575    if internal then
576        flaginternals[internal] = true -- for bookmarks and so
577        local used = usedinternals[internal]
578        if used == defaultview or used == true then
579            return pagereferences[page]
580        else
581            if type(internal) ~= "string" then
582                internal = useautoprefix(internal)
583            end
584            return pdfdictionary {
585                S = pdf_goto,
586                D = internal,
587            }
588        end
589    else
590        return pagereferences[page]
591    end
592end
593
594local function pdflinkname(destination,internal,page)
595    local method = references.innermethod
596    if method == v_auto then
597        local used = defaultview
598        if internal then
599            flaginternals[internal] = true -- for bookmarks and so
600            used = usedinternals[internal] or defaultview
601        end
602        if used == defaultview then -- or used == true then
603            return pagereferences[page]
604        else
605            return pdfdictionary {
606                S = pdf_goto,
607                D = destination,
608            }
609        end
610    elseif method == v_name then
611     -- flaginternals[internal] = true -- for bookmarks and so
612        return pdfdictionary {
613            S = pdf_goto,
614            D = destination,
615        }
616    else
617        return pagereferences[page]
618    end
619end
620
621-- annotations
622
623local function pdffilelink(filename,destination,page,actions)
624    if not filename or filename == "" or file.basename(filename) == tex.jobname then
625        return false
626    end
627    filename = file.addsuffix(filename,"pdf")
628    if (not destination or destination == "") or (references.outermethod == v_page) then
629        destination = pdfarray { (page or 1) - 1, pdf_fit }
630    end
631    return pdfdictionary {
632        S         = pdf_gotor, -- can also be pdf_launch
633        F         = filename,
634        D         = destination or defaultdestination,
635        NewWindow = actions.newwindow and true or nil,
636    }
637end
638
639local untex = references.urls.untex
640
641local function pdfurllink(url,destination,page)
642    if not url or url == "" then
643        return false
644    end
645    if untex_urls then
646        url = untex(url) -- last minute cleanup of \* and spaces
647    end
648    if destination and destination ~= "" then
649        url = url .. "#" .. destination
650    end
651    return pdfdictionary {
652        S   = pdf_uri,
653        URI = url,
654    }
655end
656
657local function pdflaunch(program,parameters)
658    if not program or program == "" then
659        return false
660    end
661    return pdfdictionary {
662        S = pdf_launch,
663        F = program,
664        D = ".",
665        P = parameters ~= "" and parameters or nil
666    }
667end
668
669local function pdfjavascript(name,arguments)
670    local script = javascriptcode(name,arguments) -- make into object (hash)
671    if script then
672        return pdfdictionary {
673            S  = pdf_javascript,
674            JS = script,
675        }
676    end
677end
678
679local function pdfaction(actions)
680    local nofactions = #actions
681    if nofactions > 0 then
682        local a = actions[1]
683        local action = runners[a.kind]
684        if action then
685            action = action(a,actions)
686        end
687        if action then
688            local first = action
689            for i=2,nofactions do
690                local a = actions[i]
691                local what = runners[a.kind]
692                if what then
693                    what = what(a,actions)
694                end
695                if action == what then
696                    -- ignore this one, else we get a loop
697                elseif what then
698                    action.Next = what
699                    action = what
700                else
701                    -- error
702                    return nil
703                end
704            end
705            return first, actions.n or #actions
706        end
707    end
708end
709
710lpdf.action = pdfaction
711
712function codeinjections.prerollreference(actions) -- share can become option
713    if actions then
714        local main, n = pdfaction(actions)
715        if main then
716            local bs, bc = pdfborder()
717            main = pdfdictionary {
718                Subtype = pdf_link,
719             -- Border  = bs,
720                Border  = pdfshareobjectreference(bs),
721                C       = bc,
722                H       = (not actions.highlight and pdf_n) or nil,
723                A       = pdfshareobjectreference(main),
724                F       = 4, -- print (mandate in pdf/a)
725            }
726            return main("A"), n
727        end
728    end
729end
730
731-- local function use_normal_annotations()
732--
733--     local function reference(width,height,depth,prerolled) -- keep this one
734--         if prerolled then
735--             if trace_references then
736--                 report_references("width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
737--             end
738--             return pdfannotation_node(width,height,depth,prerolled)
739--         end
740--     end
741--
742--     local function finishreference()
743--     end
744--
745--     return reference, finishreference
746--
747-- end
748
749-- eventually we can do this for special refs only
750
751local hashed     = { }
752local nofunique  = 0
753local nofused    = 0
754local nofspecial = 0
755local share      = true
756
757local f_annot    = formatters["<< /Type /Annot %s /Rect [ %.6N %.6N %.6N %.6N ] >>"]
758local f_quadp    = formatters["<< /Type /Annot %s /QuadPoints [ %s ] /Rect [ %.6N %.6N %.6N %.6N ] >>"]
759
760directives.register("references.sharelinks", function(v)
761    share = v
762end)
763
764setmetatableindex(hashed,function(t,k)
765    local v = pdfdelayedobject(k)
766    if share then
767        t[k] = v
768    end
769    nofunique = nofunique + 1
770    return v
771end)
772
773local function toquadpoints(paths)
774    local t, n = { }, 0
775    for i=1,#paths do
776        local path = paths[i]
777        local size = #path
778        for j=1,size do
779            local p = path[j]
780            n = n + 1 ; t[n] = p[1]
781            n = n + 1 ; t[n] = p[2]
782        end
783        local m = size % 4
784        if m > 0 then
785            local p = path[size]
786            for j=size+1,m do
787                n = n + 1 ; t[n] = p[1]
788                n = n + 1 ; t[n] = p[2]
789            end
790        end
791    end
792    return concat(t," ")
793end
794
795local function finishreference(specification)
796    local prerolled  = specification.prerolled
797    local quadpoints = specification.mesh
798    local llx, lly,
799          urx, ury   = pdfrectangle(specification.width,specification.height,specification.depth)
800    local specifier  = nil
801    if quadpoints and #quadpoints > 0 then
802        specifier = f_quadp(prerolled,toquadpoints(quadpoints),llx,lly,urx,ury)
803    else
804        specifier = f_annot(prerolled,llx,lly,urx,ury)
805    end
806    nofused = nofused + 1
807    return pdfregisterannotation(hashed[specifier])
808end
809
810local function finishannotation(specification)
811    local prerolled = specification.prerolled
812    local objref    = specification.objref
813    if type(prerolled) == "function" then
814        prerolled = prerolled()
815    end
816    local annot = f_annot(prerolled,pdfrectangle(specification.width,specification.height,specification.depth))
817    if objref then
818        pdfdelayedobject(annot,objref)
819    else
820        objref = pdfdelayedobject(annot)
821    end
822    nofspecial = nofspecial + 1
823    return pdfregisterannotation(objref)
824end
825
826function nodeinjections.reference(width,height,depth,prerolled,mesh)
827    if prerolled then
828        if trace_references then
829            report_references("link: width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
830        end
831        return new_latelua {
832            action    = finishreference,
833            width     = width,
834            height    = height,
835            depth     = depth,
836            prerolled = prerolled,
837            mesh      = mesh,
838        }
839    end
840end
841
842function nodeinjections.annotation(width,height,depth,prerolled,objref)
843    if prerolled then
844        if trace_references then
845            report_references("special: width %p, height %p, depth %p, prerolled %a",width,height,depth,
846                type(prerolled) == "string" and prerolled or "-")
847        end
848        return new_latelua {
849            action    = finishannotation,
850            width     = width,
851            height    = height,
852            depth     = depth,
853            prerolled = prerolled,
854            objref    = objref or false,
855        }
856    end
857end
858
859-- beware, we register during a latelua sweep so we have to make sure that
860-- we finalize after that (also in a latelua for the moment as we have no
861-- callback yet)
862
863local annotations = nil
864
865function lpdf.registerannotation(n)
866    if annotations then
867        annotations[#annotations+1] = pdfreference(n)
868    else
869        annotations = pdfarray { pdfreference(n) } -- no need to use lpdf.array cum suis
870    end
871end
872
873pdfregisterannotation = lpdf.registerannotation
874
875function lpdf.annotationspecification()
876    if annotations then
877        local r = pdfdelayedobject(tostring(annotations)) -- delayed so okay in latelua
878        if r then
879            pdfaddtopageattributes("Annots",pdfreference(r))
880        end
881        annotations = nil
882    end
883end
884
885lpdf.registerpagefinalizer(lpdf.annotationspecification,"finalize annotations")
886
887statistics.register("pdf annotations", function()
888    if nofused > 0 or nofspecial > 0 then
889        return format("%s links (%s unique), %s special",nofused,nofunique,nofspecial)
890    else
891        return nil
892    end
893end)
894
895-- runners and specials
896
897local splitter = lpeg.splitat(",",true)
898
899runners["inner"] = function(var,actions)
900    local internal = false
901    local name     = nil
902    local method   = references.innermethod
903    local vi       = var.i
904    local page     = var.r
905    if vi then
906        local vir = vi.references
907        if vir then
908            -- todo: no need for it when we have a real reference ... although we need
909            -- this mess for prefixes anyway
910            local reference = vir.reference
911            if reference and reference ~= "" then
912                reference = lpegmatch(splitter,reference) or reference
913                var.inner = reference
914                local prefix = var.p
915                if prefix and prefix ~= "" then
916                    var.prefix = prefix
917                    name = prefix .. ":" .. reference
918                else
919                    name = reference
920                end
921            end
922            internal = vir.internal
923            if internal then
924                flaginternals[internal] = true
925            end
926        end
927    end
928    if name then
929        return pdflinkname(name,internal,page)
930    elseif internal then
931        return pdflinkinternal(internal,page)
932    elseif page then
933        return pdflinkpage(page)
934    else
935        -- real bad
936    end
937end
938
939runners["inner with arguments"] = function(var,actions)
940    report_references("todo: inner with arguments")
941    return false
942end
943
944runners["outer"] = function(var,actions)
945    local file, url = references.checkedfileorurl(var.outer,var.outer)
946    if file  then
947        return pdffilelink(file,var.arguments,nil,actions)
948    elseif url then
949        return pdfurllink(url,var.arguments,nil,actions)
950    end
951end
952
953runners["outer with inner"] = function(var,actions)
954    return pdffilelink(references.checkedfile(var.outer),var.inner,var.r,actions)
955end
956
957runners["special outer with operation"] = function(var,actions)
958    local handler = specials[var.special]
959    return handler and handler(var,actions)
960end
961
962runners["special outer"] = function(var,actions)
963    report_references("todo: special outer")
964    return false
965end
966
967runners["special"] = function(var,actions)
968    local handler = specials[var.special]
969    return handler and handler(var,actions)
970end
971
972runners["outer with inner with arguments"] = function(var,actions)
973    report_references("todo: outer with inner with arguments")
974    return false
975end
976
977runners["outer with special and operation and arguments"] = function(var,actions)
978    report_references("todo: outer with special and operation and arguments")
979    return false
980end
981
982runners["outer with special"] = function(var,actions)
983    report_references("todo: outer with special")
984    return false
985end
986
987runners["outer with special and operation"] = function(var,actions)
988    report_references("todo: outer with special and operation")
989    return false
990end
991
992runners["special operation"]                = runners["special"]
993runners["special operation with arguments"] = runners["special"]
994
995local reported = { }
996
997function specials.internal(var,actions) -- better resolve in strc-ref
998    local o = var.operation
999    local i = o and tonumber(o)
1000    local v = i and references.internals[i]
1001    if v then
1002        flaginternals[i] = true -- also done in pdflinkinternal
1003        return pdflinkinternal(i,v.references.realpage)
1004    end
1005    local v = i or o or "<unset>"
1006    if not reported[v] then
1007        report_references("no internal reference %a",v)
1008        reported[v] = true
1009    end
1010end
1011
1012-- realpage already resolved
1013
1014specials.i = specials.internal
1015
1016local pages = references.pages
1017
1018function specials.page(var,actions)
1019    local file = var.f
1020    if file then
1021        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1022    else
1023        local p = var.r
1024        if not p then -- todo: call special from reference code
1025            p = pages[var.operation]
1026            if type(p) == "function" then -- double
1027                p = p()
1028            else
1029                p = references.realpageofpage(tonumber(p))
1030            end
1031        end
1032        return pdflinkpage(p or var.operation)
1033    end
1034end
1035
1036function specials.realpage(var,actions)
1037    local file = var.f
1038    if file then
1039        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1040    else
1041        return pdflinkpage(var.operation)
1042    end
1043end
1044
1045function specials.userpage(var,actions)
1046    local file = var.f
1047    if file then
1048        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1049    else
1050        local p = var.r
1051        if not p then -- todo: call special from reference code
1052            p = var.operation
1053            if p then -- no function and special check here. only numbers
1054                p = references.realpageofpage(tonumber(p))
1055            end
1056         -- if p then
1057         --     var.r = p
1058         -- end
1059        end
1060        return pdflinkpage(p or var.operation)
1061    end
1062end
1063
1064function specials.deltapage(var,actions)
1065    local p = tonumber(var.operation)
1066    if p then
1067        p = references.checkedrealpage(p + texgetcount("realpageno"))
1068        return pdflinkpage(p)
1069    end
1070end
1071
1072-- sections
1073
1074function specials.section(var,actions)
1075    -- a bit duplicate
1076    local sectionname = var.arguments
1077    local destination = var.operation
1078    local internal    = structures.sections.internalreference(sectionname,destination)
1079    if internal then
1080        var.special   = "internal"
1081        var.operation = internal
1082        var.arguments = nil
1083        return specials.internal(var,actions)
1084    end
1085end
1086
1087-- todo, do this in references namespace ordered instead (this is an experiment)
1088
1089local splitter = lpeg.splitat(":")
1090
1091function specials.order(var,actions) -- references.specials !
1092    local operation = var.operation
1093    if operation then
1094        local kind, name, n = lpegmatch(splitter,operation)
1095        local order = structures.lists.ordered[kind]
1096        order = order and order[name]
1097        local v = order[tonumber(n)]
1098        local r = v and v.references.realpage
1099        if r then
1100            var.operation = r -- brrr, but test anyway
1101            return specials.page(var,actions)
1102        end
1103    end
1104end
1105
1106function specials.url(var,actions)
1107    return pdfurllink(references.checkedurl(var.operation),var.arguments,nil,actions)
1108end
1109
1110function specials.file(var,actions)
1111    return pdffilelink(references.checkedfile(var.operation),var.arguments,nil,actions)
1112end
1113
1114function specials.fileorurl(var,actions)
1115    local file, url = references.checkedfileorurl(var.operation,var.operation)
1116    if file then
1117        return pdffilelink(file,var.arguments,nil,actions)
1118    elseif url then
1119        return pdfurllink(url,var.arguments,nil,actions)
1120    end
1121end
1122
1123function specials.program(var,content)
1124    local program = references.checkedprogram(var.operation)
1125    return pdflaunch(program,var.arguments)
1126end
1127
1128function specials.javascript(var)
1129    return pdfjavascript(var.operation,var.arguments)
1130end
1131
1132specials.JS = specials.javascript
1133
1134executers.importform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ImportFDF") }
1135executers.exportform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ExportFDF") }
1136executers.first       = pdfdictionary { S = pdf_named, N = pdfconstant("FirstPage") }
1137executers.previous    = pdfdictionary { S = pdf_named, N = pdfconstant("PrevPage") }
1138executers.next        = pdfdictionary { S = pdf_named, N = pdfconstant("NextPage") }
1139executers.last        = pdfdictionary { S = pdf_named, N = pdfconstant("LastPage") }
1140executers.backward    = pdfdictionary { S = pdf_named, N = pdfconstant("GoBack") }
1141executers.forward     = pdfdictionary { S = pdf_named, N = pdfconstant("GoForward") }
1142executers.print       = pdfdictionary { S = pdf_named, N = pdfconstant("Print") }
1143executers.exit        = pdfdictionary { S = pdf_named, N = pdfconstant("Quit") }
1144executers.close       = pdfdictionary { S = pdf_named, N = pdfconstant("Close") }
1145executers.save        = pdfdictionary { S = pdf_named, N = pdfconstant("Save") }
1146executers.savenamed   = pdfdictionary { S = pdf_named, N = pdfconstant("SaveAs") }
1147executers.opennamed   = pdfdictionary { S = pdf_named, N = pdfconstant("Open") }
1148executers.help        = pdfdictionary { S = pdf_named, N = pdfconstant("HelpUserGuide") }
1149executers.toggle      = pdfdictionary { S = pdf_named, N = pdfconstant("FullScreen") }
1150executers.search      = pdfdictionary { S = pdf_named, N = pdfconstant("Find") }
1151executers.searchagain = pdfdictionary { S = pdf_named, N = pdfconstant("FindAgain") }
1152executers.gotopage    = pdfdictionary { S = pdf_named, N = pdfconstant("GoToPage") }
1153executers.query       = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:Query") }
1154executers.queryagain  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:NextHit") }
1155executers.fitwidth    = pdfdictionary { S = pdf_named, N = pdfconstant("FitWidth") }
1156executers.fitheight   = pdfdictionary { S = pdf_named, N = pdfconstant("FitHeight") }
1157
1158local function fieldset(arguments)
1159    -- [\dogetfieldset{#1}]
1160    return nil
1161end
1162
1163function executers.resetform(arguments)
1164    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
1165    return pdfdictionary {
1166        S     = pdfconstant("ResetForm"),
1167        Field = fieldset(arguments[1])
1168    }
1169end
1170
1171local formmethod = "post" -- "get" "post"
1172local formformat = "xml"  -- "xml" "html" "fdf"
1173
1174-- bit 3 = html bit 6 = xml bit 4 = get
1175
1176local flags = {
1177    get = {
1178        html = 12, fdf = 8, xml = 40,
1179    },
1180    post = {
1181        html = 4, fdf = 0, xml = 32,
1182    }
1183}
1184
1185function executers.submitform(arguments)
1186    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
1187    local flag = flags[formmethod] or flags.post
1188    flag = (flag and (flag[formformat] or flag.xml)) or 32 -- default: post, xml
1189    return pdfdictionary {
1190        S     = pdfconstant("SubmitForm"),
1191        F     = arguments[1],
1192        Field = fieldset(arguments[2]),
1193        Flags = flag,
1194    -- \PDFsubmitfiller
1195    }
1196end
1197
1198local pdf_hide = pdfconstant("Hide")
1199
1200function executers.hide(arguments)
1201    return pdfdictionary {
1202        S = pdf_hide,
1203        H = true,
1204        T = arguments,
1205    }
1206end
1207
1208function executers.show(arguments)
1209    return pdfdictionary {
1210        S = pdf_hide,
1211        H = false,
1212        T = arguments,
1213    }
1214end
1215
1216local pdf_movie  = pdfconstant("Movie")
1217local pdf_start  = pdfconstant("Start")
1218local pdf_stop   = pdfconstant("Stop")
1219local pdf_resume = pdfconstant("Resume")
1220local pdf_pause  = pdfconstant("Pause")
1221
1222local function movie_or_sound(operation,what,arguments)
1223    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
1224    return pdfdictionary {
1225        S         = pdf_movie,
1226        T         = format("%s %s",what,arguments[1] or "noname"),
1227        Operation = operation,
1228    }
1229end
1230
1231function executers.startmovie (arguments) return movie_or_sound(pdf_start ,"movie",arguments) end
1232function executers.stopmovie  (arguments) return movie_or_sound(pdf_stop  ,"movie",arguments) end
1233function executers.resumemovie(arguments) return movie_or_sound(pdf_resume,"movie",arguments) end
1234function executers.pausemovie (arguments) return movie_or_sound(pdf_pause ,"movie",arguments) end
1235
1236function executers.startsound (arguments) return movie_or_sound(pdf_start ,"sound",arguments) end
1237function executers.stopsound  (arguments) return movie_or_sound(pdf_stop  ,"sound",arguments) end
1238function executers.resumesound(arguments) return movie_or_sound(pdf_resume,"sound",arguments) end
1239function executers.pausesound (arguments) return movie_or_sound(pdf_pause ,"sound",arguments) end
1240
1241function specials.action(var)
1242    local operation = var.operation
1243    if var.operation and operation ~= "" then
1244        local e = executers[operation]
1245        if type(e) == "table" then
1246            return e
1247        elseif type(e) == "function" then
1248            return e(var.arguments)
1249        end
1250    end
1251end
1252
1253local function build(levels,start,parent,method,nested)
1254    local startlevel = levels[start].level
1255    local noflevels  = #levels
1256    local i = start
1257    local n = 0
1258    local child, entry, m, prev, first, last, f, l
1259    while i and i <= noflevels do
1260        local current = levels[i]
1261        if current.usedpage == false then
1262            -- safeguard
1263            i = i + 1
1264        else
1265            local level     = current.level
1266            local title     = current.title
1267            local reference = current.reference
1268            local opened    = current.opened
1269            local reftype   = type(reference)
1270            local block     = nil
1271            local variant   = "unknown"
1272            if reftype == "table" then
1273                -- we're okay
1274                variant  = "list"
1275                block    = reference.block
1276                realpage = reference.realpage
1277            elseif reftype == "string" then
1278                local resolved = references.identify("",reference)
1279                realpage = resolved and structures.references.setreferencerealpage(resolved) or 0
1280                if realpage > 0 then
1281                    variant   = "realpage"
1282                    realpage  = realpage
1283                    reference = structures.pages.collected[realpage]
1284                    block     = reference and reference.block
1285                end
1286            elseif reftype == "number" then
1287                if reference > 0 then
1288                    variant   = "realpage"
1289                    realpage  = reference
1290                    reference = structures.pages.collected[realpage]
1291                    block     = reference and reference.block
1292                end
1293            else
1294                -- error
1295            end
1296            current.block = block
1297            if variant == "unknown" then
1298                -- error, ignore
1299                i = i + 1
1300         -- elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].reference.block) then
1301            elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].block) then
1302                if nested then -- could be an option but otherwise we quit too soon
1303                    if entry then
1304                        pdfflushobject(child,entry)
1305                    else
1306                        report_bookmarks("error 1")
1307                    end
1308                    return i, n, first, last
1309                else
1310                    report_bookmarks("confusing level change at level %a around %a",level,title)
1311                    startlevel = level
1312                end
1313            end
1314            if level == startlevel then
1315                if trace_bookmarks then
1316                    report_bookmarks("%3i %w%s %s",realpage,(level-1)*2,(opened and "+") or "-",title)
1317                end
1318                local prev = child
1319                child = pdfreserveobject()
1320                if entry then
1321                    entry.Next = child and pdfreference(child)
1322                    pdfflushobject(prev,entry)
1323                end
1324                local action = nil
1325                if variant == "list" then
1326                    action = pdflinkinternal(reference.internal,reference.realpage)
1327                elseif variant == "realpage" then
1328                    action = pagereferences[realpage]
1329                else
1330                    -- hm, what to do
1331                end
1332                entry = pdfdictionary {
1333                    Title  = pdfunicode(title),
1334                    Parent = parent,
1335                    Prev   = prev and pdfreference(prev),
1336                    A      = action,
1337                }
1338             -- entry.Dest = pdflinkinternal(reference.internal,reference.realpage)
1339                if not first then
1340                    first, last = child, child
1341                end
1342                prev = child
1343                last = prev
1344                n = n + 1
1345                i = i + 1
1346            elseif i < noflevels and level > startlevel then
1347                i, m, f, l = build(levels,i,pdfreference(child),method,true)
1348                if entry then
1349                    entry.Count = (opened and m) or -m
1350                    if m > 0 then
1351                        entry.First = pdfreference(f)
1352                        entry.Last  = pdfreference(l)
1353                    end
1354                else
1355                    report_bookmarks("error 2")
1356                end
1357            else
1358                -- missing intermediate level but ok
1359                i, m, f, l = build(levels,i,pdfreference(child),method,true)
1360                if entry then
1361                    entry.Count = (opened and m) or -m
1362                    if m > 0 then
1363                        entry.First = pdfreference(f)
1364                        entry.Last  = pdfreference(l)
1365                    end
1366                    pdfflushobject(child,entry)
1367                else
1368                    report_bookmarks("error 3")
1369                end
1370                return i, n, first, last
1371            end
1372        end
1373    end
1374    pdfflushobject(child,entry)
1375    return nil, n, first, last
1376end
1377
1378function codeinjections.addbookmarks(levels,method)
1379    if levels and #levels > 0 then
1380        local parent = pdfreserveobject()
1381        local _, m, first, last = build(levels,1,pdfreference(parent),method or "internal",false)
1382        local dict = pdfdictionary {
1383            Type  = pdfconstant("Outlines"),
1384            First = pdfreference(first),
1385            Last  = pdfreference(last),
1386            Count = m,
1387        }
1388        pdfflushobject(parent,dict)
1389        pdfaddtocatalog("Outlines",lpdf.reference(parent))
1390    end
1391end
1392
1393-- this could also be hooked into the frontend finalizer
1394
1395lpdf.registerdocumentfinalizer(function() bookmarks.place() end,1,"bookmarks") -- hm, why indirect call
1396