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