lpdf-ini.lmt /size: 52 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-ini'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to lpdf-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- This file is the starting point for PDF related features. Although quite a bit
11-- evolved over time, most of what we do in MkIV and LMTX already was available in
12-- MkII (with e.g. pdfTeX) anyway, but it was implemented in TeX. We're talking of
13-- arbitrary annotations, media like audio and video, widgets (aka forms) with
14-- chains and appearances, comments, attachments, javascript based manipulation of
15-- layers, graphic trickery like shading, color spaces, transparancy, flash stuff,
16-- executing commands, accessing the interface, etc. In that respect there isn't
17-- much really new here, after all MkII was there before the turn of the century,
18-- but it's just more fun to maintain it in Lua than in low level TeX. Also, because
19-- we no longer deal with other engines, there is no need to go low level TeX, which
20-- makes for better code.
21--
22-- However, over the decades PDF evolved and it shows. For instance audio and video
23-- support changed and became worse. Some things were dropped (smil, flash, movies,
24-- audio). Using appearances for widgets became a pain because it sort of assumes
25-- that you construct these forms in acrobat which then leads to bugs becoming
26-- features which means that certain things simply don't work (initializations,
27-- chained widgets, funny dingabt defaults, etc), probably because they never were
28-- tested when viewers evolved.
29--
30-- Attachment are also a fragile bit. And comments that at some point became
31-- dependent on rendering annotations ... it all deserves no beauty price because
32-- reliable simplicity was replaced by unreliable complexity. Something that might
33-- work today often didn't in the past and might fail in the future, if only because
34-- it more relates to the viewer user interface, maybe changing security demands or
35-- whatever. We cannot predict this. A side effect is that we keep adapting and even
36-- worse, have to remove features that originally were expected to stay (media
37-- stuff). To some extend it's a waste of time to get it all supported, also because
38-- the open source viewers lag behind. It makes no sense to keep tons of code
39-- arround that will never be used (again).
40--
41-- Also, I don't think that these PDF features were added with something else than
42-- Acrobat in mind: a flexible system like TeX that actually could inject these low
43-- level features right from the moment that they showed up (and before they were
44-- fully tested) is not mainstream enough to be taken into account. One cannot blame
45-- a commercial product for its own priorities. The evolution of the web might also
46-- have interfered with the agendas.
47--
48-- As a consequence, the code that we use is spread over files and it might change
49-- over time as we try to adapt. But it's easy for the mentioned features to fix one
50-- aspect and break another. Eventually we might see more of these fancy features to
51-- be removed because they make no sense on the long run, than such features being
52-- added. In retrospect maybe many such features were just experiments: anchored in
53-- time for throw away documents (like presentations), never meant to be used on the
54-- long term. In that respect PDF is a disappointment.
55
56-- Comment: beware of "too many locals" problem here.
57
58-- Is there a way to prevent Acrobat to pop up this "Ask AI assistent" bar ... why
59-- should I want an summary of an image or even a text. Should we assume stupid human
60-- readers from now on? I really never longed for a summary.
61
62local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset
63local concat = table.concat
64local char, byte, format, sub, tohex = string.char, string.byte, string.format, string.sub, string.tohex
65local utfchar, utfbyte, utfvalues = utf.char, utf.byte, utf.values
66local sind, cosd, max, min = math.sind, math.cosd, math.max, math.min
67local sort, sortedhash = table.sort, table.sortedhash
68local P, C, R, S, Cc, Cs, V = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs, lpeg.V
69local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
70local formatters = string.formatters
71local isboolean = string.is_boolean
72local hextointeger, octtointeger = string.hextointeger,string.octtointeger
73
74local report_objects    = logs.reporter("backend","objects")
75local report_finalizing = logs.reporter("backend","finalizing")
76local report_blocked    = logs.reporter("backend","blocked")
77
78local implement         = interfaces and interfaces.implement
79local context           = context
80
81-- In ConTeXt MkIV we use utf8 exclusively so all strings get mapped onto a hex
82-- encoded utf16 string type between <>. We could probably save some bytes by using
83-- strings between () but then we end up with escaped ()\ characters too.
84
85pdf                     = type(pdf) == "table" and pdf or { }
86local factor            = number.dimenfactors.bp
87
88local pdfbackend        = backends and backends.registered.pdf or { }
89local codeinjections    = pdfbackend.codeinjections
90local nodeinjections    = pdfbackend.nodeinjections
91
92lpdf       = lpdf or { }
93local lpdf = lpdf
94lpdf.flags = lpdf.flags or { } -- will be filled later
95
96table.setmetatableindex(lpdf, function(t,k)
97    report_blocked("function %a is not accessible",k)
98    os.exit()
99end)
100
101local trace_finalizers = false  trackers.register("backend.finalizers", function(v) trace_finalizers = v end)
102local trace_resources  = false  trackers.register("backend.resources",  function(v) trace_resources  = v end)
103
104    -- helpers
105
106local f_hex_4 = formatters["%04X"]
107local f_hex_2 = formatters["%02X"]
108
109local h_hex_4 = table.setmetatableindex(function(t,k) -- we already have this somewhere
110    if k < 0 then
111     --report("fatal h_hex_4 error: %i",k)
112        return "0000"
113    elseif k < 256 then -- maybe 512
114        -- not sparse in this range
115        for i=0,255 do
116            t[i] = f_hex_4(i)
117        end
118        return t[k]
119    else
120        local v = f_hex_4(k)
121        t[k] = v
122        return v
123    end
124end)
125
126local h_hex_2 = table.setmetatableindex(function(t,k) -- we already have this somewhere
127    if type(k) == "string" then
128        local v = f_hex_2(byte(k))
129        t[k] = v
130        return v
131    elseif k < 0 or k > 255 then
132     -- report("fatal h_hex_2 error: %i",k)
133        return "00"
134    else
135        local v = f_hex_2(k)
136        t[k] = v
137        return v
138    end
139end)
140
141lpdf.h_hex_2 = h_hex_2
142lpdf.h_hex_4 = h_hex_4
143
144
145do
146
147    -- This is for a future feature (still under investigation and consideration). So,
148    -- it is work in progress (and brings a harmless overhead for now).
149
150    local initializers = { }
151
152    function lpdf.registerinitializer(initialize)
153        initializers[#initializers+1] = initialize
154    end
155
156    function lpdf.initialize(f)
157        for i=1,#initializers do
158            initializers[i]()
159        end
160    end
161
162end
163
164local pdfreserveobject
165local pdfimmediateobject
166
167updaters.register("backends.pdf.latebindings",function()
168    pdfreserveobject   = lpdf.reserveobject
169    pdfimmediateobject = lpdf.immediateobject
170end)
171
172do
173
174    local pdfgetmatrix, pdfhasmatrix, pdfgetpos
175
176    updaters.register("backends.pdf.latebindings",function()
177        job.positions.registerhandlers {
178            getpos  = drivers.getpos,
179            getrpos = drivers.getrpos,
180            gethpos = drivers.gethpos,
181            getvpos = drivers.getvpos,
182        }
183        pdfgetmatrix = lpdf.getmatrix
184        pdfhasmatrix = lpdf.hasmatrix
185        pdfgetpos    = drivers.getpos
186    end)
187
188    function lpdf.getpos() return pdfgetpos() end
189
190    local function corners(llx,lly,urx,ury,rx,sx,sy,ry)
191        return
192            llx * rx + lly * sy, llx * sx + lly * ry,
193            urx * rx + lly * sy, urx * sx + lly * ry,
194            urx * rx + ury * sy, urx * sx + ury * ry,
195            llx * rx + ury * sy, llx * sx + ury * ry
196    end
197
198    local function bounds(llx,lly,urx,ury,rx,sx,sy,ry)
199        local x1 = llx * rx + lly * sy
200        local y1 = llx * sx + lly * ry
201        local x2 = urx * rx + lly * sy
202        local y2 = urx * sx + lly * ry
203        local x3 = urx * rx + ury * sy
204        local y3 = urx * sx + ury * ry
205        local x4 = llx * rx + ury * sy
206        local y4 = llx * sx + ury * ry
207        llx = min(x1,x2,x3,x4)
208        lly = min(y1,y2,y3,y4)
209        urx = max(x1,x2,x3,x4)
210        ury = max(y1,y2,y3,y4)
211        return llx, lly, urx, ury
212    end
213
214 -- function lpdf.transform(llx,lly,urx,ury) -- not yet used so unchecked
215 --     if pdfhasmatrix() then
216 --         local sx, rx, ry, sy = pdfgetmatrix()
217 --         return bounds(llx,lly,urx,ury,sx,rx,ry,sy)
218 --     else
219 --         return llx, lly, urx, ury
220 --     end
221 -- end
222
223    function lpdf.rectangle(width,height,depth,offset)
224        local tx, ty = pdfgetpos()
225        if offset then
226            tx     = tx     -   offset -- hm, not transformed
227            ty     = ty     +   offset -- hm, not transformed
228            width  = width  + 2*offset
229            height = height +   offset
230            depth  = depth  +   offset
231        end
232        if pdfhasmatrix() then
233            local rx, sx, sy, ry = pdfgetmatrix()
234            local llx, lly, urx, ury = bounds(0,-depth,width,height,rx,sx,sy,ry)
235            tx = tx + sx * height
236            ty = ty - rx * height + height
237            return
238                factor * (tx + llx),
239                factor * (ty + lly),
240                factor * (tx + urx),
241                factor * (ty + ury)
242        else
243            return
244                factor *  tx,          -- llx
245                factor * (ty - depth), -- lly
246                factor * (tx + width), -- urx
247                factor * (ty + height) -- ury
248        end
249    end
250
251    function lpdf.quads(zero,width,height,depth,offset,noscale)
252        local tx, ty = pdfgetpos()
253        if offset then
254            tx     = tx     -   offset -- hm, not transformed
255            ty     = ty     +   offset -- hm, not transformed
256            width  = width  + 2*offset
257            height = height +   offset
258            depth  = depth  +   offset
259        end
260        if pdfhasmatrix() then
261            local rx, sx, sy, ry = pdfgetmatrix()
262            local x1, y1, x2, y2, x3, y3, x4, y4 = corners(zero,-depth,width,height,rx,sx,sy,ry)
263            tx = tx + sx * height
264            ty = ty - rx * height + height
265            return
266                factor * (tx + x1), factor * (ty + y1),
267                factor * (tx + x2), factor * (ty + y2),
268                factor * (tx + x3), factor * (ty + y3),
269                factor * (tx + x4), factor * (ty + y4),
270                zero
271        else
272            local llx = factor *  tx
273            local lly = factor * (ty - depth)
274            local urx = factor * (tx + width)
275            local ury = factor * (ty + height)
276            return llx, lly, urx, lly, urx, ury, llx, ury, 0
277        end
278    end
279
280end
281
282local tosixteen, fromsixteen, topdfdoc, frompdfdoc, toeight, fromeight
283
284do
285
286    local cache = table.setmetatableindex(function(t,k) -- can be made weak
287        local v = utfbyte(k)
288        if v < 0x10000 then
289            v = format("%04x",v)
290        else
291            v = v - 0x10000
292            v = format("%04x%04x",(v>>10)+0xD800,v%1024+0xDC00)
293        end
294        t[k] = v
295        return v
296    end)
297
298    local unified = Cs(Cc("<feff") * (lpeg.patterns.utf8character/cache)^1 * Cc(">"))
299
300    tosixteen = function(str) -- an lpeg might be faster (no table)
301        if not str or str == "" then
302            return "<feff>" -- not () as we want an indication that it's unicode
303        else
304            return lpegmatch(unified,str)
305        end
306    end
307
308    -- we could make a helper for this
309
310    local more = 0
311
312    local pattern = C(4) / function(s) -- needs checking !
313        local now = hextointeger(s)
314        if more > 0 then
315            now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000
316            more = 0
317            return utfchar(now)
318        elseif now >= 0xD800 and now <= 0xDBFF then
319            more = now
320            return "" -- else the c's end up in the stream
321        else
322            return utfchar(now)
323        end
324    end
325
326    local pattern = P(true) / function() more = 0 end * Cs(pattern^0)
327
328    fromsixteen = function(str)
329        if not str or str == "" then
330            return ""
331        else
332            return lpegmatch(pattern,str)
333        end
334    end
335
336    local toregime   = regimes and regimes.toregime
337    local fromregime = regimes and regimes.fromregime
338    local escaped    = Cs(
339        Cc("(")
340      * (
341            S("()\n\r\t\b\f")/"\\%0"
342          + P("\\")/"\\\\"
343          + P(1)
344        )^0
345      * Cc(")")
346    )
347
348    topdfdoc = function(str,default)
349        if not str or str == "" then
350            return ""
351        else
352            return lpegmatch(escaped,toregime("pdfdoc",str,default)) -- could be combined if needed
353        end
354    end
355
356    frompdfdoc = function(str)
357        if not str or str == "" then
358            return ""
359        else
360            return fromregime("pdfdoc",str)
361        end
362    end
363
364    if not toregime   then topdfdoc   = function(s) return s end end
365    if not fromregime then frompdfdoc = function(s) return s end end
366
367    toeight = function(str)
368        if not str or str == "" then
369            return "()"
370        else
371            return lpegmatch(escaped,str)
372        end
373    end
374
375    -- use an oct hash
376
377    local unescape = Cs((
378       P("\\")/"" * (
379           S("()")
380         + S("\n\r")^1 / ""
381         + S("nrtbf") / { n = "\n", r = "\r", t = "\t", b = "\b", f = "\f" }
382         + (lpegpatterns.octdigit * lpegpatterns.octdigit^-2) / function(s) return char(octtointeger(s)) end
383       )
384     + P("\\\\") / "\\"
385     + P(1)
386--      - P(")") -- when inlined
387     )^0)
388
389    fromeight = function(str)
390        if not str or str == "" then
391            return ""
392        else
393            return lpegmatch(unescape,str)
394        end
395    end
396
397    lpegpatterns.pdffromeight = unescape
398
399--     local u_pattern = lpegpatterns.utfbom_16_be * lpegpatterns.utf16_to_utf8_be -- official
400--                     + lpegpatterns.utfbom_16_le * lpegpatterns.utf16_to_utf8_le -- we've seen these
401
402--     local h_pattern = lpegpatterns.hextobytes
403
404--     local zero = S(" \n\r\t") + P("\\ ")
405--     local one  = C(4)
406--     local two  = P("d") * R("89","af") * C(2) * C(4)
407
408--     local x_pattern = P { "start",
409--         start     = V("wrapped") + V("unwrapped") + V("original"),
410--         original  = Cs(P(1)^0),
411--         wrapped   = P("<") * V("unwrapped") * P(">") * P(-1),
412--         unwrapped = P("feff")
413--                   * Cs( (
414--                         zero  / ""
415--                       + two   / function(a,b)
416--                                     a = (hextointeger(a) - 0xD800) * 1024
417--                                     b = (hextointeger(b) - 0xDC00)
418--                                     return utfchar(a+b)
419--                                 end
420--                       + one   / function(a)
421--                                     return utfchar(hextointeger(a))
422--                                 end
423--                     )^1 ) * P(-1)
424--                   + P("fffe")
425--                   * Cs( (
426--                         zero  / ""
427--                       + two   / function(b,a)
428--                                     a = (hextointeger(a) - 0xD800) * 1024
429--                                     b = (hextointeger(b) - 0xDC00)
430--                                     return utfchar(a+b)
431--                                 end
432--                       + one   / function(a)
433--                                     return utfchar(hextointeger(a))
434--                                 end
435--                     )^1 ) * P(-1)
436--     }
437
438--     function lpdf.frombytes(s,ishex,isutf)
439--         if not s or s == "" then
440--             return ""
441--         elseif ishex then
442--             local x = lpegmatch(x_pattern,s)
443--             if x then
444--                 return x
445--             end
446--             local h = lpegmatch(h_pattern,s)
447--             if h then
448--                 return h
449--             end
450--         else
451--             local u = lpegmatch(u_pattern,s)
452--             if u then
453--                 return u
454--             end
455--         end
456--         return lpegmatch(unescape,s)
457--     end
458
459    local encodingvalues = pdfe.getencodingvalues()
460
461    encodingvalues = table.swapped(encodingvalues,encodingvalues)
462
463    lpdf.encodingvalues = encodingvalues
464
465    local utf16flags  = encodingvalues.utf16be | encodingvalues.utf16le
466    local utf16toutf8 = string.utf16toutf8
467
468    function lpdf.frombytes(s,flags)
469        if not s or s == "" then
470            return ""
471        elseif flags & utf16flags ~= 0 then
472-- print("original",s)
473-- print("utf8    ",utf16toutf8(s))
474            return utf16toutf8(s)
475        else
476-- print("original",s)
477            return s
478        end
479    end
480
481    lpdf.tosixteen   = tosixteen
482    lpdf.toeight     = toeight
483    lpdf.topdfdoc    = topdfdoc
484    lpdf.fromsixteen = fromsixteen
485    lpdf.fromeight   = fromeight
486    lpdf.frompdfdoc  = frompdfdoc
487
488end
489
490local pdfescaped do
491
492    local replacer = S("\0\t\n\r\f ()[]{}/%%#\\") / {
493        ["\00"]="#00",
494        ["\09"]="#09",
495        ["\10"]="#0a",
496        ["\12"]="#0c",
497        ["\13"]="#0d",
498        [ " " ]="#20",
499        [ "#" ]="#23",
500        [ "%" ]="#25",
501        [ "(" ]="#28",
502        [ ")" ]="#29",
503        [ "/" ]="#2f",
504        [ "[" ]="#5b",
505        [ "\\"]="#5c",
506        [ "]" ]="#5d",
507        [ "{" ]="#7b",
508        [ "}" ]="#7d",
509    } + P(1)
510
511    local p_escaped_1 = Cs(Cc("/") * replacer^0)
512    local p_escaped_2 = Cs(          replacer^0)
513
514    pdfescaped = function(str,slash)
515        return lpegmatch(slash and p_escaped_1 or p_escaped_2,str) or str
516    end
517
518    lpdf.escaped = pdfescaped
519
520end
521
522local tostring_a, tostring_d
523
524do
525
526    local f_key_null       = formatters["%s null"]
527    local f_key_value      = formatters["%s %s"]
528 -- local f_key_dictionary = formatters["%s << % t >>"]
529 -- local f_dictionary     = formatters["<< % t >>"]
530    local f_key_dictionary = formatters["%s << %s >>"]
531    local f_dictionary     = formatters["<< %s >>"]
532 -- local f_key_array      = formatters["%s [ % t ]"]
533 -- local f_array          = formatters["[ % t ]"]
534    local f_key_array      = formatters["%s [ %s ]"]
535    local f_array          = formatters["[ %s ]"]
536    local f_key_number     = formatters["%s %N"]  -- always with max 9 digits and integer is possible
537    local f_tonumber       = formatters["%N"]     -- always with max 9 digits and integer is possible
538
539    tostring_d = function(t,contentonly,key)
540        if next(t) then
541            local r = { }
542            local n = 0
543            local e
544            for k, v in next, t do
545                if k == "__extra__" then
546                    e = v
547                elseif k == "__stream__" then
548                    -- do nothing (yet)
549                else
550                    n = n + 1
551                    r[n] = k
552                end
553            end
554            if n > 1 then
555                sort(r)
556            end
557            for i=1,n do
558                local k  = r[i]
559                local v  = t[k]
560                local tv = type(v)
561                -- mostly tables
562                --
563                k = pdfescaped(k,true)
564                --
565                if tv == "table" then
566                 -- local mv = getmetatable(v)
567                 -- if mv and mv.__lpdftype then
568                    if v.__lpdftype__ then
569                     -- if v == t then
570                     --     report_objects("ignoring circular reference in dictionary")
571                     --     r[i] = f_key_null(k)
572                     -- else
573                            r[i] = f_key_value(k,tostring(v))
574                     -- end
575                    elseif v[1] then
576                        r[i] = f_key_value(k,tostring_a(v))
577                    else
578                        r[i] = f_key_value(k,tostring_d(v))
579                    end
580                elseif tv == "string" then
581                    r[i] = f_key_value(k,toeight(v))
582                elseif tv == "number" then
583                    r[i] = f_key_number(k,v)
584                else
585                    r[i] = f_key_value(k,tostring(v))
586                end
587            end
588            if e then
589                r[n+1] = e
590            end
591            r = concat(r," ")
592            if contentonly then
593                return r
594            elseif key then
595                return f_key_dictionary(pdfescaped(key,true),r)
596            else
597                return f_dictionary(r)
598            end
599        elseif contentonly then
600            return ""
601        else
602            return "<< >>"
603        end
604    end
605
606    tostring_a = function(t,contentonly,key)
607        local tn = #t
608        if tn ~= 0 then
609            local r = { }
610            for k=1,tn do
611                local v = t[k]
612                local tv = type(v)
613                -- mostly numbers and tables
614                if tv == "number" then
615                    r[k] = f_tonumber(v)
616                elseif tv == "table" then
617                 -- local mv = getmetatable(v)
618                 -- if mv and mv.__lpdftype then
619                    if v.__lpdftype__ then
620                     -- if v == t then
621                     --     report_objects("ignoring circular reference in array")
622                     --     r[k] = "null"
623                     -- else
624                            r[k] = tostring(v)
625                     -- end
626                    elseif v[1] then
627                        r[k] = tostring_a(v)
628                    else
629                        r[k] = tostring_d(v)
630                    end
631                elseif tv == "string" then
632                    r[k] = toeight(v)
633                else
634                    r[k] = tostring(v)
635                end
636            end
637            local e = t.__extra__
638            if e then
639                r[tn+1] = e
640            end
641            r = concat(r," ")
642            if contentonly then
643                return r
644            elseif key then
645                return f_key_array(pdfescaped(key,true),r)
646            else
647                return f_array(r)
648            end
649        elseif contentonly then
650            return ""
651        else
652            return "[ ]"
653        end
654    end
655
656end
657
658local f_tonumber = formatters["%N"]
659
660local tostring_x = function(t) return concat(t," ")       end
661local tostring_s = function(t) return toeight(t[1])       end
662local tostring_p = function(t) return topdfdoc(t[1],t[2]) end
663local tostring_u = function(t) return tosixteen(t[1])     end
664----- tostring_n = function(t) return tostring(t[1])      end -- tostring not needed
665local tostring_n = function(t) return f_tonumber(t[1])    end -- tostring not needed
666local tostring_c = function(t) return t[1]                end -- already prefixed (hashed)
667local tostring_z = function()  return "null"              end
668local tostring_t = function()  return "true"              end
669local tostring_f = function()  return "false"             end
670local tostring_r = function(t) local n = t[1] return n and n > 0 and (n .. " 0 R") or "null" end
671
672local tostring_v = function(t)
673    local s = t[1]
674    if type(s) == "table" then
675        return concat(s)
676    else
677        return s
678    end
679end
680
681local tostring_l = function(t)
682    local s = t[1]
683    if not s or s == "" then
684        return "()"
685    elseif t[2] then
686        return "<" .. s .. ">"
687    else
688        -- we could convert to hex
689        return toeight(s)
690    end
691end
692
693local function value_x(t) return t                  end
694local function value_s(t) return t[1]               end
695local function value_p(t) return t[1]               end
696local function value_u(t) return t[1]               end
697local function value_n(t) return t[1]               end
698local function value_c(t) return sub(t[1],2)        end
699local function value_d(t) return tostring_d(t,true) end
700local function value_a(t) return tostring_a(t,true) end
701local function value_z()  return nil                end
702local function value_t(t) return t.value or true    end
703local function value_f(t) return t.value or false   end
704local function value_r(t) return t[1] or 0          end -- null
705local function value_v(t) return t[1]               end
706local function value_l(t) return t[1]               end
707
708local function add_to_d(t,v)
709    local k = type(v)
710    if k == "string" then
711        if t.__extra__ then
712            t.__extra__ = t.__extra__ .. " " .. v
713        else
714            t.__extra__ = v
715        end
716    elseif k == "table" then
717        for kk, vv in next, v do
718            local tkk = rawget(t,kk)
719            if tkk and tostring(tkk) ~= tostring(vv) then
720                report_objects("duplicate key %a with different values",kk)
721            end
722            t[kk] = vv
723        end
724    end
725    return t
726end
727
728local function add_to_a(t,v)
729    local k = type(v)
730    if k == "string" then
731        if t.__extra__ then
732            t.__extra__ = t.__extra__ .. " " .. v
733        else
734            t.__extra__ = v
735        end
736    elseif k == "table" then
737        local n = #t
738        for i=1,#v do
739            n = n + 1
740            t[n] = v[i]
741        end
742    end
743    return t
744end
745
746local function add_x(t,k,v) rawset(t,k,tostring(v)) end
747
748local mt_x = { __index = { __lpdftype__ = "stream"     }, __tostring = tostring_x, __call = value_x, __newindex = add_x }
749local mt_d = { __index = { __lpdftype__ = "dictionary" }, __tostring = tostring_d, __call = value_d, __add = add_to_d }
750local mt_a = { __index = { __lpdftype__ = "array"      }, __tostring = tostring_a, __call = value_a, __add = add_to_a }
751local mt_u = { __index = { __lpdftype__ = "unicode"    }, __tostring = tostring_u, __call = value_u }
752local mt_s = { __index = { __lpdftype__ = "string"     }, __tostring = tostring_s, __call = value_s }
753local mt_p = { __index = { __lpdftype__ = "docstring"  }, __tostring = tostring_p, __call = value_p }
754local mt_n = { __index = { __lpdftype__ = "number"     }, __tostring = tostring_n, __call = value_n }
755local mt_c = { __index = { __lpdftype__ = "constant"   }, __tostring = tostring_c, __call = value_c }
756local mt_z = { __index = { __lpdftype__ = "null"       }, __tostring = tostring_z, __call = value_z }
757local mt_t = { __index = { __lpdftype__ = "true"       }, __tostring = tostring_t, __call = value_t }
758local mt_f = { __index = { __lpdftype__ = "false"      }, __tostring = tostring_f, __call = value_f }
759local mt_r = { __index = { __lpdftype__ = "reference"  }, __tostring = tostring_r, __call = value_r }
760local mt_v = { __index = { __lpdftype__ = "verbose"    }, __tostring = tostring_v, __call = value_v }
761local mt_l = { __index = { __lpdftype__ = "literal"    }, __tostring = tostring_l, __call = value_l }
762
763local function pdfstream(t) -- we need to add attributes
764    if t then
765        local tt = type(t)
766        if tt == "table" then
767            for i=1,#t do
768                t[i] = tostring(t[i])
769            end
770        elseif tt == "string" then
771            t = { t }
772        else
773            t = { tostring(t) }
774        end
775    end
776    return setmetatable(t or { },mt_x)
777end
778
779local function pdfdictionary(t)
780    return setmetatable(t or { },mt_d)
781end
782
783local function pdfarray(t)
784    if type(t) == "string" then
785        return setmetatable({ t },mt_a)
786    else
787        return setmetatable(t or { },mt_a)
788    end
789end
790
791local function pdfstring(str,default)
792    return setmetatable({ str or default or "" },mt_s)
793end
794
795local function pdfdocstring(str,default,defaultchar)
796    return setmetatable({ str or default or "", defaultchar or " " },mt_p)
797end
798
799local function pdfunicode(str,default)
800    return setmetatable({ str or default or "" },mt_u) -- could be a string
801end
802
803local function pdfliteral(str,hex) -- can also produce a hex <> instead of () literal
804    return setmetatable({ str, hex },mt_l)
805end
806
807local pdfnumber, pdfconstant
808
809do
810
811    local cache = { } -- can be weak
812
813    pdfnumber = function(n,default) -- 0-10
814        if not n then
815            n = default
816        end
817        local c = cache[n]
818        if not c then
819            c = setmetatable({ n },mt_n)
820        --  cache[n] = c -- too many numbers
821        end
822        return c
823    end
824
825    for i=-1,9 do cache[i] = pdfnumber(i) end
826
827    local escaped = lpdf.escaped
828
829    local cache = table.setmetatableindex(function(t,k)
830        local v = setmetatable({ escaped(k,true) }, mt_c)
831        t[k] = v
832        return v
833    end)
834
835    pdfconstant = function(str,default)
836        if not str then
837            str = default or "none"
838        end
839        return cache[str]
840    end
841
842end
843
844local pdfnull, pdfboolean, pdfreference, pdfverbose
845
846do
847
848    local p_null  = { } setmetatable(p_null, mt_z)
849    local p_true  = { } setmetatable(p_true, mt_t)
850    local p_false = { } setmetatable(p_false,mt_f)
851
852    pdfnull = function()
853        return p_null
854    end
855
856    pdfboolean = function(b,default)
857        if type(b) == "boolean" then
858            return b and p_true or p_false
859        else
860            return default and p_true or p_false
861        end
862    end
863
864    -- print(pdfboolean(false),pdfboolean(false,false),pdfboolean(false,true))
865    -- print(pdfboolean(true),pdfboolean(true,false),pdfboolean(true,true))
866    -- print(pdfboolean(nil,true),pdfboolean(nil,false))
867
868    local r_zero = setmetatable({ 0 },mt_r)
869
870    pdfreference = function(r)  -- maybe make a weak table
871        if r and r ~= 0 then
872            return setmetatable({ r },mt_r)
873        else
874            return r_zero
875        end
876    end
877
878    local v_zero  = setmetatable({ 0  },mt_v)
879    local v_empty = setmetatable({ "" },mt_v)
880
881    pdfverbose = function(t) -- maybe check for type
882        if t == 0 then
883            return v_zero
884        elseif t == "" then
885            return v_empty
886        else
887            return setmetatable({ t },mt_v)
888        end
889    end
890
891end
892
893lpdf.stream      = pdfstream -- THIS WILL PROBABLY CHANGE
894lpdf.dictionary  = pdfdictionary
895lpdf.array       = pdfarray
896lpdf.docstring   = pdfdocstring
897lpdf.string      = pdfstring
898lpdf.unicode     = pdfunicode
899lpdf.number      = pdfnumber
900lpdf.constant    = pdfconstant
901lpdf.null        = pdfnull
902lpdf.boolean     = pdfboolean
903lpdf.reference   = pdfreference
904lpdf.verbose     = pdfverbose
905lpdf.literal     = pdfliteral
906
907
908do
909
910    local options = {
911     -- unknown  = 0x0001, -- bit  1
912     -- unknown  = 0x0002, -- bit  2
913        print    = 0x0004, -- bit  3
914        modify   = 0x0008, -- bit  4
915        extract  = 0x0010, -- bit  5
916        add      = 0x0020, -- bit  6
917     -- unknown  = 0x0040, -- bit  7
918     -- unknown  = 0x0080, -- bit  8
919        fillin   = 0x0100, -- bit  9
920        access   = 0x0200, -- bit 10
921        assemble = 0x0400, -- bit 11
922        quality  = 0x0800, -- bit 12
923     -- unknown  = 0x1000, -- bit 13
924     -- unknown  = 0x2000, -- bit 14
925     -- unknown  = 0x4000, -- bit 15
926     -- unknown  = 0x8000, -- bit 16
927    }
928
929    lpdf.permissions = options
930
931    function lpdf.topermissions(permissions)
932        if permissions and permissions > 0 then
933            local t = { }
934            for k, v in next, options do
935                if permissions & v ~= 0 then
936                    t[k] = true
937                end
938            end
939            return t
940        end
941    end
942
943end
944
945if not callbacks then return lpdf end
946
947-- three priority levels, default=2
948
949local pagefinalizers     = { { }, { }, { } }
950local documentfinalizers = { { }, { }, { } }
951
952local pageresources, pageattributes, pagesattributes
953
954local function resetpageproperties()
955    pageresources   = pdfdictionary()
956    pageattributes  = pdfdictionary()
957    pagesattributes = pdfdictionary()
958end
959
960function lpdf.getpageproperties()
961    return {
962        pageresources   = pageresources,
963        pageattributes  = pageattributes,
964        pagesattributes = pagesattributes,
965    }
966end
967
968resetpageproperties()
969
970lpdf.registerinitializer(resetpageproperties)
971
972local function addtopageresources  (k,v) pageresources  [k] = v end
973local function addtopageattributes (k,v) pageattributes [k] = v end
974local function addtopagesattributes(k,v) pagesattributes[k] = v end
975
976lpdf.addtopageresources   = addtopageresources
977lpdf.addtopageattributes  = addtopageattributes
978lpdf.addtopagesattributes = addtopagesattributes
979
980local function set(where,what,f,when,comment)
981    if type(when) == "string" then
982        when, comment = 2, when
983    elseif not when then
984        when = 2
985    end
986    local w = where[when]
987    w[#w+1] = { f, comment }
988    if trace_finalizers then
989        report_finalizing("%s set: [%s,%s]",what,when,#w)
990    end
991end
992
993local function run(where,what)
994    if trace_finalizers then
995        report_finalizing("start backend, category %a, n %a",what,#where)
996    end
997    for i=1,#where do
998        local w = where[i]
999        for j=1,#w do
1000            local wj = w[j]
1001            if trace_finalizers then
1002                report_finalizing("%s finalizer: [%s,%s] %s",what,i,j,wj[2] or "")
1003            end
1004            wj[1]()
1005        end
1006    end
1007    if trace_finalizers then
1008        report_finalizing("stop finalizing")
1009    end
1010end
1011
1012local function registerpagefinalizer(f,when,comment)
1013    set(pagefinalizers,"page",f,when,comment)
1014end
1015
1016local function registerdocumentfinalizer(f,when,comment)
1017    set(documentfinalizers,"document",f,when,comment)
1018end
1019
1020lpdf.registerpagefinalizer     = registerpagefinalizer
1021lpdf.registerdocumentfinalizer = registerdocumentfinalizer
1022
1023function lpdf.finalizepage(shipout)
1024    if shipout and not environment.initex then
1025     -- resetpageproperties() -- maybe better before
1026        run(pagefinalizers,"page")
1027        resetpageproperties() -- maybe better before
1028    end
1029end
1030
1031local finalized = false
1032
1033function lpdf.finalizedocument()
1034    if not environment.initex and not finalized then
1035        run(documentfinalizers,"document")
1036        finalized = true
1037    end
1038end
1039
1040callbacks.register("finish_pdfpage", lpdf.finalizepage)
1041callbacks.register("finish_pdffile", lpdf.finalizedocument)
1042
1043do
1044
1045    -- some minimal tracing, handy for checking the order
1046
1047    local function trace_set(what,key)
1048        if trace_resources then
1049            report_finalizing("setting key %a in %a",key,what)
1050        end
1051    end
1052
1053    local function trace_flush(what)
1054        if trace_resources then
1055            report_finalizing("flushing %a",what)
1056        end
1057    end
1058
1059    lpdf.protectresources = true
1060
1061    local catalog = pdfdictionary { Type = pdfconstant("Catalog") } -- nicer, but when we assign we nil the Type
1062    local info    = pdfdictionary { Type = pdfconstant("Info")    } -- nicer, but when we assign we nil the Type
1063    ----- names   = pdfdictionary { Type = pdfconstant("Names")   } -- nicer, but when we assign we nil the Type
1064
1065    local function checkcatalog()
1066        if not environment.initex then
1067            trace_flush("catalog")
1068            return true
1069        end
1070    end
1071
1072    local function checkinfo()
1073        if not environment.initex then
1074            trace_flush("info")
1075            if lpdf.majorversion() > 1 then
1076                for k, v in next, info do
1077                    if k == "CreationDate" or k == "ModDate" then
1078                        -- mandate >= 2.0
1079                    else
1080                        info[k] = nil
1081                    end
1082                end
1083            end
1084            return true
1085        end
1086    end
1087
1088    local function flushcatalog()
1089        if checkcatalog() then
1090            catalog.Type = nil
1091        end
1092    end
1093
1094    local function flushinfo()
1095        if checkinfo() then
1096            info.Type = nil
1097        end
1098    end
1099
1100    local userdata = nil
1101
1102    function lpdf.setuserdata(key,value)
1103        if not userdata then
1104            userdata = pdfdictionary()
1105        end
1106        userdata[key] = pdfunicode(tostring(value))
1107    end
1108
1109    function lpdf.getcatalog()
1110        if checkcatalog() then
1111            catalog.Type = pdfconstant("Catalog")
1112            -- A bit fuzzy and useless. Has to be a direct one (makes one wonder about what drives
1113            -- these demands, like limitations in certain aplications).
1114            catalog.Extensions = pdfdictionary {
1115                Type = pdfconstant("Extensions"),
1116                LMTX = pdfdictionary {
1117                    BaseVersion    = pdfconstant("2.0"),
1118                    ExtensionLevel = 1,
1119                }
1120            }
1121            local statistics = pdfdictionary {
1122                FontRegistries = lpdf.noffontregistries(),
1123            }
1124            if next(statistics) then
1125                -- we use this as fast way to check if we have to check for fonts
1126                -- and can use the fast path
1127                statistics.Type = pdfconstant("Statistics")
1128                catalog.LMTX_Statistics = pdfreference(pdfimmediateobject(tostring(statistics)))
1129            end
1130            --
1131            local labels = structures.pages.getlabels()
1132            if labels and next(labels) then
1133                for k, v in next, labels do
1134                    if type(v) == "table" then
1135                        labels[k] = pdfarray(v)
1136                    end
1137                end
1138                labels = pdfdictionary {
1139                    Type   = pdfconstant("Pages"),
1140                    Labels = pdfdictionary(labels),
1141                }
1142                catalog.LMTX_Pages = pdfreference(pdfimmediateobject(tostring(labels)))
1143            end
1144            --
1145            return pdfreference(pdfimmediateobject(tostring(catalog)))
1146        end
1147    end
1148
1149    function lpdf.getinfo()
1150        if checkinfo() then
1151            return pdfreference(pdfimmediateobject(tostring(info)))
1152        end
1153    end
1154
1155    function lpdf.addtocatalog(k,v)
1156        if not (lpdf.protectresources and catalog[k]) then
1157            trace_set("catalog",k)
1158            catalog[k] = v
1159        end
1160    end
1161
1162    function lpdf.addtoinfo(k,v)
1163        if not (lpdf.protectresources and info[k]) then
1164            trace_set("info",k)
1165            info[k] = v
1166        end
1167    end
1168
1169    local names = pdfdictionary {}
1170
1171    local function flushnames()
1172        if next(names) and not environment.initex then
1173         -- The type used to be present when we started with pdf but at some point disappeared from
1174         -- the specification, so let's ditch it. The Type key is kind of weird anyway as sometimes
1175         -- it is mandate and sometimes not.
1176         -- names.Type = pdfconstant("Names")
1177            trace_flush("names")
1178            lpdf.addtocatalog("Names",pdfreference(pdfimmediateobject(tostring(names))))
1179        end
1180    end
1181
1182    function lpdf.addtonames(k,v)
1183        if not (lpdf.protectresources and names[k]) then
1184            trace_set("names",  k)
1185            names  [k] = v
1186        end
1187    end
1188
1189    local r_extgstates, r_colorspaces, r_patterns, r_shades
1190    local d_extgstates, d_colorspaces, d_patterns, d_shades
1191    local p_extgstates, p_colorspaces, p_patterns, p_shades
1192
1193    lpdf.registerinitializer(function()
1194        r_extgstates = nil ; r_colorspaces = nil ; r_patterns = nil ; r_shades = nil ;
1195        d_extgstates = nil ; d_colorspaces = nil ; d_patterns = nil ; d_shades = nil ;
1196        p_extgstates = nil ; p_colorspaces = nil ; p_patterns = nil ; p_shades = nil ;
1197    end)
1198
1199    local function checkextgstates () if d_extgstates  then addtopageresources("ExtGState", p_extgstates ) end end
1200    local function checkcolorspaces() if d_colorspaces then addtopageresources("ColorSpace",p_colorspaces) end end
1201    local function checkpatterns   () if d_patterns    then addtopageresources("Pattern",   p_patterns   ) end end
1202    local function checkshades     () if d_shades      then addtopageresources("Shading",   p_shades     ) end end
1203
1204    local function flushextgstates () if d_extgstates  then trace_flush("extgstates")  pdfimmediateobject(r_extgstates, tostring(d_extgstates )) end end
1205    local function flushcolorspaces() if d_colorspaces then trace_flush("colorspaces") pdfimmediateobject(r_colorspaces,tostring(d_colorspaces)) end end
1206    local function flushpatterns   () if d_patterns    then trace_flush("patterns")    pdfimmediateobject(r_patterns,   tostring(d_patterns   )) end end
1207    local function flushshades     () if d_shades      then trace_flush("shades")      pdfimmediateobject(r_shades,     tostring(d_shades     )) end end
1208
1209    -- patterns are special as they need resources to so we can get recursive references and in that case
1210    -- acrobat doesn't show anything (other viewers handle it well)
1211    --
1212    -- todo: share them
1213    -- todo: force when not yet set
1214
1215    local pdfgetfontobjectnumber
1216
1217    updaters.register("backends.pdf.latebindings",function()
1218        pdfgetfontobjectnumber = lpdf.getfontobjectnumber
1219    end)
1220
1221    local f_font = formatters["%s%d"]
1222
1223    function lpdf.collectedresources(options)
1224        local ExtGState  = d_extgstates  and next(d_extgstates ) and p_extgstates
1225        local ColorSpace = d_colorspaces and next(d_colorspaces) and p_colorspaces
1226        local Pattern    = d_patterns    and next(d_patterns   ) and p_patterns
1227        local Shading    = d_shades      and next(d_shades     ) and p_shades
1228        local Font
1229        if options and options.patterns == false then
1230            Pattern = nil
1231        end
1232        local fonts = options and options.fonts
1233        if fonts and next(fonts) then
1234            local prefix = options.fontprefix or "F"
1235            Font = pdfdictionary { }
1236            for k, v in sortedhash(fonts) do
1237                Font[f_font(prefix,v)] = pdfreference(pdfgetfontobjectnumber(k))
1238            end
1239        end
1240        if ExtGState or ColorSpace or Pattern or Shading or Font then
1241            local collected = pdfdictionary {
1242                ExtGState  = ExtGState,
1243                ColorSpace = ColorSpace,
1244                Pattern    = Pattern,
1245                Shading    = Shading,
1246                Font       = Font,
1247            }
1248            if options and options.serialize == false then
1249                return collected
1250            else
1251                return collected()
1252            end
1253        elseif options and options.notempty then
1254            return nil
1255        elseif options and options.serialize == false then
1256            return pdfdictionary { }
1257        else
1258            return ""
1259        end
1260    end
1261
1262    function lpdf.adddocumentextgstate (k,v)
1263        if not d_extgstates then
1264            r_extgstates = pdfreserveobject()
1265            d_extgstates = pdfdictionary()
1266            p_extgstates = pdfreference(r_extgstates)
1267        end
1268        d_extgstates[k] = v
1269    end
1270
1271    function lpdf.adddocumentcolorspace(k,v)
1272        if not d_colorspaces then
1273            r_colorspaces = pdfreserveobject()
1274            d_colorspaces = pdfdictionary()
1275            p_colorspaces = pdfreference(r_colorspaces)
1276        end
1277        d_colorspaces[k] = v
1278    end
1279
1280    function lpdf.adddocumentpattern(k,v)
1281        if not d_patterns then
1282            r_patterns = pdfreserveobject()
1283            d_patterns = pdfdictionary()
1284            p_patterns = pdfreference(r_patterns)
1285        end
1286        d_patterns[k] = v
1287    end
1288
1289    function lpdf.adddocumentshade(k,v)
1290        if not d_shades then
1291            r_shades = pdfreserveobject()
1292            d_shades = pdfdictionary()
1293            p_shades = pdfreference(r_shades)
1294        end
1295        d_shades[k] = v
1296    end
1297
1298    registerdocumentfinalizer(flushextgstates,3,"extended graphic states")
1299    registerdocumentfinalizer(flushcolorspaces,3,"color spaces")
1300    registerdocumentfinalizer(flushpatterns,3,"patterns")
1301    registerdocumentfinalizer(flushshades,3,"shades")
1302
1303    registerdocumentfinalizer(flushnames,3,"names") -- before catalog
1304    registerdocumentfinalizer(flushcatalog,3,"catalog")
1305    registerdocumentfinalizer(flushinfo,3,"info")
1306
1307    registerpagefinalizer(checkextgstates,3,"extended graphic states")
1308    registerpagefinalizer(checkcolorspaces,3,"color spaces")
1309    registerpagefinalizer(checkpatterns,3,"patterns")
1310    registerpagefinalizer(checkshades,3,"shades")
1311
1312end
1313
1314-- in strc-bkm: lpdf.registerdocumentfinalizer(function() structures.bookmarks.place() end,1)
1315
1316function lpdf.rotationcm(a)
1317    local s = sind(a)
1318    local c = cosd(a)
1319    return format("%.6F %.6F %.6F %.6F 0 0 cm",c,s,-s,c)
1320end
1321
1322-- return nil is nicer in test prints
1323
1324function lpdf.checkedkey(t,key,variant)
1325    local pn = t and t[key]
1326    if pn ~= nil then
1327        local tn = type(pn)
1328        if tn == variant then
1329            if variant == "string" then
1330                if pn ~= "" then
1331                    return pn
1332                end
1333            elseif variant == "table" then
1334                if next(pn) then
1335                    return pn
1336                end
1337            else
1338                return pn
1339            end
1340        elseif tn == "string" then
1341            if variant == "number" then
1342                return tonumber(pn)
1343            elseif variant == "boolean" then
1344                return isboolean(pn,nil,true)
1345            end
1346        end
1347    end
1348 -- return nil
1349end
1350
1351function lpdf.checkedvalue(value,variant) -- code not shared
1352    if value ~= nil then
1353        local tv = type(value)
1354        if tv == variant then
1355            if variant == "string" then
1356                if value ~= "" then
1357                    return value
1358                end
1359            elseif variant == "table" then
1360                if next(value) then
1361                    return value
1362                end
1363            else
1364                return value
1365            end
1366        elseif tv == "string" then
1367            if variant == "number" then
1368                return tonumber(value)
1369            elseif variant == "boolean" then
1370                return isboolean(value,nil,true)
1371            end
1372        end
1373    end
1374end
1375
1376function lpdf.limited(n,min,max,default)
1377    if not n then
1378        return default
1379    else
1380        n = tonumber(n)
1381        if not n then
1382            return default
1383        elseif n > max then
1384            return max
1385        elseif n < min then
1386            return min
1387        else
1388            return n
1389        end
1390    end
1391end
1392
1393-- The next variant of ActualText is what Taco and I could come up with
1394-- eventually. As of September 2013 Acrobat copies okay, Sumatra copies a
1395-- question mark, pdftotext injects an extra space and Okular adds a
1396-- newline plus space.
1397
1398-- return formatters["BT /Span << /ActualText (CONTEXT) >> BDC [<feff>] TJ % t EMC ET"](code)
1399
1400if implement then
1401
1402    local f_actual_text_p     = formatters["BT /Span << /ActualText <feff%s> >> BDC %s EMC ET"]
1403    local f_actual_text_b     = formatters["BT /Span << /ActualText <feff%s> >> BDC"]
1404    local f_actual_text_b_not = formatters["/Span << /ActualText <feff%s> >> BDC"]
1405    local f_actual_text       = formatters["/Span <</ActualText %s >> BDC"]
1406
1407    local f_alternative_text  = formatters["/Span <</Alt %s >> BDC"]
1408
1409    local s_actual_text_e     <const> = "EMC ET"
1410    local s_actual_text_e_not <const> = "EMC"
1411
1412    local context   = context
1413    local pdfdirect = nodes.pool.directliteral -- we can use nuts.write deep down
1414    local tounicode = fonts.mappings.tounicode
1415
1416    -- These convert the string:
1417
1418    function codeinjections.startactualtext(str)
1419        return f_actual_text(tosixteen(str))
1420    end
1421
1422    function codeinjections.stopactualtext()
1423        return s_actual_text_e_not
1424    end
1425
1426    function codeinjections.startalternativetext(str)
1427        return f_alternative_text(tosixteen(str))
1428    end
1429
1430    function codeinjections.stopalternativetext()
1431        return s_actual_text_e_not
1432    end
1433
1434    -- These assume a unicode number of already converted one without bom.
1435
1436    function codeinjections.unicodetoactualtext(unicode,pdfcode)
1437        return f_actual_text_p(type(unicode) == "string" and unicode or tounicode(unicode),pdfcode)
1438    end
1439
1440    function codeinjections.startunicodetoactualtext(unicode)
1441        return f_actual_text_b(type(unicode) == "string" and unicode or tounicode(unicode))
1442    end
1443
1444    function codeinjections.stopunicodetoactualtext()
1445        return s_actual_text_e
1446    end
1447
1448    function codeinjections.startunicodetoactualtextdirect(unicode)
1449        return f_actual_text_b_not(type(unicode) == "string" and unicode or tounicode(unicode))
1450    end
1451
1452    function codeinjections.stopunicodetoactualtextdirect()
1453        return s_actual_text_e_not
1454    end
1455
1456    implement {
1457        name      = "startactualtext",
1458        arguments = "string",
1459        actions   = function(str)
1460            context(pdfdirect(f_actual_text(tosixteen(str))))
1461        end
1462    }
1463
1464    implement {
1465        name      = "stopactualtext",
1466        actions   = function()
1467            context(pdfdirect("EMC"))
1468        end
1469    }
1470
1471    implement {
1472        name      = "startalternativetext",
1473        arguments = "string",
1474        actions   = function(str)
1475            context(pdfdirect(f_alternative_text(tosixteen(str))))
1476        end
1477    }
1478
1479    implement {
1480        name      = "stopalternativetext",
1481        actions   = function()
1482            context(pdfdirect("EMC"))
1483        end
1484    }
1485
1486    local setstate  = nodes.nuts.pool.setstate
1487
1488    function nodeinjections.startalternate(str)
1489        return setstate(f_actual_text(tosixteen(str)))
1490    end
1491
1492    function nodeinjections.stopalternate()
1493        return setstate("EMC")
1494    end
1495
1496    implement {
1497        name      = "pdfsetuserdata",
1498        arguments = "2 strings",
1499        protected = true,
1500        public    = true,
1501        actions   = lpdf.setuserdata
1502    }
1503
1504end
1505
1506-- Bah, tikz uses \immediate for some reason which is probably a bug, so the usage
1507-- will deal with that. However, we will not provide the serialization.
1508
1509if implement then
1510
1511    implement { name = "pdfbackendcurrentresources",                   public = true, untraced  = true,                            actions = { lpdf.collectedresources, context } }
1512    implement { name = "pdfbackendsetcatalog",        usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtocatalog }
1513    implement { name = "pdfbackendsetinfo",           usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b,c) lpdf.addtoinfo(a,b,c) end } -- gets adapted
1514    implement { name = "pdfbackendsetname",           usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtonames }
1515    implement { name = "pdfbackendsetpageattribute",  usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopageattributes }
1516    implement { name = "pdfbackendsetpagesattribute", usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopagesattributes }
1517    implement { name = "pdfbackendsetpageresource",   usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopageresources }
1518    implement { name = "pdfbackendsetextgstate",      usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentextgstate (a,pdfverbose(b)) end }
1519    implement { name = "pdfbackendsetcolorspace",     usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentcolorspace(a,pdfverbose(b)) end }
1520    implement { name = "pdfbackendsetpattern",        usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentpattern   (a,pdfverbose(b)) end }
1521    implement { name = "pdfbackendsetshade",          usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentshade     (a,pdfverbose(b)) end }
1522
1523end
1524
1525-- more helpers: copy from lepd to lpdf
1526
1527function lpdf.copyconstant(v)
1528    if v ~= nil then
1529        return pdfconstant(v)
1530    end
1531end
1532
1533function lpdf.copyboolean(v)
1534    if v ~= nil then
1535        return pdfboolean(v)
1536    end
1537end
1538
1539function lpdf.copyunicode(v)
1540    if v then
1541        return pdfunicode(v)
1542    end
1543end
1544
1545function lpdf.copyarray(a)
1546    if a then
1547        local t = pdfarray()
1548        for i=1,#a do
1549            t[i] = a(i)
1550        end
1551        return t
1552    end
1553end
1554
1555function lpdf.copydictionary(d)
1556    if d then
1557        local t = pdfdictionary()
1558        for k, v in next, d do
1559            t[k] = d(k)
1560        end
1561        return t
1562    end
1563end
1564
1565function lpdf.copynumber(v)
1566    return v
1567end
1568
1569function lpdf.copyinteger(v)
1570    return v -- maybe checking or round ?
1571end
1572
1573function lpdf.copyfloat(v)
1574    return v
1575end
1576
1577function lpdf.copystring(v)
1578    if v then
1579        return pdfstring(v)
1580    end
1581end
1582
1583do
1584
1585    -- This is obsolete but old viewers might still use it as directive for what to send to a
1586    -- postscript printer. The bad is that even 2.0 validation can bark about it missing but
1587    -- maybe we should no longer care abotu that.
1588
1589    local a_procset = nil
1590    local d_procset = nil
1591    local include   = false
1592
1593    lpdf.registerinitializer(function()
1594        a_procset = nil
1595        d_procset = nil
1596    end)
1597
1598    function lpdf.setincludeprocset(v)
1599        include = v
1600    end
1601
1602    function lpdf.procset(dict)
1603        if not include or lpdf.majorversion() > 1 then
1604            return nil
1605        else
1606            if not a_procset then
1607                a_procset = pdfarray {
1608                    pdfconstant("PDF"),
1609                    pdfconstant("Text"),
1610                    pdfconstant("ImageB"),
1611                    pdfconstant("ImageC"),
1612                    pdfconstant("ImageI"),
1613                }
1614                a_procset = pdfreference(pdfimmediateobject(tostring(a_procset)))
1615            end
1616            if dict then
1617                if not d_procset then
1618                    d_procset = pdfdictionary {
1619                        ProcSet = a_procset
1620                    }
1621                    d_procset = pdfreference(pdfimmediateobject(tostring(d_procset)))
1622                end
1623                return d_procset
1624            else
1625                return a_procset
1626            end
1627        end
1628    end
1629
1630end
1631