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. 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_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) 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, getrpos = drivers.getrpos, gethpos = drivers.gethpos, getvpos = drivers.getvpos, } pdfgetmatrix = lpdf.getmatrix pdfhasmatrix = lpdf.hasmatrix pdfgetpos = drivers.getpos end) function lpdf.getpos() return pdfgetpos() end -- local function transform(llx,lly,urx,ury,rx,sx,sy,ry) -- local x1 = llx * rx + lly * sy -- local y1 = llx * sx + lly * ry -- local x2 = llx * rx + ury * sy -- local y2 = llx * sx + ury * ry -- local x3 = urx * rx + lly * sy -- local y3 = urx * sx + lly * ry -- local x4 = urx * rx + ury * sy -- local y4 = urx * 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() -- local w, h = urx - llx, ury - lly -- return llx, lly, llx + sy*w - ry*h, lly + sx*h - rx*w -- -- return transform(llx,lly,urx,ury,sx,rx,ry,sy) -- else -- return llx, lly, urx, ury -- end -- end -- funny values for tx and ty function lpdf.rectangle(width,height,depth,offset) local tx, ty = pdfgetpos() if offset then tx = tx - offset ty = ty + offset width = width + 2*offset height = height + offset depth = depth + offset end if pdfhasmatrix() then local rx, sx, sy, ry = pdfgetmatrix() return factor * tx, factor * (ty - ry*depth + sx*width), factor * (tx + rx*width - sy*height), factor * (ty + ry*height - sx*width) else return factor * tx, factor * (ty - depth), factor * (tx + width), factor * (ty + height) 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("")) tosixteen = function(str) -- an lpeg might be faster (no table) if not str or str == "" then return "" -- 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) } function lpdf.frombytes(s,hex) if not s or s == "" then return "" end if hex 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 lpdf.tosixteen = tosixteen lpdf.toeight = toeight lpdf.topdfdoc = topdfdoc lpdf.fromsixteen = fromsixteen lpdf.fromeight = fromeight lpdf.frompdfdoc = frompdfdoc 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 r[k] = f_tonumber(v) 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 return "(" .. 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 k, v in next, v do t[k] = v 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 } 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 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 callbacks.register("finish_pdfpage", lpdf.finalizepage) callbacks.register("finish_pdffile", lpdf.finalizedocument) 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 catalog = pdfdictionary { Type = pdfconstant("Catalog") } -- nicer, but when we assign we nil the Type local info = pdfdictionary { Type = pdfconstant("Info") } -- nicer, but when we assign we nil the Type ----- names = pdfdictionary { Type = pdfconstant("Names") } -- nicer, but when we assign we nil the Type local function checkcatalog() if not environment.initex then trace_flush("catalog") return true end end local function checkinfo() if not environment.initex 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 function lpdf.getcatalog() if checkcatalog() then catalog.Type = pdfconstant("Catalog") return pdfreference(pdfimmediateobject(tostring(catalog))) end end function lpdf.getinfo() if 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 not (lpdf.protectresources and info[k]) then trace_set("info",k) info[k] = v end end local names = pdfdictionary { -- Type = pdfconstant("Names") } local function flushnames() if next(names) and not environment.initex then 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 [] TJ % t EMC ET"](code) if implement then local f_actual_text_p = formatters["BT /Span << /ActualText >> BDC %s EMC ET"] local f_actual_text_b = formatters["BT /Span << /ActualText >> BDC"] local f_actual_text_b_not = formatters["/Span << /ActualText >> BDC"] local f_actual_text = formatters["/Span <> BDC"] local s_actual_text_e = "EMC ET" local s_actual_text_e_not = "EMC" local context = context local pdfdirect = nodes.pool.directliteral -- we can use nuts.write deep down local tounicode = fonts.mappings.tounicode 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 = "string", actions = function(str) context(pdfdirect(f_actual_text(tosixteen(str)))) end } implement { name = "stopactualtext", 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 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. local a_procset, d_procset lpdf.registerinitializer(function() a_procset = nil d_procset = nil end) function lpdf.procset(dict) 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