if not modules then modules = { } end modules ['lpdf-ini'] = {
    version   = 1.001,
    optimize  = true,
    comment   = "companion to lpdf-ini.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- This file is the starting point for PDF related features. Although quite a bit
-- evolved over time, most of what we do in MkIV and LMTX already was available in
-- MkII (with e.g. pdfTeX) anyway, but it was implemented in TeX. We're talking of
-- arbitrary annotations, media like audio and video, widgets (aka forms) with
-- chains and appearances, comments, attachments, javascript based manipulation of
-- layers, graphic trickery like shading, color spaces, transparancy, flash stuff,
-- executing commands, accessing the interface, etc. In that respect there isn't
-- much really new here, after all MkII was there before the turn of the century,
-- but it's just more fun to maintain it in Lua than in low level TeX. Also, because
-- we no longer deal with other engines, there is no need to go low level TeX, which
-- makes for better code.
--
-- However, over the decades PDF evolved and it shows. For instance audio and video
-- support changed and became worse. Some things were dropped (smil, flash, movies,
-- audio). Using appearances for widgets became a pain because it sort of assumes
-- that you construct these forms in acrobat which then leads to bugs becoming
-- features which means that certain things simply don't work (initializations,
-- chained widgets, funny dingabt defaults, etc), probably because they never were
-- tested when viewers evolved.
--
-- Attachment are also a fragile bit. And comments that at some point became
-- dependent on rendering annotations ... it all deserves no beauty price because
-- reliable simplicity was replaced by unreliable complexity. Something that might
-- work today often didn't in the past and might fail in the future, if only because
-- it more relates to the viewer user interface, maybe changing security demands or
-- whatever. We cannot predict this. A side effect is that we keep adapting and even
-- worse, have to remove features that originally were expected to stay (media
-- stuff). To some extend it's a waste of time to get it all supported, also because
-- the open source viewers lag behind. It makes no sense to keep tons of code
-- arround that will never be used (again).
--
-- Also, I don't think that these PDF features were added with something else than
-- Acrobat in mind: a flexible system like TeX that actually could inject these low
-- level features right from the moment that they showed up (and before they were
-- fully tested) is not mainstream enough to be taken into account. One cannot blame
-- a commercial product for its own priorities. The evolution of the web might also
-- have interfered with the agendas.
--
-- As a consequence, the code that we use is spread over files and it might change
-- over time as we try to adapt. But it's easy for the mentioned features to fix one
-- aspect and break another. Eventually we might see more of these fancy features to
-- be removed because they make no sense on the long run, than such features being
-- added. In retrospect maybe many such features were just experiments: anchored in
-- time for throw away documents (like presentations), never meant to be used on the
-- long term. In that respect PDF is a disappointment.

-- Comment: beware of "too many locals" problem here.

-- Is there a way to prevent Acrobat to pop up this "Ask AI assistent" bar ... why
-- should I want an summary of an image or even a text. Should we assume stupid human
-- readers from now on? I really never longed for a summary.

local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset
local concat = table.concat
local char, byte, format, sub, tohex = string.char, string.byte, string.format, string.sub, string.tohex
local utfchar, utfbyte, utfvalues = utf.char, utf.byte, utf.values
local sind, cosd, max, min = math.sind, math.cosd, math.max, math.min
local sort, sortedhash = table.sort, table.sortedhash
local P, C, R, S, Cc, Cs, V = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs, lpeg.V
local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
local formatters = string.formatters
local isboolean = string.is_boolean
local hextointeger, octtointeger = string.hextointeger,string.octtointeger

local report_userdata   = logs.reporter("backend","userdata")
local report_objects    = logs.reporter("backend","objects")
local report_finalizing = logs.reporter("backend","finalizing")
local report_blocked    = logs.reporter("backend","blocked")

local implement         = interfaces and interfaces.implement
local context           = context

-- In ConTeXt MkIV we use utf8 exclusively so all strings get mapped onto a hex
-- encoded utf16 string type between <>. We could probably save some bytes by using
-- strings between () but then we end up with escaped ()\ characters too.

pdf                     = type(pdf) == "table" and pdf or { }
local factor            = number.dimenfactors.bp

local pdfbackend        = backends and backends.registered.pdf or { }
local codeinjections    = pdfbackend.codeinjections
local nodeinjections    = pdfbackend.nodeinjections

lpdf       = lpdf or { }
local lpdf = lpdf
lpdf.flags = lpdf.flags or { } -- will be filled later

table.setmetatableindex(lpdf, function(t,k)
    report_blocked("function %a is not accessible",k)
    os.exit()
end)

local trace_finalizers = false  trackers.register("backend.finalizers", function(v) trace_finalizers = v end)
local trace_resources  = false  trackers.register("backend.resources",  function(v) trace_resources  = v end)

-- helpers

-- local f_hex_4 = formatters["%04X"]
-- local f_hex_2 = formatters["%02X"]

-- local h_hex_4 = table.setmetatableindex(function(t,k) -- we already have this somewhere
--     if k < 0 then
--       --report("fatal h_hex_4 error: %i",k)
--         return "0000"
--     elseif k < 256 then -- maybe 512
--         -- not sparse in this range
--         for i=0,255 do
--             t[i] = f_hex_4(i)
--         end
--         return t[k]
--     else
--         local v = f_hex_4(k)
--         t[k] = v
--         return v
--     end
-- end)

-- local h_hex_2 = table.setmetatableindex(function(t,k) -- we already have this somewhere
--     if type(k) == "string" then
--         local v = f_hex_2(byte(k))
--         t[k] = v
--         return v
--     elseif k < 0 or k > 255 then
--      -- report("fatal h_hex_2 error: %i",k)
--         return "00"
--     else
--         local v = f_hex_2(k)
--         t[k] = v
--         return v
--     end
-- end)

local h_hex_2 = string.hashes.X02
local h_hex_4 = string.hashes.X04

lpdf.h_hex_2 = h_hex_2
lpdf.h_hex_4 = h_hex_4

