diff options
Diffstat (limited to 'tex/context/base/mkxl/lpdf-fld.lmt')
-rw-r--r-- | tex/context/base/mkxl/lpdf-fld.lmt | 1501 |
1 files changed, 1501 insertions, 0 deletions
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 |