lpdf-fld.lmt /size: 52 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-fld'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- TURN OFF: preferences -> forms -> highlight -> etc
10
11-- The problem with widgets is that so far each version of acrobat has some
12-- rendering problem. I tried to keep up with this but it makes no sense to do so as
13-- one cannot rely on the viewer not changing. Especially Btn fields are tricky as
14-- their appearences need to be synchronized in the case of children but e.g.
15-- acrobat 10 does not retain the state and forces a check symbol. If you make a
16-- file in acrobat then it has MK entries that seem to overload the already present
17-- appearance streams (they're probably only meant for printing) as it looks like
18-- the viewer has some fallback on (auto generated) MK behaviour built in. So ...
19-- hard to test. Unfortunately not even the default appearance is generated. This
20-- will probably be solved at some point.
21--
22-- Also, for some reason the viewer does not always show custom appearances when
23-- fields are being rolled over or clicked upon, and circles or checks pop up when
24-- you don't expect them. I fear that this kind of instability eventually will kill
25-- pdf forms. After all, the manual says: "individual annotation handlers may ignore
26-- this entry and provide their own appearances" and one might wonder what
27-- 'individual' means here, but effectively this renders the whole concept of
28-- appearances useless.
29--
30-- Okay, here is one observation. A pdf file contains objects and one might consider
31-- each one to be a static entity when read in. However, acrobat starts rendering
32-- and seems to manipulate (appearance streams) of objects in place (this is visible
33-- when the file is saved again). And, combined with some other caching and hashing,
34-- this might give side effects for shared objects. So, it seems that for some cases
35-- one can best be not too clever and not share but duplicate information. Of course
36-- this defeats the whole purpose of these objects. Of course I can be wrong.
37--
38-- A rarther weird side effect of the viewer is that the highlighting of fields
39-- obscures values, unless you uses one of the BS variants, and this makes custum
40-- appearances rather useless as there is no way to control this apart from changing
41-- the viewer preferences. It could of course be a bug but it would be nice if the
42-- highlighting was at least transparent. I have no clue why the built in shapes
43-- work ok (some xform based appearances are generated) while equally valid other
44-- xforms fail. It looks like acrobat appearances come on top (being refered to in
45-- the MK) while custom ones are behind the highlight rectangle. One can disable the
46-- "Show border hover color for fields" option in the preferences. If you load
47-- java-imp-rhh this side effect gets disabled and you get what you expect (it took
48-- me a while to figure out this hack).
49--
50-- When highlighting is enabled, those default symbols flash up, so it looks like we
51-- have some inteference between this setting and custom appearances.
52--
53-- Anyhow, the NeedAppearances is really needed in order to get a rendering for
54-- printing especially when highlighting (those colorfull foregrounds) is on.
55
56local tostring, tonumber, next = tostring, tonumber, next
57local gmatch, lower, format, formatters = string.gmatch, string.lower, string.format, string.formatters
58local lpegmatch = lpeg.match
59local todimen = string.todimen
60local sortedhash = table.sortedhash
61local trace_fields = false  trackers.register("backend.fields", function(v) trace_fields = v end)
62
63local report_fields = logs.reporter("backend","fields")
64
65local context                 = context
66
67local bpfactor                <const> = number.dimenfactors.bp
68
69local references              = structures.references
70local settings_to_array       = utilities.parsers.settings_to_array
71
72local pdfbackend              = backends.registered.pdf
73local nodeinjections          = pdfbackend.nodeinjections
74local codeinjections          = pdfbackend.codeinjections
75local registrations           = pdfbackend.registrations
76
77local registeredsymbol        = codeinjections.registeredsymbol
78
79local lpdf                    = lpdf
80local pdfstream               = lpdf.stream
81local pdfdictionary           = lpdf.dictionary
82local pdfarray                = lpdf.array
83local pdfreference            = lpdf.reference
84local pdfunicode              = lpdf.unicode
85local pdfstring               = lpdf.string
86local pdfconstant             = lpdf.constant
87local pdfaction               = lpdf.action
88local pdfflushobject          = lpdf.flushobject
89local pdfshareobjectreference = lpdf.shareobjectreference
90local pdfshareobject          = lpdf.shareobject
91local pdfreserveobject        = lpdf.reserveobject
92local pdfpagereference        = lpdf.pagereference
93local pdfmajorversion         = lpdf.majorversion
94local pdfcolor                = lpdf.color
95local pdfcolorvalues          = lpdf.colorvalues
96local pdflayerreference       = lpdf.layerreference
97
98local hpack_node              = nodes.hpack
99
100local submitoutputformat      = 0 --  0=unknown 1=HTML 2=FDF 3=XML   => not yet used, needs to be checked
101
102local pdf_widget              = pdfconstant("Widget")
103local pdf_tx                  = pdfconstant("Tx")
104local pdf_sig                 = pdfconstant("Sig")
105local pdf_ch                  = pdfconstant("Ch")
106local pdf_btn                 = pdfconstant("Btn")
107local pdf_yes                 = pdfconstant("Yes")
108local pdf_off                 = pdfconstant("Off")
109local pdf_p                   = pdfconstant("P") -- None Invert Outline Push
110local pdf_n                   = pdfconstant("N") -- None Invert Outline Push
111--
112local pdf_no_rect             = pdfarray { 0, 0, 0, 0 }
113
114local signature               = nil
115
116local splitter = lpeg.splitat("=>")
117
118local formats = {
119    html = 1, fdf = 2, xml = 3,
120}
121
122function codeinjections.setformsmethod(name)
123    submitoutputformat = formats[lower(name)] or formats.xml
124end
125
126local flag = { -- /Ff
127    ReadOnly          = 0x00000001, -- 2^ 0
128    Required          = 0x00000002, -- 2^ 1
129    NoExport          = 0x00000004, -- 2^ 2
130    MultiLine         = 0x00001000, -- 2^12
131    Password          = 0x00002000, -- 2^13
132    NoToggleToOff     = 0x00004000, -- 2^14
133    Radio             = 0x00008000, -- 2^15
134    PushButton        = 0x00010000, -- 2^16
135    PopUp             = 0x00020000, -- 2^17
136    Edit              = 0x00040000, -- 2^18
137    Sort              = 0x00080000, -- 2^19
138    FileSelect        = 0x00100000, -- 2^20
139    DoNotSpellCheck   = 0x00400000, -- 2^22
140    DoNotScroll       = 0x00800000, -- 2^23
141    Comb              = 0x01000000, -- 2^24
142    RichText          = 0x02000000, -- 2^25
143    RadiosInUnison    = 0x02000000, -- 2^25
144    CommitOnSelChange = 0x04000000, -- 2^26
145}
146
147local plus = { -- /F
148    Invisible         = 0x00000001, -- 2^0
149    Hidden            = 0x00000002, -- 2^1
150    Printable         = 0x00000004, -- 2^2
151    Print             = 0x00000004, -- 2^2
152    NoZoom            = 0x00000008, -- 2^3
153    NoRotate          = 0x00000010, -- 2^4
154    NoView            = 0x00000020, -- 2^5
155    ReadOnly          = 0x00000040, -- 2^6
156    Locked            = 0x00000080, -- 2^7
157    ToggleNoView      = 0x00000100, -- 2^8
158    LockedContents    = 0x00000200, -- 2^9
159    AutoView          = 0x00000100, -- 2^8
160}
161
162-- todo: check what is interfaced
163
164flag.readonly    = flag.ReadOnly
165flag.required    = flag.Required
166flag.protected   = flag.Password
167flag.sorted      = flag.Sort
168flag.unavailable = flag.NoExport
169flag.nocheck     = flag.DoNotSpellCheck
170flag.fixed       = flag.DoNotScroll
171flag.file        = flag.FileSelect
172
173plus.hidden      = plus.Hidden
174plus.printable   = plus.Printable
175plus.auto        = plus.AutoView
176
177lpdf.flags.widgets     = flag
178lpdf.flags.annotations = plus
179
180-- some day .. lpeg with function or table
181
182local function fieldflag(specification) -- /Ff
183    local o, n = specification.option, 0
184    if o and o ~= "" then
185        for f in gmatch(o,"[^, ]+") do
186         -- n = n + (flag[f] or 0)
187            n = n | (flag[f] or 0)
188        end
189    end
190    return n
191end
192
193local function fieldplus(specification) -- /F
194    local o, n = specification.option, 0
195    if o and o ~= "" then
196        for p in gmatch(o,"[^, ]+") do
197         -- n = n + (plus[p] or 0)
198            n = n | (plus[p] or 0)
199        end
200    end
201    return n
202end
203
204-- keep:
205--
206-- local function checked(what)
207--     local set, bug = references.identify("",what)
208--     if not bug and #set > 0 then
209--         local r, n = pdfaction(set)
210--         return pdfshareobjectreference(r)
211--     end
212-- end
213--
214-- local function fieldactions(specification) -- share actions
215--     local d, a = { }, nil
216--     a = specification.mousedown
217--      or specification.clickin           if a and a ~= "" then d.D  = checked(a) end
218--     a = specification.mouseup
219--      or specification.clickout          if a and a ~= "" then d.U  = checked(a) end
220--     a = specification.regionin          if a and a ~= "" then d.E  = checked(a) end -- Enter
221--     a = specification.regionout         if a and a ~= "" then d.X  = checked(a) end -- eXit
222--     a = specification.afterkey          if a and a ~= "" then d.K  = checked(a) end
223--     a = specification.format            if a and a ~= "" then d.F  = checked(a) end
224--     a = specification.validate          if a and a ~= "" then d.V  = checked(a) end
225--     a = specification.calculate         if a and a ~= "" then d.C  = checked(a) end
226--     a = specification.focusin           if a and a ~= "" then d.Fo = checked(a) end
227--     a = specification.focusout          if a and a ~= "" then d.Bl = checked(a) end
228--     a = specification.openpage          if a and a ~= "" then d.PO = checked(a) end
229--     a = specification.closepage         if a and a ~= "" then d.PC = checked(a) end
230--  -- a = specification.visiblepage       if a and a ~= "" then d.PV = checked(a) end
231--  -- a = specification.invisiblepage     if a and a ~= "" then d.PI = checked(a) end
232--     return next(d) and pdfdictionary(d)
233-- end
234
235local mapping = {
236    mousedown         = "D",    clickin  = "D",
237    mouseup           = "U",    clickout = "U",
238    regionin          = "E",
239    regionout         = "X",
240    afterkey          = "K",
241    format            = "F",
242    validate          = "V",
243    calculate         = "C",
244    focusin           = "Fo",
245    focusout          = "Bl",
246    openpage          = "PO",
247    closepage         = "PC",
248 -- visiblepage       = "PV",
249 -- invisiblepage     = "PI",
250}
251
252local function fieldactions(specification) -- share actions
253    local d = nil
254    for key, target in sortedhash(mapping) do -- sort so that we can compare pdf
255        local code = specification[key]
256        if code and code ~= "" then
257         -- local a = checked(code)
258            local set, bug = references.identify("",code)
259            if not bug and #set > 0 then
260                local a = pdfaction(set) -- r, n
261                if a then
262                    local r = pdfshareobjectreference(a)
263                    if d then
264                        d[target] = r
265                    else
266                        d = pdfdictionary { [target] = r }
267                    end
268                else
269                    report_fields("invalid field action %a, case %s",code,2)
270                end
271            else
272                report_fields("invalid field action %a, case %s",code,1)
273            end
274        end
275    end
276 -- if d then
277 --     d = pdfshareobjectreference(d) -- not much overlap or maybe only some patterns
278 -- end
279    return d
280end
281
282-- fonts and color
283
284local pdfdocencodingvector, pdfdocencodingcapsule
285
286-- The pdf doc encoding vector is needed in order to trigger propper unicode. Interesting is that when
287-- a glyph is not in the vector, it is still visible as it is taken from some other font. Messy.
288
289-- To be checked: only when text/line fields.
290
291local function checkpdfdocencoding()
292    report_fields("adding pdfdoc encoding vector")
293    local encoding = dofile(resolvers.findfile("lpdf-enc.lmt")) -- no checking, fatal if not present
294    pdfdocencodingvector = pdfreference(pdfflushobject(encoding))
295    local capsule = pdfdictionary {
296        PDFDocEncoding = pdfdocencodingvector
297    }
298    pdfdocencodingcapsule = pdfreference(pdfflushobject(capsule))
299    checkpdfdocencoding = function() end
300end
301
302local fontnames = {
303    rm = {
304        tf = "Times-Roman",
305        bf = "Times-Bold",
306        it = "Times-Italic",
307        sl = "Times-Italic",
308        bi = "Times-BoldItalic",
309        bs = "Times-BoldItalic",
310    },
311    ss = {
312        tf = "Helvetica",
313        bf = "Helvetica-Bold",
314        it = "Helvetica-Oblique",
315        sl = "Helvetica-Oblique",
316        bi = "Helvetica-BoldOblique",
317        bs = "Helvetica-BoldOblique",
318    },
319    tt = {
320        tf = "Courier",
321        bf = "Courier-Bold",
322        it = "Courier-Oblique",
323        sl = "Courier-Oblique",
324        bi = "Courier-BoldOblique",
325        bs = "Courier-BoldOblique",
326    },
327    symbol = {
328        dingbats = "ZapfDingbats",
329    }
330}
331
332local usedfonts = { }
333
334local function fieldsurrounding(specification)
335    local fontsize        = specification.fontsize or "12pt"
336    local fontstyle       = specification.fontstyle or "rm"
337    local fontalternative = specification.fontalternative or "tf"
338    local colorvalue      = tonumber(specification.colorvalue)
339    local s = fontnames[fontstyle]
340    if not s then
341        fontstyle, s = "rm", fontnames.rm
342    end
343    local a = s[fontalternative]
344    if not a then
345        alternative, a = "tf", s.tf
346    end
347    local tag = fontstyle .. fontalternative
348    fontsize  = todimen(fontsize)
349    fontsize  = fontsize and (bpfactor * fontsize) or 12
350    fontraise = 0.1 * fontsize -- todo: figure out what the natural one is and compensate for strutdp
351    local fontcode  = formatters["%0.4F Tf %0.4F Ts"](fontsize,fontraise)
352    -- we could test for colorvalue being 1 (black) and omit it then
353    local colorcode = pdfcolor(3,colorvalue) -- we force an rgb color space
354    if trace_fields then
355        report_fields("using font, style %a, alternative %a, size %p, tag %a, code %a",fontstyle,fontalternative,fontsize,tag,fontcode)
356        report_fields("using color, value %a, code %a",colorvalue,colorcode)
357    end
358    local stream = pdfstream {
359        pdfconstant(tag),
360        formatters["%s %s"](fontcode,colorcode)
361    }
362    usedfonts[tag] = a -- the name
363    -- move up with "x.y Ts"
364    return tostring(stream)
365end
366
367-- Can we use any font?
368
369codeinjections.fieldsurrounding = fieldsurrounding
370
371local function registerfonts()
372    if next(usedfonts) then
373        checkpdfdocencoding() -- already done
374        local pdffontlist    = pdfdictionary()
375        local pdffonttype    = pdfconstant("Font")
376        local pdffontsubtype = pdfconstant("Type1")
377        for tag, name in sortedhash(usedfonts) do
378            local f = pdfdictionary {
379                Type     = pdffonttype,
380                Subtype  = pdffontsubtype,
381                Name     = pdfconstant(tag),
382                BaseFont = pdfconstant(name),
383                Encoding = pdfdocencodingvector,
384            }
385            pdffontlist[tag] = pdfreference(pdfflushobject(f))
386        end
387        return pdffontlist
388    end
389end
390
391-- symbols
392
393local function fieldappearances(specification)
394    -- todo: caching
395    local values  = specification.values
396    local default = specification.default -- todo
397    if not values then
398        -- error
399        return
400    end
401    local v = settings_to_array(values)
402    local n, r, d
403    if #v == 1 then
404        n, r, d = v[1], v[1], v[1]
405    elseif #v == 2 then
406        n, r, d = v[1], v[1], v[2]
407    else
408        n, r, d = v[1], v[2], v[3]
409    end
410    local appearance = pdfdictionary {
411        N = registeredsymbol(n),
412        R = registeredsymbol(r),
413        D = registeredsymbol(d),
414    }
415    return pdfshareobjectreference(appearance)
416--     return pdfreference(pdfflushobject(appearance))
417end
418
419-- The rendering part of form support has always been crappy and didn't really
420-- improve over time. Did bugs become features? Who knows. Why provide for instance
421-- control over appearance and then ignore it when the mouse clicks someplace else.
422-- Strangely enough a lot of effort went into JavaScript support while basic
423-- appearance control of checkboxes stayed poor. I found this link when googling for
424-- conformation after the n^th time looking into this behaviour:
425--
426-- https://stackoverflow.com/questions/15479855/pdf-appearance-streams-checkbox-not-shown-correctly-after-focus-lost
427--
428-- ... "In particular check boxes, therefore, whenever not interacting with the user, shall
429-- be displayed using their normal captions, not their appearances."
430--
431-- So: don't use check boxes. In fact, even radio buttons can have these funny "flash some
432-- funny symbol" side effect when clocking on them. I tried all combinations if /H and /AP
433-- and /AS and ... Because (afaiks) the acrobat interface assumes that one uses dingbats no
434-- one really cared about getting custom appeances done well. This erratic behaviour might
435-- as well be the reason why no open source viewer ever bothered implementing forms. It's
436-- probably also why most forms out there look kind of bad.
437
438local function fieldstates_precheck(specification)
439    local values  = specification.values
440    local default = specification.default
441    if not values or values == "" then
442        return
443    end
444    local yes = settings_to_array(values)[1]
445    local yesshown, yesvalue = lpegmatch(splitter,yes)
446    if not (yesshown and yesvalue) then
447        yesshown = yes
448    end
449    return default == settings_to_array(yesshown)[1] and pdf_yes or pdf_off
450end
451
452local function fieldstates_check(specification)
453    -- we don't use Opt here (too messy for radio buttons)
454    local values  = specification.values
455    local default = specification.default
456    if not values or values == "" then
457        -- error
458        return
459    end
460    local v = settings_to_array(values)
461    local yes, off, yesn, yesr, yesd, offn, offr, offd
462    if #v == 1 then
463        yes, off = v[1], v[1]
464    else
465        yes, off = v[1], v[2]
466    end
467    local yesshown, yesvalue = lpegmatch(splitter,yes)
468    if not (yesshown and yesvalue) then
469        yesshown = yes, yes
470    end
471    yes = settings_to_array(yesshown)
472    local offshown, offvalue = lpegmatch(splitter,off)
473    if not (offshown and offvalue) then
474        offshown = off, off
475    end
476    off = settings_to_array(offshown)
477    if #yes == 1 then
478        yesn, yesr, yesd = yes[1], yes[1], yes[1]
479    elseif #yes == 2 then
480        yesn, yesr, yesd = yes[1], yes[1], yes[2]
481    else
482        yesn, yesr, yesd = yes[1], yes[2], yes[3]
483    end
484    if #off == 1 then
485        offn, offr, offd = off[1], off[1], off[1]
486    elseif #off == 2 then
487        offn, offr, offd = off[1], off[1], off[2]
488    else
489        offn, offr, offd = off[1], off[2], off[3]
490    end
491    if not yesvalue then
492        yesvalue = yesdefault or yesn
493    end
494    if not offvalue then
495        offvalue = offn
496    end
497    if default == yesn then
498        default  = pdf_yes
499        yesvalue = yesvalue == yesn and "Yes" or "Off"
500    else
501        default  = pdf_off
502        yesvalue = "Off"
503    end
504    local appearance
505 -- if false then
506    if true then
507        -- needs testing
508        appearance = pdfdictionary { -- maybe also cache components
509            N = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesn), Off = registeredsymbol(offn) }),
510            R = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesr), Off = registeredsymbol(offr) }),
511            D = pdfshareobjectreference(pdfdictionary { Yes = registeredsymbol(yesd), Off = registeredsymbol(offd) }),
512        }
513    else
514        appearance = pdfdictionary { -- maybe also cache components
515            N = pdfdictionary { Yes = registeredsymbol(yesn), Off = registeredsymbol(offn) },
516            R = pdfdictionary { Yes = registeredsymbol(yesr), Off = registeredsymbol(offr) },
517            D = pdfdictionary { Yes = registeredsymbol(yesd), Off = registeredsymbol(offd) }
518        }
519    end
520    local appearanceref = pdfshareobjectreference(appearance)
521 -- local appearanceref = pdfreference(pdfflushobject(appearance))
522    return appearanceref, default, yesvalue
523end
524
525-- It looks like there is always a (MK related) symbol used and that the appearances
526-- are only used as ornaments behind a symbol. So, contrary to what we did when
527-- widgets showed up, we now limit ourself to more dumb definitions. Especially when
528-- highlighting is enabled weird interferences happen. So, we play safe (some nice
529-- code has been removed that worked well till recently).
530
531local function fieldstates_radio(specification,name,parent)
532    local values  = values  or specification.values
533    local default = default or parent.default -- specification.default
534    if not values or values == "" then
535        -- error
536        return
537    end
538    local v = settings_to_array(values)
539    local yes, off, yesn, yesr, yesd, offn, offr, offd
540    if #v == 1 then
541        yes, off = v[1], v[1]
542    else
543        yes, off = v[1], v[2]
544    end
545    -- yes keys might be the same in the three appearances within a field
546    -- but can best be different among fields ... don't ask why
547    local yessymbols, yesvalue = lpegmatch(splitter,yes) -- n,r,d=>x
548    if not (yessymbols and yesvalue) then
549        yessymbols = yes
550    end
551    if not yesvalue then
552        yesvalue = name
553    end
554    yessymbols = settings_to_array(yessymbols)
555    if #yessymbols == 1 then
556        yesn = yessymbols[1]
557        yesr = yesn
558        yesd = yesr
559    elseif #yessymbols == 2 then
560        yesn = yessymbols[1]
561        yesr = yessymbols[2]
562        yesd = yesr
563    else
564        yesn = yessymbols[1]
565        yesr = yessymbols[2]
566        yesd = yessymbols[3]
567    end
568    -- we don't care about names, as all will be /Off
569    local offsymbols = lpegmatch(splitter,off) or off
570    offsymbols = settings_to_array(offsymbols)
571    if #offsymbols == 1 then
572        offn = offsymbols[1]
573        offr = offn
574        offd = offr
575    elseif #offsymbols == 2 then
576        offn = offsymbols[1]
577        offr = offsymbols[2]
578        offd = offr
579    else
580        offn = offsymbols[1]
581        offr = offsymbols[2]
582        offd = offsymbols[3]
583    end
584    if default == name then
585        default = pdfconstant(name)
586    else
587        default = pdf_off
588    end
589    --
590    local appearance
591    if false then -- needs testing
592        appearance = pdfdictionary { -- maybe also cache components
593            N = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesn), Off = registeredsymbol(offn) }),
594            R = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesr), Off = registeredsymbol(offr) }),
595            D = pdfshareobjectreference(pdfdictionary { [name] = registeredsymbol(yesd), Off = registeredsymbol(offd) }),
596        }
597    else
598        appearance = pdfdictionary { -- maybe also cache components
599            N = pdfdictionary { [name] = registeredsymbol(yesn), Off = registeredsymbol(offn) },
600            R = pdfdictionary { [name] = registeredsymbol(yesr), Off = registeredsymbol(offr) },
601            D = pdfdictionary { [name] = registeredsymbol(yesd), Off = registeredsymbol(offd) }
602        }
603    end
604    local appearanceref = pdfshareobjectreference(appearance) -- pdfreference(pdfflushobject(appearance))
605    return appearanceref, default, yesvalue
606end
607
608local function fielddefault(field,pdf_yes)
609    local default = field.default
610    if not default or default == "" then
611        local values = settings_to_array(field.values)
612        default = values[1]
613    end
614    if not default or default == "" then
615        return pdf_off
616    else
617        return pdf_yes or pdfconstant(default)
618    end
619end
620
621local function fieldoptions(specification)
622    local values  = specification.values
623    local default = specification.default
624    if values then
625        local v = settings_to_array(values)
626        for i=1,#v do
627            local vi = v[i]
628            local shown, value = lpegmatch(splitter,vi)
629            if shown and value then
630                v[i] = pdfarray { pdfunicode(value), shown }
631            else
632                v[i] = pdfunicode(v[i])
633            end
634        end
635        return pdfarray(v)
636    end
637end
638
639local mapping = {
640    -- acrobat compliant (messy, probably some pdfdoc encoding interference here)
641    check   = "4", -- 0x34
642    circle  = "l", -- 0x6C
643    cross   = "8", -- 0x38
644    diamond = "u", -- 0x75
645    square  = "n", -- 0x6E
646    star    = "H", -- 0x48
647}
648
649local function todingbat(n)
650    if n and n ~= "" then
651        return mapping[n] or ""
652    end
653end
654
655local function fieldrendering(specification)
656    local bvalue = tonumber(specification.backgroundcolorvalue)
657    local fvalue = tonumber(specification.framecolorvalue)
658    local svalue = specification.fontsymbol
659    if bvalue or fvalue or (svalue and svalue ~= "") then
660        return pdfdictionary {
661            BG = bvalue and pdfarray { pdfcolorvalues(3,bvalue) } or nil, -- or zero_bg,
662            BC = fvalue and pdfarray { pdfcolorvalues(3,fvalue) } or nil, -- or zero_bc,
663            CA = svalue and pdfstring (svalue) or nil,
664        }
665    end
666end
667
668-- layers
669
670local function fieldlayer(specification) -- we can move this in line
671    local layer = specification.layer
672    return (layer and pdflayerreference(layer)) or nil
673end
674
675-- defining
676
677local fields, radios, clones, fieldsets, calculationset = { }, { }, { }, { }, nil
678
679local xfdftemplate <const> = [[
680<?xml version='1.0' encoding='UTF-8'?>
681
682<xfdf xmlns='http://ns.adobe.com/xfdf/'>
683  <f href='%s.pdf'/>
684  <fields>
685%s
686  </fields>
687</xfdf>
688]]
689
690function codeinjections.exportformdata(name)
691    local result = { }
692    for k, v in sortedhash(fields) do
693        result[#result+1] = formatters["    <field name='%s'><value>%s</value></field>"](v.name or k,v.default or "")
694    end
695    local base = file.basename(tex.jobname)
696    local xfdf = format(xfdftemplate,base,table.concat(result,"\n"))
697    if not name or name == "" then
698        name = base
699    end
700    io.savedata(file.addsuffix(name,"xfdf"),xfdf)
701end
702
703function codeinjections.definefieldset(tag,list)
704    fieldsets[tag] = list
705end
706
707function codeinjections.getfieldset(tag)
708    return fieldsets[tag]
709end
710
711local function fieldsetlist(tag)
712    if tag then
713        local ft = fieldsets[tag]
714        if ft then
715            local a = pdfarray()
716            for name in gmatch(list,"[^, ]+") do
717                local f = field[name]
718                if f and f.pobj then
719                    a[#a+1] = pdfreference(f.pobj)
720                end
721            end
722            return a
723        end
724    end
725end
726
727function codeinjections.setfieldcalculationset(tag)
728    calculationset = tag
729end
730
731interfaces.implement {
732    name      = "setfieldcalculationset",
733    actions   = codeinjections.setfieldcalculationset,
734    arguments = "string",
735}
736
737local function predefinesymbols(specification)
738    local values = specification.values
739    if values then
740        local symbols = settings_to_array(values)
741        for i=1,#symbols do
742            local symbol = symbols[i]
743            local a, b = lpegmatch(splitter,symbol)
744            codeinjections.presetsymbol(a or symbol)
745        end
746    end
747end
748
749function codeinjections.getdefaultfieldvalue(name)
750    local f = fields[name]
751    if f then
752        local values  = f.values
753        local default = f.default
754        if not default or default == "" then
755            local symbols = settings_to_array(values)
756            local symbol = symbols[1]
757            if symbol then
758                local a, b = lpegmatch(splitter,symbol) -- splits at =>
759                default = a or symbol
760            end
761        end
762        return default
763    end
764end
765
766function codeinjections.definefield(specification)
767    local n = specification.name
768    local f = fields[n]
769    if not f then
770        local fieldtype = specification.type
771        if not fieldtype then
772            if trace_fields then
773                report_fields("invalid definition for %a, unknown type",n)
774            end
775        elseif fieldtype == "radio" then
776            local values = specification.values
777            if values and values ~= "" then
778                values = settings_to_array(values)
779                for v=1,#values do
780                    radios[values[v]] = { parent = n }
781                end
782                fields[n] = specification
783                if trace_fields then
784                    report_fields("defining %a as type %a",n,"radio")
785                end
786            elseif trace_fields then
787                report_fields("invalid definition of radio %a, missing values",n)
788            end
789        elseif fieldtype == "sub" then
790            -- not in main field list !
791            local radio = radios[n]
792            if radio then
793                -- merge specification
794                for key, value in next, specification do
795                    radio[key] = value
796                end
797                if trace_fields then
798                    local p = radios[n] and radios[n].parent
799                    report_fields("defining %a as type sub of radio %a",n,p)
800                end
801            elseif trace_fields then
802                report_fields("invalid definition of radio sub %a, no parent given",n)
803            end
804            predefinesymbols(specification)
805        elseif fieldtype == "text" or fieldtype == "line" then
806            fields[n] = specification
807            if trace_fields then
808                report_fields("defining %a as type %a",n,fieldtype)
809            end
810            if specification.values ~= "" and specification.default == "" then
811                specification.default, specification.values = specification.values, nil
812            end
813        else
814            fields[n] = specification
815            if trace_fields then
816                report_fields("defining %a as type %a",n,fieldtype)
817            end
818            predefinesymbols(specification)
819        end
820    elseif trace_fields then
821        report_fields("invalid definition for %a, already defined",n)
822    end
823end
824
825function codeinjections.clonefield(specification) -- obsolete
826    local p = specification.parent
827    local c = specification.children
828    local v = specification.alternative
829    if not p or not c then
830        if trace_fields then
831            report_fields("invalid clone, children %a, parent %a, alternative %a",c,p,v)
832        end
833        return
834    end
835    local x = fields[p] or radios[p]
836    if not x then
837        if trace_fields then
838            report_fields("invalid clone, unknown parent %a",p)
839        end
840        return
841    end
842    for n in gmatch(c,"[^, ]+") do
843        local f, r, c = fields[n], radios[n], clones[n]
844        if f or r or c then
845            if trace_fields then
846                report_fields("already cloned, child %a, parent %a, alternative %a",n,p,v)
847            end
848        else
849            if trace_fields then
850                report_fields("cloning, child %a, parent %a, alternative %a",n,p,v)
851            end
852            clones[n] = specification
853            predefinesymbols(specification)
854        end
855    end
856end
857
858function codeinjections.getfieldcategory(name)
859    local f = fields[name] or radios[name] or clones[name]
860    if f then
861        local g = f.category
862        if not g or g == "" then
863            local v, p, t = f.alternative, f.parent, f.type
864            if v == "clone" or v == "copy" then
865                f = fields[p] or radios[p]
866                g = f and f.category
867            elseif t == "sub" then
868                f = fields[p]
869                g = f and f.category
870            end
871        end
872        return g
873    end
874end
875
876--
877
878function codeinjections.validfieldcategory(name)
879    return fields[name] or radios[name] or clones[name]
880end
881
882function codeinjections.validfieldset(name)
883    return fieldsets[tag]
884end
885
886function codeinjections.validfield(name)
887    return fields[name]
888end
889
890--
891
892local alignments = {
893    flushleft  = 0, right  = 0,
894    center     = 1, middle = 1,
895    flushright = 2, left   = 2,
896}
897
898local function fieldalignment(specification)
899    return alignments[specification.align] or 0
900end
901
902local function enhance(specification,option)
903    local so = specification.option
904    if so and so ~= "" then
905        specification.option = so .. "," .. option
906    else
907        specification.option = option
908    end
909    return specification
910end
911
912-- finish (if we also collect parents we can inline the kids which is
913-- more efficient ... but hardly anyone used widgets so ...)
914
915local collected     = pdfarray()
916local forceencoding = false
917
918-- todo : check #opt
919
920local function finishfields()
921    local sometext = forceencoding
922    local somefont = next(usedfonts)
923    for name, field in sortedhash(fields) do
924        local kids = field.kids
925        if kids then
926            pdfflushobject(field.kidsnum,kids)
927        end
928        local opt = field.opt
929        if opt then
930            pdfflushobject(field.optnum,opt)
931        end
932        local type = field.type
933        if not sometext and (type == "text" or type == "line") then
934            sometext = true
935        end
936        if type == "signed" then
937            signature = true
938        end
939    end
940    for name, field in sortedhash(radios) do
941        local kids = field.kids
942        if kids then
943            pdfflushobject(field.kidsnum,kids)
944        end
945        local opt = field.opt
946        if opt then
947            pdfflushobject(field.optnum,opt)
948        end
949    end
950    if #collected > 0 then
951        local acroform = pdfdictionary {
952            Fields          = pdfreference(pdfflushobject(collected)),
953            CO              = fieldsetlist(calculationset),
954        }
955        if signature then
956            acroform.SigFlags = 3
957        elseif pdfmajorversion() == 1 then
958            acroform.NeedAppearances = true
959        else
960            -- depricated
961        end
962        if sometext or somefont then
963            checkpdfdocencoding()
964            if sometext then
965                usedfonts.tttf = fontnames.tt.tf
966                acroform.DA = "/tttf 12 Tf 0 g"
967            end
968            acroform.DR = pdfdictionary {
969                Font     = registerfonts(),
970                Encoding = pdfdocencodingcapsule,
971            }
972        end
973        -- maybe:
974     -- if sometext then
975     --     checkpdfdocencoding()
976     --     if sometext then
977     --         usedfonts.tttf = fontnames.tt.tf
978     --         acroform.DA = "/tttf 12 Tf 0 g"
979     --     end
980     --     acroform.DR = pdfdictionary {
981     --         Font     = registerfonts(),
982     --         Encoding = pdfdocencodingcapsule,
983     --     }
984     -- elseif somefont then
985     --     acroform.DR = pdfdictionary {
986     --         Font     = registerfonts(),
987     --     }
988     -- end
989        lpdf.addtocatalog("AcroForm",pdfreference(pdfflushobject(acroform)))
990    end
991end
992
993lpdf.registerdocumentfinalizer(finishfields,"form fields")
994
995local methods = { }
996
997function nodeinjections.typesetfield(name,specification)
998    local field = fields[name] or radios[name] or clones[name]
999    if not field then
1000        report_fields( "unknown child %a",name)
1001        -- unknown field
1002        return
1003    end
1004    local alternative, parent = field.alternative, field.parent
1005    if alternative == "copy" or alternative == "clone" then -- only in clones
1006        field = fields[parent] or radios[parent]
1007    end
1008    local method = methods[field.type]
1009    if method then
1010        return method(name,specification,alternative)
1011    else
1012        report_fields( "unknown method %a for child %a",field.type,name)
1013    end
1014end
1015
1016local function save_parent(field,specification,d)
1017    local kidsnum = pdfreserveobject()
1018    d.Kids        = pdfreference(kidsnum)
1019    field.kidsnum = kidsnum
1020    field.kids    = pdfarray()
1021--     if d.Opt then
1022--         local optnum = pdfreserveobject()
1023--         d.Opt        = pdfreference(optnum)
1024--         field.optnum = optnum
1025--         field.opt    = pdfarray()
1026--     end
1027    local pnum = pdfflushobject(d)
1028    field.pobj = pnum
1029    collected[#collected+1] = pdfreference(pnum)
1030end
1031
1032local function save_kid(field,specification,d,optname)
1033    local kn = pdfreserveobject()
1034    field.kids[#field.kids+1] = pdfreference(kn)
1035--     if optname then
1036--         local opt = field.opt
1037--         if opt then
1038--             opt[#opt+1] = optname
1039--         end
1040--     end
1041    local width  = specification.width  or 0
1042    local height = specification.height or 0
1043    local depth  = specification.depth  or 0
1044    local box = hpack_node(nodeinjections.annotation(width,height,depth,d(),kn))
1045    -- redundant
1046    box.width  = width
1047    box.height = height
1048    box.depth  = depth
1049    return box
1050end
1051
1052local function makelineparent(field,specification)
1053    local text   = pdfunicode(field.default)
1054    local length = tonumber(specification.length or 0) or 0
1055    local d      = pdfdictionary {
1056        Subtype  = pdf_widget,
1057        T        = pdfunicode(specification.title),
1058        F        = fieldplus(specification),
1059        Ff       = fieldflag(specification),
1060        OC       = fieldlayer(specification),
1061        DA       = fieldsurrounding(specification),
1062        AA       = fieldactions(specification),
1063        FT       = pdf_tx,
1064        Q        = fieldalignment(specification),
1065        MaxLen   = length == 0 and 1000 or length,
1066        DV       = text,
1067        V        = text,
1068    }
1069    save_parent(field,specification,d)
1070end
1071
1072local function makelinechild(name,specification)
1073    local field  = clones[name]
1074    local parent = nil
1075    if field then
1076        parent = fields[field.parent]
1077        if not parent.pobj then
1078            if trace_fields then
1079                report_fields("forcing parent text %a",parent.name)
1080            end
1081            makelineparent(parent,specification)
1082        end
1083    else
1084        parent = fields[name]
1085        field  = parent
1086        if not parent.pobj then
1087            if trace_fields then
1088                report_fields("using parent text %a",name)
1089            end
1090            makelineparent(parent,specification)
1091        end
1092    end
1093    if trace_fields then
1094        report_fields("using child text %a",name)
1095    end
1096    -- we could save a little by not setting some key/value when it's the
1097    -- same as parent but it would cost more memory to keep track of it
1098    local d = pdfdictionary {
1099        Subtype = pdf_widget,
1100        Parent  = pdfreference(parent.pobj),
1101        F       = fieldplus(specification),
1102        OC      = fieldlayer(specification),
1103        DA      = fieldsurrounding(specification),
1104        AA      = fieldactions(specification),
1105        MK      = fieldrendering(specification),
1106        Q       = fieldalignment(specification),
1107    }
1108    return save_kid(parent,specification,d)
1109end
1110
1111function methods.line(name,specification)
1112    return makelinechild(name,specification)
1113end
1114
1115function methods.text(name,specification)
1116    return makelinechild(name,enhance(specification,"MultiLine"))
1117end
1118
1119-- copy of line ... probably also needs a /Lock
1120
1121local function makesignatureparent(field,specification)
1122    local text   = pdfunicode(field.default)
1123    local length = tonumber(specification.length or 0) or 0
1124    local value  = lpdf.registersignature(field.values)
1125    local d      = pdfdictionary {
1126        Subtype  = pdf_widget,
1127        T        = pdfunicode(specification.title),
1128        F        = fieldplus(specification),
1129        Ff       = fieldflag(specification),
1130        OC       = fieldlayer(specification),
1131        DA       = fieldsurrounding(specification),
1132        AA       = fieldactions(specification),
1133        FT       = pdf_sig,
1134        Q        = fieldalignment(specification),
1135        MaxLen   = length == 0 and 1000 or length,
1136        DV       = not value and text or nil,
1137        V        = value or text,
1138    }
1139    save_parent(field,specification,d)
1140end
1141
1142local function makesignaturechild(name,specification)
1143    local field  = clones[name]
1144    local parent = nil
1145    if field then
1146        parent = fields[field.parent]
1147        if not parent.pobj then
1148            if trace_fields then
1149                report_fields("forcing parent signature %a",parent.name)
1150            end
1151            makesignatureparent(parent,specification)
1152        end
1153    else
1154        parent = fields[name]
1155        field  = parent
1156        if not parent.pobj then
1157            if trace_fields then
1158                report_fields("using parent text %a",name)
1159            end
1160            makesignatureparent(parent,specification)
1161        end
1162    end
1163    if trace_fields then
1164        report_fields("using child text %a",name)
1165    end
1166    -- we could save a little by not setting some key/value when it's the
1167    -- same as parent but it would cost more memory to keep track of it
1168    local d = pdfdictionary {
1169        Subtype = pdf_widget,
1170        Parent  = pdfreference(parent.pobj),
1171        F       = fieldplus(specification),
1172        OC      = fieldlayer(specification),
1173        DA      = fieldsurrounding(specification),
1174        AA      = fieldactions(specification),
1175        MK      = fieldrendering(specification),
1176        Q       = fieldalignment(specification),
1177-- AP = pdfdictionary {
1178--     N = pdfreference(lpdf.flushstreamobject("q 0 0 100 100 re f Q"))
1179-- }
1180    }
1181    return save_kid(parent,specification,d)
1182end
1183
1184function methods.signature(name,specification)
1185    return makesignaturechild(name,specification)
1186end
1187
1188--
1189
1190function methods.signed(name,specification)
1191    local field = fields[name]
1192    if field then
1193        specification.option = "hidden" -- for now
1194        local value = lpdf.registersignature(field.values)
1195        if not value or value == "" then
1196            value = "pkcs7"
1197        end
1198        local d = pdfdictionary {
1199            Subtype  = pdf_widget,
1200            T        = pdfunicode(specification.title),
1201            F        = fieldplus(specification),
1202            Ff       = fieldflag(specification),
1203            OC       = fieldlayer(specification),
1204         -- DA       = fieldsurrounding(specification),
1205         -- AA       = fieldactions(specification),
1206            FT       = pdf_sig,
1207         -- Q        = fieldalignment(specification),
1208            V        = lpdf.registersignature(value),
1209        }
1210        --
1211        local kn = pdfreserveobject()
1212        local width  = specification.width  or 0
1213        local height = specification.height or 0
1214        local depth  = specification.depth  or 0
1215        local box = hpack_node(nodeinjections.annotation(width,height,depth,d(),kn))
1216        -- redundant
1217        box.width  = width
1218        box.height = height
1219        box.depth  = depth
1220        collected[#collected+1] = pdfreference(kn)
1221        return box
1222    end
1223end
1224
1225--
1226
1227local function makechoiceparent(field,specification)
1228    local d = pdfdictionary {
1229        Subtype  = pdf_widget,
1230        T        = pdfunicode(specification.title),
1231        F        = fieldplus(specification),
1232        Ff       = fieldflag(specification),
1233        OC       = fieldlayer(specification),
1234        AA       = fieldactions(specification),
1235        FT       = pdf_ch,
1236        Opt      = fieldoptions(field), -- todo
1237    }
1238    save_parent(field,specification,d)
1239end
1240
1241local function makechoicechild(name,specification)
1242    local field  = clones[name]
1243    local parent = nil
1244    if field then
1245        parent = fields[field.parent]
1246        if not parent.pobj then
1247            if trace_fields then
1248                report_fields("forcing parent choice %a",parent.name)
1249            end
1250            makechoiceparent(parent,specification,extras)
1251        end
1252    else
1253        parent = fields[name]
1254        field  = parent
1255        if not parent.pobj then
1256            if trace_fields then
1257                report_fields("using parent choice %a",name)
1258            end
1259            makechoiceparent(parent,specification,extras)
1260        end
1261    end
1262    if trace_fields then
1263        report_fields("using child choice %a",name)
1264    end
1265    local d = pdfdictionary {
1266        Subtype = pdf_widget,
1267        Parent  = pdfreference(parent.pobj),
1268        F       = fieldplus(specification),
1269        OC      = fieldlayer(specification),
1270        AA      = fieldactions(specification),
1271    }
1272    return save_kid(parent,specification,d) -- do opt here
1273end
1274
1275function methods.choice(name,specification)
1276    return makechoicechild(name,specification)
1277end
1278
1279function methods.popup(name,specification)
1280    return makechoicechild(name,enhance(specification,"PopUp"))
1281end
1282
1283function methods.combo(name,specification)
1284    return makechoicechild(name,enhance(specification,"PopUp,Edit"))
1285end
1286
1287local function makecheckparent(field,specification)
1288    local default = fieldstates_precheck(field)
1289    local d = pdfdictionary {
1290        T  = pdfunicode(specification.title), -- todo: when tracing use a string
1291        F  = fieldplus(specification),
1292        Ff = fieldflag(specification),
1293        OC = fieldlayer(specification),
1294        AA = fieldactions(specification), -- can be shared
1295        FT = pdf_btn,
1296        V  = fielddefault(field,default),
1297    }
1298    save_parent(field,specification,d)
1299end
1300
1301local function makecheckchild(name,specification)
1302    local field  = clones[name]
1303    local parent = nil
1304    if field then
1305        parent = fields[field.parent]
1306        if not parent.pobj then
1307            if trace_fields then
1308                report_fields("forcing parent check %a",parent.name)
1309            end
1310            makecheckparent(parent,specification,extras)
1311        end
1312    else
1313        parent = fields[name]
1314        field  = parent
1315        if not parent.pobj then
1316            if trace_fields then
1317                report_fields("using parent check %a",name)
1318            end
1319            makecheckparent(parent,specification,extras)
1320        end
1321    end
1322    if trace_fields then
1323        report_fields("using child check %a",name)
1324    end
1325    local d = pdfdictionary {
1326        Subtype = pdf_widget,
1327        Parent  = pdfreference(parent.pobj),
1328        F       = fieldplus(specification),
1329        OC      = fieldlayer(specification),
1330        AA      = fieldactions(specification), -- can be shared
1331        H       = pdf_n,
1332    }
1333    local fontsymbol = specification.fontsymbol
1334    if fontsymbol and fontsymbol ~= "" then
1335        specification.fontsymbol = todingbat(fontsymbol)
1336        specification.fontstyle = "symbol"
1337        specification.fontalternative = "dingbats"
1338        d.DA = fieldsurrounding(specification)
1339        d.MK = fieldrendering(specification)
1340        return save_kid(parent,specification,d)
1341    else
1342        local appearance, default, value = fieldstates_check(field)
1343        d.AS = default
1344        d.AP = appearance
1345        return save_kid(parent,specification,d)
1346    end
1347end
1348
1349function methods.check(name,specification)
1350    return makecheckchild(name,specification)
1351end
1352
1353local function makepushparent(field,specification) -- check if we can share with the previous
1354    local d = pdfdictionary {
1355        Subtype = pdf_widget,
1356        T       = pdfunicode(specification.title),
1357        F       = fieldplus(specification),
1358        Ff      = fieldflag(specification),
1359        OC      = fieldlayer(specification),
1360        AA      = fieldactions(specification), -- can be shared
1361        FT      = pdf_btn,
1362        AP      = fieldappearances(field),
1363        H       = pdf_p,
1364    }
1365    save_parent(field,specification,d)
1366end
1367
1368local function makepushchild(name,specification)
1369    local field, parent = clones[name], nil
1370    if field then
1371        parent = fields[field.parent]
1372        if not parent.pobj then
1373            if trace_fields then
1374                report_fields("forcing parent push %a",parent.name)
1375            end
1376            makepushparent(parent,specification)
1377        end
1378    else
1379        parent = fields[name]
1380        field = parent
1381        if not parent.pobj then
1382            if trace_fields then
1383                report_fields("using parent push %a",name)
1384            end
1385            makepushparent(parent,specification)
1386        end
1387    end
1388    if trace_fields then
1389        report_fields("using child push %a",name)
1390    end
1391    local fontsymbol = specification.fontsymbol
1392    local d = pdfdictionary {
1393        Subtype = pdf_widget,
1394        Parent  = pdfreference(field.pobj),
1395        F       = fieldplus(specification),
1396        OC      = fieldlayer(specification),
1397        AA      = fieldactions(specification), -- can be shared
1398        H       = pdf_p,
1399    }
1400    if fontsymbol and fontsymbol ~= "" then
1401        specification.fontsymbol = todingbat(fontsymbol)
1402        specification.fontstyle = "symbol"
1403        specification.fontalternative = "dingbats"
1404        d.DA = fieldsurrounding(specification)
1405        d.MK = fieldrendering(specification)
1406    else
1407        d.AP = fieldappearances(field)
1408    end
1409    return save_kid(parent,specification,d)
1410end
1411
1412function methods.push(name,specification)
1413    return makepushchild(name,enhance(specification,"PushButton"))
1414end
1415
1416local function makeradioparent(field,specification)
1417    specification = enhance(specification,"Radio,RadiosInUnison,Print,NoToggleToOff")
1418    local d = pdfdictionary {
1419        T  = field.name,
1420        FT = pdf_btn,
1421     -- F  = fieldplus(specification),
1422        Ff = fieldflag(specification),
1423     -- H  = pdf_n,
1424        V  = fielddefault(field),
1425    }
1426    save_parent(field,specification,d)
1427end
1428
1429-- local function makeradiochild(name,specification)
1430--     local field  = clones[name]
1431--     local parent = nil
1432--     local pname  = nil
1433--     if field then
1434--         pname  = field.parent
1435--         field  = radios[pname]
1436--         parent = fields[pname]
1437--         if not parent.pobj then
1438--             if trace_fields then
1439--                 report_fields("forcing parent radio %a",parent.name)
1440--             end
1441--             makeradioparent(parent,parent)
1442--         end
1443--     else
1444--         field = radios[name]
1445--         if not field then
1446--             report_fields("there is some problem with field %a",name)
1447--             return nil
1448--         end
1449--         pname  = field.parent
1450--         parent = fields[pname]
1451--         if not parent.pobj then
1452--             if trace_fields then
1453--                 report_fields("using parent radio %a",name)
1454--             end
1455--             makeradioparent(parent,parent)
1456--         end
1457--     end
1458--     if trace_fields then
1459--         report_fields("using child radio %a with values %a and default %a",name,field.values,field.default)
1460--     end
1461--     local fontsymbol = specification.fontsymbol
1462--  -- fontsymbol = "circle"
1463--     local d = pdfdictionary {
1464--         Subtype = pdf_widget,
1465--         Parent  = pdfreference(parent.pobj),
1466--         F       = fieldplus(specification),
1467--         OC      = fieldlayer(specification),
1468--         AA      = fieldactions(specification),
1469--         H       = pdf_n,
1470-- -- H       = pdf_p,
1471-- -- P       = pdfpagereference(true),
1472--     }
1473--     if fontsymbol and fontsymbol ~= "" then
1474--         specification.fontsymbol = todingbat(fontsymbol)
1475--         specification.fontstyle = "symbol"
1476--         specification.fontalternative = "dingbats"
1477--         d.DA = fieldsurrounding(specification)
1478--         d.MK = fieldrendering(specification)
1479--         return save_kid(parent,specification,d) -- todo: what if no value
1480--     else
1481--         local appearance, default, value = fieldstates_radio(field,name,fields[pname])
1482--         d.AP = appearance
1483--         d.AS = default -- /Whatever
1484--         return save_kid(parent,specification,d,value)
1485--     end
1486-- end
1487
1488local function makeradiochild(name,specification)
1489    local field, parent = clones[name], nil
1490    if field then
1491        field  = radios[field.parent]
1492        parent = fields[field.parent]
1493        if not parent.pobj then
1494            if trace_fields then
1495                report_fields("forcing parent radio %a",parent.name)
1496            end
1497            makeradioparent(parent,parent)
1498        end
1499    else
1500        field = radios[name]
1501        if not field then
1502            report_fields("there is some problem with field %a",name)
1503            return nil
1504        end
1505        parent = fields[field.parent]
1506        if not parent.pobj then
1507            if trace_fields then
1508                report_fields("using parent radio %a",name)
1509            end
1510            makeradioparent(parent,parent)
1511        end
1512    end
1513    if trace_fields then
1514        report_fields("using child radio %a with values %a and default %a",name,field.values,field.default)
1515    end
1516    local fontsymbol = specification.fontsymbol
1517 -- fontsymbol = "circle"
1518    local d = pdfdictionary {
1519        Subtype = pdf_widget,
1520        Parent  = pdfreference(parent.pobj),
1521        F       = fieldplus(specification),
1522        OC      = fieldlayer(specification),
1523        AA      = fieldactions(specification),
1524        H       = pdf_n,
1525    }
1526    if fontsymbol and fontsymbol ~= "" then
1527        specification.fontsymbol = todingbat(fontsymbol)
1528        specification.fontstyle = "symbol"
1529        specification.fontalternative = "dingbats"
1530        d.DA = fieldsurrounding(specification)
1531        d.MK = fieldrendering(specification)
1532    end
1533    local appearance, default, value = fieldstates_radio(field,name,fields[field.parent])
1534    d.AP = appearance
1535    d.AS = default -- /Whatever
1536-- d.MK = pdfdictionary { BC = pdfarray {0}, BG = pdfarray { 1 } }
1537d.BS = pdfdictionary { S = pdfconstant("I"), W = 1 }
1538    return save_kid(parent,specification,d,value)
1539end
1540
1541function methods.sub(name,specification)
1542    return makeradiochild(name,enhance(specification,"Radio,RadiosInUnison"))
1543end
1544