do

    -- This is for a future feature (still under investigation and consideration). So,
    -- it is work in progress (and brings a harmless overhead for now).

    local initializers = { }

    function lpdf.registerinitializer(initialize)
        initializers[#initializers+1] = initialize
    end

    function lpdf.initialize(f)
        for i=1,#initializers do
            initializers[i]()
        end
    end

end

local pdfreserveobject
local pdfimmediateobject

updaters.register("backends.pdf.latebindings",function()
    pdfreserveobject   = lpdf.reserveobject
    pdfimmediateobject = lpdf.immediateobject
end)

do

    local pdfgetmatrix, pdfhasmatrix, pdfgetpos

    updaters.register("backends.pdf.latebindings",function()
        job.positions.registerhandlers {
            getpos    = drivers.getpos,
            gethvrpos = drivers.gethvrpos,
            gethpos   = drivers.gethpos,
            getvpos   = drivers.getvpos,
            getrpos   = drivers.getrpos,
        }
        pdfgetmatrix = lpdf.getmatrix
        pdfhasmatrix = lpdf.hasmatrix
        pdfgetpos    = drivers.getpos
    end)

    function lpdf.getpos() return pdfgetpos() end

    local function corners(llx,lly,urx,ury,rx,sx,sy,ry)
        return
            llx * rx + lly * sy, llx * sx + lly * ry,
            urx * rx + lly * sy, urx * sx + lly * ry,
            urx * rx + ury * sy, urx * sx + ury * ry,
            llx * rx + ury * sy, llx * sx + ury * ry
    end

    local function bounds(llx,lly,urx,ury,rx,sx,sy,ry)
        local x1 = llx * rx + lly * sy
        local y1 = llx * sx + lly * ry
        local x2 = urx * rx + lly * sy
        local y2 = urx * sx + lly * ry
        local x3 = urx * rx + ury * sy
        local y3 = urx * sx + ury * ry
        local x4 = llx * rx + ury * sy
        local y4 = llx * sx + ury * ry
        llx = min(x1,x2,x3,x4)
        lly = min(y1,y2,y3,y4)
        urx = max(x1,x2,x3,x4)
        ury = max(y1,y2,y3,y4)
        return llx, lly, urx, ury
    end

 -- function lpdf.transform(llx,lly,urx,ury) -- not yet used so unchecked
 --     if pdfhasmatrix() then
 --         local sx, rx, ry, sy = pdfgetmatrix()
 --         return bounds(llx,lly,urx,ury,sx,rx,ry,sy)
 --     else
 --         return llx, lly, urx, ury
 --     end
 -- end

    function lpdf.rectangle(width,height,depth,offset)
        local tx, ty = pdfgetpos()
        if offset then
            tx     = tx     -   offset -- hm, not transformed
            ty     = ty     +   offset -- hm, not transformed
            width  = width  + 2*offset
            height = height +   offset
            depth  = depth  +   offset
        end
        if pdfhasmatrix() then
            local rx, sx, sy, ry, _, _, px, py = pdfgetmatrix()
         -- print("tx px dx",tx*factor,px*factor,dx*factor)
            local llx, lly, urx, ury = bounds(0,-depth,width,height,rx,sx,sy,ry)
         -- tx = tx + sx * height
         -- ty = ty - rx * height + height
            local dx = tx - px
            tx = px + sx * height + dx * rx -- kind of
            ty = py - rx * height + dx * sx -- horrible
            return
                factor * (tx + llx),
                factor * (ty + lly),
                factor * (tx + urx),
                factor * (ty + ury)
        else
            return
                factor *  tx,          -- llx
                factor * (ty - depth), -- lly
                factor * (tx + width), -- urx
                factor * (ty + height) -- ury
        end
    end

    function lpdf.quads(zero,width,height,depth,offset,noscale)
        local tx, ty = pdfgetpos()
        if offset then
            tx     = tx     -   offset -- hm, not transformed
            ty     = ty     +   offset -- hm, not transformed
            width  = width  + 2*offset
            height = height +   offset
            depth  = depth  +   offset
        end
        if pdfhasmatrix() then
            local rx, sx, sy, ry, _, _, px, py = pdfgetmatrix()
            local x1, y1, x2, y2, x3, y3, x4, y4 = corners(zero,-depth,width,height,rx,sx,sy,ry)
         -- tx = tx + sx * height
         -- ty = ty - rx * height + height
            local dx = tx - px
            tx = px + sx * height + dx * rx -- kind of
            ty = py - rx * height + dx * sx -- horrible
            return
                factor * (tx + x1), factor * (ty + y1),
                factor * (tx + x2), factor * (ty + y2),
                factor * (tx + x3), factor * (ty + y3),
                factor * (tx + x4), factor * (ty + y4),
                zero
        else
            local llx = factor *  tx
            local lly = factor * (ty - depth)
            local urx = factor * (tx + width)
            local ury = factor * (ty + height)
            return llx, lly, urx, lly, urx, ury, llx, ury, 0
        end
    end

end

local tosixteen, fromsixteen, topdfdoc, frompdfdoc, toeight, fromeight

do

    local cache = table.setmetatableindex(function(t,k) -- can be made weak
        local v = utfbyte(k)
        if v < 0x10000 then
            v = format("%04x",v)
        else
            v = v - 0x10000
            v = format("%04x%04x",(v>>10)+0xD800,v%1024+0xDC00)
        end
        t[k] = v
        return v
    end)

    local unified = Cs(Cc("<feff") * (lpeg.patterns.utf8character/cache)^1 * Cc(">"))

    tosixteen = function(str) -- an lpeg might be faster (no table)
        if not str or str == "" then
            return "<feff>" -- not () as we want an indication that it's unicode
        else
            return lpegmatch(unified,str)
        end
    end

    -- we could make a helper for this

    local more = 0

    local pattern = C(4) / function(s) -- needs checking !
        local now = hextointeger(s)
        if more > 0 then
            now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000
            more = 0
            return utfchar(now)
        elseif now >= 0xD800 and now <= 0xDBFF then
            more = now
            return "" -- else the c's end up in the stream
        else
            return utfchar(now)
        end
    end

    local pattern = P(true) / function() more = 0 end * Cs(pattern^0)

    fromsixteen = function(str)
        if not str or str == "" then
            return ""
        else
            return lpegmatch(pattern,str)
        end
    end

    local toregime   = regimes and regimes.toregime
    local fromregime = regimes and regimes.fromregime
    local escaped    = Cs(
        Cc("(")
      * (
            S("()\n\r\t\b\f")/"\\%0"
          + P("\\")/"\\\\"
          + P(1)
        )^0
      * Cc(")")
    )

    topdfdoc = function(str,default)
        if not str or str == "" then
            return ""
        else
            return lpegmatch(escaped,toregime("pdfdoc",str,default)) -- could be combined if needed
        end
    end

    frompdfdoc = function(str)
        if not str or str == "" then
            return ""
        else
            return fromregime("pdfdoc",str)
        end
    end

    if not toregime   then topdfdoc   = function(s) return s end end
    if not fromregime then frompdfdoc = function(s) return s end end

    toeight = function(str)
        if not str or str == "" then
            return "()"
        else
            return lpegmatch(escaped,str)
        end
    end

    -- use an oct hash

    local unescape = Cs((
       P("\\")/"" * (
           S("()")
         + S("\n\r")^1 / ""
         + S("nrtbf") / { n = "\n", r = "\r", t = "\t", b = "\b", f = "\f" }
         + (lpegpatterns.octdigit * lpegpatterns.octdigit^-2) / function(s) return char(octtointeger(s)) end
       )
     + P("\\\\") / "\\"
     + P(1)
--      - P(")") -- when inlined
     )^0)

    fromeight = function(str)
        if not str or str == "" then
            return ""
        else
            return lpegmatch(unescape,str)
        end
    end

    lpegpatterns.pdffromeight = unescape

--     local u_pattern = lpegpatterns.utfbom_16_be * lpegpatterns.utf16_to_utf8_be -- official
--                     + lpegpatterns.utfbom_16_le * lpegpatterns.utf16_to_utf8_le -- we've seen these

--     local h_pattern = lpegpatterns.hextobytes

--     local zero = S(" \n\r\t") + P("\\ ")
--     local one  = C(4)
--     local two  = P("d") * R("89","af") * C(2) * C(4)

--     local x_pattern = P { "start",
--         start     = V("wrapped") + V("unwrapped") + V("original"),
--         original  = Cs(P(1)^0),
--         wrapped   = P("<") * V("unwrapped") * P(">") * P(-1),
--         unwrapped = P("feff")
--                   * Cs( (
--                         zero  / ""
--                       + two   / function(a,b)
--                                     a = (hextointeger(a) - 0xD800) * 1024
--                                     b = (hextointeger(b) - 0xDC00)
--                                     return utfchar(a+b)
--                                 end
--                       + one   / function(a)
--                                     return utfchar(hextointeger(a))
--                                 end
--                     )^1 ) * P(-1)
--                   + P("fffe")
--                   * Cs( (
--                         zero  / ""
--                       + two   / function(b,a)
--                                     a = (hextointeger(a) - 0xD800) * 1024
--                                     b = (hextointeger(b) - 0xDC00)
--                                     return utfchar(a+b)
--                                 end
--                       + one   / function(a)
--                                     return utfchar(hextointeger(a))
--                                 end
--                     )^1 ) * P(-1)
--     }

--     function lpdf.frombytes(s,ishex,isutf)
--         if not s or s == "" then
--             return ""
--         elseif ishex then
--             local x = lpegmatch(x_pattern,s)
--             if x then
--                 return x
--             end
--             local h = lpegmatch(h_pattern,s)
--             if h then
--                 return h
--             end
--         else
--             local u = lpegmatch(u_pattern,s)
--             if u then
--                 return u
--             end
--         end
--         return lpegmatch(unescape,s)
--     end

    local encodingvalues = pdfe.getencodingvalues()

    encodingvalues = table.swapped(encodingvalues,encodingvalues)

    lpdf.encodingvalues = encodingvalues

    local utf16flags  = encodingvalues.utf16be | encodingvalues.utf16le
    local utf16toutf8 = string.utf16toutf8

    function lpdf.frombytes(s,flags)
        if not s or s == "" then
            return ""
        elseif flags & utf16flags ~= 0 then
            return utf16toutf8(s)
        else
            return s
        end
    end

    lpdf.tosixteen   = tosixteen
    lpdf.toeight     = toeight
    lpdf.topdfdoc    = topdfdoc
    lpdf.fromsixteen = fromsixteen
    lpdf.fromeight   = fromeight
    lpdf.frompdfdoc  = frompdfdoc

    local more = 0

    local pattern = (
        lpeg.C(4) / function(s) -- needs checking !
            local now = hextointeger(s)
            if more > 0 then
                now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000
                more = 0
                return now
            elseif now >= 0xD800 and now <= 0xDBFF then
                more = now
            else
                return now
            end
        end
    )^1

    lpdf.splitsixteen = function(str)
        if str and str ~= "" then
            more = 0
            return lpegmatch(pattern,str)
        end
    end

end

local pdfescaped do

    local replacer = S("\0\t\n\r\f ()[]{}/%%#\\") / {
        ["\00"]="#00",
        ["\09"]="#09",
        ["\10"]="#0a",
        ["\12"]="#0c",
        ["\13"]="#0d",
        [ " " ]="#20",
        [ "#" ]="#23",
        [ "%" ]="#25",
        [ "(" ]="#28",
        [ ")" ]="#29",
        [ "/" ]="#2f",
        [ "[" ]="#5b",
        [ "\\"]="#5c",
        [ "]" ]="#5d",
        [ "{" ]="#7b",
        [ "}" ]="#7d",
    } + P(1)

    local p_escaped_1 = Cs(Cc("/") * replacer^0)
    local p_escaped_2 = Cs(          replacer^0)

    pdfescaped = function(str,slash)
        return lpegmatch(slash and p_escaped_1 or p_escaped_2,str) or str
    end

    lpdf.escaped = pdfescaped

end

local tostring_a, tostring_d

do

    local f_key_null       = formatters["%s null"]
    local f_key_value      = formatters["%s %s"]
 -- local f_key_dictionary = formatters["%s << % t >>"]
 -- local f_dictionary     = formatters["<< % t >>"]
    local f_key_dictionary = formatters["%s << %s >>"]
    local f_dictionary     = formatters["<< %s >>"]
 -- local f_key_array      = formatters["%s [ % t ]"]
 -- local f_array          = formatters["[ % t ]"]
    local f_key_array      = formatters["%s [ %s ]"]
    local f_array          = formatters["[ %s ]"]
    local f_key_number     = formatters["%s %N"]  -- always with max 9 digits and integer is possible
    local f_tonumber       = formatters["%N"]     -- always with max 9 digits and integer is possible

    tostring_d = function(t,contentonly,key)
        if next(t) then
            local r = { }
            local n = 0
            local e
            for k, v in next, t do
                if k == "__extra__" then
                    e = v
                elseif k == "__stream__" then
                    -- do nothing (yet)
                else
                    n = n + 1
                    r[n] = k
                end
            end
            if n > 1 then
                sort(r)
            end
            for i=1,n do
                local k  = r[i]
                local v  = t[k]
                local tv = type(v)
                -- mostly tables
                --
                k = pdfescaped(k,true)
                --
                if tv == "table" then
                 -- local mv = getmetatable(v)
                 -- if mv and mv.__lpdftype then
                    if v.__lpdftype__ then
                     -- if v == t then
                     --     report_objects("ignoring circular reference in dictionary")
                     --     r[i] = f_key_null(k)
                     -- else
                            r[i] = f_key_value(k,tostring(v))
                     -- end
                    elseif v[1] then
                        r[i] = f_key_value(k,tostring_a(v))
                    else
                        r[i] = f_key_value(k,tostring_d(v))
                    end
                elseif tv == "string" then
                    r[i] = f_key_value(k,toeight(v))
                elseif tv == "number" then
                    r[i] = f_key_number(k,v)
                else
                    r[i] = f_key_value(k,tostring(v))
                end
            end
            if e then
                r[n+1] = e
            end
            r = concat(r," ")
            if contentonly then
                return r
            elseif key then
                return f_key_dictionary(pdfescaped(key,true),r)
            else
                return f_dictionary(r)
            end
        elseif contentonly then
            return ""
        else
            return "<< >>"
        end
    end

    tostring_a = function(t,contentonly,key)
        local tn = #t
        if tn ~= 0 then
            local r = { }
            for k=1,tn do
                local v = t[k]
                local tv = type(v)
                -- mostly numbers and tables
                if tv == "number" then
                    if v == 0 then
                        r[k] = 0
                    else
                        r[k] = f_tonumber(v)
                    end
                elseif tv == "table" then
                 -- local mv = getmetatable(v)
                 -- if mv and mv.__lpdftype then
                    if v.__lpdftype__ then
                     -- if v == t then
                     --     report_objects("ignoring circular reference in array")
                     --     r[k] = "null"
                     -- else
                            r[k] = tostring(v)
                     -- end
                    elseif v[1] then
                        r[k] = tostring_a(v)
                    else
                        r[k] = tostring_d(v)
                    end
                elseif tv == "string" then
                    r[k] = toeight(v)
                else
                    r[k] = tostring(v)
                end
            end
            local e = t.__extra__
            if e then
                r[tn+1] = e
            end
            r = concat(r," ")
            if contentonly then
                return r
            elseif key then
                return f_key_array(pdfescaped(key,true),r)
            else
                return f_array(r)
            end
        elseif contentonly then
            return ""
        else
            return "[ ]"
        end
    end

end

local f_tonumber = formatters["%N"]

local tostring_x = function(t) return concat(t," ")       end
local tostring_s = function(t) return toeight(t[1])       end
local tostring_p = function(t) return topdfdoc(t[1],t[2]) end
local tostring_u = function(t) return tosixteen(t[1])     end
----- tostring_n = function(t) return tostring(t[1])      end -- tostring not needed
local tostring_n = function(t) return f_tonumber(t[1])    end -- tostring not needed
local tostring_c = function(t) return t[1]                end -- already prefixed (hashed)
local tostring_z = function()  return "null"              end
local tostring_t = function()  return "true"              end
local tostring_f = function()  return "false"             end
local tostring_r = function(t) local n = t[1] return n and n > 0 and (n .. " 0 R") or "null" end

local tostring_v = function(t)
    local s = t[1]
    if type(s) == "table" then
        return concat(s)
    else
        return s
    end
end

local tostring_l = function(t)
    local s = t[1]
    if not s or s == "" then
        return "()"
    elseif t[2] then
        return "<" .. s .. ">"
    else
        -- we could convert to hex
        return toeight(s)
    end
end

local function value_x(t) return t                  end
local function value_s(t) return t[1]               end
local function value_p(t) return t[1]               end
local function value_u(t) return t[1]               end
local function value_n(t) return t[1]               end
local function value_c(t) return sub(t[1],2)        end
local function value_d(t) return tostring_d(t,true) end
local function value_a(t) return tostring_a(t,true) end
local function value_z()  return nil                end
local function value_t(t) return t.value or true    end
local function value_f(t) return t.value or false   end
local function value_r(t) return t[1] or 0          end -- null
local function value_v(t) return t[1]               end
local function value_l(t) return t[1]               end

local function add_to_d(t,v)
    local k = type(v)
    if k == "string" then
        if t.__extra__ then
            t.__extra__ = t.__extra__ .. " " .. v
        else
            t.__extra__ = v
        end
    elseif k == "table" then
        for kk, vv in next, v do
            local tkk = rawget(t,kk)
            if tkk and tostring(tkk) ~= tostring(vv) then
                report_objects("duplicate key %a with different values",kk)
            end
            t[kk] = vv
        end
    end
    return t
end

local function add_to_a(t,v)
    local k = type(v)
    if k == "string" then
        if t.__extra__ then
            t.__extra__ = t.__extra__ .. " " .. v
        else
            t.__extra__ = v
        end
    elseif k == "table" then
        local n = #t
        for i=1,#v do
            n = n + 1
            t[n] = v[i]
        end
    end
    return t
end

local function add_x(t,k,v) rawset(t,k,tostring(v)) end

local mt_x = { __index = { __lpdftype__ = "stream"     }, __tostring = tostring_x, __call = value_x, __newindex = add_x }
local mt_d = { __index = { __lpdftype__ = "dictionary" }, __tostring = tostring_d, __call = value_d, __add = add_to_d }
local mt_a = { __index = { __lpdftype__ = "array"      }, __tostring = tostring_a, __call = value_a, __add = add_to_a }
local mt_u = { __index = { __lpdftype__ = "unicode"    }, __tostring = tostring_u, __call = value_u }
local mt_s = { __index = { __lpdftype__ = "string"     }, __tostring = tostring_s, __call = value_s }
local mt_p = { __index = { __lpdftype__ = "docstring"  }, __tostring = tostring_p, __call = value_p }
local mt_n = { __index = { __lpdftype__ = "number"     }, __tostring = tostring_n, __call = value_n }
local mt_c = { __index = { __lpdftype__ = "constant"   }, __tostring = tostring_c, __call = value_c }
local mt_z = { __index = { __lpdftype__ = "null"       }, __tostring = tostring_z, __call = value_z }
local mt_t = { __index = { __lpdftype__ = "true"       }, __tostring = tostring_t, __call = value_t }
local mt_f = { __index = { __lpdftype__ = "false"      }, __tostring = tostring_f, __call = value_f }
local mt_r = { __index = { __lpdftype__ = "reference"  }, __tostring = tostring_r, __call = value_r }
local mt_v = { __index = { __lpdftype__ = "verbose"    }, __tostring = tostring_v, __call = value_v }
local mt_l = { __index = { __lpdftype__ = "literal"    }, __tostring = tostring_l, __call = value_l }

do

    -- these can become helpers

    local find = string.find

    local tostring_u1 = function(t)
        local s = t[1]
        if #s == 0 then
            return "()"
        elseif find(s,"[\x00-\x1F\x27-\x28\x80-\xBF]") then
            -- nop: control codes         : 0x00 .. 0x1F
            -- yes: before left parent    : we don't like escaping
            -- nop: left and right parent : 0x27 .. 0x28
            -- yes: after right parent    : we don't like escaping
            -- nop: after tilde           : 0x80 .. 0xBF
            return tosixteen(s)
        else
            return "(" .. s .. ")"
        end
    end

    local tostring_u2 = function(t)
        local s = t[1]
        if #s == 0 then
            return "()"
        elseif find(s,"[\x80-\xBF]") then
         -- return "(\xEF\xBB\xBF" .. s .. ")"
            return "(\239\187\191" .. s .. ")"
        else
            return "(" .. s .. ")"
        end
    end

    local function setutfstringmode(mode)
        tostring_u = mode and tostring_u2 or tostring_u1
        mt_u.__tostring = tostring_u
    end

    lpdf.setutfstringmode = setutfstringmode

    directives.register("backend.pdf.utf", setutfstringmode)

    setutfstringmode(false)

end


local function pdfstream(t) -- we need to add attributes
    if t then
        local tt = type(t)
        if tt == "table" then
            for i=1,#t do
                t[i] = tostring(t[i])
            end
        elseif tt == "string" then
            t = { t }
        else
            t = { tostring(t) }
        end
    end
    return setmetatable(t or { },mt_x)
end

local function pdfdictionary(t)
    return setmetatable(t or { },mt_d)
end

local function pdfarray(t)
    if type(t) == "string" then
        return setmetatable({ t },mt_a)
    else
        return setmetatable(t or { },mt_a)
    end
end

local function pdfstring(str,default)
    return setmetatable({ str or default or "" },mt_s)
end

local function pdfdocstring(str,default,defaultchar)
    return setmetatable({ str or default or "", defaultchar or " " },mt_p)
end

local function pdfunicode(str,default)
    return setmetatable({ str or default or "" },mt_u) -- could be a string
end

local function pdfliteral(str,hex) -- can also produce a hex <> instead of () literal
    return setmetatable({ str, hex },mt_l)
end

local pdfnumber, pdfconstant

do

    local cache = { } -- can be weak

    pdfnumber = function(n,default) -- 0-10
        if not n then
            n = default
        end
        local c = cache[n]
        if not c then
            c = setmetatable({ n },mt_n)
        --  cache[n] = c -- too many numbers
        end
        return c
    end

    for i=-1,9 do cache[i] = pdfnumber(i) end

    local escaped = lpdf.escaped

    local cache = table.setmetatableindex(function(t,k)
        local v = setmetatable({ escaped(k,true) }, mt_c)
        t[k] = v
        return v
    end)

    pdfconstant = function(str,default)
        if not str then
            str = default or "none"
        end
        return cache[str]
    end

end

local pdfnull, pdfboolean, pdfreference, pdfverbose

do

    local p_null  = { } setmetatable(p_null, mt_z)
    local p_true  = { } setmetatable(p_true, mt_t)
    local p_false = { } setmetatable(p_false,mt_f)

    pdfnull = function()
        return p_null
    end

    pdfboolean = function(b,default)
        if type(b) == "boolean" then
            return b and p_true or p_false
        else
            return default and p_true or p_false
        end
    end

    -- print(pdfboolean(false),pdfboolean(false,false),pdfboolean(false,true))
    -- print(pdfboolean(true),pdfboolean(true,false),pdfboolean(true,true))
    -- print(pdfboolean(nil,true),pdfboolean(nil,false))

    local r_zero = setmetatable({ 0 },mt_r)

    pdfreference = function(r)  -- maybe make a weak table
        if r and r ~= 0 then
            return setmetatable({ r },mt_r)
        else
            return r_zero
        end
    end

    local v_zero  = setmetatable({ 0  },mt_v)
    local v_empty = setmetatable({ "" },mt_v)

    pdfverbose = function(t) -- maybe check for type
        if t == 0 then
            return v_zero
        elseif t == "" then
            return v_empty
        else
            return setmetatable({ t },mt_v)
        end
    end

end

lpdf.stream      = pdfstream -- THIS WILL PROBABLY CHANGE
lpdf.dictionary  = pdfdictionary
lpdf.array       = pdfarray
lpdf.docstring   = pdfdocstring
lpdf.string      = pdfstring
lpdf.unicode     = pdfunicode
lpdf.number      = pdfnumber
lpdf.constant    = pdfconstant
lpdf.null        = pdfnull
lpdf.boolean     = pdfboolean
lpdf.reference   = pdfreference
lpdf.verbose     = pdfverbose
lpdf.literal     = pdfliteral


do

    local options = {
     -- unknown  = 0x0001, -- bit  1
     -- unknown  = 0x0002, -- bit  2
        print    = 0x0004, -- bit  3
        modify   = 0x0008, -- bit  4
        extract  = 0x0010, -- bit  5
        add      = 0x0020, -- bit  6
     -- unknown  = 0x0040, -- bit  7
     -- unknown  = 0x0080, -- bit  8
        fillin   = 0x0100, -- bit  9
        access   = 0x0200, -- bit 10
        assemble = 0x0400, -- bit 11
        quality  = 0x0800, -- bit 12
     -- unknown  = 0x1000, -- bit 13
     -- unknown  = 0x2000, -- bit 14
     -- unknown  = 0x4000, -- bit 15
     -- unknown  = 0x8000, -- bit 16
    }

    lpdf.permissions = options

    function lpdf.topermissions(permissions)
        if permissions and permissions > 0 then
            local t = { }
            for k, v in next, options do
                if permissions & v ~= 0 then
                    t[k] = true
                end
            end
            return t
        end
    end

end

if not callbacks then return lpdf end

-- three priority levels, default=2

local pagefinalizers     = { { }, { }, { } }
local documentfinalizers = { { }, { }, { } }

local pageresources, pageattributes, pagesattributes

local function resetpageproperties()
    pageresources   = pdfdictionary()
    pageattributes  = pdfdictionary()
    pagesattributes = pdfdictionary()
end

function lpdf.getpageproperties()
    return {
        pageresources   = pageresources,
        pageattributes  = pageattributes,
        pagesattributes = pagesattributes,
    }
end

resetpageproperties()

lpdf.registerinitializer(resetpageproperties)

local function addtopageresources  (k,v) pageresources  [k] = v end
local function addtopageattributes (k,v) pageattributes [k] = v end
local function addtopagesattributes(k,v) pagesattributes[k] = v end

lpdf.addtopageresources   = addtopageresources
lpdf.addtopageattributes  = addtopageattributes
lpdf.addtopagesattributes = addtopagesattributes

local function set(where,what,f,when,comment)
    if type(when) == "string" then
        when, comment = 2, when
    elseif not when then
        when = 2
    end
    local w = where[when]
    w[#w+1] = { f, comment }
    if trace_finalizers then
        report_finalizing("%s set: [%s,%s]",what,when,#w)
    end
end

local function run(where,what)
    if trace_finalizers then
        report_finalizing("start backend, category %a, n %a",what,#where)
    end
    for i=1,#where do
        local w = where[i]
        for j=1,#w do
            local wj = w[j]
            if trace_finalizers then
                report_finalizing("%s finalizer: [%s,%s] %s",what,i,j,wj[2] or "")
            end
            wj[1]()
        end
    end
    if trace_finalizers then
        report_finalizing("stop finalizing")
    end
end

local function registerpagefinalizer(f,when,comment)
    set(pagefinalizers,"page",f,when,comment)
end

local function registerdocumentfinalizer(f,when,comment)
    set(documentfinalizers,"document",f,when,comment)
end

lpdf.registerpagefinalizer     = registerpagefinalizer
lpdf.registerdocumentfinalizer = registerdocumentfinalizer

function lpdf.finalizepage(shipout)
    if shipout and not environment.initex then
     -- resetpageproperties() -- maybe better before
        run(pagefinalizers,"page")
        resetpageproperties() -- maybe better before
    end
end

local finalized = false

function lpdf.finalizedocument()
    if not environment.initex and not finalized then
        run(documentfinalizers,"document")
        finalized = true
    end
end

do

    -- some minimal tracing, handy for checking the order

    local function trace_set(what,key)
        if trace_resources then
            report_finalizing("setting key %a in %a",key,what)
        end
    end

    local function trace_flush(what)
        if trace_resources then
            report_finalizing("flushing %a",what)
        end
    end

    lpdf.protectresources = true

    local info    = pdfdictionary { Type = pdfconstant("Info") }
    local catalog = pdfdictionary { Type = pdfconstant("Catalog") }
    ----- names   = pdfdictionary { Type = pdfconstant("Names")   }

    local function checkcatalog()
        if environment.initex then
            -- nothing
        else
            trace_flush("catalog")
            return true
        end
    end

    local function checkinfo()
        if environment.initex then
            -- nothing
        elseif lpdf.getformatoption("info") then
            trace_flush("info")
            if lpdf.majorversion() > 1 then
                for k, v in next, info do
                    if k == "CreationDate" or k == "ModDate" then
                        -- mandate >= 2.0
                    else
                        info[k] = nil
                    end
                end
            end
            return true
        end
    end

    local function flushcatalog()
        if checkcatalog() then
            catalog.Type = nil
        end
    end

    local function flushinfo()
        if checkinfo() then
            info.Type = nil
        end
    end

    local userdata = nil
    local comments = nil

    function codeinjections.setuserdata(key,value)
        if not userdata then
            userdata = pdfdictionary()
        end
        if userdata[key] then
            report_userdata("entry %a is already set",key)
        else
            userdata[key] = pdfunicode(tostring(value))
        end
    end

    function codeinjections.setjobcomment(value)
        if not comments then
            comments = pdfarray()
        end
        comments[#comments+1] = pdfunicode(tostring(value))
    end

    function lpdf.getcatalog()
        if checkcatalog() then
            catalog.Type = pdfconstant("Catalog")
            if lpdf.getformatoption("extensions") then
                -- A bit fuzzy and useless. Has to be a direct one (makes one wonder about what drives
                -- these demands, like limitations in certain applications).
                catalog.Extensions = pdfdictionary {
                    Type = pdfconstant("Extensions"),
                    LMTX = pdfdictionary {
                        BaseVersion    = pdfconstant("2.0"),
                        ExtensionLevel = 1,
                    }
                }
            else
                -- We just assume that adding extension info is okay. After all we registered the
                -- namespace and the ptex ones are also accepted.
            end
            local statistics = pdfdictionary {
                FontRegistries = lpdf.noffontregistries(),
            }
            if next(statistics) then
                -- we use this as fast way to check if we have to check for fonts
                -- and can use the fast path
                statistics.Type = pdfconstant("Statistics")
                catalog.LMTX_Statistics = pdfreference(pdfimmediateobject(tostring(statistics)))
            end
            --
            if userdata and next(userdata) then
                userdata.Type = pdfconstant("Userdata")
                catalog.LMTX_Userdata = pdfreference(pdfimmediateobject(tostring(userdata)))
            end
            --
            if comments and next(comments) then
                comments.Type = pdfconstant("Comments")
                catalog.LMTX_Comments = pdfreference(pdfimmediateobject(tostring(comments)))
            end
            --
            local labels = structures.pages.getlabels()
            if labels and next(labels) then
                for k, v in sortedhash(labels) do
                    if type(v) == "table" then
                        labels[k] = pdfarray(v)
                    end
                end
                labels = pdfdictionary {
                    Type   = pdfconstant("Pages"),
                    Labels = pdfdictionary(labels),
                }
                catalog.LMTX_Pages = pdfreference(pdfimmediateobject(tostring(labels)))
            end
            --
            local tagging = lpdf.gettaggingstatus()

            if tagging and next(tagging) then
                tagging = pdfdictionary {
                    Type     = pdfconstant("Tagging"),
                    Version  = "1.00",
                    Profiles = tagging.profiles,
                    Comment  = tagging.comment,
                }
                catalog.LMTX_Tagging = pdfreference(pdfimmediateobject(tostring(tagging)))
            end
            return pdfreference(pdfimmediateobject(tostring(catalog)))
        end
    end

    function lpdf.getinfo()
        if info and checkinfo() then
            return pdfreference(pdfimmediateobject(tostring(info)))
        end
    end

    function lpdf.addtocatalog(k,v)
        if not (lpdf.protectresources and catalog[k]) then
            trace_set("catalog",k)
            catalog[k] = v
        end
    end

    function lpdf.addtoinfo(k,v)
        if type(info) == "table" and not (lpdf.protectresources and info[k]) then
            trace_set("info",k)
            info[k] = v
        end
    end

    local names = pdfdictionary {}

    local function flushnames()
        if next(names) and not environment.initex then
         -- The type used to be present when we started with pdf but at some point disappeared from
         -- the specification, so let's ditch it. The Type key is kind of weird anyway as sometimes
         -- it is mandate and sometimes not.
         -- names.Type = pdfconstant("Names")
            trace_flush("names")
            lpdf.addtocatalog("Names",pdfreference(pdfimmediateobject(tostring(names))))
        end
    end

    function lpdf.addtonames(k,v)
        if not (lpdf.protectresources and names[k]) then
            trace_set("names",  k)
            names  [k] = v
        end
    end

    local r_extgstates, r_colorspaces, r_patterns, r_shades
    local d_extgstates, d_colorspaces, d_patterns, d_shades
    local p_extgstates, p_colorspaces, p_patterns, p_shades

    lpdf.registerinitializer(function()
        r_extgstates = nil ; r_colorspaces = nil ; r_patterns = nil ; r_shades = nil ;
        d_extgstates = nil ; d_colorspaces = nil ; d_patterns = nil ; d_shades = nil ;
        p_extgstates = nil ; p_colorspaces = nil ; p_patterns = nil ; p_shades = nil ;
    end)

    local function checkextgstates () if d_extgstates  then addtopageresources("ExtGState", p_extgstates ) end end
    local function checkcolorspaces() if d_colorspaces then addtopageresources("ColorSpace",p_colorspaces) end end
    local function checkpatterns   () if d_patterns    then addtopageresources("Pattern",   p_patterns   ) end end
    local function checkshades     () if d_shades      then addtopageresources("Shading",   p_shades     ) end end

    local function flushextgstates () if d_extgstates  then trace_flush("extgstates")  pdfimmediateobject(r_extgstates, tostring(d_extgstates )) end end
    local function flushcolorspaces() if d_colorspaces then trace_flush("colorspaces") pdfimmediateobject(r_colorspaces,tostring(d_colorspaces)) end end
    local function flushpatterns   () if d_patterns    then trace_flush("patterns")    pdfimmediateobject(r_patterns,   tostring(d_patterns   )) end end
    local function flushshades     () if d_shades      then trace_flush("shades")      pdfimmediateobject(r_shades,     tostring(d_shades     )) end end

    -- patterns are special as they need resources to so we can get recursive references and in that case
    -- acrobat doesn't show anything (other viewers handle it well)
    --
    -- todo: share them
    -- todo: force when not yet set

    local pdfgetfontobjectnumber

    updaters.register("backends.pdf.latebindings",function()
        pdfgetfontobjectnumber = lpdf.getfontobjectnumber
    end)

    local f_font = formatters["%s%d"]

    function lpdf.collectedresources(options)
        local ExtGState  = d_extgstates  and next(d_extgstates ) and p_extgstates
        local ColorSpace = d_colorspaces and next(d_colorspaces) and p_colorspaces
        local Pattern    = d_patterns    and next(d_patterns   ) and p_patterns
        local Shading    = d_shades      and next(d_shades     ) and p_shades
        local Font
        if options and options.patterns == false then
            Pattern = nil
        end
        local fonts = options and options.fonts
        if fonts and next(fonts) then
            local prefix = options.fontprefix or "F"
            Font = pdfdictionary { }
            for k, v in sortedhash(fonts) do
                Font[f_font(prefix,v)] = pdfreference(pdfgetfontobjectnumber(k))
            end
        end
        if ExtGState or ColorSpace or Pattern or Shading or Font then
            local collected = pdfdictionary {
                ExtGState  = ExtGState,
                ColorSpace = ColorSpace,
                Pattern    = Pattern,
                Shading    = Shading,
                Font       = Font,
            }
            if options and options.serialize == false then
                return collected
            else
                return collected()
            end
        elseif options and options.notempty then
            return nil
        elseif options and options.serialize == false then
            return pdfdictionary { }
        else
            return ""
        end
    end

    function lpdf.adddocumentextgstate (k,v)
        if not d_extgstates then
            r_extgstates = pdfreserveobject()
            d_extgstates = pdfdictionary()
            p_extgstates = pdfreference(r_extgstates)
        end
        d_extgstates[k] = v
    end

    function lpdf.adddocumentcolorspace(k,v)
        if not d_colorspaces then
            r_colorspaces = pdfreserveobject()
            d_colorspaces = pdfdictionary()
            p_colorspaces = pdfreference(r_colorspaces)
        end
        d_colorspaces[k] = v
    end

    function lpdf.adddocumentpattern(k,v)
        if not d_patterns then
            r_patterns = pdfreserveobject()
            d_patterns = pdfdictionary()
            p_patterns = pdfreference(r_patterns)
        end
        d_patterns[k] = v
    end

    function lpdf.adddocumentshade(k,v)
        if not d_shades then
            r_shades = pdfreserveobject()
            d_shades = pdfdictionary()
            p_shades = pdfreference(r_shades)
        end
        d_shades[k] = v
    end

    registerdocumentfinalizer(flushextgstates,3,"extended graphic states")
    registerdocumentfinalizer(flushcolorspaces,3,"color spaces")
    registerdocumentfinalizer(flushpatterns,3,"patterns")
    registerdocumentfinalizer(flushshades,3,"shades")

    registerdocumentfinalizer(flushnames,3,"names") -- before catalog
    registerdocumentfinalizer(flushcatalog,3,"catalog")
    registerdocumentfinalizer(flushinfo,3,"info")

    registerpagefinalizer(checkextgstates,3,"extended graphic states")
    registerpagefinalizer(checkcolorspaces,3,"color spaces")
    registerpagefinalizer(checkpatterns,3,"patterns")
    registerpagefinalizer(checkshades,3,"shades")

end

-- in strc-bkm: lpdf.registerdocumentfinalizer(function() structures.bookmarks.place() end,1)

function lpdf.rotationcm(a)
    local s = sind(a)
    local c = cosd(a)
    return format("%.6F %.6F %.6F %.6F 0 0 cm",c,s,-s,c)
end

-- return nil is nicer in test prints

function lpdf.checkedkey(t,key,variant)
    local pn = t and t[key]
    if pn ~= nil then
        local tn = type(pn)
        if tn == variant then
            if variant == "string" then
                if pn ~= "" then
                    return pn
                end
            elseif variant == "table" then
                if next(pn) then
                    return pn
                end
            else
                return pn
            end
        elseif tn == "string" then
            if variant == "number" then
                return tonumber(pn)
            elseif variant == "boolean" then
                return isboolean(pn,nil,true)
            end
        end
    end
 -- return nil
end

function lpdf.checkedvalue(value,variant) -- code not shared
    if value ~= nil then
        local tv = type(value)
        if tv == variant then
            if variant == "string" then
                if value ~= "" then
                    return value
                end
            elseif variant == "table" then
                if next(value) then
                    return value
                end
            else
                return value
            end
        elseif tv == "string" then
            if variant == "number" then
                return tonumber(value)
            elseif variant == "boolean" then
                return isboolean(value,nil,true)
            end
        end
    end
end

function lpdf.limited(n,min,max,default)
    if not n then
        return default
    else
        n = tonumber(n)
        if not n then
            return default
        elseif n > max then
            return max
        elseif n < min then
            return min
        else
            return n
        end
    end
end

-- The next variant of ActualText is what Taco and I could come up with
-- eventually. As of September 2013 Acrobat copies okay, Sumatra copies a
-- question mark, pdftotext injects an extra space and Okular adds a
-- newline plus space.

-- return formatters["BT /Span << /ActualText (CONTEXT) >> BDC [<feff>] TJ % t EMC ET"](code)

if implement then

    local f_actual_text_p     = formatters["BT /Span << /ActualText <feff%s> >> BDC %s EMC ET"]
    local f_actual_text_b     = formatters["BT /Span << /ActualText <feff%s> >> BDC"]
    local f_actual_text_b_not = formatters["/Span << /ActualText <feff%s> >> BDC"]
    local f_actual_text       = formatters["/Span <</ActualText %s >> BDC"]

    local f_alternative_text  = formatters["/Span <</Alt %s >> BDC"]

    local s_actual_text_e     <const> = "EMC ET"
    local s_actual_text_e_not <const> = "EMC"

    local context   = context
    local pdfdirect = nodes.pool.directliteral -- we can use nuts.write deep down
    local tounicode = fonts.mappings.tounicode

    -- These convert the string:

    function codeinjections.startactualtext(str)
        return f_actual_text(tosixteen(str))
    end

    function codeinjections.stopactualtext()
        return s_actual_text_e_not
    end

    function codeinjections.startalternativetext(str)
        return f_alternative_text(tosixteen(str))
    end

    function codeinjections.stopalternativetext()
        return s_actual_text_e_not
    end

    -- These assume a unicode number of already converted one without bom.

    function codeinjections.unicodetoactualtext(unicode,pdfcode)
        return f_actual_text_p(type(unicode) == "string" and unicode or tounicode(unicode),pdfcode)
    end

    function codeinjections.startunicodetoactualtext(unicode)
        return f_actual_text_b(type(unicode) == "string" and unicode or tounicode(unicode))
    end

    function codeinjections.stopunicodetoactualtext()
        return s_actual_text_e
    end

    function codeinjections.startunicodetoactualtextdirect(unicode)
        return f_actual_text_b_not(type(unicode) == "string" and unicode or tounicode(unicode))
    end

    function codeinjections.stopunicodetoactualtextdirect()
        return s_actual_text_e_not
    end

    implement {
        name      = "startactualtext",
        arguments = "argument",
        actions   = function(str)
            context(pdfdirect(f_actual_text(tosixteen(str))))
        end
    }

    implement {
        name      = "stopactualtext",
        actions   = function()
            context(pdfdirect("EMC"))
        end
    }

    implement {
        name      = "startalternativetext",
        arguments = "argument",
        actions   = function(str)
            context(pdfdirect(f_alternative_text(tosixteen(str))))
        end
    }

    implement {
        name      = "stopalternativetext",
        actions   = function()
            context(pdfdirect("EMC"))
        end
    }

    local setstate  = nodes.nuts.pool.setstate

    function nodeinjections.startalternate(str)
        return setstate(f_actual_text(tosixteen(str)))
    end

    function nodeinjections.stopalternate()
        return setstate("EMC")
    end

    implement {
        name      = "pdfsetuserdata",
        arguments = "2 arguments",
        protected = true,
        public    = true,
        actions   = codeinjections.setuserdata
    }

end

-- Bah, tikz uses \immediate for some reason which is probably a bug, so the usage
-- will deal with that. However, we will not provide the serialization.

if implement then

    implement { name = "pdfbackendcurrentresources",                   public = true, untraced  = true,                            actions = { lpdf.collectedresources, context } }
    implement { name = "pdfbackendsetcatalog",        usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtocatalog }
    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
    implement { name = "pdfbackendsetname",           usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtonames }
    implement { name = "pdfbackendsetpageattribute",  usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopageattributes }
    implement { name = "pdfbackendsetpagesattribute", usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopagesattributes }
    implement { name = "pdfbackendsetpageresource",   usage = "value", public = true, protected = true, arguments = "2 arguments", actions = lpdf.addtopageresources }
    implement { name = "pdfbackendsetextgstate",      usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentextgstate (a,pdfverbose(b)) end }
    implement { name = "pdfbackendsetcolorspace",     usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentcolorspace(a,pdfverbose(b)) end }
    implement { name = "pdfbackendsetpattern",        usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentpattern   (a,pdfverbose(b)) end }
    implement { name = "pdfbackendsetshade",          usage = "value", public = true, protected = true, arguments = "2 arguments", actions = function(a,b) lpdf.adddocumentshade     (a,pdfverbose(b)) end }

end

-- more helpers: copy from lepd to lpdf

function lpdf.copyconstant(v)
    if v ~= nil then
        return pdfconstant(v)
    end
end

function lpdf.copyboolean(v)
    if v ~= nil then
        return pdfboolean(v)
    end
end

function lpdf.copyunicode(v)
    if v then
        return pdfunicode(v)
    end
end

function lpdf.copyarray(a)
    if a then
        local t = pdfarray()
        for i=1,#a do
            t[i] = a(i)
        end
        return t
    end
end

function lpdf.copydictionary(d)
    if d then
        local t = pdfdictionary()
        for k, v in next, d do
            t[k] = d(k)
        end
        return t
    end
end

function lpdf.copynumber(v)
    return v
end

function lpdf.copyinteger(v)
    return v -- maybe checking or round ?
end

function lpdf.copyfloat(v)
    return v
end

function lpdf.copystring(v)
    if v then
        return pdfstring(v)
    end
end

do

    -- This is obsolete but old viewers might still use it as directive for what to send to a
    -- postscript printer. The bad is that even 2.0 validation can bark about it missing but
    -- maybe we should no longer care about that.

    local a_procset = nil
    local d_procset = nil

    lpdf.registerinitializer(function()
        a_procset = nil
        d_procset = nil
    end)

    function lpdf.procset(dict)
        if not lpdf.getformatoption("procsets") or lpdf.majorversion() > 1 then
            return nil
        else
            if not a_procset then
                a_procset = pdfarray {
                    pdfconstant("PDF"),
                    pdfconstant("Text"),
                    pdfconstant("ImageB"),
                    pdfconstant("ImageC"),
                    pdfconstant("ImageI"),
                }
                a_procset = pdfreference(pdfimmediateobject(tostring(a_procset)))
            end
            if dict then
                if not d_procset then
                    d_procset = pdfdictionary {
                        ProcSet = a_procset
                    }
                    d_procset = pdfreference(pdfimmediateobject(tostring(d_procset)))
                end
                return d_procset
            else
                return a_procset
            end
        end
    end

end
