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 pdfflushobject = lpdf.flushobject local pdfshareobjectreference = lpdf.shareobjectreference local pdfshareobject = lpdf.shareobject local pdfreserveobject = lpdf.reserveobject local pdfpagereference = lpdf.pagereference local pdfaction = lpdf.action local pdfmajorversion = lpdf.majorversion local pdfcolor = lpdf.color local pdfcolorvalues = lpdf.colorvalues local pdflayerreference = lpdf.layerreference local hpack_node = nodes.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.lua")) -- 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 = [[ %s ]] function codeinjections.exportformdata(name) local result = { } for k, v in sortedhash(fields) do result[#result+1] = formatters[" %s"](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