lpdf-ano.lmt /size: 69 Kb    last modification: 2025-02-21 11:03
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, max = math.min, math.max
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                  <const> = variables.auto
45local v_page                  <const> = variables.page
46local v_name                  <const> = variables.name
47
48local factor                  <const> = 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 pdfstring               = lpdf.string
91local pdfconstant             = lpdf.constant
92local pdfnull                 = lpdf.null
93local pdfaddtocatalog         = lpdf.addtocatalog
94local pdfaddtonames           = lpdf.addtonames
95local pdfaddtopageattributes  = lpdf.addtopageattributes
96local pdfrectangle            = lpdf.rectangle
97local pdfquads                = lpdf.quads
98
99local pdfflushobject          = lpdf.flushobject
100local pdfshareobjectreference = lpdf.shareobjectreference
101local pdfreserveobject        = lpdf.reserveobject
102local pdfpagereference        = lpdf.pagereference
103local pdfdelayedobject        = lpdf.delayedobject
104local pdfmajorversion         = lpdf.majorversion
105
106-- defined later on:
107
108local pdfregisterannotation
109
110-- todo: 3dview
111
112----- pdf_annot               = pdfconstant("Annot")
113local pdf_uri                 = pdfconstant("URI")
114local pdf_gotor               = pdfconstant("GoToR")
115local pdf_goto                = pdfconstant("GoTo")
116local pdf_launch              = pdfconstant("Launch")
117local pdf_javascript          = pdfconstant("JavaScript")
118local pdf_highlight           = pdfconstant("Highlight")
119local pdf_link                = pdfconstant("Link")
120local pdf_n                   = pdfconstant("N")
121local pdf_t                   = pdfconstant("T")
122local pdf_fit                 = pdfconstant("Fit")
123local pdf_named               = pdfconstant("Named")
124
125local autoprefix              = "#"
126local usedautoprefixes        = { }
127
128local crappytaggingmode       = 0
129
130updaters.register("structures.tagging",function(version)
131    if tex.conditionals.c_strc_tags_global then
132        crappytaggingmode = tonumber(version) or 0
133    end
134end)
135
136function codeinjections.setautoprefix(prefix)
137    autoprefix = prefix ~= "" and prefix or autoprefix
138end
139
140local function registerautoprefix(name)
141    local internal = autoprefix .. name
142    if usedautoprefixes[internal] == nil then
143        usedautoprefixes[internal] = false
144    end
145    return internal
146end
147
148local function useautoprefix(name)
149    local internal = autoprefix .. name
150    usedautoprefixes[internal] = true
151    return internal
152end
153
154local function checkautoprefixes(destinations)
155    for k, v in next, usedautoprefixes do
156        if not v then
157            if trace_destinations then
158                report_destinations("flushing unused autoprefix %a",k)
159            end
160            destinations[k] = nil
161        end
162    end
163end
164
165-- officially newlines are supposed to be ignored in sorting and there might
166-- be escapes but we can decide to unicode them
167
168local pdfmakenametree  do
169
170    local maxslice = 32 -- could be made configureable ... 64 is also ok
171    local convert  = pdfstring
172
173    directives.register("backend.pdf.unicodenames",function(v)
174        convert = v and pdfunicode or pdfstring
175    end)
176
177    pdfmakenametree = function(list,apply)
178        if not next(list) then
179            return
180        end
181        local slices   = { }
182        local sorted   = sortedkeys(list)
183        local size     = #sorted
184        local maxslice = maxslice
185        if size <= 1.5*maxslice then
186            maxslice = size
187        end
188        for i=1,size,maxslice do
189            local amount = min(i+maxslice-1,size)
190            local names  = pdfarray { }
191            local n      = 0
192            for j=i,amount do
193                local name   = sorted[j]
194                local target = list[name]
195                n = n + 1 ; names[n] = convert(name)
196                n = n + 1 ; names[n] = apply and apply(target) or target
197            end
198            local first = sorted[i]
199            local last  = sorted[amount]
200            local limits = pdfarray {
201                convert(first),
202                convert(last),
203            }
204            local d = pdfdictionary {
205                Names  = names,
206                Limits = limits,
207            }
208            slices[#slices+1] = {
209                reference = pdfreference(pdfflushobject(d)),
210                limits    = limits,
211            }
212        end
213        local function collectkids(slices,first,last)
214            local f = slices[first]
215            local l = slices[last]
216            if f and l then
217                local k = pdfarray()
218                local n = 0
219                local d = pdfdictionary {
220                    Kids   = k,
221                    Limits = pdfarray {
222                        f.limits[1],
223                        l.limits[2],
224                    },
225                }
226                for i=first,last do
227                    n = n + 1 ; k[n] = slices[i].reference
228                end
229                return d
230            end
231        end
232        if #slices == 1 then
233            return slices[1].reference
234        else
235            while true do
236                local size = #slices
237                if size > maxslice then
238                    local temp = { }
239                    local n    = 0
240                    for i=1,size,maxslice do
241                        local kids = collectkids(slices,i,min(i+maxslice-1,size))
242                        if kids then
243                            n = n + 1
244                            temp[n] = {
245                                reference = pdfreference(pdfflushobject(kids)),
246                                limits    = kids.Limits,
247                            }
248                        else
249                            -- error
250                        end
251                    end
252                    slices = temp
253                else
254                    local kids = collectkids(slices,1,size)
255                    if kids then
256                        return pdfreference(pdfflushobject(kids))
257                    else
258                        -- error
259                        return
260                    end
261                end
262            end
263        end
264    end
265
266    lpdf.makenametree = pdfmakenametree
267
268end
269
270-- Bah, I hate this kind of features .. anyway, as we have delayed resolving we
271-- only support a document-wide setup and it has to be set before the first one
272-- is used. Also, we default to a non-intrusive gray and the outline is kept
273-- thin without dashing lines. This is as far as I'm prepared to go. This way
274-- it can also be used as a debug feature.
275--
276-- A bit of granularity is possible by specifying colors per kind (+special) but
277-- it assumes that one knows what categories there are (we report unknown ones
278-- once). This is an undocumented feature (at least in manuals). Keep in mind that
279-- after decades of pdf being around borders are not supported widely. They are
280-- ugly anyway and they basically contradict quality typesetting. Directives are:
281--
282-- references.border=color
283--
284-- references.border=inner:red
285-- references.border=special operation:blue
286-- references.border=special operation+url:green
287
288-- This will become a plugin once I have time for it: the directive can load the
289-- code then: lpdf-ano-imp-border.lmt or so.
290
291local pdf_border_style = pdfarray { 0, 0, 0 } -- radius radius linewidth
292local pdf_border_color = nil
293local set_border       = false
294local use_borders      = false
295
296local function pdfborder(actions)
297    set_border = true
298    if use_borders and actions then
299        local action  = actions[1]
300        if action then
301            local kind    = action.kind
302            local special = action.special or ""
303            local border  = use_borders[kind]
304            if not border then
305                report_references("no border color defined for %a",kind)
306                use_borders[kind] = { [""] = pdf_border_color }
307            else
308                border = border[special]
309                if border then
310                    return pdf_border_style, border
311                else
312                    report_references("no border color defined for %a : %a",kind,special)
313                    use_borders[kind] = { [special] = pdf_border_color }
314                end
315            end
316        end
317    end
318    return pdf_border_style, pdf_border_color
319end
320
321lpdf.border = pdfborder
322
323-- This can be a more generic lpdf helper:
324
325local pdfcolorarray do
326
327    local colorlist  = attributes.list[attributes.private('color')]
328    local colorvalue = attributes.colors.value
329
330    pdfcolorarray = function(v)
331        if v then
332            v = colorlist and colorlist[v]
333            if v then
334                v = colorvalue(v)
335                if v then
336                    v = pdfarray { v[3], v[4], v[5] } -- always rgb
337                end
338            end
339        end
340        if not v then
341            v = pdfarray { .8, .8, .8 }
342        end
343        return v
344    end
345
346end
347
348directives.register("references.border",function(v)
349    if v and not set_border then
350        if type(v) == "string" then
351            if find(v,":") then
352                if not use_borders then
353                    use_borders = { }
354                end
355                local kind, special, color = match(v,"^([a-z%s]+)+*([a-z%s]*):(.+)$")
356                if kind then
357                    local c = pdfcolorarray(color)
358                    if c then
359                        local k = use_borders[kind]
360                        if k then
361                            k[special] = c
362                        else
363                            use_borders[kind] = { [special] = c }
364                        end
365                    end
366                end
367            else
368                pdf_border_color = pdfcolorarray(v)
369            end
370        end
371        if not pdf_border_color then
372            pdf_border_color = pdfarray { .6, .6, .6 } -- no reduce to { 0.6 } as there are buggy viewers out there
373        end
374        pdf_border_style = pdfarray { 0, 0, .5 } -- < 0.5 is not show by acrobat (at least not in my version)
375    end
376end)
377
378-- the used and flag code here is somewhat messy in the sense
379-- that it belongs in strc-ref but at the same time depends on
380-- the backend so we keep it here
381
382-- the caching is somewhat memory intense on the one hand but
383-- it saves many small temporary tables so it might pay off
384
385local pagedestinations = setmetatableindex(function(t,k)
386    k = tonumber(k)
387    if not k or k <= 0 then
388        return pdfnull()
389    end
390    local v = rawget(t,k)
391    if not v then
392        v = k > 0 and pdfarray {
393            pdfreference(pdfpagereference(k)),
394            pdf_fit,
395        } or pdfnull()
396        t[k] = v
397    end
398    return v
399end)
400
401local crappypagedestinations = setmetatableindex(function(t,k)
402    k = tonumber(k)
403    if not k or k <= 0 then
404        return pdfnull()
405    end
406    local v = rawget(t,k)
407    if not v then
408        local o = codeinjections.getreferencestructureobject(false,k) or 0
409        v = o > 0 and pdfarray {
410            pdfreference(o),
411            pdf_fit,
412        } or pdfnull()
413        t[k] = v
414    end
415    return v
416end)
417
418local pagereferences = setmetatableindex(function(t,k)
419    k = tonumber(k)
420    if not k or k <= 0 then
421        return nil
422    end
423    local v = rawget(t,k)
424    if v then
425        return v
426    end
427    local v = pdfdictionary { -- can be cached
428        S  = pdf_goto,
429        D  = pagedestinations[k],
430        SD = crappytaggingmode > 1 and crappypagedestinations[k] or nil,
431    }
432    t[k] = v
433    return v
434end)
435
436local defaultdestination = pdfarray { 0, pdf_fit }
437
438-- fit is default (see lpdf-nod)
439
440local destinations = { }
441local reported     = setmetatableindex("table")
442
443local function pdfregisterdestination(name,reference)
444    local d = destinations[name]
445    if d then
446        if not reported[name][reference] then
447            report_destinations("ignoring duplicate destination %a with reference %a",name,reference)
448            reported[name][reference] = true
449        end
450    else
451        destinations[name] = reference
452    end
453end
454
455lpdf.registerdestination = pdfregisterdestination
456
457logs.registerfinalactions(function()
458    if log_destinations and next(destinations) then
459        local report = logs.startfilelogging("references","used destinations")
460        local n = 0
461        for destination, pagenumber in table.sortedhash(destinations) do
462            report("% 4i : %-5s : %s",pagenumber,usedviews[destination] or defaultview,destination)
463            n = n + 1
464        end
465        logs.stopfilelogging()
466        report_destinations("%s destinations saved in log file",n)
467    end
468end)
469
470local function pdfdestinationspecification()
471    if next(destinations) then -- safeguard
472        checkautoprefixes(destinations)
473        local r = pdfmakenametree(destinations,pdfreference)
474        if r then
475            pdfaddtonames("Dests",r)
476        end
477        if not log_destinations then
478            destinations = nil
479        end
480    end
481end
482
483lpdf.destinationspecification = pdfdestinationspecification
484
485lpdf.registerdocumentfinalizer(pdfdestinationspecification,"collect destinations")
486
487-- todo
488
489local destinations = { }
490
491local v_standard  <const> = variables.standard
492local v_frame     <const> = variables.frame
493local v_width     <const> = variables.width
494local v_minwidth  <const> = variables.minwidth
495local v_height    <const> = variables.height
496local v_minheight <const> = variables.minheight
497local v_fit       <const> = variables.fit
498local v_tight     <const> = variables.tight
499
500local mapping = {
501    [v_standard]  = v_standard,  xyz   = v_standard,
502    [v_frame]     = v_frame,     fitr  = v_frame,
503    [v_width]     = v_width,     fith  = v_width,
504    [v_minwidth]  = v_minwidth,  fitbh = v_minwidth,
505    [v_height]    = v_height,    fitv  = v_height,
506    [v_minheight] = v_minheight, fitbv = v_minheight,
507    [v_fit]       = v_fit,       fit   = v_fit,
508    [v_tight]     = v_tight,     fitb  = v_tight,
509}
510
511local defaultview   = v_fit
512local offset        = 0 -- 65536*5
513
514local c_realpageno  <const> = tex.iscount("realpageno")
515
516-- nicer is to create dictionaries and set properties but it's a bit overkill
517
518-- The problem with the following settings is that they are guesses: we never know
519-- if a box is part of something larger that needs to be in view, or that we are
520-- dealing with a vbox or vtop so the used h/d values cannot be trusted in a tight
521-- view. Of course some decent additional offset would be nice so maybe i'll add
522-- that some day. I never use anything else than 'fit' anyway as I think that the
523-- document should fit the device (and vice versa). In fact, with todays swipe
524-- and finger zooming this whole view is rather useless and as with any zooming
525-- one looses the overview and keeps zooming.
526
527local destinationactions, defaultaction  do
528
529    local f_xyz   = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] >>"]
530    local f_fit   = formatters["<< /D [ %i 0 R /Fit ] >>"]
531    local f_fitb  = formatters["<< /D [ %i 0 R /FitB ] >>"]
532    local f_fith  = formatters["<< /D [ %i 0 R /FitH %.6N ] >>"]
533    local f_fitv  = formatters["<< /D [ %i 0 R /FitV %.6N ] >>"]
534    local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] >>"]
535    local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] >>"]
536    local f_fitr  = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"]
537
538    destinationactions = {
539        -- local left,top with no zoom
540        [v_standard] = function(r,w,h,d,o)
541            local tx, ty = getpos()
542            return f_xyz(r,tx*factor,(ty+h+2*o)*factor)
543        end,
544        -- fit rectangle in window
545        [v_frame] = function(r,w,h,d,o)
546            return f_fitr(r,pdfrectangle(w,h,d,o))
547        end,
548        -- top coordinate, fit width of page in window
549        [v_width] = function(r,w,h,d,o)
550            return f_fith(r,(getvpos()+h+o)*factor)
551        end,
552        -- top coordinate, fit width of content in window
553        [v_minwidth] = function(r,w,h,d,o)
554            return f_fitbh(r,(getvpos()+h+o)*factor)
555        end,
556        -- left coordinate, fit height of page in window
557        [v_height] = function(r,w,h,d,o)
558            return f_fitv(r,(gethpos())*factor)
559        end,
560        -- left coordinate, fit height of content in window
561        [v_minheight] = function(r,w,h,d,o)
562            return f_fitbv(r,(gethpos())*factor)
563        end,
564        -- fit content in window
565        [v_tight] = f_fitb,
566        -- fit content in window
567        [v_fit] = f_fit,
568    }
569
570    defaultaction = destinationactions[defaultview]
571
572end
573
574local xdestinationactions, xdefaultaction  do
575
576    local f_xyz   = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] /SD [ %i 0 R /XYZ %.6N %.6N null ] >>"]
577    local f_fit   = formatters["<< /D [ %i 0 R /Fit ] /SD [ %i 0 R /Fit ] >>"]
578    local f_fitb  = formatters["<< /D [ %i 0 R /FitB ] /SD [ %i 0 R /FitB ] >>"]
579    local f_fith  = formatters["<< /D [ %i 0 R /FitH %.6N ] /SD [ %i 0 R /FitH %.6N ] >>"]
580    local f_fitv  = formatters["<< /D [ %i 0 R /FitV %.6N ] /SD[ %i 0 R /FitV %.6N ] >>"]
581    local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] /SD [ %i 0 R /FitBH %.6N ] >>"]
582    local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] /SD [ %i 0 R /FitBV %.6N ] >>"]
583    local f_fitr  = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] /SD [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"]
584
585    xdestinationactions = {
586        [v_standard] = function(r,w,h,d,o,s)
587            local tx, ty = getpos()
588            local x = tx*factor
589            local y = (ty+h+2*o)*factor
590            return f_xyz(r,x,y,s,x,y)
591        end,
592        [v_frame] = function(r,w,h,d,o,s)
593            return f_fitr(r,pdfrectangle(w,h,d,o),s,pdfrectangle(w,h,d,o))
594        end,
595        [v_width] = function(r,w,h,d,o,s)
596            local v = (getvpos()+h+o)*factor
597            return f_fith(r,v,s,v)
598        end,
599        [v_minwidth] = function(r,w,h,d,o,s)
600            local h = (getvpos()+h+o)*factor
601            return f_fitbh(r,h,s,h)
602        end,
603        [v_height] = function(r,w,h,d,o,s)
604            local v = gethpos()*factor
605            return f_fitv(r,v,s,v)
606        end,
607        [v_minheight] = function(r,w,h,d,o,s)
608            local h = gethpos()*factor
609            return f_fitbv(r,h,s,h)
610        end,
611        [v_tight] = function(r,w,h,d,o,s)
612            return f_fitb(r,s)
613        end,
614        [v_fit] = function(r,w,h,d,o,s)
615            return f_fit(r,s)
616        end,
617    }
618
619    xdefaultaction = xdestinationactions[defaultview]
620
621end
622
623directives.register("destinations.offset", function(v)
624    offset = string.todimen(v) or 0
625end)
626
627-- A complication is that we need to use named destinations when we have views so we
628-- end up with a mix. A previous versions just output multiple destinations but now
629-- that we moved all to here we can be more sparse.
630
631local f_fit = formatters["<< /D [ %i 0 R /Fit ] >>"]
632
633local pagedestinations = setmetatableindex(function(t,k) -- not the same as the one above!
634    local v = pdfdelayedobject(f_fit(k))
635    t[k] = v
636    return v
637end)
638
639local function flushdestination(specification)
640    local names = specification.names
641    local view  = specification.view
642    local page  = texgetcount(c_realpageno)
643    local r     = pdfpagereference(page)
644    if (crappytaggingmode < 2) and (references.innermethod ~= v_name) and (view == defaultview or not view or view == "") then
645        r = pagedestinations[r]
646    else
647        local action, o
648        if crappytaggingmode > 1 then
649            local internal = specification.internal -- actually attribute
650            if internal or page then
651                o = codeinjections.getreferencestructureobject(internal,page) -- actually attribute
652                if o and o > 0 then
653                    action = view and xdestinationactions[view] or xdefaultaction
654local d = autoprefix .. internal
655local f = false
656for i=1,#names do
657    if names[i] == d then
658        f = true
659        break
660    end
661end
662if not f then
663    names[#names+1] = d
664end
665                else
666                    o = nil
667                end
668            end
669        end
670        if not action then
671            action = view and destinationactions[view] or defaultaction
672        end
673-- if o then
674--     local objref = pdfreserveobject()
675--     local specifier = action(r,specification.width,specification.height,specification.depth,offset,o)
676--     pdfdelayedobject(specifier(),objref)
677--     r = pdfregisterannotation(objref)
678-- else
679        r = pdfdelayedobject(action(r,specification.width,specification.height,specification.depth,offset,o))
680-- end
681    end
682    for n=1,#names do
683        local name = names[n]
684        if name then
685            pdfregisterdestination(name,r)
686        end
687    end
688end
689
690function nodeinjections.destination(width,height,depth,names,view,internal)
691    -- todo check if begin end node / was comment
692    view = view and mapping[view] or defaultview
693    if trace_destinations then
694        report_destinations("width %p, height %p, depth %p, names %|t, view %a",width,height,depth,names,view)
695    end
696    local method = references.innermethod
697    local noview = view == defaultview
698    local doview = false
699    -- we could save some aut's by using a name when given but it doesn't pay off apart
700    -- from making the code messy and tracing hard .. we only save some destinations
701    -- which we already share anyway
702    if method == v_page then
703        for n=1,#names do
704            local name = names[n]
705            if name then
706                local used = usedviews[name]
707                if used and used ~= true then
708                    -- already done, maybe a warning
709                elseif type(name) == "number" then
710                 -- if noview then
711                 --     usedviews[name] = view
712                 --     names[n] = false
713                 -- else
714                        usedviews[name] = view
715                        names[n] = false
716                 -- end
717                else
718                    usedviews[name] = view
719                end
720            end
721        end
722    elseif method == v_name then
723        for n=1,#names do
724            local name = names[n]
725            if name then
726                local used = usedviews[name]
727                if used and used ~= true then
728                    -- already done, maybe a warning
729                elseif type(name) == "number" then
730                    local used = usedinternals[name]
731                    usedviews[name] = view
732                    names[n] = registerautoprefix(name)
733                    doview = true
734                else
735                    usedviews[name] = view
736                    doview = true
737                end
738            end
739        end
740    else
741        for n=1,#names do
742            local name = names[n]
743            if name then
744                if usedviews[name] then
745                    -- already done, maybe a warning
746                elseif type(name) == "number" then
747                    if noview then
748                        usedviews[name] = view
749                        names[n] = false
750                    else
751                        local used = usedinternals[name]
752                        if used and used ~= defaultview then
753                            usedviews[name] = view
754                            names[n] = registerautoprefix(name)
755                            doview = true
756                        else
757                            names[n] = false
758                        end
759                    end
760                else
761                    usedviews[name] = view
762                    doview = true
763                end
764            end
765        end
766    end
767    if doview or crappytaggingmode > 0 then
768        return new_latelua {
769--             kind   = "destination",
770            action   = flushdestination,
771            width    = width,
772            height   = height,
773            depth    = depth,
774            names    = names,
775            view     = view,
776            internal = internal,
777        }
778    end
779end
780
781-- we could share dictionaries ... todo
782
783local function pdflinkpage(page)
784    return pagereferences[page]
785end
786
787local function pdflinkinternal(internal,page)
788 -- local method = references.innermethod
789    if internal then
790        flaginternals[internal] = true -- for bookmarks and so
791        if crappytaggingmode > 1 then
792            local o = codeinjections.getreferencestructureobject(internal,page)
793            if o and o > 0 then
794                if type(internal) ~= "string" then
795                    internal = useautoprefix(internal)
796                end
797                return pdfdictionary {
798                    S  = pdf_goto,
799                    D  = internal, -- redundant here (needs checking)
800                    SD = pdfarray { pdfreference(o), pdf_fit } -- link or nested
801                }
802            end
803        end
804        local used = usedinternals[internal]
805        if type(internal) ~= "string" then
806            internal = useautoprefix(internal)
807        end
808        if used == defaultview or used == true then
809            return pagereferences[page]
810        else
811            if type(internal) ~= "string" then
812                internal = useautoprefix(internal)
813            end
814            return pdfdictionary {
815                S  = pdf_goto,
816                D  = internal,
817                SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
818            }
819        end
820    else
821        return pagereferences[page]
822    end
823end
824
825local function pdflinkname(destination,internal,page)
826    local method = references.innermethod
827    if method == v_auto then
828        local used = defaultview
829        if internal then
830            flaginternals[internal] = true -- for bookmarks and so
831            used = usedinternals[internal] or defaultview
832        end
833        if used == defaultview then -- or used == true then
834            return pagereferences[page]
835        else
836            return pdfdictionary {
837                S  = pdf_goto,
838                D  = destination,
839                SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
840            }
841        end
842    elseif method == v_name then
843     -- flaginternals[internal] = true -- for bookmarks and so
844        return pdfdictionary {
845            S  = pdf_goto,
846            D  = destination,
847            SD = crappytaggingmode > 1 and crappypagedestinations[page] or nil,
848        }
849    else
850        return pagereferences[page]
851    end
852end
853
854-- annotations
855
856local pdffilelink  do
857
858    local valid = table.setmetatableindex(function(t,filename)
859        local found = false
860        if lfs.isfile(filename) then
861            report_destinations("loading destinations from file %a",filename)
862            local pdffile = lpdf.epdf.load(filename)
863            if pdffile then
864                local pages        = pdffile.pages
865                local nofpages     = pdffile.nofpages
866                local destinations = pdffile.destinations
867                if pages and nofpages > 0 and destinations then
868                    local reverse = swapped(pages)
869                    local total   = 0
870                          found   = { }
871                    for k, v in next, destinations do
872                        local D = v.D
873                        if D then
874                            found[k] = reverse[D[1]]
875                            total    = total + 1
876                        end
877                    end
878                    t[filename] = found
879                    report_destinations("%i destinations on %i pages found",total,nofpages)
880                end
881            end
882        end
883        return found
884    end)
885
886    local pagefromhash = structures.references.pagefromhash
887
888    pdffilelink = function(filename,destination,page,actions)
889        if not filename or filename == "" or file.basename(filename) == tex.jobname then
890            return false
891        end
892        filename = file.addsuffix(filename,"pdf")
893        -- page auto name
894        local forcepage = false
895        if not destination or destination == "" then
896            forcepage = true
897        elseif references.outermethod == v_page then
898            if not page then
899                local hash = valid[filename]
900                page = hash and hash[destination]
901                if not page or trace_externals then
902                    report_destinations("no %s destination %a in file %a","page",destination,filename)
903                end
904            end
905            forcepage = true
906        else -- name or auto, maybe only check with auto
907            local hash = valid[filename]
908            if hash then
909                local p = nil
910                p, destination = pagefromhash(hash,destination,page,actions)
911                if p then
912                    if references.outermethod == v_name then
913                        -- keep destination string
914                    elseif page then
915                        if p ~= page then
916                            report_destinations("page %i for destination %a in %a conflicts, %i expected",page,destination,filename,p)
917                            page = p
918                        end
919                        forcepage = true
920                    elseif p then
921                        page = p
922                        forcepage = true
923                    end
924                else
925                    if not page or trace_externals then
926                        report_destinations("no %s destination %a in file %a","name",destination,filename)
927                    end
928                    forcepage = true
929                end
930         -- else
931                -- keep destination string
932            end
933        end
934        if forcepage then
935            destination = pdfarray { (page or 1) - 1, pdf_fit }
936        end
937        return pdfdictionary {
938            S         = pdf_gotor, -- can also be pdf_launch
939            F         = filename,
940            D         = destination or defaultdestination,
941            NewWindow = actions.newwindow and true or nil,
942        }
943    end
944
945end
946
947local untex = references.urls.untex
948
949local function pdfurllink(url,destination,page)
950    if not url or url == "" then
951        return false
952    end
953    if untex_urls then
954        url = untex(url) -- last minute cleanup of \* and spaces
955    end
956    if destination and destination ~= "" then
957        url = url .. "#" .. destination
958    end
959    return pdfdictionary {
960        S   = pdf_uri,
961        URI = url,
962    }
963end
964
965local function pdflaunch(program,parameters)
966    if not program or program == "" then
967        return false
968    end
969    return pdfdictionary {
970        S = pdf_launch,
971        F = program,
972        D = ".",
973        P = parameters ~= "" and parameters or nil
974    }
975end
976
977local function pdfjavascript(name,arguments)
978    local script = javascriptcode(name,arguments) -- make into object (hash)
979    if script then
980        return pdfdictionary {
981            S  = pdf_javascript,
982            JS = script,
983        }
984    end
985end
986
987-- local function pdfhighlight(name,arguments)
988--     return pdfdictionary {
989--         S       = pdf_highlight,
990--         C       = pdfarray { 1, 1, 0 },
991--         Title   = name and pdfunicode(name) or nil,
992--         Content = arguments and pdfunicode(arguments) or nil
993--     }
994-- end
995
996local function pdfaction(actions)
997    local nofactions = #actions
998    if nofactions > 0 then
999        local a = actions[1]
1000        local action = runners[a.kind]
1001        if action then
1002            action = action(a,actions)
1003        end
1004        if action then
1005            local first = action
1006            for i=2,nofactions do
1007                local a = actions[i]
1008                local what = runners[a.kind]
1009                if what then
1010                    what = what(a,actions)
1011                end
1012                if action == what then
1013                    -- ignore this one, else we get a loop
1014                elseif what then
1015                    action.Next = what
1016                    action = what
1017                else
1018                    -- error
1019                    return nil
1020                end
1021            end
1022            return first, actions.n or #actions
1023        end
1024    end
1025end
1026
1027lpdf.action = pdfaction
1028
1029-- Because a highlight annotation is actually an area we cheat a bit and implement it here instead
1030-- of in the widget module (like text and attachments).
1031
1032local nofhighlights = 0
1033local fixhighlight  = false
1034
1035directives.register("backend.pdf.fixhighlight", function(v)
1036    fixhighlight = v
1037end)
1038
1039function codeinjections.prerollreference(actions,index) -- share can become option
1040    if actions then
1041        local action = actions[1]
1042        if action and action.special == "highlight" then
1043            nofhighlights = nofhighlights + 1
1044         -- local creation = lpdf.pdftimestamp(os.date("%Y-%m-%dT%H:%M:%S") .. os.timezone())
1045            local author   = action.operation
1046            local contents = action.arguments
1047            local color    = pdfcolorarray(author and "pdfhighlight:" .. author or nil)
1048            local bs, bc   = pdfborder(actions)
1049
1050            local main = pdfdictionary {
1051                Subtype     = pdf_highlight,
1052                Border      = pdfshareobjectreference(bs),
1053                C           = color or pdfarray { .8, .8, .8 },
1054                T           = author and pdfunicode(author) or nil,
1055                Contents    = contents and contents ~= "" and pdfunicode(contents) or nil,
1056                F           = 4, -- print (mandate in pdf/a)
1057             -- M           = pdfstring(creation),
1058                NM          = pdfstring("LMTX:" .. nofhighlights), -- also makes for a unique mesh reference
1059                LMTX_QP1243 = fixhighlight and true or nil,
1060            }
1061            actions.forcemesh = true
1062            return main, 1
1063        else
1064            local main, n = pdfaction(actions)
1065            if main then
1066                local bs, bc = pdfborder(actions)
1067                main = pdfdictionary {
1068                    Subtype = pdf_link,
1069                    Border  = pdfshareobjectreference(bs),
1070                    C       = bc,
1071                    H       = (not actions.highlight and pdf_n) or nil,
1072                    A       = pdfshareobjectreference(main),
1073                    F       = 4, -- print (mandate in pdf/a)
1074                }
1075                return main, n
1076            end
1077        end
1078    end
1079end
1080
1081-- local function use_normal_annotations()
1082--
1083--     local function reference(width,height,depth,prerolled) -- keep this one
1084--         if prerolled then
1085--             if trace_references then
1086--                 report_references("width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
1087--             end
1088--             return pdfannotation_node(width,height,depth,prerolled)
1089--         end
1090--     end
1091--
1092--     local function finishreference()
1093--     end
1094--
1095--     return reference, finishreference
1096--
1097-- end
1098
1099-- eventually we can do this for special refs only
1100
1101local hashed     = { }
1102local nofunique  = 0
1103local nofused    = 0
1104local nofspecial = 0
1105local share      = true
1106
1107local refobjects = { }
1108
1109local f_annot    = formatters["<< /Type /Annot %s /Rect [ %.6N %.6N %.6N %.6N ] >>"]
1110local f_quadp    = formatters["<< /Type /Annot %s /QuadPoints [ %s ] /Rect [ %.6N %.6N %.6N %.6N ] >>"]
1111
1112directives.register("references.sharelinks", function(v)
1113    share = v
1114end)
1115
1116setmetatableindex(hashed,function(t,k)
1117    local v = pdfdelayedobject(k)
1118    if share then
1119        t[k] = v
1120    end
1121    nofunique = nofunique + 1
1122    return v
1123end)
1124
1125local function toquadpoints(paths,bugged)
1126    local t, n = { }, 0
1127    if bugged then
1128        -- According to the specification we go ll lr ur ul. This seems to be okay for links but not for e.g.
1129        -- highlights. In links the viewer looks at the areas as links and that's a different code path than
1130        -- rendering the highlight and obviously that code expects a different order: tl tr bl br otherwise
1131        -- (2024) Acrobat, Sumatra, Okular etc. give weird results (also depending on them drawing straiht
1132        -- lines or curves. It looks like an acrobat bug became a viewer specification but the specification
1133        -- doesn't mention it.
1134        for i=1,#paths do
1135            local p = paths[i]
1136            p[3], p[4] = p[4], p[3]
1137        end
1138    end
1139    --
1140    for i=1,#paths do
1141        local path = paths[i]
1142        local size = #path
1143        for j=1,size do
1144            local p = path[j]
1145            n = n + 1 ; t[n] = p[1]
1146            n = n + 1 ; t[n] = p[2]
1147        end
1148        local m = size % 4
1149        if m > 0 then
1150            local p = path[size]
1151            for j=size+1,m do
1152                n = n + 1 ; t[n] = p[1]
1153                n = n + 1 ; t[n] = p[2]
1154            end
1155        end
1156    end
1157    return concat(t," ")
1158end
1159
1160local finishreference  do
1161
1162    -- Just a gimmick but one that adds runtime and memory usage. it is only needed
1163    -- when we have many long references spanning lines.
1164
1165    local collected = allocate()
1166    local tobesaved = allocate()
1167
1168    local jobmeshedup = {
1169        collected = collected,
1170        tobesaved = tobesaved,
1171    }
1172
1173    job.meshedup = jobmeshedup
1174
1175    local function initializer()
1176        tobesaved = jobmeshedup.tobesaved
1177        collected = jobmeshedup.collected
1178    end
1179
1180    job.register('job.meshedup.collected',tobesaved,initializer)
1181
1182    local checkmesh     = false
1183    local lastprerolled = false
1184    local lastmeshedup  = false
1185    local lastrealpage  = 0
1186
1187    local count = 0
1188    local total = 0
1189    local valid = 0
1190    local more  = 0
1191
1192    local function meshup(prerolled,llx,lly,urx,ury)
1193        local q = nil
1194        local r = texgetcount(c_realpageno)
1195        local p = collected[r] and collected[r][prerolled]
1196        if r ~= lastrealpage then
1197            lastrealpage  = r
1198            lastprerolled = false
1199            lastmeshedup  = false
1200        end
1201        if prerolled == lastprerolled then
1202            if not more then
1203                valid = valid + 1
1204                more  = true
1205            end
1206            total = total + 1
1207            if #lastmeshed == 4 then
1208                local t = tobesaved[r]
1209                if not t then
1210                    t = { }
1211                    tobesaved[r] = t
1212                end
1213                t[prerolled] = lastmeshed
1214            end
1215            lastmeshed[#lastmeshed+1] = llx
1216            lastmeshed[#lastmeshed+1] = lly
1217            lastmeshed[#lastmeshed+1] = urx
1218            lastmeshed[#lastmeshed+1] = ury
1219        else
1220            lastprerolled = prerolled
1221            lastmeshed    = { llx, lly, urx, ury }
1222            count         = count + 1
1223            more          = false
1224        end
1225        if p then
1226            if p.done then
1227                q = true
1228            else
1229                q = { }
1230                if #p > 0 then
1231                    for i=1,#p,4 do
1232                        local lx = p[i]
1233                        local ly = p[i+1]
1234                        local ux = p[i+2]
1235                        local uy = p[i+3]
1236                        if i == 1 then
1237                            llx = min(lx,ux)
1238                            lly = min(ly,uy)
1239                            urx = max(lx,ux)
1240                            ury = max(ly,uy)
1241                        else
1242                            llx = min(llx,lx,ux)
1243                            lly = min(lly,ly,uy)
1244                            urx = max(urx,lx,ux)
1245                            ury = max(ury,ly,uy)
1246                        end
1247                        q[#q+1] = { { lx, ly }, { ux, ly }, { ux, uy }, { lx, uy } }
1248                    end
1249                end
1250                p.done = true
1251            end
1252        end
1253        return llx, lly, urx, ury, q
1254    end
1255
1256    -- Maybe some day: when we want a mesh then we need to move the lines according to
1257    -- the transformation which means that we need to keep track of the vertical
1258    -- progression in a paragraph and that is something I don't want to do right now.
1259    -- After all, these features are often somwehat fragile in viewers anyway.
1260
1261 -- local function qmeshup(prerolled,x1,y1,x2,y2,x3,y3,x4,y4)
1262 --     local q = nil
1263 --     local r = texgetcount(c_realpageno)
1264 --     local p = collected[r] and collected[r][prerolled]
1265 --     if r ~= lastrealpage then
1266 --         lastrealpage  = r
1267 --         lastprerolled = false
1268 --         lastmeshedup  = false
1269 --     end
1270 --     if prerolled == lastprerolled then
1271 --         if not more then
1272 --             valid = valid + 1
1273 --             more  = true
1274 --         end
1275 --         total = total + 1
1276 --         if #lastmeshed == 8 then
1277 --             local t = tobesaved[r]
1278 --             if not t then
1279 --                 t = { }
1280 --                 tobesaved[r] = t
1281 --             end
1282 --             t[prerolled] = lastmeshed
1283 --         end
1284 --         lastmeshed[#lastmeshed+1] = x1
1285 --         lastmeshed[#lastmeshed+1] = y1
1286 --         lastmeshed[#lastmeshed+1] = x2
1287 --         lastmeshed[#lastmeshed+1] = y2
1288 --         lastmeshed[#lastmeshed+1] = x3
1289 --         lastmeshed[#lastmeshed+1] = y3
1290 --         lastmeshed[#lastmeshed+1] = x4
1291 --         lastmeshed[#lastmeshed+1] = y4
1292 --     else
1293 --         lastprerolled = prerolled
1294 --         lastmeshed    = { x1, y1, x2, y2, x3, y3, x4, y4 }
1295 --         count         = count + 1
1296 --         more          = false
1297 --     end
1298 --     if p then
1299 --         if p.done then
1300 --             q = true
1301 --         else
1302 --             q = { }
1303 --             if #p > 0 then
1304 --                 for i=1,#p,8 do
1305 --                     local x1 = p[i]
1306 --                     local y1 = p[i+1]
1307 --                     local x2 = p[i+2]
1308 --                     local y2 = p[i+3]
1309 --                     local x3 = p[i+4]
1310 --                     local y3 = p[i+5]
1311 --                     local x4 = p[i+6]
1312 --                     local y4 = p[i+7]
1313 --                     if i == 1 then
1314 --                         llx = min(x1,x2,x3,x4)
1315 --                         lly = min(y1,y2,y3,y4)
1316 --                         urx = max(x1,x2,x3,x4)
1317 --                         ury = max(y1,y2,y3,y4)
1318 --                     else
1319 --                         llx = min(llx,x1,x2,x3,x4)
1320 --                         lly = min(lly,y1,y2,y3,y4)
1321 --                         urx = max(urx,x1,x2,x3,x4)
1322 --                         ury = max(ury,y1,y2,y3,y4)
1323 --                     end
1324 --                     q[#q+1] = { { x1, y1 }, { x2, y2 }, { x3, y3 }, { x4, y4 } }
1325 --                 end
1326 --             end
1327 --             p.done = true
1328 --         end
1329 --     end
1330 --     return llx, lly, urx, ury, q
1331 -- end
1332
1333    local function identify(prerolled,llx,lly,urx,ury)
1334        local r = texgetcount(c_realpageno)
1335        if r ~= lastrealpage then
1336            lastrealpage  = r
1337            lastprerolled = false
1338            lastmeshedup  = false
1339        end
1340        if prerolled == lastprerolled then
1341            if not more then
1342                valid = valid + 1
1343                more  = true
1344            end
1345            total = total + 1
1346        else
1347            lastprerolled = prerolled
1348            count         = count + 1
1349            more          = false
1350        end
1351        return llx, lly, urx, ury, nil
1352    end
1353
1354    statistics.register("meshed up references", function()
1355        if count > 0 then
1356            return format("%i seen, %i valid, %i total",count,valid,total)
1357        else
1358            return nil
1359        end
1360    end)
1361
1362    directives.register("references.meshup", function(v)
1363        checkmesh = (v == "identify" and identify) or (v and meshup) or nil
1364    end)
1365
1366    -- End of gimmick.
1367
1368    local function checked(prerolled,specification)
1369        local llx, lly, urx, ury = pdfrectangle(specification.width,specification.height,specification.depth)
1370        local quadpoints = specification.mesh
1371        local forcemesh = specification.forcemesh
1372        local rotated = specification.rotated
1373        if quadpoints then
1374            -- this always wins when set (normally only happens with mp graphics)
1375        elseif checkmesh then
1376            llx, lly, urx, ury, quadpoints = checkmesh(prerolled,llx,lly,urx,ury)
1377            if quadpoints == true then
1378                return
1379            end
1380        elseif forcemesh then
1381            llx, lly, urx, ury, quadpoints = meshup(prerolled,llx,lly,urx,ury)
1382            if quadpoints == true then
1383                return
1384            end
1385        end
1386        if forcemesh then
1387            if fixhighlight and specification.prerolled and tostring(specification.prerolled.Subtype) == "/Highlight" then
1388                bugged = true
1389            end
1390            if rotated or not quadpoints then
1391                quadpoints = { { { llx, lly }, { urx, lly }, { urx, ury }, { llx, ury } } }
1392            end
1393        elseif rotated then
1394            local x1, y1, x2, y2, x3, y3, x4, y4, llx, lly, urx, ury = pdfquads(0,llx/factor,urx/factor,ury/factor)
1395            quadpoints = { { { x1, y1 }, { x2, y2 }, { x3, y3 }, { x4, y4 } } }
1396        end
1397        if quadpoints and #quadpoints > 0 then
1398            prerolled = f_quadp(prerolled,toquadpoints(quadpoints,bugged),llx,lly,urx,ury)
1399        else
1400            prerolled = f_annot(prerolled,llx,lly,urx,ury)
1401        end
1402        return prerolled
1403    end
1404
1405    finishreference = function(specification)
1406        local prerolled = specification.prerolled
1407        local refatt    = specification.reference
1408        if crappytaggingmode > 0 then
1409            if type(prerolled) ~= "string" then
1410                -- validators want this but what to put in it that is not redundant
1411                prerolled.Contents     = pdfunicode(specification.description or "link")
1412                prerolled.StructParent = codeinjections.getlinkstructureparent(refatt)
1413                prerolled = prerolled()
1414            end
1415            local specifier = checked(prerolled,specification)
1416            if not specifier then
1417                return
1418            end
1419            local objref = pdfreserveobject()
1420            specification.objref = objref
1421         -- nofunique = nofunique + 1
1422            nofused = nofused + 1
1423            if refatt then
1424                refobjects[refatt] = objref
1425            end
1426            pdfdelayedobject(specifier,objref)
1427            return pdfregisterannotation(objref)
1428        else
1429            if type(prerolled) ~= "string" then
1430                prerolled = prerolled()
1431            end
1432            local specifier = checked(prerolled,specification)
1433            if not specifier then
1434                return
1435            end
1436            local objref = hashed[specifier]
1437            specification.objref = objref
1438            nofused = nofused + 1
1439            if refatt then
1440                refobjects[refatt] = objref
1441            end
1442            return pdfregisterannotation(objref)
1443        end
1444    end
1445
1446end
1447
1448function codeinjections.getrefobj(refatt) -- bad name but experiment anyway
1449    return refobjects[refatt]
1450end
1451
1452local function finishannotation(specification)
1453    local prerolled = specification.prerolled
1454    local objref    = specification.objref
1455    if type(prerolled) == "function" then
1456        prerolled = prerolled()
1457    end
1458    if type(prerolled) ~= "string" then
1459        prerolled = prerolled()
1460    end
1461    local annot = f_annot(prerolled,pdfrectangle(specification.width,specification.height,specification.depth))
1462    if objref then
1463        pdfdelayedobject(annot,objref)
1464    else
1465        objref = pdfdelayedobject(annot)
1466        specification.objref = objref
1467    end
1468    nofspecial = nofspecial + 1
1469    return pdfregisterannotation(objref)
1470end
1471
1472function nodeinjections.reference(reference,width,height,depth,prerolled,mesh,description,forcemesh,rotated)
1473    if prerolled then
1474        if trace_references then
1475            report_references("link: width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled)
1476        end
1477        return new_latelua {
1478         -- kind      = "reference",
1479            action      = finishreference,
1480            reference   = reference,
1481            width       = width,
1482            height      = height,
1483            depth       = depth,
1484            prerolled   = prerolled,
1485            mesh        = mesh,
1486            description = description,
1487            forcemesh   = forcemesh,
1488            rotated     = rotated,
1489        }
1490    end
1491end
1492
1493function nodeinjections.annotation(width,height,depth,prerolled,objref)
1494    if prerolled then
1495        if trace_references then
1496            report_references("special: width %p, height %p, depth %p, prerolled %a",width,height,depth,
1497                type(prerolled) == "string" and prerolled or "-")
1498        end
1499        return new_latelua {
1500         -- kind      = "annotation",
1501            action    = finishannotation,
1502            width     = width,
1503            height    = height,
1504            depth     = depth,
1505            prerolled = prerolled,
1506            objref    = objref or false,
1507        }
1508    end
1509end
1510
1511-- beware, we register during a latelua sweep so we have to make sure that
1512-- we finalize after that (also in a latelua for the moment as we have no
1513-- callback yet)
1514
1515local annotations = nil
1516
1517pdfregisterannotation = function(n)
1518    if annotations then
1519        annotations[#annotations+1] = pdfreference(n)
1520    else
1521        annotations = pdfarray { pdfreference(n) } -- no need to use lpdf.array cum suis
1522    end
1523    return n
1524end
1525
1526lpdf.registerannotation = pdfregisterannotation
1527
1528-- Another weird version 2 demand: there should be a Tabs entry because otherwise a viewer
1529-- can choose its own order. So why not just have a recommended default? If a viewer wants
1530-- to be compliant it will obey that.
1531
1532function lpdf.annotationspecification()
1533    if annotations then
1534        local r = pdfdelayedobject(tostring(annotations)) -- delayed so okay in latelua
1535        if r then
1536            pdfaddtopageattributes("Annots",pdfreference(r))
1537         -- if pdfmajorversion() > 1 then
1538                pdfaddtopageattributes("Tabs",pdfconstant("S")) -- structure
1539         -- end
1540        end
1541        annotations = nil
1542    end
1543end
1544
1545lpdf.registerpagefinalizer(lpdf.annotationspecification,"finalize annotations")
1546
1547statistics.register("pdf annotations", function()
1548    if nofused > 0 or nofspecial > 0 then
1549        return format("%s links (%s unique), %s special",nofused,nofunique,nofspecial)
1550    else
1551        return nil
1552    end
1553end)
1554
1555-- runners and specials
1556
1557local splitter = lpeg.splitat(",",true)
1558
1559runners["inner"] = function(var,actions)
1560    local internal = false
1561    local name     = nil
1562    local method   = references.innermethod
1563    local vi       = var.i
1564    local page     = var.r
1565    if vi then
1566        local vir = vi.references
1567        if vir then
1568            -- todo: no need for it when we have a real reference ... although we need
1569            -- this mess for prefixes anyway
1570            local reference = vir.reference
1571            if reference and reference ~= "" then
1572                reference = lpegmatch(splitter,reference) or reference
1573                var.inner = reference
1574                local prefix = var.p
1575                if prefix and prefix ~= "" then
1576                    var.prefix = prefix
1577                    name = prefix .. ":" .. reference
1578                else
1579                    name = reference
1580                end
1581            end
1582            internal = vir.internal
1583            if internal then
1584                flaginternals[internal] = true
1585            end
1586        end
1587    end
1588    if name then
1589        return pdflinkname(name,internal,page)
1590--     elseif internal or crappytaggingmode > 1 then
1591    elseif internal then
1592        return pdflinkinternal(internal,page,true)
1593    elseif page then
1594        return pdflinkpage(page)
1595    else
1596        -- real bad
1597    end
1598end
1599
1600runners["inner with arguments"] = function(var,actions)
1601    report_references("todo: inner with arguments")
1602    return false
1603end
1604
1605runners["outer"] = function(var,actions)
1606    local file, url = references.checkedfileorurl(var.outer,var.outer)
1607    if file  then
1608        return pdffilelink(file,var.arguments,nil,actions)
1609    elseif url then
1610        return pdfurllink(url,var.arguments,nil,actions)
1611    end
1612end
1613
1614runners["outer with inner"] = function(var,actions)
1615    if var.r then
1616        actions.realpage = var.r
1617    end
1618    return pdffilelink(references.checkedfile(var.outer),var.inner,var.r,actions)
1619end
1620
1621runners["special outer with operation"] = function(var,actions)
1622    local handler = specials[var.special]
1623    return handler and handler(var,actions)
1624end
1625
1626runners["special outer"] = function(var,actions)
1627    report_references("todo: special outer")
1628    return false
1629end
1630
1631runners["special"] = function(var,actions)
1632    local handler = specials[var.special]
1633    return handler and handler(var,actions)
1634end
1635
1636runners["outer with inner with arguments"] = function(var,actions)
1637    report_references("todo: outer with inner with arguments")
1638    return false
1639end
1640
1641runners["outer with special and operation and arguments"] = function(var,actions)
1642    report_references("todo: outer with special and operation and arguments")
1643    return false
1644end
1645
1646runners["outer with special"] = function(var,actions)
1647    report_references("todo: outer with special")
1648    return false
1649end
1650
1651runners["outer with special and operation"] = function(var,actions)
1652    report_references("todo: outer with special and operation")
1653    return false
1654end
1655
1656runners["special operation"]                = runners["special"]
1657runners["special operation with arguments"] = runners["special"]
1658
1659local reported = { }
1660
1661function specials.internal(var,actions) -- better resolve in strc-ref
1662    local o = var.operation
1663    local i = o and tonumber(o)
1664    local v = i and references.internals[i]
1665    if v then
1666        flaginternals[i] = true -- also done in pdflinkinternal
1667        return pdflinkinternal(i,v.references.realpage)
1668    end
1669    local v = i or o or "<unset>"
1670    if not reported[v] then
1671        report_references("no internal reference %a",v)
1672        reported[v] = true
1673    end
1674end
1675
1676-- realpage already resolved
1677
1678specials.i = specials.internal
1679
1680local pages = references.pages
1681
1682function specials.page(var,actions)
1683    local file = var.f
1684    if file then
1685        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1686    else
1687        local p = var.r
1688        if not p then -- todo: call special from reference code
1689            p = pages[var.operation]
1690            if type(p) == "function" then -- double
1691                p = p()
1692            else
1693                p = references.realpageofpage(tonumber(p))
1694            end
1695        end
1696        return pdflinkpage(p or var.operation)
1697    end
1698end
1699
1700function specials.realpage(var,actions)
1701    local file = var.f
1702    if file then
1703        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1704    else
1705        return pdflinkpage(var.operation)
1706    end
1707end
1708
1709function specials.userpage(var,actions)
1710    local file = var.f
1711    if file then
1712        return pdffilelink(references.checkedfile(file),nil,var.operation,actions)
1713    else
1714        local p = var.r
1715        if not p then -- todo: call special from reference code
1716            p = var.operation
1717            if p then -- no function and special check here. only numbers
1718                p = references.realpageofpage(tonumber(p))
1719            end
1720         -- if p then
1721         --     var.r = p
1722         -- end
1723        end
1724        return pdflinkpage(p or var.operation)
1725    end
1726end
1727
1728function specials.deltapage(var,actions)
1729    local p = tonumber(var.operation)
1730    if p then
1731        p = references.checkedrealpage(p + texgetcount(c_realpageno))
1732        return pdflinkpage(p)
1733    end
1734end
1735
1736-- sections
1737
1738function specials.section(var,actions)
1739    -- a bit duplicate
1740    local sectionname = var.arguments
1741    local destination = var.operation
1742    local internal    = structures.sections.internalreference(sectionname,destination)
1743    if internal then
1744        var.special   = "internal"
1745        var.operation = internal
1746        var.arguments = nil
1747        return specials.internal(var,actions)
1748    end
1749end
1750
1751-- todo, do this in references namespace ordered instead (this is an experiment)
1752
1753local splitter = lpeg.splitat(":")
1754
1755function specials.order(var,actions) -- references.specials !
1756    local operation = var.operation
1757    if operation then
1758        local kind, name, n = lpegmatch(splitter,operation)
1759        local order = structures.lists.ordered[kind]
1760        order = order and order[name]
1761        local v = order[tonumber(n)]
1762        local r = v and v.references.realpage
1763        if r then
1764            var.operation = r -- brrr, but test anyway
1765            return specials.page(var,actions)
1766        end
1767    end
1768end
1769
1770function specials.url(var,actions)
1771    return pdfurllink(references.checkedurl(var.operation),var.arguments,nil,actions)
1772end
1773
1774function specials.file(var,actions)
1775    return pdffilelink(references.checkedfile(var.operation),var.arguments,nil,actions)
1776end
1777
1778function specials.fileorurl(var,actions)
1779    local file, url = references.checkedfileorurl(var.operation,var.operation)
1780    if file then
1781        return pdffilelink(file,var.arguments,nil,actions)
1782    elseif url then
1783        return pdfurllink(url,var.arguments,nil,actions)
1784    end
1785end
1786
1787function specials.program(var,content)
1788    local program = references.checkedprogram(var.operation)
1789    return pdflaunch(program,var.arguments)
1790end
1791
1792function specials.javascript(var)
1793    return pdfjavascript(var.operation,var.arguments)
1794end
1795
1796specials.JS = specials.javascript
1797
1798function specials.highlight(var)
1799    -- this is just a signal (for now as we piggy back on normal references)
1800 -- return pdfhighlight(var.operation,var.arguments)
1801end
1802
1803executers.importform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ImportFDF") }
1804executers.exportform  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ExportFDF") }
1805executers.first       = pdfdictionary { S = pdf_named, N = pdfconstant("FirstPage") }
1806executers.previous    = pdfdictionary { S = pdf_named, N = pdfconstant("PrevPage") }
1807executers.next        = pdfdictionary { S = pdf_named, N = pdfconstant("NextPage") }
1808executers.last        = pdfdictionary { S = pdf_named, N = pdfconstant("LastPage") }
1809executers.backward    = pdfdictionary { S = pdf_named, N = pdfconstant("GoBack") }
1810executers.forward     = pdfdictionary { S = pdf_named, N = pdfconstant("GoForward") }
1811executers.print       = pdfdictionary { S = pdf_named, N = pdfconstant("Print") }
1812executers.exit        = pdfdictionary { S = pdf_named, N = pdfconstant("Quit") }
1813executers.close       = pdfdictionary { S = pdf_named, N = pdfconstant("Close") }
1814executers.save        = pdfdictionary { S = pdf_named, N = pdfconstant("Save") }
1815executers.savenamed   = pdfdictionary { S = pdf_named, N = pdfconstant("SaveAs") }
1816executers.opennamed   = pdfdictionary { S = pdf_named, N = pdfconstant("Open") }
1817executers.help        = pdfdictionary { S = pdf_named, N = pdfconstant("HelpUserGuide") }
1818executers.toggle      = pdfdictionary { S = pdf_named, N = pdfconstant("FullScreen") }
1819executers.search      = pdfdictionary { S = pdf_named, N = pdfconstant("Find") }
1820executers.searchagain = pdfdictionary { S = pdf_named, N = pdfconstant("FindAgain") }
1821executers.gotopage    = pdfdictionary { S = pdf_named, N = pdfconstant("GoToPage") }
1822executers.query       = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:Query") }
1823executers.queryagain  = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:NextHit") }
1824executers.fitwidth    = pdfdictionary { S = pdf_named, N = pdfconstant("FitWidth") }
1825executers.fitheight   = pdfdictionary { S = pdf_named, N = pdfconstant("FitHeight") }
1826
1827local function fieldset(arguments)
1828    -- [\dogetfieldset{#1}]
1829    return nil
1830end
1831
1832function executers.resetform(arguments)
1833    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
1834    return pdfdictionary {
1835        S     = pdfconstant("ResetForm"),
1836        Field = fieldset(arguments[1])
1837    }
1838end
1839
1840local formmethod = "post" -- "get" "post"
1841local formformat = "xml"  -- "xml" "html" "fdf"
1842
1843-- bit 3 = html bit 6 = xml bit 4 = get
1844
1845local flags = {
1846    get = {
1847        html = 12, fdf = 8, xml = 40,
1848    },
1849    post = {
1850        html = 4, fdf = 0, xml = 32,
1851    }
1852}
1853
1854function executers.submitform(arguments)
1855    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
1856    local flag = flags[formmethod] or flags.post
1857    flag = (flag and (flag[formformat] or flag.xml)) or 32 -- default: post, xml
1858    return pdfdictionary {
1859        S     = pdfconstant("SubmitForm"),
1860        F     = arguments[1],
1861        Field = fieldset(arguments[2]),
1862        Flags = flag,
1863    -- \PDFsubmitfiller
1864    }
1865end
1866
1867local pdf_hide = pdfconstant("Hide")
1868
1869function executers.hide(arguments)
1870    return pdfdictionary {
1871        S = pdf_hide,
1872        H = true,
1873        T = arguments,
1874    }
1875end
1876
1877function executers.show(arguments)
1878    return pdfdictionary {
1879        S = pdf_hide,
1880        H = false,
1881        T = arguments,
1882    }
1883end
1884
1885function specials.action(var)
1886    local operation = var.operation
1887    if var.operation and operation ~= "" then
1888        local e = executers[operation]
1889        if type(e) == "table" then
1890            return e
1891        elseif type(e) == "function" then
1892            return e(var.arguments)
1893        end
1894    end
1895end
1896
1897local function build(levels,start,parent,method,nested)
1898    local startlevel = levels[start].level
1899    local noflevels  = #levels
1900    local i = start
1901    local n = 0
1902    local child, entry, m, prev, first, last, f, l
1903    while i and i <= noflevels do
1904        local current = levels[i]
1905        if current.usedpage == false then
1906            -- safeguard
1907            i = i + 1
1908        else
1909            local level     = current.level
1910            local title     = current.title
1911            local reference = current.reference
1912            local opened    = current.opened
1913            local reftype   = type(reference)
1914            local block     = nil
1915            local variant   = "unknown"
1916            if reftype == "table" then
1917                -- we're okay
1918                variant  = "list"
1919                block    = reference.block
1920                realpage = reference.realpage
1921            elseif reftype == "string" then
1922                local resolved = references.identify("",reference)
1923                realpage = resolved and structures.references.setreferencerealpage(resolved) or 0
1924                if realpage > 0 then
1925                    variant   = "realpage"
1926                    realpage  = realpage
1927                    reference = structures.pages.collected[realpage]
1928                    block     = reference and reference.block
1929                end
1930            elseif reftype == "number" then
1931                if reference > 0 then
1932                    variant   = "realpage"
1933                    realpage  = reference
1934                    reference = structures.pages.collected[realpage]
1935                    block     = reference and reference.block
1936                end
1937            else
1938                -- error
1939            end
1940            current.block = block
1941            if variant == "unknown" then
1942                -- error, ignore
1943                i = i + 1
1944         -- elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].reference.block) then
1945            elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].block) then
1946                if nested then -- could be an option but otherwise we quit too soon
1947                    if entry then
1948                        pdfflushobject(child,entry)
1949                    else
1950                        report_bookmarks("error 1")
1951                    end
1952                    return i, n, first, last
1953                else
1954                    if i > 1 and block == levels[i-1].block then
1955                        -- or maybe never report
1956                        report_bookmarks("confusing level change at level %a around %a",level,title)
1957                    end
1958                    startlevel = level
1959                end
1960            end
1961            if level == startlevel then
1962                if trace_bookmarks then
1963                    report_bookmarks("%3i %w%s %s",realpage,(level-1)*2,(opened and "+") or "-",title)
1964                end
1965                local prev = child
1966                child = pdfreserveobject()
1967                if entry then
1968                    entry.Next = child and pdfreference(child)
1969                    pdfflushobject(prev,entry)
1970                end
1971                local action = nil
1972                if variant == "list" then
1973                    action = pdflinkinternal(reference.internal,reference.realpage)
1974                elseif variant == "realpage" then
1975                    action = pagereferences[realpage]
1976                else
1977                    -- hm, what to do
1978                end
1979                entry = pdfdictionary {
1980                    Title  = pdfunicode(title),
1981                    Parent = parent,
1982                    Prev   = prev and pdfreference(prev),
1983                    A      = action,
1984                }
1985             -- entry.Dest = pdflinkinternal(reference.internal,reference.realpage)
1986                if not first then
1987                    first, last = child, child
1988                end
1989                prev = child
1990                last = prev
1991                n = n + 1
1992                i = i + 1
1993            elseif i < noflevels and level > startlevel then
1994                i, m, f, l = build(levels,i,pdfreference(child),method,true)
1995                if entry then
1996                    entry.Count = (opened and m) or -m
1997                    if m > 0 then
1998                        entry.First = pdfreference(f)
1999                        entry.Last  = pdfreference(l)
2000                    end
2001                else
2002                    report_bookmarks("error 2")
2003                end
2004            else
2005                -- missing intermediate level but ok
2006                i, m, f, l = build(levels,i,pdfreference(child),method,true)
2007                if entry then
2008                    entry.Count = (opened and m) or -m
2009                    if m > 0 then
2010                        entry.First = pdfreference(f)
2011                        entry.Last  = pdfreference(l)
2012                    end
2013                    pdfflushobject(child,entry)
2014                else
2015                    report_bookmarks("error 3")
2016                end
2017                return i, n, first, last
2018            end
2019        end
2020    end
2021    pdfflushobject(child,entry)
2022    return nil, n, first, last
2023end
2024
2025function codeinjections.addbookmarks(levels,method)
2026    if levels and #levels > 0 then
2027        local parent = pdfreserveobject()
2028        local _, m, first, last = build(levels,1,pdfreference(parent),method or "internal",false)
2029        local dict = pdfdictionary {
2030            Type  = pdfconstant("Outlines"),
2031            First = pdfreference(first),
2032            Last  = pdfreference(last),
2033            Count = m,
2034        }
2035        pdfflushobject(parent,dict)
2036        pdfaddtocatalog("Outlines",lpdf.reference(parent))
2037    end
2038end
2039
2040-- this could also be hooked into the frontend finalizer
2041
2042lpdf.registerdocumentfinalizer(function() bookmarks.place() end,1,"bookmarks") -- hm, why indirect call
2043