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