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