lpdf-ini.lua /size: 53 Kb    last modification: 2024-01-16 09:02
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-- beware of "too many locals" here
11--
12-- The lua files are still hybrid ones but we keep that as a reference for the
13-- lmt variants that started out as copies.
14
15local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset
16local char, byte, format, gsub, concat, match, sub, gmatch = string.char, string.byte, string.format, string.gsub, table.concat, string.match, string.sub, string.gmatch
17local utfchar, utfbyte, utfvalues = utf.char, utf.byte, utf.values
18local sind, cosd, max, min = math.sind, math.cosd, math.max, math.min
19local sort, sortedhash = table.sort, table.sortedhash
20local P, C, R, S, Cc, Cs, V = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs, lpeg.V
21local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
22local formatters = string.formatters
23local isboolean = string.is_boolean
24local rshift = bit32.rshift
25local osdate, ostime = os.date, os.time
26
27local report_objects    = logs.reporter("backend","objects")
28local report_finalizing = logs.reporter("backend","finalizing")
29local report_blocked    = logs.reporter("backend","blocked")
30
31local implement         = interfaces.implement
32
33local context           = context
34
35-- In ConTeXt MkIV we use utf8 exclusively so all strings get mapped onto a hex
36-- encoded utf16 string type between <>. We could probably save some bytes by using
37-- strings between () but then we end up with escaped ()\ too.
38
39pdf                     = type(pdf) == "table" and pdf or { }
40local factor            = number.dimenfactors.bp
41
42local codeinjections    = { }
43local nodeinjections    = { }
44
45local backends          = backends
46
47local pdfbackend        = {
48    comment        = "backend for directly generating pdf output",
49    nodeinjections = nodeinjections,
50    codeinjections = codeinjections,
51    registrations  = { },
52    tables         = { },
53}
54
55backends.pdf = pdfbackend
56
57lpdf       = lpdf or { }
58local lpdf = lpdf
59lpdf.flags = lpdf.flags or { } -- will be filled later
60
61local trace_finalizers = false  trackers.register("backend.finalizers", function(v) trace_finalizers = v end)
62local trace_resources  = false  trackers.register("backend.resources",  function(v) trace_resources  = v end)
63local trace_objects    = false  trackers.register("backend.objects",    function(v) trace_objects    = v end)
64local trace_details    = false  trackers.register("backend.details",    function(v) trace_details    = v end)
65
66do
67
68    local pdfsetmajorversion, pdfsetminorversion, pdfgetmajorversion, pdfgetminorversion
69    local pdfsetcompresslevel, pdfsetobjectcompresslevel, pdfgetcompresslevel, pdfgetobjectcompresslevel
70    local pdfsetsuppressoptionalinfo, pdfsetomitcidset, pdfsetomitcharset
71
72    updaters.register("backend.update.lpdf",function()
73        pdfsetmajorversion         = pdf.setmajorversion
74        pdfsetminorversion         = pdf.setminorversion
75        pdfgetmajorversion         = pdf.getmajorversion
76        pdfgetminorversion         = pdf.getminorversion
77
78        pdfsetcompresslevel        = pdf.setcompresslevel
79        pdfsetobjectcompresslevel  = pdf.setobjcompresslevel
80        pdfgetcompresslevel        = pdf.getcompresslevel
81        pdfgetobjectcompresslevel  = pdf.getobjcompresslevel
82
83        pdfsetsuppressoptionalinfo = pdf.setsuppressoptionalinfo
84        pdfsetomitcidset           = pdf.setomitcidset
85        pdfsetomitcharset          = pdf.setomitcharset
86    end)
87
88    function lpdf.setversion(major,minor)
89        pdfsetmajorversion(major or 1)
90        pdfsetminorversion(minor or 7)
91    end
92
93    function lpdf.getversion(major,minor)
94        return pdfgetmajorversion(), pdfgetminorversion()
95    end
96
97    function lpdf.majorversion() return pdfgetmajorversion() end
98    function lpdf.minorversion() return pdfgetminorversion() end
99
100    local frozen = false
101    local clevel = 3
102    local olevel = 1
103
104    function lpdf.setcompression(level,objectlevel,freeze)
105        if not frozen then
106            if pdfsetcompresslevel then
107                pdfsetcompresslevel(level or 3)
108                pdfsetobjectcompresslevel(objectlevel or level or 3)
109            else
110                clevel = level
111                olevel = objectlevel
112            end
113            frozen = freeze
114        end
115    end
116
117    function lpdf.getcompression()
118        if pdfgetcompresslevel then
119            return pdfgetcompresslevel(), pdfgetobjectcompresslevel()
120        else
121            return clevel, olevel
122        end
123    end
124
125    function lpdf.compresslevel()
126        if pdfgetcompresslevel then
127            return pdfgetcompresslevel()
128        else
129            return clevel
130        end
131    end
132
133    function lpdf.objectcompresslevel()
134        if pdfgetobjectcompresslevel then
135            return pdfgetobjectcompresslevel()
136        else
137            return olevel
138        end
139    end
140
141    function lpdf.setsuppressoptionalinfo(n)
142        if pdfsetsuppressoptionalinfo then
143            pdfsetsuppressoptionalinfo(n) -- todo
144        end
145    end
146
147    function lpdf.setomitcidset(v)
148        return pdfsetomitcidset(v)
149    end
150
151    function lpdf.setomitcharset(v)
152        return pdfsetomitcharset(v)
153    end
154
155end
156
157do
158
159    local pdfgetxformname, pdfincludeimage
160
161    updaters.register("backend.update.lpdf",function()
162        pdfgetxformname = pdf.getxformname
163        pdfincludeimage = pdf.includeimage
164    end)
165
166    function lpdf.getxformname(id) return pdfgetxformname(id) end
167    function lpdf.includeimage(id) return pdfincludeimage(id) end
168
169end
170
171    local pdfsetpageresources, pdfsetpageattributes, pdfsetpagesattributes
172    local pdfreserveobject, pdfimmediateobject, pdfdeferredobject, pdfreferenceobject
173    local pdfgetpagereference
174
175    updaters.register("backend.update.lpdf",function()
176        pdfreserveobject      = pdf.reserveobj
177        pdfimmediateobject    = pdf.immediateobj
178        pdfdeferredobject     = pdf.obj
179        pdfreferenceobject    = pdf.refobj
180
181        pdfgetpagereference   = pdf.getpageref
182
183        pdfsetpageresources   = pdf.setpageresources
184        pdfsetpageattributes  = pdf.setpageattributes
185        pdfsetpagesattributes = pdf.setpagesattributes
186    end)
187
188local jobpositions = job.positions
189local getpos       = jobpositions.getpos
190local getrpos      = jobpositions.getrpos
191
192jobpositions.registerhandlers {
193    getpos  = pdf.getpos,
194 -- getrpos = pdf.getrpos,
195    gethpos = pdf.gethpos,
196    getvpos = pdf.getvpos,
197}
198
199do
200
201    local pdfgetmatrix, pdfhasmatrix, pdfprint
202
203    updaters.register("backend.update.lpdf",function()
204        pdfgetmatrix = pdf.getmatrix
205        pdfhasmatrix = pdf.hasmatrix
206        pdfprint     = pdf.print
207    end)
208
209    function lpdf.print(...)
210        return pdfprint(...)
211    end
212
213    -- local function transform(llx,lly,urx,ury,rx,sx,sy,ry)
214    --     local x1 = llx * rx + lly * sy
215    --     local y1 = llx * sx + lly * ry
216    --     local x2 = llx * rx + ury * sy
217    --     local y2 = llx * sx + ury * ry
218    --     local x3 = urx * rx + lly * sy
219    --     local y3 = urx * sx + lly * ry
220    --     local x4 = urx * rx + ury * sy
221    --     local y4 = urx * sx + ury * ry
222    --     llx = min(x1,x2,x3,x4);
223    --     lly = min(y1,y2,y3,y4);
224    --     urx = max(x1,x2,x3,x4);
225    --     ury = max(y1,y2,y3,y4);
226    --     return llx, lly, urx, ury
227    -- end
228    --
229    -- function lpdf.transform(llx,lly,urx,ury) -- not yet used so unchecked
230    --     if pdfhasmatrix() then
231    --         local sx, rx, ry, sy = pdfgetmatrix()
232    --         local w, h = urx - llx, ury - lly
233    --         return llx, lly, llx + sy*w - ry*h, lly + sx*h - rx*w
234    --      -- return transform(llx,lly,urx,ury,sx,rx,ry,sy)
235    --     else
236    --         return llx, lly, urx, ury
237    --     end
238    -- end
239
240    -- funny values for tx and ty
241
242    function lpdf.rectangle(width,height,depth,offset)
243        local tx, ty = getpos() -- pdfgetpos, maybe some day use dir here
244        if offset then
245            tx     = tx     -   offset
246            ty     = ty     +   offset
247            width  = width  + 2*offset
248            height = height +   offset
249            depth  = depth  +   offset
250        end
251        if pdfhasmatrix() then
252            local rx, sx, sy, ry = pdfgetmatrix()
253            return
254                factor *  tx,
255                factor * (ty - ry*depth  + sx*width),
256                factor * (tx + rx*width  - sy*height),
257                factor * (ty + ry*height - sx*width)
258        else
259            return
260                factor *  tx,
261                factor * (ty - depth),
262                factor * (tx + width),
263                factor * (ty + height)
264        end
265    end
266
267end
268
269-- we could use a hash of predefined unicodes
270
271-- local function tosixteen(str) -- an lpeg might be faster (no table)
272--     if not str or str == "" then
273--         return "<feff>" -- not () as we want an indication that it's unicode
274--     else
275--         local r, n = { "<feff" }, 1
276--         for b in utfvalues(str) do
277--             n = n + 1
278--             if b < 0x10000 then
279--                 r[n] = format("%04x",b)
280--             else
281--                 r[n] = format("%04x%04x",rshift(b,10),b%1024+0xDC00)
282--             end
283--         end
284--         n = n + 1
285--         r[n] = ">"
286--         return concat(r)
287--     end
288-- end
289
290local tosixteen, fromsixteen, topdfdoc, frompdfdoc, toeight, fromeight
291
292do
293
294    local escaped = Cs(Cc("(") * (S("\\()\n\r\t\b\f")/"\\%0" + P(1))^0 * Cc(")"))
295
296    local cache = table.setmetatableindex(function(t,k) -- can be made weak
297        local v = utfbyte(k)
298        if v < 0x10000 then
299            v = format("%04x",v)
300        else
301            v = v - 0x10000
302            v = format("%04x%04x",rshift(v,10)+0xD800,v%1024+0xDC00)
303         -- v = format("%04x%04x",rshift(v-0x10000,10)+0xD800,v%1024+0xDC00)
304        end
305        t[k] = v
306        return v
307    end)
308
309    local unified = Cs(Cc("<feff") * (lpeg.patterns.utf8character/cache)^1 * Cc(">"))
310
311    tosixteen = function(str) -- an lpeg might be faster (no table)
312        if not str or str == "" then
313            return "<feff>" -- not () as we want an indication that it's unicode
314        else
315            return lpegmatch(unified,str)
316        end
317    end
318
319    local more = 0
320
321    local pattern = C(4) / function(s) -- needs checking !
322        local now = tonumber(s,16)
323        if more > 0 then
324            now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000
325            more = 0
326            return utfchar(now)
327        elseif now >= 0xD800 and now <= 0xDBFF then
328            more = now
329            return "" -- else the c's end up in the stream
330        else
331            return utfchar(now)
332        end
333    end
334
335    local pattern = P(true) / function() more = 0 end * Cs(pattern^0)
336
337    fromsixteen = function(str)
338        if not str or str == "" then
339            return ""
340        else
341            return lpegmatch(pattern,str)
342        end
343    end
344
345    local toregime   = regimes.toregime
346    local fromregime = regimes.fromregime
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    local b_pattern = Cs((P("\\")/"" * (
376        S("()")
377      + S("nrtbf") / { n = "\n", r = "\r", t = "\t", b = "\b", f = "\f" }
378      + lpegpatterns.octdigit^-3 / function(s) return char(tonumber(s,8)) end)
379    + P(1))^0)
380
381    fromeight = function(str)
382        if not str or str == "" then
383            return ""
384        else
385            return lpegmatch(unescape,str)
386        end
387    end
388
389    local u_pattern = lpegpatterns.utfbom_16_be * lpegpatterns.utf16_to_utf8_be -- official
390                    + lpegpatterns.utfbom_16_le * lpegpatterns.utf16_to_utf8_le -- we've seen these
391
392    local h_pattern = lpegpatterns.hextobytes
393
394    local zero = S(" \n\r\t") + P("\\ ")
395    local one  = C(4)
396    local two  = P("d") * R("89","af") * C(2) * C(4)
397
398    local x_pattern = P { "start",
399        start     = V("wrapped") + V("unwrapped") + V("original"),
400        original  = Cs(P(1)^0),
401        wrapped   = P("<") * V("unwrapped") * P(">") * P(-1),
402        unwrapped = P("feff")
403                  * Cs( (
404                        zero  / ""
405                      + two   / function(a,b)
406                                    a = (tonumber(a,16) - 0xD800) * 1024
407                                    b = (tonumber(b,16) - 0xDC00)
408                                    return utfchar(a+b)
409                                end
410                      + one   / function(a)
411                                    return utfchar(tonumber(a,16))
412                                end
413                    )^1 ) * P(-1)
414    }
415
416    function lpdf.frombytes(s,hex)
417        if not s or s == "" then
418            return ""
419        end
420        if hex then
421            local x = lpegmatch(x_pattern,s)
422            if x then
423                return x
424            end
425            local h = lpegmatch(h_pattern,s)
426            if h then
427                return h
428            end
429        else
430            local u = lpegmatch(u_pattern,s)
431            if u then
432                return u
433            end
434        end
435        return lpegmatch(b_pattern,s)
436    end
437
438    lpdf.tosixteen   = tosixteen
439    lpdf.toeight     = toeight
440    lpdf.topdfdoc    = topdfdoc
441    lpdf.fromsixteen = fromsixteen
442    lpdf.fromeight   = fromeight
443    lpdf.frompdfdoc  = frompdfdoc
444
445end
446
447local tostring_a, tostring_d
448
449do
450
451    local f_key_null       = formatters["/%s null"]
452    local f_key_value      = formatters["/%s %s"]
453 -- local f_key_dictionary = formatters["/%s << % t >>"]
454 -- local f_dictionary     = formatters["<< % t >>"]
455    local f_key_dictionary = formatters["/%s << %s >>"]
456    local f_dictionary     = formatters["<< %s >>"]
457 -- local f_key_array      = formatters["/%s [ % t ]"]
458 -- local f_array          = formatters["[ % t ]"]
459    local f_key_array      = formatters["/%s [ %s ]"]
460    local f_array          = formatters["[ %s ]"]
461    local f_key_number     = formatters["/%s %N"]  -- always with max 9 digits and integer is possible
462    local f_tonumber       = formatters["%N"]      -- always with max 9 digits and integer is possible
463
464    tostring_d = function(t,contentonly,key)
465        if next(t) then
466            local r = { }
467            local n = 0
468            local e
469            for k, v in next, t do
470                if k == "__extra__" then
471                    e = v
472                elseif k == "__stream__" then
473                    -- do nothing (yet)
474                else
475                    n = n + 1
476                    r[n] = k
477                end
478            end
479            if n > 1 then
480                sort(r)
481            end
482            for i=1,n do
483                local k  = r[i]
484                local v  = t[k]
485                local tv = type(v)
486                -- mostly tables
487                if tv == "table" then
488                 -- local mv = getmetatable(v)
489                 -- if mv and mv.__lpdftype then
490                    if v.__lpdftype__ then
491                     -- if v == t then
492                     --     report_objects("ignoring circular reference in dirctionary")
493                     --     r[i] = f_key_null(k)
494                     -- else
495                            r[i] = f_key_value(k,tostring(v))
496                     -- end
497                    elseif v[1] then
498                        r[i] = f_key_value(k,tostring_a(v))
499                    else
500                        r[i] = f_key_value(k,tostring_d(v))
501                    end
502                elseif tv == "string" then
503                    r[i] = f_key_value(k,toeight(v))
504                elseif tv == "number" then
505                    r[i] = f_key_number(k,v)
506                else
507                    r[i] = f_key_value(k,tostring(v))
508                end
509            end
510            if e then
511                r[n+1] = e
512            end
513            r = concat(r," ")
514            if contentonly then
515                return r
516            elseif key then
517                return f_key_dictionary(key,r)
518            else
519                return f_dictionary(r)
520            end
521        elseif contentonly then
522            return ""
523        else
524            return "<< >>"
525        end
526    end
527
528    tostring_a = function(t,contentonly,key)
529        local tn = #t
530        if tn ~= 0 then
531            local r = { }
532            for k=1,tn do
533                local v = t[k]
534                local tv = type(v)
535                -- mostly numbers and tables
536                if tv == "number" then
537                    r[k] = f_tonumber(v)
538                elseif tv == "table" then
539                 -- local mv = getmetatable(v)
540                 -- if mv and mv.__lpdftype then
541                    if v.__lpdftype__ then
542                     -- if v == t then
543                     --     report_objects("ignoring circular reference in array")
544                     --     r[k] = "null"
545                     -- else
546                            r[k] = tostring(v)
547                     -- end
548                    elseif v[1] then
549                        r[k] = tostring_a(v)
550                    else
551                        r[k] = tostring_d(v)
552                    end
553                elseif tv == "string" then
554                    r[k] = toeight(v)
555                else
556                    r[k] = tostring(v)
557                end
558            end
559            local e = t.__extra__
560            if e then
561                r[tn+1] = e
562            end
563            r = concat(r," ")
564            if contentonly then
565                return r
566            elseif key then
567                return f_key_array(key,r)
568            else
569                return f_array(r)
570            end
571        elseif contentonly then
572            return ""
573        else
574            return "[ ]"
575        end
576    end
577
578end
579
580local f_tonumber = formatters["%N"]
581
582local tostring_x = function(t) return concat(t," ")       end
583local tostring_s = function(t) return toeight(t[1])       end
584local tostring_p = function(t) return topdfdoc(t[1],t[2]) end
585local tostring_u = function(t) return tosixteen(t[1])     end
586----- tostring_n = function(t) return tostring(t[1])      end -- tostring not needed
587local tostring_n = function(t) return f_tonumber(t[1])    end -- tostring not needed
588local tostring_c = function(t) return t[1]                end -- already prefixed (hashed)
589local tostring_z = function()  return "null"              end
590local tostring_t = function()  return "true"              end
591local tostring_f = function()  return "false"             end
592local tostring_r = function(t) local n = t[1] return n and n > 0 and (n .. " 0 R") or "null" end
593
594local tostring_v = function(t)
595    local s = t[1]
596    if type(s) == "table" then
597        return concat(s)
598    else
599        return s
600    end
601end
602
603local tostring_l = function(t)
604    local s = t[1]
605    if not s or s == "" then
606        return "()"
607    elseif t[2] then
608        return "<" .. s .. ">"
609    else
610        return "(" .. s .. ")"
611    end
612end
613
614local function value_x(t) return t                  end
615local function value_s(t) return t[1]               end
616local function value_p(t) return t[1]               end
617local function value_u(t) return t[1]               end
618local function value_n(t) return t[1]               end
619local function value_c(t) return sub(t[1],2)        end
620local function value_d(t) return tostring_d(t,true) end
621local function value_a(t) return tostring_a(t,true) end
622local function value_z()  return nil                end
623local function value_t(t) return t.value or true    end
624local function value_f(t) return t.value or false   end
625local function value_r(t) return t[1] or 0          end -- null
626local function value_v(t) return t[1]               end
627local function value_l(t) return t[1]               end
628
629local function add_to_d(t,v)
630    local k = type(v)
631    if k == "string" then
632        if t.__extra__ then
633            t.__extra__ = t.__extra__ .. " " .. v
634        else
635            t.__extra__ = v
636        end
637    elseif k == "table" then
638        for k, v in next, v do
639            t[k] = v
640        end
641    end
642    return t
643end
644
645local function add_to_a(t,v)
646    local k = type(v)
647    if k == "string" then
648        if t.__extra__ then
649            t.__extra__ = t.__extra__ .. " " .. v
650        else
651            t.__extra__ = v
652        end
653    elseif k == "table" then
654        local n = #t
655        for i=1,#v do
656            n = n + 1
657            t[n] = v[i]
658        end
659    end
660    return t
661end
662
663local function add_x(t,k,v) rawset(t,k,tostring(v)) end
664
665-- local mt_x = { __index = { __lpdftype__ = "stream"     }, __lpdftype = "stream",     __tostring = tostring_x, __call = value_x, __newindex = add_x }
666-- local mt_d = { __index = { __lpdftype__ = "dictionary" }, __lpdftype = "dictionary", __tostring = tostring_d, __call = value_d, __add = add_to_d }
667-- local mt_a = { __index = { __lpdftype__ = "array"      }, __lpdftype = "array",      __tostring = tostring_a, __call = value_a, __add = add_to_a }
668-- local mt_u = { __index = { __lpdftype__ = "unicode"    }, __lpdftype = "unicode",    __tostring = tostring_u, __call = value_u }
669-- local mt_s = { __index = { __lpdftype__ = "string"     }, __lpdftype = "string",     __tostring = tostring_s, __call = value_s }
670-- local mt_p = { __index = { __lpdftype__ = "docstring"  }, __lpdftype = "docstring",  __tostring = tostring_p, __call = value_p }
671-- local mt_n = { __index = { __lpdftype__ = "number"     }, __lpdftype = "number",     __tostring = tostring_n, __call = value_n }
672-- local mt_c = { __index = { __lpdftype__ = "constant"   }, __lpdftype = "constant",   __tostring = tostring_c, __call = value_c }
673-- local mt_z = { __index = { __lpdftype__ = "null"       }, __lpdftype = "null",       __tostring = tostring_z, __call = value_z }
674-- local mt_t = { __index = { __lpdftype__ = "true"       }, __lpdftype = "true",       __tostring = tostring_t, __call = value_t }
675-- local mt_f = { __index = { __lpdftype__ = "false"      }, __lpdftype = "false",      __tostring = tostring_f, __call = value_f }
676-- local mt_r = { __index = { __lpdftype__ = "reference"  }, __lpdftype = "reference",  __tostring = tostring_r, __call = value_r }
677-- local mt_v = { __index = { __lpdftype__ = "verbose"    }, __lpdftype = "verbose",    __tostring = tostring_v, __call = value_v }
678-- local mt_l = { __index = { __lpdftype__ = "literal"    }, __lpdftype = "literal",    __tostring = tostring_l, __call = value_l }
679
680local mt_x = { __index = { __lpdftype__ = "stream"     }, __tostring = tostring_x, __call = value_x, __newindex = add_x }
681local mt_d = { __index = { __lpdftype__ = "dictionary" }, __tostring = tostring_d, __call = value_d, __add = add_to_d }
682local mt_a = { __index = { __lpdftype__ = "array"      }, __tostring = tostring_a, __call = value_a, __add = add_to_a }
683local mt_u = { __index = { __lpdftype__ = "unicode"    }, __tostring = tostring_u, __call = value_u }
684local mt_s = { __index = { __lpdftype__ = "string"     }, __tostring = tostring_s, __call = value_s }
685local mt_p = { __index = { __lpdftype__ = "docstring"  }, __tostring = tostring_p, __call = value_p }
686local mt_n = { __index = { __lpdftype__ = "number"     }, __tostring = tostring_n, __call = value_n }
687local mt_c = { __index = { __lpdftype__ = "constant"   }, __tostring = tostring_c, __call = value_c }
688local mt_z = { __index = { __lpdftype__ = "null"       }, __tostring = tostring_z, __call = value_z }
689local mt_t = { __index = { __lpdftype__ = "true"       }, __tostring = tostring_t, __call = value_t }
690local mt_f = { __index = { __lpdftype__ = "false"      }, __tostring = tostring_f, __call = value_f }
691local mt_r = { __index = { __lpdftype__ = "reference"  }, __tostring = tostring_r, __call = value_r }
692local mt_v = { __index = { __lpdftype__ = "verbose"    }, __tostring = tostring_v, __call = value_v }
693local mt_l = { __index = { __lpdftype__ = "literal"    }, __tostring = tostring_l, __call = value_l }
694
695local function pdfstream(t) -- we need to add attributes
696    if t then
697        local tt = type(t)
698        if tt == "table" then
699            for i=1,#t do
700                t[i] = tostring(t[i])
701            end
702        elseif tt == "string" then
703            t = { t }
704        else
705            t = { tostring(t) }
706        end
707    end
708    return setmetatable(t or { },mt_x)
709end
710
711local function pdfdictionary(t)
712    return setmetatable(t or { },mt_d)
713end
714
715local function pdfarray(t)
716    if type(t) == "string" then
717        return setmetatable({ t },mt_a)
718    else
719        return setmetatable(t or { },mt_a)
720    end
721end
722
723local function pdfstring(str,default)
724    return setmetatable({ str or default or "" },mt_s)
725end
726
727local function pdfdocstring(str,default,defaultchar)
728    return setmetatable({ str or default or "", defaultchar or " " },mt_p)
729end
730
731local function pdfunicode(str,default)
732    return setmetatable({ str or default or "" },mt_u) -- could be a string
733end
734
735local function pdfliteral(str,hex) -- can also produce a hex <> instead of () literal
736    return setmetatable({ str, hex },mt_l)
737end
738
739local pdfnumber, pdfconstant
740
741do
742
743    local cache = { } -- can be weak
744
745    pdfnumber = function(n,default) -- 0-10
746        if not n then
747            n = default
748        end
749        local c = cache[n]
750        if not c then
751            c = setmetatable({ n },mt_n)
752        --  cache[n] = c -- too many numbers
753        end
754        return c
755    end
756
757    for i=-1,9 do cache[i] = pdfnumber(i) end
758
759    local replacer = S("\0\t\n\r\f ()[]{}/%%#\\") / {
760        ["\00"]="#00",
761        ["\09"]="#09",
762        ["\10"]="#0a",
763        ["\12"]="#0c",
764        ["\13"]="#0d",
765        [ " " ]="#20",
766        [ "#" ]="#23",
767        [ "%" ]="#25",
768        [ "(" ]="#28",
769        [ ")" ]="#29",
770        [ "/" ]="#2f",
771        [ "[" ]="#5b",
772        [ "\\"]="#5c",
773        [ "]" ]="#5d",
774        [ "{" ]="#7b",
775        [ "}" ]="#7d",
776    } + P(1)
777
778    local escaped = Cs(Cc("/") * replacer^0)
779
780    local cache = table.setmetatableindex(function(t,k)
781        local v = setmetatable({ lpegmatch(escaped,k) }, mt_c)
782        t[k] = v
783        return v
784    end)
785
786    pdfconstant = function(str,default)
787        if not str then
788            str = default or "none"
789        end
790        return cache[str]
791    end
792
793    local escaped = Cs(replacer^0)
794
795    function lpdf.escaped(str)
796        return lpegmatch(escaped,str) or str
797    end
798
799end
800
801local pdfnull, pdfboolean, pdfreference, pdfverbose
802
803do
804
805    local p_null  = { } setmetatable(p_null, mt_z)
806    local p_true  = { } setmetatable(p_true, mt_t)
807    local p_false = { } setmetatable(p_false,mt_f)
808
809    pdfnull = function()
810        return p_null
811    end
812
813    pdfboolean = function(b,default)
814        if type(b) == "boolean" then
815            return b and p_true or p_false
816        else
817            return default and p_true or p_false
818        end
819    end
820
821    -- print(pdfboolean(false),pdfboolean(false,false),pdfboolean(false,true))
822    -- print(pdfboolean(true),pdfboolean(true,false),pdfboolean(true,true))
823    -- print(pdfboolean(nil,true),pdfboolean(nil,false))
824
825    local r_zero = setmetatable({ 0 },mt_r)
826
827    pdfreference = function(r)  -- maybe make a weak table
828        if r and r ~= 0 then
829            return setmetatable({ r },mt_r)
830        else
831            return r_zero
832        end
833    end
834
835    local v_zero  = setmetatable({ 0  },mt_v)
836    local v_empty = setmetatable({ "" },mt_v)
837
838    pdfverbose = function(t) -- maybe check for type
839        if t == 0 then
840            return v_zero
841        elseif t == "" then
842            return v_empty
843        else
844            return setmetatable({ t },mt_v)
845        end
846    end
847
848end
849
850lpdf.stream      = pdfstream -- THIS WILL PROBABLY CHANGE
851lpdf.dictionary  = pdfdictionary
852lpdf.array       = pdfarray
853lpdf.docstring   = pdfdocstring
854lpdf.string      = pdfstring
855lpdf.unicode     = pdfunicode
856lpdf.number      = pdfnumber
857lpdf.constant    = pdfconstant
858lpdf.null        = pdfnull
859lpdf.boolean     = pdfboolean
860lpdf.reference   = pdfreference
861lpdf.verbose     = pdfverbose
862lpdf.literal     = pdfliteral
863
864local names, cache = { }, { }
865
866function lpdf.reserveobject(name)
867    local r = pdfreserveobject() -- we don't support "annot"
868    if name then
869        names[name] = r
870        if trace_objects then
871            report_objects("reserving number %a under name %a",r,name)
872        end
873    elseif trace_objects then
874        report_objects("reserving number %a",r)
875    end
876    return r
877end
878
879local nofpages = 0
880
881local texgetcount = tex.getcount
882
883function lpdf.pagereference(n,complete) -- true | false | nil | n [true,false]
884    if nofpages == 0 then
885        nofpages = structures.pages.nofpages
886        if nofpages == 0 then
887            nofpages = 1
888        end
889    end
890    if n == true or not n then
891        complete = n
892        n = texgetcount("realpageno")
893    end
894    local r = n > nofpages and pdfgetpagereference(nofpages) or pdfgetpagereference(n)
895    return complete and pdfreference(r) or r
896end
897
898function lpdf.nofpages()
899    return structures.pages.nofpages
900end
901
902function lpdf.obj(...)
903    pdfdeferredobject(...)
904end
905
906function lpdf.immediateobj(...)
907    pdfimmediateobject(...)
908end
909
910function lpdf.delayedobject(data,n)
911    if n then
912        pdfdeferredobject(n,data)
913    else
914        n = pdfdeferredobject(data)
915    end
916    pdfreferenceobject(n)
917    return n
918end
919
920function lpdf.flushobject(name,data)
921    if data then
922        local named = names[name]
923        if named then
924            if not trace_objects then
925            elseif trace_details then
926                report_objects("flushing data to reserved object with name %a, data: %S",name,data)
927            else
928                report_objects("flushing data to reserved object with name %a",name)
929            end
930            return pdfimmediateobject(named,tostring(data))
931        else
932            if not trace_objects then
933            elseif trace_details then
934                report_objects("flushing data to reserved object with number %s, data: %S",name,data)
935            else
936                report_objects("flushing data to reserved object with number %s",name)
937            end
938            return pdfimmediateobject(name,tostring(data))
939        end
940    else
941        if trace_objects and trace_details then
942            report_objects("flushing data: %S",name)
943        end
944        return pdfimmediateobject(tostring(name))
945    end
946end
947
948function lpdf.flushstreamobject(data,dict,compressed,objnum) -- default compressed
949    if trace_objects then
950        report_objects("flushing stream object of %s bytes",#data)
951    end
952    local dtype    = type(dict)
953    local kind     = compressed == "raw" and "raw" or "stream"
954    local nolength = nil
955    if compressed == "raw" then
956        compressed = nil
957        nolength   = true
958     -- data       = string.formatters["<< %s >>stream\n%s\nendstream"](attr,data)
959    end
960    return pdfdeferredobject {
961        objnum        = objnum,
962        immediate     = true,
963        nolength      = nolength,
964        compresslevel = compressed == false and 0 or nil,
965        type          = "stream",
966        string        = data,
967        attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
968    }
969end
970
971function lpdf.flushstreamfileobject(filename,dict,compressed,objnum) -- default compressed
972    if trace_objects then
973        report_objects("flushing stream file object %a",filename)
974    end
975    local dtype = type(dict)
976    return pdfdeferredobject {
977        objnum        = objnum,
978        immediate     = true,
979        compresslevel = compressed == false and 0 or nil,
980        type          = "stream",
981        file          = filename,
982        attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
983    }
984end
985
986local shareobjectcache, shareobjectreferencecache = { }, { }
987
988function lpdf.shareobject(content)
989    if content == nil then
990        -- invalid object not created
991    else
992        content = tostring(content)
993        local o = shareobjectcache[content]
994        if not o then
995            o = pdfimmediateobject(content)
996            shareobjectcache[content] = o
997        end
998        return o
999    end
1000end
1001
1002function lpdf.shareobjectreference(content)
1003    if content == nil then
1004        -- invalid object not created
1005    else
1006        content = tostring(content)
1007        local r = shareobjectreferencecache[content]
1008        if not r then
1009            local o = shareobjectcache[content]
1010            if not o then
1011                o = pdfimmediateobject(content)
1012                shareobjectcache[content] = o
1013            end
1014            r = pdfreference(o)
1015            shareobjectreferencecache[content] = r
1016        end
1017        return r
1018    end
1019end
1020
1021-- three priority levels, default=2
1022
1023local pagefinalizers     = { { }, { }, { } }
1024local documentfinalizers = { { }, { }, { } }
1025
1026local pageresources, pageattributes, pagesattributes
1027
1028local function resetpageproperties()
1029    pageresources   = pdfdictionary()
1030    pageattributes  = pdfdictionary()
1031    pagesattributes = pdfdictionary()
1032end
1033
1034function lpdf.getpageproperties()
1035    return {
1036        pageresources   = pageresources,
1037        pageattributes  = pageattributes,
1038        pagesattributes = pagesattributes,
1039    }
1040end
1041
1042resetpageproperties()
1043
1044local function setpageproperties()
1045    pdfsetpageresources  (pageresources  ())
1046    pdfsetpageattributes (pageattributes ())
1047    pdfsetpagesattributes(pagesattributes())
1048end
1049
1050local function addtopageresources  (k,v) pageresources  [k] = v end
1051local function addtopageattributes (k,v) pageattributes [k] = v end
1052local function addtopagesattributes(k,v) pagesattributes[k] = v end
1053
1054lpdf.addtopageresources   = addtopageresources
1055lpdf.addtopageattributes  = addtopageattributes
1056lpdf.addtopagesattributes = addtopagesattributes
1057
1058local function set(where,what,f,when,comment)
1059    if type(when) == "string" then
1060        when, comment = 2, when
1061    elseif not when then
1062        when = 2
1063    end
1064    local w = where[when]
1065    w[#w+1] = { f, comment }
1066    if trace_finalizers then
1067        report_finalizing("%s set: [%s,%s]",what,when,#w)
1068    end
1069end
1070
1071local function run(where,what)
1072    if trace_finalizers then
1073        report_finalizing("start backend, category %a, n %a",what,#where)
1074    end
1075    for i=1,#where do
1076        local w = where[i]
1077        for j=1,#w do
1078            local wj = w[j]
1079            if trace_finalizers then
1080                report_finalizing("%s finalizer: [%s,%s] %s",what,i,j,wj[2] or "")
1081            end
1082            wj[1]()
1083        end
1084    end
1085    if trace_finalizers then
1086        report_finalizing("stop finalizing")
1087    end
1088end
1089
1090local function registerpagefinalizer(f,when,comment)
1091    set(pagefinalizers,"page",f,when,comment)
1092end
1093
1094local function registerdocumentfinalizer(f,when,comment)
1095    set(documentfinalizers,"document",f,when,comment)
1096end
1097
1098lpdf.registerpagefinalizer     = registerpagefinalizer
1099lpdf.registerdocumentfinalizer = registerdocumentfinalizer
1100
1101function lpdf.finalizepage(shipout)
1102    if shipout and not environment.initex then
1103     -- resetpageproperties() -- maybe better before
1104        run(pagefinalizers,"page")
1105        setpageproperties()
1106        resetpageproperties() -- maybe better before
1107    end
1108end
1109
1110function lpdf.finalizedocument()
1111    if not environment.initex then
1112        run(documentfinalizers,"document")
1113        function lpdf.finalizedocument()
1114         -- report_finalizing("serious error: the document is finalized multiple times")
1115            function lpdf.finalizedocument() end
1116        end
1117    end
1118end
1119
1120callbacks.register("finish_pdfpage", lpdf.finalizepage)
1121callbacks.register("finish_pdffile", lpdf.finalizedocument)
1122
1123do
1124
1125    local pdfsetinfo, pdfsetcatalog, pdfsettrailerid -- pdfsetnames pdfsettrailer
1126
1127    updaters.register("backend.update.lpdf",function()
1128        pdfsetinfo                 = pdf.setinfo
1129        pdfsetcatalog              = pdf.setcatalog
1130        pdfsettrailerid            = pdf.settrailerid
1131    end)
1132
1133    function lpdf.settrailerid(id)
1134        pdfsettrailerid(id)
1135    end
1136
1137    -- some minimal tracing, handy for checking the order
1138
1139    local function trace_set(what,key)
1140        if trace_resources then
1141            report_finalizing("setting key %a in %a",key,what)
1142        end
1143    end
1144
1145    local function trace_flush(what)
1146        if trace_resources then
1147            report_finalizing("flushing %a",what)
1148        end
1149    end
1150
1151    lpdf.protectresources = true
1152
1153    local catalog = pdfdictionary { Type = pdfconstant("Catalog") } -- nicer, but when we assign we nil the Type
1154    local info    = pdfdictionary { Type = pdfconstant("Info")    } -- nicer, but when we assign we nil the Type
1155    ----- names   = pdfdictionary { Type = pdfconstant("Names")   } -- nicer, but when we assign we nil the Type
1156
1157    local function checkcatalog()
1158        if not environment.initex then
1159            trace_flush("catalog")
1160            return true
1161        end
1162    end
1163
1164    local function checkinfo()
1165        if not environment.initex then
1166            trace_flush("info")
1167            if lpdf.majorversion() > 1 then
1168                for k, v in next, info do
1169                    if k == "CreationDate" or k == "ModDate" then
1170                        -- mandate >= 2.0
1171                    else
1172                        info[k] = nil
1173                    end
1174                end
1175            end
1176            return true
1177        end
1178    end
1179
1180    local function flushcatalog()
1181        if checkcatalog() then
1182            catalog.Type = nil
1183            pdfsetcatalog(catalog())
1184        end
1185    end
1186
1187    local function flushinfo()
1188        if checkinfo() then
1189            info.Type = nil
1190            pdfsetinfo(info())
1191        end
1192    end
1193
1194    function lpdf.getcatalog()
1195        if checkcatalog() then
1196            catalog.Type = pdfconstant("Catalog")
1197            return pdfreference(pdfimmediateobject(tostring(catalog)))
1198        end
1199    end
1200
1201    function lpdf.getinfo()
1202        if checkinfo() then
1203            return pdfreference(pdfimmediateobject(tostring(info)))
1204        end
1205    end
1206
1207    function lpdf.addtocatalog(k,v)
1208        if not (lpdf.protectresources and catalog[k]) then
1209            trace_set("catalog",k)
1210            catalog[k] = v
1211        end
1212    end
1213
1214    function lpdf.addtoinfo(k,v)
1215        if not (lpdf.protectresources and info[k]) then
1216            trace_set("info",k)
1217            info[k] = v
1218        end
1219    end
1220
1221    -- local function lpdf.addtonames(k,v)
1222    --     if not (lpdf.protectresources and names[k]) then
1223    --         trace_set("names",k)
1224    --         names[k] = v
1225    --     end
1226    -- end
1227
1228    local names = pdfdictionary {
1229     -- Type = pdfconstant("Names")
1230    }
1231
1232    local function flushnames()
1233        if next(names) and not environment.initex then
1234            names.Type = pdfconstant("Names")
1235            trace_flush("names")
1236            lpdf.addtocatalog("Names",pdfreference(pdfimmediateobject(tostring(names))))
1237        end
1238    end
1239
1240    function lpdf.addtonames(k,v)
1241        if not (lpdf.protectresources and names[k]) then
1242            trace_set("names",  k)
1243            names  [k] = v
1244        end
1245    end
1246
1247    local r_extgstates, r_colorspaces, r_patterns, r_shades
1248    local d_extgstates, d_colorspaces, d_patterns, d_shades
1249    local p_extgstates, p_colorspaces, p_patterns, p_shades
1250
1251    local function checkextgstates () if d_extgstates  then addtopageresources("ExtGState", p_extgstates ) end end
1252    local function checkcolorspaces() if d_colorspaces then addtopageresources("ColorSpace",p_colorspaces) end end
1253    local function checkpatterns   () if d_patterns    then addtopageresources("Pattern",   p_patterns   ) end end
1254    local function checkshades     () if d_shades      then addtopageresources("Shading",   p_shades     ) end end
1255
1256    local function flushextgstates () if d_extgstates  then trace_flush("extgstates")  pdfimmediateobject(r_extgstates, tostring(d_extgstates )) end end
1257    local function flushcolorspaces() if d_colorspaces then trace_flush("colorspaces") pdfimmediateobject(r_colorspaces,tostring(d_colorspaces)) end end
1258    local function flushpatterns   () if d_patterns    then trace_flush("patterns")    pdfimmediateobject(r_patterns,   tostring(d_patterns   )) end end
1259    local function flushshades     () if d_shades      then trace_flush("shades")      pdfimmediateobject(r_shades,     tostring(d_shades     )) end end
1260
1261    -- patterns are special as they need resources to so we can get recursive references and in that case
1262    -- acrobat doesn't show anything (other viewers handle it well)
1263    --
1264    -- todo: share them
1265    -- todo: force when not yet set
1266
1267    local f_font = formatters["%s%d"]
1268
1269    function lpdf.collectedresources(options)
1270        local ExtGState  = d_extgstates  and next(d_extgstates ) and p_extgstates
1271        local ColorSpace = d_colorspaces and next(d_colorspaces) and p_colorspaces
1272        local Pattern    = d_patterns    and next(d_patterns   ) and p_patterns
1273        local Shading    = d_shades      and next(d_shades     ) and p_shades
1274        local Font
1275        if options and options.patterns == false then
1276            Pattern = nil
1277        end
1278        local fonts = options and options.fonts
1279        if fonts and next(fonts) then
1280            local pdfgetfontobjnumber = lpdf.getfontobjnumber
1281            if pdfgetfontobjnumber then
1282                local prefix = options.fontprefix or "F"
1283                Font = pdfdictionary { }
1284                for k, v in sortedhash(fonts) do
1285                    Font[f_font(prefix,v)] = pdfreference(pdfgetfontobjnumber(k))
1286                end
1287            end
1288        end
1289        if ExtGState or ColorSpace or Pattern or Shading or Font then
1290            local collected = pdfdictionary {
1291                ExtGState  = ExtGState,
1292                ColorSpace = ColorSpace,
1293                Pattern    = Pattern,
1294                Shading    = Shading,
1295                Font       = Font,
1296            }
1297            if options and options.serialize == false then
1298                return collected
1299            else
1300                return collected()
1301            end
1302        elseif options and options.notempty then
1303            return nil
1304        elseif options and options.serialize == false then
1305            return pdfdictionary { }
1306        else
1307            return ""
1308        end
1309    end
1310
1311    function lpdf.adddocumentextgstate (k,v)
1312        if not d_extgstates then
1313            r_extgstates = pdfreserveobject()
1314            d_extgstates = pdfdictionary()
1315            p_extgstates = pdfreference(r_extgstates)
1316        end
1317        d_extgstates[k] = v
1318    end
1319
1320    function lpdf.adddocumentcolorspace(k,v)
1321        if not d_colorspaces then
1322            r_colorspaces = pdfreserveobject()
1323            d_colorspaces = pdfdictionary()
1324            p_colorspaces = pdfreference(r_colorspaces)
1325        end
1326        d_colorspaces[k] = v
1327    end
1328
1329    function lpdf.adddocumentpattern(k,v)
1330        if not d_patterns then
1331            r_patterns = pdfreserveobject()
1332            d_patterns = pdfdictionary()
1333            p_patterns = pdfreference(r_patterns)
1334        end
1335        d_patterns[k] = v
1336    end
1337
1338    function lpdf.adddocumentshade(k,v)
1339        if not d_shades then
1340            r_shades = pdfreserveobject()
1341            d_shades = pdfdictionary()
1342            p_shades = pdfreference(r_shades)
1343        end
1344        d_shades[k] = v
1345    end
1346
1347    registerdocumentfinalizer(flushextgstates,3,"extended graphic states")
1348    registerdocumentfinalizer(flushcolorspaces,3,"color spaces")
1349    registerdocumentfinalizer(flushpatterns,3,"patterns")
1350    registerdocumentfinalizer(flushshades,3,"shades")
1351
1352    registerdocumentfinalizer(flushnames,3,"names") -- before catalog
1353    registerdocumentfinalizer(flushcatalog,3,"catalog")
1354    registerdocumentfinalizer(flushinfo,3,"info")
1355
1356    registerpagefinalizer(checkextgstates,3,"extended graphic states")
1357    registerpagefinalizer(checkcolorspaces,3,"color spaces")
1358    registerpagefinalizer(checkpatterns,3,"patterns")
1359    registerpagefinalizer(checkshades,3,"shades")
1360
1361end
1362
1363-- in strc-bkm: lpdf.registerdocumentfinalizer(function() structures.bookmarks.place() end,1)
1364
1365function lpdf.rotationcm(a)
1366    local s = sind(a)
1367    local c = cosd(a)
1368    return format("%.6F %.6F %.6F %.6F 0 0 cm",c,s,-s,c)
1369end
1370
1371-- ! -> universaltime
1372
1373do
1374
1375    -- It's a bit of a historical mess here.
1376
1377    local metadata  = nil
1378    local timestamp = backends.timestamp()
1379
1380    function lpdf.getmetadata()
1381        if not metadata then
1382            local contextversion      = environment.version
1383            local luatexversion       = format("%1.2f",LUATEXVERSION)
1384            local luatexfunctionality = tostring(LUATEXFUNCTIONALITY)
1385            metadata = {
1386                producer            = format("LuaTeX-%s",luatexversion),
1387                creator             = format("LuaTeX %s %s + ConTeXt MkIV %s",luatexversion,luatexfunctionality,contextversion),
1388                luatexversion       = luatexversion,
1389                contextversion      = contextversion,
1390                luatexfunctionality = luatexfunctionality,
1391                luaversion          = tostring(LUAVERSION),
1392                platform            = os.platform,
1393                time                = timestamp,
1394            }
1395        end
1396        return metadata
1397    end
1398
1399    function lpdf.settime(n)
1400        if n then
1401            n = converters.totime(n)
1402            if n then
1403                converters.settime(n)
1404                timestamp = backends.timestamp()
1405            end
1406        end
1407        if metadata then
1408            metadata.time = timestamp
1409        end
1410        return timestamp
1411    end
1412
1413 -- lpdf.settime(tonumber(resolvers.variable("starttime")) or tonumber(resolvers.variable("SOURCE_DATE_EPOCH"))) -- bah
1414    lpdf.settime(tonumber(resolvers.variable("starttime")))
1415
1416    function lpdf.pdftimestamp(str)
1417        local t = type(str)
1418        if t == "string" then
1419            local Y, M, D, h, m, s, Zs, Zh, Zm = match(str,"^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)([%+%-])(%d%d):(%d%d)$")
1420            return Y and format("D:%s%s%s%s%s%s%s%s'%s",Y,M,D,h,m,s,Zs,Zh,Zm)
1421        else
1422            return osdate("D:%Y%m%d%H%M%S",t == "number" and str or ostime()) -- maybe "!D..." : universal time
1423        end
1424    end
1425
1426    function lpdf.id(date)
1427        local banner = environment.jobname or tex.jobname or "unknown"
1428        if not date then
1429            return banner
1430        else
1431            return format("%s | %s",banner,timestamp)
1432        end
1433    end
1434
1435end
1436
1437-- return nil is nicer in test prints
1438
1439function lpdf.checkedkey(t,key,variant)
1440    local pn = t and t[key]
1441    if pn ~= nil then
1442        local tn = type(pn)
1443        if tn == variant then
1444            if variant == "string" then
1445                if pn ~= "" then
1446                    return pn
1447                end
1448            elseif variant == "table" then
1449                if next(pn) then
1450                    return pn
1451                end
1452            else
1453                return pn
1454            end
1455        elseif tn == "string" then
1456            if variant == "number" then
1457                return tonumber(pn)
1458            elseif variant == "boolean" then
1459                return isboolean(pn,nil,true)
1460            end
1461        end
1462    end
1463 -- return nil
1464end
1465
1466function lpdf.checkedvalue(value,variant) -- code not shared
1467    if value ~= nil then
1468        local tv = type(value)
1469        if tv == variant then
1470            if variant == "string" then
1471                if value ~= "" then
1472                    return value
1473                end
1474            elseif variant == "table" then
1475                if next(value) then
1476                    return value
1477                end
1478            else
1479                return value
1480            end
1481        elseif tv == "string" then
1482            if variant == "number" then
1483                return tonumber(value)
1484            elseif variant == "boolean" then
1485                return isboolean(value,nil,true)
1486            end
1487        end
1488    end
1489end
1490
1491function lpdf.limited(n,min,max,default)
1492    if not n then
1493        return default
1494    else
1495        n = tonumber(n)
1496        if not n then
1497            return default
1498        elseif n > max then
1499            return max
1500        elseif n < min then
1501            return min
1502        else
1503            return n
1504        end
1505    end
1506end
1507
1508-- The next variant of ActualText is what Taco and I could come up with
1509-- eventually. As of September 2013 Acrobat copies okay, Sumatra copies a
1510-- question mark, pdftotext injects an extra space and Okular adds a
1511-- newline plus space.
1512
1513-- return formatters["BT /Span << /ActualText (CONTEXT) >> BDC [<feff>] TJ % t EMC ET"](code)
1514
1515do
1516
1517    local f_actual_text_p     = formatters["BT /Span << /ActualText <feff%s> >> BDC %s EMC ET"]
1518    local f_actual_text_b     = formatters["BT /Span << /ActualText <feff%s> >> BDC"]
1519    local s_actual_text_e     = "EMC ET"
1520    local f_actual_text_b_not = formatters["/Span << /ActualText <feff%s> >> BDC"]
1521    local s_actual_text_e_not = "EMC"
1522    local f_actual_text       = formatters["/Span <</ActualText %s >> BDC"]
1523
1524    local context   = context
1525    local pdfdirect = nodes.pool.directliteral -- we can use nuts.write deep down
1526    local tounicode = fonts.mappings.tounicode
1527
1528    function codeinjections.unicodetoactualtext(unicode,pdfcode)
1529        return f_actual_text_p(type(unicode) == "string" and unicode or tounicode(unicode),pdfcode)
1530    end
1531
1532    function codeinjections.startunicodetoactualtext(unicode)
1533        return f_actual_text_b(type(unicode) == "string" and unicode or tounicode(unicode))
1534    end
1535
1536    function codeinjections.stopunicodetoactualtext()
1537        return s_actual_text_e
1538    end
1539
1540    function codeinjections.startunicodetoactualtextdirect(unicode)
1541        return f_actual_text_b_not(type(unicode) == "string" and unicode or tounicode(unicode))
1542    end
1543
1544    function codeinjections.stopunicodetoactualtextdirect()
1545        return s_actual_text_e_not
1546    end
1547
1548    implement {
1549        name      = "startactualtext",
1550        arguments = "string",
1551        actions   = function(str)
1552            context(pdfdirect(f_actual_text(tosixteen(str))))
1553        end
1554    }
1555
1556    implement {
1557        name      = "stopactualtext",
1558        actions   = function()
1559            context(pdfdirect("EMC"))
1560        end
1561    }
1562
1563end
1564
1565-- interface
1566
1567implement { name = "lpdf_collectedresources",                             actions = { lpdf.collectedresources, context } }
1568implement { name = "lpdf_addtocatalog",          arguments = "2 strings", actions = lpdf.addtocatalog }
1569implement { name = "lpdf_addtoinfo",             arguments = "2 strings", actions = function(a,b,c) lpdf.addtoinfo(a,b,c) end } -- gets adapted
1570implement { name = "lpdf_addtonames",            arguments = "2 strings", actions = lpdf.addtonames }
1571implement { name = "lpdf_addtopageattributes",   arguments = "2 strings", actions = lpdf.addtopageattributes }
1572implement { name = "lpdf_addtopagesattributes",  arguments = "2 strings", actions = lpdf.addtopagesattributes }
1573implement { name = "lpdf_addtopageresources",    arguments = "2 strings", actions = lpdf.addtopageresources }
1574implement { name = "lpdf_adddocumentextgstate",  arguments = "2 strings", actions = function(a,b) lpdf.adddocumentextgstate (a,pdfverbose(b)) end }
1575implement { name = "lpdf_adddocumentcolorspace", arguments = "2 strings", actions = function(a,b) lpdf.adddocumentcolorspace(a,pdfverbose(b)) end }
1576implement { name = "lpdf_adddocumentpattern",    arguments = "2 strings", actions = function(a,b) lpdf.adddocumentpattern   (a,pdfverbose(b)) end }
1577implement { name = "lpdf_adddocumentshade",      arguments = "2 strings", actions = function(a,b) lpdf.adddocumentshade     (a,pdfverbose(b)) end }
1578
1579-- more helpers: copy from lepd to lpdf
1580
1581function lpdf.copyconstant(v)
1582    if v ~= nil then
1583        return pdfconstant(v)
1584    end
1585end
1586
1587function lpdf.copyboolean(v)
1588    if v ~= nil then
1589        return pdfboolean(v)
1590    end
1591end
1592
1593function lpdf.copyunicode(v)
1594    if v then
1595        return pdfunicode(v)
1596    end
1597end
1598
1599function lpdf.copyarray(a)
1600    if a then
1601        local t = pdfarray()
1602        for i=1,#a do
1603            t[i] = a(i)
1604        end
1605        return t
1606    end
1607end
1608
1609function lpdf.copydictionary(d)
1610    if d then
1611        local t = pdfdictionary()
1612        for k, v in next, d do
1613            t[k] = d(k)
1614        end
1615        return t
1616    end
1617end
1618
1619function lpdf.copynumber(v)
1620    return v
1621end
1622
1623function lpdf.copyinteger(v)
1624    return v -- maybe checking or round ?
1625end
1626
1627function lpdf.copyfloat(v)
1628    return v
1629end
1630
1631function lpdf.copystring(v)
1632    if v then
1633        return pdfstring(v)
1634    end
1635end
1636
1637do
1638
1639    local pdfincludechar, pdfincludecharlist, pdfincludefont
1640    local pdfgetfontname, pdfgetfontobjnum
1641    local pdfsetmapfile, pdfsetmapline
1642
1643    updaters.register("backend.update.lpdf",function()
1644        pdfincludechar     = pdf.includechar
1645        pdfincludefont     = pdf.includefont
1646        pdfincludecharlist = pdf.includecharlist
1647        pdfgetfontname     = pdf.getfontname
1648        pdfgetfontobjnum   = pdf.getfontobjnum
1649        pdfsetmapfile      = pdf.mapfile
1650        pdfsetmapline      = pdf.mapline
1651    end)
1652
1653    function lpdf.includechar(f,c) pdfincludechar(f,c) end
1654    function lpdf.includefont(...) pdfincludefont(...) end
1655
1656    function lpdf.includecharlist(f,c) pdfincludecharlist(f,c) end -- can be disabled
1657
1658    function lpdf.getfontname     (id) return pdfgetfontname  (id) end
1659    function lpdf.getfontobjnumber(id) return pdfgetfontobjnum(id) end
1660
1661    function lpdf.setmapfile(...) pdfsetmapfile(...) end
1662    function lpdf.setmapline(...) pdfsetmapline(...) end
1663
1664end
1665
1666do
1667
1668    -- This is obsolete but old viewers might still use it as directive
1669    -- for what to send to a postscript printer.
1670
1671    local a_procset, d_procset
1672
1673    function lpdf.procset(dict)
1674        if not a_procset then
1675            a_procset = pdfarray {
1676                pdfconstant("PDF"),
1677                pdfconstant("Text"),
1678                pdfconstant("ImageB"),
1679                pdfconstant("ImageC"),
1680                pdfconstant("ImageI"),
1681            }
1682            a_procset = pdfreference(pdfimmediateobject(tostring(a_procset)))
1683        end
1684        if dict then
1685            if not d_procset then
1686                d_procset = pdfdictionary {
1687                    ProcSet = a_procset
1688                }
1689                d_procset = pdfreference(pdfimmediateobject(tostring(d_procset)))
1690            end
1691            return d_procset
1692        else
1693            return a_procset
1694        end
1695    end
1696
1697end
1698
1699-- a left-over
1700
1701if environment.arguments.nocompression then
1702    lpdf.setcompression(0,0,true)
1703end
1704