summaryrefslogtreecommitdiff
path: root/tex
diff options
context:
space:
mode:
Diffstat (limited to 'tex')
-rw-r--r--tex/context/base/mkii/cont-new.mkii2
-rw-r--r--tex/context/base/mkii/context.mkii2
-rw-r--r--tex/context/base/mkiv/cont-new.mkiv2
-rw-r--r--tex/context/base/mkiv/context.mkiv2
-rw-r--r--tex/context/base/mkiv/font-imp-effects.lua2
-rw-r--r--tex/context/base/mkiv/l-os.lua2
-rw-r--r--tex/context/base/mkiv/lpdf-ini.lua3
-rw-r--r--tex/context/base/mkiv/mult-prm.lua2
-rw-r--r--tex/context/base/mkiv/status-files.pdfbin26204 -> 26137 bytes
-rw-r--r--tex/context/base/mkiv/status-lua.pdfbin256055 -> 256029 bytes
-rw-r--r--tex/context/base/mkxl/back-pdf.lmt55
-rw-r--r--tex/context/base/mkxl/back-pdf.mkxl38
-rw-r--r--tex/context/base/mkxl/back-pdp.lmt291
-rw-r--r--tex/context/base/mkxl/cont-new.mkxl2
-rw-r--r--tex/context/base/mkxl/context.mkxl2
-rw-r--r--tex/context/base/mkxl/enco-ini.mkxl2
-rw-r--r--tex/context/base/mkxl/lpdf-ano.lmt1401
-rw-r--r--tex/context/base/mkxl/lpdf-aux.lmt152
-rw-r--r--tex/context/base/mkxl/lpdf-col.lmt845
-rw-r--r--tex/context/base/mkxl/lpdf-emb.lmt61
-rw-r--r--tex/context/base/mkxl/lpdf-enc.lmt157
-rw-r--r--tex/context/base/mkxl/lpdf-epa.lmt1105
-rw-r--r--tex/context/base/mkxl/lpdf-fld.lmt1501
-rw-r--r--tex/context/base/mkxl/lpdf-fmt.lmt1020
-rw-r--r--tex/context/base/mkxl/lpdf-fnt.lmt194
-rw-r--r--tex/context/base/mkxl/lpdf-grp.lmt300
-rw-r--r--tex/context/base/mkxl/lpdf-img.lmt12
-rw-r--r--tex/context/base/mkxl/lpdf-ini.lmt1366
-rw-r--r--tex/context/base/mkxl/lpdf-lmt.lmt481
-rw-r--r--tex/context/base/mkxl/lpdf-mis.lmt668
-rw-r--r--tex/context/base/mkxl/lpdf-mov.lmt68
-rw-r--r--tex/context/base/mkxl/lpdf-pde.lmt1211
-rw-r--r--tex/context/base/mkxl/lpdf-ren.lmt399
-rw-r--r--tex/context/base/mkxl/lpdf-res.lmt41
-rw-r--r--tex/context/base/mkxl/lpdf-tag.lmt740
-rw-r--r--tex/context/base/mkxl/lpdf-u3d.lmt495
-rw-r--r--tex/context/base/mkxl/lpdf-wid.lmt789
-rw-r--r--tex/context/base/mkxl/lpdf-xmp.lmt311
-rw-r--r--tex/context/base/mkxl/math-ini.mkxl26
-rw-r--r--tex/context/base/mkxl/pack-com.mkxl36
-rw-r--r--tex/context/base/mkxl/publ-ini.mkxl2
-rw-r--r--tex/context/base/mkxl/spac-hor.mkxl2
-rw-r--r--tex/context/base/mkxl/syst-ini.mkxl24
-rw-r--r--tex/generic/context/luatex/luatex-fonts-merged.lua4
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
index dc575dca5..ea407d533 100644
--- a/tex/context/base/mkiv/status-files.pdf
+++ b/tex/context/base/mkiv/status-files.pdf
Binary files differ
diff --git a/tex/context/base/mkiv/status-lua.pdf b/tex/context/base/mkiv/status-lua.pdf
index 216c5f4c9..97cefc1a8 100644
--- a/tex/context/base/mkiv/status-lua.pdf
+++ b/tex/context/base/mkiv/status-lua.pdf
Binary files differ
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