diff options
Diffstat (limited to 'tex')
44 files changed, 13559 insertions, 259 deletions
diff --git a/tex/context/base/mkii/cont-new.mkii b/tex/context/base/mkii/cont-new.mkii index f3f004ad8..1f3cca5b3 100644 --- a/tex/context/base/mkii/cont-new.mkii +++ b/tex/context/base/mkii/cont-new.mkii @@ -11,7 +11,7 @@ %C therefore copyrighted by \PRAGMA. See mreadme.pdf for %C details. -\newcontextversion{2020.11.30 10:20} +\newcontextversion{2020.12.01 17:48} %D This file is loaded at runtime, thereby providing an %D excellent place for hacks, patches, extensions and new diff --git a/tex/context/base/mkii/context.mkii b/tex/context/base/mkii/context.mkii index e4bc700cb..3bdad465e 100644 --- a/tex/context/base/mkii/context.mkii +++ b/tex/context/base/mkii/context.mkii @@ -20,7 +20,7 @@ %D your styles an modules. \edef\contextformat {\jobname} -\edef\contextversion{2020.11.30 10:20} +\edef\contextversion{2020.12.01 17:48} %D For those who want to use this: diff --git a/tex/context/base/mkiv/cont-new.mkiv b/tex/context/base/mkiv/cont-new.mkiv index fe5504ffe..ee813411b 100644 --- a/tex/context/base/mkiv/cont-new.mkiv +++ b/tex/context/base/mkiv/cont-new.mkiv @@ -13,7 +13,7 @@ % \normalend % uncomment this to get the real base runtime -\newcontextversion{2020.11.30 10:20} +\newcontextversion{2020.12.01 17:48} %D This file is loaded at runtime, thereby providing an excellent place for hacks, %D patches, extensions and new features. There can be local overloads in cont-loc diff --git a/tex/context/base/mkiv/context.mkiv b/tex/context/base/mkiv/context.mkiv index 46eab5afe..5b566f070 100644 --- a/tex/context/base/mkiv/context.mkiv +++ b/tex/context/base/mkiv/context.mkiv @@ -45,7 +45,7 @@ %D {YYYY.MM.DD HH:MM} format. \edef\contextformat {\jobname} -\edef\contextversion{2020.11.30 10:20} +\edef\contextversion{2020.12.01 17:48} %D Kind of special: diff --git a/tex/context/base/mkiv/font-imp-effects.lua b/tex/context/base/mkiv/font-imp-effects.lua index bd6cce879..ee9f644a9 100644 --- a/tex/context/base/mkiv/font-imp-effects.lua +++ b/tex/context/base/mkiv/font-imp-effects.lua @@ -281,7 +281,7 @@ end -- local show_effect = { "lua", "print('!')" } ----- shiftmode = false -- test in mkiv and lmtx -local shiftmode = CONTEXTLMTXMODE > 0 +local shiftmode = CONTEXTLMTXMODE and CONTEXTLMTXMODE > 0 local function manipulateeffect(tfmdata) local effect = tfmdata.properties.effect diff --git a/tex/context/base/mkiv/l-os.lua b/tex/context/base/mkiv/l-os.lua index 1e0135094..73841074c 100644 --- a/tex/context/base/mkiv/l-os.lua +++ b/tex/context/base/mkiv/l-os.lua @@ -358,6 +358,8 @@ elseif name == "macosx" then platform = "osx-intel" elseif find(architecture,"x86_64",1,true) then platform = "osx-64" + elseif find(architecture,"arm64",1,true) then + platform = "osx-64" else platform = "osx-ppc" end diff --git a/tex/context/base/mkiv/lpdf-ini.lua b/tex/context/base/mkiv/lpdf-ini.lua index 1f6dac938..c27270747 100644 --- a/tex/context/base/mkiv/lpdf-ini.lua +++ b/tex/context/base/mkiv/lpdf-ini.lua @@ -8,6 +8,9 @@ if not modules then modules = { } end modules ['lpdf-ini'] = { } -- beware of "too many locals" here +-- +-- The lua files are still hybrid ones but we keep that as a reference for the +-- lmt variants that started out as copies. local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset local char, byte, format, gsub, concat, match, sub, gmatch = string.char, string.byte, string.format, string.gsub, table.concat, string.match, string.sub, string.gmatch diff --git a/tex/context/base/mkiv/mult-prm.lua b/tex/context/base/mkiv/mult-prm.lua index 725253995..97bc83ff1 100644 --- a/tex/context/base/mkiv/mult-prm.lua +++ b/tex/context/base/mkiv/mult-prm.lua @@ -295,6 +295,7 @@ return { "gleaders", "glet", "gletcsname", + "gluespecdef", "glyphdatafield", "glyphdimensionsmode", "glyphoptions", @@ -384,6 +385,7 @@ return { "mathstyle", "mathsurroundmode", "mathsurroundskip", + "mugluespecdef", "mutable", "noaligned", "noboundary", diff --git a/tex/context/base/mkiv/status-files.pdf b/tex/context/base/mkiv/status-files.pdf Binary files differindex dc575dca5..ea407d533 100644 --- a/tex/context/base/mkiv/status-files.pdf +++ b/tex/context/base/mkiv/status-files.pdf diff --git a/tex/context/base/mkiv/status-lua.pdf b/tex/context/base/mkiv/status-lua.pdf Binary files differindex 216c5f4c9..97cefc1a8 100644 --- a/tex/context/base/mkiv/status-lua.pdf +++ b/tex/context/base/mkiv/status-lua.pdf diff --git a/tex/context/base/mkxl/back-pdf.lmt b/tex/context/base/mkxl/back-pdf.lmt new file mode 100644 index 000000000..44d0230bd --- /dev/null +++ b/tex/context/base/mkxl/back-pdf.lmt @@ -0,0 +1,55 @@ +if not modules then modules = { } end modules ['back-pdf'] = { + version = 1.001, + comment = "companion to back-pdf.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files" +} + +-- We hide the pdf table from users so that we can guarantee no interference with +-- the way we manage resources, info, etc. Users should use the \type {lpdf} +-- interface instead. If needed I will provide replacement functionality. + +local setmetatableindex = table.setmetatableindex + +local pdfsetcompression +local pdfimmediateobject + +updaters.register("backend.update.lpdf",function() + pdfsetcompression = lpdf.setcompression + pdfimmediateobject = lpdf.immediateobject +end) + +interfaces.implement { + name = "setpdfcompression", + arguments = { "integer", "integer" }, + actions = function(...) pdfsetcompression(...) end, +} + +do + + local dummy = function() end + local report = logs.reporter("backend") + + local function unavailable(t,k) + report("calling unavailable pdf.%s function",k) + t[k] = dummy + return dummy + end + + updaters.register("backend.update",function() + -- + -- For now we keep this for tikz. If really needed some more can be made + -- accessible but it has to happen in a controlled way then, for instance + -- by first loading or enabling some compatibility layer so that we can + -- trace possible interferences. + -- + pdf = { + immediateobj = pdfimmediateobject + } + setmetatableindex(pdf,unavailable) + end) + +end + +backends.install("pdf") diff --git a/tex/context/base/mkxl/back-pdf.mkxl b/tex/context/base/mkxl/back-pdf.mkxl index 18aa8354b..171c6e7b6 100644 --- a/tex/context/base/mkxl/back-pdf.mkxl +++ b/tex/context/base/mkxl/back-pdf.mkxl @@ -17,31 +17,31 @@ \writestatus{loading}{ConTeXt Backend Macros / PDF} -\registerctxluafile{lpdf-ini}{optimize} +\registerctxluafile{lpdf-ini}{autosuffix,optimize} \registerctxluafile{lpdf-lmt}{autosuffix,optimize} -\registerctxluafile{lpdf-col}{} +\registerctxluafile{lpdf-col}{autosuffix} \registerctxluafile{lpdf-vfc}{autosuffix} -\registerctxluafile{lpdf-xmp}{} -\registerctxluafile{lpdf-ano}{} -\registerctxluafile{lpdf-res}{} -\registerctxluafile{lpdf-mis}{} -\registerctxluafile{lpdf-ren}{} -\registerctxluafile{lpdf-grp}{} -\registerctxluafile{lpdf-wid}{} -\registerctxluafile{lpdf-fld}{} -\registerctxluafile{lpdf-mov}{} -\registerctxluafile{lpdf-u3d}{} % this will become a module +\registerctxluafile{lpdf-xmp}{autosuffix} +\registerctxluafile{lpdf-ano}{autosuffix} +\registerctxluafile{lpdf-res}{autosuffix} +\registerctxluafile{lpdf-mis}{autosuffix} +\registerctxluafile{lpdf-ren}{autosuffix} +\registerctxluafile{lpdf-grp}{autosuffix} +\registerctxluafile{lpdf-wid}{autosuffix} +\registerctxluafile{lpdf-fld}{autosuffix} +\registerctxluafile{lpdf-mov}{autosuffix} +\registerctxluafile{lpdf-u3d}{autosuffix} % this will become a module %registerctxluafile{lpdf-swf}{} % this will become a module -\registerctxluafile{lpdf-tag}{} -\registerctxluafile{lpdf-fmt}{} -\registerctxluafile{lpdf-pde}{} +\registerctxluafile{lpdf-tag}{autosuffix} +\registerctxluafile{lpdf-fmt}{autosuffix} +\registerctxluafile{lpdf-pde}{autosuffix} \registerctxluafile{lpdf-img}{autosuffix,optimize} -\registerctxluafile{lpdf-epa}{} +\registerctxluafile{lpdf-epa}{autosuffix} \registerctxluafile{lpdf-emb}{autosuffix,optimize} -\registerctxluafile{lpdf-fnt}{} +\registerctxluafile{lpdf-fnt}{autosuffix} -\registerctxluafile{back-pdp}{} -\registerctxluafile{back-pdf}{} % some code will move to lpdf-* +\registerctxluafile{back-pdp}{autosuffix} +\registerctxluafile{back-pdf}{autosuffix} % some code will move to lpdf-* \loadmkxlfile{back-u3d} % this will become a module %loadmkxlfile{back-swf} % this will become a module diff --git a/tex/context/base/mkxl/back-pdp.lmt b/tex/context/base/mkxl/back-pdp.lmt new file mode 100644 index 000000000..1b3a17007 --- /dev/null +++ b/tex/context/base/mkxl/back-pdp.lmt @@ -0,0 +1,291 @@ +if not modules then modules = { } end modules ['back-pdp'] = { + version = 1.001, + 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 is temporary ... awaiting a better test .. basically we can +-- always use this: pdf primitives. + +local context = context +local lpdf = lpdf + +local pdfreserveobject +local pdfcompresslevel +local pdfobject +local pdfpagereference +local pdfgetxformname +local pdfminorversion +local pdfmajorversion + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + pdfcompresslevel = lpdf.compresslevel + pdfobject = lpdf.object + pdfpagereference = lpdf.pagereference + pdfgetxformname = lpdf.getxformname + pdfminorversion = lpdf.minorversion + pdfmajorversion = lpdf.majorversion +end) + +local tokenscanners = tokens.scanners +local scanword = tokenscanners.word +local scankeyword = tokenscanners.keyword +local scanstring = tokenscanners.string +local scaninteger = tokenscanners.integer +local scanwhd = tokenscanners.whd + +local trace = false trackers.register("backend", function(v) trace = v end) +local report = logs.reporter("backend") + +local nodepool = nodes.pool +local newliteral = nodepool.literal +local newsave = nodepool.save +local newrestore = nodepool.restore +local newsetmatrix = nodepool.setmatrix + +local implement = interfaces.implement +local constants = interfaces.constants +local variables = interfaces.variables + +-- literals + +local function pdf_literal() + context(newliteral(scanword() or "origin",scanstring())) +end + +-- objects + +local lastobjnum = 0 + +local function pdf_obj() + if scankeyword("reserveobjnum") then + lastobjnum = pdfreserveobject() + if trace then + report("\\pdfobj reserveobjnum: object %i",lastobjnum) + end + else + local immediate = true + local objnum = scankeyword("useobjnum") and scaninteger() or pdfreserveobject() + local uncompress = scankeyword("uncompressed") or pdfcompresslevel() == 0 + local streamobject = scankeyword("stream") + local attributes = scankeyword("attr") and scanstring() or nil + local fileobject = scankeyword("file") + local content = scanstring() + local object = streamobject and { + type = "stream", + objnum = objnum, + immediate = immediate, + attr = attributes, + compresslevel = uncompress and 0 or nil, + } or { + type = "raw", + objnum = objnum, + immediate = immediate, + } + if fileobject then + object.file = content + -- object.filename = content + else + object.string = content + end + pdfobject(object) + lastobjnum = objnum + if trace then + report("\\pdfobj: object %i",lastobjnum) + end + end +end + +local function pdf_lastobj() + context("%i",lastobjnum) + if trace then + report("\\lastobj: object %i",lastobjnum) + end +end + +local function pdf_refobj() + local objnum = scaninteger() + if trace then + report("\\refobj: object %i (todo)",objnum) + end +end + +-- annotations + +local lastobjnum = 0 + +local function pdf_annot() + if scankeyword("reserveobjnum") then + lastobjnum = pdfreserveobject() + if trace then + report("\\pdfannot reserveobjnum: object %i",lastobjnum) + end + else + local width = false + local height = false + local depth = false + local data = false + local object = false + local attr = false + -- + if scankeyword("useobjnum") then + object = scancount() + report("\\pdfannot useobjectnum is not (yet) supported") + end + local width, height, depth = scanwhd() + if scankeyword("attr") then + attr = scanstring() + end + data = scanstring() + context(backends.nodeinjections.annotation(width or 0,height or 0,depth or 0,data or "")) + end +end + +local function pdf_dest() + local name = false + local zoom = false + local view = false + local width = false + local height = false + local depth = false + if scankeyword("num") then + report("\\pdfdest num is not (yet) supported") + elseif scankeyword("name") then + name = scanstring() + end + if scankeyword("xyz") then + view = "xyz" + if scankeyword("zoom") then + report("\\pdfdest zoom is ignored") + zoom = scancount() -- will be divided by 1000 in the backend + end + elseif scankeyword("fitbh") then + view = "fitbh" + elseif scankeyword("fitbv") then + view = "fitbv" + elseif scankeyword("fitb") then + view = "fitb" + elseif scankeyword("fith") then + view = "fith" + elseif scankeyword("fitv") then + view = "fitv" + elseif scankeyword("fitr") then + view = "fitr" + width, height, depth = scanwhd() + elseif scankeyword("fit") then + view = "fit" + end + context(backends.nodeinjections.destination(width or 0,height or 0,depth or 0,{ name or "" },view or "fit")) +end + +-- management + +local function pdf_save() + context(newsave()) +end + +local function pdf_restore() + context(newrestore()) +end + +local function pdf_setmatrix() + context(newsetmatrix(scanstring())) +end + +-- extras + +-- extensions: literal dest annot save restore setmatrix obj refobj colorstack +-- startlink endlink startthread endthread thread outline glyphtounicode fontattr +-- mapfile mapline includechars catalog info names trailer + +local extensions = { + literal = pdf_literal, + obj = pdf_obj, + refobj = pdf_refobj, + dest = pdf_dest, + annot = pdf_annot, + save = pdf_save, + restore = pdf_restore, + setmatrix = pdf_setmatrix, +} + +local function pdf_extension() + local w = scanword() + if w then + local e = extensions[w] + if e then + e() + else + report("\\pdfextension: unsupported %a",w) + end + end +end + +-- feedbacks: colorstackinit creationdate fontname fontobjnum fontsize lastannot +-- lastlink lastobj pageref retval revision version xformname + +local feedbacks = { + lastobj = pdf_lastobj, + pageref = function() context(pdfpagereference()) end, + xformname = function() context(pdfgetxformname ()) end, +} + +local function pdf_feedback() + local w = scanword() + if w then + local f = feedbacks[w] + if f then + f() + else + report("\\pdffeedback: unsupported %a",w) + end + end +end + +-- variables: (integers:) compresslevel decimaldigits gamma gentounicode +-- ignoreunknownimages imageaddfilename imageapplygamma imagegamma imagehicolor +-- imageresolution inclusioncopyfonts inclusionerrorlevel majorversion minorversion +-- objcompresslevel omitcharset omitcidset pagebox pkfixeddpi pkresolution +-- recompress suppressoptionalinfo uniqueresname (dimensions:) destmargin horigin +-- linkmargin threadmargin vorigin xformmargin (tokenlists:) pageattr pageresources +-- pagesattr pkmode trailerid xformattr xformresources + +local variables = { + minorversion = function() context(pdfminorversion()) end, + majorversion = function() context(pdfmajorversion()) end, +} + +local function pdf_variable() + local w = scanword() + if w then + local f = variables[w] + if f then + f() + else + report("\\pdfvariable: unsupported %a",w) + end + else + print("missing variable") + end +end + +-- kept: + +implement { name = "pdfextension", actions = pdf_extension } +implement { name = "pdffeedback", actions = pdf_feedback } +implement { name = "pdfvariable", actions = pdf_variable } + +-- for the moment (tikz) + +implement { name = "pdfliteral", actions = pdf_literal } +implement { name = "pdfobj", actions = pdf_obj } +implement { name = "pdflastobj", actions = pdf_lastobj } +implement { name = "pdfrefobj", actions = pdf_refobj } +--------- { name = "pdfannot", actions = pdf_annot } +--------- { name = "pdfdest", actions = pdf_dest } +--------- { name = "pdfsave", actions = pdf_save } +--------- { name = "pdfrestore", actions = pdf_restore } +--------- { name = "pdfsetmatrix", actions = pdf_setmatrix } diff --git a/tex/context/base/mkxl/cont-new.mkxl b/tex/context/base/mkxl/cont-new.mkxl index f4ce10be5..9ba35aa2a 100644 --- a/tex/context/base/mkxl/cont-new.mkxl +++ b/tex/context/base/mkxl/cont-new.mkxl @@ -13,7 +13,7 @@ % \normalend % uncomment this to get the real base runtime -\newcontextversion{2020.11.30 10:20} +\newcontextversion{2020.12.01 17:48} %D This file is loaded at runtime, thereby providing an excellent place for hacks, %D patches, extensions and new features. There can be local overloads in cont-loc diff --git a/tex/context/base/mkxl/context.mkxl b/tex/context/base/mkxl/context.mkxl index 613e650de..e6b88efc5 100644 --- a/tex/context/base/mkxl/context.mkxl +++ b/tex/context/base/mkxl/context.mkxl @@ -29,7 +29,7 @@ %D {YYYY.MM.DD HH:MM} format. \immutable\edef\contextformat {\jobname} -\immutable\edef\contextversion{2020.11.30 10:20} +\immutable\edef\contextversion{2020.12.01 17:48} %overloadmode 1 % check frozen / warning %overloadmode 2 % check frozen / error diff --git a/tex/context/base/mkxl/enco-ini.mkxl b/tex/context/base/mkxl/enco-ini.mkxl index 6dd800bf3..9bf442eec 100644 --- a/tex/context/base/mkxl/enco-ini.mkxl +++ b/tex/context/base/mkxl/enco-ini.mkxl @@ -383,7 +383,7 @@ \permanent\protected\def\normalunderscore{\ifmmode\mathunderscore\else\textunderscore\fi} \pushoverloadmode - \let\_\normalunderscore + \enforced\let\_\normalunderscore \popoverloadmode %D To be sorted out: diff --git a/tex/context/base/mkxl/lpdf-ano.lmt b/tex/context/base/mkxl/lpdf-ano.lmt new file mode 100644 index 000000000..86bcd4ad5 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-ano.lmt @@ -0,0 +1,1401 @@ +if not modules then modules = { } end modules ['lpdf-ano'] = { + version = 1.001, + 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" +} + +-- when using rotation: \disabledirectives[refences.sharelinks] (maybe flag links) + +-- todo: /AA << WC << ... >> >> : WillClose actions etc + +-- internal references are indicated by a number (and turned into <autoprefix><number>) +-- we only flush internal destinations that are referred + +local next, tostring, tonumber, rawget, type = next, tostring, tonumber, rawget, type +local rep, format, find = string.rep, string.format, string.find +local min = math.min +local lpegmatch = lpeg.match +local formatters = string.formatters +local sortedkeys, concat = table.sortedkeys, table.concat + +local backends, lpdf = backends, lpdf + +local trace_references = false trackers.register("references.references", function(v) trace_references = v end) +local trace_destinations = false trackers.register("references.destinations", function(v) trace_destinations = v end) +local trace_bookmarks = false trackers.register("references.bookmarks", function(v) trace_bookmarks = v end) + +local log_destinations = false directives.register("destinations.log", function(v) log_destinations = v end) +local untex_urls = true directives.register("references.untexurls", function(v) untex_urls = v end) + +local report_references = logs.reporter("backend","references") +local report_destinations = logs.reporter("backend","destinations") +local report_bookmarks = logs.reporter("backend","bookmarks") + +local variables = interfaces.variables +local v_auto = variables.auto +local v_page = variables.page +local v_name = variables.name + +local factor = number.dimenfactors.bp + +local settings_to_array = utilities.parsers.settings_to_array + +local allocate = utilities.storage.allocate +local setmetatableindex = table.setmetatableindex + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations + +local javascriptcode = interactions.javascripts.code + +local references = structures.references +local bookmarks = structures.bookmarks + +local flaginternals = references.flaginternals +local usedinternals = references.usedinternals +local usedviews = references.usedviews + +local runners = references.runners +local specials = references.specials +local handlers = references.handlers +local executers = references.executers + +local nodepool = nodes.pool + +local new_latelua = nodepool.latelua + +local texgetcount = tex.getcount + +local jobpositions = job.positions +local getpos = jobpositions.getpos +local gethpos = jobpositions.gethpos +local getvpos = jobpositions.getvpos + +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfreference = lpdf.reference +local pdfunicode = lpdf.unicode +local pdfconstant = lpdf.constant +local pdfnull = lpdf.null +local pdfaddtocatalog = lpdf.addtocatalog +local pdfaddtonames = lpdf.addtonames +local pdfaddtopageattributes = lpdf.addtopageattributes +local pdfrectangle = lpdf.rectangle + +local pdfflushobject +local pdfshareobjectreference +local pdfreserveobject +local pdfpagereference +local pdfdelayedobject +local pdfregisterannotation + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfshareobjectreference = lpdf.shareobjectreference + pdfreserveobject = lpdf.reserveobject + pdfpagereference = lpdf.pagereference + pdfdelayedobject = lpdf.delayedobject + pdfregisterannotation = lpdf.registerannotation +end) + +-- todo: 3dview + +----- pdf_annot = pdfconstant("Annot") +local pdf_uri = pdfconstant("URI") +local pdf_gotor = pdfconstant("GoToR") +local pdf_goto = pdfconstant("GoTo") +local pdf_launch = pdfconstant("Launch") +local pdf_javascript = pdfconstant("JavaScript") +local pdf_link = pdfconstant("Link") +local pdf_n = pdfconstant("N") +local pdf_t = pdfconstant("T") +local pdf_fit = pdfconstant("Fit") +local pdf_named = pdfconstant("Named") + +local autoprefix = "#" +local usedautoprefixes = { } + +local function registerautoprefix(name) + local internal = autoprefix .. name + if usedautoprefixes[internal] == nil then + usedautoprefixes[internal] = false + end + return internal +end + +local function useautoprefix(name) + local internal = autoprefix .. name + usedautoprefixes[internal] = true + return internal +end + +local function checkautoprefixes(destinations) + for k, v in next, usedautoprefixes do + if not v then + if trace_destinations then + report_destinations("flushing unused autoprefix %a",k) + end + destinations[k] = nil + end + end +end + +local maxslice = 32 -- could be made configureable ... 64 is also ok + +local function pdfmakenametree(list,apply) + if not next(list) then + return + end + local slices = { } + local sorted = sortedkeys(list) + local size = #sorted + local maxslice = maxslice + if size <= 1.5*maxslice then + maxslice = size + end + for i=1,size,maxslice do + local amount = min(i+maxslice-1,size) + local names = pdfarray { } + local n = 0 + for j=i,amount do + local name = sorted[j] + local target = list[name] + n = n + 1 ; names[n] = tostring(name) + n = n + 1 ; names[n] = apply and apply(target) or target + end + local first = sorted[i] + local last = sorted[amount] + local limits = pdfarray { + first, + last, + } + local d = pdfdictionary { + Names = names, + Limits = limits, + } + slices[#slices+1] = { + reference = pdfreference(pdfflushobject(d)), + limits = limits, + } + end + local function collectkids(slices,first,last) + local f = slices[first] + local l = slices[last] + if f and l then + local k = pdfarray() + local n = 0 + local d = pdfdictionary { + Kids = k, + Limits = pdfarray { + f.limits[1], + l.limits[2], + }, + } + for i=first,last do + n = n + 1 ; k[n] = slices[i].reference + end + return d + end + end + if #slices == 1 then + return slices[1].reference + else + while true do + local size = #slices + if size > maxslice then + local temp = { } + local n = 0 + for i=1,size,maxslice do + local kids = collectkids(slices,i,min(i+maxslice-1,size)) + if kids then + n = n + 1 + temp[n] = { + reference = pdfreference(pdfflushobject(kids)), + limits = kids.Limits, + } + else + -- error + end + end + slices = temp + else + local kids = collectkids(slices,1,size) + if kids then + return pdfreference(pdfflushobject(kids)) + else + -- error + return + end + end + end + end +end + +lpdf.makenametree = pdfmakenametree + +-- Bah, I hate this kind of features .. anyway, as we have delayed resolving we +-- only support a document-wide setup and it has to be set before the first one +-- is used. Also, we default to a non-intrusive gray and the outline is kept +-- thin without dashing lines. This is as far as I'm prepared to go. This way +-- it can also be used as a debug feature. + +local pdf_border_style = pdfarray { 0, 0, 0 } -- radius radius linewidth +local pdf_border_color = nil +local set_border = false + +local function pdfborder() + set_border = true + return pdf_border_style, pdf_border_color +end + +lpdf.border = pdfborder + +directives.register("references.border",function(v) + if v and not set_border then + if type(v) == "string" then + local m = attributes.list[attributes.private('color')] or { } + local c = m and m[v] + local v = c and attributes.colors.value(c) + if v then + local r = v[3] + local g = v[4] + local b = v[5] + -- if r == g and g == b then + -- pdf_border_color = pdfarray { r } -- reduced, not not ... bugged viewers + -- else + pdf_border_color = pdfarray { r, g, b } -- always rgb + -- end + end + end + if not pdf_border_color then + pdf_border_color = pdfarray { .6, .6, .6 } -- no reduce to { 0.6 } as there are buggy viewers out there + end + pdf_border_style = pdfarray { 0, 0, .5 } -- < 0.5 is not show by acrobat (at least not in my version) + end +end) + +-- the used and flag code here is somewhat messy in the sense +-- that it belongs in strc-ref but at the same time depends on +-- the backend so we keep it here + +-- the caching is somewhat memory intense on the one hand but +-- it saves many small temporary tables so it might pay off + +local pagedestinations = setmetatableindex(function(t,k) + k = tonumber(k) + if not k or k <= 0 then + return pdfnull() + end + local v = rawget(t,k) + if v then + -- report_references("page number expected, got %s: %a",type(k),k) + return v + end + local v = k > 0 and pdfarray { + pdfreference(pdfpagereference(k)), + pdf_fit, + } or pdfnull() + t[k] = v + return v +end) + +local pagereferences = setmetatableindex(function(t,k) + k = tonumber(k) + if not k or k <= 0 then + return nil + end + local v = rawget(t,k) + if v then + return v + end + local v = pdfdictionary { -- can be cached + S = pdf_goto, + D = pagedestinations[k], + } + t[k] = v + return v +end) + +local defaultdestination = pdfarray { 0, pdf_fit } + +-- fit is default (see lpdf-nod) + +local destinations = { } +local reported = setmetatableindex("table") + +local function pdfregisterdestination(name,reference) + local d = destinations[name] + if d then + if not reported[name][reference] then + report_destinations("ignoring duplicate destination %a with reference %a",name,reference) + reported[name][reference] = true + end + else + destinations[name] = reference + end +end + +lpdf.registerdestination = pdfregisterdestination + +logs.registerfinalactions(function() + if log_destinations and next(destinations) then + local report = logs.startfilelogging("references","used destinations") + local n = 0 + for destination, pagenumber in table.sortedhash(destinations) do + report("% 4i : %-5s : %s",pagenumber,usedviews[destination] or defaultview,destination) + n = n + 1 + end + logs.stopfilelogging() + report_destinations("%s destinations saved in log file",n) + end +end) + +local function pdfdestinationspecification() + if next(destinations) then -- safeguard + checkautoprefixes(destinations) + local r = pdfmakenametree(destinations,pdfreference) + if r then + pdfaddtonames("Dests",r) + end + if not log_destinations then + destinations = nil + end + end +end + +lpdf.destinationspecification = pdfdestinationspecification + +lpdf.registerdocumentfinalizer(pdfdestinationspecification,"collect destinations") + +-- todo + +local destinations = { } + +local f_xyz = formatters["<< /D [ %i 0 R /XYZ %.6N %.6N null ] >>"] +local f_fit = formatters["<< /D [ %i 0 R /Fit ] >>"] +local f_fitb = formatters["<< /D [ %i 0 R /FitB ] >>"] +local f_fith = formatters["<< /D [ %i 0 R /FitH %.6N ] >>"] +local f_fitv = formatters["<< /D [ %i 0 R /FitV %.6N ] >>"] +local f_fitbh = formatters["<< /D [ %i 0 R /FitBH %.6N ] >>"] +local f_fitbv = formatters["<< /D [ %i 0 R /FitBV %.6N ] >>"] +local f_fitr = formatters["<< /D [ %i 0 R /FitR %.6N %.6N %.6N %.6N ] >>"] + +local v_standard = variables.standard +local v_frame = variables.frame +local v_width = variables.width +local v_minwidth = variables.minwidth +local v_height = variables.height +local v_minheight = variables.minheight +local v_fit = variables.fit +local v_tight = variables.tight + +-- nicer is to create dictionaries and set properties but it's a bit overkill + +-- The problem with the following settings is that they are guesses: we never know +-- if a box is part of something larger that needs to be in view, or that we are +-- dealing with a vbox or vtop so the used h/d values cannot be trusted in a tight +-- view. Of course some decent additional offset would be nice so maybe i'll add +-- that some day. I never use anything else than 'fit' anyway as I think that the +-- document should fit the device (and vice versa). In fact, with todays swipe +-- and finger zooming this whole view is rather useless and as with any zooming +-- one looses the overview and keeps zooming. + +-- todo: scaling + +-- local destinationactions = { +-- [v_standard] = function(r,w,h,d) return f_xyz (r,gethpos()*factor,(getvpos()+h)*factor) end, -- local left,top with no zoom +-- [v_frame] = function(r,w,h,d) return f_fitr (r,pdfrectangle(w,h,d)) end, -- fit rectangle in window +-- [v_width] = function(r,w,h,d) return f_fith (r,(getvpos()+h)*factor) end, -- top coordinate, fit width of page in window +-- [v_minwidth] = function(r,w,h,d) return f_fitbh(r,(getvpos()+h)*factor) end, -- top coordinate, fit width of content in window +-- [v_height] = function(r,w,h,d) return f_fitv (r,gethpos()*factor) end, -- left coordinate, fit height of page in window +-- [v_minheight] = function(r,w,h,d) return f_fitbv(r,gethpos()*factor) end, -- left coordinate, fit height of content in window [v_fit] = f_fit, -- fit page in window +-- [v_tight] = f_fitb, -- fit content in window +-- [v_fit] = f_fit, +-- } + +local destinationactions = { + [v_standard] = function(r,w,h,d,o) -- local left,top with no zoom + local tx, ty = getpos() + return f_xyz(r,tx*factor,(ty+h+2*o)*factor) -- we can assume margins + end, + [v_frame] = function(r,w,h,d,o) -- fit rectangle in window + return f_fitr(r,pdfrectangle(w,h,d,o)) + end, + [v_width] = function(r,w,h,d,o) -- top coordinate, fit width of page in window + return f_fith(r,(getvpos()+h+o)*factor) + end, + [v_minwidth] = function(r,w,h,d,o) -- top coordinate, fit width of content in window + return f_fitbh(r,(getvpos()+h+o)*factor) + end, + [v_height] = function(r,w,h,d,o) -- left coordinate, fit height of page in window + return f_fitv(r,(gethpos())*factor) + end, + [v_minheight] = function(r,w,h,d,o) -- left coordinate, fit height of content in window + return f_fitbv(r,(gethpos())*factor) + end, + [v_tight] = f_fitb, -- fit content in window + [v_fit] = f_fit, -- fit content in window +} + +local mapping = { + [v_standard] = v_standard, xyz = v_standard, + [v_frame] = v_frame, fitr = v_frame, + [v_width] = v_width, fith = v_width, + [v_minwidth] = v_minwidth, fitbh = v_minwidth, + [v_height] = v_height, fitv = v_height, + [v_minheight] = v_minheight, fitbv = v_minheight, + [v_fit] = v_fit, fit = v_fit, + [v_tight] = v_tight, fitb = v_tight, +} + +local defaultview = v_fit +local defaultaction = destinationactions[defaultview] +local offset = 0 -- 65536*5 + +directives.register("destinations.offset", function(v) + offset = string.todimen(v) or 0 +end) + +-- A complication is that we need to use named destinations when we have views so we +-- end up with a mix. A previous versions just output multiple destinations but now +-- that we moved all to here we can be more sparse. + +local pagedestinations = setmetatableindex(function(t,k) -- not the same as the one above! + local v = pdfdelayedobject(f_fit(k)) + t[k] = v + return v +end) + +local function flushdestination(specification) + local names = specification.names + local view = specification.view + local r = pdfpagereference(texgetcount("realpageno")) + if (references.innermethod ~= v_name) and (view == defaultview or not view or view == "") then + r = pagedestinations[r] + else + local action = view and destinationactions[view] or defaultaction + r = pdfdelayedobject(action(r,specification.width,specification.height,specification.depth,offset)) + end + for n=1,#names do + local name = names[n] + if name then + pdfregisterdestination(name,r) + end + end +end + +function nodeinjections.destination(width,height,depth,names,view) + -- todo check if begin end node / was comment + view = view and mapping[view] or defaultview + if trace_destinations then + report_destinations("width %p, height %p, depth %p, names %|t, view %a",width,height,depth,names,view) + end + local method = references.innermethod + local noview = view == defaultview + local doview = false + -- we could save some aut's by using a name when given but it doesn't pay off apart + -- from making the code messy and tracing hard .. we only save some destinations + -- which we already share anyway + if method == v_page then + for n=1,#names do + local name = names[n] + local used = usedviews[name] + if used and used ~= true then + -- already done, maybe a warning + elseif type(name) == "number" then + -- if noview then + -- usedviews[name] = view + -- names[n] = false + -- else + usedviews[name] = view + names[n] = false + -- end + else + usedviews[name] = view + end + end + elseif method == v_name then + for n=1,#names do + local name = names[n] + local used = usedviews[name] + if used and used ~= true then + -- already done, maybe a warning + elseif type(name) == "number" then + local used = usedinternals[name] + usedviews[name] = view + names[n] = registerautoprefix(name) + doview = true + else + usedviews[name] = view + doview = true + end + end + else + for n=1,#names do + local name = names[n] + if usedviews[name] then + -- already done, maybe a warning + elseif type(name) == "number" then + if noview then + usedviews[name] = view + names[n] = false + else + local used = usedinternals[name] + if used and used ~= defaultview then + usedviews[name] = view + names[n] = registerautoprefix(name) + doview = true + else + names[n] = false + end + end + else + usedviews[name] = view + doview = true + end + end + end + if doview then + return new_latelua { + action = flushdestination, + width = width, + height = height, + depth = depth, + names = names, + view = view, + } + end +end + +-- we could share dictionaries ... todo + +local function pdflinkpage(page) + return pagereferences[page] +end + +local function pdflinkinternal(internal,page) + -- local method = references.innermethod + if internal then + flaginternals[internal] = true -- for bookmarks and so + local used = usedinternals[internal] + if used == defaultview or used == true then + return pagereferences[page] + else + if type(internal) ~= "string" then + internal = useautoprefix(internal) + end + return pdfdictionary { + S = pdf_goto, + D = internal, + } + end + else + return pagereferences[page] + end +end + +local function pdflinkname(destination,internal,page) + local method = references.innermethod + if method == v_auto then + local used = defaultview + if internal then + flaginternals[internal] = true -- for bookmarks and so + used = usedinternals[internal] or defaultview + end + if used == defaultview then -- or used == true then + return pagereferences[page] + else + return pdfdictionary { + S = pdf_goto, + D = destination, + } + end + elseif method == v_name then + -- flaginternals[internal] = true -- for bookmarks and so + return pdfdictionary { + S = pdf_goto, + D = destination, + } + else + return pagereferences[page] + end +end + +-- annotations + +local function pdffilelink(filename,destination,page,actions) + if not filename or filename == "" or file.basename(filename) == tex.jobname then + return false + end + filename = file.addsuffix(filename,"pdf") + if (not destination or destination == "") or (references.outermethod == v_page) then + destination = pdfarray { (page or 1) - 1, pdf_fit } + end + return pdfdictionary { + S = pdf_gotor, -- can also be pdf_launch + F = filename, + D = destination or defaultdestination, + NewWindow = actions.newwindow and true or nil, + } +end + +local untex = references.urls.untex + +local function pdfurllink(url,destination,page) + if not url or url == "" then + return false + end + if untex_urls then + url = untex(url) -- last minute cleanup of \* and spaces + end + if destination and destination ~= "" then + url = url .. "#" .. destination + end + return pdfdictionary { + S = pdf_uri, + URI = url, + } +end + +local function pdflaunch(program,parameters) + if not program or program == "" then + return false + end + return pdfdictionary { + S = pdf_launch, + F = program, + D = ".", + P = parameters ~= "" and parameters or nil + } +end + +local function pdfjavascript(name,arguments) + local script = javascriptcode(name,arguments) -- make into object (hash) + if script then + return pdfdictionary { + S = pdf_javascript, + JS = script, + } + end +end + +local function pdfaction(actions) + local nofactions = #actions + if nofactions > 0 then + local a = actions[1] + local action = runners[a.kind] + if action then + action = action(a,actions) + end + if action then + local first = action + for i=2,nofactions do + local a = actions[i] + local what = runners[a.kind] + if what then + what = what(a,actions) + end + if action == what then + -- ignore this one, else we get a loop + elseif what then + action.Next = what + action = what + else + -- error + return nil + end + end + return first, actions.n or #actions + end + end +end + +lpdf.action = pdfaction + +function codeinjections.prerollreference(actions) -- share can become option + if actions then + local main, n = pdfaction(actions) + if main then + local bs, bc = pdfborder() + main = pdfdictionary { + Subtype = pdf_link, + -- Border = bs, + Border = pdfshareobjectreference(bs), + C = bc, + H = (not actions.highlight and pdf_n) or nil, + A = pdfshareobjectreference(main), + F = 4, -- print (mandate in pdf/a) + } + return main("A"), n + end + end +end + +-- local function use_normal_annotations() +-- +-- local function reference(width,height,depth,prerolled) -- keep this one +-- if prerolled then +-- if trace_references then +-- report_references("width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled) +-- end +-- return pdfannotation_node(width,height,depth,prerolled) +-- end +-- end +-- +-- local function finishreference() +-- end +-- +-- return reference, finishreference +-- +-- end + +-- eventually we can do this for special refs only + +local hashed = { } +local nofunique = 0 +local nofused = 0 +local nofspecial = 0 +local share = true + +local f_annot = formatters["<< /Type /Annot %s /Rect [ %.6N %.6N %.6N %.6N ] >>"] +local f_quadp = formatters["<< /Type /Annot %s /QuadPoints [ %s ] /Rect [ %.6N %.6N %.6N %.6N ] >>"] + +directives.register("references.sharelinks", function(v) + share = v +end) + +setmetatableindex(hashed,function(t,k) + local v = pdfdelayedobject(k) + if share then + t[k] = v + end + nofunique = nofunique + 1 + return v +end) + +local function toquadpoints(paths) + local t, n = { }, 0 + for i=1,#paths do + local path = paths[i] + local size = #path + for j=1,size do + local p = path[j] + n = n + 1 ; t[n] = p[1] + n = n + 1 ; t[n] = p[2] + end + local m = size % 4 + if m > 0 then + local p = path[size] + for j=size+1,m do + n = n + 1 ; t[n] = p[1] + n = n + 1 ; t[n] = p[2] + end + end + end + return concat(t," ") +end + +local function finishreference(specification) + local prerolled = specification.prerolled + local quadpoints = specification.mesh + local llx, lly, + urx, ury = pdfrectangle(specification.width,specification.height,specification.depth) + local specifier = nil + if quadpoints and #quadpoints > 0 then + specifier = f_quadp(prerolled,toquadpoints(quadpoints),llx,lly,urx,ury) + else + specifier = f_annot(prerolled,llx,lly,urx,ury) + end + nofused = nofused + 1 + return pdfregisterannotation(hashed[specifier]) +end + +local function finishannotation(specification) + local prerolled = specification.prerolled + local objref = specification.objref + if type(prerolled) == "function" then + prerolled = prerolled() + end + local annot = f_annot(prerolled,pdfrectangle(specification.width,specification.height,specification.depth)) + if objref then + pdfdelayedobject(annot,objref) + else + objref = pdfdelayedobject(annot) + end + nofspecial = nofspecial + 1 + return pdfregisterannotation(objref) +end + +function nodeinjections.reference(width,height,depth,prerolled,mesh) + if prerolled then + if trace_references then + report_references("link: width %p, height %p, depth %p, prerolled %a",width,height,depth,prerolled) + end + return new_latelua { + action = finishreference, + width = width, + height = height, + depth = depth, + prerolled = prerolled, + mesh = mesh, + } + end +end + +function nodeinjections.annotation(width,height,depth,prerolled,objref) + if prerolled then + if trace_references then + report_references("special: width %p, height %p, depth %p, prerolled %a",width,height,depth, + type(prerolled) == "string" and prerolled or "-") + end + return new_latelua { + action = finishannotation, + width = width, + height = height, + depth = depth, + prerolled = prerolled, + objref = objref or false, + } + end +end + +-- beware, we register during a latelua sweep so we have to make sure that +-- we finalize after that (also in a latelua for the moment as we have no +-- callback yet) + +local annotations = nil + +function lpdf.registerannotation(n) + if annotations then + annotations[#annotations+1] = pdfreference(n) + else + annotations = pdfarray { pdfreference(n) } -- no need to use lpdf.array cum suis + end +end + +pdfregisterannotation = lpdf.registerannotation + +function lpdf.annotationspecification() + if annotations then + local r = pdfdelayedobject(tostring(annotations)) -- delayed so okay in latelua + if r then + pdfaddtopageattributes("Annots",pdfreference(r)) + end + annotations = nil + end +end + +lpdf.registerpagefinalizer(lpdf.annotationspecification,"finalize annotations") + +statistics.register("pdf annotations", function() + if nofused > 0 or nofspecial > 0 then + return format("%s links (%s unique), %s special",nofused,nofunique,nofspecial) + else + return nil + end +end) + +-- runners and specials + +local splitter = lpeg.splitat(",",true) + +runners["inner"] = function(var,actions) + local internal = false + local name = nil + local method = references.innermethod + local vi = var.i + local page = var.r + if vi then + local vir = vi.references + if vir then + -- todo: no need for it when we have a real reference ... although we need + -- this mess for prefixes anyway + local reference = vir.reference + if reference and reference ~= "" then + reference = lpegmatch(splitter,reference) or reference + var.inner = reference + local prefix = var.p + if prefix and prefix ~= "" then + var.prefix = prefix + name = prefix .. ":" .. reference + else + name = reference + end + end + internal = vir.internal + if internal then + flaginternals[internal] = true + end + end + end + if name then + return pdflinkname(name,internal,page) + elseif internal then + return pdflinkinternal(internal,page) + elseif page then + return pdflinkpage(page) + else + -- real bad + end +end + +runners["inner with arguments"] = function(var,actions) + report_references("todo: inner with arguments") + return false +end + +runners["outer"] = function(var,actions) + local file, url = references.checkedfileorurl(var.outer,var.outer) + if file then + return pdffilelink(file,var.arguments,nil,actions) + elseif url then + return pdfurllink(url,var.arguments,nil,actions) + end +end + +runners["outer with inner"] = function(var,actions) + return pdffilelink(references.checkedfile(var.outer),var.inner,var.r,actions) +end + +runners["special outer with operation"] = function(var,actions) + local handler = specials[var.special] + return handler and handler(var,actions) +end + +runners["special outer"] = function(var,actions) + report_references("todo: special outer") + return false +end + +runners["special"] = function(var,actions) + local handler = specials[var.special] + return handler and handler(var,actions) +end + +runners["outer with inner with arguments"] = function(var,actions) + report_references("todo: outer with inner with arguments") + return false +end + +runners["outer with special and operation and arguments"] = function(var,actions) + report_references("todo: outer with special and operation and arguments") + return false +end + +runners["outer with special"] = function(var,actions) + report_references("todo: outer with special") + return false +end + +runners["outer with special and operation"] = function(var,actions) + report_references("todo: outer with special and operation") + return false +end + +runners["special operation"] = runners["special"] +runners["special operation with arguments"] = runners["special"] + +local reported = { } + +function specials.internal(var,actions) -- better resolve in strc-ref + local o = var.operation + local i = o and tonumber(o) + local v = i and references.internals[i] + if v then + flaginternals[i] = true -- also done in pdflinkinternal + return pdflinkinternal(i,v.references.realpage) + end + local v = i or o or "<unset>" + if not reported[v] then + report_references("no internal reference %a",v) + reported[v] = true + end +end + +-- realpage already resolved + +specials.i = specials.internal + +local pages = references.pages + +function specials.page(var,actions) + local file = var.f + if file then + return pdffilelink(references.checkedfile(file),nil,var.operation,actions) + else + local p = var.r + if not p then -- todo: call special from reference code + p = pages[var.operation] + if type(p) == "function" then -- double + p = p() + else + p = references.realpageofpage(tonumber(p)) + end + end + return pdflinkpage(p or var.operation) + end +end + +function specials.realpage(var,actions) + local file = var.f + if file then + return pdffilelink(references.checkedfile(file),nil,var.operation,actions) + else + return pdflinkpage(var.operation) + end +end + +function specials.userpage(var,actions) + local file = var.f + if file then + return pdffilelink(references.checkedfile(file),nil,var.operation,actions) + else + local p = var.r + if not p then -- todo: call special from reference code + p = var.operation + if p then -- no function and special check here. only numbers + p = references.realpageofpage(tonumber(p)) + end + -- if p then + -- var.r = p + -- end + end + return pdflinkpage(p or var.operation) + end +end + +function specials.deltapage(var,actions) + local p = tonumber(var.operation) + if p then + p = references.checkedrealpage(p + texgetcount("realpageno")) + return pdflinkpage(p) + end +end + +-- sections + +function specials.section(var,actions) + -- a bit duplicate + local sectionname = var.arguments + local destination = var.operation + local internal = structures.sections.internalreference(sectionname,destination) + if internal then + var.special = "internal" + var.operation = internal + var.arguments = nil + return specials.internal(var,actions) + end +end + +-- todo, do this in references namespace ordered instead (this is an experiment) + +local splitter = lpeg.splitat(":") + +function specials.order(var,actions) -- references.specials ! + local operation = var.operation + if operation then + local kind, name, n = lpegmatch(splitter,operation) + local order = structures.lists.ordered[kind] + order = order and order[name] + local v = order[tonumber(n)] + local r = v and v.references.realpage + if r then + var.operation = r -- brrr, but test anyway + return specials.page(var,actions) + end + end +end + +function specials.url(var,actions) + return pdfurllink(references.checkedurl(var.operation),var.arguments,nil,actions) +end + +function specials.file(var,actions) + return pdffilelink(references.checkedfile(var.operation),var.arguments,nil,actions) +end + +function specials.fileorurl(var,actions) + local file, url = references.checkedfileorurl(var.operation,var.operation) + if file then + return pdffilelink(file,var.arguments,nil,actions) + elseif url then + return pdfurllink(url,var.arguments,nil,actions) + end +end + +function specials.program(var,content) + local program = references.checkedprogram(var.operation) + return pdflaunch(program,var.arguments) +end + +function specials.javascript(var) + return pdfjavascript(var.operation,var.arguments) +end + +specials.JS = specials.javascript + +executers.importform = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ImportFDF") } +executers.exportform = pdfdictionary { S = pdf_named, N = pdfconstant("AcroForm:ExportFDF") } +executers.first = pdfdictionary { S = pdf_named, N = pdfconstant("FirstPage") } +executers.previous = pdfdictionary { S = pdf_named, N = pdfconstant("PrevPage") } +executers.next = pdfdictionary { S = pdf_named, N = pdfconstant("NextPage") } +executers.last = pdfdictionary { S = pdf_named, N = pdfconstant("LastPage") } +executers.backward = pdfdictionary { S = pdf_named, N = pdfconstant("GoBack") } +executers.forward = pdfdictionary { S = pdf_named, N = pdfconstant("GoForward") } +executers.print = pdfdictionary { S = pdf_named, N = pdfconstant("Print") } +executers.exit = pdfdictionary { S = pdf_named, N = pdfconstant("Quit") } +executers.close = pdfdictionary { S = pdf_named, N = pdfconstant("Close") } +executers.save = pdfdictionary { S = pdf_named, N = pdfconstant("Save") } +executers.savenamed = pdfdictionary { S = pdf_named, N = pdfconstant("SaveAs") } +executers.opennamed = pdfdictionary { S = pdf_named, N = pdfconstant("Open") } +executers.help = pdfdictionary { S = pdf_named, N = pdfconstant("HelpUserGuide") } +executers.toggle = pdfdictionary { S = pdf_named, N = pdfconstant("FullScreen") } +executers.search = pdfdictionary { S = pdf_named, N = pdfconstant("Find") } +executers.searchagain = pdfdictionary { S = pdf_named, N = pdfconstant("FindAgain") } +executers.gotopage = pdfdictionary { S = pdf_named, N = pdfconstant("GoToPage") } +executers.query = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:Query") } +executers.queryagain = pdfdictionary { S = pdf_named, N = pdfconstant("AcroSrch:NextHit") } +executers.fitwidth = pdfdictionary { S = pdf_named, N = pdfconstant("FitWidth") } +executers.fitheight = pdfdictionary { S = pdf_named, N = pdfconstant("FitHeight") } + +local function fieldset(arguments) + -- [\dogetfieldset{#1}] + return nil +end + +function executers.resetform(arguments) + arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments) + return pdfdictionary { + S = pdfconstant("ResetForm"), + Field = fieldset(arguments[1]) + } +end + +local formmethod = "post" -- "get" "post" +local formformat = "xml" -- "xml" "html" "fdf" + +-- bit 3 = html bit 6 = xml bit 4 = get + +local flags = { + get = { + html = 12, fdf = 8, xml = 40, + }, + post = { + html = 4, fdf = 0, xml = 32, + } +} + +function executers.submitform(arguments) + arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments) + local flag = flags[formmethod] or flags.post + flag = (flag and (flag[formformat] or flag.xml)) or 32 -- default: post, xml + return pdfdictionary { + S = pdfconstant("SubmitForm"), + F = arguments[1], + Field = fieldset(arguments[2]), + Flags = flag, + -- \PDFsubmitfiller + } +end + +local pdf_hide = pdfconstant("Hide") + +function executers.hide(arguments) + return pdfdictionary { + S = pdf_hide, + H = true, + T = arguments, + } +end + +function executers.show(arguments) + return pdfdictionary { + S = pdf_hide, + H = false, + T = arguments, + } +end + +local pdf_movie = pdfconstant("Movie") +local pdf_start = pdfconstant("Start") +local pdf_stop = pdfconstant("Stop") +local pdf_resume = pdfconstant("Resume") +local pdf_pause = pdfconstant("Pause") + +local function movie_or_sound(operation,arguments) + arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments) + return pdfdictionary { + S = pdf_movie, + T = format("movie %s",arguments[1] or "noname"), + Operation = operation, + } +end + +function executers.startmovie (arguments) return movie_or_sound(pdf_start ,arguments) end +function executers.stopmovie (arguments) return movie_or_sound(pdf_stop ,arguments) end +function executers.resumemovie(arguments) return movie_or_sound(pdf_resume,arguments) end +function executers.pausemovie (arguments) return movie_or_sound(pdf_pause ,arguments) end + +function executers.startsound (arguments) return movie_or_sound(pdf_start ,arguments) end +function executers.stopsound (arguments) return movie_or_sound(pdf_stop ,arguments) end +function executers.resumesound(arguments) return movie_or_sound(pdf_resume,arguments) end +function executers.pausesound (arguments) return movie_or_sound(pdf_pause ,arguments) end + +function specials.action(var) + local operation = var.operation + if var.operation and operation ~= "" then + local e = executers[operation] + if type(e) == "table" then + return e + elseif type(e) == "function" then + return e(var.arguments) + end + end +end + +local function build(levels,start,parent,method,nested) + local startlevel = levels[start].level + local noflevels = #levels + local i = start + local n = 0 + local child, entry, m, prev, first, last, f, l + while i and i <= noflevels do + local current = levels[i] + if current.usedpage == false then + -- safeguard + i = i + 1 + else + local level = current.level + local title = current.title + local reference = current.reference + local opened = current.opened + local reftype = type(reference) + local block = nil + local variant = "unknown" + if reftype == "table" then + -- we're okay + variant = "list" + block = reference.block + realpage = reference.realpage + elseif reftype == "string" then + local resolved = references.identify("",reference) + realpage = resolved and structures.references.setreferencerealpage(resolved) or 0 + if realpage > 0 then + variant = "realpage" + realpage = realpage + reference = structures.pages.collected[realpage] + block = reference and reference.block + end + elseif reftype == "number" then + if reference > 0 then + variant = "realpage" + realpage = reference + reference = structures.pages.collected[realpage] + block = reference and reference.block + end + else + -- error + end + current.block = block + if variant == "unknown" then + -- error, ignore + i = i + 1 + -- elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].reference.block) then + elseif (level < startlevel) or (i > 1 and block ~= levels[i-1].block) then + if nested then -- could be an option but otherwise we quit too soon + if entry then + pdfflushobject(child,entry) + else + report_bookmarks("error 1") + end + return i, n, first, last + else + report_bookmarks("confusing level change at level %a around %a",level,title) + startlevel = level + end + end + if level == startlevel then + if trace_bookmarks then + report_bookmarks("%3i %w%s %s",realpage,(level-1)*2,(opened and "+") or "-",title) + end + local prev = child + child = pdfreserveobject() + if entry then + entry.Next = child and pdfreference(child) + pdfflushobject(prev,entry) + end + local action = nil + if variant == "list" then + action = pdflinkinternal(reference.internal,reference.realpage) + elseif variant == "realpage" then + action = pagereferences[realpage] + else + -- hm, what to do + end + entry = pdfdictionary { + Title = pdfunicode(title), + Parent = parent, + Prev = prev and pdfreference(prev), + A = action, + } + -- entry.Dest = pdflinkinternal(reference.internal,reference.realpage) + if not first then + first, last = child, child + end + prev = child + last = prev + n = n + 1 + i = i + 1 + elseif i < noflevels and level > startlevel then + i, m, f, l = build(levels,i,pdfreference(child),method,true) + if entry then + entry.Count = (opened and m) or -m + if m > 0 then + entry.First = pdfreference(f) + entry.Last = pdfreference(l) + end + else + report_bookmarks("error 2") + end + else + -- missing intermediate level but ok + i, m, f, l = build(levels,i,pdfreference(child),method,true) + if entry then + entry.Count = (opened and m) or -m + if m > 0 then + entry.First = pdfreference(f) + entry.Last = pdfreference(l) + end + pdfflushobject(child,entry) + else + report_bookmarks("error 3") + end + return i, n, first, last + end + end + end + pdfflushobject(child,entry) + return nil, n, first, last +end + +function codeinjections.addbookmarks(levels,method) + if levels and #levels > 0 then + local parent = pdfreserveobject() + local _, m, first, last = build(levels,1,pdfreference(parent),method or "internal",false) + local dict = pdfdictionary { + Type = pdfconstant("Outlines"), + First = pdfreference(first), + Last = pdfreference(last), + Count = m, + } + pdfflushobject(parent,dict) + pdfaddtocatalog("Outlines",lpdf.reference(parent)) + end +end + +-- this could also be hooked into the frontend finalizer + +lpdf.registerdocumentfinalizer(function() bookmarks.place() end,1,"bookmarks") -- hm, why indirect call diff --git a/tex/context/base/mkxl/lpdf-aux.lmt b/tex/context/base/mkxl/lpdf-aux.lmt new file mode 100644 index 000000000..0d7cecbb8 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-aux.lmt @@ -0,0 +1,152 @@ +if not modules then modules = { } end modules ['lpdf-aux'] = { + version = 1.001, + 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" +} + +local tonumber = tonumber +local format, concat = string.format, table.concat +local utfchar, utfbyte, char = utf.char, utf.byte, string.char +local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns +local P, C, R, S, Cc, Cs, V = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs, lpeg.V +local rshift = bit32.rshift + +lpdf = lpdf or { } + +-- tosixteen -- + +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 = format("%04x%04x",rshift(v,10),v%1024+0xDC00) + end + t[k] = v + return v +end) + +local unified = Cs(Cc("<feff") * (lpegpatterns.utf8character/cache)^1 * Cc(">")) + +function lpdf.tosixteen(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 + +-- fromsixteen -- + +-- local zero = S(" \n\r\t") + P("\\ ") +-- local one = C(4) +-- local two = P("d") * R("89","af") * C(2) * C(4) +-- +-- local 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 = (tonumber(a,16) - 0xD800) * 1024 +-- b = (tonumber(b,16) - 0xDC00) +-- return utfchar(a+b) +-- end +-- + one / function(a) +-- return utfchar(tonumber(a,16)) +-- end +-- )^1 ) * P(-1) +-- } +-- +-- function lpdf.fromsixteen(s) +-- return lpegmatch(pattern,s) or s +-- end + +local more = 0 + +local pattern = C(4) / function(s) -- needs checking ! + local now = tonumber(s,16) + if more > 0 then + now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000 -- the 0x10000 smells wrong + 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) + +function lpdf.fromsixteen(str) + if not str or str == "" then + return "" + else + return lpegmatch(pattern,str) + end +end + +-- frombytes -- + +local b_pattern = Cs((P("\\")/"" * ( + S("()") + + S("nrtbf")/ { n = "\n", r = "\r", t = "\t", b = "\b", f = "\f" } + + lpegpatterns.octdigit^-3 / function(s) return char(tonumber(s,8)) end) ++ P(1))^0) + +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 = (tonumber(a,16) - 0xD800) * 1024 + b = (tonumber(b,16) - 0xDC00) + return utfchar(a+b) + end + + one / function(a) + return utfchar(tonumber(a,16)) + 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(b_pattern,s) +end + +-- done -- diff --git a/tex/context/base/mkxl/lpdf-col.lmt b/tex/context/base/mkxl/lpdf-col.lmt new file mode 100644 index 000000000..5ebd4bd79 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-col.lmt @@ -0,0 +1,845 @@ +if not modules then modules = { } end modules ['lpdf-col'] = { + version = 1.001, + 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" +} + +-- slants also page ? + +local type, next, tostring, tonumber = type, next, tostring, tonumber +local char, byte, format, gsub, rep, gmatch = string.char, string.byte, string.format, string.gsub, string.rep, string.gmatch +local settings_to_array, settings_to_numbers = utilities.parsers.settings_to_array, utilities.parsers.settings_to_numbers +local concat = table.concat +local round = math.round +local formatters = string.formatters + +local backends, lpdf, nodes = backends, lpdf, nodes + +local allocate = utilities.storage.allocate + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations + +local nodepool = nodes.nuts.pool +local register = nodepool.register +local pageliteral = nodepool.pageliteral + +local pdfconstant = lpdf.constant +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfreference = lpdf.reference +local pdfverbose = lpdf.verbose + +local pdfflushobject +local pdfdelayedobject +local pdfflushstreamobject +local pdfshareobjectreference + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfdelayedobject = lpdf.delayedobject + pdfflushstreamobject = lpdf.flushstreamobject + pdfshareobjectreference = lpdf.shareobjectreference +end) + +local addtopageattributes = lpdf.addtopageattributes +local adddocumentcolorspace = lpdf.adddocumentcolorspace +local adddocumentextgstate = lpdf.adddocumentextgstate + +local colors = attributes.colors +local registercolor = colors.register +local colorsvalue = colors.value +local forcedmodel = colors.forcedmodel +local getpagecolormodel = colors.getpagecolormodel +local colortoattributes = colors.toattributes + +local transparencies = attributes.transparencies +local registertransparancy = transparencies.register +local transparenciesvalue = transparencies.value +local transparencytoattribute = transparencies.toattribute + +local unsetvalue = attributes.unsetvalue + +local setmetatableindex = table.setmetatableindex + +local c_transparency = pdfconstant("Transparency") + +local f_gray = formatters["%.3N g %.3N G"] +local f_rgb = formatters["%.3N %.3N %.3N rg %.3N %.3N %.3N RG"] +local f_cmyk = formatters["%.3N %.3N %.3N %.3N k %.3N %.3N %.3N %.3N K"] +local f_spot = formatters["/%s cs /%s CS %s SCN %s scn"] +local f_tr = formatters["Tr%s"] +local f_cm = formatters["q %.6N %.6N %.6N %.6N %.6N %.6N cm"] +local f_effect = formatters["%s Tc %s w %s Tr"] -- %.6N ? +local f_tr_gs = formatters["/Tr%s gs"] +local f_num_1 = formatters["%.3N %.3N"] +local f_num_2 = formatters["%.3N %.3N"] +local f_num_3 = formatters["%.3N %.3N %.3N"] +local f_num_4 = formatters["%.3N %.3N %.3N %.3N"] + +local report_color = logs.reporter("colors","backend") + +-- page groups (might move to lpdf-ini.lua) + +local colorspaceconstants = allocate { -- v_none is ignored + gray = pdfconstant("DeviceGray"), + rgb = pdfconstant("DeviceRGB"), + cmyk = pdfconstant("DeviceCMYK"), + all = pdfconstant("DeviceRGB"), -- brr +} + +local transparencygroups = { } + +lpdf.colorspaceconstants = colorspaceconstants +lpdf.transparencygroups = transparencygroups + +setmetatableindex(transparencygroups, function(transparencygroups,colormodel) + local cs = colorspaceconstants[colormodel] + if cs then + local d = pdfdictionary { + S = c_transparency, + CS = cs, + I = true, + } + -- local g = pdfreference(pdfflushobject(tostring(d))) + local g = pdfreference(pdfdelayedobject(tostring(d))) + transparencygroups[colormodel] = g + return g + else + transparencygroups[colormodel] = false + return false + end +end) + +local function addpagegroup() + local model = getpagecolormodel() + if model then + local g = transparencygroups[model] + if g then + addtopageattributes("Group",g) + end + end +end + +lpdf.registerpagefinalizer(addpagegroup,3,"pagegroup") + +-- injection code (needs a bit reordering) + +-- color injection + +function nodeinjections.rgbcolor(r,g,b) + return register(pageliteral(f_rgb(r,g,b,r,g,b))) +end + +function nodeinjections.cmykcolor(c,m,y,k) + return register(pageliteral(f_cmyk(c,m,y,k,c,m,y,k))) +end + +function nodeinjections.graycolor(s) -- caching 0/1 does not pay off + return register(pageliteral(f_gray(s,s))) +end + +function nodeinjections.spotcolor(n,f,d,p) + if type(p) == "string" then + p = gsub(p,","," ") -- brr misuse of spot + end + return register(pageliteral(f_spot(n,n,p,p))) +end + +function nodeinjections.transparency(n) + return register(pageliteral(f_tr_gs(n))) +end + +-- a bit weird but let's keep it here for a while + +local effects = { + normal = 0, + inner = 0, + outer = 1, + both = 2, + hidden = 3, +} + +local bp = number.dimenfactors.bp + +function nodeinjections.effect(effect,stretch,rulethickness) + -- always, no zero test (removed) + rulethickness = bp * rulethickness + effect = effects[effect] or effects['normal'] + return register(pageliteral(f_effect(stretch,rulethickness,effect))) -- watch order +end + +-- spot- and indexcolors + +local pdf_separation = pdfconstant("Separation") +local pdf_indexed = pdfconstant("Indexed") +local pdf_device_n = pdfconstant("DeviceN") +local pdf_device_rgb = pdfconstant("DeviceRGB") +local pdf_device_cmyk = pdfconstant("DeviceCMYK") +local pdf_device_gray = pdfconstant("DeviceGray") +local pdf_extgstate = pdfconstant("ExtGState") + +local pdf_rgb_range = pdfarray { 0, 1, 0, 1, 0, 1 } +local pdf_cmyk_range = pdfarray { 0, 1, 0, 1, 0, 1, 0, 1 } +local pdf_gray_range = pdfarray { 0, 1 } + +local f_rgb_function = formatters["dup %s mul exch dup %s mul exch %s mul"] +local f_cmyk_function = formatters["dup %s mul exch dup %s mul exch dup %s mul exch %s mul"] +local f_gray_function = formatters["%s mul"] + +local documentcolorspaces = pdfdictionary() + +local spotcolorhash = { } -- not needed +local spotcolornames = { } +local indexcolorhash = { } +local delayedindexcolors = { } + +function registrations.spotcolorname(name,e) + spotcolornames[name] = e or name +end + +function registrations.getspotcolorreference(name) + return spotcolorhash[name] +end + +-- beware: xpdf/okular/evince cannot handle the spot->process shade + +-- This should become delayed i.e. only flush when used; in that case we need +-- need to store the specification and then flush them when accesssomespotcolor +-- is called. At this moment we assume that splotcolors that get defined are +-- also used which keeps the overhad small anyway. Tricky for mp ... + +local processcolors + +local function registersomespotcolor(name,noffractions,names,p,colorspace,range,funct) + noffractions = tonumber(noffractions) or 1 -- to be checked + if noffractions == 0 then + -- can't happen + elseif noffractions == 1 then + local dictionary = pdfdictionary { + FunctionType = 4, + Domain = { 0, 1 }, + Range = range, + } + local calculations = pdfflushstreamobject(format("{ %s }",funct),dictionary) + -- local calculations = pdfobject { + -- type = "stream", + -- immediate = true, + -- string = format("{ %s }",funct), + -- attr = dictionary(), + -- } + local array = pdfarray { + pdf_separation, + pdfconstant(spotcolornames[name] or name), + colorspace, + pdfreference(calculations), + } + local m = pdfflushobject(array) + local mr = pdfreference(m) + spotcolorhash[name] = m + documentcolorspaces[name] = mr + adddocumentcolorspace(name,mr) + else + local cnames = pdfarray() + local domain = pdfarray() + local colorants = pdfdictionary() + for n in gmatch(names,"[^,]+") do + local name = spotcolornames[n] or n + -- the cmyk names assume that they are indeed these colors + if n == "cyan" then + name = "Cyan" + elseif n == "magenta" then + name = "Magenta" + elseif n == "yellow" then + name = "Yellow" + elseif n == "black" then + name = "Black" + else + local sn = spotcolorhash[name] or spotcolorhash[n] + if not sn then + report_color("defining %a as colorant",name) + colors.definespotcolor("",name,"p=1",true) + sn = spotcolorhash[name] or spotcolorhash[n] + end + if sn then + colorants[name] = pdfreference(sn) + else + -- maybe some day generate colorants (spot colors for multi) automatically + report_color("unknown colorant %a, using black instead",name or n) + name = "Black" + end + end + cnames[#cnames+1] = pdfconstant(name) + domain[#domain+1] = 0 + domain[#domain+1] = 1 + end + if not processcolors then + local specification = pdfdictionary { + ColorSpace = pdfconstant("DeviceCMYK"), + Components = pdfarray { + pdfconstant("Cyan"), + pdfconstant("Magenta"), + pdfconstant("Yellow"), + pdfconstant("Black") + } + } + processcolors = pdfreference(pdfflushobject(specification)) + end + local dictionary = pdfdictionary { + FunctionType = 4, + Domain = domain, + Range = range, + } + local calculation = pdfflushstreamobject(format("{ %s %s }",rep("pop ",noffractions),funct),dictionary) + local channels = pdfdictionary { + Subtype = pdfconstant("NChannel"), + Colorants = colorants, + Process = processcolors, + } + local array = pdfarray { + pdf_device_n, + cnames, + colorspace, + pdfreference(calculation), + pdfshareobjectreference(tostring(channels)), -- optional but needed for shades + } + local m = pdfflushobject(array) + local mr = pdfreference(m) + spotcolorhash[name] = m + documentcolorspaces[name] = mr + adddocumentcolorspace(name,mr) + end +end + +-- wrong name + +local function registersomeindexcolor(name,noffractions,names,p,colorspace,range,funct) + noffractions = tonumber(noffractions) or 1 -- to be checked + local cnames = pdfarray() + local domain = pdfarray() + local names = settings_to_array(#names == 0 and name or names) + local values = settings_to_numbers(p) + names [#names +1] = "None" + values[#values+1] = 1 + -- check for #names == #values + for i=1,#names do + local name = names[i] + local spot = spotcolornames[name] + cnames[#cnames+1] = pdfconstant(spot ~= "" and spot or name) + domain[#domain+1] = 0 + domain[#domain+1] = 1 + end + local dictionary = pdfdictionary { + FunctionType = 4, + Domain = domain, + Range = range, + } + local n = pdfflushstreamobject(format("{ %s %s }",rep("exch pop ",noffractions),funct),dictionary) -- exch pop + local a = pdfarray { + pdf_device_n, + cnames, + colorspace, + pdfreference(n), + } + local vector = { } + local set = { } + local n = #values + for i=255,0,-1 do + for j=1,n do + set[j] = format("%02X",round(values[j]*i)) + end + vector[#vector+1] = concat(set) + end + vector = pdfverbose { "<", concat(vector, " "), ">" } + local n = pdfflushobject(pdfarray{ pdf_indexed, a, 255, vector }) + adddocumentcolorspace(format("%s_indexed",name),pdfreference(n)) + return n +end + +-- actually, names (parent) is the hash + +local function delayindexcolor(name,names,func) + local hash = (names ~= "" and names) or name + delayedindexcolors[hash] = func +end + +local function indexcolorref(name) -- actually, names (parent) is the hash + local parent = colors.spotcolorparent(name) + local data = indexcolorhash[name] + if data == nil then + local delayedindexcolor = delayedindexcolors[parent] + if type(delayedindexcolor) == "function" then + data = delayedindexcolor() + delayedindexcolors[parent] = true + end + indexcolorhash[parent] = data or false + end + return data +end + +function registrations.rgbspotcolor(name,noffractions,names,p,r,g,b) + if noffractions == 1 then + registersomespotcolor(name,noffractions,names,p,pdf_device_rgb,pdf_rgb_range,f_rgb_function(r,g,b)) + else + registersomespotcolor(name,noffractions,names,p,pdf_device_rgb,pdf_rgb_range,f_num_3(r,g,b)) + end + delayindexcolor(name,names,function() + return registersomeindexcolor(name,noffractions,names,p,pdf_device_rgb,pdf_rgb_range,f_rgb_function(r,g,b)) + end) +end + +function registrations.cmykspotcolor(name,noffractions,names,p,c,m,y,k) + if noffractions == 1 then + registersomespotcolor(name,noffractions,names,p,pdf_device_cmyk,pdf_cmyk_range,f_cmyk_function(c,m,y,k)) + else + registersomespotcolor(name,noffractions,names,p,pdf_device_cmyk,pdf_cmyk_range,f_num_4(c,m,y,k)) + end + delayindexcolor(name,names,function() + return registersomeindexcolor(name,noffractions,names,p,pdf_device_cmyk,pdf_cmyk_range,f_cmyk_function(c,m,y,k)) + end) +end + +function registrations.grayspotcolor(name,noffractions,names,p,s) + if noffractions == 1 then + registersomespotcolor(name,noffractions,names,p,pdf_device_gray,pdf_gray_range,f_gray_function(s)) + else + registersomespotcolor(name,noffractions,names,p,pdf_device_gray,pdf_gray_range,f_num_1(s)) + end + delayindexcolor(name,names,function() + return registersomeindexcolor(name,noffractions,names,p,pdf_device_gray,pdf_gray_range,f_gray_function(s)) + end) +end + +function registrations.rgbindexcolor(name,noffractions,names,p,r,g,b) + registersomeindexcolor(name,noffractions,names,p,pdf_device_rgb,pdf_rgb_range,f_rgb_function(r,g,b)) +end + +function registrations.cmykindexcolor(name,noffractions,names,p,c,m,y,k) + registersomeindexcolor(name,noffractions,names,p,pdf_device_cmyk,pdf_cmyk_range,f_cmyk_function(c,m,y,k)) +end + +function registrations.grayindexcolor(name,noffractions,names,p,s) + registersomeindexcolor(name,noffractions,names,p,pdf_device_gray,pdf_gray_range,f_gray_function(s)) +end + +function codeinjections.setfigurecolorspace(data,figure) + local color = data.request.color + if color then -- != v_default + local ref = indexcolorref(color) + if ref then + figure.colorspace = ref + data.used.color = color + data.used.colorref = ref + end + end +end + +-- transparency + +local pdftransparencies = { [0] = + pdfconstant("Normal"), + pdfconstant("Normal"), + pdfconstant("Multiply"), + pdfconstant("Screen"), + pdfconstant("Overlay"), + pdfconstant("SoftLight"), + pdfconstant("HardLight"), + pdfconstant("ColorDodge"), + pdfconstant("ColorBurn"), + pdfconstant("Darken"), + pdfconstant("Lighten"), + pdfconstant("Difference"), + pdfconstant("Exclusion"), + pdfconstant("Hue"), + pdfconstant("Saturation"), + pdfconstant("Color"), + pdfconstant("Luminosity"), + pdfconstant("Compatible"), -- obsolete; 'Normal' is used in this case +} + +local documenttransparencies = { } +local transparencyhash = { } -- share objects + +local done, signaled = false, false + +function registrations.transparency(n,a,t) + if not done then + local d = pdfdictionary { + Type = pdf_extgstate, + ca = 1, + CA = 1, + BM = pdftransparencies[1], + AIS = false, + } + local m = pdfflushobject(d) + local mr = pdfreference(m) + transparencyhash[0] = m + documenttransparencies[0] = mr + adddocumentextgstate("Tr0",mr) + done = true + end + if n > 0 and not transparencyhash[n] then + local d = pdfdictionary { + Type = pdf_extgstate, + ca = tonumber(t), + CA = tonumber(t), + BM = pdftransparencies[tonumber(a)] or pdftransparencies[0], + AIS = false, + } + local m = pdfflushobject(d) + local mr = pdfreference(m) + transparencyhash[n] = m + documenttransparencies[n] = mr + adddocumentextgstate(f_tr(n),mr) + end +end + +statistics.register("page group warning", function() + if done then + local model = getpagecolormodel() + if model and not transparencygroups[model] then + return "transparencies are used but no pagecolormodel is set" + end + end +end) + +-- Literals needed to inject code in the mp stream, we cannot use attributes there +-- since literals may have qQ's, much may go away once we have mplib code in place. +-- +-- This module assumes that some functions are defined in the colors namespace +-- which most likely will be loaded later. + +local function lpdfcolor(model,ca,default) -- todo: use gray when no color + if colors.supported then + local cv = colorsvalue(ca) + if cv then + if model == 1 then + model = cv[1] + end + model = forcedmodel(model) + if model == 2 then + local s = cv[2] + return f_gray(s,s) + elseif model == 3 then + local r = cv[3] + local g = cv[4] + local b = cv[5] + return f_rgb(r,g,b,r,g,b) + elseif model == 4 then + local c = cv[6] + local m = cv[7] + local y = cv[8] + local k = cv[9] + return f_cmyk(c,m,y,k,c,m,y,k) + else + local n = cv[10] + local f = cv[11] + local d = cv[12] + local p = cv[13] + if type(p) == "string" then + p = gsub(p,","," ") -- brr misuse of spot + end + return f_spot(n,n,p,p) + end + else + return f_gray(default or 0,default or 0) + end + else + return "" + end +end + +lpdf.color = lpdfcolor + +interfaces.implement { + name = "lpdf_color", + actions = { lpdfcolor, context }, + arguments = "integer" +} + +function lpdf.colorspec(model,ca,default) + if ca and ca > 0 then + local cv = colors.value(ca) + if cv then + if model == 1 then + model = cv[1] + end + if model == 2 then + return pdfarray { cv[2] } + elseif model == 3 then + return pdfarray { cv[3],cv[4],cv[5] } + elseif model == 4 then + return pdfarray { cv[6],cv[7],cv[8],cv[9] } + elseif model == 5 then + return pdfarray { cv[13] } + end + end + end + if default then + return default + end +end + +function lpdf.pdfcolor(attribute) -- bonus, for pgf and friends + return lpdfcolor(1,attribute) +end + +function lpdf.transparency(ct,default) -- kind of overlaps with transparencycode + -- beware, we need this hack because normally transparencies are not + -- yet registered and therefore the number is not not known ... we + -- might use the attribute number itself in the future + if transparencies.supported then + local ct = transparenciesvalue(ct) + if ct then + return f_tr_gs(registertransparancy(nil,ct[1],ct[2],true)) + else + return f_tr_gs(0) + end + else + return "" + end +end + +function lpdf.colorvalue(model,ca,default) + local cv = colorsvalue(ca) + if cv then + if model == 1 then + model = cv[1] + end + model = forcedmodel(model) + if model == 2 then + return f_num_1(cv[2]) + elseif model == 3 then + return f_num_3(cv[3],cv[4],cv[5]) + elseif model == 4 then + return f_num_4(cv[6],cv[7],cv[8],cv[9]) + else + return f_num_1(cv[13]) + end + else + return f_num_1(default or 0) + end +end + +function lpdf.colorvalues(model,ca,default) + local cv = colorsvalue(ca) + if cv then + if model == 1 then + model = cv[1] + end + model = forcedmodel(model) + if model == 2 then + return cv[2] + elseif model == 3 then + return cv[3], cv[4], cv[5] + elseif model == 4 then + return cv[6], cv[7], cv[8], cv[9] + elseif model == 5 then + return cv[13] + end + else + return default or 0 + end +end + +function lpdf.transparencyvalue(ta,default) + local tv = transparenciesvalue(ta) + if tv then + return tv[2] + else + return default or 1 + end +end + +function lpdf.colorspace(model,ca) + local cv = colorsvalue(ca) + if cv then + if model == 1 then + model = cv[1] + end + model = forcedmodel(model) + if model == 2 then + return "DeviceGray" + elseif model == 3 then + return "DeviceRGB" + elseif model == 4 then + return "DeviceCMYK" + end + end + return "DeviceGRAY" +end + +-- by registering we getconversion for free (ok, at the cost of overhead) + +local intransparency = false +local pdfcolor = lpdf.color + +function lpdf.rgbcode(model,r,g,b) + if colors.supported then + return pdfcolor(model,registercolor(nil,'rgb',r,g,b)) + else + return "" + end +end + +function lpdf.cmykcode(model,c,m,y,k) + if colors.supported then + return pdfcolor(model,registercolor(nil,'cmyk',c,m,y,k)) + else + return "" + end +end + +function lpdf.graycode(model,s) + if colors.supported then + return pdfcolor(model,registercolor(nil,'gray',s)) + else + return "" + end +end + +function lpdf.spotcode(model,n,f,d,p) + if colors.supported then + return pdfcolor(model,registercolor(nil,'spot',n,f,d,p)) -- incorrect + else + return "" + end +end + +function lpdf.transparencycode(a,t) + if transparencies.supported then + intransparency = true + return f_tr_gs(registertransparancy(nil,a,t,true)) -- true forces resource + else + return "" + end +end + +function lpdf.finishtransparencycode() + if transparencies.supported and intransparency then + intransparency = false + return f_tr_gs(0) -- we happen to know this -) + else + return "" + end +end + +-- this will move to lpdf-spe.lua an dwe then can also add a metatable with +-- normal context colors + +do + + local pdfcolor = lpdf.color + local pdftransparency = lpdf.transparency + + local f_slant = formatters["q 1 0 %N 1 0 0 cm"] + + -- local fillcolors = { + -- red = { "pdf", "page", "1 0 0 rg" }, + -- green = { "pdf", "page", "0 1 0 rg" }, + -- blue = { "pdf", "page", "0 0 1 rg" }, + -- gray = { "pdf", "page", ".5 g" }, + -- black = { "pdf", "page", "0 g" }, + -- palered = { "pdf", "page", "1 .75 .75 rg" }, + -- palegreen = { "pdf", "page", ".75 1 .75 rg" }, + -- paleblue = { "pdf", "page", ".75 .75 1 rg" }, + -- palegray = { "pdf", "page", ".75 g" }, + -- } + -- + -- local strokecolors = { + -- red = { "pdf", "page", "1 0 0 RG" }, + -- green = { "pdf", "page", "0 1 0 RG" }, + -- blue = { "pdf", "page", "0 0 1 RG" }, + -- gray = { "pdf", "page", ".5 G" }, + -- black = { "pdf", "page", "0 G" }, + -- palered = { "pdf", "page", "1 .75 .75 RG" }, + -- palegreen = { "pdf", "page", ".75 1 .75 RG" }, + -- paleblue = { "pdf", "page", ".75 .75 1 RG" }, + -- palegray = { "pdf", "page", ".75 G" }, + -- } + -- + -- backends.pdf.tables.vfspecials = allocate { -- todo: distinguish between glyph and rule color + -- + -- red = { "pdf", "page", "1 0 0 rg 1 0 0 RG" }, + -- green = { "pdf", "page", "0 1 0 rg 0 1 0 RG" }, + -- blue = { "pdf", "page", "0 0 1 rg 0 0 1 RG" }, + -- gray = { "pdf", "page", ".75 g .75 G" }, + -- black = { "pdf", "page", "0 g 0 G" }, + -- + -- -- rulecolors = fillcolors, + -- -- fillcolors = fillcolors, + -- -- strokecolors = strokecolors, + -- + -- startslant = function(a) return { "pdf", "origin", f_slant(a) } end, + -- stopslant = { "pdf", "origin", "Q" }, + -- + -- } + + local slants = setmetatableindex(function(t,k) + local v = { "pdf", "origin", f_slant(a) } + t[k] = v + return k + end) + + local function startslant(a) + return slants[a] + end + + local c_cache = setmetatableindex(function(t,m) + local v = setmetatableindex(function(t,c) + local p = { "pdf", "page", "q " .. pdfcolor(m,c) } + t[c] = p + return p + end) + t[m] = v + return v + end) + + -- we inherit the outer transparency + + local t_cache = setmetatableindex(function(t,transparency) + local p = pdftransparency(transparency) + local v = setmetatableindex(function(t,colormodel) + local v = setmetatableindex(function(t,color) + local v = { "pdf", "page", "q " .. pdfcolor(colormodel,color) .. " " .. p } + t[color] = v + return v + end) + t[colormodel] = v + return v + end) + t[transparency] = v + return v + end) + + local function startcolor(k) + local m, c = colortoattributes(k) + local t = transparencytoattribute(k) + if t then + return t_cache[t][m][c] + else + return c_cache[m][c] + end + end + + -- A problem is that we need to transfer back and this is kind of + -- messy so we force text mode .. i'll do a better job on that but + -- will experiment first (both engines). Virtual fonts will change + -- anyway. + + backends.pdf.tables.vfspecials = allocate { -- todo: distinguish between glyph and rule color + + startcolor = startcolor, + -- stopcolor = { "pdf", "page", "0 g 0 G Q" }, + stopcolor = { "pdf", "text", "Q" }, + + startslant = startslant, + -- stopslant = { "pdf", "origin", "Q" }, + stopslant = { "pdf", "text", "Q" }, + + } + +end diff --git a/tex/context/base/mkxl/lpdf-emb.lmt b/tex/context/base/mkxl/lpdf-emb.lmt index d2da4473a..994ae2e07 100644 --- a/tex/context/base/mkxl/lpdf-emb.lmt +++ b/tex/context/base/mkxl/lpdf-emb.lmt @@ -48,9 +48,16 @@ local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfstring = lpdf.string local pdfreference = lpdf.reference -local pdfreserveobject = lpdf.reserveobject -local pdfflushobject = lpdf.flushobject -local pdfflushstreamobject = lpdf.flushstreamobject + +local pdfreserveobject +local pdfflushobject +local pdfflushstreamobject + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + pdfflushobject = lpdf.flushobject + pdfflushstreamobject = lpdf.flushstreamobject +end) local report_fonts = logs.reporter("backend","fonts") @@ -302,7 +309,7 @@ end -- Map file mess. -local loadmapfile, loadmapline, getmapentry do +local getmapentry do -- We only need to pick up the filename and optionally the enc file -- as we only use them for old school virtual math fonts. We might as @@ -313,7 +320,7 @@ local loadmapfile, loadmapline, getmapentry do local mappings = { } - loadmapline = function(n) + lpdf.loadmapline = function(n) if trace_fonts then report_fonts("mapline: %s",n) end @@ -323,7 +330,7 @@ local loadmapfile, loadmapline, getmapentry do end end - loadmapfile = function(n) + lpdf.loadmapfile = function(n) local okay, data = resolvers.loadbinfile(n,"map") if okay and data then data = splitlines(data) @@ -2215,22 +2222,28 @@ end) -- this is temporary -local done = false - -updaters.register("backend.update.pdf",function() - if not done then - function pdf.getfontobjnum (k) return objects[k] end - function pdf.getfontname (k) return names [k] end - function pdf.includechar () end -- maybe, when we need it - function pdf.includefont () end -- maybe, when we need it - function pdf.includecharlist () end -- maybe, when we need it - function pdf.setomitcidset (v) includecidset = not toboolean(v) end - function pdf.setomitcharset () end -- we don't need that in lmtx - function pdf.setsuppressoptionalinfo() end -- we don't need that in lmtx - function pdf.mapfile (n) loadmapfile(n) end - function pdf.mapline (n) loadmapline(n) end - -- this will change +function lpdf.setomitcidset(v) + -- dummy: no longer needed + includecidset = not toboolean(v) +end + +function lpdf.setomitcharset(v) + -- dummy +end + +function lpdf.getfontobjectnumber(k) + return objects[k] +end + +function lpdf.getfontname(k) + return names[k] +end + +-- local done = false -- todo: + +-- updaters.register("backend.update.pdf",function() +-- if not done then lpdf.registerdocumentfinalizer(lpdf.flushfonts,1,"wrapping up fonts") - done = true - end -end) +-- done = true +-- end +-- end) diff --git a/tex/context/base/mkxl/lpdf-enc.lmt b/tex/context/base/mkxl/lpdf-enc.lmt new file mode 100644 index 000000000..090fb15cd --- /dev/null +++ b/tex/context/base/mkxl/lpdf-enc.lmt @@ -0,0 +1,157 @@ +if not modules then modules = { } end modules ['lpdf-enc'] = { + version = 1.001, + 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" +} + +-- delayed loading + +local pdfconstant = lpdf.constant + +return lpdf.dictionary { + Type = pdfconstant("Encoding"), + Differences = lpdf.array { + 24, + pdfconstant("breve"), + pdfconstant("caron"), + pdfconstant("circumflex"), + pdfconstant("dotaccent"), + pdfconstant("hungarumlaut"), + pdfconstant("ogonek"), + pdfconstant("ring"), + pdfconstant("tilde"), + 39, + pdfconstant("quotesingle"), + 96, + pdfconstant("grave"), + 128, + pdfconstant("bullet"), + pdfconstant("dagger"), + pdfconstant("daggerdbl"), + pdfconstant("ellipsis"), + pdfconstant("emdash"), + pdfconstant("endash"), + pdfconstant("florin"), + pdfconstant("fraction"), + pdfconstant("guilsinglleft"), + pdfconstant("guilsinglright"), + pdfconstant("minus"), + pdfconstant("perthousand"), + pdfconstant("quotedblbase"), + pdfconstant("quotedblleft"), + pdfconstant("quotedblright"), + pdfconstant("quoteleft"), + pdfconstant("quoteright"), + pdfconstant("quotesinglbase"), + pdfconstant("trademark"), + pdfconstant("fi"), + pdfconstant("fl"), + pdfconstant("Lslash"), + pdfconstant("OE"), + pdfconstant("Scaron"), + pdfconstant("Ydieresis"), + pdfconstant("Zcaron"), + pdfconstant("dotlessi"), + pdfconstant("lslash"), + pdfconstant("oe"), + pdfconstant("scaron"), + pdfconstant("zcaron"), + 160, + pdfconstant("Euro"), + 164, + pdfconstant("currency"), + 166, + pdfconstant("brokenbar"), + 168, + pdfconstant("dieresis"), + pdfconstant("copyright"), + pdfconstant("ordfeminine"), + 172, + pdfconstant("logicalnot"), + pdfconstant(".notdef"), + pdfconstant("registered"), + pdfconstant("macron"), + pdfconstant("degree"), + pdfconstant("plusminus"), + pdfconstant("twosuperior"), + pdfconstant("threesuperior"), + pdfconstant("acute"), + pdfconstant("mu"), + 183, + pdfconstant("periodcentered"), + pdfconstant("cedilla"), + pdfconstant("onesuperior"), + pdfconstant("ordmasculine"), + 188, + pdfconstant("onequarter"), + pdfconstant("onehalf"), + pdfconstant("threequarters"), + 192, + pdfconstant("Agrave"), + pdfconstant("Aacute"), + pdfconstant("Acircumflex"), + pdfconstant("Atilde"), + pdfconstant("Adieresis"), + pdfconstant("Aring"), + pdfconstant("AE"), + pdfconstant("Ccedilla"), + pdfconstant("Egrave"), + pdfconstant("Eacute"), + pdfconstant("Ecircumflex"), + pdfconstant("Edieresis"), + pdfconstant("Igrave"), + pdfconstant("Iacute"), + pdfconstant("Icircumflex"), + pdfconstant("Idieresis"), + pdfconstant("Eth"), + pdfconstant("Ntilde"), + pdfconstant("Ograve"), + pdfconstant("Oacute"), + pdfconstant("Ocircumflex"), + pdfconstant("Otilde"), + pdfconstant("Odieresis"), + pdfconstant("multiply"), + pdfconstant("Oslash"), + pdfconstant("Ugrave"), + pdfconstant("Uacute"), + pdfconstant("Ucircumflex"), + pdfconstant("Udieresis"), + pdfconstant("Yacute"), + pdfconstant("Thorn"), + pdfconstant("germandbls"), + pdfconstant("agrave"), + pdfconstant("aacute"), + pdfconstant("acircumflex"), + pdfconstant("atilde"), + pdfconstant("adieresis"), + pdfconstant("aring"), + pdfconstant("ae"), + pdfconstant("ccedilla"), + pdfconstant("egrave"), + pdfconstant("eacute"), + pdfconstant("ecircumflex"), + pdfconstant("edieresis"), + pdfconstant("igrave"), + pdfconstant("iacute"), + pdfconstant("icircumflex"), + pdfconstant("idieresis"), + pdfconstant("eth"), + pdfconstant("ntilde"), + pdfconstant("ograve"), + pdfconstant("oacute"), + pdfconstant("ocircumflex"), + pdfconstant("otilde"), + pdfconstant("odieresis"), + pdfconstant("divide"), + pdfconstant("oslash"), + pdfconstant("ugrave"), + pdfconstant("uacute"), + pdfconstant("ucircumflex"), + pdfconstant("udieresis"), + pdfconstant("yacute"), + pdfconstant("thorn"), + pdfconstant("ydieresis"), + }, +} diff --git a/tex/context/base/mkxl/lpdf-epa.lmt b/tex/context/base/mkxl/lpdf-epa.lmt new file mode 100644 index 000000000..00d9f3c4b --- /dev/null +++ b/tex/context/base/mkxl/lpdf-epa.lmt @@ -0,0 +1,1105 @@ +if not modules then modules = { } end modules ['lpdf-epa'] = { + version = 1.001, + comment = "companion to lpdf-epa.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files" +} + +-- Links can also have quadpoint + +-- embedded files ... not bound to a page + +local type, tonumber, next = type, tonumber, next +local format, gsub, lower, find = string.format, string.gsub, string.lower, string.find +local formatters = string.formatters +local concat, merged = table.concat, table.merged +local abs = math.abs +local expandname = file.expandname +local allocate = utilities.storage.allocate +local bor, band = bit32.bor, bit32.band +local isfile = lfs.isfile + +local trace_links = false trackers.register("figures.links", function(v) trace_links = v end) +local trace_comments = false trackers.register("figures.comments", function(v) trace_comments = v end) +local trace_fields = false trackers.register("figures.fields", function(v) trace_fields = v end) +local trace_outlines = false trackers.register("figures.outlines", function(v) trace_outlines = v end) + +local report_link = logs.reporter("backend","link") +local report_comment = logs.reporter("backend","comment") +local report_field = logs.reporter("backend","field") +local report_outline = logs.reporter("backend","outline") + +local lpdf = lpdf +local backends = backends +local context = context + +local nodeinjections = backends.pdf.nodeinjections + +local pdfarray = lpdf.array +local pdfdictionary = lpdf.dictionary +local pdfconstant = lpdf.constant +local pdfreference = lpdf.reference + +local pdfreserveobject +local pdfgetpos + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + pdfgetpos = lpdf.getpos +end) + +local pdfcopyboolean = lpdf.copyboolean +local pdfcopyunicode = lpdf.copyunicode +local pdfcopyarray = lpdf.copyarray +local pdfcopydictionary = lpdf.copydictionary +local pdfcopynumber = lpdf.copynumber +local pdfcopyinteger = lpdf.copyinteger +local pdfcopystring = lpdf.copystring +local pdfcopyconstant = lpdf.copyconstant + +local createimage = images.create +local embedimage = images.embed + +local hpack_node = nodes.hpack + +local loadpdffile = lpdf.epdf.load + +local nameonly = file.nameonly + +local variables = interfaces.variables +local codeinjections = backends.pdf.codeinjections +----- urlescaper = lpegpatterns.urlescaper +----- utftohigh = lpegpatterns.utftohigh +local escapetex = characters.filters.utf.private.escape + +local bookmarks = structures.bookmarks + +local maxdimen = 0x3FFFFFFF -- 2^30-1 + +local bpfactor = number.dimenfactors.bp + +local layerspec = { + "epdfcontent" +} + +local getpos = function() getpos = backends.codeinjections.getpos return getpos() end + +local collected = allocate() +local tobesaved = allocate() + +local jobembedded = { + collected = collected, + tobesaved = tobesaved, +} + +job.embedded = jobembedded + +local function initializer() + tobesaved = jobembedded.tobesaved + collected = jobembedded.collected +end + +job.register('job.embedded.collected',tobesaved,initializer) + +local function validdocument(specification) + if figures and not specification then + specification = figures and figures.current() + specification = specification and specification.status + end + if specification then + local fullname = specification.fullname + local expanded = lower(expandname(fullname)) + -- we could add a check for duplicate page insertion + tobesaved[expanded] = true + --- but that is messy anyway so we forget about it + return specification, fullname, loadpdffile(fullname) -- costs time + end +end + +local function getmediasize(specification,pagedata) + local xscale = specification.xscale or 1 + local yscale = specification.yscale or 1 + ----- size = specification.size or "crop" -- todo + local mediabox = pagedata.MediaBox + local llx = mediabox[1] + local lly = mediabox[2] + local urx = mediabox[3] + local ury = mediabox[4] + local width = xscale * (urx - llx) -- \\overlaywidth, \\overlayheight + local height = yscale * (ury - lly) -- \\overlaywidth, \\overlayheight + return llx, lly, urx, ury, width, height, xscale, yscale +end + +local function getdimensions(annotation,llx,lly,xscale,yscale,width,height,report) + local rectangle = annotation.Rect + local a_llx = rectangle[1] + local a_lly = rectangle[2] + local a_urx = rectangle[3] + local a_ury = rectangle[4] + local x = xscale * (a_llx - llx) + local y = yscale * (a_lly - lly) + local w = xscale * (a_urx - a_llx) + local h = yscale * (a_ury - a_lly) + if w > width or h > height or w < 0 or h < 0 or abs(x) > (maxdimen/2) or abs(y) > (maxdimen/2) then + report("broken rectangle [%.6F %.6F %.6F %.6F] (max: %.6F)",a_llx,a_lly,a_urx,a_ury,maxdimen/2) + return + end + return x, y, w, h, a_llx, a_lly, a_urx, a_ury +end + +local layerused = false + +-- local function initializelayer(height,width) +-- if not layerused then +-- context.definelayer(layerspec, { height = height .. "bp", width = width .. "bp" }) +-- layerused = true +-- end +-- end + +local function initializelayer(height,width) +-- if not layerused then + context.setuplayer(layerspec, { height = height .. "bp", width = width .. "bp" }) + layerused = true +-- end +end + +function codeinjections.flushmergelayer() + if layerused then + context.flushlayer(layerspec) + layerused = false + end +end + +local f_namespace = formatters["lpdf-epa-%s-"] + +local function makenamespace(filename) + filename = gsub(lower(nameonly(filename)),"[^%a%d]+","-") + return f_namespace(filename) +end + +local function add_link(x,y,w,h,destination,what) + x = x .. "bp" + y = y .. "bp" + w = w .. "bp" + h = h .. "bp" + if trace_links then + report_link("destination %a, type %a, dx %s, dy %s, wd %s, ht %s",destination,what,x,y,w,h) + end + local locationspec = { -- predefining saves time + x = x, + y = y, + preset = "leftbottom", + } + local buttonspec = { + width = w, + height = h, + offset = variables.overlay, + frame = trace_links and variables.on or variables.off, + } + context.setlayer ( + layerspec, + locationspec, + function() context.button ( buttonspec, "", { destination } ) end + -- context.nested.button(buttonspec, "", { destination }) -- time this + ) +end + +local function link_goto(x,y,w,h,document,annotation,pagedata,namespace) + local a = annotation.A + if a then + local destination = a.D -- [ 18 0 R /Fit ] + local what = "page" + if type(destination) == "string" then + local destinations = document.destinations + local wanted = destinations[destination] + destination = wanted and wanted.D -- is this ok? isn't it destination already a string? + if destination then what = "named" end + end + local pagedata = destination and destination[1] + if pagedata then + local destinationpage = pagedata.number + if destinationpage then + add_link(x,y,w,h,namespace .. destinationpage,what) + end + end + end +end + +local function link_uri(x,y,w,h,document,annotation) + local url = annotation.A.URI + if url then + -- url = lpegmatch(urlescaper,url) + -- url = lpegmatch(utftohigh,url) + url = escapetex(url) + add_link(x,y,w,h,formatters["url(%s)"](url),"url") + end +end + +-- The rules in PDF on what a 'file specification' is, is in fact quite elaborate +-- (see section 3.10 in the 1.7 reference) so we need to test for string as well +-- as a table. TH/20140916 + +-- When embedded is set then files need to have page references which is seldom the +-- case but you can generate them with context: +-- +-- \setupinteraction[state=start,page={page,page}] +-- +-- see tests/mkiv/interaction/cross[1|2|3].tex for an example + +local embedded = false directives.register("figures.embedded", function(v) embedded = v end) +local reported = { } + +local function link_file(x,y,w,h,document,annotation) + local a = annotation.A + if a then + local filename = a.F + if type(filename) == "table" then + filename = filename.F + end + if filename then + filename = escapetex(filename) + local destination = a.D + if not destination then + add_link(x,y,w,h,formatters["file(%s)"](filename),"file") + elseif type(destination) == "string" then + add_link(x,y,w,h,formatters["%s::%s"](filename,destination),"file (named)") + else + -- hm, zero offset so maybe: destination + 1 + destination = tonumber(destination[1]) -- array + if destination then + destination = destination + 1 + local loaded = collected[lower(expandname(filename))] + if embedded and loaded then + add_link(x,y,w,h,makenamespace(filename) .. destination,what) + else + if loaded and not reported[filename] then + report_link("reference to an also loaded file %a, consider using directive: figures.embedded",filename) + reported[filename] = true + end + add_link(x,y,w,h,formatters["%s::page(%s)"](filename,destination),"file (page)") + end + else + add_link(x,y,w,h,formatters["file(%s)"](filename),"file") + end + end + end + end +end + +-- maybe handler per subtype and then one loop but then what about order ... + +function codeinjections.mergereferences(specification) + local specification, fullname, document = validdocument(specification) + if not document then + return "" + end + local pagenumber = specification.page or 1 + local pagedata = document.pages[pagenumber] + local annotations = pagedata and pagedata.Annots + local namespace = makenamespace(fullname) + local reference = namespace .. pagenumber + if annotations and #annotations > 0 then + local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale) + initializelayer(height,width) + for i=1,#annotations do + local annotation = annotations[i] + if annotation then + if annotation.Subtype == "Link" then + local a = annotation.A + if not a then + local d = annotation.Dest + if d then + annotation.A = { S = "GoTo", D = d } -- no need for a dict + end + end + if not a then + report_link("missing link annotation") + else + local x, y, w, h = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_link) + if x then + local linktype = a.S + if linktype == "GoTo" then + link_goto(x,y,w,h,document,annotation,pagedata,namespace) + elseif linktype == "GoToR" then + link_file(x,y,w,h,document,annotation) + elseif linktype == "URI" then + link_uri(x,y,w,h,document,annotation) + elseif trace_links then + report_link("unsupported link annotation %a",linktype) + end + end + end + end + elseif trace_links then + report_link("broken annotation, index %a",i) + end + end + end + -- moved outside previous test + context.setgvalue("figurereference",reference) -- global, todo: setmacro + if trace_links then + report_link("setting figure reference to %a",reference) + end + specification.reference = reference + return namespace +end + +function codeinjections.mergeviewerlayers(specification) + -- todo: parse included page for layers .. or only for whole document inclusion + if true then + return + end + local specification, fullname, document = validdocument(specification) + if not document then + return "" + end + local namespace = makenamespace(fullname) + local layers = document.layers + if layers then + for i=1,#layers do + local layer = layers[i] + if layer then + local tag = namespace .. gsub(layer," ",":") + local title = tag + if trace_links then + report_link("using layer %a",tag) + end + attributes.viewerlayers.define { -- also does some cleaning + tag = tag, -- todo: #3A or so + title = title, + visible = variables.start, + editable = variables.yes, + printable = variables.yes, + } + codeinjections.useviewerlayer(tag) + elseif trace_links then + report_link("broken layer, index %a",i) + end + end + end +end + +-- It took a bit of puzzling and playing around to come to the following +-- implementation. In the end it looks simple but as usual it takes a while +-- to see what the specification (and implementation) boils down to. Lots of +-- shared properties and such. The scaling took some trial and error as +-- viewers differ. I had to extend some low level helpers to make it more +-- comfortable. Hm, the specification is somewhat incomplete as some fields +-- are permitted even if not mentioned so in the end we can share more code. +-- +-- If all works ok, we can get rid of some copies which saves time and space. + +local commentlike = { + Text = "text", + FreeText = "freetext", + Line = "line", + Square = "shape", + Circle = "shape", + Polygon = "poly", + PolyLine = "poly", + Highlight = "markup", + Underline = "markup", + Squiggly = "markup", + StrikeOut = "markup", + Caret = "text", + Stamp = "stamp", + Ink = "ink", + Popup = "popup", +} + +local function copyBS(v) -- dict can be shared + if v then + -- return pdfdictionary { + -- Type = copypdfconstant(V.Type), + -- W = copypdfnumber (V.W), + -- S = copypdfstring (V.S), + -- D = copypdfarray (V.D), + -- } + return copypdfdictionary(v) + end +end + +local function copyBE(v) -- dict can be shared + if v then + -- return pdfdictionary { + -- S = copypdfstring(V.S), + -- I = copypdfnumber(V.I), + -- } + return copypdfdictionary(v) + end +end + +local function copyBorder(v) -- dict can be shared + if v then + -- todo + return copypdfarray(v) + end +end + +local function copyPopup(v,references) + if v then + local p = references[v] + if p then + return pdfreference(p) + end + end +end + +local function copyParent(v,references) + if v then + local p = references[v] + if p then + return pdfreference(p) + end + end +end + +local function copyIRT(v,references) + if v then + local p = references[v] + if p then + return pdfreference(p) + end + end +end + +local function copyC(v) + if v then + -- todo: check color space + return pdfcopyarray(v) + end +end + +local function finalizer(d,xscale,yscale,a_llx,a_ury) + local q = d.QuadPoints or d.Vertices or d.CL + if q then + return function() + local h, v = pdfgetpos() -- already scaled + for i=1,#q,2 do + q[i] = xscale * q[i] + (h*bpfactor - xscale * a_llx) + q[i+1] = yscale * q[i+1] + (v*bpfactor - yscale * a_ury) + end + return d() + end + end + q = d.InkList or d.Path + if q then + return function() + local h, v = pdfgetpos() -- already scaled + for i=1,#q do + local q = q[i] + for i=1,#q,2 do + q[i] = xscale * q[i] + (h*bpfactor - xscale * a_llx) + q[i+1] = yscale * q[i+1] + (v*bpfactor - yscale * a_ury) + end + end + return d() + end + end + return d() +end + +local validstamps = { + Approved = true, + Experimental = true, + NotApproved = true, + AsIs = true, + Expired = true, + NotForPublicRelease = true, + Confidential = true, + Final = true, + Sold = true, + Departmental = true, + ForComment = true, + TopSecret = true, + Draft = true, + ForPublicRelease = true, +} + +-- todo: we can use runlocal instead of steps + +local function validStamp(v) + local name = "Stamped" -- fallback + if v then + local ok = validstamps[v] + if ok then + name = ok + else + for k in next, validstamps do + if find(v,k.."$") then + name = k + validstamps[v] = k + break + end + end + end + end + -- we temporary return to \TEX: + context.predefinesymbol { name } + context.step() + -- beware, an error is not reported + return pdfconstant(name), codeinjections.analyzenormalsymbol(name) +end + +local annotationflags = lpdf.flags.annotations + +local function copyF(v,lock) -- todo: bxor 24 + if lock then + v = bor(v or 0,annotationflags.ReadOnly + annotationflags.Locked + annotationflags.LockedContents) + end + if v then + return pdfcopyinteger(v) + end +end + +-- Speed is not really an issue so we don't optimize this code too much. In the end (after +-- testing we end up with less code that we started with. + +function codeinjections.mergecomments(specification) + local specification, fullname, document = validdocument(specification) + if not document then + return "" + end + local pagenumber = specification.page or 1 + local pagedata = document.pages[pagenumber] + local annotations = pagedata and pagedata.Annots + if annotations and #annotations > 0 then + local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale) + initializelayer(height,width) + -- + local lockflags = specification.lock -- todo: proper parameter + local references = { } + local usedpopups = { } + for i=1,#annotations do + local annotation = annotations[i] + if annotation then + local subtype = annotation.Subtype + if commentlike[subtype] then + references[annotation] = pdfreserveobject() + local p = annotation.Popup + if p then + usedpopups[p] = true + end + end + end + end + -- + for i=1,#annotations do + -- we keep the order + local annotation = annotations[i] + if annotation then + local reference = references[annotation] + if reference then + local subtype = annotation.Subtype + local kind = commentlike[subtype] + if kind ~= "popup" or usedpopups[annotation] then + local x, y, w, h, a_llx, a_lly, a_urx, a_ury = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_comment) + if x then + local voffset = h + local dictionary = pdfdictionary { + Subtype = pdfconstant (subtype), + -- common (skipped: P AP AS OC AF BM StructParent) + Contents = pdfcopyunicode(annotation.Contents), + NM = pdfcopystring (annotation.NM), + M = pdfcopystring (annotation.M), + F = copyF (annotation.F,lockflags), + C = copyC (annotation.C), + ca = pdfcopynumber (annotation.ca), + CA = pdfcopynumber (annotation.CA), + Lang = pdfcopystring (annotation.Lang), + -- also common + CreationDate = pdfcopystring (annotation.CreationDate), + T = pdfcopyunicode(annotation.T), + Subj = pdfcopyunicode(annotation.Subj), + -- border + Border = pdfcopyarray (annotation.Border), + BS = copyBS (annotation.BS), + BE = copyBE (annotation.BE), + -- sort of common + Popup = copyPopup (annotation.Popup,references), + RC = pdfcopyunicode(annotation.RC) -- string or stream + } + if kind == "markup" then + dictionary.IRT = copyIRT (annotation.IRT,references) + dictionary.RT = pdfconstant (annotation.RT) + dictionary.IT = pdfcopyconstant (annotation.IT) + dictionary.QuadPoints = pdfcopyarray (annotation.QuadPoints) + -- dictionary.RD = pdfcopyarray (annotation.RD) + elseif kind == "text" then + -- somehow F fails to view : /F 24 : bit4=nozoom bit5=norotate + dictionary.F = nil + dictionary.Open = pdfcopyboolean (annotation.Open) + dictionary.Name = pdfcopyunicode (annotation.Name) + dictionary.State = pdfcopystring (annotation.State) + dictionary.StateModel = pdfcopystring (annotation.StateModel) + dictionary.IT = pdfcopyconstant (annotation.IT) + dictionary.QuadPoints = pdfcopyarray (annotation.QuadPoints) + dictionary.RD = pdfcopyarray (annotation.RD) -- caret + dictionary.Sy = pdfcopyconstant (annotation.Sy) -- caret + voffset = 0 + elseif kind == "freetext" then + dictionary.DA = pdfcopystring (annotation.DA) + dictionary.Q = pdfcopyinteger (annotation.Q) + dictionary.DS = pdfcopystring (annotation.DS) + dictionary.CL = pdfcopyarray (annotation.CL) + dictionary.IT = pdfcopyconstant (annotation.IT) + dictionary.LE = pdfcopyconstant (annotation.LE) + -- dictionary.RC = pdfcopystring (annotation.RC) + elseif kind == "line" then + dictionary.LE = pdfcopyarray (annotation.LE) + dictionary.IC = pdfcopyarray (annotation.IC) + dictionary.LL = pdfcopynumber (annotation.LL) + dictionary.LLE = pdfcopynumber (annotation.LLE) + dictionary.Cap = pdfcopyboolean (annotation.Cap) + dictionary.IT = pdfcopyconstant (annotation.IT) + dictionary.LLO = pdfcopynumber (annotation.LLO) + dictionary.CP = pdfcopyconstant (annotation.CP) + dictionary.Measure = pdfcopydictionary(annotation.Measure) -- names + dictionary.CO = pdfcopyarray (annotation.CO) + voffset = 0 + elseif kind == "shape" then + dictionary.IC = pdfcopyarray (annotation.IC) + -- dictionary.RD = pdfcopyarray (annotation.RD) + voffset = 0 + elseif kind == "stamp" then + local name, appearance = validStamp(annotation.Name) + dictionary.Name = name + dictionary.AP = appearance + voffset = 0 + elseif kind == "ink" then + dictionary.InkList = pdfcopyarray (annotation.InkList) + elseif kind == "poly" then + dictionary.Vertices = pdfcopyarray (annotation.Vertices) + -- dictionary.LE = pdfcopyarray (annotation.LE) -- todo: names in array + dictionary.IC = pdfcopyarray (annotation.IC) + dictionary.IT = pdfcopyconstant (annotation.IT) + dictionary.Measure = pdfcopydictionary(annotation.Measure) + dictionary.Path = pdfcopyarray (annotation.Path) + -- dictionary.RD = pdfcopyarray (annotation.RD) + elseif kind == "popup" then + dictionary.Open = pdfcopyboolean (annotation.Open) + dictionary.Parent = copyParent (annotation.Parent,references) + voffset = 0 + end + if dictionary then + local locationspec = { + x = x .. "bp", + y = y .. "bp", + voffset = voffset .. "bp", + preset = "leftbottom", + } + local finalize = finalizer(dictionary,xscale,yscale,a_llx,a_ury) + context.setlayer(layerspec,locationspec,function() + context(hpack_node(nodeinjections.annotation(w/bpfactor,h/bpfactor,0,finalize,reference))) + end) + end + end + else + -- report_comment("skipping annotation, index %a",i) + end + end + elseif trace_comments then + report_comment("broken annotation, index %a",i) + end + end + end + return namespace +end + +local widgetflags = lpdf.flags.widgets + +local function flagstoset(flag,flags) + local t = { } + if flags then + for k, v in next, flags do + if band(flag,v) ~= 0 then + t[k] = true + end + end + end + return t +end + +-- BS : border style dict +-- R : rotation 0 90 180 270 +-- BG : background array +-- CA : caption string +-- RC : roll over caption +-- AC : down caption +-- I/RI/IX : icon streams +-- IF : fit dictionary +-- TP : text position number + +-- Opt : array of texts +-- TI : top index + +-- V : value +-- DV : default value +-- DS : default string +-- RV : rich +-- Q : quadding (0=left 1=middle 2=right) + +function codeinjections.mergefields(specification) + local specification, fullname, document = validdocument(specification) + if not document then + return "" + end + local pagenumber = specification.page or 1 + local pagedata = document.pages[pagenumber] + local annotations = pagedata and pagedata.Annots + if annotations and #annotations > 0 then + local llx, lly, urx, ury, width, height, xscale, yscale = getmediasize(specification,pagedata,xscale,yscale) + initializelayer(height,width) + -- + for i=1,#annotations do + -- we keep the order + local annotation = annotations[i] + if annotation then + local subtype = annotation.Subtype + if subtype == "Widget" then + local parent = annotation.Parent or { } + local name = annotation.T or parent.T + local what = annotation.FT or parent.FT + if name and what then + local x, y, w, h, a_llx, a_lly, a_urx, a_ury = getdimensions(annotation,llx,lly,xscale,yscale,width,height,report_field) + if x then + x = x .. "bp" + y = y .. "bp" + local W, H = w, h + w = w .. "bp" + h = h .. "bp" + if trace_fields then + report_field("field %a, type %a, dx %s, dy %s, wd %s, ht %s",name,what,x,y,w,h) + end + local locationspec = { + x = x, + y = y, + preset = "leftbottom", + } + -- + local aflags = flagstoset(annotation.F or parent.F, annotationflags) + local wflags = flagstoset(annotation.Ff or parent.Ff, widgetflags) + if what == "Tx" then + -- DA DV F FT MaxLen MK Q T V | AA OC + if wflags.MultiLine then + wflags.MultiLine = nil + what = "text" + else + what = "line" + end + -- via context + local fieldspec = { + width = w, + height = h, + offset = variables.overlay, + frame = trace_links and variables.on or variables.off, + n = annotation.MaxLen or (parent and parent.MaxLen), + type = what, + option = concat(merged(aflags,wflags),","), + } + context.setlayer (layerspec,locationspec,function() + context.definefieldbody ( { name } , fieldspec ) + context.fieldbody ( { name } ) + end) + -- + elseif what == "Btn" then + if wflags.Radio or wflags.RadiosInUnison then + -- AP AS DA F Ff FT H MK T V | AA OC + wflags.Radio = nil + wflags.RadiosInUnison = nil + what = "radio" + elseif wflags.PushButton then + -- AP DA F Ff FT H MK T | AA OC + -- + -- Push buttons only have an appearance and some associated + -- actions so they are not worth copying. + -- + wflags.PushButton = nil + what = "push" + else + -- AP AS DA F Ff FT H MK T V | OC AA + what = "check" + -- direct + local AP = annotation.AP or (parent and parent.AP) + if AP then + local a = document.__xrefs__[AP] + if a and pdfe.copyappearance then + local o = pdfe.copyappearance(document,a) + if o then + AP = pdfreference(o) + end + end + end + local dictionary = pdfdictionary { + Subtype = pdfconstant("Widget"), + FT = pdfconstant("Btn"), + T = pdfcopyunicode(annotation.T or parent.T), + F = pdfcopyinteger(annotation.F or parent.F), + Ff = pdfcopyinteger(annotation.Ff or parent.Ff), + AS = pdfcopyconstant(annotation.AS or (parent and parent.AS)), + AP = AP and pdfreference(AP), + } + local finalize = dictionary() + context.setlayer(layerspec,locationspec,function() + context(hpack_node(nodeinjections.annotation(W/bpfactor,H/bpfactor,0,finalize))) + end) + -- + end + elseif what == "Ch" then + -- F Ff FT Opt T | AA OC (rest follows) + if wflags.PopUp then + wflags.PopUp = nil + if wflags.Edit then + wflags.Edit = nil + what = "combo" + else + what = "popup" + end + else + what = "choice" + end + elseif what == "Sig" then + what = "signature" + else + what = nil + end + end + end + end + end + end + end +end + +-- Beware, bookmarks can be in pdfdoc encoding or in unicode. However, in mkiv we +-- write out the strings in unicode (hex). When we read them in, we check for a bom +-- and convert to utf. + +function codeinjections.getbookmarks(filename) + + -- The first version built a nested tree and flattened that afterwards ... but I decided + -- to keep it simple and flat. + + local list = bookmarks.extras.get(filename) + + if list then + return list + else + list = { } + end + + local document = nil + + if isfile(filename) then + document = loadpdffile(filename) + else + report_outline("unknown file %a",filename) + bookmarks.extras.register(filename,list) + return list + end + + local outlines = document.Catalog.Outlines + local pages = document.pages + local nofpages = document.nofpages + local destinations = document.destinations + + -- I need to check this destination analyzer with the one in annotations .. best share + -- code (and not it's inconsistent). On the todo list ... + + local function setdestination(current,entry) + local destination = nil + local action = current.A + if action then + local subtype = action.S + if subtype == "GoTo" then + destination = action.D + local kind = type(destination) + if kind == "string" then + entry.destination = destination + destination = destinations[destination] + local pagedata = destination and destination[1] + if pagedata then + entry.realpage = pagedata.number + end + elseif kind == "table" then + local pageref = #destination + if pageref then + local pagedata = pages[pageref] + if pagedata then + entry.realpage = pagedata.number + end + end + end + -- elseif subtype then + -- report("unsupported bookmark action %a",subtype) + end + else + local destination = current.Dest + if destination then + if type(destination) == "string" then + local wanted = destinations[destination] + destination = wanted and wanted.D + if destination then + entry.destination = destination + end + else + local pagedata = destination and destination[1] + if pagedata and pagedata.Type == "Page" then + entry.realpage = pagedata.number + -- else + -- report("unsupported bookmark destination (no page)") + end + end + end + end + end + + local function traverse(current,depth) + while current do + -- local title = current.Title + local title = current("Title") -- can be pdfdoc or unicode + if title then + local entry = { + level = depth, + title = title, + } + list[#list+1] = entry + setdestination(current,entry) + if trace_outlines then + report_outline("%w%s",2*depth,title) + end + end + local first = current.First + if first then + local current = first + while current do + local title = current.Title + if title and trace_outlines then + report_outline("%w%s",2*depth,title) + end + local entry = { + level = depth, + title = title, + } + setdestination(current,entry) + list[#list+1] = entry + traverse(current.First,depth+1) + current = current.Next + end + end + current = current.Next + end + end + + if outlines then + if trace_outlines then + report_outline("outline of %a:",document.filename) + report_outline() + end + traverse(outlines,0) + if trace_outlines then + report_outline() + end + elseif trace_outlines then + report_outline("no outline in %a",document.filename) + end + + bookmarks.extras.register(filename,list) + + return list + +end + +function codeinjections.mergebookmarks(specification) + -- codeinjections.getbookmarks(document) + if not specification then + specification = figures and figures.current() + specification = specification and specification.status + end + if specification then + local fullname = specification.fullname + local bookmarks = backends.codeinjections.getbookmarks(fullname) + local realpage = tonumber(specification.page) or 1 + for i=1,#bookmarks do + local b = bookmarks[i] + if not b.usedpage then + if b.realpage == realpage then + if trace_options then + report_outline("using %a at page %a of file %a",b.title,realpage,fullname) + end + b.usedpage = true + b.section = structures.sections.currentsectionindex() + b.pageindex = specification.pageindex + end + end + end + end +end + +-- A bit more than a placeholder but in the same perspective as +-- inclusion of comments and fields: +-- +-- getinfo{ filename = "tt.pdf", metadata = true } +-- getinfo{ filename = "tt.pdf", page = 1, metadata = "xml" } +-- getinfo("tt.pdf") + +function codeinjections.getinfo(specification) + if type(specification) == "string" then + specification = { filename = specification } + end + local filename = specification.filename + if type(filename) == "string" and isfile(filename) then + local pdffile = loadpdffile(filename) + if pdffile then + local pagenumber = specification.page or 1 + local metadata = specification.metadata + local catalog = pdffile.Catalog + local info = pdffile.Info + local pages = pdffile.pages + local nofpages = pdffile.nofpages + if metadata then + local m = catalog.Metadata + if m then + m = m() + if metadata == "xml" then + metadata = xml.convert(m) + else + metadata = m + end + else + metadata = nil + end + else + metadata = nil + end + if pagenumber > nofpages then + pagenumber = nofpages + end + local nobox = { 0, 0, 0, 0 } + local crop = nobox + local media = nobox + local page = pages[pagenumber] + if page then + crop = page.CropBox or nobox + media = page.MediaBox or crop or nobox + end + local bbox = crop or media or nobox + return { + filename = filename, + pdfversion = tonumber(catalog.Version), + nofpages = nofpages, + title = info.Title, + creator = info.Creator, + producer = info.Producer, + creationdate = info.CreationDate, + modification = info.ModDate, + metadata = metadata, + width = bbox[4] - bbox[2], + height = bbox[3] - bbox[1], + cropbox = { crop[1], crop[2], crop[3], crop[4] }, -- we need access + mediabox = { media[1], media[2], media[3], media[4] } , -- we need access + } + end + end +end diff --git a/tex/context/base/mkxl/lpdf-fld.lmt b/tex/context/base/mkxl/lpdf-fld.lmt new file mode 100644 index 000000000..eacbb085d --- /dev/null +++ b/tex/context/base/mkxl/lpdf-fld.lmt @@ -0,0 +1,1501 @@ +if not modules then modules = { } end modules ['lpdf-fld'] = { + version = 1.001, + 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" +} + +-- TURN OFF: preferences -> forms -> highlight -> etc + +-- The problem with widgets is that so far each version of acrobat has some +-- rendering problem. I tried to keep up with this but it makes no sense to do so as +-- one cannot rely on the viewer not changing. Especially Btn fields are tricky as +-- their appearences need to be synchronized in the case of children but e.g. +-- acrobat 10 does not retain the state and forces a check symbol. If you make a +-- file in acrobat then it has MK entries that seem to overload the already present +-- appearance streams (they're probably only meant for printing) as it looks like +-- the viewer has some fallback on (auto generated) MK behaviour built in. So ... +-- hard to test. Unfortunately not even the default appearance is generated. This +-- will probably be solved at some point. +-- +-- Also, for some reason the viewer does not always show custom appearances when +-- fields are being rolled over or clicked upon, and circles or checks pop up when +-- you don't expect them. I fear that this kind of instability eventually will kill +-- pdf forms. After all, the manual says: "individual annotation handlers may ignore +-- this entry and provide their own appearances" and one might wonder what +-- 'individual' means here, but effectively this renders the whole concept of +-- appearances useless. +-- +-- Okay, here is one observation. A pdf file contains objects and one might consider +-- each one to be a static entity when read in. However, acrobat starts rendering +-- and seems to manipulate (appearance streams) of objects in place (this is visible +-- when the file is saved again). And, combined with some other caching and hashing, +-- this might give side effects for shared objects. So, it seems that for some cases +-- one can best be not too clever and not share but duplicate information. Of course +-- this defeats the whole purpose of these objects. Of course I can be wrong. +-- +-- A rarther weird side effect of the viewer is that the highlighting of fields +-- obscures values, unless you uses one of the BS variants, and this makes custum +-- appearances rather useless as there is no way to control this apart from changing +-- the viewer preferences. It could of course be a bug but it would be nice if the +-- highlighting was at least transparent. I have no clue why the built in shapes +-- work ok (some xform based appearances are generated) while equally valid other +-- xforms fail. It looks like acrobat appearances come on top (being refered to in +-- the MK) while custom ones are behind the highlight rectangle. One can disable the +-- "Show border hover color for fields" option in the preferences. If you load +-- java-imp-rhh this side effect gets disabled and you get what you expect (it took +-- me a while to figure out this hack). +-- +-- When highlighting is enabled, those default symbols flash up, so it looks like we +-- have some inteference between this setting and custom appearances. +-- +-- Anyhow, the NeedAppearances is really needed in order to get a rendering for +-- printing especially when highlighting (those colorfull foregrounds) is on. + +local tostring, tonumber, next = tostring, tonumber, next +local gmatch, lower, format, formatters = string.gmatch, string.lower, string.format, string.formatters +local lpegmatch = lpeg.match +local bpfactor, todimen = number.dimenfactors.bp, string.todimen +local sortedhash = table.sortedhash +local trace_fields = false trackers.register("backends.fields", function(v) trace_fields = v end) + +local report_fields = logs.reporter("backend","fields") + +local backends, lpdf = backends, lpdf + +local variables = interfaces.variables +local context = context + +local references = structures.references +local settings_to_array = utilities.parsers.settings_to_array + +local pdfbackend = backends.pdf + +local nodeinjections = pdfbackend.nodeinjections +local codeinjections = pdfbackend.codeinjections +local registrations = pdfbackend.registrations + +local registeredsymbol = codeinjections.registeredsymbol + +local pdfstream = lpdf.stream +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfreference = lpdf.reference +local pdfunicode = lpdf.unicode +local pdfstring = lpdf.string +local pdfconstant = lpdf.constant +local pdfaction = lpdf.action + +local pdfflushobject +local pdfshareobjectreference +local pdfshareobject +local pdfreserveobject +local pdfpagereference +local pdfmajorversion + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfshareobjectreference = lpdf.shareobjectreference + pdfshareobject = lpdf.shareobject + pdfreserveobject = lpdf.reserveobject + pdfpagereference = lpdf.pagereference + pdfmajorversion = lpdf.majorversion +end) + +local pdfcolor = lpdf.color +local pdfcolorvalues = lpdf.colorvalues +local pdflayerreference = lpdf.layerreference + +local hpack_node = node.hpack + +local submitoutputformat = 0 -- 0=unknown 1=HTML 2=FDF 3=XML => not yet used, needs to be checked + +local pdf_widget = pdfconstant("Widget") +local pdf_tx = pdfconstant("Tx") +local pdf_sig = pdfconstant("Sig") +local pdf_ch = pdfconstant("Ch") +local pdf_btn = pdfconstant("Btn") +local pdf_yes = pdfconstant("Yes") +local pdf_off = pdfconstant("Off") +local pdf_p = pdfconstant("P") -- None Invert Outline Push +local pdf_n = pdfconstant("N") -- None Invert Outline Push +-- +local pdf_no_rect = pdfarray { 0, 0, 0, 0 } + +local splitter = lpeg.splitat("=>") + +local formats = { + html = 1, fdf = 2, xml = 3, +} + +function codeinjections.setformsmethod(name) + submitoutputformat = formats[lower(name)] or formats.xml +end + +local flag = { -- /Ff + ReadOnly = 0x00000001, -- 2^ 0 + Required = 0x00000002, -- 2^ 1 + NoExport = 0x00000004, -- 2^ 2 + MultiLine = 0x00001000, -- 2^12 + Password = 0x00002000, -- 2^13 + NoToggleToOff = 0x00004000, -- 2^14 + Radio = 0x00008000, -- 2^15 + PushButton = 0x00010000, -- 2^16 + PopUp = 0x00020000, -- 2^17 + Edit = 0x00040000, -- 2^18 + Sort = 0x00080000, -- 2^19 + FileSelect = 0x00100000, -- 2^20 + DoNotSpellCheck = 0x00400000, -- 2^22 + DoNotScroll = 0x00800000, -- 2^23 + Comb = 0x01000000, -- 2^24 + RichText = 0x02000000, -- 2^25 + RadiosInUnison = 0x02000000, -- 2^25 + CommitOnSelChange = 0x04000000, -- 2^26 +} + +local plus = { -- /F + Invisible = 0x00000001, -- 2^0 + Hidden = 0x00000002, -- 2^1 + Printable = 0x00000004, -- 2^2 + Print = 0x00000004, -- 2^2 + NoZoom = 0x00000008, -- 2^3 + NoRotate = 0x00000010, -- 2^4 + NoView = 0x00000020, -- 2^5 + ReadOnly = 0x00000040, -- 2^6 + Locked = 0x00000080, -- 2^7 + ToggleNoView = 0x00000100, -- 2^8 + LockedContents = 0x00000200, -- 2^9 + AutoView = 0x00000100, -- 2^8 +} + +-- todo: check what is interfaced + +flag.readonly = flag.ReadOnly +flag.required = flag.Required +flag.protected = flag.Password +flag.sorted = flag.Sort +flag.unavailable = flag.NoExport +flag.nocheck = flag.DoNotSpellCheck +flag.fixed = flag.DoNotScroll +flag.file = flag.FileSelect + +plus.hidden = plus.Hidden +plus.printable = plus.Printable +plus.auto = plus.AutoView + +lpdf.flags.widgets = flag +lpdf.flags.annotations = plus + +-- some day .. lpeg with function or table + +local function fieldflag(specification) -- /Ff + local o, n = specification.option, 0 + if o and o ~= "" then + for f in gmatch(o,"[^, ]+") do + n = n + (flag[f] or 0) + end + end + return n +end + +local function fieldplus(specification) -- /F + local o, n = specification.option, 0 + if o and o ~= "" then + for p in gmatch(o,"[^, ]+") do + n = n + (plus[p] or 0) + end + end +-- n = n + 4 + return n +end + +-- keep: +-- +-- local function checked(what) +-- local set, bug = references.identify("",what) +-- if not bug and #set > 0 then +-- local r, n = pdfaction(set) +-- return pdfshareobjectreference(r) +-- end +-- end +-- +-- local function fieldactions(specification) -- share actions +-- local d, a = { }, nil +-- a = specification.mousedown +-- or specification.clickin if a and a ~= "" then d.D = checked(a) end +-- a = specification.mouseup +-- or specification.clickout if a and a ~= "" then d.U = checked(a) end +-- a = specification.regionin if a and a ~= "" then d.E = checked(a) end -- Enter +-- a = specification.regionout if a and a ~= "" then d.X = checked(a) end -- eXit +-- a = specification.afterkey if a and a ~= "" then d.K = checked(a) end +-- a = specification.format if a and a ~= "" then d.F = checked(a) end +-- a = specification.validate if a and a ~= "" then d.V = checked(a) end +-- a = specification.calculate if a and a ~= "" then d.C = checked(a) end +-- a = specification.focusin if a and a ~= "" then d.Fo = checked(a) end +-- a = specification.focusout if a and a ~= "" then d.Bl = checked(a) end +-- a = specification.openpage if a and a ~= "" then d.PO = checked(a) end +-- a = specification.closepage if a and a ~= "" then d.PC = checked(a) end +-- -- a = specification.visiblepage if a and a ~= "" then d.PV = checked(a) end +-- -- a = specification.invisiblepage if a and a ~= "" then d.PI = checked(a) end +-- return next(d) and pdfdictionary(d) +-- end + +local mapping = { + mousedown = "D", clickin = "D", + mouseup = "U", clickout = "U", + regionin = "E", + regionout = "X", + afterkey = "K", + format = "F", + validate = "V", + calculate = "C", + focusin = "Fo", + focusout = "Bl", + openpage = "PO", + closepage = "PC", + -- visiblepage = "PV", + -- invisiblepage = "PI", +} + +local function fieldactions(specification) -- share actions + local d = nil + for key, target in sortedhash(mapping) do -- sort so that we can compare pdf + local code = specification[key] + if code and code ~= "" then + -- local a = checked(code) + local set, bug = references.identify("",code) + if not bug and #set > 0 then + local a = pdfaction(set) -- r, n + if a then + local r = pdfshareobjectreference(a) + if d then + d[target] = r + else + d = pdfdictionary { [target] = r } + end + else + report_fields("invalid field action %a, case %s",code,2) + end + else + report_fields("invalid field action %a, case %s",code,1) + end + end + end + -- if d then + -- d = pdfshareobjectreference(d) -- not much overlap or maybe only some patterns + -- end + return d +end + +-- fonts and color + +local pdfdocencodingvector, pdfdocencodingcapsule + +-- The pdf doc encoding vector is needed in order to trigger propper unicode. Interesting is that when +-- a glyph is not in the vector, it is still visible as it is taken from some other font. Messy. + +-- To be checked: only when text/line fields. + +local function checkpdfdocencoding() + report_fields("adding pdfdoc encoding vector") + local encoding = dofile(resolvers.findfile("lpdf-enc.lmt")) -- no checking, fatal if not present + pdfdocencodingvector = pdfreference(pdfflushobject(encoding)) + local capsule = pdfdictionary { + PDFDocEncoding = pdfdocencodingvector + } + pdfdocencodingcapsule = pdfreference(pdfflushobject(capsule)) + checkpdfdocencoding = function() end +end + +local fontnames = { + rm = { + tf = "Times-Roman", + bf = "Times-Bold", + it = "Times-Italic", + sl = "Times-Italic", + bi = "Times-BoldItalic", + bs = "Times-BoldItalic", + }, + ss = { + tf = "Helvetica", + bf = "Helvetica-Bold", + it = "Helvetica-Oblique", + sl = "Helvetica-Oblique", + bi = "Helvetica-BoldOblique", + bs = "Helvetica-BoldOblique", + }, + tt = { + tf = "Courier", + bf = "Courier-Bold", + it = "Courier-Oblique", + sl = "Courier-Oblique", + bi = "Courier-BoldOblique", + bs = "Courier-BoldOblique", + }, + symbol = { + dingbats = "ZapfDingbats", + } +} + +local usedfonts = { } + +local function fieldsurrounding(specification) + local fontsize = specification.fontsize or "12pt" + local fontstyle = specification.fontstyle or "rm" + local fontalternative = specification.fontalternative or "tf" + local colorvalue = tonumber(specification.colorvalue) + local s = fontnames[fontstyle] + if not s then + fontstyle, s = "rm", fontnames.rm + end + local a = s[fontalternative] + if not a then + alternative, a = "tf", s.tf + end + local tag = fontstyle .. fontalternative + fontsize = todimen(fontsize) + fontsize = fontsize and (bpfactor * fontsize) or 12 + fontraise = 0.1 * fontsize -- todo: figure out what the natural one is and compensate for strutdp + local fontcode = formatters["%0.4F Tf %0.4F Ts"](fontsize,fontraise) + -- we could test for colorvalue being 1 (black) and omit it then + local colorcode = pdfcolor(3,colorvalue) -- we force an rgb color space + if trace_fields then + report_fields("using font, style %a, alternative %a, size %p, tag %a, code %a",fontstyle,fontalternative,fontsize,tag,fontcode) + report_fields("using color, value %a, code %a",colorvalue,colorcode) + end + local stream = pdfstream { + pdfconstant(tag), + formatters["%s %s"](fontcode,colorcode) + } + usedfonts[tag] = a -- the name + -- move up with "x.y Ts" + return tostring(stream) +end + +-- Can we use any font? + +codeinjections.fieldsurrounding = fieldsurrounding + +local function registerfonts() + if next(usedfonts) then + checkpdfdocencoding() -- already done + local pdffontlist = pdfdictionary() + local pdffonttype = pdfconstant("Font") + local pdffontsubtype = pdfconstant("Type1") + for tag, name in sortedhash(usedfonts) do + local f = pdfdictionary { + Type = pdffonttype, + Subtype = pdffontsubtype, + Name = pdfconstant(tag), + BaseFont = pdfconstant(name), + Encoding = pdfdocencodingvector, + } + pdffontlist[tag] = pdfreference(pdfflushobject(f)) + end + return pdffontlist + end +end + +-- symbols + +local function fieldappearances(specification) + -- todo: caching + local values = specification.values + local default = specification.default -- todo + if not values then + -- error + return + end + local v = settings_to_array(values) + local n, r, d + if #v == 1 then + n, r, d = v[1], v[1], v[1] + elseif #v == 2 then + n, r, d = v[1], v[1], v[2] + else + n, r, d = v[1], v[2], v[3] + end + local appearance = pdfdictionary { + N = registeredsymbol(n), + R = registeredsymbol(r), + D = registeredsymbol(d), + } + return pdfshareobjectreference(appearance) +-- return pdfreference(pdfflushobject(appearance)) +end + +-- The rendering part of form support has always been crappy and didn't really +-- improve over time. Did bugs become features? Who knows. Why provide for instance +-- control over appearance and then ignore it when the mouse clicks someplace else. +-- Strangely enough a lot of effort went into JavaScript support while basic +-- appearance control of checkboxes stayed poor. I found this link when googling for +-- conformation after the n^th time looking into this behaviour: +-- +-- https://stackoverflow.com/questions/15479855/pdf-appearance-streams-checkbox-not-shown-correctly-after-focus-lost +-- +-- ... "In particular check boxes, therefore, whenever not interacting with the user, shall +-- be displayed using their normal captions, not their appearances." +-- +-- So: don't use check boxes. In fact, even radio buttons can have these funny "flash some +-- funny symbol" side effect when clocking on them. I tried all combinations if /H and /AP +-- and /AS and ... Because (afaiks) the acrobat interface assumes that one uses dingbats no +-- one really cared about getting custom appeances done well. This erratic behaviour might +-- as well be the reason why no open source viewer ever bothered implementing forms. It's +-- probably also why most forms out there look kind of bad. + +local function fieldstates_precheck(specification) + local values = specification.values + local default = specification.default + if not values or values == "" then + return + end + local yes = settings_to_array(values)[1] + local yesshown, yesvalue = lpegmatch(splitter,yes) + if not (yesshown and yesvalue) then + yesshown = yes + end + return default == settings_to_array(yesshown)[1] and pdf_yes or pdf_off +end + +local function fieldstates_check(specification) + -- we don't use Opt here (too messy for radio buttons) + local values = specification.values + local default = specification.default + if not values or values == "" then + -- error + return + end + local v = settings_to_array(values) + local yes, off, yesn, yesr, yesd, offn, offr, offd + if #v == 1 then + yes, off = v[1], v[1] + else + yes, off = v[1], v[2] + end + local yesshown, yesvalue = lpegmatch(splitter,yes) + if not (yesshown and yesvalue) then + yesshown = yes, yes + end + yes = settings_to_array(yesshown) + local offshown, offvalue = lpegmatch(splitter,off) + if not (offshown and offvalue) then + offshown = off, off + end + off = settings_to_array(offshown) + if #yes == 1 then + yesn, yesr, yesd = yes[1], yes[1], yes[1] + elseif #yes == 2 then + yesn, yesr, yesd = yes[1], yes[1], yes[2] + else + yesn, yesr, yesd = yes[1], yes[2], yes[3] + end + if #off == 1 then + offn, offr, offd = off[1], off[1], off[1] + elseif #off == 2 then + offn, offr, offd = off[1], off[1], off[2] + else + offn, offr, offd = off[1], off[2], off[3] + end + if not yesvalue then + yesvalue = yesdefault or yesn + end + if not offvalue then + offvalue = offn + end + if default == yesn then + default = pdf_yes + yesvalue = yesvalue == yesn and "Yes" or "Off" + else + default = pdf_off + yesvalue = "Off" + end + local appearance + -- if false then + if true then + -- needs testing + appearance = pdfdictionary { -- maybe also cache components + N = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesn), Off = registeredsymbol(offn) }), + R = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesr), Off = registeredsymbol(offr) }), + D = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesd), Off = registeredsymbol(offd) }), + } + else + appearance = pdfdictionary { -- maybe also cache components + N = pdfdictionary { Yes = registeredsymbol(yesn), Off = registeredsymbol(offn) }, + R = pdfdictionary { Yes = registeredsymbol(yesr), Off = registeredsymbol(offr) }, + D = pdfdictionary { Yes = registeredsymbol(yesd), Off = registeredsymbol(offd) } + } + end + local appearanceref = pdfshareobjectreference(appearance) + -- local appearanceref = pdfreference(pdfflushobject(appearance)) + return appearanceref, default, yesvalue +end + +-- It looks like there is always a (MK related) symbol used and that the appearances +-- are only used as ornaments behind a symbol. So, contrary to what we did when +-- widgets showed up, we now limit ourself to more dumb definitions. Especially when +-- highlighting is enabled weird interferences happen. So, we play safe (some nice +-- code has been removed that worked well till recently). + +local function fieldstates_radio(specification,name,parent) + local values = values or specification.values + local default = default or parent.default -- specification.default + if not values or values == "" then + -- error + return + end + local v = settings_to_array(values) + local yes, off, yesn, yesr, yesd, offn, offr, offd + if #v == 1 then + yes, off = v[1], v[1] + else + yes, off = v[1], v[2] + end + -- yes keys might be the same in the three appearances within a field + -- but can best be different among fields ... don't ask why + local yessymbols, yesvalue = lpegmatch(splitter,yes) -- n,r,d=>x + if not (yessymbols and yesvalue) then + yessymbols = yes + end + if not yesvalue then + yesvalue = name + end + yessymbols = settings_to_array(yessymbols) + if #yessymbols == 1 then + yesn = yessymbols[1] + yesr = yesn + yesd = yesr + elseif #yessymbols == 2 then + yesn = yessymbols[1] + yesr = yessymbols[2] + yesd = yesr + else + yesn = yessymbols[1] + yesr = yessymbols[2] + yesd = yessymbols[3] + end + -- we don't care about names, as all will be /Off + local offsymbols = lpegmatch(splitter,off) or off + offsymbols = settings_to_array(offsymbols) + if #offsymbols == 1 then + offn = offsymbols[1] + offr = offn + offd = offr + elseif #offsymbols == 2 then + offn = offsymbols[1] + offr = offsymbols[2] + offd = offr + else + offn = offsymbols[1] + offr = offsymbols[2] + offd = offsymbols[3] + end + if default == name then + default = pdfconstant(name) + else + default = pdf_off + end + -- + local appearance + if false then -- needs testing + appearance = pdfdictionary { -- maybe also cache components + N = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesn), Off = registeredsymbol(offn) }), + R = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesr), Off = registeredsymbol(offr) }), + D = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesd), Off = registeredsymbol(offd) }), + } + else + appearance = pdfdictionary { -- maybe also cache components + N = pdfdictionary { [name] = registeredsymbol(yesn), Off = registeredsymbol(offn) }, + R = pdfdictionary { [name] = registeredsymbol(yesr), Off = registeredsymbol(offr) }, + D = pdfdictionary { [name] = registeredsymbol(yesd), Off = registeredsymbol(offd) } + } + end + local appearanceref = pdfshareobjectreference(appearance) -- pdfreference(pdfflushobject(appearance)) + return appearanceref, default, yesvalue +end + +local function fielddefault(field,pdf_yes) + local default = field.default + if not default or default == "" then + local values = settings_to_array(field.values) + default = values[1] + end + if not default or default == "" then + return pdf_off + else + return pdf_yes or pdfconstant(default) + end +end + +local function fieldoptions(specification) + local values = specification.values + local default = specification.default + if values then + local v = settings_to_array(values) + for i=1,#v do + local vi = v[i] + local shown, value = lpegmatch(splitter,vi) + if shown and value then + v[i] = pdfarray { pdfunicode(value), shown } + else + v[i] = pdfunicode(v[i]) + end + end + return pdfarray(v) + end +end + +local mapping = { + -- acrobat compliant (messy, probably some pdfdoc encoding interference here) + check = "4", -- 0x34 + circle = "l", -- 0x6C + cross = "8", -- 0x38 + diamond = "u", -- 0x75 + square = "n", -- 0x6E + star = "H", -- 0x48 +} + +local function todingbat(n) + if n and n ~= "" then + return mapping[n] or "" + end +end + +local function fieldrendering(specification) + local bvalue = tonumber(specification.backgroundcolorvalue) + local fvalue = tonumber(specification.framecolorvalue) + local svalue = specification.fontsymbol + if bvalue or fvalue or (svalue and svalue ~= "") then + return pdfdictionary { + BG = bvalue and pdfarray { pdfcolorvalues(3,bvalue) } or nil, -- or zero_bg, + BC = fvalue and pdfarray { pdfcolorvalues(3,fvalue) } or nil, -- or zero_bc, + CA = svalue and pdfstring (svalue) or nil, + } + end +end + +-- layers + +local function fieldlayer(specification) -- we can move this in line + local layer = specification.layer + return (layer and pdflayerreference(layer)) or nil +end + +-- defining + +local fields, radios, clones, fieldsets, calculationset = { }, { }, { }, { }, nil + +local xfdftemplate = [[ +<?xml version='1.0' encoding='UTF-8'?> + +<xfdf xmlns='http://ns.adobe.com/xfdf/'> + <f href='%s.pdf'/> + <fields> +%s + </fields> +</xfdf> +]] + +function codeinjections.exportformdata(name) + local result = { } + for k, v in sortedhash(fields) do + result[#result+1] = formatters[" <field name='%s'><value>%s</value></field>"](v.name or k,v.default or "") + end + local base = file.basename(tex.jobname) + local xfdf = format(xfdftemplate,base,table.concat(result,"\n")) + if not name or name == "" then + name = base + end + io.savedata(file.addsuffix(name,"xfdf"),xfdf) +end + +function codeinjections.definefieldset(tag,list) + fieldsets[tag] = list +end + +function codeinjections.getfieldset(tag) + return fieldsets[tag] +end + +local function fieldsetlist(tag) + if tag then + local ft = fieldsets[tag] + if ft then + local a = pdfarray() + for name in gmatch(list,"[^, ]+") do + local f = field[name] + if f and f.pobj then + a[#a+1] = pdfreference(f.pobj) + end + end + return a + end + end +end + +function codeinjections.setfieldcalculationset(tag) + calculationset = tag +end + +interfaces.implement { + name = "setfieldcalculationset", + actions = codeinjections.setfieldcalculationset, + arguments = "string", +} + +local function predefinesymbols(specification) + local values = specification.values + if values then + local symbols = settings_to_array(values) + for i=1,#symbols do + local symbol = symbols[i] + local a, b = lpegmatch(splitter,symbol) + codeinjections.presetsymbol(a or symbol) + end + end +end + +function codeinjections.getdefaultfieldvalue(name) + local f = fields[name] + if f then + local values = f.values + local default = f.default + if not default or default == "" then + local symbols = settings_to_array(values) + local symbol = symbols[1] + if symbol then + local a, b = lpegmatch(splitter,symbol) -- splits at => + default = a or symbol + end + end + return default + end +end + +function codeinjections.definefield(specification) + local n = specification.name + local f = fields[n] + if not f then + local fieldtype = specification.type + if not fieldtype then + if trace_fields then + report_fields("invalid definition for %a, unknown type",n) + end + elseif fieldtype == "radio" then + local values = specification.values + if values and values ~= "" then + values = settings_to_array(values) + for v=1,#values do + radios[values[v]] = { parent = n } + end + fields[n] = specification + if trace_fields then + report_fields("defining %a as type %a",n,"radio") + end + elseif trace_fields then + report_fields("invalid definition of radio %a, missing values",n) + end + elseif fieldtype == "sub" then + -- not in main field list ! + local radio = radios[n] + if radio then + -- merge specification + for key, value in next, specification do + radio[key] = value + end + if trace_fields then + local p = radios[n] and radios[n].parent + report_fields("defining %a as type sub of radio %a",n,p) + end + elseif trace_fields then + report_fields("invalid definition of radio sub %a, no parent given",n) + end + predefinesymbols(specification) + elseif fieldtype == "text" or fieldtype == "line" then + fields[n] = specification + if trace_fields then + report_fields("defining %a as type %a",n,fieldtype) + end + if specification.values ~= "" and specification.default == "" then + specification.default, specification.values = specification.values, nil + end + else + fields[n] = specification + if trace_fields then + report_fields("defining %a as type %a",n,fieldtype) + end + predefinesymbols(specification) + end + elseif trace_fields then + report_fields("invalid definition for %a, already defined",n) + end +end + +function codeinjections.clonefield(specification) -- obsolete + local p = specification.parent + local c = specification.children + local v = specification.alternative + if not p or not c then + if trace_fields then + report_fields("invalid clone, children %a, parent %a, alternative %a",c,p,v) + end + return + end + local x = fields[p] or radios[p] + if not x then + if trace_fields then + report_fields("invalid clone, unknown parent %a",p) + end + return + end + for n in gmatch(c,"[^, ]+") do + local f, r, c = fields[n], radios[n], clones[n] + if f or r or c then + if trace_fields then + report_fields("already cloned, child %a, parent %a, alternative %a",n,p,v) + end + else + if trace_fields then + report_fields("cloning, child %a, parent %a, alternative %a",n,p,v) + end + clones[n] = specification + predefinesymbols(specification) + end + end +end + +function codeinjections.getfieldcategory(name) + local f = fields[name] or radios[name] or clones[name] + if f then + local g = f.category + if not g or g == "" then + local v, p, t = f.alternative, f.parent, f.type + if v == "clone" or v == "copy" then + f = fields[p] or radios[p] + g = f and f.category + elseif t == "sub" then + f = fields[p] + g = f and f.category + end + end + return g + end +end + +-- + +function codeinjections.validfieldcategory(name) + return fields[name] or radios[name] or clones[name] +end + +function codeinjections.validfieldset(name) + return fieldsets[tag] +end + +function codeinjections.validfield(name) + return fields[name] +end + +-- + +local alignments = { + flushleft = 0, right = 0, + center = 1, middle = 1, + flushright = 2, left = 2, +} + +local function fieldalignment(specification) + return alignments[specification.align] or 0 +end + +local function enhance(specification,option) + local so = specification.option + if so and so ~= "" then + specification.option = so .. "," .. option + else + specification.option = option + end + return specification +end + +-- finish (if we also collect parents we can inline the kids which is +-- more efficient ... but hardly anyone used widgets so ...) + +local collected = pdfarray() +local forceencoding = false + +-- todo : check #opt + +local function finishfields() + local sometext = forceencoding + local somefont = next(usedfonts) + for name, field in sortedhash(fields) do + local kids = field.kids + if kids then + pdfflushobject(field.kidsnum,kids) + end + local opt = field.opt + if opt then + pdfflushobject(field.optnum,opt) + end + local type = field.type + if not sometext and (type == "text" or type == "line") then + sometext = true + end + end + for name, field in sortedhash(radios) do + local kids = field.kids + if kids then + pdfflushobject(field.kidsnum,kids) + end + local opt = field.opt + if opt then + pdfflushobject(field.optnum,opt) + end + end + if #collected > 0 then + local acroform = pdfdictionary { + NeedAppearances = pdfmajorversion() == 1 or nil, + Fields = pdfreference(pdfflushobject(collected)), + CO = fieldsetlist(calculationset), + } + if sometext or somefont then + checkpdfdocencoding() + if sometext then + usedfonts.tttf = fontnames.tt.tf + acroform.DA = "/tttf 12 Tf 0 g" + end + acroform.DR = pdfdictionary { + Font = registerfonts(), + Encoding = pdfdocencodingcapsule, + } + end + -- maybe: + -- if sometext then + -- checkpdfdocencoding() + -- if sometext then + -- usedfonts.tttf = fontnames.tt.tf + -- acroform.DA = "/tttf 12 Tf 0 g" + -- end + -- acroform.DR = pdfdictionary { + -- Font = registerfonts(), + -- Encoding = pdfdocencodingcapsule, + -- } + -- elseif somefont then + -- acroform.DR = pdfdictionary { + -- Font = registerfonts(), + -- } + -- end + lpdf.addtocatalog("AcroForm",pdfreference(pdfflushobject(acroform))) + end +end + +lpdf.registerdocumentfinalizer(finishfields,"form fields") + +local methods = { } + +function nodeinjections.typesetfield(name,specification) + local field = fields[name] or radios[name] or clones[name] + if not field then + report_fields( "unknown child %a",name) + -- unknown field + return + end + local alternative, parent = field.alternative, field.parent + if alternative == "copy" or alternative == "clone" then -- only in clones + field = fields[parent] or radios[parent] + end + local method = methods[field.type] + if method then + return method(name,specification,alternative) + else + report_fields( "unknown method %a for child %a",field.type,name) + end +end + +local function save_parent(field,specification,d) + local kidsnum = pdfreserveobject() + d.Kids = pdfreference(kidsnum) + field.kidsnum = kidsnum + field.kids = pdfarray() +-- if d.Opt then +-- local optnum = pdfreserveobject() +-- d.Opt = pdfreference(optnum) +-- field.optnum = optnum +-- field.opt = pdfarray() +-- end + local pnum = pdfflushobject(d) + field.pobj = pnum + collected[#collected+1] = pdfreference(pnum) +end + +local function save_kid(field,specification,d,optname) + local kn = pdfreserveobject() + field.kids[#field.kids+1] = pdfreference(kn) +-- if optname then +-- local opt = field.opt +-- if opt then +-- opt[#opt+1] = optname +-- end +-- end + local width = specification.width or 0 + local height = specification.height or 0 + local depth = specification.depth or 0 + local box = hpack_node(nodeinjections.annotation(width,height,depth,d(),kn)) + -- redundant + box.width = width + box.height = height + box.depth = depth + return box +end + +local function makelineparent(field,specification) + local text = pdfunicode(field.default) + local length = tonumber(specification.length or 0) or 0 + local d = pdfdictionary { + Subtype = pdf_widget, + T = pdfunicode(specification.title), + F = fieldplus(specification), + Ff = fieldflag(specification), + OC = fieldlayer(specification), + DA = fieldsurrounding(specification), + AA = fieldactions(specification), + FT = pdf_tx, + Q = fieldalignment(specification), + MaxLen = length == 0 and 1000 or length, + DV = text, + V = text, + } + save_parent(field,specification,d) +end + +local function makelinechild(name,specification) + local field = clones[name] + local parent = nil + if field then + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent text %a",parent.name) + end + makelineparent(parent,specification) + end + else + parent = fields[name] + field = parent + if not parent.pobj then + if trace_fields then + report_fields("using parent text %a",name) + end + makelineparent(parent,specification) + end + end + if trace_fields then + report_fields("using child text %a",name) + end + -- we could save a little by not setting some key/value when it's the + -- same as parent but it would cost more memory to keep track of it + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(parent.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + DA = fieldsurrounding(specification), + AA = fieldactions(specification), + MK = fieldrendering(specification), + Q = fieldalignment(specification), + } + return save_kid(parent,specification,d) +end + +function methods.line(name,specification) + return makelinechild(name,specification) +end + +function methods.text(name,specification) + return makelinechild(name,enhance(specification,"MultiLine")) +end + +-- copy of line ... probably also needs a /Lock + +local function makesignatureparent(field,specification) + local text = pdfunicode(field.default) + local length = tonumber(specification.length or 0) or 0 + local d = pdfdictionary { + Subtype = pdf_widget, + T = pdfunicode(specification.title), + F = fieldplus(specification), + Ff = fieldflag(specification), + OC = fieldlayer(specification), + DA = fieldsurrounding(specification), + AA = fieldactions(specification), + FT = pdf_sig, + Q = fieldalignment(specification), + MaxLen = length == 0 and 1000 or length, + DV = text, + V = text, + } + save_parent(field,specification,d) +end + +local function makesignaturechild(name,specification) + local field = clones[name] + local parent = nil + if field then + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent signature %a",parent.name) + end + makesignatureparent(parent,specification) + end + else + parent = fields[name] + field = parent + if not parent.pobj then + if trace_fields then + report_fields("using parent text %a",name) + end + makesignatureparent(parent,specification) + end + end + if trace_fields then + report_fields("using child text %a",name) + end + -- we could save a little by not setting some key/value when it's the + -- same as parent but it would cost more memory to keep track of it + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(parent.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + DA = fieldsurrounding(specification), + AA = fieldactions(specification), + MK = fieldrendering(specification), + Q = fieldalignment(specification), + } + return save_kid(parent,specification,d) +end + +function methods.signature(name,specification) + return makesignaturechild(name,specification) +end +-- + +local function makechoiceparent(field,specification) + local d = pdfdictionary { + Subtype = pdf_widget, + T = pdfunicode(specification.title), + F = fieldplus(specification), + Ff = fieldflag(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), + FT = pdf_ch, + Opt = fieldoptions(field), -- todo + } + save_parent(field,specification,d) +end + +local function makechoicechild(name,specification) + local field = clones[name] + local parent = nil + if field then + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent choice %a",parent.name) + end + makechoiceparent(parent,specification,extras) + end + else + parent = fields[name] + field = parent + if not parent.pobj then + if trace_fields then + report_fields("using parent choice %a",name) + end + makechoiceparent(parent,specification,extras) + end + end + if trace_fields then + report_fields("using child choice %a",name) + end + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(parent.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), + } + return save_kid(parent,specification,d) -- do opt here +end + +function methods.choice(name,specification) + return makechoicechild(name,specification) +end + +function methods.popup(name,specification) + return makechoicechild(name,enhance(specification,"PopUp")) +end + +function methods.combo(name,specification) + return makechoicechild(name,enhance(specification,"PopUp,Edit")) +end + +local function makecheckparent(field,specification) + local default = fieldstates_precheck(field) + local d = pdfdictionary { + T = pdfunicode(specification.title), -- todo: when tracing use a string + F = fieldplus(specification), + Ff = fieldflag(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), -- can be shared + FT = pdf_btn, + V = fielddefault(field,default), + } + save_parent(field,specification,d) +end + +local function makecheckchild(name,specification) + local field = clones[name] + local parent = nil + if field then + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent check %a",parent.name) + end + makecheckparent(parent,specification,extras) + end + else + parent = fields[name] + field = parent + if not parent.pobj then + if trace_fields then + report_fields("using parent check %a",name) + end + makecheckparent(parent,specification,extras) + end + end + if trace_fields then + report_fields("using child check %a",name) + end + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(parent.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), -- can be shared + H = pdf_n, + } + local fontsymbol = specification.fontsymbol + if fontsymbol and fontsymbol ~= "" then + specification.fontsymbol = todingbat(fontsymbol) + specification.fontstyle = "symbol" + specification.fontalternative = "dingbats" + d.DA = fieldsurrounding(specification) + d.MK = fieldrendering(specification) + return save_kid(parent,specification,d) + else + local appearance, default, value = fieldstates_check(field) + d.AS = default + d.AP = appearance + return save_kid(parent,specification,d) + end +end + +function methods.check(name,specification) + return makecheckchild(name,specification) +end + +local function makepushparent(field,specification) -- check if we can share with the previous + local d = pdfdictionary { + Subtype = pdf_widget, + T = pdfunicode(specification.title), + F = fieldplus(specification), + Ff = fieldflag(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), -- can be shared + FT = pdf_btn, + AP = fieldappearances(field), + H = pdf_p, + } + save_parent(field,specification,d) +end + +local function makepushchild(name,specification) + local field, parent = clones[name], nil + if field then + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent push %a",parent.name) + end + makepushparent(parent,specification) + end + else + parent = fields[name] + field = parent + if not parent.pobj then + if trace_fields then + report_fields("using parent push %a",name) + end + makepushparent(parent,specification) + end + end + if trace_fields then + report_fields("using child push %a",name) + end + local fontsymbol = specification.fontsymbol + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(field.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), -- can be shared + H = pdf_p, + } + if fontsymbol and fontsymbol ~= "" then + specification.fontsymbol = todingbat(fontsymbol) + specification.fontstyle = "symbol" + specification.fontalternative = "dingbats" + d.DA = fieldsurrounding(specification) + d.MK = fieldrendering(specification) + else + d.AP = fieldappearances(field) + end + return save_kid(parent,specification,d) +end + +function methods.push(name,specification) + return makepushchild(name,enhance(specification,"PushButton")) +end + +local function makeradioparent(field,specification) + specification = enhance(specification,"Radio,RadiosInUnison,Print,NoToggleToOff") + local d = pdfdictionary { + T = field.name, + FT = pdf_btn, + -- F = fieldplus(specification), + Ff = fieldflag(specification), + -- H = pdf_n, + V = fielddefault(field), + } + save_parent(field,specification,d) +end + +-- local function makeradiochild(name,specification) +-- local field = clones[name] +-- local parent = nil +-- local pname = nil +-- if field then +-- pname = field.parent +-- field = radios[pname] +-- parent = fields[pname] +-- if not parent.pobj then +-- if trace_fields then +-- report_fields("forcing parent radio %a",parent.name) +-- end +-- makeradioparent(parent,parent) +-- end +-- else +-- field = radios[name] +-- if not field then +-- report_fields("there is some problem with field %a",name) +-- return nil +-- end +-- pname = field.parent +-- parent = fields[pname] +-- if not parent.pobj then +-- if trace_fields then +-- report_fields("using parent radio %a",name) +-- end +-- makeradioparent(parent,parent) +-- end +-- end +-- if trace_fields then +-- report_fields("using child radio %a with values %a and default %a",name,field.values,field.default) +-- end +-- local fontsymbol = specification.fontsymbol +-- -- fontsymbol = "circle" +-- local d = pdfdictionary { +-- Subtype = pdf_widget, +-- Parent = pdfreference(parent.pobj), +-- F = fieldplus(specification), +-- OC = fieldlayer(specification), +-- AA = fieldactions(specification), +-- H = pdf_n, +-- -- H = pdf_p, +-- -- P = pdfpagereference(true), +-- } +-- if fontsymbol and fontsymbol ~= "" then +-- specification.fontsymbol = todingbat(fontsymbol) +-- specification.fontstyle = "symbol" +-- specification.fontalternative = "dingbats" +-- d.DA = fieldsurrounding(specification) +-- d.MK = fieldrendering(specification) +-- return save_kid(parent,specification,d) -- todo: what if no value +-- else +-- local appearance, default, value = fieldstates_radio(field,name,fields[pname]) +-- d.AP = appearance +-- d.AS = default -- /Whatever +-- return save_kid(parent,specification,d,value) +-- end +-- end + +local function makeradiochild(name,specification) + local field, parent = clones[name], nil + if field then + field = radios[field.parent] + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("forcing parent radio %a",parent.name) + end + makeradioparent(parent,parent) + end + else + field = radios[name] + if not field then + report_fields("there is some problem with field %a",name) + return nil + end + parent = fields[field.parent] + if not parent.pobj then + if trace_fields then + report_fields("using parent radio %a",name) + end + makeradioparent(parent,parent) + end + end + if trace_fields then + report_fields("using child radio %a with values %a and default %a",name,field.values,field.default) + end + local fontsymbol = specification.fontsymbol + -- fontsymbol = "circle" + local d = pdfdictionary { + Subtype = pdf_widget, + Parent = pdfreference(parent.pobj), + F = fieldplus(specification), + OC = fieldlayer(specification), + AA = fieldactions(specification), + H = pdf_n, + } + if fontsymbol and fontsymbol ~= "" then + specification.fontsymbol = todingbat(fontsymbol) + specification.fontstyle = "symbol" + specification.fontalternative = "dingbats" + d.DA = fieldsurrounding(specification) + d.MK = fieldrendering(specification) + end + local appearance, default, value = fieldstates_radio(field,name,fields[field.parent]) + d.AP = appearance + d.AS = default -- /Whatever +-- d.MK = pdfdictionary { BC = pdfarray {0}, BG = pdfarray { 1 } } +d.BS = pdfdictionary { S = pdfconstant("I"), W = 1 } + return save_kid(parent,specification,d,value) +end + +function methods.sub(name,specification) + return makeradiochild(name,enhance(specification,"Radio,RadiosInUnison")) +end diff --git a/tex/context/base/mkxl/lpdf-fmt.lmt b/tex/context/base/mkxl/lpdf-fmt.lmt new file mode 100644 index 000000000..c6a3f25ff --- /dev/null +++ b/tex/context/base/mkxl/lpdf-fmt.lmt @@ -0,0 +1,1020 @@ +if not modules then modules = { } end modules ['lpdf-fmt'] = { + version = 1.001, + comment = "companion to lpdf-ini.mkiv", + author = "Peter Rolf and Hans Hagen", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files", +} + +-- Thanks to Luigi and Steffen for testing. + +-- context --directives="backend.format=PDF/X-1a:2001" --trackers=backend.format yourfile + +local tonumber = tonumber +local lower, gmatch, format, find = string.lower, string.gmatch, string.format, string.find +local concat, serialize, sortedhash = table.concat, table.serialize, table.sortedhash + +local trace_format = false trackers.register("backend.format", function(v) trace_format = v end) +local trace_variables = false trackers.register("backend.variables", function(v) trace_variables = v end) + +local report_backend = logs.reporter("backend","profiles") + +local backends, lpdf = backends, lpdf + +local codeinjections = backends.pdf.codeinjections + +local variables = interfaces.variables +local viewerlayers = attributes.viewerlayers +local colors = attributes.colors +local transparencies = attributes.transparencies + +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfconstant = lpdf.constant +local pdfreference = lpdf.reference +local pdfflushobject = lpdf.flushobject +local pdfstring = lpdf.string +local pdfverbose = lpdf.verbose +local pdfflushstreamfileobject = lpdf.flushstreamfileobject + +local addtoinfo = lpdf.addtoinfo +local injectxmpinfo = lpdf.injectxmpinfo +local insertxmpinfo = lpdf.insertxmpinfo + +local settings_to_array = utilities.parsers.settings_to_array +local settings_to_hash = utilities.parsers.settings_to_hash + +--[[ + Comments by Peter: + + output intent : only one profile per color space (and device class) + default color space : (theoretically) several profiles per color space possible + + The default color space profiles define the current gamuts (part of/all the + colors we have in the document), while the output intent profile declares the + gamut of the output devices (the colors that we get normally a printer or + monitor). + + Example: + + I have two RGB pictures (both 'painted' in /DeviceRGB) and I declare sRGB as + default color space for one picture and AdobeRGB for the other. As output + intent I use ISO_coated_v2_eci.icc. + + If I had more than one output intent profile for the combination CMYK/printer I + can't decide which one to use. But it is no problem to use several default color + space profiles for the same color space as it's just a different color + transformation. The relation between picture and profile is clear. +]]-- + +local channels = { + gray = 1, + grey = 1, + rgb = 3, + cmyk = 4, +} + +local prefixes = { + gray = "DefaultGray", + grey = "DefaultGray", + rgb = "DefaultRGB", + cmyk = "DefaultCMYK", +} + +local formatspecification = nil +local formatname = nil + +-- * correspondent document wide flags (write once) needed for permission tests + +-- defaults as mt + +local formats = utilities.storage.allocate { + version = { + external_icc_profiles = 1.4, -- 'p' in name; URL reference of output intent + jbig2_compression = 1.4, + jpeg2000_compression = 1.5, -- not supported yet + nchannel_colorspace = 1.6, -- 'n' in name; n-channel colorspace support + open_prepress_interface = 1.3, -- 'g' in name; reference to external graphics + optional_content = 1.5, + transparency = 1.4, + object_compression = 1.5, + attachments = 1.7, + }, + default = { + pdf_version = 1.7, -- todo: block tex primitive + format_name = "default", + xmp_file = "lpdf-pdx.xml", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + nchannel_colorspace = true, -- unknown + internal_icc_profiles = true, -- controls profile inclusion + external_icc_profiles = true, -- controls profile inclusion + include_intents = true, + open_prepress_interface = true, -- unknown + optional_content = true, -- todo: block at lua level + transparency = true, -- todo: block at lua level + jbig2_compression = true, -- todo: block at lua level (dropped anyway) + jpeg2000_compression = true, -- todo: block at lua level (dropped anyway) + include_cidsets = true, + include_charsets = true, + attachments = true, + inject_metadata = function() + -- nothing + end + }, + data = { + ["pdf/x-1a:2001"] = { + pdf_version = 1.3, + format_name = "PDF/X-1a:2001", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + spot_colors = true, + internal_icc_profiles = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + addtoinfo("GTS_PDFXVersion","PDF/X-1a:2001") + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'><pdfxid:GTS_PDFXVersion>PDF/X-1a:2001</pdfxid:GTS_PDFXVersion></rdf:Description>",false) + end + }, + ["pdf/x-1a:2003"] = { + pdf_version = 1.4, + format_name = "PDF/X-1a:2003", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + spot_colors = true, + internal_icc_profiles = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + addtoinfo("GTS_PDFXVersion","PDF/X-1a:2003") + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'><pdfxid:GTS_PDFXVersion>PDF/X-1a:2003</pdfxid:GTS_PDFXVersion></rdf:Description>",false) + end + }, + ["pdf/x-3:2002"] = { + pdf_version = 1.3, + format_name = "PDF/X-3:2002", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + include_intents = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + addtoinfo("GTS_PDFXVersion","PDF/X-3:2002") + end + }, + ["pdf/x-3:2003"] = { + pdf_version = 1.4, + format_name = "PDF/X-3:2003", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + include_intents = true, + jbig2_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + addtoinfo("GTS_PDFXVersion","PDF/X-3:2003") + end + }, + ["pdf/x-4"] = { + pdf_version = 1.6, + format_name = "PDF/X-4", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + include_intents = true, + optional_content = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'><pdfxid:GTS_PDFXVersion>PDF/X-4</pdfxid:GTS_PDFXVersion></rdf:Description>",false) + insertxmpinfo("xml://rdf:Description/xmpMM:InstanceID","<xmpMM:VersionID>1</xmpMM:VersionID>",false) + insertxmpinfo("xml://rdf:Description/xmpMM:InstanceID","<xmpMM:RenditionClass>default</xmpMM:RenditionClass>",false) + end + }, + ["pdf/x-4p"] = { + pdf_version = 1.6, + format_name = "PDF/X-4p", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + external_icc_profiles = true, + include_intents = true, + optional_content = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'><pdfxid:GTS_PDFXVersion>PDF/X-4p</pdfxid:GTS_PDFXVersion></rdf:Description>",false) + insertxmpinfo("xml://rdf:Description/xmpMM:InstanceID","<xmpMM:VersionID>1</xmpMM:VersionID>",false) + insertxmpinfo("xml://rdf:Description/xmpMM:InstanceID","<xmpMM:RenditionClass>default</xmpMM:RenditionClass>",false) + end + }, + ["pdf/x-5g"] = { + pdf_version = 1.6, + format_name = "PDF/X-5g", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + include_intents = true, + open_prepress_interface = true, + optional_content = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + -- todo + end + }, + ["pdf/x-5pg"] = { + pdf_version = 1.6, + format_name = "PDF/X-5pg", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + external_icc_profiles = true, + include_intents = true, + open_prepress_interface = true, + optional_content = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + -- todo + end + }, + ["pdf/x-5n"] = { + pdf_version = 1.6, + format_name = "PDF/X-5n", + xmp_file = "lpdf-pdx.xml", + gts_flag = "GTS_PDFX", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + calibrated_rgb_colors = true, + spot_colors = true, + cielab_colors = true, + internal_icc_profiles = true, + include_intents = true, + optional_content = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + nchannel_colorspace = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + -- todo + end + }, + ["pdf/a-1a:2005"] = { + pdf_version = 1.4, + format_name = "pdf/a-1a:2005", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, -- new: forms are allowed (with limitations); no JS, other restrictions are unknown (TODO) + tagging = true, -- new: the only difference to PDF/A-1b + internal_icc_profiles = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>1</pdfaid:part><pdfaid:conformance>A</pdfaid:conformance></rdf:Description>",false) + end + }, + ["pdf/a-1b:2005"] = { + pdf_version = 1.4, + format_name = "pdf/a-1b:2005", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + internal_icc_profiles = true, + include_cidsets = true, + include_charsets = true, + attachments = false, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>1</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance></rdf:Description>",false) + end + }, + -- Only PDF/A Attachments are allowed but we don't check the attachments + -- for any quality: they are just blobs. + ["pdf/a-2a"] = { + pdf_version = 1.7, + format_name = "pdf/a-2a", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = true, + internal_icc_profiles = true, + transparency = true, -- new + jbig2_compression = true, + jpeg2000_compression = true, -- new + object_compression = true, -- new + include_cidsets = false, + include_charsets = false, + attachments = true, -- new + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>2</pdfaid:part><pdfaid:conformance>A</pdfaid:conformance></rdf:Description>",false) + end + }, + ["pdf/a-2b"] = { + pdf_version = 1.7, + format_name = "pdf/a-2b", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = false, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = false, + include_charsets = false, + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>2</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance></rdf:Description>",false) + end + }, + -- This is like the b variant, but it requires Unicode mapping of fonts + -- which we do anyway. + ["pdf/a-2u"] = { + pdf_version = 1.7, + format_name = "pdf/a-2u", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = false, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = false, + include_charsets = false, + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>2</pdfaid:part><pdfaid:conformance>U</pdfaid:conformance></rdf:Description>",false) + end + }, + -- Any type of attachment is allowed but we don't check the quality + -- of them. + ["pdf/a-3a"] = { + pdf_version = 1.7, + format_name = "pdf/a-3a", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = true, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = false, + include_charsets = false, + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>3</pdfaid:part><pdfaid:conformance>A</pdfaid:conformance></rdf:Description>",false) + end + }, + ["pdf/a-3b"] = { + pdf_version = 1.7, + format_name = "pdf/a-3b", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = false, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = false, + include_charsets = false, + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>3</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance></rdf:Description>",false) + end + }, + ["pdf/a-3u"] = { + pdf_version = 1.7, + format_name = "pdf/a-3u", + xmp_file = "lpdf-pda.xml", + gts_flag = "GTS_PDFA1", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = false, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = false, + include_charsets = false, + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>3</pdfaid:part><pdfaid:conformance>U</pdfaid:conformance></rdf:Description>",false) + end + }, + ["pdf/ua-1"] = { -- based on PDF/A-3a, but no 'gts_flag' + pdf_version = 1.7, + format_name = "pdf/ua-1", + xmp_file = "lpdf-pua.xml", + gray_scale = true, + cmyk_colors = true, + rgb_colors = true, + spot_colors = true, + calibrated_rgb_colors = true, -- unknown + cielab_colors = true, -- unknown + include_intents = true, + forms = true, + tagging = true, + internal_icc_profiles = true, + transparency = true, + jbig2_compression = true, + jpeg2000_compression = true, + object_compression = true, + include_cidsets = true, + include_charsets = true, --- really ? + attachments = true, + inject_metadata = function() + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'><pdfaid:part>3</pdfaid:part><pdfaid:conformance>A</pdfaid:conformance></rdf:Description>",false) + injectxmpinfo("xml://rdf:RDF","<rdf:Description rdf:about='' xmlns:pdfuaid='http://www.aiim.org/pdfua/ns/id/'><pdfuaid:part>1</pdfuaid:part></rdf:Description>",false) + end + }, + } +} + +lpdf.formats = formats -- it does not hurt to have this one visible + +local filenames = { + "colorprofiles.xml", + "colorprofiles.lua", +} + +local function locatefile(filename) + local fullname = resolvers.findfile(filename,"icc",1,true) + if not fullname or fullname == "" then + fullname = resolvers.finders.byscheme("loc",filename) -- could be specific to the project + end + return fullname or "" +end + +local function loadprofile(name,filename) + local profile = false + local databases = filename and filename ~= "" and settings_to_array(filename) or filenames + for i=1,#databases do + local filename = locatefile(databases[i]) + if filename and filename ~= "" then + local suffix = file.suffix(filename) + local lname = lower(name) + if suffix == "xml" then + local xmldata = xml.load(filename) -- no need for caching it + if xmldata then + profile = xml.filter(xmldata,format('xml://profiles/profile/(info|filename)[lower(text())=="%s"]/../table()',lname)) + end + elseif suffix == "lua" then + local luadata = loadfile(filename) + luadata = ludata and luadata() + if luadata then + profile = luadata[name] or luadata[lname] -- hashed + if not profile then + for i=1,#luadata do + local li = luadata[i] + if lower(li.info) == lname then -- indexed + profile = li + break + end + end + end + end + end + if profile then + if next(profile) then + report_backend("profile specification %a loaded from %a",name,filename) + return profile + elseif trace_format then + report_backend("profile specification %a loaded from %a but empty",name,filename) + end + return false + end + end + end + report_backend("profile specification %a not found in %a",name,concat(filenames, ", ")) +end + +local function urls(url) + if not url or url == "" then + return nil + else + local u = pdfarray() + for url in gmatch(url,"([^, ]+)") do + if find(url,"^http") then + u[#u+1] = pdfdictionary { + FS = pdfconstant("URL"), + F = pdfstring(url), + } + end + end + return u + end +end + +local function profilename(filename) + return lower(file.basename(filename)) +end + +local internalprofiles = { } + +local function handleinternalprofile(s,include) + local filename, colorspace = s.filename or "", s.colorspace or "" + if filename == "" or colorspace == "" then + report_backend("error in internal profile specification: %s",serialize(s,false)) + else + local tag = profilename(filename) + local profile = internalprofiles[tag] + if not profile then + local colorspace = lower(colorspace) + if include then + -- local fullname = resolvers.findctxfile(filename) or "" + local fullname = locatefile(filename) + local channel = channels[colorspace] or nil + if fullname == "" then + report_backend("error, couldn't locate profile %a",filename) + elseif not channel then + report_backend("error, couldn't resolve channel entry for colorspace %a",colorspace) + else + profile = pdfflushstreamfileobject(fullname,pdfdictionary{ N = channel },false) -- uncompressed + internalprofiles[tag] = profile + if trace_format then + report_backend("including %a color profile from %a",colorspace,fullname) + end + end + else + internalprofiles[tag] = true + if trace_format then + report_backend("not including %a color profile %a",colorspace,filename) + end + end + end + return profile + end +end + +local externalprofiles = { } + +local function handleexternalprofile(s,include) -- specification (include ignored here) + local name, url, filename, checksum, version, colorspace = + s.info or s.filename or "", s.url or "", s.filename or "", s.checksum or "", s.version or "", s.colorspace or "" + if false then -- somehow leads to invalid pdf + local iccprofile = colors.iccprofile(filename) + if iccprofile then + name = name ~= "" and name or iccprofile.tags.desc.cleaned or "" + url = url ~= "" and url or iccprofile.tags.dmnd.cleaned or "" + checksum = checksum ~= "" and checksum or file.checksum(iccprofile.fullname) or "" + version = version ~= "" and version or iccprofile.header.version or "" + colorspace = colorspace ~= "" and colorspace or iccprofile.header.colorspace or "" + end + -- table.print(iccprofile) + end + if name == "" or url == "" or checksum == "" or version == "" or colorspace == "" or filename == "" then + local profile = handleinternalprofile(s) + if profile then + report_backend("incomplete external profile specification, falling back to internal") + else + report_backend("error in external profile specification: %s",serialize(s,false)) + end + else + local tag = profilename(filename) + local profile = externalprofiles[tag] + if not profile then + local d = pdfdictionary { + ProfileName = name, -- not file name! + ProfileCS = colorspace, + URLs = urls(url), -- array containing at least one URL + CheckSum = pdfverbose { "<", lower(checksum), ">" }, -- 16byte MD5 hash + ICCVersion = pdfverbose { "<", version, ">" }, -- bytes 8..11 from the header of the ICC profile, as a hex string + } + profile = pdfflushobject(d) + externalprofiles[tag] = profile + end + return profile + end +end + +local loadeddefaults = { } + +local function handledefaultprofile(s,spec) -- specification + local filename, colorspace = s.filename or "", lower(s.colorspace or "") + if filename == "" or colorspace == "" then + report_backend("error in default profile specification: %s",serialize(s,false)) + elseif not loadeddefaults[colorspace] then + local tag = profilename(filename) + local n = internalprofiles[tag] -- or externalprofiles[tag] + if n == true then -- not internalized + report_backend("no default profile %a for colorspace %a",filename,colorspace) + elseif n then + local a = pdfarray { + pdfconstant("ICCBased"), + pdfreference(n), + } + -- used in page /Resources, so this must be inserted at runtime + lpdf.adddocumentcolorspace(prefixes[colorspace],pdfreference(pdfflushobject(a))) + loadeddefaults[colorspace] = true + report_backend("setting %a as default %a color space",filename,colorspace) + else + report_backend("no default profile %a for colorspace %a",filename,colorspace) + end + elseif trace_format then + report_backend("a default %a colorspace is already in use",colorspace) + end +end + +local loadedintents = { } +local intents = pdfarray() + +local function handleoutputintent(s,spec) + local url = s.url or "" + local filename = s.filename or "" + local name = s.info or filename + local id = s.id or "" + local outputcondition = s.outputcondition or "" + local info = s.info or "" + if name == "" or id == "" then + report_backend("error in output intent specification: %s",serialize(s,false)) + elseif not loadedintents[name] then + local tag = profilename(filename) + local internal, external = internalprofiles[tag], externalprofiles[tag] + if internal or external then + local d = { + Type = pdfconstant("OutputIntent"), + S = pdfconstant(spec.gts_flag or "GTS_PDFX"), + OutputConditionIdentifier = id, + RegistryName = url, + OutputCondition = outputcondition, + Info = info, + } + if internal and internal ~= true then + d.DestOutputProfile = pdfreference(internal) + elseif external and external ~= true then + d.DestOutputProfileRef = pdfreference(external) + else + report_backend("omitting reference to profile for intent %a",name) + end + intents[#intents+1] = pdfreference(pdfflushobject(pdfdictionary(d))) + if trace_format then + report_backend("setting output intent to %a with id %a for entry %a",name,id,#intents) + end + else + report_backend("invalid output intent %a",name) + end + loadedintents[name] = true + elseif trace_format then + report_backend("an output intent with name %a is already in use",name) + end +end + +local function handleiccprofile(message,spec,name,filename,how,options,alwaysinclude,gts_flag) + if name and name ~= "" then + local list = settings_to_array(name) + for i=1,#list do + local name = list[i] + local profile = loadprofile(name,filename) + if trace_format then + report_backend("handling %s %a",message,name) + end + if profile then + if formatspecification.cmyk_colors then + profile.colorspace = profile.colorspace or "CMYK" + else + profile.colorspace = profile.colorspace or "RGB" + end + local external = formatspecification.external_icc_profiles + local internal = formatspecification.internal_icc_profiles + local include = formatspecification.include_intents + local always, never = options[variables.always], options[variables.never] + if always or alwaysinclude then + if trace_format then + report_backend("forcing internal profiles") -- can make preflight unhappy + end + -- internal, external = true, false + internal, external = not never, false + elseif never then + if trace_format then + report_backend("forcing external profiles") -- can make preflight unhappy + end + internal, external = false, true + end + if external then + if trace_format then + report_backend("handling external profiles cf. %a",name) + end + handleexternalprofile(profile,false) + else + if trace_format then + report_backend("handling internal profiles cf. %a",name) + end + if internal then + handleinternalprofile(profile,always or include) + else + report_backend("no profile inclusion for %a",formatname) + end + end + how(profile,spec) + elseif trace_format then + report_backend("unknown profile %a",name) + end + end + end +end + +local function flushoutputintents() + if #intents > 0 then + lpdf.addtocatalog("OutputIntents",pdfreference(pdfflushobject(intents))) + end +end + +lpdf.registerdocumentfinalizer(flushoutputintents,2,"output intents") + +function codeinjections.setformat(s) + local format = s.format or "" + local level = tonumber(s.level) + local intent = s.intent or "" + local profile = s.profile or "" + local option = s.option or "" + local filename = s.file or "" + if format ~= "" then + local spec = formats.data[lower(format)] + if spec then + formatspecification = spec + formatname = spec.format_name + report_backend("setting format to %a",formatname) + local xmp_file = formatspecification.xmp_file or "" + if xmp_file == "" then + -- weird error + else + codeinjections.setxmpfile(xmp_file) + end + if not level then + level = 3 -- good compromise, default anyway + end + local pdf_version = spec.pdf_version * 10 + local inject_metadata = spec.inject_metadata + local majorversion = math.floor(math.div(pdf_version,10)) + local minorversion = math.floor(math.mod(pdf_version,10)) + local objectcompression = spec.object_compression and pdf_version >= 15 + local compresslevel = level or lpdf.compresslevel() -- keep default + local objectcompresslevel = (objectcompression and (level or lpdf.objectcompresslevel())) or 0 + lpdf.setcompression(compresslevel,objectcompresslevel) + lpdf.setversion(majorversion,minorversion) + if objectcompression then + report_backend("forcing pdf version %s.%s, compression level %s, object compression level %s", + majorversion,minorversion,compresslevel,objectcompresslevel) + elseif compresslevel > 0 then + report_backend("forcing pdf version %s.%s, compression level %s, object compression disabled", + majorversion,minorversion,compresslevel) + else + report_backend("forcing pdf version %s.%s, compression disabled", + majorversion,minorversion) + end + -- + -- cid sets can always omitted now, but those validators still complain so let's + -- for a while keep it (for luigi): + -- + lpdf.setomitcidset (formatspecification.include_cidsets == false and 1 or 0) -- why a number + lpdf.setomitcharset(formatspecification.include_charsets == false and 1 or 0) -- why a number + -- + -- maybe block by pdf version + -- + codeinjections.settaggingsupport(formatspecification.tagging) + codeinjections.setattachmentsupport(formatspecification.attachments) + -- + -- context.setupcolors { -- not this way + -- cmyk = spec.cmyk_colors and variables.yes or variables.no, + -- rgb = spec.rgb_colors and variables.yes or variables.no, + -- } + -- + colors.forcesupport( + spec.gray_scale or false, + spec.rgb_colors or false, + spec.cmyk_colors or false, + spec.spot_colors or false, + spec.nchannel_colorspace or false + ) + transparencies.forcesupport( + spec.transparency or false + ) + viewerlayers.forcesupport( + spec.optional_content or false + ) + viewerlayers.setfeatures( + spec.has_order or false -- new + ) + -- + -- spec.jbig2_compression : todo, block in image inclusion + -- spec.jpeg2000_compression : todo, block in image inclusion + -- + if type(inject_metadata) == "function" then + inject_metadata() + end + local options = settings_to_hash(option) + handleiccprofile("color profile",spec,profile,filename,handledefaultprofile,options,true) + handleiccprofile("output intent",spec,intent,filename,handleoutputintent,options,false) + if trace_variables then + for k, v in sortedhash(formats.default) do + local v = formatspecification[k] + if type(v) ~= "function" then + report_backend("%a = %a",k,v or false) + end + end + end + function codeinjections.setformat(noname) + if trace_format then + report_backend("error, format is already set to %a, ignoring %a",formatname,noname.format) + end + end + else + report_backend("error, format %a is not supported",format) + end + elseif level then + lpdf.setcompression(level,level) + else + -- we ignore this as we hook it in \everysetupbackend + end +end + +directives.register("backend.format", function(v) -- table ! + local tv = type(v) + if tv == "table" then + codeinjections.setformat(v) + elseif tv == "string" then + codeinjections.setformat { format = v } + end +end) + +interfaces.implement { + name = "setformat", + actions = codeinjections.setformat, + arguments = { { "*" } } +} + +function codeinjections.getformatoption(key) + return formatspecification and formatspecification[key] +end + +-- function codeinjections.getformatspecification() +-- return formatspecification +-- end + +function codeinjections.supportedformats() + local t = { } + for k, v in sortedhash(formats.data) do + t[#t+1] = k + end + return t +end + +-- The following is somewhat cleaner but then we need to flag that there are +-- color spaces set so that the page flusher does not optimize the (at that +-- moment) still empty array away. So, next(d_colorspaces) should then become +-- a different test, i.e. also on flag. I'll add that when we need more forward +-- referencing. +-- +-- local function embedprofile = handledefaultprofile +-- +-- local function flushembeddedprofiles() +-- for colorspace, filename in next, defaults do +-- embedprofile(colorspace,filename) +-- end +-- end +-- +-- local function handledefaultprofile(s) +-- defaults[lower(s.colorspace)] = s.filename +-- end +-- +-- lpdf.registerdocumentfinalizer(flushembeddedprofiles,1,"embedded color profiles") diff --git a/tex/context/base/mkxl/lpdf-fnt.lmt b/tex/context/base/mkxl/lpdf-fnt.lmt new file mode 100644 index 000000000..ee16303b0 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-fnt.lmt @@ -0,0 +1,194 @@ +if not modules then modules = { } end modules ['lpdf-fnt'] = { + version = 1.001, + 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 is experimental code. + +local match, gmatch = string.match, string.gmatch +local tonumber, rawget = tonumber, rawget + +local pdfe = lpdf.epdf +local pdfreference = lpdf.reference + +local pdfreserveobject + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject +end) + +local tobemerged = { } +local trace_merge = false trackers.register("graphics.fonts",function(v) trace_merge = v end) +local report_merge = logs.reporter("graphics","fonts") + +local function register(usedname,cleanname) + local cleanname = cleanname or fonts.names.cleanname(usedname) + local fontid = fonts.definers.internal { name = cleanname } + if fontid then + local objref = pdfreserveobject() + if trace_merge then + report_merge("registering %a with name %a, id %a and object %a",usedname,cleanname,fontid,objref) + end + return { + id = fontid, + reference = objref, + indices = { }, + cleanname = cleanname, + } + end + return false +end + +function lpdf.registerfont(usedname,cleanname) + local v = register(usedname,cleanname) + tobemerged[usedname] = v + return v +end + +table.setmetatableindex(tobemerged,function(t,k) + return lpdf.registerfont(k) +end) + +local function finalizefont(v) + local indextoslot = fonts.helpers.indextoslot + if v then + local id = v.id + local n = 0 + for i in next, v.indices do + local u = indextoslot(id,i) + n = n + 1 + end + v.n = n + end +end + +statistics.register("merged fonts", function() + if next(tobemerged) then + local t = { } + for k, v in table.sortedhash(tobemerged) do + t[#t+1] = string.formatters["%s (+%i)"](k,v.n) + end + return table.concat(t," ") + end +end) + +function lpdf.finalizefonts() + for k, v in next, tobemerged do + finalizefont(v) + end +end + +callback.register("font_descriptor_objnum_provider",function(name) + local m = rawget(tobemerged,name) + if m then + -- finalizefont(m) + local r = m.reference or 0 + if trace_merge then + report_merge("using object %a for font descriptor of %a",r,name) + end + return r + end + return 0 +end) + +local function getunicodes1(str,indices) + for s in gmatch(str,"beginbfrange%s*(.-)%s*endbfrange") do + for first, last, offset in gmatch(s,"<([^>]+)>%s+<([^>]+)>%s+<([^>]+)>") do + for i=tonumber(first,16),tonumber(last,16) do + indices[i] = true + end + end + end + for s in gmatch(str,"beginbfchar%s*(.-)%s*endbfchar") do + for old, new in gmatch(s,"<([^>]+)>%s+<([^>]+)>") do + indices[tonumber(old,16)] = true + end + end +end + +local function getunicodes2(widths,indices) + for i=1,#widths,2 do + local start = widths[i] + local count = #widths[i+1] + if start and count then + for i=start,start+count-1 do + indices[i] = true + end + end + end +end + +local function checkedfonts(pdfdoc,xref,copied,page) + local list = page.Resources.Font + local done = { } + for k, somefont in pdfe.expanded(list) do + if somefont.Subtype == "Type0" and somefont.Encoding == "Identity-H" then + local descendants = somefont.DescendantFonts + if descendants then + for i=1,#descendants do + local d = descendants[i] + if d then + local subtype = d.Subtype + if subtype == "CIDFontType0" or subtype == "CIDFontType2" then + local basefont = somefont.BaseFont + if basefont then + local fontname = match(basefont,"^[A-Z]+%+(.+)$") + local fontdata = tobemerged[fontname] + if fontdata then + local descriptor = d.FontDescriptor + if descriptor then + local okay = false + local widths = d.W + if widths then + getunicodes2(widths,fontdata.indices) + okay = true + else + local tounicode = somefont.ToUnicode + if tounicode then + getunicodes1(tounicode(),fontdata.indices) + okay = true + end + end + if okay then + local r = xref[descriptor] + done[r] = fontdata.reference + end + end + end + end + end + end + end + end + end + end + return next(done) and done +end + +function lpdf.epdf.plugin(pdfdoc,xref,copied,page) -- needs checking + local done = checkedfonts(pdfdoc,xref,copied,page) + if done then + return { + FontDescriptor = function(xref,copied,object,key,value,copyobject) + local r = value[3] + local d = done[r] + if d then + return pdfreference(d) + else + return copyobject(xref,copied,object,key,value) + end + end + } + end +end + +lpdf.registerdocumentfinalizer(lpdf.finalizefonts) + +-- already defined in font-ocl but the context variatn will go here +-- +-- function lpdf.vfimage(wd,ht,dp,data,name) +-- return { "image", { filename = name, width = wd, height = ht, depth = dp } } +-- end diff --git a/tex/context/base/mkxl/lpdf-grp.lmt b/tex/context/base/mkxl/lpdf-grp.lmt new file mode 100644 index 000000000..3b45123e3 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-grp.lmt @@ -0,0 +1,300 @@ +if not modules then modules = { } end modules ['lpdf-grp'] = { + version = 1.001, + 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" +} + +local type, tonumber = type, tonumber +local formatters, gsub = string.formatters, string.gsub +local concat = table.concat +local round = math.round + +local backends, lpdf = backends, lpdf + +local nodeinjections = backends.pdf.nodeinjections + +local colors = attributes.colors +local basepoints = number.dimenfactors.bp + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations + +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfconstant = lpdf.constant +local pdfboolean = lpdf.boolean +local pdfreference = lpdf.reference + +local pdfflushobject + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject +end) + +local createimage = images.create +local wrapimage = images.wrap +local embedimage = images.embed + +-- can also be done indirectly: +-- +-- 12 : << /AntiAlias false /ColorSpace 8 0 R /Coords [ 0.0 0.0 1.0 0.0 ] /Domain [ 0.0 1.0 ] /Extend [ true true ] /Function 22 0 R /ShadingType 2 >> +-- 22 : << /Bounds [ ] /Domain [ 0.0 1.0 ] /Encode [ 0.0 1.0 ] /FunctionType 3 /Functions [ 31 0 R ] >> +-- 31 : << /C0 [ 1.0 0.0 ] /C1 [ 0.0 1.0 ] /Domain [ 0.0 1.0 ] /FunctionType 2 /N 1.0 >> + +local function shade(stype,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions) + local func = nil + -- + -- domain has to be consistently added in all dictionaries here otherwise + -- acrobat fails with a drawing error + -- + domain = pdfarray(domain) + n = tonumber(n) + -- + if steps then + local list = pdfarray() + local bounds = pdfarray() + local encode = pdfarray() + for i=1,steps do + if i < steps then + bounds[i] = fractions[i] or 1 + end + encode[2*i-1] = 0 + encode[2*i] = 1 + list [i] = pdfdictionary { + FunctionType = 2, + Domain = domain, + C0 = pdfarray(color_a[i]), + C1 = pdfarray(color_b[i]), + N = n, + } + end + func = pdfdictionary { + FunctionType = 3, + Bounds = bounds, + Encode = encode, + Functions = list, + Domain = domain, + } + else + func = pdfdictionary { + FunctionType = 2, + Domain = domain, + C0 = pdfarray(color_a), + C1 = pdfarray(color_b), + N = n, + } + end + separation = separation and registrations.getspotcolorreference(separation) + local s = pdfdictionary { + ShadingType = stype, + ColorSpace = separation and pdfreference(separation) or pdfconstant(colorspace), + Domain = domain, + Function = pdfreference(pdfflushobject(func)), + Coords = pdfarray(coordinates), + Extend = pdfarray { true, true }, + AntiAlias = pdfboolean(true), + } + lpdf.adddocumentshade(name,pdfreference(pdfflushobject(s))) +end + +function lpdf.circularshade(name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions) + shade(3,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions) +end + +function lpdf.linearshade(name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions) + shade(2,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions) +end + +-- inline bitmaps but xform'd +-- +-- we could derive the colorspace if we strip the data +-- and divide by x*y + +local template = "q BI %s ID %s > EI Q" +local factor = 72/300 + +function nodeinjections.injectbitmap(t) + -- encoding is ascii hex, no checking here + local xresolution, yresolution = t.xresolution or 0, t.yresolution or 0 + if xresolution == 0 or yresolution == 0 then + return -- fatal error + end + local colorspace = t.colorspace + if colorspace ~= "rgb" and colorspace ~= "cmyk" and colorspace ~= "gray" then + -- not that efficient but ok + local d = gsub(t.data,"[^0-9a-f]","") + local b = math.round(#d / (xresolution * yresolution)) + if b == 2 then + colorspace = "gray" + elseif b == 6 then + colorspace = "rgb" + elseif b == 8 then + colorspace = "cmyk" + end + end + colorspace = lpdf.colorspaceconstants[colorspace] + if not colorspace then + return -- fatal error + end + local d = pdfdictionary { + W = xresolution, + H = yresolution, + CS = colorspace, + BPC = 8, + F = pdfconstant("AHx"), + -- CS = nil, + -- BPC = 1, + -- IM = true, + } + -- for some reasons it only works well if we take a 1bp boundingbox + local urx, ury = 1/basepoints, 1/basepoints + -- urx = (xresolution/300)/basepoints + -- ury = (yresolution/300)/basepoints + local width, height = t.width or 0, t.height or 0 + if width == 0 and height == 0 then + width = factor * xresolution / basepoints + height = factor * yresolution / basepoints + elseif width == 0 then + width = height * xresolution / yresolution + elseif height == 0 then + height = width * yresolution / xresolution + end + local a = pdfdictionary { + BBox = pdfarray { 0, 0, urx * basepoints, ury * basepoints } + } + local image = createimage { + stream = formatters[template](d(),t.data), + width = width, + height = height, + bbox = { 0, 0, urx, ury }, + attr = a(), + nobbox = true, + } + return wrapimage(image) +end + +-- general graphic helpers + +function codeinjections.setfigurealternative(data,figure) + local request = data.request + local display = request.display + if display and display ~= "" then + local nested = figures.push { + name = display, + page = request.page, + size = request.size, + prefix = request.prefix, + cache = request.cache, + width = request.width, + height = request.height, + } + figures.identify() + local displayfigure = figures.check() + if displayfigure then + -- figure.aform = true + embedimage(figure) + local a = pdfarray { + pdfdictionary { + Image = pdfreference(figure.objnum), + DefaultForPrinting = true, + } + } + local d = pdfdictionary { + Alternates = pdfreference(pdfflushobject(a)), + } + displayfigure.attr = d() + figures.pop() + return displayfigure, nested + else + figures.pop() + end + end +end + +function codeinjections.getpreviewfigure(request) + local figure = figures.initialize(request) + if not figure then + return + end + figure = figures.identify(figure) + if not (figure and figure.status and figure.status.fullname) then + return + end + figure = figures.check(figure) + if not (figure and figure.status and figure.status.fullname) then + return + end + local image = figure.status.private + if image then + embedimage(image) + end + return figure +end + +function codeinjections.setfiguremask(data,figure) -- mark + local request = data.request + local mask = request.mask + if mask and mask ~= "" then + figures.push { + name = mask, + page = request.page, + size = request.size, + prefix = request.prefix, + cache = request.cache, + width = request.width, + height = request.height, + } + mask = figures.identify() + mask = figures.check(mask) + if mask then + local image = mask.status.private + if image then + figures.include(mask) + embedimage(image) + local d = pdfdictionary { + Interpolate = false, + SMask = pdfreference(mask.status.objectnumber), + } + figure.attr = d() + end + end + figures.pop() + end +end + +-- experimental (q Q is not really needed) + +local saveboxresource = tex.boxresources.save +local nofpatterns = 0 +local f_pattern = formatters["q /Pattern cs /%s scn 0 0 %.6N %.6N re f Q"] + +function lpdf.registerpattern(specification) + nofpatterns = nofpatterns + 1 + local d = pdfdictionary { + Type = pdfconstant("Pattern"), + PatternType = 1, + PaintType = 1, + TilingType = 2, + XStep = (specification.width or 10) * basepoints, + YStep = (specification.height or 10) * basepoints, + Matrix = { + 1, 0, 0, 1, + (specification.hoffset or 0) * basepoints, + (specification.voffset or 0) * basepoints, + }, + } + + local resources = lpdf.collectedresources{ patterns = false } + local attributes = d() + local onlybounds = 1 + local patternobj = saveboxresource(specification.number,attributes,resources,true,onlybounds) + lpdf.adddocumentpattern("Pt" .. nofpatterns,lpdf.reference(patternobj )) + return nofpatterns +end + +function lpdf.patternstream(n,width,height) + return f_pattern("Pt" .. n,width*basepoints,height*basepoints) +end diff --git a/tex/context/base/mkxl/lpdf-img.lmt b/tex/context/base/mkxl/lpdf-img.lmt index fc53740f6..83d3dfae6 100644 --- a/tex/context/base/mkxl/lpdf-img.lmt +++ b/tex/context/base/mkxl/lpdf-img.lmt @@ -43,12 +43,18 @@ local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfstring = lpdf.string -local pdfflushstreamobject = lpdf.flushstreamobject local pdfreference = lpdf.reference local pdfverbose = lpdf.verbose -local pdfmajorversion = lpdf.majorversion -local pdfminorversion = lpdf.minorversion +local pdfflushstreamobject +local pdfmajorversion +local pdfminorversion + +updaters.register("backend.update.lpdf",function() + pdfflushstreamobject = lpdf.flushstreamobject + pdfmajorversion = lpdf.majorversion + pdfminorversion = lpdf.minorversion +end) local createimage = images.create diff --git a/tex/context/base/mkxl/lpdf-ini.lmt b/tex/context/base/mkxl/lpdf-ini.lmt new file mode 100644 index 000000000..f7f45f5a3 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-ini.lmt @@ -0,0 +1,1366 @@ +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" +} + +-- beware of "too many locals" here + +local setmetatable, getmetatable, type, next, tostring, tonumber, rawset = setmetatable, getmetatable, type, next, tostring, tonumber, rawset +local char, byte, format, gsub, concat, match, sub, gmatch = string.char, string.byte, string.format, string.gsub, table.concat, string.match, string.sub, string.gmatch +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 rshift = bit32.rshift +local osdate, ostime = os.date, os.time + +local report_objects = logs.reporter("backend","objects") +local report_finalizing = logs.reporter("backend","finalizing") +local report_blocked = logs.reporter("backend","blocked") + +local implement = 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 ()\ too. + +pdf = type(pdf) == "table" and pdf or { } +local factor = number.dimenfactors.bp + +local codeinjections = { } +local nodeinjections = { } + +local backends = backends + +local pdfbackend = { + comment = "backend for directly generating pdf output", + nodeinjections = nodeinjections, + codeinjections = codeinjections, + registrations = { }, + tables = { }, +} + +backends.pdf = pdfbackend + +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) + +local pdfreserveobject +local pdfimmediateobject + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + pdfimmediateobject = lpdf.immediateobject +end) + +do + + updaters.register("backend.update.lpdf",function() + job.positions.registerhandlers { + getpos = drivers.getpos, + getrpos = drivers.getrpos, + gethpos = drivers.gethpos, + getvpos = drivers.getvpos, + } + lpdf.getpos = drivers.getpos + end) + + local pdfgetmatrix, pdfhasmatrix, pdfprint, pdfgetpos + + updaters.register("backend.update.lpdf",function() + pdfgetmatrix = lpdf.getmatrix + pdfhasmatrix = lpdf.hasmatrix + pdfprint = lpdf.print + pdfgetpos = lpdf.getpos + 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 + +-- we could use a hash of predefined unicodes + +-- local function tosixteen(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 +-- local r, n = { "<feff" }, 1 +-- for b in utfvalues(str) do +-- n = n + 1 +-- if b < 0x10000 then +-- r[n] = format("%04x",b) +-- else +-- r[n] = format("%04x%04x",rshift(b,10),b%1024+0xDC00) +-- end +-- end +-- n = n + 1 +-- r[n] = ">" +-- return concat(r) +-- end +-- end + +local tosixteen, fromsixteen, topdfdoc, frompdfdoc, toeight, fromeight + +do + + local escaped = Cs(Cc("(") * (S("\\()\n\r\t\b\f")/"\\%0" + P(1))^0 * Cc(")")) + + 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 = format("%04x%04x",rshift(v,10),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 + + local more = 0 + + local pattern = C(4) / function(s) -- needs checking ! + local now = tonumber(s,16) + if more > 0 then + now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000 -- the 0x10000 smells wrong + 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.toregime + local fromregime = regimes.fromregime + + 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 + + local b_pattern = Cs((P("\\")/"" * ( + S("()") + + S("nrtbf") / { n = "\n", r = "\r", t = "\t", b = "\b", f = "\f" } + + lpegpatterns.octdigit^-3 / function(s) return char(tonumber(s,8)) end) + + P(1))^0) + + fromeight = function(str) + if not str or str == "" then + return "" + else + return lpegmatch(unescape,str) + end + end + + 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 = (tonumber(a,16) - 0xD800) * 1024 + b = (tonumber(b,16) - 0xDC00) + return utfchar(a+b) + end + + one / function(a) + return utfchar(tonumber(a,16)) + 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(b_pattern,s) + end + + lpdf.tosixteen = tosixteen + lpdf.toeight = toeight + lpdf.topdfdoc = topdfdoc + lpdf.fromsixteen = fromsixteen + lpdf.fromeight = fromeight + lpdf.frompdfdoc = frompdfdoc + +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 + 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 dirctionary") + -- 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(key,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(key,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" }, __lpdftype = "stream", __tostring = tostring_x, __call = value_x, __newindex = add_x } +-- local mt_d = { __index = { __lpdftype__ = "dictionary" }, __lpdftype = "dictionary", __tostring = tostring_d, __call = value_d, __add = add_to_d } +-- local mt_a = { __index = { __lpdftype__ = "array" }, __lpdftype = "array", __tostring = tostring_a, __call = value_a, __add = add_to_a } +-- local mt_u = { __index = { __lpdftype__ = "unicode" }, __lpdftype = "unicode", __tostring = tostring_u, __call = value_u } +-- local mt_s = { __index = { __lpdftype__ = "string" }, __lpdftype = "string", __tostring = tostring_s, __call = value_s } +-- local mt_p = { __index = { __lpdftype__ = "docstring" }, __lpdftype = "docstring", __tostring = tostring_p, __call = value_p } +-- local mt_n = { __index = { __lpdftype__ = "number" }, __lpdftype = "number", __tostring = tostring_n, __call = value_n } +-- local mt_c = { __index = { __lpdftype__ = "constant" }, __lpdftype = "constant", __tostring = tostring_c, __call = value_c } +-- local mt_z = { __index = { __lpdftype__ = "null" }, __lpdftype = "null", __tostring = tostring_z, __call = value_z } +-- local mt_t = { __index = { __lpdftype__ = "true" }, __lpdftype = "true", __tostring = tostring_t, __call = value_t } +-- local mt_f = { __index = { __lpdftype__ = "false" }, __lpdftype = "false", __tostring = tostring_f, __call = value_f } +-- local mt_r = { __index = { __lpdftype__ = "reference" }, __lpdftype = "reference", __tostring = tostring_r, __call = value_r } +-- local mt_v = { __index = { __lpdftype__ = "verbose" }, __lpdftype = "verbose", __tostring = tostring_v, __call = value_v } +-- local mt_l = { __index = { __lpdftype__ = "literal" }, __lpdftype = "literal", __tostring = tostring_l, __call = value_l } + +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 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 escaped = Cs(Cc("/") * replacer^0) + + local cache = table.setmetatableindex(function(t,k) + local v = setmetatable({ lpegmatch(escaped,k) }, 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 + + local escaped = Cs(replacer^0) + + function lpdf.escaped(str) + return lpegmatch(escaped,str) or 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 + +-- 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() + +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 + +function lpdf.finalizedocument() + if not environment.initex then + run(documentfinalizers,"document") + function lpdf.finalizedocument() + -- report_finalizing("serious error: the document is finalized multiple times") + function lpdf.finalizedocument() end + end + 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 +-- pdfsetcatalog(catalog()) + 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 + + 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("backend.update.lpdf",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 + +-- ! -> universaltime + +do + + -- It's a bit of a historical mess here. + + local metadata = nil + local timestamp = backends.timestamp() + + function lpdf.getmetadata() + if not metadata then + local contextversion = environment.version + local luatexversion = format("%1.2f",LUATEXVERSION) + local luatexfunctionality = tostring(LUATEXFUNCTIONALITY) + metadata = { + producer = format("LuaTeX-%s",luatexversion), + creator = format("LuaTeX %s %s + ConTeXt MkIV %s",luatexversion,luatexfunctionality,contextversion), + luatexversion = luatexversion, + contextversion = contextversion, + luatexfunctionality = luatexfunctionality, + luaversion = tostring(LUAVERSION), + platform = os.platform, + time = timestamp, + } + end + return metadata + end + + function lpdf.settime(n) + if n then + n = converters.totime(n) + if n then + converters.settime(n) + timestamp = backends.timestamp() + end + end + if metadata then + metadata.time = timestamp + end + return timestamp + end + + lpdf.settime(tonumber(resolvers.variable("start_time")) or tonumber(resolvers.variable("SOURCE_DATE_EPOCH"))) -- bah + + function lpdf.pdftimestamp(str) + local t = type(str) + if t == "string" then + local Y, M, D, h, m, s, Zs, Zh, Zm = match(str,"^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)([%+%-])(%d%d):(%d%d)$") + return Y and format("D:%s%s%s%s%s%s%s%s'%s'",Y,M,D,h,m,s,Zs,Zh,Zm) + else + return osdate("D:%Y%m%d%H%M%S",t == "number" and str or ostime()) -- maybe "!D..." : universal time + end + end + + function lpdf.id(date) + local banner = environment.jobname or tex.jobname or "unknown" + if not date then + return banner + else + return format("%s | %s",banner,timestamp) + end + end + +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) + +do + + 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 s_actual_text_e = "EMC ET" + local f_actual_text_b_not = formatters["/Span << /ActualText <feff%s> >> BDC"] + local s_actual_text_e_not = "EMC" + local f_actual_text = formatters["/Span <</ActualText %s >> BDC"] + + 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 + } + +end + +-- interface + +implement { name = "lpdf_collectedresources", actions = { lpdf.collectedresources, context } } +implement { name = "lpdf_addtocatalog", arguments = "2 strings", actions = lpdf.addtocatalog } +implement { name = "lpdf_addtoinfo", arguments = "2 strings", actions = function(a,b,c) lpdf.addtoinfo(a,b,c) end } -- gets adapted +implement { name = "lpdf_addtonames", arguments = "2 strings", actions = lpdf.addtonames } +implement { name = "lpdf_addtopageattributes", arguments = "2 strings", actions = lpdf.addtopageattributes } +implement { name = "lpdf_addtopagesattributes", arguments = "2 strings", actions = lpdf.addtopagesattributes } +implement { name = "lpdf_addtopageresources", arguments = "2 strings", actions = lpdf.addtopageresources } +implement { name = "lpdf_adddocumentextgstate", arguments = "2 strings", actions = function(a,b) lpdf.adddocumentextgstate (a,pdfverbose(b)) end } +implement { name = "lpdf_adddocumentcolorspace", arguments = "2 strings", actions = function(a,b) lpdf.adddocumentcolorspace(a,pdfverbose(b)) end } +implement { name = "lpdf_adddocumentpattern", arguments = "2 strings", actions = function(a,b) lpdf.adddocumentpattern (a,pdfverbose(b)) end } +implement { name = "lpdf_adddocumentshade", arguments = "2 strings", actions = function(a,b) lpdf.adddocumentshade (a,pdfverbose(b)) 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 + + 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 diff --git a/tex/context/base/mkxl/lpdf-lmt.lmt b/tex/context/base/mkxl/lpdf-lmt.lmt index 2bbf5ba61..32dfa574f 100644 --- a/tex/context/base/mkxl/lpdf-lmt.lmt +++ b/tex/context/base/mkxl/lpdf-lmt.lmt @@ -49,38 +49,56 @@ local zlibcompress = (xzip or zlib).compress local nuts = nodes.nuts local tonut = nodes.tonut -local getdata = nuts.getdata -local getsubtype = nuts.getsubtype -local getwhd = nuts.getwhd -local flushlist = nuts.flush_list - -local pdfincludeimage = lpdf.includeimage -local pdfgetfontname = lpdf.getfontname -local pdfgetfontobjnumber = lpdf.getfontobjnumber - -local pdfreserveobject = lpdf.reserveobject -local pdfpagereference = lpdf.pagereference -local pdfflushobject = lpdf.flushobject -local pdfsharedobject = lpdf.shareobjectreference local pdfreference = lpdf.reference local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant -local pdfflushstreamobject = lpdf.flushstreamobject local pdfliteral = lpdf.literal -- not to be confused with a whatsit! -local pdf_pages = pdfconstant("Pages") -local pdf_page = pdfconstant("Page") -local pdf_xobject = pdfconstant("XObject") -local pdf_form = pdfconstant("Form") +local pdfreserveobject +local pdfpagereference +local pdfflushobject +local pdfsharedobject +local pdfflushstreamobject +local pdfdeferredobject +local pdfimmediateobject -local fonthashes = fonts.hashes -local characters = fonthashes.characters -local descriptions = fonthashes.descriptions -local parameters = fonthashes.parameters -local properties = fonthashes.properties +local pdfgetfontname +local pdfgetfontobjectnumber -local report = logs.reporter("backend") +local pdfgetpagereference + +updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + pdfpagereference = lpdf.pagereference + pdfflushobject = lpdf.flushobject + pdfsharedobject = lpdf.shareobjectreference + pdfflushstreamobject = lpdf.flushstreamobject + pdfdeferredobject = lpdf.deferredobject + pdfimmediateobject = lpdf.immediateobject + -- + pdfgetfontname = lpdf.getfontname + pdfgetfontobjectnumber = lpdf.getfontobjectnumber + -- + pdfgetpagereference = lpdf.getpagereference +end) + +local pdf_pages = pdfconstant("Pages") +local pdf_page = pdfconstant("Page") +local pdf_xobject = pdfconstant("XObject") +local pdf_form = pdfconstant("Form") + +local fonthashes = fonts.hashes +local characters = fonthashes.characters +local descriptions = fonthashes.descriptions +local parameters = fonthashes.parameters +local properties = fonthashes.properties + +local report = logs.reporter("backend") +local report_objects = logs.reporter("backend","objects") + +local trace_objects = false trackers.register("backend.objects", function(v) trace_objects = v end) +local trace_details = false trackers.register("backend.details", function(v) trace_details = v end) -- used variables @@ -659,6 +677,8 @@ local flushliteral do local textliteral_code = literalvalues.text local fontliteral_code = literalvalues.font + local getdata = nuts.getdata + flushliteral = function(current,pos_h,pos_v,mode,str) if mode then if not str then @@ -717,38 +737,36 @@ local flushliteral do end end - updaters.register("backend.update.pdf",function() - function pdf.print(mode,str) - -- This only works inside objects, don't change this to flush - -- in between. It's different from luatex but okay. - if str then - mode = literalvalues[mode] + function lpdf.print(mode,str) + -- This only works inside objects, don't change this to flush + -- in between. It's different from luatex but okay. + if str then + mode = literalvalues[mode] + else + mode, str = originliteral_code, mode + end + if str and str ~= "" then + if mode == originliteral_code then + pdf_goto_pagemode() + -- pdf_set_pos(pdf_h,pdf_v) + elseif mode == pageliteral_code then + pdf_goto_pagemode() + elseif mode == textliteral_code then + pdf_goto_textmode() + elseif mode == fontliteral_code then + pdf_goto_fontmode() + elseif mode == alwaysliteral_code then + pdf_end_string_nl() + need_tm = true + elseif mode == rawliteral_code then + pdf_end_string_nl() else - mode, str = originliteral_code, mode - end - if str and str ~= "" then - if mode == originliteral_code then - pdf_goto_pagemode() - -- pdf_set_pos(pdf_h,pdf_v) - elseif mode == pageliteral_code then - pdf_goto_pagemode() - elseif mode == textliteral_code then - pdf_goto_textmode() - elseif mode == fontliteral_code then - pdf_goto_fontmode() - elseif mode == alwaysliteral_code then - pdf_end_string_nl() - need_tm = true - elseif mode == rawliteral_code then - pdf_end_string_nl() - else - pdf_goto_pagemode() - -- pdf_set_pos(pdf_h,pdf_v) - end - b = b + 1 ; buffer[b] = str + pdf_goto_pagemode() + -- pdf_set_pos(pdf_h,pdf_v) end + b = b + 1 ; buffer[b] = str end - end) + end end @@ -763,6 +781,8 @@ local flushsave, flushrestore, flushsetmatrix do local f_matrix = formatters["%s 0 0 cm"] + local getdata = nuts.getdata + flushsave = function(current,pos_h,pos_v) nofpositions = nofpositions + 1 positions[nofpositions] = { pos_h, pos_v, nofmatrices } @@ -831,11 +851,11 @@ local flushsave, flushrestore, flushsetmatrix do do - local function hasmatrix() + function lpdf.hasmatrix() return nofmatrices > 0 end - local function getmatrix() + function lpdf.getmatrix() if nofmatrices > 0 then return unpack(matrices[nofmatrices]) else @@ -843,11 +863,6 @@ local flushsave, flushrestore, flushsetmatrix do end end - updaters.register("backend.update.pdf",function() - pdf.hasmatrix = hasmatrix - pdf.getmatrix = getmatrix - end) - end pushorientation = function(orientation,pos_h,pos_v,pos_r) @@ -899,6 +914,10 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do local setprop = nuts.setprop local getprop = nuts.getprop + local getwhd = nuts.getwhd + local flushlist = nuts.flush_list + local getdata = nuts.getdata + local normalrule_code = rulecodes.normal local boxrule_code = rulecodes.box local imagerule_code = rulecodes.image @@ -942,9 +961,7 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do end end - updaters.register("backend.update.pdf",function() - pdf.getxformname = getxformname - end) + lpdf.getxformname = getxformname local function saveboxresource(box,attributes,resources,immediate,kind,margin) n = n + 1 @@ -1012,7 +1029,8 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do end end - updaters.register("backend.update.tex",function() +-- updaters.register("backend.update.tex",function() + updaters.register("backend.update.lpdf",function() tex.saveboxresource = saveboxresource tex.useboxresource = useboxresource tex.getboxresourcedimensions = getboxresourcedimensions @@ -1069,7 +1087,7 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do local imageresources, n = { }, 0 - getximagename = function(index) + getximagename = function(index) -- not used local l = imageresources[index] if l then return l.name @@ -1078,10 +1096,6 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do end end - updaters.register("backend.update.pdf",function() - pdf.getximagename = getximagename - end) - -- Groups are flushed immediately but we can decide to make them into a -- specific whatsit ... but not now. We could hash them if needed when -- we use lot sof them in mp ... but not now. @@ -1115,6 +1129,12 @@ local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do -- end of experiment + local pdfincludeimage + + updaters.register("backend.update.lpdf",function() + pdfincludeimage = lpdf.includeimage + end) + local function flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v) local width, @@ -1626,7 +1646,7 @@ local finalize do if next(usedfonts) then fonts = pdfdictionary { } for k, v in next, usedfonts do - fonts[f_font(v)] = pdfreference(pdfgetfontobjnumber(k)) -- we can overload for testing + fonts[f_font(v)] = pdfreference(pdfgetfontobjectnumber(k)) -- we can overload for testing end end @@ -1636,7 +1656,6 @@ local finalize do if next(usedxforms) or next(usedximages) or next(usedxgroups) then xforms = pdfdictionary { } for k in sortedhash(usedxforms) do - -- xforms[f_form(k)] = pdfreference(k) xforms[f_form(getxformname(k))] = pdfreference(k) end for k, v in sortedhash(usedximages) do @@ -1766,7 +1785,6 @@ local finalize do wrapper.Resources = next(boxresources) and boxresources or nil wrapper.ProcSet = lpdf.procset() - -- pdfflushstreamobject(content,wrapper,false,objectnumber) pdfflushstreamobject(content,wrapper,false,specification.objnum) end @@ -1793,15 +1811,6 @@ local finalize do end -updaters.register("backend.update.pdf",function() - job.positions.registerhandlers { - getpos = drivers.getpos, - getrpos = drivers.getrpos, - gethpos = drivers.gethpos, - getvpos = drivers.getvpos, - } -end) - updaters.register("backend.update",function() local saveboxresource = tex.boxresources.save -- @@ -1859,45 +1868,51 @@ local s_stream_e = "\010endstream\010endobj\010" do - local function setinfo() end -- we get it - local function setcatalog() end -- we get it + -- Versions can be set but normally are managed by the official standards. When possible + -- reading and writing should look at these values. - local function settrailerid(v) - trailerid = v or false + function lpdf.setversion(major,minor) + majorversion = tonumber(major) or majorversion + minorversion = tonumber(minor) or minorversion end - local function setmajorversion(v) majorversion = tonumber(v) or majorversion end - local function setminorversion(v) minorversion = tonumber(v) or minorversion end + function lpdf.getversion(major,minor) + return majorversion, minorversion + end - local function getmajorversion(v) return majorversion end - local function getminorversion(v) return minorversion end + function lpdf.majorversion() return majorversion end + function lpdf.minorversion() return minorversion end - local function setcompresslevel (v) compress = v and v ~= 0 and true or false end - local function setobjcompresslevel(v) objectstream = v and v ~= 0 and true or false end + -- It makes no sense to support levels so we only enable and disable and stick to level 3 + -- which is both fast and efficient. - local function getcompresslevel (v) return compress and 3 or 0 end - local function getobjcompresslevel(v) return objectstream and 1 or 0 end + local frozen = false + local clevel = 3 + local olevel = 1 - local function setpageresources () end -- needs to be sorted out - local function setpageattributes () end - local function setpagesattributes() end + function lpdf.setcompression(level,objectlevel,freeze) + if not frozen then + compress = level and level ~= 0 and true or false + objectstream = objectlevel and objectlevel ~= 0 and true or false + frozen = freeze + end + end - updaters.register("backend.update.pdf",function() - pdf.setinfo = setinfo - pdf.setcatalog = setcatalog - pdf.settrailerid = settrailerid - pdf.setmajorversion = setmajorversion - pdf.setminorversion = setminorversion - pdf.getmajorversion = getmajorversion - pdf.getminorversion = getminorversion - pdf.setcompresslevel = setcompresslevel - pdf.setobjcompresslevel = setobjcompresslevel - pdf.getcompresslevel = getcompresslevel - pdf.getobjcompresslevel = getobjcompresslevel - pdf.setpageresources = setpageresources - pdf.setpageattributes = setpageattributes - pdf.setpagesattributes = setpagesattributes - end) + function lpdf.getcompression() + return compress and olevel or 0, objectstream and clevel or 0 + end + + function lpdf.compresslevel() + return compress and olevel or 0 + end + + function lpdf.objectcompresslevel() + return objectstream and clevel or 0 + end + + if environment.arguments.nocompression then + lpdf.setcompression(0,0,true) + end end @@ -1975,26 +1990,174 @@ local addtocache, flushcache, cache do end -local function pdfreserveobj() - nofobjects = nofobjects + 1 - objects[nofobjects] = false - return nofobjects +do + + local names = { } + local cache = { } + local nofpages = 0 + + local texgetcount = tex.getcount + + function lpdf.reserveobject(name) + nofobjects = nofobjects + 1 + objects[nofobjects] = false + if name then + names[name] = nofobjects + if trace_objects then + report_objects("reserving number %a under name %a",nofobjects,name) + end + elseif trace_objects then + report_objects("reserving number %a",nofobjects) + end + return nofobjects + end + + function lpdf.pagereference(n,complete) -- true | false | nil | n [true,false] + if nofpages == 0 then + nofpages = structures.pages.nofpages + if nofpages == 0 then + nofpages = 1 + end + end + if n == true or not n then + complete = n + n = texgetcount("realpageno") + end + local r = n > nofpages and pdfgetpagereference(nofpages) or pdfgetpagereference(n) + return complete and pdfreference(r) or r + end + + function lpdf.nofpages() + return structures.pages.nofpages + end + + function lpdf.object(...) + pdfdeferredobject(...) + end + + function lpdf.delayedobject(data,n) + if n then + pdfdeferredobject(n,data) + else + n = pdfdeferredobject(data) + end +-- pdfreferenceobject(n) + return n + end + + function lpdf.flushobject(name,data) + if data then + local named = names[name] + if named then + if not trace_objects then + elseif trace_details then + report_objects("flushing data to reserved object with name %a, data: %S",name,data) + else + report_objects("flushing data to reserved object with name %a",name) + end + return pdfimmediateobject(named,tostring(data)) + else + if not trace_objects then + elseif trace_details then + report_objects("flushing data to reserved object with number %s, data: %S",name,data) + else + report_objects("flushing data to reserved object with number %s",name) + end + return pdfimmediateobject(name,tostring(data)) + end + else + if trace_objects and trace_details then + report_objects("flushing data: %S",name) + end + return pdfimmediateobject(tostring(name)) + end + end + + function lpdf.flushstreamobject(data,dict,compressed,objnum) -- default compressed + if trace_objects then + report_objects("flushing stream object of %s bytes",#data) + end + local dtype = type(dict) + local kind = compressed == "raw" and "raw" or "stream" + local nolength = nil + if compressed == "raw" then + compressed = nil + nolength = true + -- data = string.formatters["<< %s >>stream\n%s\nendstream"](attr,data) + end + return pdfdeferredobject { + objnum = objnum, + immediate = true, + nolength = nolength, + compresslevel = compressed == false and 0 or nil, + type = "stream", + string = data, + attr = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil, + } + end + + function lpdf.flushstreamfileobject(filename,dict,compressed,objnum) -- default compressed + if trace_objects then + report_objects("flushing stream file object %a",filename) + end + local dtype = type(dict) + return pdfdeferredobject { + objnum = objnum, + immediate = true, + compresslevel = compressed == false and 0 or nil, + type = "stream", + file = filename, + attr = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil, + } + end + + local shareobjectcache, shareobjectreferencecache = { }, { } + + function lpdf.shareobject(content) + if content == nil then + -- invalid object not created + else + content = tostring(content) + local o = shareobjectcache[content] + if not o then + o = pdfimmediateobject(content) + shareobjectcache[content] = o + end + return o + end + end + + function lpdf.shareobjectreference(content) + if content == nil then + -- invalid object not created + else + content = tostring(content) + local r = shareobjectreferencecache[content] + if not r then + local o = shareobjectcache[content] + if not o then + o = pdfimmediateobject(content) + shareobjectcache[content] = o + end + r = pdfreference(o) + shareobjectreferencecache[content] = r + end + return r + end + end + end local pages = table.setmetatableindex(function(t,k) - local v = pdfreserveobj() + local v = pdfreserveobject() t[k] = v return v end) -local function getpageref(n) +function lpdf.getpagereference(n) return pages[n] end -local function refobj() - -- not needed, as we have auto-delay -end - local function flushnormalobj(data,n) if not n then nofobjects = nofobjects + 1 @@ -2094,24 +2257,7 @@ flushdeferred = function() -- was forward defined end end --- n = pdf.obj([n,] objtext) --- n = pdf.obj([n,] "file", filename) --- n = pdf.obj([n,] "stream", streamtext [, attrtext]) --- n = pdf.obj([n,] "streamfile", filename [, attrtext]) --- --- n = pdf.obj { --- type = <string>, -- raw|stream --- immediate = <boolean>, --- objnum = <number>, --- attr = <string>, --- compresslevel = <number>, --- objcompression = <boolean>, --- file = <string>, --- string = <string>, --- nolength = <boolean>, --- } - -local function obj(a,b,c,d) +function lpdf.immediateobject(a,b,c,d) local kind --, immediate local objnum, data, attr, filename local compresslevel, objcompression, nolength @@ -2183,15 +2329,7 @@ local function obj(a,b,c,d) return objnum end -updaters.register("backend.update.pdf",function() - pdf.reserveobj = pdfreserveobj - pdf.getpageref = getpageref - pdf.refobj = refobj - pdf.flushstreamobj = flushstreamobj - pdf.flushnormalobj = flushnormalobj - pdf.obj = obj - pdf.immediateobj = obj -end) +lpdf.deferredobject = lpdf.immediateobject -- In lua 5.4 the methods are now moved one metalevel deeper so we need to get them -- from mt.__index instead. (I did get that at first.) It makes for a slightly (imo) @@ -2483,7 +2621,7 @@ end -- For the moment we overload it here, although back-fil.lua eventually will -- be merged with back-pdf as it's pdf specific, or maybe back-imp-pdf or so. -updaters.register("backend.update.pdf",function() +do -- updaters.register("backend.update.pdf",function() -- We overload img but at some point it will even go away, so we just -- reimplement what we need in context. This will change completely i.e. @@ -2603,7 +2741,7 @@ updaters.register("backend.update.pdf",function() return n end - function pdf.includeimage(index) + function lpdf.includeimage(index) local specification = indices[index] if specification then local bbox = specification.bbox @@ -2612,7 +2750,7 @@ updaters.register("backend.update.pdf",function() local xsize = bbox[3] - xorigin -- we need the original ones, not the 'rotated' ones local ysize = bbox[4] - yorigin -- we need the original ones, not the 'rotated' ones local transform = specification.transform or 0 - local objnum = specification.objnum or pdfreserveobj() + local objnum = specification.objnum or pdfreserveobject() local groupref = nil local kind = specification.kind or specification.type or img_none -- determines scaling type return @@ -2625,19 +2763,28 @@ updaters.register("backend.update.pdf",function() end end -end) +end -- ) -updaters.register("backend.update.lpdf",function() +do -- updaters.register("backend.update.lpdf",function() -- todo: an md5 or sha2 hash can save space -- todo: make a type 3 font instead -- todo: move to lpdf namespace - local pdfimage = lpdf.epdf.image - local newpdf = pdfimage.new - local openpdf = pdfimage.open - local closepdf = pdfimage.close - local copypage = pdfimage.copy + local pdfimage + local newpdf + local openpdf + local closepdf + local copypage + + + updaters.register("backend.update.lpdf",function() + pdfimage = lpdf.epdf.image + newpdf = pdfimage.new + openpdf = pdfimage.open + closepdf = pdfimage.close + copypage = pdfimage.copy + end) local embedimage = images.embed @@ -2668,7 +2815,6 @@ updaters.register("backend.update.lpdf",function() index = image.index topdf[id] = index end - -- pdf.print or pdf.literal flushimage(index,wd,ht,dp,pos_h,pos_v) end @@ -2721,7 +2867,7 @@ updaters.register("backend.update.lpdf",function() lpdf.vfimage = pdfvfimage -end) +end -- ) -- The driver. @@ -2743,19 +2889,14 @@ do local function prepare(driver) if not environment.initex then -- install new functions in pdf namespace - updaters.apply("backend.update.pdf") +-- updaters.apply("backend.update.pdf") -- install new functions in lpdf namespace updaters.apply("backend.update.lpdf") -- adapt existing shortcuts to lpdf namespace - updaters.apply("backend.update.tex") - -- adapt existing shortcuts to tex namespace +-- updaters.apply("backend.update.tex") +-- -- adapt existing shortcuts to tex namespace updaters.apply("backend.update") -- - -- if rawget(pdf,"setforcefile") then - -- pdf.setforcefile(false) -- default anyway - -- end - -- - -- pdfname = file.addsuffix(tex.jobname,"pdf") pdfname = tex.jobname .. ".pdf" openfile(pdfname) -- @@ -2776,7 +2917,7 @@ do -- end -- - environment.lmtxmode = CONTEXTLMTXMODE + environment.lmtxmode = true -- CONTEXTLMTXMODE -- converter = drivers.converters.lmtx useddriver = driver diff --git a/tex/context/base/mkxl/lpdf-mis.lmt b/tex/context/base/mkxl/lpdf-mis.lmt new file mode 100644 index 000000000..6870a1ad4 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-mis.lmt @@ -0,0 +1,668 @@ +if not modules then modules = { } end modules ['lpdf-mis'] = { + version = 1.001, + 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" +} + +-- Although we moved most pdf handling to the lua end, we didn't change +-- the overall approach. For instance we share all resources i.e. we +-- don't make subsets for each xform or page. The current approach is +-- quite efficient. A big difference between MkII and MkIV is that we +-- now use forward references. In this respect the MkII code shows that +-- it evolved over a long period, when backends didn't provide forward +-- referencing and references had to be tracked in multiple passes. Of +-- course there are a couple of more changes. + +local next, tostring, type = next, tostring, type +local format, gsub, formatters = string.format, string.gsub, string.formatters +local concat, flattened = table.concat, table.flattened +local settings_to_array = utilities.parsers.settings_to_array + +local backends, lpdf, nodes = backends, lpdf, nodes + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations + +local nuts = nodes.nuts +local copy_node = nuts.copy + +local nodepool = nuts.pool +local pageliteral = nodepool.pageliteral +local register = nodepool.register + +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfconstant = lpdf.constant +local pdfreference = lpdf.reference +local pdfunicode = lpdf.unicode +local pdfverbose = lpdf.verbose +local pdfstring = lpdf.string +local pdfaction = lpdf.action + +local pdfflushobject +local pdfflushstreamobject +local pdfminorversion + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfflushstreamobject = lpdf.flushstreamobject + pdfminorversion = lpdf.minorversion +end) + +local formattedtimestamp = lpdf.pdftimestamp +local adddocumentextgstate = lpdf.adddocumentextgstate +local addtocatalog = lpdf.addtocatalog +local addtoinfo = lpdf.addtoinfo +local addtopageattributes = lpdf.addtopageattributes +local addtonames = lpdf.addtonames + +local pdfgetmetadata = lpdf.getmetadata + +local texset = tex.set + +local variables = interfaces.variables + +local v_stop = variables.stop +local v_none = variables.none +local v_max = variables.max +local v_bookmark = variables.bookmark +local v_fit = variables.fit +local v_doublesided = variables.doublesided +local v_singlesided = variables.singlesided +local v_default = variables.default +local v_auto = variables.auto +local v_fixed = variables.fixed +local v_landscape = variables.landscape +local v_portrait = variables.portrait +local v_page = variables.page +local v_paper = variables.paper +local v_attachment = variables.attachment +local v_layer = variables.layer +local v_lefttoright = variables.lefttoright +local v_righttoleft = variables.righttoleft +local v_title = variables.title +local v_nomenubar = variables.nomenubar + +local positive = register(pageliteral("/GSpositive gs")) +local negative = register(pageliteral("/GSnegative gs")) +local overprint = register(pageliteral("/GSoverprint gs")) +local knockout = register(pageliteral("/GSknockout gs")) + +local omitextraboxes = false + +directives.register("backend.omitextraboxes", function(v) omitextraboxes = v end) + +local function initializenegative() + local a = pdfarray { 0, 1 } + local g = pdfconstant("ExtGState") + local d = pdfdictionary { + FunctionType = 4, + Range = a, + Domain = a, + } + local negative = pdfdictionary { Type = g, TR = pdfreference(pdfflushstreamobject("{ 1 exch sub }",d)) } -- can be shared + local positive = pdfdictionary { Type = g, TR = pdfconstant("Identity") } + adddocumentextgstate("GSnegative", pdfreference(pdfflushobject(negative))) + adddocumentextgstate("GSpositive", pdfreference(pdfflushobject(positive))) + initializenegative = nil +end + +local function initializeoverprint() + local g = pdfconstant("ExtGState") + local knockout = pdfdictionary { Type = g, OP = false, OPM = 0 } + local overprint = pdfdictionary { Type = g, OP = true, OPM = 1 } + adddocumentextgstate("GSknockout", pdfreference(pdfflushobject(knockout))) + adddocumentextgstate("GSoverprint", pdfreference(pdfflushobject(overprint))) + initializeoverprint = nil +end + +function nodeinjections.overprint() + if initializeoverprint then initializeoverprint() end + return copy_node(overprint) +end +function nodeinjections.knockout () + if initializeoverprint then initializeoverprint() end + return copy_node(knockout) +end + +function nodeinjections.positive() + if initializenegative then initializenegative() end + return copy_node(positive) +end +function nodeinjections.negative() + if initializenegative then initializenegative() end + return copy_node(negative) +end + +-- function codeinjections.addtransparencygroup() +-- -- png: /CS /DeviceRGB /I true +-- local d = pdfdictionary { +-- S = pdfconstant("Transparency"), +-- I = true, +-- K = true, +-- } +-- lpdf.registerpagefinalizer(function() addtopageattributes("Group",d) end) -- hm +-- end + +-- actions (todo: store and update when changed) + +local openpage, closepage, opendocument, closedocument + +function codeinjections.registerdocumentopenaction(open) + opendocument = open +end + +function codeinjections.registerdocumentcloseaction(close) + closedocument = close +end + +function codeinjections.registerpageopenaction(open) + openpage = open +end + +function codeinjections.registerpagecloseaction(close) + closepage = close +end + +local function flushdocumentactions() + if opendocument then + addtocatalog("OpenAction",pdfaction(opendocument)) + end + if closedocument then + addtocatalog("CloseAction",pdfaction(closedocument)) + end +end + +local function flushpageactions() + if openpage or closepage then + local d = pdfdictionary() + if openpage then + d.O = pdfaction(openpage) + end + if closepage then + d.C = pdfaction(closepage) + end + addtopageattributes("AA",d) + end +end + +lpdf.registerpagefinalizer (flushpageactions, "page actions") +lpdf.registerdocumentfinalizer(flushdocumentactions,"document actions") + +--- info : this can change and move elsewhere + +local identity = { } + +function codeinjections.setupidentity(specification) + for k, v in next, specification do + if v ~= "" then + identity[k] = v + end + end +end + +function codeinjections.getidentityvariable(name) + return identity[name] +end + +local done = false -- using "setupidentity = function() end" fails as the meaning is frozen in register + +local function setupidentity() + if not done then + local metadata = pdfgetmetadata() + local creator = metadata.creator + local version = metadata.contextversion + local time = metadata.time + local jobname = environment.jobname or tex.jobname or "unknown" + -- + local title = identity.title + if not title or title == "" then + title = tex.jobname + end + addtoinfo("Title", pdfunicode(title), title) + local subtitle = identity.subtitle or "" + if subtitle ~= "" then + addtoinfo("Subject", pdfunicode(subtitle), subtitle) + end + local author = identity.author or "" + if author ~= "" then + addtoinfo("Author", pdfunicode(author), author) -- '/Author' in /Info, 'Creator' in XMP + end + addtoinfo("Creator", pdfunicode(creator), creator) + addtoinfo("CreationDate", pdfstring(formattedtimestamp(time))) + local date = identity.date or "" + local pdfdate = date and formattedtimestamp(date) + if pdfdate then + addtoinfo("ModDate", pdfstring(pdfdate), date) + else + -- users should enter the date in 2010-01-19T23:27:50+01:00 format + -- and if not provided that way we use the creation time instead + addtoinfo("ModDate", pdfstring(formattedtimestamp(time)),time) + end + local keywords = identity.keywords or "" + if keywords ~= "" then + keywords = concat(settings_to_array(keywords), " ") + addtoinfo("Keywords", pdfunicode(keywords), keywords) + end + local id = lpdf.id() + addtoinfo("ID", pdfstring(id), id) -- needed for pdf/x + -- + addtoinfo("ConTeXt.Version",version) + -- + local lmtx = codeinjections.lmtxmode() + if lmtx then + addtoinfo("ConTeXt.LMTX",formatters["%0.2f"](lmtx)) + end + -- + addtoinfo("ConTeXt.Time",os.date("%Y-%m-%d %H:%M")) + addtoinfo("ConTeXt.Jobname",jobname) + addtoinfo("ConTeXt.Url","www.pragma-ade.com") + addtoinfo("ConTeXt.Support","contextgarden.net") + addtoinfo("TeX.Support","tug.org") + -- + done = true + else + -- no need for a message + end +end + +lpdf.registerpagefinalizer(setupidentity,"identity") + +-- or when we want to be able to set things after pag e1: +-- +-- lpdf.registerdocumentfinalizer(setupidentity,1,"identity") + +local function flushjavascripts() + local t = interactions.javascripts.flushpreambles() + if #t > 0 then + local a = pdfarray() + local pdf_javascript = pdfconstant("JavaScript") + for i=1,#t do + local ti = t[i] + local name = ti[1] + local script = ti[2] + local j = pdfdictionary { + S = pdf_javascript, + JS = pdfreference(pdfflushstreamobject(script)), + } + a[#a+1] = pdfstring(name) + a[#a+1] = pdfreference(pdfflushobject(j)) + end + addtonames("JavaScript",pdfreference(pdfflushobject(pdfdictionary{ Names = a }))) + end +end + +lpdf.registerdocumentfinalizer(flushjavascripts,"javascripts") + +-- -- -- + +local plusspecs = { + [v_max] = { + mode = "FullScreen", + }, + [v_bookmark] = { + mode = "UseOutlines", + }, + [v_attachment] = { + mode = "UseAttachments", + }, + [v_layer] = { + mode = "UseOC", + }, + [v_fit] = { + fit = true, + }, + [v_doublesided] = { + layout = "TwoColumnRight", + }, + [v_fixed] = { + fixed = true, + }, + [v_landscape] = { + duplex = "DuplexFlipShortEdge", + }, + [v_portrait] = { + duplex = "DuplexFlipLongEdge", + }, + [v_page] = { + duplex = "Simplex" , + }, + [v_paper] = { + paper = true, + }, + [v_title] ={ + title = true, + }, + [v_lefttoright] ={ + direction = "L2R", + }, + [v_righttoleft] ={ + direction = "R2L", + }, + [v_nomenubar] ={ + nomenubar = true, + }, +} + +local pagespecs = { + -- + [v_max] = plusspecs[v_max], + [v_bookmark] = plusspecs[v_bookmark], + [v_attachment] = plusspecs[v_attachment], + [v_layer] = plusspecs[v_layer], + [v_lefttoright] = plusspecs[v_lefttoright], + [v_righttoleft] = plusspecs[v_righttoleft], + [v_title] = plusspecs[v_title], + -- + [v_none] = { + }, + [v_fit] = { + mode = "UseNone", + fit = true, + }, + [v_doublesided] = { + mode = "UseNone", + layout = "TwoColumnRight", + fit = true, + }, + [v_singlesided] = { + mode = "UseNone" + }, + [v_default] = { + mode = "UseNone", + layout = "auto", + }, + [v_auto] = { + mode = "UseNone", + layout = "auto", + }, + [v_fixed] = { + mode = "UseNone", + layout = "auto", + fixed = true, -- noscale + }, + [v_landscape] = { + mode = "UseNone", + layout = "auto", + fixed = true, + duplex = "DuplexFlipShortEdge", + }, + [v_portrait] = { + mode = "UseNone", + layout = "auto", + fixed = true, + duplex = "DuplexFlipLongEdge", + }, + [v_page] = { + mode = "UseNone", + layout = "auto", + fixed = true, + duplex = "Simplex", + }, + [v_paper] = { + mode = "UseNone", + layout = "auto", + fixed = true, + duplex = "Simplex", + paper = true, + }, + [v_nomenubar] = { + mode = "UseNone", + layout = "auto", + nomenubar = true, + }, +} + +local pagespec, topoffset, leftoffset, height, width, doublesided = "default", 0, 0, 0, 0, false +local cropoffset, bleedoffset, trimoffset, artoffset = 0, 0, 0, 0 +local marked = false +local copies = false + +local getpagedimensions getpagedimensions = function() + getpagedimensions = backends.codeinjections.getpagedimensions + return getpagedimensions() +end + +function codeinjections.setupcanvas(specification) + local paperheight = specification.paperheight + local paperwidth = specification.paperwidth + local paperdouble = specification.doublesided + -- + paperwidth, paperheight = codeinjections.setpagedimensions(paperwidth,paperheight) + -- + pagespec = specification.mode or pagespec + topoffset = specification.topoffset or 0 + leftoffset = specification.leftoffset or 0 + height = specification.height or paperheight + width = specification.width or paperwidth + marked = specification.print + -- + copies = specification.copies + if copies and copies < 2 then + copies = false + end + -- + cropoffset = specification.cropoffset or 0 + trimoffset = cropoffset - (specification.trimoffset or 0) + bleedoffset = trimoffset - (specification.bleedoffset or 0) + artoffset = bleedoffset - (specification.artoffset or 0) + -- + if paperdouble ~= nil then + doublesided = paperdouble + end +end + +local function documentspecification() + if not pagespec or pagespec == "" then + pagespec = v_default + end + local settings = settings_to_array(pagespec) + -- so the first one detemines the defaults + local first = settings[1] + local defaults = pagespecs[first] + local spec = defaults or pagespecs[v_default] + -- successive keys can modify this + if spec.layout == "auto" then + if doublesided then + local s = pagespecs[v_doublesided] -- to be checked voor interfaces + for k, v in next, s do + spec[k] = v + end + else + spec.layout = false + end + end + -- we start at 2 when we have a valid first default set + for i=defaults and 2 or 1,#settings do + local s = plusspecs[settings[i]] + if s then + for k, v in next, s do + spec[k] = v + end + end + end + -- maybe interfaces.variables + local layout = spec.layout + local mode = spec.mode + local fit = spec.fit + local fixed = spec.fixed + local duplex = spec.duplex + local paper = spec.paper + local title = spec.title + local direction = spec.direction + local nomenubar = spec.nomenubar + if layout then + addtocatalog("PageLayout",pdfconstant(layout)) + end + if mode then + addtocatalog("PageMode",pdfconstant(mode)) + end + local prints = nil + if marked then + local pages = structures.pages + local marked = pages.allmarked(marked) + local nofmarked = marked and #marked or 0 + if nofmarked > 0 then + -- the spec is wrong in saying that numbering starts at 1 which of course makes + -- sense as most real documents start with page 0 .. sigh + for i=1,#marked do marked[i] = marked[i] - 1 end + prints = pdfarray(flattened(pages.toranges(marked))) + end + end + if fit or fixed or duplex or copies or paper or prints or title or direction or nomenubar then + addtocatalog("ViewerPreferences",pdfdictionary { + FitWindow = fit and true or nil, + PrintScaling = fixed and pdfconstant("None") or nil, + Duplex = duplex and pdfconstant(duplex) or nil, + NumCopies = copies and copies or nil, + PickTrayByPDFSize = paper and true or nil, + PrintPageRange = prints or nil, + DisplayDocTitle = title and true or nil, + Direction = direction and pdfconstant(direction) or nil, + HideMenubar = nomenubar and true or nil, + }) + end + addtoinfo ("Trapped", pdfconstant("False")) -- '/Trapped' in /Info, 'Trapped' in XMP + addtocatalog("Version", pdfconstant(format("1.%s",pdfminorversion()))) + addtocatalog("Lang", pdfstring(tokens.getters.macro("currentmainlanguage"))) +end + +-- temp hack: the mediabox is not under our control and has a precision of 5 digits + +local factor = number.dimenfactors.bp +local f_value = formatters["%.6N"] + +local function boxvalue(n) -- we could share them + return pdfverbose(f_value(factor * n)) +end + +local function pagespecification() + local paperwidth, paperheight = codeinjections.getpagedimensions() + local llx = leftoffset + local lly = paperheight + topoffset - height + local urx = width - leftoffset + local ury = paperheight - topoffset + -- boxes can be cached + local function extrabox(WhatBox,offset,always) + if offset ~= 0 or always then + addtopageattributes(WhatBox, pdfarray { + boxvalue(llx + offset), + boxvalue(lly + offset), + boxvalue(urx - offset), + boxvalue(ury - offset), + }) + end + end + if omitextraboxes then + -- only useful for testing / comparing + else + extrabox("CropBox",cropoffset,true) -- mandate for rendering + extrabox("TrimBox",trimoffset,true) -- mandate for pdf/x + extrabox("BleedBox",bleedoffset) -- optional + -- extrabox("ArtBox",artoffset) -- optional .. unclear what this is meant to do + end +end + +lpdf.registerpagefinalizer(pagespecification,"page specification") +lpdf.registerdocumentfinalizer(documentspecification,"document specification") + +-- Page Label support ... +-- +-- In principle we can also support /P (prefix) as we can just use the verbose form +-- and we can then forget about the /St (start) as we don't care about those few +-- extra bytes due to lack of collapsing. Anyhow, for that we need a stupid prefix +-- variant and that's not on the agenda now. + +local map = { + numbers = "D", + Romannumerals = "R", + romannumerals = "r", + Characters = "A", + characters = "a", +} + +-- local function featurecreep() +-- local pages, lastconversion, list = structures.pages.tobesaved, nil, pdfarray() +-- local getstructureset = structures.sets.get +-- for i=1,#pages do +-- local p = pages[i] +-- if not p then +-- return -- fatal error +-- else +-- local numberdata = p.numberdata +-- if numberdata then +-- local conversionset = numberdata.conversionset +-- if conversionset then +-- local conversion = getstructureset("structure:conversions",p.block,conversionset,1,"numbers") +-- if conversion ~= lastconversion then +-- lastconversion = conversion +-- list[#list+1] = i - 1 -- pdf starts numbering at 0 +-- list[#list+1] = pdfdictionary { S = pdfconstant(map[conversion] or map.numbers) } +-- end +-- end +-- end +-- if not lastconversion then +-- lastconversion = "numbers" +-- list[#list+1] = i - 1 -- pdf starts numbering at 0 +-- list[#list+1] = pdfdictionary { S = pdfconstant(map.numbers) } +-- end +-- end +-- end +-- addtocatalog("PageLabels", pdfdictionary { Nums = list }) +-- end + +local function featurecreep() + local pages = structures.pages.tobesaved + local list = pdfarray() + local getset = structures.sets.get + local stopped = false + local oldlabel = nil + local olconversion = nil + for i=1,#pages do + local p = pages[i] + if not p then + return -- fatal error + end + local label = p.viewerprefix or "" + if p.status == v_stop then + if not stopped then + list[#list+1] = i - 1 -- pdf starts numbering at 0 + list[#list+1] = pdfdictionary { + P = pdfunicode(label), + } + stopped = true + end + oldlabel = nil + oldconversion = nil + stopped = false + else + local numberdata = p.numberdata + local conversion = nil + local number = p.number + if numberdata then + local conversionset = numberdata.conversionset + if conversionset then + conversion = getset("structure:conversions",p.block,conversionset,1,"numbers") + end + end + conversion = conversion and map[conversion] or map.numbers + if number == 1 or oldlabel ~= label or oldconversion ~= conversion then + list[#list+1] = i - 1 -- pdf starts numbering at 0 + list[#list+1] = pdfdictionary { + S = pdfconstant(conversion), + St = number, + P = label ~= "" and pdfunicode(label) or nil, + } + end + oldlabel = label + oldconversion = conversion + stopped = false + end + end + addtocatalog("PageLabels", pdfdictionary { Nums = list }) +end + +lpdf.registerdocumentfinalizer(featurecreep,"featurecreep") diff --git a/tex/context/base/mkxl/lpdf-mov.lmt b/tex/context/base/mkxl/lpdf-mov.lmt new file mode 100644 index 000000000..42ba6fb00 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-mov.lmt @@ -0,0 +1,68 @@ +if not modules then modules = { } end modules ['lpdf-mov'] = { + version = 1.001, + 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" +} + +local format = string.format + +local lpdf = lpdf +local context = context + +local nodeinjections = backends.pdf.nodeinjections +local pdfconstant = lpdf.constant +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfborder = lpdf.border +local write_node = node.write + +function nodeinjections.insertmovie(specification) + -- managed in figure inclusion: width, height, factor, repeat, controls, preview, label, foundname + local width = specification.width + local height = specification.height + local factor = specification.factor or number.dimenfactors.bp + local moviedict = pdfdictionary { + F = specification.foundname, + Aspect = pdfarray { factor * width, factor * height }, + Poster = (specification.preview and true) or false, + } + local controldict = pdfdictionary { + ShowControls = (specification.controls and true) or false, + Mode = (specification["repeat"] and pdfconstant("Repeat")) or nil, + } + local bs, bc = pdfborder() + local action = pdfdictionary { + Subtype = pdfconstant("Movie"), + Border = bs, + C = bc, + T = format("movie %s",specification.label), + Movie = moviedict, + A = controldict, + } + write_node(nodeinjections.annotation(width,height,0,action())) -- test: context(...) +end + +function nodeinjections.insertsound(specification) + -- managed in interaction: repeat, label, foundname + local soundclip = interactions.soundclips.soundclip(specification.label) + if soundclip then + local controldict = pdfdictionary { + Mode = (specification["repeat"] and pdfconstant("Repeat")) or nil + } + local sounddict = pdfdictionary { + F = soundclip.filename + } + local bs, bc = pdfborder() + local action = pdfdictionary { + Subtype = pdfconstant("Movie"), + Border = bs, + C = bc, + T = format("sound %s",specification.label), + Movie = sounddict, + A = controldict, + } + write_node(nodeinjections.annotation(0,0,0,action())) -- test: context(...) + end +end diff --git a/tex/context/base/mkxl/lpdf-pde.lmt b/tex/context/base/mkxl/lpdf-pde.lmt new file mode 100644 index 000000000..7fb14ada2 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-pde.lmt @@ -0,0 +1,1211 @@ +if not modules then modules = { } end modules ['lpdf-epd'] = { + version = 1.001, + comment = "companion to lpdf-epa.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files", + history = "this one replaces the poppler/pdfe binding", +} + +-- \enabledirectives[graphics.pdf.uselua] +-- \enabledirectives[graphics.pdf.recompress] +-- \enabledirectives[graphics.pdf.stripmarked] + +-- maximum integer : +2^32 +-- maximum real : +2^15 +-- minimum real : 1/(2^16) + +-- get_flagged : does that still work + +-- ppdoc_permissions (ppdoc *pdf); + +-- PPSTRING_ENCODED 1 << 0 +-- PPSTRING_DECODED 1 << 1 +-- PPSTRING_EXEC 1 << 2 postscript only +-- PPSTRING_PLAIN 0 +-- PPSTRING_BASE16 1 << 3 +-- PPSTRING_BASE85 1 << 4 +-- PPSTRING_UTF16BE 1 << 5 +-- PPSTRING_UTF16LE 1 << 6 + +-- PPDOC_ALLOW_PRINT 1 << 2 printing +-- PPDOC_ALLOW_MODIFY 1 << 3 filling form fields, signing, creating template pages +-- PPDOC_ALLOW_COPY 1 << 4 copying, copying for accessibility +-- PPDOC_ALLOW_ANNOTS 1 << 5 filling form fields, copying, signing +-- PPDOC_ALLOW_EXTRACT 1 << 9 contents copying for accessibility +-- PPDOC_ALLOW_ASSEMBLY 1 << 10 no effect +-- PPDOC_ALLOW_PRINT_HIRES 1 << 11 no effect + +-- PPCRYPT_NONE 0 no encryption, go ahead +-- PPCRYPT_DONE 1 encryption present but password succeeded, go ahead +-- PPCRYPT_PASS -1 encryption present, need non-empty password +-- PPCRYPT_FAIL -2 invalid or unsupported encryption (eg. undocumented in pdf spec) + +local setmetatable, type, next = setmetatable, type, next +local tostring, tonumber, unpack = tostring, tonumber, unpack +local char, byte, find = string.char, string.byte, string.find +local abs = math.abs +local concat, swapped, sortedhash, sortedkeys = table.concat, table.swapped, table.sortedhash, table.sortedkeys +local utfchar = string.char +local setmetatableindex = table.setmetatableindex +local ioopen = io.open + +local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns +local P, C, S, R, Ct, Cc, V, Carg, Cs, Cf, Cg = lpeg.P, lpeg.C, lpeg.S, lpeg.R, lpeg.Ct, lpeg.Cc, lpeg.V, lpeg.Carg, lpeg.Cs, lpeg.Cf, lpeg.Cg + +if not lpdf then + require("lpdf-aux") +end + +if not (number and number.dimenfactors) then + require("util-dim") +end + +local pdfe = pdfe + lpdf = lpdf or { } +local lpdf = lpdf +local lpdf_epdf = { } + lpdf.epdf = lpdf_epdf + +local pdfopen = pdfe.open +local pdfopenfile = pdfe.openfile +local pdfnew = pdfe.new +local pdfclose = pdfe.close + +local getcatalog = pdfe.getcatalog +local getinfo = pdfe.getinfo +local gettrailer = pdfe.gettrailer +local getnofpages = pdfe.getnofpages +local getversion = pdfe.getversion +local getbox = pdfe.getbox +local getstatus = pdfe.getstatus +local unencrypt = pdfe.unencrypt + +local dictionarytotable = pdfe.dictionarytotable +local arraytotable = pdfe.arraytotable +local pagestotable = pdfe.pagestotable +local readwholestream = pdfe.readwholestream + +local getfromreference = pdfe.getfromreference + +local report_epdf = logs.reporter("epdf") + +local allocate = utilities.storage.allocate + +local bpfactor = number.dimenfactors.bp + +local objectcodes = { [0] = + "none", + "null", + "bool", + "integer", + "number", + "name", + "string", + "array", + "dictionary", + "stream", + "reference", +} + +local encryptioncodes = { + [0] = "notencrypted", + [1] = "unencrypted", + [-1] = "protected", + [-2] = "failure", +} + +objectcodes = allocate(swapped(objectcodes,objectcodes)) +encryptioncodes = allocate(swapped(encryptioncodes,encryptioncodes)) + +pdfe.objectcodes = objectcodes +pdfe.encryptioncodes = encryptioncodes + +local null_object_code = objectcodes.null +local reference_object_code = objectcodes.reference + +local none_object_code = objectcodes.none +local null_object_code = objectcodes.null +local bool_object_code = objectcodes.bool +local integer_object_code = objectcodes.integer +local number_object_code = objectcodes.number +local name_object_code = objectcodes.name +local string_object_code = objectcodes.string +local array_object_code = objectcodes.array +local dictionary_object_code = objectcodes.dictionary +local stream_object_code = objectcodes.stream +local reference_object_code = objectcodes.reference + +local checked_access +local get_flagged -- from pdfe -> lpdf + +if lpdf.dictionary then + + -- we're in context + + local pdfdictionary = lpdf.dictionary + local pdfarray = lpdf.array + local pdfconstant = lpdf.constant + local pdfstring = lpdf.string + local pdfunicode = lpdf.unicode + + get_flagged = function(t,f,k) + local tk = t[k] -- triggers resolve + local fk = f[k] + if not fk then + return tk + elseif fk == "name" then + return pdfconstant(tk) + elseif fk == "array" then + return pdfarray(tk) + elseif fk == "dictionary" then + return pdfarray(tk) + elseif fk == "rawtext" then + return pdfstring(tk) + elseif fk == "unicode" then + return pdfunicode(tk) + else + return tk + end + end + +else + + get_flagged = function(t,f,k) + return t[k] + end + +end + +-- We need to convert the string from utf16 although there is no way to +-- check if we have a regular string starting with a bom. So, we have +-- na dilemma here: a pdf doc encoded string can be invalid utf. + +-- <hex encoded> : implicit 0 appended if odd +-- (byte encoded) : \( \) \\ escaped +-- +-- <FE><FF> : utf16be +-- +-- \r \r \t \b \f \( \) \\ \NNN and \<newline> : append next line +-- +-- the getString function gives back bytes so we don't need to worry about +-- the hex aspect. + +local some_dictionary +local some_array +local some_stream +local some_reference + +local some_string = lpdf.frombytes + +local function get_value(document,t,key) + if not key then + return + end + local value = t[key] + if not value then + return + end + if type(value) ~= "table" then + return value + end + -- we can assume names to be simple and strings to be tables + local kind = value[1] + if kind == name_object_code then + return value[2] + elseif kind == string_object_code then + return some_string(value[2],value[3]) + elseif kind == array_object_code then + return some_array(value[2],document) + elseif kind == dictionary_object_code then + return some_dictionary(value[2],document) + elseif kind == stream_object_code then + return some_stream(value,document) + elseif kind == reference_object_code then + return some_reference(value,document) + end + return value +end + +some_dictionary = function (d,document) + local f = dictionarytotable(d,true) + local t = setmetatable({ __raw__ = f, __type__ = dictionary_object_code }, { + __index = function(t,k) + return get_value(document,f,k) + end, + __call = function(t,k) + return get_flagged(t,f,k) + end, + } ) + return t, "dictionary" +end + +some_array = function (a,document) + local f = arraytotable(a,true) + local n = #f + local t = setmetatable({ __raw__ = f, __type__ = array_object_code, n = n }, { + __index = function(t,k) + return get_value(document,f,k) + end, + __call = function(t,k) + return get_flagged(t,f,k) + end, + __len = function(t,k) + return n + end, + } ) + return t, "array" +end + +some_stream = function(s,d,document) + local f = dictionarytotable(d,true) + local t = setmetatable({ __raw__ = f, __type__ = stream_object_code }, { + __index = function(t,k) + return get_value(document,f,k) + end, + __call = function(t,raw) + if raw == false then + return readwholestream(s,false) -- original + else + return readwholestream(s,true) -- uncompressed + end + end, + } ) + return t, "stream" +end + +some_reference = function(r,document) + local objnum = r[3] + local cached = document.__cache__[objnum] + if not cached then + local kind, object, b, c = getfromreference(r[2]) + if kind == dictionary_object_code then + cached = some_dictionary(object,document) + elseif kind == array_object_code then + cached = some_array(object,document) + elseif kind == stream_object_code then + cached = some_stream(object,b,document) + else + cached = { kind, object, b, c } + -- really cache this? + end + document.__cache__[objnum] = cached + document.__xrefs__[cached] = objnum + end + return cached +end + +local resolvers = { } +lpdf_epdf.resolvers = resolvers + +local function resolve(document,k) + local resolver = resolvers[k] + if resolver then + local entry = resolver(document) + document[k] = entry + return entry + end +end + +local function getnames(document,n,target) -- direct + if n then + local Names = n.Names + if Names then + if not target then + target = { } + end + for i=1,#Names,2 do + target[Names[i]] = Names[i+1] + end + else + local Kids = n.Kids + if Kids then + for i=1,#Kids do + target = getnames(document,Kids[i],target) + end + end + end + return target + end +end + +local function getkids(document,n,target) -- direct + if n then + local Kids = n.Kids + if Kids then + for i=1,#Kids do + target = getkids(document,Kids[i],target) + end + elseif target then + target[#target+1] = n + else + target = { n } + end + return target + end +end + +function resolvers.destinations(document) + local Names = document.Catalog.Names + return getnames(document,Names and Names.Dests) +end + +function resolvers.javascripts(document) + local Names = document.Catalog.Names + return getnames(document,Names and Names.JavaScript) +end + +function resolvers.widgets(document) + local Names = document.Catalog.AcroForm + return Names and Names.Fields +end + +function resolvers.embeddedfiles(document) + local Names = document.Catalog.Names + return getnames(document,Names and Names.EmbeddedFiles) +end + +-- /OCProperties << +-- /OCGs [ 15 0 R 17 0 R 19 0 R 21 0 R 23 0 R 25 0 R 27 0 R ] +-- /D << +-- /Order [ 15 0 R 17 0 R 19 0 R 21 0 R 23 0 R 25 0 R 27 0 R ] +-- /ON [ 15 0 R 17 0 R 19 0 R 21 0 R 23 0 R 25 0 R 27 0 R ] +-- /OFF [ ] +-- >> +-- >> + +function resolvers.layers(document) + local properties = document.Catalog.OCProperties + if properties then + local layers = properties.OCGs + if layers then + local t = { } + for i=1,#layers do + local layer = layers[i] + t[i] = layer.Name + end + -- t.n = n + return t + end + end +end + +function resolvers.structure(document) + -- this might become a tree + return document.Catalog.StructTreeRoot +end + +function resolvers.pages(document) + local __data__ = document.__data__ + local __xrefs__ = document.__xrefs__ + local __cache__ = document.__cache__ + -- + local nofpages = document.nofpages + local pages = { } + local rawpages = pagestotable(__data__) + document.pages = pages + -- + for pagenumber=1,nofpages do + local rawpagedata = rawpages[pagenumber] + if rawpagedata then + local pagereference = rawpagedata[3] + local pageobject = rawpagedata[1] + local pagedata = some_dictionary(pageobject,document) + if pagedata and pageobject then + pagedata.number = pagenumber + pagedata.MediaBox = getbox(pageobject,"MediaBox") + pagedata.CropBox = getbox(pageobject,"CropBox") + pagedata.BleedBox = getbox(pageobject,"BleedBox") + pagedata.ArtBox = getbox(pageobject,"ArtBox") + pagedata.TrimBox = getbox(pageobject,"TrimBox") + pages[pagenumber] = pagedata + __xrefs__[pagedata] = pagereference + __cache__[pagereference] = pagedata + else + report_epdf("missing pagedata for page %i, case %i",pagenumber,1) + end + else + report_epdf("missing pagedata for page %i, case %i",pagenumber,2) + end + end + -- + -- pages.n = nofpages + -- + return pages +end + +local loaded = { } +local nofloaded = 0 + +function lpdf_epdf.load(filename,userpassword,ownerpassword,fromstring) + local document = loaded[filename] + if not document then + statistics.starttiming(lpdf_epdf) + local __data__ + local __file__ + if fromstring then + __data__ = pdfnew(filename,#filename) + elseif pdfopenfile then + __data__ = pdfopenfile(ioopen(filename,"rb")) + else + __data__ = pdfopen(filename) + end + if __data__ then + if userpassword and getstatus(__data__) < 0 then + unencrypt(__data__,userpassword,nil) + end + if ownerpassword and getstatus(__data__) < 0 then + unencrypt(__data__,nil,ownerpassword) + end + if getstatus(__data__) < 0 then + report_epdf("the document is encrypted, provide proper passwords",getstatus(__data__)) + __data__ = false + end + if __data__ then + document = { + filename = filename, + nofcopied = 0, + copied = { }, + __cache__ = { }, + __xrefs__ = { }, + __fonts__ = { }, + __copied__ = { }, + __data__ = __data__, + } + document.Catalog = some_dictionary(getcatalog(__data__),document) + document.Info = some_dictionary(getinfo(__data__),document) + document.Trailer = some_dictionary(gettrailer(__data__),document) + -- + setmetatableindex(document,resolve) + -- + document.majorversion, document.minorversion = getversion(__data__) + -- + document.nofpages = getnofpages(__data__) + else + document = false + end + else + document = false + end + loaded[filename] = document + loaded[document] = document + statistics.stoptiming(lpdf_epdf) + -- print(statistics.elapsedtime(lpdf_epdf)) + end + if document then + nofloaded = nofloaded + 1 + end + return document or nil +end + +function lpdf_epdf.unload(filename) + if type(filename) == "table" then + filename = filename.filename + end + if type(filename) == "string" then + local document = loaded[filename] + if document then + loaded[document] = nil + loaded[filename] = nil + pdfclose(document.__data__) + end + end +end + +-- for k, v in expanded(t) do + +local function expanded(t) + local function iterator(raw,k) + local k, v = next(raw,k) + if v then + return k, t[k] + end + end + return iterator, t.__raw__, nil +end + +---------.expand = expand +lpdf_epdf.expanded = expanded + +-- we could resolve the text stream in one pass if we directly handle the +-- font but why should we complicate things + +local spaces = lpegpatterns.whitespace^1 +local optspaces = lpegpatterns.whitespace^0 +local comment = P("%") * (1 - lpegpatterns.newline)^0 +local numchar = P("\\")/"" * (R("09")^3/function(s) return char(tonumber(s,8)) end) + + P("\\") * P(1) +local key = P("/") * C(R("AZ","az","09","__")^1) +local number = Ct(Cc("number") * (lpegpatterns.number/tonumber)) +local keyword = Ct(Cc("name") * key) +local operator = C((R("AZ","az")+P("*")+P("'")+P('"'))^1) + +local grammar = P { "start", + start = (comment + keyword + number + V("dictionary") + V("array") + V("hexstring") + V("decstring") + spaces)^1, + keyvalue = key * optspaces * V("start"), + array = Ct(Cc("array") * P("[") * Ct(V("start")^1) * P("]")), + dictionary = Ct(Cc("dict") * P("<<") * Ct(V("keyvalue")^1) * P(">>")), + hexstring = Ct(Cc("hex") * P("<") * Cs(( 1-P(">"))^1) * P(">")), + decstring = Ct(Cc("dec") * P("(") * Cs((numchar+1-(P")"))^1) * P(")")), -- untested +} + +local operation = Ct(grammar^1 * operator) +local parser = Ct((operation + P(1))^1) + +-- todo: speed this one up + +local numchar = P("\\") * (R("09")^3 + P(1)) +local number = lpegpatterns.number +local keyword = P("/") * R("AZ","az","09","__")^1 +local operator = (R("AZ","az")+P("*")+P("'")+P('"'))^1 + +local skipstart = P("BDC") + P("BMC") + P("DP") + P("MP") +local skipstop = P("EMC") +local skipkeep = P("/ActualText") + +local grammar = P { "skip", + start = keyword + number + V("dictionary") + V("array") + V("hexstring") + V("decstring") + spaces, + keyvalue = optspaces * (keyword * optspaces * V("start") * optspaces)^1, + xeyvalue = optspaces * ((keyword - skipkeep) * optspaces * V("start") * optspaces)^1, + array = P("[") * V("start")^0 * P("]"), + dictionary = P("<<") * V("keyvalue")^0 * P(">>"), + xictionary = P("<<") * V("xeyvalue")^0 * P(">>"), + hexstring = P("<") * ( 1-P(">"))^0 * P(">"), + decstring = P("(") * (numchar+1-(P")"))^0 * P(")"), + skip = (optspaces * ( keyword * optspaces * V("xictionary") * optspaces * skipstart + skipstop) / "") + + V("start") + + operator +} + +local stripper = Cs((grammar + P(1))^1) + +function lpdf_epdf.parsecontent(str) + return lpegmatch(parser,str) +end + +function lpdf_epdf.stripcontent(str) + if find(str,"EMC") then + return lpegmatch(stripper,str) + else + return str + end +end + +-- beginbfrange : <start> <stop> <firstcode> +-- <start> <stop> [ <firstsequence> <firstsequence> <firstsequence> ] +-- beginbfchar : <code> <newcodes> + +local fromsixteen = lpdf.fromsixteen -- maybe inline the lpeg ... but not worth it + +local function f_bfchar(t,a,b) + t[tonumber(a,16)] = fromsixteen(b) +end + +local function f_bfrange_1(t,a,b,c) + print("todo 1",a,b,c) + -- c is string + -- todo t[tonumber(a,16)] = fromsixteen(b) +end + +local function f_bfrange_2(t,a,b,c) + print("todo 2",a,b,c) + -- c is table + -- todo t[tonumber(a,16)] = fromsixteen(b) +end + +local optionals = spaces^0 +local hexstring = optionals * P("<") * C((1-P(">"))^1) * P(">") +local bfchar = Carg(1) * hexstring * hexstring / f_bfchar +local bfrange = Carg(1) * hexstring * hexstring * hexstring / f_bfrange_1 + + Carg(1) * hexstring * hexstring * optionals * P("[") * Ct(hexstring^1) * optionals * P("]") / f_bfrange_2 +local fromunicode = ( + P("beginbfchar" ) * bfchar ^1 * optionals * P("endbfchar" ) + + P("beginbfrange") * bfrange^1 * optionals * P("endbfrange") + + spaces + + P(1) +)^1 * Carg(1) + +local function analyzefonts(document,resources) -- unfinished, see mtx-pdf for better code + local fonts = document.__fonts__ + if resources then + local fontlist = resources.Font + if fontlist then + for id, data in expanded(fontlist) do + if not fonts[id] then + -- a quick hack ... I will look into it more detail if I find a real + -- -application for it + local tounicode = data.ToUnicode() + if tounicode then + tounicode = lpegmatch(fromunicode,tounicode,1,{}) + end + fonts[id] = { + tounicode = type(tounicode) == "table" and tounicode or { } + } + setmetatableindex(fonts[id],"self") + end + end + end + end + return fonts +end + +lpdf_epdf.analyzefonts = analyzefonts + +local more = 0 +local unic = nil -- cheaper than passing each time as Carg(1) + +local p_hex_to_utf = C(4) / function(s) -- needs checking ! + local now = tonumber(s,16) + if more > 0 then + now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000 -- the 0x10000 smells wrong + more = 0 + return unic[now] or utfchar(now) + elseif now >= 0xD800 and now <= 0xDBFF then + more = now + -- return "" + else + return unic[now] or utfchar(now) + end +end + +local p_dec_to_utf = C(1) / function(s) -- needs checking ! + local now = byte(s) + return unic[now] or utfchar(now) +end + +local p_hex_to_utf = P(true) / function() more = 0 end * Cs(p_hex_to_utf^1) +local p_dec_to_utf = P(true) / function() more = 0 end * Cs(p_dec_to_utf^1) + +function lpdf_epdf.getpagecontent(document,pagenumber) + + local page = document.pages[pagenumber] + + if not page then + return + end + + local fonts = analyzefonts(document,page.Resources) + + local content = page.Contents() or "" + local list = lpegmatch(parser,content) + local font = nil + -- local unic = nil + + for i=1,#list do + local entry = list[i] + local size = #entry + local operator = entry[size] + if operator == "Tf" then + font = fonts[entry[1][2]] + unic = font and font.tounicode or { } + elseif operator == "TJ" then + local data = entry[1] -- { "array", { ... } } + local list = data[2] -- { { ... }, { ... } } + for i=1,#list do + local li = list[i] +-- if type(li) == "table" then + local kind = li[1] + if kind == "hex" then + list[i] = lpegmatch(p_hex_to_utf,li[2]) + elseif kind == "string" then + list[i] = lpegmatch(p_dec_to_utf,li[2]) + else + list[i] = li[2] -- kern + end +-- else +-- -- kern +-- end + end + elseif operator == "Tj" or operator == "'" or operator == '"' then + -- { string, Tj } { string, ' } { n, m, string, " } + local data = entry[size-1] + local list = data[2] + local kind = list[1] + if kind == "hex" then + list[2] = lpegmatch(p_hex_to_utf,li[2]) + elseif kind == "string" then + list[2] = lpegmatch(p_dec_to_utf,li[2]) + end + end + end + + unic = nil -- can be collected + + return list + +end + +-- This is also an experiment. When I really need it I can improve it, for instance +-- with proper position calculating. It might be usefull for some search or so. + +local softhyphen = utfchar(0xAD) .. "$" +local linefactor = 1.3 + +function lpdf_epdf.contenttotext(document,list) -- maybe signal fonts + local last_y = 0 + local last_f = 0 + local text = { } + local last = 0 + + for i=1,#list do + local entry = list[i] + local size = #entry + local operator = entry[size] + if operator == "Tf" then + last_f = entry[2][2] -- size + elseif operator == "TJ" then + local data = entry[1] -- { "array", { ... } } + local list = data[2] -- { { ... }, { ... } } + for i=1,#list do + local li = list[i] + local kind = type(li) + if kind == "string" then + last = last + 1 + text[last] = li + elseif kind == "number" and li < -50 then + last = last + 1 + text[last] = " " + end + end + elseif operator == "Tj" then + last = last + 1 + local li = entry[size-1] + local kind = type(li) + if kind == "string" then + last = last + 1 + text[last] = li + end + elseif operator == "cm" or operator == "Tm" then + local data = entry + local ty = entry[6][2] + local dy = abs(last_y - ty) + if dy > linefactor*last_f then + if last > 0 then + if find(text[last],softhyphen,1,true) then + -- ignore + else + last = last + 1 + text[last] = "\n" + end + end + end + last_y = ty + end + end + + return concat(text) +end + +function lpdf_epdf.getstructure(document,list) -- just a test + local depth = 0 + for i=1,#list do + local entry = list[i] + local size = #entry + local operator = entry[size] + if operator == "BDC" then + report_epdf("%w%s : %s",depth,entry[1] or "?",entry[2] and entry[2].MCID or "?") + depth = depth + 1 + elseif operator == "EMC" then + depth = depth - 1 + elseif operator == "TJ" then + local list = entry[1] + for i=1,#list do + local li = list[i] + if type(li) == "string" then + report_epdf("%w > %s",depth,li) + elseif li < -50 then + report_epdf("%w >",depth,li) + end + end + elseif operator == "Tj" then + report_epdf("%w > %s",depth,entry[size-1]) + end + end +end + +if images then do + + -- This can be made a bit faster (just get raw data and pass it) but I will + -- do that later. In the end the benefit is probably neglectable. + + local recompress = false + local stripmarked = false + + local copydictionary = nil + local copyarray = nil + + local pdfreference = lpdf.reference + local pdfconstant = lpdf.constant + local pdfarray = lpdf.array + local pdfdictionary = lpdf.dictionary + local pdfnull = lpdf.null + local pdfliteral = lpdf.literal + + local pdfreserveobject + local shareobjectreference + local pdfflushobject + local pdfflushstreamobject + + updaters.register("backend.update.lpdf",function() + pdfreserveobject = lpdf.reserveobject + shareobjectreference = lpdf.shareobjectreference + pdfflushobject = lpdf.flushobject + pdfflushstreamobject = lpdf.flushstreamobject + end) + + local report = logs.reporter("backend","xobjects") + + local factor = 65536 / (7200/7227) -- 1/number.dimenfactors.bp + + local createimage = images.create + + directives.register("graphics.pdf.recompress", function(v) recompress = v end) + directives.register("graphics.pdf.stripmarked", function(v) stripmarked = v end) + + local function scaledbbox(b) + return { b[1]*factor, b[2]*factor, b[3]*factor, b[4]*factor } + end + + local codecs = { + ASCIIHexDecode = true, + ASCII85Decode = true, + RunLengthDecode = true, + FlateDecode = true, + LZWDecode = true, + } + + local function deepcopyobject(xref,copied,value) + -- no need for tables, just nested loop with obj + local objnum = xref[value] + if objnum then + local usednum = copied[objnum] + if usednum then + -- report("%s object %i is reused",kind,objnum) + else + usednum = pdfreserveobject() + copied[objnum] = usednum + local entry = value + local kind = entry.__type__ + if kind == array_object_code then + local a = copyarray(xref,copied,entry) + pdfflushobject(usednum,tostring(a)) + elseif kind == dictionary_object_code then + local d = copydictionary(xref,copied,entry) + pdfflushobject(usednum,tostring(d)) + elseif kind == stream_object_code then + local d = copydictionary(xref,copied,entry) + local filter = d.Filter + if filter and codecs[filter] and recompress then + -- recompress + d.Filter = nil + d.Length = nil + d.DecodeParms = nil -- relates to filter + d.DL = nil -- needed? + local s = entry() -- get uncompressed stream + pdfflushstreamobject(s,d,true,usednum) -- compress stream + else + -- keep as-is, even Length which indicates the + -- decompressed length + local s = entry(false) -- get compressed stream + -- pdfflushstreamobject(s,d,false,usednum,true) -- don't compress stream + pdfflushstreamobject(s,d,"raw",usednum) -- don't compress stream + end + else + local t = type(value) + if t == "string" then + value = pdfconstant(value) + elseif t == "table" then + local kind = value[1] + local entry = value[2] + if kind == name_object_code then + value = pdfconstant(entry) + elseif kind == string_object_code then + value = pdfliteral(entry,value[3]) + elseif kind == null_object_code then + value = pdfnull() + elseif kind == reference_object_code then + value = deepcopyobject(xref,copied,entry) + elseif entry == nil then + value = pdfnull() + else + value = tostring(entry) + end + end + pdfflushobject(usednum,value) + end + end + return pdfreference(usednum) + elseif kind == stream_object_code then + report("stream not done: %s", objectcodes[kind] or "?") + else + report("object not done: %s", objectcodes[kind] or "?") + end + end + + local function copyobject(xref,copied,object,key,value) + if not value then + value = object.__raw__[key] + end + local t = type(value) + if t == "string" then + return pdfconstant(value) + elseif t ~= "table" then + return value + end + local kind = value[1] + if kind == name_object_code then + return pdfconstant(value[2]) + elseif kind == string_object_code then + return pdfliteral(value[2],value[3]) + elseif kind == array_object_code then + return copyarray(xref,copied,object[key]) + elseif kind == dictionary_object_code then + return copydictionary(xref,copied,object[key]) + elseif kind == null_object_code then + return pdfnull() + elseif kind == reference_object_code then + -- expand + return deepcopyobject(xref,copied,object[key]) + else + report("weird: %s", objecttypes[kind] or "?") + end + end + + copyarray = function (xref,copied,object) + local target = pdfarray() + local source = object.__raw__ + for i=1,#source do + target[i] = copyobject(xref,copied,object,i,source[i]) + end + return target + end + + local plugins = nil + + -- Sorting the hash slows down upto 5% bit but it is still as fast as the C + -- code. We could loop over the index instead but sorting might be nicer in + -- the end. + + copydictionary = function (xref,copied,object) + local target = pdfdictionary() + local source = object.__raw__ + -- for key, value in next, source do + for key, value in sortedhash(source) do + if plugins then + local p = plugins[key] + if p then + target[key] = p(xref,copied,object,key,value,copyobject) -- maybe a table of methods + else + target[key] = copyobject(xref,copied,object,key,value) + end + else + target[key] = copyobject(xref,copied,object,key,value) + end + end + return target + end + + -- local function copyresources(pdfdoc,xref,copied,pagedata) + -- local Resources = pagedata.Resources + -- if Resources then + -- local r = pdfreserveobject() + -- local d = copydictionary(xref,copied,Resources) + -- pdfflushobject(r,tostring(d)) + -- return pdfreference(r) + -- end + -- end + + local function copyresources(pdfdoc,xref,copied,pagedata) + local Resources = pagedata.Resources + -- + -- -- This needs testing: + -- + -- if not Resources then + -- local Parent = page.Parent + -- while (Parent and (Parent.__type__ == dictionary_object_code or Parent.__type__ == reference_object_code) do + -- Resources = Parent.Resources + -- if Resources then + -- break + -- end + -- Parent = Parent.Parent + -- end + -- end + if Resources then + local d = copydictionary(xref,copied,Resources) + return shareobjectreference(d) + end + end + + local openpdf = lpdf_epdf.load + local closepdf = lpdf_epdf.unload + + -- todo: keep track of already open files + + local function newpdf(str,userpassword,ownerpassword) + return openpdf(str,userpassword,ownerpassword,true) + end + + local sizes = { + crop = "CropBox", + media = "MediaBox", + bleed = "BleedBox", + art = "ArtBox", + trim = "TrimBox", + } + + local function querypdf(pdfdoc,pagenumber,size) + if pdfdoc then + if not pagenumber then + pagenumber = 1 + end + local root = pdfdoc.Catalog + local page = pdfdoc.pages[pagenumber] + if page then + local sizetag = sizes[size or "crop"] or sizes.crop + local mediabox = page.MediaBox or { 0, 0, 0, 0 } + local cropbox = page[sizetag] or mediabox + return { + filename = pdfdoc.filename, + pagenumber = pagenumber, + nofpages = pdfdoc.nofpages, + boundingbox = scaledbbox(cropbox), + cropbox = cropbox, + mediabox = mediabox, + bleedbox = page.BleedBox or cropbox, + trimbox = page.TrimBox or cropbox, + artbox = page.ArtBox or cropbox, + rotation = page.Rotate or 0, + xsize = cropbox[3] - cropbox[1], + ysize = cropbox[4] - cropbox[2], + } + end + end + end + + local function copypage(pdfdoc,pagenumber,attributes,compact,width,height,attr) + if pdfdoc then + local root = pdfdoc.Catalog + local page = pdfdoc.pages[pagenumber or 1] + local pageinfo = querypdf(pdfdoc,pagenumber) + local contents = page.Contents + if contents then + local xref = pdfdoc.__xrefs__ + local copied = pdfdoc.__copied__ + if compact and lpdf_epdf.plugin then + plugins = lpdf_epdf.plugin(pdfdoc,xref,copied,page) + end + local xobject = pdfdictionary { + Type = pdfconstant("XObject"), + Subtype = pdfconstant("Form"), + FormType = 1, + Group = copyobject(xref,copied,page,"Group"), + LastModified = copyobject(xref,copied,page,"LastModified"), + Metadata = copyobject(xref,copied,page,"Metadata"), + PieceInfo = copyobject(xref,copied,page,"PieceInfo"), + Resources = copyresources(pdfdoc,xref,copied,page), + SeparationInfo = copyobject(xref,copied,page,"SeparationInfo"), + } + attr + if attributes then + for k, v in expanded(attributes) do + page[k] = v -- maybe nested + end + end + local content = "" + local nolength = nil + local ctype = contents.__type__ + -- we always recompress because image object streams can not be + -- influenced (yet) + if ctype == stream_object_code then + if stripmarked then + content = contents() -- uncompressed + local stripped = lpdf_epdf.stripcontent(content) + if stripped ~= content then + -- report("%i bytes stripped on page %i",#content-#stripped,pagenumber or 1) + content = stripped + end + elseif recompress then + content = contents() -- uncompressed + else + local Filter = copyobject(xref,copied,contents,"Filter") + local Length = copyobject(xref,copied,contents,"Length") + if Length and Filter then + nolength = true + xobject.Length = Length + xobject.Filter = Filter + content = contents(false) -- uncompressed + else + content = contents() -- uncompressed + end + end + elseif ctype == array_object_code then + content = { } + for i=1,#contents do + content[i] = contents[i]() -- uncompressed + end + content = concat(content," ") + end + -- still not nice: we double wrap now + plugins = nil + local rotation = pageinfo.rotation + local boundingbox = pageinfo.boundingbox + local transform = nil + if rotation == 90 then + transform = 3 + elseif rotation == 180 then + transform = 2 + elseif rotation == 270 then + transform = 1 + elseif rotation > 1 and rotation < 4 then + transform = rotation + end + xobject.BBox = pdfarray { + boundingbox[1] * bpfactor, + boundingbox[2] * bpfactor, + boundingbox[3] * bpfactor, + boundingbox[4] * bpfactor, + } + -- maybe like bitmaps + return createimage { -- beware: can be a img.new or a dummy + bbox = boundingbox, + transform = transform, + nolength = nolength, + nobbox = true, + notype = true, + stream = content, -- todo: no compress, pass directly also length, filter etc + attr = xobject(), + kind = images.types.stream, + } + else + -- maybe report an error + end + end + end + + lpdf_epdf.image = { + open = openpdf, + close = closepdf, + new = newpdf, + query = querypdf, + copy = copypage, + } + +-- lpdf.injectors.pdf = function(specification) +-- local d = lpdf_epdf.load(specification.filename) +-- print(d) +-- end + + +end end + +-- local d = lpdf_epdf.load("e:/tmp/oeps.pdf") +-- inspect(d) +-- inspect(d.Catalog.Lang) +-- inspect(d.Catalog.OCProperties.D.AS[1].Event) +-- inspect(d.Catalog.Metadata()) +-- inspect(d.Catalog.Pages.Kids[1]) +-- inspect(d.layers) +-- inspect(d.pages) +-- inspect(d.destinations) +-- inspect(lpdf_epdf.getpagecontent(d,1)) +-- inspect(lpdf_epdf.contenttotext(document,lpdf_epdf.getpagecontent(d,1))) +-- inspect(lpdf_epdf.getstructure(document,lpdf_epdf.getpagecontent(d,1))) diff --git a/tex/context/base/mkxl/lpdf-ren.lmt b/tex/context/base/mkxl/lpdf-ren.lmt new file mode 100644 index 000000000..8aa61e1dc --- /dev/null +++ b/tex/context/base/mkxl/lpdf-ren.lmt @@ -0,0 +1,399 @@ +if not modules then modules = { } end modules ['lpdf-ren'] = { + version = 1.001, + 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" +} + +-- rendering + +local tostring, tonumber, next = tostring, tonumber, next +local concat = table.concat +local formatters = string.formatters +local settings_to_array = utilities.parsers.settings_to_array +local getrandom = utilities.randomizer.get + +local backends, lpdf, nodes, node = backends, lpdf, nodes, node + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations +local viewerlayers = attributes.viewerlayers + +local references = structures.references + +references.executers = references.executers or { } +local executers = references.executers + +local variables = interfaces.variables + +local v_no = variables.no +local v_yes = variables.yes +local v_start = variables.start +local v_stop = variables.stop +local v_reset = variables.reset +local v_auto = variables.auto +local v_random = variables.random + +local pdfconstant = lpdf.constant +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfreference = lpdf.reference + +local pdfflushobject +local pdfreserveobject + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfreserveobject = lpdf.reserveobject +end) + +local addtopageattributes = lpdf.addtopageattributes +local addtopageresources = lpdf.addtopageresources +local addtocatalog = lpdf.addtocatalog + +local escaped = lpdf.escaped + +local nuts = nodes.nuts +local copy_node = nuts.copy + +local nodepool = nuts.pool +local register = nodepool.register +local pageliteral = nodepool.pageliteral + +local pdf_ocg = pdfconstant("OCG") +local pdf_ocmd = pdfconstant("OCMD") +local pdf_off = pdfconstant("OFF") +local pdf_on = pdfconstant("ON") +local pdf_view = pdfconstant("View") +local pdf_design = pdfconstant("Design") +local pdf_toggle = pdfconstant("Toggle") +local pdf_setocgstate = pdfconstant("SetOCGState") + +local pdf_print = { + [v_yes] = pdfdictionary { PrintState = pdf_on }, + [v_no ] = pdfdictionary { PrintState = pdf_off }, +} + +local pdf_intent = { + [v_yes] = pdf_view, + [v_no] = pdf_design, +} + +local pdf_export = { + [v_yes] = pdf_on, + [v_no] = pdf_off, +} + +-- We can have references to layers before they are places, for instance from +-- hide and vide actions. This is why we need to be able to force usage of layers +-- at several moments. + +-- management + +local pdfln, pdfld = { }, { } +local textlayers, hidelayers, videlayers = pdfarray(), pdfarray(), pdfarray() +local pagelayers, pagelayersreference, cache = nil, nil, { } +local alphabetic = { } + +local escapednames = table.setmetatableindex(function(t,k) + local v = escaped(k) + t[k] = v + return v +end) + +local specifications = { } +local initialized = { } + +function codeinjections.defineviewerlayer(specification) + if viewerlayers.supported and textlayers then + local tag = specification.tag + if not specifications[tag] then + specifications[tag] = specification + end + end +end + +local function useviewerlayer(name) -- move up so that we can use it as local + if not environment.initex and not initialized[name] then + local specification = specifications[name] + if specification then + specifications[name] = nil -- or not + initialized [name] = true + if not pagelayers then + pagelayers = pdfdictionary() + pagelayersreference = pdfreserveobject() + end + local tag = specification.tag + -- todo: reserve + local nn = pdfreserveobject() + local nr = pdfreference(nn) + local nd = pdfdictionary { + Type = pdf_ocg, + Name = specification.title or "unknown", + Usage = { + Intent = pdf_intent[specification.editable or v_yes], -- disable layer hiding by user (useless) + Print = pdf_print [specification.printable or v_yes], -- printable or not + Export = pdf_export[specification.export or v_yes], -- export or not + }, + } + cache[#cache+1] = { nn, nd } + pdfln[tag] = nr -- was n + local dn = pdfreserveobject() + local dr = pdfreference(dn) + local dd = pdfdictionary { + Type = pdf_ocmd, + OCGs = pdfarray { nr }, + } + cache[#cache+1] = { dn, dd } + pdfld[tag] = dr + textlayers[#textlayers+1] = nr + alphabetic[tag] = nr + if specification.visible == v_start then + videlayers[#videlayers+1] = nr + else + hidelayers[#hidelayers+1] = nr + end + pagelayers[escapednames[tag]] = dr -- check + else + -- todo: message + end + end +end + +codeinjections.useviewerlayer = useviewerlayer + +local function layerreference(name) + local r = pdfln[name] + if r then + return r + else + useviewerlayer(name) + return pdfln[name] + end +end + +lpdf.layerreference = layerreference -- also triggered when a hide or vide happens + +local function flushtextlayers() + if viewerlayers.supported then + if pagelayers then + pdfflushobject(pagelayersreference,pagelayers) + end + for i=1,#cache do + local ci = cache[i] + pdfflushobject(ci[1],ci[2]) + end + if textlayers and #textlayers > 0 then -- we can group them if needed, like: layout + local sortedlayers = { } + for k, v in table.sortedhash(alphabetic) do + sortedlayers[#sortedlayers+1] = v -- maybe do a proper numeric sort as well + end + local d = pdfdictionary { + OCGs = textlayers, + D = pdfdictionary { + Name = "Document", + -- Order = (viewerlayers.hasorder and textlayers) or nil, + Order = (viewerlayers.hasorder and sortedlayers) or nil, + ON = videlayers, + OFF = hidelayers, + BaseState = pdf_on, + AS = pdfarray { + pdfdictionary { + Category = pdfarray { pdfconstant("Print") }, + Event = pdfconstant("Print"), + OCGs = (viewerlayers.hasorder and sortedlayers) or nil, + } + }, + }, + } + addtocatalog("OCProperties",d) + textlayers = nil + end + end +end + +local function flushpagelayers() -- we can share these + if pagelayers then + addtopageresources("Properties",pdfreference(pagelayersreference)) -- we could cache this + end +end + +lpdf.registerpagefinalizer (flushpagelayers,"layers") +lpdf.registerdocumentfinalizer(flushtextlayers,"layers") + +local function setlayer(what,arguments) + -- maybe just a gmatch of even better, earlier in lpeg + arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments) + local state = pdfarray { what } + for i=1,#arguments do + local p = layerreference(arguments[i]) + if p then + state[#state+1] = p + end + end + return pdfdictionary { + S = pdf_setocgstate, + State = state, + } +end + +function executers.hidelayer (arguments) return setlayer(pdf_off, arguments) end +function executers.videlayer (arguments) return setlayer(pdf_on, arguments) end +function executers.togglelayer(arguments) return setlayer(pdf_toggle,arguments) end + +-- injection + +local f_bdc = formatters["/OC /%s BDC"] +local s_emc = "EMC" + +function codeinjections.startlayer(name) -- used in mp + if not name then + name = "unknown" + end + useviewerlayer(name) + return f_bdc(escapednames[name]) +end + +function codeinjections.stoplayer(name) -- used in mp + return s_emc +end + +local cache = { } +local stop = nil + +function nodeinjections.startlayer(name) + local c = cache[name] + if not c then + useviewerlayer(name) + c = register(pageliteral(f_bdc(escapednames[name]))) + cache[name] = c + end + return copy_node(c) +end + +function nodeinjections.stoplayer() + if not stop then + stop = register(pageliteral(s_emc)) + end + return copy_node(stop) +end + +-- experimental stacker code (slow, can be optimized): !!!! TEST CODE !!!! + +local values = viewerlayers.values +local startlayer = codeinjections.startlayer +local stoplayer = codeinjections.stoplayer + +function nodeinjections.startstackedlayer(s,t,first,last) + local r = { } + for i=first,last do + r[#r+1] = startlayer(values[t[i]]) + end + r = concat(r," ") + return pageliteral(r) +end + +function nodeinjections.stopstackedlayer(s,t,first,last) + local r = { } + for i=last,first,-1 do + r[#r+1] = stoplayer() + end + r = concat(r," ") + return pageliteral(r) +end + +function nodeinjections.changestackedlayer(s,t1,first1,last1,t2,first2,last2) + local r = { } + for i=last1,first1,-1 do + r[#r+1] = stoplayer() + end + for i=first2,last2 do + r[#r+1] = startlayer(values[t2[i]]) + end + r = concat(r," ") + return pageliteral(r) +end + +-- transitions + +local pagetransitions = { + {"split","in","vertical"}, {"split","in","horizontal"}, + {"split","out","vertical"}, {"split","out","horizontal"}, + {"blinds","horizontal"}, {"blinds","vertical"}, + {"box","in"}, {"box","out"}, + {"wipe","east"}, {"wipe","west"}, {"wipe","north"}, {"wipe","south"}, + {"dissolve"}, + {"glitter","east"}, {"glitter","south"}, + {"fly","in","east"}, {"fly","in","west"}, {"fly","in","north"}, {"fly","in","south"}, + {"fly","out","east"}, {"fly","out","west"}, {"fly","out","north"}, {"fly","out","south"}, + {"push","east"}, {"push","west"}, {"push","north"}, {"push","south"}, + {"cover","east"}, {"cover","west"}, {"cover","north"}, {"cover","south"}, + {"uncover","east"}, {"uncover","west"}, {"uncover","north"}, {"uncover","south"}, + {"fade"}, +} + +local mapping = { + split = { "S" , pdfconstant("Split") }, + blinds = { "S" , pdfconstant("Blinds") }, + box = { "S" , pdfconstant("Box") }, + wipe = { "S" , pdfconstant("Wipe") }, + dissolve = { "S" , pdfconstant("Dissolve") }, + glitter = { "S" , pdfconstant("Glitter") }, + replace = { "S" , pdfconstant("R") }, + fly = { "S" , pdfconstant("Fly") }, + push = { "S" , pdfconstant("Push") }, + cover = { "S" , pdfconstant("Cover") }, + uncover = { "S" , pdfconstant("Uncover") }, + fade = { "S" , pdfconstant("Fade") }, + horizontal = { "Dm" , pdfconstant("H") }, + vertical = { "Dm" , pdfconstant("V") }, + ["in"] = { "M" , pdfconstant("I") }, + out = { "M" , pdfconstant("O") }, + east = { "Di" , 0 }, + north = { "Di" , 90 }, + west = { "Di" , 180 }, + south = { "Di" , 270 }, +} + +local last = 0 + +-- n: number, "stop", "reset", "random", "a,b,c" delay: number, "none" + +function codeinjections.setpagetransition(specification) + local n, delay = specification.n, specification.delay + if not n or n == "" then + return -- let's forget about it + elseif n == v_auto then + if last >= #pagetransitions then + last = 0 + end + n = last + 1 + elseif n == v_stop then + return + elseif n == v_reset then + last = 0 + return + elseif n == v_random then + n = getrandom("transition",1,#pagetransitions) + else + n = tonumber(n) + end + local t = n and pagetransitions[n] or pagetransitions[1] + if not t then + t = settings_to_array(n) + end + if t and #t > 0 then + local d = pdfdictionary() + for i=1,#t do + local m = mapping[t[i]] + d[m[1]] = m[2] + end + delay = tonumber(delay) + if delay and delay > 0 then + addtopageattributes("Dur",delay) + end + addtopageattributes("Trans",d) + end +end diff --git a/tex/context/base/mkxl/lpdf-res.lmt b/tex/context/base/mkxl/lpdf-res.lmt new file mode 100644 index 000000000..d3c591343 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-res.lmt @@ -0,0 +1,41 @@ +if not modules then modules = { } end modules ['lpdf-res'] = { + version = 1.001, + 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" +} + +local codeinjections = backends.codeinjections + +local nuts = nodes.nuts +local tonut = nodes.tonut + +local setwhd = nuts.setwhd +local setlist = nuts.setlist + +local new_hlist = nuts.pool.hlist + +local boxresources = tex.boxresources +local saveboxresource = boxresources.save +local useboxresource = boxresources.use +local getboxresourcedimensions = boxresources.getdimensions + +local pdfcollectedresources = lpdf.collectedresources + +function codeinjections.registerboxresource(n,offset) + local r = saveboxresource(n,nil,pdfcollectedresources(),true,0,offset or 0) -- direct, todo: accept functions as attr/resources + return r +end + +function codeinjections.restoreboxresource(index) + local hbox = new_hlist() + local list, wd, ht, dp = useboxresource(index) + setlist(hbox,tonut(list)) + setwhd(hbox,wd,ht,dp) + return hbox -- so we return a nut ! +end + +function codeinjections.boxresourcedimensions(index) + return getboxresourcedimensions(index) +end diff --git a/tex/context/base/mkxl/lpdf-tag.lmt b/tex/context/base/mkxl/lpdf-tag.lmt new file mode 100644 index 000000000..5cc2f5012 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-tag.lmt @@ -0,0 +1,740 @@ +if not modules then modules = { } end modules ['lpdf-tag'] = { + version = 1.001, + comment = "companion to lpdf-tag.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files" +} + +local next, type = next, type +local format, match, gmatch = string.format, string.match, string.gmatch +local concat, sortedhash = table.concat, table.sortedhash +local lpegmatch, P, S, C = lpeg.match, lpeg.P, lpeg.S, lpeg.C +local settings_to_hash = utilities.parsers.settings_to_hash +local formatters = string.formatters + +local trace_tags = false trackers.register("structures.tags", function(v) trace_tags = v end) +local trace_info = false trackers.register("structures.tags.info", function(v) trace_info = v end) + +local report_tags = logs.reporter("backend","tags") + +local backends = backends +local lpdf = lpdf +local nodes = nodes + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections + +local enableaction = nodes.tasks.enableaction +local disableaction = nodes.tasks.disableaction + +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfboolean = lpdf.boolean +local pdfconstant = lpdf.constant +local pdfreference = lpdf.reference +local pdfunicode = lpdf.unicode +local pdfmakenametree = lpdf.makenametree + +local addtocatalog = lpdf.addtocatalog +local addtopageattributes = lpdf.addtopageattributes + +local pdfflushobject +local pdfreserveobject +local pdfpagereference + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfreserveobject = lpdf.reserveobject + pdfpagereference = lpdf.pagereference +end) + +local texgetcount = tex.getcount + +local nodecodes = nodes.nodecodes + +local hlist_code = nodecodes.hlist +local vlist_code = nodecodes.vlist +local glyph_code = nodecodes.glyph + +local a_tagged = attributes.private('tagged') +local a_image = attributes.private('image') + +local nuts = nodes.nuts + +local nodepool = nuts.pool +local pageliteral = nodepool.pageliteral +local register = nodepool.register + +local getid = nuts.getid +local getattr = nuts.getattr +local getprev = nuts.getprev +local getnext = nuts.getnext +local getlist = nuts.getlist +local getchar = nuts.getchar + +local setlink = nuts.setlink +local setlist = nuts.setlist + +local copy_node = nuts.copy +local tosequence = nuts.tosequence + +local nextnode = nuts.traversers.node + +local structure_kids -- delayed +local structure_ref -- delayed +local parent_ref -- delayed +local root -- delayed +local names = { } +local tree = { } +local elements = { } + +local structurestags = structures.tags +local taglist = structurestags.taglist +local specifications = structurestags.specifications +local usedlabels = structurestags.labels +local properties = structurestags.properties +local usewithcare = structurestags.usewithcare + +local usedmapping = { } + +----- tagsplitter = structurestags.patterns.splitter + +local embeddedtags = false -- true will id all, for tracing, otherwise table +local f_tagid = formatters["%s-%04i"] +local embeddedfilelist = pdfarray() -- /AF crap + +-- for testing, not that it was ever used: + +directives.register("structures.tags.embed",function(v) + if type(v) == "string" then + if type(embeddedtags) ~= "table" then + embeddedtags = { } + end + for s in gmatch(v,"([^, ]+)") do + embeddedtags[s] = true + end + elseif v and not embeddedtags then + embeddedtags = true + end +end) + +-- for old times sake, not that it was ever used: + +directives.register("structures.tags.embedmath",function(v) + if not v then + -- only enable + elseif embeddedtags == true then + -- already all tagged + elseif embeddedtags then + embeddedtags.math = true + else + embeddedtags = { math = true } + end +end) + +function codeinjections.maptag(original,target,kind) + mapping[original] = { target, kind or "inline" } +end + +-- mostly the same as the annotations tree + +local function finishstructure() + if root and #structure_kids > 0 then + local nums = pdfarray() + local n = 0 + for i=1,#tree do + n = n + 1 ; nums[n] = i - 1 + n = n + 1 ; nums[n] = pdfreference(pdfflushobject(tree[i])) + end + local parenttree = pdfdictionary { + Nums = nums + } + local idtree = pdfmakenametree(names) + -- + local rolemap = pdfdictionary() + for k, v in next, usedmapping do + k = usedlabels[k] or k + local p = properties[k] + rolemap[k] = pdfconstant(p and p.pdf or "Span") -- or "Div" + end + local structuretree = pdfdictionary { + Type = pdfconstant("StructTreeRoot"), + K = pdfreference(pdfflushobject(structure_kids)), + ParentTree = pdfreference(pdfflushobject(parent_ref,parenttree)), + IDTree = idtree, + RoleMap = rolemap, -- sorted ? + } + pdfflushobject(structure_ref,structuretree) + addtocatalog("StructTreeRoot",pdfreference(structure_ref)) + -- + if lpdf.majorversion() == 1 then + local markinfo = pdfdictionary { + Marked = pdfboolean(true) or nil, + -- UserProperties = pdfboolean(true), -- maybe some day + -- Suspects = pdfboolean(true) or nil, + -- AF = #embeddedfilelist > 0 and pdfreference(pdfflushobject(embeddedfilelist)) or nil, + } + addtocatalog("MarkInfo",pdfreference(pdfflushobject(markinfo))) + end + -- + for fulltag, element in sortedhash(elements) do -- sorting is easier on comparing pdf + pdfflushobject(element.knum,element.kids) + end + end +end + +lpdf.registerdocumentfinalizer(finishstructure,"document structure") + +local index, pageref, pagenum, list = 0, nil, 0, nil + +local pdf_mcr = pdfconstant("MCR") +local pdf_struct_element = pdfconstant("StructElem") +local pdf_s = pdfconstant("S") + +local function initializepage() + index = 0 + pagenum = texgetcount("realpageno") + pageref = pdfreference(pdfpagereference(pagenum)) + list = pdfarray() + tree[pagenum] = list -- we can flush after done, todo +end + +local function finishpage() + -- flush what can be flushed + addtopageattributes("StructParents",pagenum-1) + -- there might be more + addtopageattributes("Tabs",s) +end + +-- here we can flush and free elements that are finished + +local pdf_userproperties = pdfconstant("UserProperties") + +-- /O /Table +-- /Headers [ ] + +local function makeattribute(t) + if t and next(t) then + local properties = pdfarray() + for k, v in sortedhash(t) do -- easier on comparing pdf + properties[#properties+1] = pdfdictionary { + N = pdfunicode(k), + V = pdfunicode(v), + } + end + return pdfdictionary { + O = pdf_userproperties, + P = properties, + } + end +end + +local function makeelement(fulltag,parent) + local specification = specifications[fulltag] + local tagname = specification.tagname + local tagnameused = tagname + local attributes = nil + if tagname == "ignore" then + return false + elseif tagname == "mstackertop" or tagname == "mstackerbot" or tagname == "mstackermid"then + -- TODO + return true + elseif tagname == "tabulatecell" then + local d = structurestags.gettabulatecell(fulltag) + if d and d.kind == 1 then + tagnameused = "tabulateheadcell" + end + elseif tagname == "tablecell" then + -- will become a plugin model + local d = structurestags.gettablecell(fulltag) + if d then + if d.kind == 1 then + tagnameused = "tableheadcell" + end + local rows = d.rows or 1 + local cols = d.columns or 1 + if rows > 1 or cols > 1 then + attributes = pdfdictionary { + O = pdfconstant("Table"), + RowSpan = rows > 1 and rows or nil, + ColSpan = cols > 1 and cols or nil, + } + end + + end + end + -- + local detail = specification.detail + local userdata = specification.userdata + -- + usedmapping[tagname] = true + -- + -- specification.attribute is unique + -- + local id = nil + local af = nil + if embeddedtags then + local tagindex = specification.tagindex + if embeddedtags == true or embeddedtags[tagname] then + id = f_tagid(tagname,tagindex) + af = job.fileobjreferences.collected[id] + if af then + local r = pdfreference(af) + af = pdfarray { r } + -- embeddedfilelist[#embeddedfilelist+1] = r + end + end + end + -- + local k = pdfarray() + local r = pdfreserveobject() + local t = usedlabels[tagnameused] or tagnameused + -- local a = nil + local d = pdfdictionary { + Type = pdf_struct_element, + S = pdfconstant(t), + ID = id, + T = detail and detail or nil, + P = parent.pref, + Pg = pageref, + K = pdfreference(r), + -- A = a and makeattribute(a) or nil, + A = attributes, + -- Alt = " Who cares ", + -- ActualText = " Hi Hans ", + AF = af, + } + local s = pdfreference(pdfflushobject(d)) + if id and names then + names[id] = s + end + local kids = parent.kids + kids[#kids+1] = s + local e = { + tag = t, + pref = s, + kids = k, + knum = r, + pnum = pagenum + } + elements[fulltag] = e + return e +end + +local f_BDC = formatters["/%s <</MCID %s>> BDC"] + +local function makecontent(parent,id,specification) + local tag = parent.tag + local kids = parent.kids + local last = index + if id == "image" then + local list = specification.taglist + local data = usewithcare.images[list[#list]] + local label = data and data.label + local d = pdfdictionary { + Type = pdf_mcr, + Pg = pageref, + MCID = last, + Alt = pdfunicode(label ~= "" and label or "image"), + } + kids[#kids+1] = d + elseif pagenum == parent.pnum then + kids[#kids+1] = last + else + local d = pdfdictionary { + Type = pdf_mcr, + Pg = pageref, + MCID = last, + } + -- kids[#kids+1] = pdfreference(pdfflushobject(d)) + kids[#kids+1] = d + end + -- + index = index + 1 + list[index] = parent.pref -- page related list + -- + return f_BDC(tag,last) +end + +local function makeignore(specification) + return "/Artifact BMC" +end + +-- no need to adapt head, as we always operate on lists + +local EMCliteral = nil +local visualize = nil + +function nodeinjections.addtags(head) + + if not EMCliteral then + EMCliteral = register(pageliteral("EMC")) + end + + local last = nil + local ranges = { } + local range = nil + + if not root then + structure_kids = pdfarray() + structure_ref = pdfreserveobject() + parent_ref = pdfreserveobject() + root = { pref = pdfreference(structure_ref), kids = structure_kids } + names = pdfarray() + end + + local function collectranges(head,list) + for n, id in nextnode, head do + if id == glyph_code then + -- maybe also disc +if getchar(n) ~= 0 then + local at = getattr(n,a_tagged) or false -- false: pagebody or so, so artifact + -- if not at then + -- range = nil + -- elseif ... + if last ~= at then + range = { at, "glyph", n, n, list } -- attr id start stop list + ranges[#ranges+1] = range + last = at + elseif range then + range[4] = n -- stop + end +end + elseif id == hlist_code or id == vlist_code then + local at = getattr(n,a_image) + if at then + local at = getattr(n,a_tagged) or false -- false: pagebody or so, so artifact + -- if not at then + -- range = nil + -- else + ranges[#ranges+1] = { at, "image", n, n, list } -- attr id start stop list + -- end + last = nil + else + local list = getlist(n) + if list then + collectranges(list,n) + end + end + end + end + end + + initializepage() + + collectranges(head) + + if trace_tags then + for i=1,#ranges do + local range = ranges[i] + local attr = range[1] + local id = range[2] + local start = range[3] + local stop = range[4] + local tags = taglist[attr] + if tags then -- not ok ... only first lines + report_tags("%s => %s : %05i % t",tosequence(start,start),tosequence(stop,stop),attr,tags.taglist) + end + end + end + + local top = nil + local noftop = 0 + + local function inject(start,stop,list,literal,left,right) + local prev = getprev(start) + if prev then + setlink(prev,literal) + end + if left then + setlink(literal,left,start) + else + setlink(literal,start) + end + if list and not prev then + setlist(list,literal) + end + local literal = copy_node(EMCliteral) + -- use insert instead: + local next = getnext(stop) + if next then + setlink(literal,next) + end + if right then + setlink(stop,right,literal) + else + setlink(stop,literal) + end + end + + for i=1,#ranges do + + local range = ranges[i] + local attr = range[1] + local id = range[2] + local start = range[3] + local stop = range[4] + local list = range[5] + + if attr then + + local specification = taglist[attr] + local taglist = specification.taglist + local noftags = #taglist + local common = 0 + local literal = nil + local ignore = false + + if top then + for i=1,noftags >= noftop and noftop or noftags do + if top[i] == taglist[i] then + common = i + else + break + end + end + end + + local prev = common > 0 and elements[taglist[common]] or root + + for j=common+1,noftags do + local tag = taglist[j] + local prv = elements[tag] or makeelement(tag,prev) + if prv == false then + -- ignore this one + prev = false + ignore = true + break + elseif prv == true then + -- skip this one + else + prev = prv + end + end + if prev then + literal = pageliteral(makecontent(prev,id,specification)) + elseif ignore then + literal = pageliteral(makeignore(specification)) + else + -- maybe also ignore or maybe better: comment or so + end + + if literal then + local left,right + if trace_info then + local name = specification.tagname + if name then + if not visualize then + visualize = nodes.visualizers.register("tags") + end + left = visualize(name) + right = visualize() + end + end + inject(start,stop,list,literal,left,right) + end + + top = taglist + noftop = noftags + + else + + local literal = pageliteral(makeignore(specification)) + + inject(start,stop,list,literal) + + end + + end + + finishpage() + + return head + +end + +-- variant: more structure but funny collapsing in viewer + +-- function nodeinjections.addtags(head) +-- +-- local last, ranges, range = nil, { }, nil +-- +-- local function collectranges(head,list) +-- for n, id in nextnode, head do +-- if id == glyph_code then +-- local at = getattr(n,a_tagged) +-- if not at then +-- range = nil +-- elseif last ~= at then +-- range = { at, "glyph", n, n, list } -- attr id start stop list +-- ranges[#ranges+1] = range +-- last = at +-- elseif range then +-- range[4] = n -- stop +-- end +-- elseif id == hlist_code or id == vlist_code then +-- local at = getattr(n,a_image) +-- if at then +-- local at = getattr(n,a_tagged) +-- if not at then +-- range = nil +-- else +-- ranges[#ranges+1] = { at, "image", n, n, list } -- attr id start stop list +-- end +-- last = nil +-- else +-- local nl = getlist(n) +-- collectranges(nl,n) +-- end +-- end +-- end +-- end +-- +-- initializepage() +-- +-- collectranges(head) +-- +-- if trace_tags then +-- for i=1,#ranges do +-- local range = ranges[i] +-- local attr = range[1] +-- local id = range[2] +-- local start = range[3] +-- local stop = range[4] +-- local tags = taglist[attr] +-- if tags then -- not ok ... only first lines +-- report_tags("%s => %s : %05i % t",tosequence(start,start),tosequence(stop,stop),attr,tags.taglist) +-- end +-- end +-- end +-- +-- local top = nil +-- local noftop = 0 +-- local last = nil +-- +-- for i=1,#ranges do +-- local range = ranges[i] +-- local attr = range[1] +-- local id = range[2] +-- local start = range[3] +-- local stop = range[4] +-- local list = range[5] +-- local specification = taglist[attr] +-- local taglist = specification.taglist +-- local noftags = #taglist +-- local tag = nil +-- local common = 0 +-- -- local prev = root +-- +-- if top then +-- for i=1,noftags >= noftop and noftop or noftags do +-- if top[i] == taglist[i] then +-- common = i +-- else +-- break +-- end +-- end +-- end +-- +-- local result = { } +-- local r = noftop - common +-- if r > 0 then +-- for i=1,r do +-- result[i] = "EMC" +-- end +-- end +-- +-- local prev = common > 0 and elements[taglist[common]] or root +-- +-- for j=common+1,noftags do +-- local tag = taglist[j] +-- local prv = elements[tag] or makeelement(tag,prev) +-- -- if prv == false then +-- -- -- ignore this one +-- -- prev = false +-- -- break +-- -- elseif prv == true then +-- -- -- skip this one +-- -- else +-- prev = prv +-- r = r + 1 +-- result[r] = makecontent(prev,id) +-- -- end +-- end +-- +-- if r > 0 then +-- local literal = pageliteral(concat(result,"\n")) +-- -- use insert instead: +-- local literal = pageliteral(result) +-- local prev = getprev(start) +-- if prev then +-- setlink(prev,literal) +-- end +-- setlink(literal,start) +-- if list and getlist(list) == start then +-- setlist(list,literal) +-- end +-- end +-- +-- top = taglist +-- noftop = noftags +-- last = stop +-- +-- end +-- +-- if last and noftop > 0 then +-- local result = { } +-- for i=1,noftop do +-- result[i] = "EMC" +-- end +-- local literal = pageliteral(concat(result,"\n")) +-- -- use insert instead: +-- local next = getnext(last) +-- if next then +-- setlink(literal,next) +-- end +-- setlink(last,literal) +-- end +-- +-- finishpage() +-- +-- return head +-- +-- end + +-- this belongs elsewhere (export is not pdf related) + +local permitted = true +local enabled = false + +function codeinjections.settaggingsupport(option) + if option == false then + if enabled then + disableaction("shipouts","structures.tags.handler") + disableaction("shipouts","nodes.handlers.accessibility") -- maybe not this one + disableaction("math","noads.handlers.tags") + enabled = false + end + if permitted then + if trace_tags then + report_tags("blocking structure tags") + end + permitted = false + end + end +end + +function codeinjections.enabletags() + if permitted and not enabled then + structures.tags.handler = nodeinjections.addtags + enableaction("shipouts","structures.tags.handler") + enableaction("shipouts","nodes.handlers.accessibility") + enableaction("math","noads.handlers.tags") + -- maybe also textblock + if trace_tags then + report_tags("enabling structure tags") + end + enabled = true + end +end diff --git a/tex/context/base/mkxl/lpdf-u3d.lmt b/tex/context/base/mkxl/lpdf-u3d.lmt new file mode 100644 index 000000000..6e02fde30 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-u3d.lmt @@ -0,0 +1,495 @@ +if not modules then modules = { } end modules ['lpdf-u3d'] = { + version = 1.001, + 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" +} + +-- The following code is based on a working prototype provided +-- by Michael Vidiassov. It is rewritten using the lpdf library +-- and different checking is used. The macro calls are adapted +-- (and will eventually be removed). The user interface needs +-- an overhaul. There are some messy leftovers that will be +-- removed in future versions. + +-- For some reason no one really tested this code so at some +-- point we will end up with a reimplementation. For instance +-- it makes sense to add the same activation code as with swf. + +local tonumber = tonumber +local formatters, find = string.formatters, string.find +local cos, sin, sqrt, pi, atan2, abs = math.cos, math.sin, math.sqrt, math.pi, math.atan2, math.abs + +local backends, lpdf = backends, lpdf + +local nodeinjections = backends.pdf.nodeinjections + +local pdfconstant = lpdf.constant +local pdfboolean = lpdf.boolean +local pdfunicode = lpdf.unicode +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfnull = lpdf.null +local pdfreference = lpdf.reference + +local pdfflushstreamobject +local pdfflushstreamfileobject + +updaters.register("backend.update.lpdf",function() + pdfflushstreamobject = lpdf.flushstreamobject + pdfflushstreamfileobject = lpdf.flushstreamfileobject +end) + +local checkedkey = lpdf.checkedkey +local limited = lpdf.limited + +local embedimage = images.embed + +local schemes = table.tohash { + "Artwork", "None", "White", "Day", "Night", "Hard", + "Primary", "Blue", "Red", "Cube", "CAD", "Headlamp", +} + +local modes = table.tohash { + "Solid", "SolidWireframe", "Transparent", "TransparentWireframe", "BoundingBox", + "TransparentBoundingBox", "TransparentBoundingBoxOutline", "Wireframe", + "ShadedWireframe", "HiddenWireframe", "Vertices", "ShadedVertices", "Illustration", + "SolidOutline", "ShadedIllustration", +} + +local function normalize(x, y, z) + local modulo = sqrt(x*x + y*y + z*z); + if modulo ~= 0 then + return x/modulo, y/modulo, z/modulo + else + return x, y, z + end +end + +local function rotate(vect_x,vect_y,vect_z, tet, axis_x,axis_y,axis_z) + -- rotate vect by tet about axis counterclockwise + local c, s = cos(tet*pi/180), sin(tet*pi/180) + local r = 1 - c + local n = sqrt(axis_x*axis_x+axis_y*axis_y+axis_z*axis_z) + axis_x, axis_y, axis_z = axis_x/n, axis_y/n, axis_z/n + return + (axis_x*axis_x*r+c )*vect_x + (axis_x*axis_y*r-axis_z*s)*vect_y + (axis_x*axis_z*r+axis_y*s)*vect_z, + (axis_x*axis_y*r+axis_z*s)*vect_x + (axis_y*axis_y*r+c )*vect_y + (axis_y*axis_z*r-axis_x*s)*vect_z, + (axis_x*axis_z*r-axis_y*s)*vect_x + (axis_y*axis_z*r+axis_x*s)*vect_y + (axis_z*axis_z*r+c )*vect_z +end + +local function make3dview(view) + + local name = view.name + local name = pdfunicode(name ~= "" and name or "unknown view") + + local viewdict = pdfdictionary { + Type = pdfconstant("3DView"), + XN = name, + IN = name, + NR = true, + } + + local bg = checkedkey(view,"bg","table") + if bg then + viewdict.BG = pdfdictionary { + Type = pdfconstant("3DBG"), + C = pdfarray { limited(bg[1],1,1,1), limited(bg[2],1,1,1), limited(bg[3],1,1,1) }, + } + end + + local lights = checkedkey(view,"lights","string") + if lights and schemes[lights] then + viewdict.LS = pdfdictionary { + Type = pdfconstant("3DLightingScheme"), + Subtype = pdfconstant(lights), + } + end + + -- camera position is taken from 3d model + + local u3dview = checkedkey(view, "u3dview", "string") + if u3dview then + viewdict.MS = pdfconstant("U3D") + viewdict.U3DPath = u3dview + end + + -- position the camera as given + + local c2c = checkedkey(view, "c2c", "table") + local coo = checkedkey(view, "coo", "table") + local roo = checkedkey(view, "roo", "number") + local azimuth = checkedkey(view, "azimuth", "number") + local altitude = checkedkey(view, "altitude", "number") + + if c2c or coo or roo or azimuth or altitude then + + local pos = checkedkey(view, "pos", "table") + local dir = checkedkey(view, "dir", "table") + local upv = checkedkey(view, "upv", "table") + local roll = checkedkey(view, "roll", "table") + + local coo_x, coo_y, coo_z = 0, 0, 0 + local dir_x, dir_y, dir_z = 0, 0, 0 + local trans_x, trans_y, trans_z = 0, 0, 0 + local left_x, left_y, left_z = 0, 0, 0 + local up_x, up_y, up_z = 0, 0, 0 + + -- point camera is aimed at + + if coo then + coo_x, coo_y, coo_z = tonumber(coo[1]) or 0, tonumber(coo[2]) or 0, tonumber(coo[3]) or 0 + end + + -- distance from camera to target + + if roo then + roo = abs(roo) + end + if not roo or roo == 0 then + roo = 0.000000000000000001 + end + + -- set it via camera position + + if pos then + dir_x = coo_x - (tonumber(pos[1]) or 0) + dir_y = coo_y - (tonumber(pos[2]) or 0) + dir_z = coo_z - (tonumber(pos[3]) or 0) + if not roo then + roo = sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z) + end + if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end + dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z) + end + + -- set it directly + + if dir then + dir_x, dir_y, dir_z = tonumber(dir[1] or 0), tonumber(dir[2] or 0), tonumber(dir[3] or 0) + if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end + dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z) + end + + -- set it movie15 style with vector from target to camera + + if c2c then + dir_x, dir_y, dir_z = - tonumber(c2c[1] or 0), - tonumber(c2c[2] or 0), - tonumber(c2c[3] or 0) + if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end + dir_x, dir_y, dir_z = normalize(dir_x,dir_y,dir_z) + end + + -- set it with azimuth and altitutde + + if altitude or azimuth then + dir_x, dir_y, dir_z = -1, 0, 0 + if altitude then dir_x, dir_y, dir_z = rotate(dir_x,dir_y,dir_z, -altitude, 0,1,0) end + if azimuth then dir_x, dir_y, dir_z = rotate(dir_x,dir_y,dir_z, azimuth, 0,0,1) end + end + + -- set it with rotation like in MathGL + + if rot then + if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_z = -1 end + dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[1]) or 0, 1,0,0) + dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[2]) or 0, 0,1,0) + dir_x,dir_y,dir_z = rotate(dir_x,dir_y,dir_z, tonumber(rot[3]) or 0, 0,0,1) + end + + -- set it with default movie15 orientation looking up y axis + + if dir_x == 0 and dir_y == 0 and dir_z == 0 then dir_y = 1 end + + -- left-vector + -- up-vector + + if upv then + up_x, up_y, up_z = tonumber(upv[1]) or 0, tonumber(upv[2]) or 0, tonumber(upv[3]) or 0 + else + -- set default up-vector + if abs(dir_x) == 0 and abs(dir_y) == 0 then + if dir_z < 0 then + up_y = 1 -- top view + else + up_y = -1 -- bottom view + end + else + -- other camera positions than top and bottom, up-vector = up_world - (up_world dot dir) dir + up_x, up_y, up_z = - dir_z*dir_x, - dir_z*dir_y, - dir_z*dir_z + 1 + end + end + + -- normalize up-vector + + up_x, up_y, up_z = normalize(up_x,up_y,up_z) + + -- left vector = up x dir + + left_x, left_y, left_z = dir_z*up_y - dir_y*up_z, dir_x*up_z - dir_z*up_x, dir_y*up_x - dir_x*up_y + + -- normalize left vector + + left_x, left_y, left_z = normalize(left_x,left_y,left_z) + + -- apply camera roll + + if roll then + local sinroll = sin((roll/180.0)*pi) + local cosroll = cos((roll/180.0)*pi) + left_x = left_x*cosroll + up_x*sinroll + left_y = left_y*cosroll + up_y*sinroll + left_z = left_z*cosroll + up_z*sinroll + up_x = up_x*cosroll + left_x*sinroll + up_y = up_y*cosroll + left_y*sinroll + up_z = up_z*cosroll + left_z*sinroll + end + + -- translation vector + + trans_x, trans_y, trans_z = coo_x - roo*dir_x, coo_y - roo*dir_y, coo_z - roo*dir_z + + viewdict.MS = pdfconstant("M") + viewdict.CO = roo + viewdict.C2W = pdfarray { + left_x, left_y, left_z, + up_x, up_y, up_z, + dir_x, dir_y, dir_z, + trans_x, trans_y, trans_z, + } + + end + + local aac = tonumber(view.aac) -- perspective projection + local mag = tonumber(view.mag) -- ortho projection + + if aac and aac > 0 and aac < 180 then + viewdict.P = pdfdictionary { + Subtype = pdfconstant("P"), + PS = pdfconstant("Min"), + FOV = aac, + } + elseif mag and mag > 0 then + viewdict.P = pdfdictionary { + Subtype = pdfconstant("O"), + OS = mag, + } + end + + local mode = modes[view.rendermode] + if mode then + pdfdictionary { + Type = pdfconstant("3DRenderMode"), + Subtype = pdfconstant(mode), + } + end + + -- crosssection + + local crosssection = checkedkey(view,"crosssection","table") + if crosssection then + local crossdict = pdfdictionary { + Type = pdfconstant("3DCrossSection") + } + + local c = checkedkey(crosssection,"point","table") or checkedkey(crosssection,"center","table") + if c then + crossdict.C = pdfarray { tonumber(c[1]) or 0, tonumber(c[2]) or 0, tonumber(c[3]) or 0 } + end + + local normal = checkedkey(crosssection,"normal","table") + if normal then + local x, y, z = tonumber(normal[1] or 0), tonumber(normal[2] or 0), tonumber(normal[3] or 0) + if sqrt(x*x + y*y + z*z) == 0 then + x, y, z = 1, 0, 0 + end + crossdict.O = pdfarray { + pdfnull, + atan2(-z,sqrt(x*x + y*y))*180/pi, + atan2(y,x)*180/pi, + } + end + + local orient = checkedkey(crosssection,"orient","table") + if orient then + crossdict.O = pdfarray { + tonumber(orient[1]) or 1, + tonumber(orient[2]) or 0, + tonumber(orient[3]) or 0, + } + end + + crossdict.IV = cross.intersection or false + crossdict.ST = cross.transparent or false + + viewdict.SA = next(crossdict) and pdfarray { crossdict } -- maybe test if # > 1 + end + + local nodes = checkedkey(view,"nodes","table") + if nodes then + local nodelist = pdfarray() + for i=1,#nodes do + local node = checkedkey(nodes,i,"table") + if node then + local position = checkedkey(node,"position","table") + nodelist[#nodelist+1] = pdfdictionary { + Type = pdfconstant("3DNode"), + N = node.name or ("node_" .. i), -- pdfunicode ? + M = position and #position == 12 and pdfarray(position), + V = node.visible or true, + O = node.opacity or 0, + RM = pdfdictionary { + Type = pdfconstant("3DRenderMode"), + Subtype = pdfconstant(node.rendermode or "Solid"), + }, + } + end + end + viewdict.NA = nodelist + end + + return viewdict + +end + +local stored_js, stored_3d, stored_pr, streams = { }, { }, { }, { } + +local f_image = formatters["q /GS gs %.6N 0 0 %.6N 0 0 cm /IM Do Q"] + +local function insert3d(spec) -- width, height, factor, display, controls, label, foundname + + local width, height, factor = spec.width, spec.height, spec.factor or number.dimenfactors.bp + local display, controls, label, foundname = spec.display, spec.controls, spec.label, spec.foundname + + local param = (display and parametersets[display]) or { } + local streamparam = (controls and parametersets[controls]) or { } + local name = "3D Artwork " .. (param.name or label or "Unknown") + + local activationdict = pdfdictionary { + TB = pdfboolean(param.toolbar,true), + NP = pdfboolean(param.tree,false), + } + + local stream = streams[label] + if not stream then + + local subtype, subdata = "U3D", io.loaddata(foundname) or "" + if find(subdata,"^PRC") then + subtype = "PRC" + elseif find(subdata,"^U3D") then + subtype = "U3D" + elseif file.suffix(foundname) == "prc" then + subtype = "PRC" + end + + local attr = pdfdictionary { + Type = pdfconstant("3D"), + Subtype = pdfconstant(subtype), + } + local streamviews = checkedkey(streamparam, "views", "table") + if streamviews then + local list = pdfarray() + for i=1,#streamviews do + local v = checkedkey(streamviews, i, "table") + if v then + list[#list+1] = make3dview(v) + end + end + attr.VA = list + end + if checkedkey(streamparam, "view", "table") then + attr.DV = make3dview(streamparam.view) + elseif checkedkey(streamparam, "view", "string") then + attr.DV = streamparam.view + end + local js = checkedkey(streamparam, "js", "string") + if js then + local jsref = stored_js[js] + if not jsref then + jsref = pdfflushstreamfileobject(js) + stored_js[js] = jsref + end + attr.OnInstantiate = pdfreference(jsref) + end + stored_3d[label] = pdfflushstreamfileobject(foundname,attr) + stream = 1 + else + stream = stream + 1 + end + streams[label] = stream + + local name = pdfunicode(name) + + local annot = pdfdictionary { + Subtype = pdfconstant("3D"), + T = name, + Contents = name, + NM = name, + ["3DD"] = pdfreference(stored_3d[label]), + ["3DA"] = activationdict, + } + if checkedkey(param,"view","table") then + annot["3DV"] = make3dview(param.view) + elseif checkedkey(param,"view","string") then + annot["3DV"] = param.view + end + + local preview = checkedkey(param,"preview","string") + if preview then + activationdict.A = pdfconstant("XA") + local tag = formatters["%s:%s:%s"](label,stream,preview) + local ref = stored_pr[tag] + if not ref then + local figure = embedimage { + filename = preview, + width = width, + height = height + } + ref = figure.objnum + stored_pr[tag] = ref + end + if ref then -- see back-pdf ** .. here we have a local /IM ! + local pw = pdfdictionary { + Type = pdfconstant("XObject"), + Subtype = pdfconstant("Form"), + FormType = 1, + BBox = pdfarray { 0, 0, pdfnumber(factor*width), pdfnumber(factor*height) }, + Matrix = pdfarray { 1, 0, 0, 1, 0, 0 }, + ProcSet = lpdf.procset(), + Resources = pdfdictionary { + XObject = pdfdictionary { + IM = pdfreference(ref) + } + }, + ExtGState = pdfdictionary { + GS = pdfdictionary { + Type = pdfconstant("ExtGState"), + CA = 1, + ca = 1, + } + }, + } + local pwd = pdfflushstreamobject(f_image(factor*width,factor*height),pw) + annot.AP = pdfdictionary { + N = pdfreference(pwd) + } + end + return annot, figure, ref + else + activationdict.A = pdfconstant("PV") + return annot, nil, nil + end +end + +function nodeinjections.insertu3d(spec) + local annotation, preview, ref = insert3d { -- just spec + foundname = spec.foundname, + width = spec.width, + height = spec.height, + factor = spec.factor, + display = spec.display, + controls = spec.controls, + label = spec.label, + } + node.write(nodeinjections.annotation(spec.width,spec.height,0,annotation())) +end diff --git a/tex/context/base/mkxl/lpdf-wid.lmt b/tex/context/base/mkxl/lpdf-wid.lmt new file mode 100644 index 000000000..268ca119e --- /dev/null +++ b/tex/context/base/mkxl/lpdf-wid.lmt @@ -0,0 +1,789 @@ +if not modules then modules = { } end modules ['lpdf-wid'] = { + version = 1.001, + 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" +} + +-- It's about time to give up on media in pdf and admit that pdf lost it to html. +-- First we had movies and sound, quite easy to deal with, but obsolete now. Then we +-- had renditions but they turned out to be unreliable from the start and look +-- obsolete too or at least they are bound to the (obsolete) flash technology for +-- rendering. They were already complex constructs. Now we have rich media which +-- instead of providing a robust future proof framework for general media types +-- again seems to depend on viewers built in (yes, also kind of obsolete) flash +-- technology, and we cannot expect this non-open technology to show up in open +-- browsers. So, in the end we can best just use links to external resources to be +-- future proof. Just look at the viewer preferences pane to see how fragile support +-- is. Interestingly u3d support is kind of built in, while e.g. mp4 support relies +-- on wrapping in swf. We used to stay ahead of the pack with support of the fancy +-- pdf features but it backfires and is not worth the trouble. And yes, for control +-- (even simple like starting and stopping videos) one has to revert to JavaScript, +-- the other fragile bit. And, now that adobe quits flash in 2020 we're without any +-- video anyway. Also, it won't play on all platforms and devices so let's wait for +-- html5 media in pdf then. + +local tonumber, next = tonumber, next +local gmatch, gsub, find, lower = string.gmatch, string.gsub, string.find, string.lower +local filenameonly, basefilename, filesuffix, addfilesuffix = file.nameonly, file.basename, file.suffix, file.addsuffix +local isfile, modificationtime = lfs.isfile, lfs.modification +local stripstring = string.strip +local settings_to_array = utilities.parsers.settings_to_array +local settings_to_hash = utilities.parsers.settings_to_hash +local sortedhash, sortedkeys = table.sortedhash, table.sortedkeys + +local report_media = logs.reporter("backend","media") +local report_attachment = logs.reporter("backend","attachment") + +local backends = backends +local lpdf = lpdf +local nodes = nodes +local context = context + +local texgetcount = tex.getcount + +local nodeinjections = backends.pdf.nodeinjections +local codeinjections = backends.pdf.codeinjections +local registrations = backends.pdf.registrations + +local executers = structures.references.executers +local variables = interfaces.variables + +local v_hidden = variables.hidden +local v_auto = variables.auto +local v_embed = variables.embed +local v_max = variables.max +local v_yes = variables.yes + +local pdfconstant = lpdf.constant +local pdfnull = lpdf.null +local pdfdictionary = lpdf.dictionary +local pdfarray = lpdf.array +local pdfreference = lpdf.reference +local pdfunicode = lpdf.unicode +local pdfstring = lpdf.string +local pdfboolean = lpdf.boolean +local pdfaction = lpdf.action +local pdfborder = lpdf.border + +local pdftransparencyvalue = lpdf.transparencyvalue +local pdfcolorvalues = lpdf.colorvalues + +local pdfflushobject +local pdfflushstreamobject +local pdfflushstreamfileobject +local pdfreserveobject +local pdfpagereference +local pdfshareobjectreference + +updaters.register("backend.update.lpdf",function() + pdfflushobject = lpdf.flushobject + pdfflushstreamobject = lpdf.flushstreamobject + pdfflushstreamfileobject = lpdf.flushstreamfileobject + pdfreserveobject = lpdf.reserveobject + pdfpagereference = lpdf.pagereference + pdfshareobjectreference = lpdf.shareobjectreference +end) + + +local hpack_node = node.hpack +local write_node = node.write -- test context(...) instead + +-- symbols + +local presets = { } -- xforms + +local function registersymbol(name,n) + presets[name] = pdfreference(n) +end + +local function registeredsymbol(name) + return presets[name] +end + +local function presetsymbol(symbol) + if not presets[symbol] then + context.predefinesymbol { symbol } + end +end + +local function presetsymbollist(list) + if list then + for symbol in gmatch(list,"[^, ]+") do + presetsymbol(symbol) + end + end +end + +codeinjections.registersymbol = registersymbol +codeinjections.registeredsymbol = registeredsymbol +codeinjections.presetsymbol = presetsymbol +codeinjections.presetsymbollist = presetsymbollist + +-- comments + +-- local symbols = { +-- Addition = pdfconstant("NewParagraph"), +-- Attachment = pdfconstant("Attachment"), +-- Balloon = pdfconstant("Comment"), +-- Check = pdfconstant("Check Mark"), +-- CheckMark = pdfconstant("Check Mark"), +-- Circle = pdfconstant("Circle"), +-- Cross = pdfconstant("Cross"), +-- CrossHairs = pdfconstant("Cross Hairs"), +-- Graph = pdfconstant("Graph"), +-- InsertText = pdfconstant("Insert Text"), +-- New = pdfconstant("Insert"), +-- Paperclip = pdfconstant("Paperclip"), +-- RightArrow = pdfconstant("Right Arrow"), +-- RightPointer = pdfconstant("Right Pointer"), +-- Star = pdfconstant("Star"), +-- Tag = pdfconstant("Tag"), +-- Text = pdfconstant("Note"), +-- TextNote = pdfconstant("Text Note"), +-- UpArrow = pdfconstant("Up Arrow"), +-- UpLeftArrow = pdfconstant("Up-Left Arrow"), +-- } + +local attachment_symbols = { + Graph = pdfconstant("Graph"), + Paperclip = pdfconstant("Paperclip"), + Pushpin = pdfconstant("PushPin"), +} + +attachment_symbols.PushPin = attachment_symbols.Pushpin +attachment_symbols.Default = attachment_symbols.Pushpin + +function lpdf.attachmentsymbols() + return sortedkeys(comment_symbols) +end + +local comment_symbols = { + Comment = pdfconstant("Comment"), + Help = pdfconstant("Help"), + Insert = pdfconstant("Insert"), + Key = pdfconstant("Key"), + Newparagraph = pdfconstant("NewParagraph"), + Note = pdfconstant("Note"), + Paragraph = pdfconstant("Paragraph"), +} + +comment_symbols.NewParagraph = Newparagraph +comment_symbols.Default = Note + +function lpdf.commentsymbols() + return sortedkeys(comment_symbols) +end + +local function analyzesymbol(symbol,collection) + if not symbol or symbol == "" then + return collection and collection.Default, nil + elseif collection and collection[symbol] then + return collection[symbol], nil + else + local setn, setr, setd + local set = settings_to_array(symbol) + if #set == 1 then + setn, setr, setd = set[1], set[1], set[1] + elseif #set == 2 then + setn, setr, setd = set[1], set[1], set[2] + else + setn, setr, setd = set[1], set[2], set[3] + end + local appearance = pdfdictionary { + N = setn and registeredsymbol(setn), + R = setr and registeredsymbol(setr), + D = setd and registeredsymbol(setd), + } + local appearanceref = pdfshareobjectreference(appearance) + return nil, appearanceref + end +end + +local function analyzenormalsymbol(symbol) + local appearance = pdfdictionary { + N = registeredsymbol(symbol), + } + local appearanceref = pdfshareobjectreference(appearance) + return appearanceref +end + +codeinjections.analyzesymbol = analyzesymbol +codeinjections.analyzenormalsymbol = analyzenormalsymbol + +local function analyzelayer(layer) + -- todo: (specification.layer ~= "" and pdfreference(specification.layer)) or nil, -- todo: ref to layer +end + +local function analyzecolor(colorvalue,colormodel) + local cvalue = colorvalue and tonumber(colorvalue) + local cmodel = colormodel and tonumber(colormodel) or 3 + return cvalue and pdfarray { pdfcolorvalues(cmodel,cvalue) } or nil +end + +local function analyzetransparency(transparencyvalue) + local tvalue = transparencyvalue and tonumber(transparencyvalue) + return tvalue and pdftransparencyvalue(tvalue) or nil +end + +-- Attachments + +local nofattachments = 0 +local attachments = { } +local filestreams = { } +local referenced = { } +local ignorereferenced = true -- fuzzy pdf spec .. twice in attachment list, can become an option +local tobesavedobjrefs = utilities.storage.allocate() +local collectedobjrefs = utilities.storage.allocate() +local permitted = true +local enabled = true + +function codeinjections.setattachmentsupport(option) + if option == false then + permitted = false + enabled = false + end +end + +local fileobjreferences = { + collected = collectedobjrefs, + tobesaved = tobesavedobjrefs, +} + +job.fileobjreferences = fileobjreferences + +local function initializer() + collectedobjrefs = job.fileobjreferences.collected or { } + tobesavedobjrefs = job.fileobjreferences.tobesaved or { } +end + +job.register('job.fileobjreferences.collected', tobesavedobjrefs, initializer) + +local function flushembeddedfiles() + if enabled and next(filestreams) then + local e = pdfarray() + local f = pdfarray() + for tag, reference in sortedhash(filestreams) do + if not reference then + report_attachment("unreferenced file, tag %a",tag) + elseif referenced[tag] == "hidden" then + e[#e+1] = pdfstring(tag) + e[#e+1] = reference -- already a reference + f[#f+1] = reference -- collect all file description references + else + -- messy spec ... when annot not in named else twice in menu list acrobat + f[#f+1] = reference + end + end + if #e > 0 then + lpdf.addtonames("EmbeddedFiles",pdfreference(pdfflushobject(pdfdictionary{ Names = e }))) + end + if #f > 0 then -- PDF/A-2|3: all associated files must have a relationship to the PDF document (global or part) + lpdf.addtocatalog("AF", pdfreference(pdfflushobject(f))) -- global (Catalog) + end + end +end + +lpdf.registerdocumentfinalizer(flushembeddedfiles,"embeddedfiles") + +function codeinjections.embedfile(specification) + if enabled then + local data = specification.data + local filename = specification.file + local name = specification.name or "" + local title = specification.title or "" + local hash = specification.hash or filename + local keepdir = specification.keepdir -- can change + local usedname = specification.usedname + local filetype = specification.filetype + local compress = specification.compress + local mimetype = specification.mimetype or specification.mime + if filename == "" then + filename = nil + end + if compress == nil then + compress = true + end + if data then + local r = filestreams[hash] + if r == false then + return nil + elseif r then + return r + elseif not filename then + filename = specification.tag + if not filename or filename == "" then + filename = specification.registered + end + if not filename or filename == "" then + filename = hash + end + end + else + if not filename then + return nil + end + local r = filestreams[hash] + if r == false then + return nil + elseif r then + return r + else + local foundname = resolvers.findbinfile(filename) or "" + if foundname == "" or not isfile(foundname) then + filestreams[filename] = false + return nil + else + specification.foundname = foundname + end + end + end + -- needs to be cleaned up: + usedname = usedname ~= "" and usedname or filename or name + local basename = keepdir == true and usedname or basefilename(usedname) + local basename = gsub(basename,"%./","") + local savename = name ~= "" and name or basename + local foundname = specification.foundname or filename + if not filetype or filetype == "" then + filetype = name and (filename and filesuffix(filename)) or "txt" + end + savename = addfilesuffix(savename,filetype) -- type is mandate for proper working in viewer + local a = pdfdictionary { + Type = pdfconstant("EmbeddedFile"), + Subtype = mimetype and mimetype ~= "" and pdfconstant(mimetype) or nil, + } + local f + if data then + f = pdfflushstreamobject(data,a) + specification.data = true -- signal that still data but already flushed + else + local attributes = lfs.attributes(foundname) + local modification = modificationtime(foundname) + a.Params = { + Size = attributes.size, + ModDate = lpdf.pdftimestamp(modification), + } + f = pdfflushstreamfileobject(foundname,a,compress) + end + local d = pdfdictionary { + Type = pdfconstant("Filespec"), + F = pdfstring(savename), + -- UF = pdfstring(savename), + UF = pdfunicode(savename), + EF = pdfdictionary { F = pdfreference(f) }, + Desc = title ~= "" and pdfunicode(title) or nil, + AFRelationship = pdfconstant("Unspecified"), -- Supplement, Data, Source, Alternative, Data + } + local r = pdfreference(pdfflushobject(d)) + filestreams[hash] = r + return r + end +end + +function nodeinjections.attachfile(specification) + if enabled then + local registered = specification.registered or "<unset>" + local data = specification.data + local hash + local filename + if data then + hash = md5.HEX(data) + else + filename = specification.file + if not filename or filename == "" then + report_attachment("no file specified, using registered %a instead",registered) + filename = registered + specification.file = registered + end + local foundname = resolvers.findbinfile(filename) or "" + if foundname == "" or not isfile(foundname) then + report_attachment("invalid filename %a, ignoring registered %a",filename,registered) + return nil + else + specification.foundname = foundname + end + hash = filename + end + specification.hash = hash + nofattachments = nofattachments + 1 + local registered = specification.registered or "" + local title = specification.title or "" + local subtitle = specification.subtitle or "" + local author = specification.author or "" + local onlyname = filename and filenameonly(filename) or "" + if registered == "" then + registered = filename + end + if author == "" and title ~= "" then + author = title + title = onlyname or "" + end + if author == "" then + author = onlyname or "<unknown>" + end + if title == "" then + title = registered + end + if title == "" and filename then + title = onlyname + end + local aref = attachments[registered] + if not aref then + aref = codeinjections.embedfile(specification) + attachments[registered] = aref + end + local reference = specification.reference + if reference and aref then + tobesavedobjrefs[reference] = aref[1] + end + if not aref then + report_attachment("skipping attachment, registered %a",registered) + -- already reported + elseif specification.method == v_hidden then + referenced[hash] = "hidden" + else + referenced[hash] = "annotation" + local name, appearance = analyzesymbol(specification.symbol,attachment_symbols) + local flags = specification.flags or 0 -- to keep it expandable + local d = pdfdictionary { + Subtype = pdfconstant("FileAttachment"), + FS = aref, + Contents = pdfunicode(title), + Name = name, + NM = pdfstring("attachment:"..nofattachments), + T = author ~= "" and pdfunicode(author) or nil, + Subj = subtitle ~= "" and pdfunicode(subtitle) or nil, + C = analyzecolor(specification.colorvalue,specification.colormodel), + CA = analyzetransparency(specification.transparencyvalue), + AP = appearance, + OC = analyzelayer(specification.layer), + -- F = pdfnull(), -- another rediculous need to satisfy validation + F = bit32.band(bit32.bor(flags,4),(1023-1-2-32-256)), -- set 3, clear 1,2,6,9; PDF 32000-1, p385 + } + local width = specification.width or 0 + local height = specification.height or 0 + local depth = specification.depth or 0 + local box = hpack_node(nodeinjections.annotation(width,height,depth,d())) + box.width = width + box.height = height + box.depth = depth + return box + end + end +end + +function codeinjections.attachmentid(filename) -- not used in context + return filestreams[filename] +end + +-- Comments + +local nofcomments = 0 +local usepopupcomments = false + +local defaultattributes = { + ["xmlns"] = "http://www.w3.org/1999/xhtml", + ["xmlns:xfa"] = "http://www.xfa.org/schema/xfa-data/1.0/", + ["xfa:contentType"] = "text/html", + ["xfa:APIVersion"] = "Acrobat:8.0.0", + ["xfa:spec"] = "2.4", +} + +local function checkcontent(text,option) + if option and option.xml then + local root = xml.convert(text) + if root and not root.er then + xml.checkbom(root) + local body = xml.first(root,"/body") + if body then + local at = body.at + for k, v in next, defaultattributes do + if not at[k] then + at[k] = v + end + end + -- local content = xml.textonly(root) + local richcontent = xml.tostring(root) + return nil, pdfunicode(richcontent) + end + end + end + return pdfunicode(text) +end + +function nodeinjections.comment(specification) -- brrr: seems to be done twice + nofcomments = nofcomments + 1 + local text = specification.data or "" + if specification.space ~= v_yes then + text = stripstring(text) + text = gsub(text,"[\n\r] *","\n") + end + text = gsub(text,"\r","\n") + local name, appearance = analyzesymbol(specification.symbol,comment_symbols) + local tag = specification.tag or "" -- this is somewhat messy as recent + local title = specification.title or "" -- versions of acrobat see the title + local subtitle = specification.subtitle or "" -- as author + local author = specification.author or "" + local option = settings_to_hash(specification.option or "") + if author ~= "" then + if subtitle == "" then + subtitle = title + elseif title ~= "" then + subtitle = subtitle .. ", " .. title + end + title = author + end + if title == "" then + title = tag + end + local content, richcontent = checkcontent(text,option) + local d = pdfdictionary { + Subtype = pdfconstant("Text"), + Open = option[v_max] and pdfboolean(true) or nil, + Contents = content, + RC = richcontent, + T = title ~= "" and pdfunicode(title) or nil, + Subj = subtitle ~= "" and pdfunicode(subtitle) or nil, + C = analyzecolor(specification.colorvalue,specification.colormodel), + CA = analyzetransparency(specification.transparencyvalue), + OC = analyzelayer(specification.layer), + Name = name, + NM = pdfstring("comment:"..nofcomments), + AP = appearance, + } + local width = specification.width or 0 + local height = specification.height or 0 + local depth = specification.depth or 0 + local box + if usepopupcomments then + -- rather useless as we can hide/vide + local nd = pdfreserveobject() + local nc = pdfreserveobject() + local c = pdfdictionary { + Subtype = pdfconstant("Popup"), + Parent = pdfreference(nd), + } + d.Popup = pdfreference(nc) + box = hpack_node( + nodeinjections.annotation(0,0,0,d(),nd), + nodeinjections.annotation(width,height,depth,c(),nc) + ) + else + box = hpack_node(nodeinjections.annotation(width,height,depth,d())) + end + box.width = width -- redundant + box.height = height -- redundant + box.depth = depth -- redundant + return box +end + +-- rendering stuff +-- +-- object_1 -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >> +-- object_2 -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >> +-- rendering -> <</Type /Rendition /S /MS [objref_1 objref_2]>> +-- +-- we only work foreward here (currently) +-- annotation is to be packed at the tex end + +-- aiff audio/aiff +-- au audio/basic +-- avi video/avi +-- mid audio/midi +-- mov video/quicktime +-- mp3 audio/x-mp3 (mpeg) +-- mp4 audio/mp4 +-- mp4 video/mp4 +-- mpeg video/mpeg +-- smil application/smil +-- swf application/x-shockwave-flash + +-- P media play parameters (evt /BE for controls etc +-- A boolean (audio) +-- C boolean (captions) +-- O boolean (overdubs) +-- S boolean (subtitles) +-- PL pdfconstant("ADBE_MCI"), + +-- F = flags, +-- T = title, +-- Contents = rubish, +-- AP = irrelevant, + +-- sound is different, no window (or zero) so we need to collect them and +-- force them if not set + +local ms, mu, mf = { }, { }, { } + +local function delayed(label) + local a = pdfreserveobject() + mu[label] = a + return pdfreference(a) +end + +local function insertrenderingwindow(specification) + local label = specification.label + -- local openpage = specification.openpage + -- local closepage = specification.closepage + if specification.option == v_auto then + if openpageaction then + -- \handlereferenceactions{\v!StartRendering{#2}} + end + if closepageaction then + -- \handlereferenceactions{\v!StopRendering {#2}} + end + end + local actions = nil + if openpage or closepage then + actions = pdfdictionary { + PO = (openpage and lpdfaction(openpage )) or nil, + PC = (closepage and lpdfaction(closepage)) or nil, + } + end + local page = tonumber(specification.page) or texgetcount("realpageno") -- todo + local r = mu[label] or pdfreserveobject() -- why the reserve here? + local a = pdfdictionary { + S = pdfconstant("Rendition"), + R = mf[label], + OP = 0, + AN = pdfreference(r), + } + local bs, bc = pdfborder() + local d = pdfdictionary { + Subtype = pdfconstant("Screen"), + P = pdfreference(pdfpagereference(page)), + A = a, -- needed in order to make the annotation clickable (i.e. don't bark) + Border = bs, + C = bc, + AA = actions, + } + local width = specification.width or 0 + local height = specification.height or 0 + if height == 0 or width == 0 then + -- todo: sound needs no window + end + write_node(nodeinjections.annotation(width,height,0,d(),r)) -- save ref + return pdfreference(r) +end + +-- some dictionaries can have a MH (must honor) or BE (best effort) capsule + +local function insertrendering(specification) + local label = specification.label + local option = settings_to_hash(specification.option) + if not mf[label] then + local filename = specification.filename + local isurl = find(filename,"://",1,true) + local mimetype = specification.mimetype or specification.mime + -- local start = pdfdictionary { + -- Type = pdfconstant("MediaOffset"), + -- S = pdfconstant("T"), -- time + -- T = pdfdictionary { -- time + -- Type = pdfconstant("Timespan"), + -- S = pdfconstant("S"), + -- V = 3, -- time in seconds + -- }, + -- } + -- local start = pdfdictionary { + -- Type = pdfconstant("MediaOffset"), + -- S = pdfconstant("F"), -- frame + -- F = 100 -- framenumber + -- } + -- local start = pdfdictionary { + -- Type = pdfconstant("MediaOffset"), + -- S = pdfconstant("M"), -- mark + -- M = "somemark", + -- } + -- local parameters = pdfdictionary { + -- BE = pdfdictionary { + -- B = start, + -- } + -- } + -- local parameters = pdfdictionary { + -- Type = pdfconstant(MediaPermissions), + -- TF = pdfstring("TEMPALWAYS") }, -- TEMPNEVER TEMPEXTRACT TEMPACCESS TEMPALWAYS + -- } + local descriptor = pdfdictionary { + Type = pdfconstant("Filespec"), + F = filename, + } + if isurl then + descriptor.FS = pdfconstant("URL") + elseif option[v_embed] then + descriptor.EF = codeinjections.embedfile { + file = filename, + mimetype = mimetype, -- yes or no + compress = false, + } + end + local clip = pdfdictionary { + Type = pdfconstant("MediaClip"), + S = pdfconstant("MCD"), + N = label, + CT = mimetype, + Alt = pdfarray { "", "file not found" }, -- language id + message + D = pdfreference(pdfflushobject(descriptor)), + -- P = pdfreference(pdfflushobject(parameters)), + } + local rendition = pdfdictionary { + Type = pdfconstant("Rendition"), + S = pdfconstant("MR"), + N = label, + C = pdfreference(pdfflushobject(clip)), + } + mf[label] = pdfreference(pdfflushobject(rendition)) + end +end + +local function insertrenderingobject(specification) -- todo + local label = specification.label + if not mf[label] then + report_media("unknown medium, label %a",label) + local clip = pdfdictionary { -- does not work that well one level up + Type = pdfconstant("MediaClip"), + S = pdfconstant("MCD"), + N = label, + D = pdfreference(unknown), -- not label but objectname, hm .. todo? + } + local rendition = pdfdictionary { + Type = pdfconstant("Rendition"), + S = pdfconstant("MR"), + N = label, + C = pdfreference(pdfflushobject(clip)), + } + mf[label] = pdfreference(pdfflushobject(rendition)) + end +end + +function codeinjections.processrendering(label) + local specification = interactions.renderings.rendering(label) + if not specification then + -- error + elseif specification.type == "external" then + insertrendering(specification) + else + insertrenderingobject(specification) + end +end + +function codeinjections.insertrenderingwindow(specification) + local label = specification.label + codeinjections.processrendering(label) + ms[label] = insertrenderingwindow(specification) +end + +local function set(operation,arguments) + codeinjections.processrendering(arguments) + return pdfdictionary { + S = pdfconstant("Rendition"), + OP = operation, + R = mf[arguments], + AN = ms[arguments] or delayed(arguments), + } +end + +function executers.startrendering (arguments) return set(0,arguments) end +function executers.stoprendering (arguments) return set(1,arguments) end +function executers.pauserendering (arguments) return set(2,arguments) end +function executers.resumerendering(arguments) return set(3,arguments) end diff --git a/tex/context/base/mkxl/lpdf-xmp.lmt b/tex/context/base/mkxl/lpdf-xmp.lmt new file mode 100644 index 000000000..313488a39 --- /dev/null +++ b/tex/context/base/mkxl/lpdf-xmp.lmt @@ -0,0 +1,311 @@ +if not modules then modules = { } end modules ['lpdf-xmp'] = { + version = 1.001, + 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", + comment = "with help from Peter Rolf", +} + +local tostring, type = tostring, type +local format, gsub = string.format, string.gsub +local utfchar = utf.char +local xmlfillin = xml.fillin +local md5HEX = md5.HEX + +local trace_xmp = false trackers.register("backend.xmp", function(v) trace_xmp = v end) +local trace_info = false trackers.register("backend.info", function(v) trace_info = v end) + +local report_xmp = logs.reporter("backend","xmp") +local report_info = logs.reporter("backend","info") + +local backends, lpdf = backends, lpdf + +local codeinjections = backends.pdf.codeinjections -- normally it is registered + +local pdfdictionary = lpdf.dictionary +local pdfconstant = lpdf.constant +local pdfreference = lpdf.reference + +local pdfgetmetadata = lpdf.getmetadata + +local pdfflushstreamobject + +updaters.register("backend.update.lpdf",function() + pdfflushstreamobject = lpdf.flushstreamobject +end) + +-- The XMP packet wrapper is kind of fixed, see page 10 of XMPSpecificationsPart1.pdf from +-- XMP-Toolkit-SDK-CC201607.zip. So we hardcode the id. + +local xpacket = format ( [[ +<?xpacket begin="%s" id="W5M0MpCehiHzreSzNTczkc9d"?> + +%%s + +<?xpacket end="w"?>]], utfchar(0xFEFF) ) + +local mapping = { + -- user defined keys (pdfx:) + ["ConTeXt.Jobname"] = { "context", "rdf:Description/pdfx:ConTeXt.Jobname" }, + ["ConTeXt.Time"] = { "date", "rdf:Description/pdfx:ConTeXt.Time" }, + ["ConTeXt.Url"] = { "context", "rdf:Description/pdfx:ConTeXt.Url" }, + ["ConTeXt.Support"] = { "context", "rdf:Description/pdfx:ConTeXt.Support" }, + ["ConTeXt.Version"] = { "context", "rdf:Description/pdfx:ConTeXt.Version" }, + ["ConTeXt.LMTX"] = { "context", "rdf:Description/pdfx:ConTeXt.LMTX" }, + ["TeX.Support"] = { "metadata","rdf:Description/pdfx:TeX.Support" }, + ["LuaTeX.Version"] = { "metadata","rdf:Description/pdfx:LuaTeX.Version" }, + ["LuaTeX.Functionality"] = { "metadata","rdf:Description/pdfx:LuaTeX.Functionality" }, + ["LuaTeX.LuaVersion"] = { "metadata","rdf:Description/pdfx:LuaTeX.LuaVersion" }, + ["LuaTeX.Platform"] = { "metadata","rdf:Description/pdfx:LuaTeX.Platform" }, + ["ID"] = { "id", "rdf:Description/pdfx:ID" }, -- has date + -- Adobe PDF schema + ["Keywords"] = { "metadata","rdf:Description/pdf:Keywords" }, + ["Producer"] = { "metadata","rdf:Description/pdf:Producer" }, + -- ["Trapped"] = { "pdf", "rdf:Description/pdf:Trapped" }, -- '/False' in /Info, but 'False' in XMP + -- Dublin Core schema + ["Author"] = { "metadata","rdf:Description/dc:creator/rdf:Seq/rdf:li" }, + ["Format"] = { "metadata","rdf:Description/dc:format" }, -- optional, but nice to have + ["Subject"] = { "metadata","rdf:Description/dc:description/rdf:Alt/rdf:li" }, + ["Title"] = { "metadata","rdf:Description/dc:title/rdf:Alt/rdf:li" }, + -- XMP Basic schema + ["CreateDate"] = { "date", "rdf:Description/xmp:CreateDate" }, + ["CreationDate"] = { "date", "rdf:Description/xmp:CreationDate" }, -- dummy + ["Creator"] = { "metadata","rdf:Description/xmp:CreatorTool" }, + ["MetadataDate"] = { "date", "rdf:Description/xmp:MetadataDate" }, + ["ModDate"] = { "date", "rdf:Description/xmp:ModDate" }, -- dummy + ["ModifyDate"] = { "date", "rdf:Description/xmp:ModifyDate" }, + -- XMP Media Management schema + ["DocumentID"] = { "id", "rdf:Description/xmpMM:DocumentID" }, -- uuid + ["InstanceID"] = { "id", "rdf:Description/xmpMM:InstanceID" }, -- uuid + ["RenditionClass"] = { "pdf", "rdf:Description/xmpMM:RenditionClass" }, -- PDF/X-4 + ["VersionID"] = { "pdf", "rdf:Description/xmpMM:VersionID" }, -- PDF/X-4 + -- additional entries + -- PDF/X + ["GTS_PDFXVersion"] = { "pdf", "rdf:Description/pdfxid:GTS_PDFXVersion" }, + -- optional entries + -- all what is visible in the 'document properties --> additional metadata' window + -- XMP Rights Management schema (optional) + ["Marked"] = { "pdf", "rdf:Description/xmpRights:Marked" }, + -- ["Owner"] = { "metadata", "rdf:Description/xmpRights:Owner/rdf:Bag/rdf:li" }, -- maybe useful (not visible) + -- ["UsageTerms"] = { "metadata", "rdf:Description/xmpRights:UsageTerms" }, -- maybe useful (not visible) + ["WebStatement"] = { "metadata", "rdf:Description/xmpRights:WebStatement" }, + -- Photoshop PDF schema (optional) + ["AuthorsPosition"] = { "metadata", "rdf:Description/photoshop:AuthorsPosition" }, + ["Copyright"] = { "metadata", "rdf:Description/photoshop:Copyright" }, + ["CaptionWriter"] = { "metadata", "rdf:Description/photoshop:CaptionWriter" }, +} + +local included = backends.included +local lpdfid = lpdf.id + +function lpdf.id() -- overload of ini + return lpdfid(included.date) +end + +local trailerid = nil +local dates = nil + +local function update() + if trailer_id then + local b = toboolean(trailer_id) or trailer_id == "" + if b then + trailer_id = "This file is processed by ConTeXt and LuaTeX." + else + trailer_id = tostring(trailer_id) + end + local h = md5HEX(trailer_id) + if b then + report_info("using frozen trailer id") + else + report_info("using hashed trailer id %a (%a)",trailer_id,h) + end + trailerid = format("[<%s> <%s>]",h,h) + end + -- + local t = type(dates) + if t == "number" or t == "string" then + local d = converters.totime(dates) + if d then + included.date = true + included.id = "fake" + report_info("forced date/time information %a will be used",lpdf.settime(d)) + trailerid = false + elseif t == "string" then + dates = toboolean(dates) + included.date = dates + if dates ~= false then + included.id = true + else + report_info("no date/time but fake id information will be added") + trailerid = true + included.id = "fake" + end + end + end +end + +lpdf.registerdocumentfinalizer(update,"trailer id and dates",1) + +directives.register("backend.trailerid", function(v) trailerid = v end) +directives.register("backend.date", function(v) dates = v end) + +local function permitdetail(what) + local m = mapping[what] + if m then + return included[m[1]] and m[2] + else + return included[what] and true or false + end +end + +lpdf.permitdetail = permitdetail + +-- maybe some day we will load the xmp file at runtime + +local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml" + +local function setxmpfile(name) + if xmp then + report_xmp("discarding loaded file %a",xmpfile) + xmp = nil + end + xmpfile = name ~= "" and name +end + +codeinjections.setxmpfile = setxmpfile + +interfaces.implement { + name = "setxmpfile", + arguments = "string", + actions = setxmpfile +} + +local function valid_xmp() + if not xmp then + -- local xmpfile = xmpfile or resolvers.findfile(xmpname) or "" + if xmpfile and xmpfile ~= "" then + xmpfile = resolvers.findfile(xmpfile) or "" + end + if not xmpfile or xmpfile == "" then + xmpfile = resolvers.findfile(xmpname) or "" + end + if xmpfile ~= "" then + report_xmp("using file %a",xmpfile) + end + local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or "" + xmp = xml.convert(xmpdata) + end + return xmp +end + +function lpdf.addxmpinfo(tag,value,check) + local pattern = permitdetail(tag) + if type(pattern) == "string" then + xmlfillin(xmp or valid_xmp(),pattern,value,check) + end +end + +-- redefined + +local pdfaddtoinfo = lpdf.addtoinfo +local pdfaddxmpinfo = lpdf.addxmpinfo + +function lpdf.addtoinfo(tag,pdfvalue,strvalue) + local pattern = permitdetail(tag) + if pattern then + pdfaddtoinfo(tag,pdfvalue) + end + if type(pattern) == "string" then + local value = strvalue or gsub(tostring(pdfvalue),"^%((.*)%)$","%1") -- hack + if trace_info then + report_info("set %a to %a",tag,value) + end + xmlfillin(xmp or valid_xmp(),pattern,value,check) + end +end + +local pdfaddtoinfo = lpdf.addtoinfo -- used later + +-- for the do-it-yourselvers + +function lpdf.insertxmpinfo(pattern,whatever,prepend) + xml.insert(xmp or valid_xmp(),pattern,whatever,prepend) +end + +function lpdf.injectxmpinfo(pattern,whatever,prepend) + xml.inject(xmp or valid_xmp(),pattern,whatever,prepend) +end + +-- flushing + +local add_xmp_blob = true directives.register("backend.xmp",function(v) add_xmp_blob = v end) + +local function flushxmpinfo() + commands.pushrandomseed() + commands.setrandomseed(os.time()) + + local documentid = "no unique document id here" + local instanceid = "no unique instance id here" + local metadata = pdfgetmetadata() + local time = metadata.time + local producer = metadata.producer + local creator = metadata.creator + + if included.id ~= "fake" then + documentid = "uuid:" .. os.uuid() + instanceid = "uuid:" .. os.uuid() + end + + pdfaddtoinfo("Producer",producer) + pdfaddtoinfo("Creator",creator) + pdfaddtoinfo("CreationDate",time) + pdfaddtoinfo("ModDate",time) + + if add_xmp_blob then + + pdfaddxmpinfo("DocumentID",documentid) + pdfaddxmpinfo("InstanceID",instanceid) + pdfaddxmpinfo("Producer",producer) + pdfaddxmpinfo("CreatorTool",creator) + pdfaddxmpinfo("CreateDate",time) + pdfaddxmpinfo("ModifyDate",time) + pdfaddxmpinfo("MetadataDate",time) + pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion) + pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality) + pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion) + pdfaddxmpinfo("LuaTeX.Platform",metadata.platform) + + local blob = xml.tostring(xml.first(xmp or valid_xmp(),"/x:xmpmeta")) + local md = pdfdictionary { + Subtype = pdfconstant("XML"), + Type = pdfconstant("Metadata"), + } + if trace_xmp then + report_xmp("data flushed, see log file") + logs.pushtarget("logfile") + report_xmp("start xmp blob") + logs.newline() + logs.writer(blob) + logs.newline() + report_xmp("stop xmp blob") + logs.poptarget() + end + blob = format(xpacket,blob) + if not verbose and lpdf.compresslevel() > 0 then + blob = gsub(blob,">%s+<","><") + end + local r = pdfflushstreamobject(blob,md,false) -- uncompressed + lpdf.addtocatalog("Metadata",pdfreference(r)) + end + + commands.poprandomseed() -- hack +end + +-- this will be enabled when we can inhibit compression for a stream at the lua end + +lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata") + +directives.register("backend.verbosexmp", function(v) + verbose = v +end) diff --git a/tex/context/base/mkxl/math-ini.mkxl b/tex/context/base/mkxl/math-ini.mkxl index d806af427..2b1a54edf 100644 --- a/tex/context/base/mkxl/math-ini.mkxl +++ b/tex/context/base/mkxl/math-ini.mkxl @@ -315,18 +315,20 @@ % e.g.: \definemathematics[i:mp][setups=i:tight,openup=yes] -\newmuskip\defaultthickmuskip \defaultthickmuskip 5mu plus 5mu -\newmuskip\defaultmedmuskip \defaultmedmuskip 4mu plus 2mu minus 4mu -\newmuskip\defaultthinmuskip \defaultthinmuskip 3mu - -\newmuskip\halfthickmuskip \halfthickmuskip 2.5mu plus 2.5mu -\newmuskip\halfmedmuskip \halfmedmuskip 2.0mu plus 1.0mu minus 2.0mu -\newmuskip\halfthinmuskip \halfthinmuskip 1.5mu - -\newcount \defaultrelpenalty \defaultrelpenalty 500 -\newcount \defaultbinoppenalty \defaultbinoppenalty 700 -\newcount \defaultprerelpenalty \defaultprerelpenalty -100 -\newcount \defaultprebinoppenalty \defaultprebinoppenalty -100 +\immutable\mugluespecdef\defaultthickmuskip 5mu plus 5mu +\immutable\mugluespecdef\defaultmedmuskip 4mu plus 2mu minus 4mu +\immutable\mugluespecdef\defaultthinmuskip 3mu + +\immutable\mugluespecdef\halfthickmuskip 2.5mu plus 2.5mu +\immutable\mugluespecdef\halfmedmuskip 2.0mu plus 1.0mu minus 2.0mu +\immutable\mugluespecdef\halfthinmuskip 1.5mu + +\immutable\mugluespecdef\hairmuskip .15mu + +\immutable\integerdef \defaultrelpenalty 500 +\immutable\integerdef \defaultbinoppenalty 700 +\immutable\integerdef \defaultprerelpenalty -100 +\immutable\integerdef \defaultprebinoppenalty -100 % we need to control these otherwise: % diff --git a/tex/context/base/mkxl/pack-com.mkxl b/tex/context/base/mkxl/pack-com.mkxl index 9b39bf900..b70e30892 100644 --- a/tex/context/base/mkxl/pack-com.mkxl +++ b/tex/context/base/mkxl/pack-com.mkxl @@ -240,6 +240,7 @@ \pack_combinations_push \edef\currentcombination{#1}% \edef\currentcombinationspec{#2}% + % \ifempty\currentcombinationspec \ifcondition\validassignment{#1}% \let\currentcombination\empty @@ -263,6 +264,29 @@ \fi \fi % +% test first: +% +% \ifempty\currentcombinationspec +% \ifhastok={#1}% +% \let\currentcombination\empty +% \setupcurrentcombination[#1]% +% \edef\currentcombinationspec{\combinationparameter\c!nx*\combinationparameter\c!ny*}% +% \orelse\ifhastok*{\currentcombination}% +% \edef\currentcombinationspec{\currentcombination*\plusone*}% +% \let\currentcombination\empty +% \orelse\ifchknum\currentcombination\or +% \edef\currentcombinationspec{\currentcombination*\plusone*}% +% \let\currentcombination\empty +% \else +% \edef\currentcombinationspec{\combinationparameter\c!nx*\combinationparameter\c!ny*}% +% \fi +% \orelse\ifhastok={#2}% +% \setupcurrentcombination[#2]% +% \edef\currentcombinationspec{\combinationparameter\c!nx*\combinationparameter\c!ny*}% +% \else +% \edef\currentcombinationspec{\currentcombinationspec*\plusone*}% +% \fi + % \forgetall % \the\everycombination @@ -320,7 +344,7 @@ \def\pack_combinations_pickup {\dostarttagged\t!combinationpair\empty % better make this text \dostarttagged\t!combinationcontent\empty - \assumelongusagecs\pack_combinations_pickup_content_indeed} + \expandafterpars\pack_combinations_pickup_content_indeed} \def\pack_combinations_pickup_content_indeed {\dowithnextboxcs\pack_combinations_pickup_content\hbox} @@ -332,10 +356,10 @@ \expandnamespacemacro\??combinationalternative\p_pack_combinations_alternative\v!text} \setvalue{\??combinationalternative\v!text}% - {\assumelongusagecs\pack_combinations_alternative_text_indeed} + {\expandafterpars\pack_combinations_alternative_text_indeed} \setvalue{\??combinationalternative\v!label}% - {\assumelongusagecs\pack_combinations_alternative_label_indeed} + {\expandafterpars\pack_combinations_alternative_label_indeed} \def\pack_combinations_alternative_text_indeed {\dowithnextboxcs\pack_combinations_pickup_caption\vtop\bgroup @@ -393,7 +417,7 @@ \m_pack_combinations_valigner{\box\b_pack_combinations_content}% % we need to save the caption for a next alignment line \pack_combinations_save_caption}% - \unless\ifnum\c_pack_combinations_y>\plusone + \ifnum\c_pack_combinations_y>\plusone \global\advance\c_pack_combinations_y\minusone \global\advance\c_pack_combinations_x\minusone \ifcase\c_pack_combinations_x @@ -705,7 +729,7 @@ % \globalsetsystemmode{pairedbox}% \pack_pairedboxes_before - \assumelongusagecs\pack_pairedboxes_first_pickup} + \expandafterpars\pack_pairedboxes_first_pickup} \permanent\protected\def\stopplacepairedbox{} % we just pick up two boxes @@ -718,7 +742,7 @@ \def\pack_pairedboxes_first {\pack_pairedboxes_between - \assumelongusagecs\pack_pairedboxes_second_pickup} + \expandafterpars\pack_pairedboxes_second_pickup} \def\pack_pairedboxes_second_pickup {\dowithnextboxcs\pack_pairedboxes_second\vbox diff --git a/tex/context/base/mkxl/publ-ini.mkxl b/tex/context/base/mkxl/publ-ini.mkxl index 6b520074a..c9f249f83 100644 --- a/tex/context/base/mkxl/publ-ini.mkxl +++ b/tex/context/base/mkxl/publ-ini.mkxl @@ -713,7 +713,7 @@ \dostoptagged \endgroup} -\permanent\protected\def\btxshowentryinline[#1]#*[#2]% +\permanent\tolerant\protected\def\btxshowentryinline[#1]#*[#2]% {\ifarguments \ctxcommand{showbtxentry("\currentbtxdataset","\currentbtxtag")} \or diff --git a/tex/context/base/mkxl/spac-hor.mkxl b/tex/context/base/mkxl/spac-hor.mkxl index 4c009c552..426580c7d 100644 --- a/tex/context/base/mkxl/spac-hor.mkxl +++ b/tex/context/base/mkxl/spac-hor.mkxl @@ -1000,8 +1000,6 @@ \permanent\protected\def\textormathspacecommand #1#2#3{\ifmmode\mskip#1#2\else#3\fi\relax} \permanent\protected\def\breakabletextormathspace#1#2#3{\ifmmode\mskip#1#2\else\hskip#1\hspaceamount\empty{#3}\fi\relax} -\newmuskip\hairmuskip \hairmuskip=.15mu - \overloaded\permanent\protected \def\hairspace {\textormathspace+\hairmuskip{.5}} \overloaded\permanent\protected \def\thinspace {\textormathspace+\thinmuskip 1} %overloaded\permanent\protected \def\medspace {\textormathspace+\medmuskip 2} % 4/18 em diff --git a/tex/context/base/mkxl/syst-ini.mkxl b/tex/context/base/mkxl/syst-ini.mkxl index bdae879fa..5e9d55559 100644 --- a/tex/context/base/mkxl/syst-ini.mkxl +++ b/tex/context/base/mkxl/syst-ini.mkxl @@ -461,21 +461,21 @@ % \newdimen \scaledpoint \immutable\scaledpoint 1sp % \newdimen \thousandpoint \immutable\thousandpoint 1000pt -\immutable\integerdef \maxcount 2147483647 +\immutable\integerdef \maxcount 2147483647 -\immutable\dimensiondef \zeropoint 0pt -\immutable\dimensiondef \onepoint 1pt -\immutable\dimensiondef \halfapoint 0.5pt -\immutable\dimensiondef \maxdimen 16383.99999pt % 1073741823sp -\immutable\dimensiondef \onebasepoint 1bp -\immutable\dimensiondef \scaledpoint 1sp -\immutable\dimensiondef \thousandpoint 1000pt +\immutable\dimensiondef \zeropoint 0pt +\immutable\dimensiondef \onepoint 1pt +\immutable\dimensiondef \halfapoint 0.5pt +\immutable\dimensiondef \maxdimen 16383.99999pt % 1073741823sp +\immutable\dimensiondef \onebasepoint 1bp +\immutable\dimensiondef \scaledpoint 1sp +\immutable\dimensiondef \thousandpoint 1000pt -\newskip \zeroskip \immutable\zeroskip 0pt plus 0pt minus 0pt +\immutable\gluespecdef \zeroskip 0pt plus 0pt minus 0pt -\newmuskip\zeromuskip \immutable\zeromuskip 0mu -\newmuskip\onemuskip \immutable\onemuskip 1mu -\newmuskip\muquad \immutable\muquad 18mu +\immutable\mugluespecdef \zeromuskip 0mu +\immutable\mugluespecdef \onemuskip 1mu +\immutable\mugluespecdef \muquad 18mu \aliased\let\points \onepoint \aliased\let\halfpoint\halfapoint diff --git a/tex/generic/context/luatex/luatex-fonts-merged.lua b/tex/generic/context/luatex/luatex-fonts-merged.lua index a90920888..23171b9bf 100644 --- a/tex/generic/context/luatex/luatex-fonts-merged.lua +++ b/tex/generic/context/luatex/luatex-fonts-merged.lua @@ -1,6 +1,6 @@ -- merged file : c:/data/develop/context/sources/luatex-fonts-merged.lua -- parent file : c:/data/develop/context/sources/luatex-fonts.lua --- merge date : 2020-11-30 10:20 +-- merge date : 2020-12-01 17:48 do -- begin closure to overcome local limits and interference @@ -37169,7 +37169,7 @@ local function setmathcharacters(tfmdata,characters,mathparameters,dx,dy,squeeze end end end -local shiftmode=CONTEXTLMTXMODE>0 +local shiftmode=CONTEXTLMTXMODE and CONTEXTLMTXMODE>0 local function manipulateeffect(tfmdata) local effect=tfmdata.properties.effect if effect then |