lpdf-wid.lmt /size: 30 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-wid'] = {
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-- It's about time to give up on media in pdf and admit that pdf lost it to html.
10-- First we had movies and sound, quite easy to deal with, but obsolete now. Then we
11-- had renditions but they turned out to be unreliable from the start and look
12-- obsolete too or at least they are bound to the (obsolete) flash technology for
13-- rendering. They were already complex constructs. Now we have rich media which
14-- instead of providing a robust future proof framework for general media types
15-- again seems to depend on viewers built in (yes, also kind of obsolete) flash
16-- technology, and we cannot expect this non-open technology to show up in open
17-- browsers. So, in the end we can best just use links to external resources to be
18-- future proof. Just look at the viewer preferences pane to see how fragile support
19-- is. Interestingly u3d support is kind of built in, while e.g. mp4 support relies
20-- on wrapping in swf. We used to stay ahead of the pack with support of the fancy
21-- pdf features but it backfires and is not worth the trouble. And yes, for control
22-- (even simple like starting and stopping videos) one has to revert to JavaScript,
23-- the other fragile bit. And, now that adobe quits flash in 2020 we're without any
24-- video anyway. Also, it won't play on all platforms and devices so let's wait for
25-- html5 media in pdf then.
26--
27-- See mail by Michal Vlasák to the mailing list that discusses current support in
28-- viewers and also mentions (and submitted) a few fixes wrt embedding media. The
29-- old sound and movie features are gone.
30
31local tonumber, next = tonumber, next
32local gmatch, gsub, find, lower = string.gmatch, string.gsub, string.find, string.lower
33local filenameonly, basefilename, filesuffix, addfilesuffix = file.nameonly, file.basename, file.suffix, file.addsuffix
34local isfile, modificationtime = lfs.isfile, lfs.modification
35local stripstring = string.strip
36local settings_to_array = utilities.parsers.settings_to_array
37local settings_to_hash = utilities.parsers.settings_to_hash
38local sortedhash, sortedkeys = table.sortedhash, table.sortedkeys
39
40local report_media             = logs.reporter("backend","media")
41local report_attachment        = logs.reporter("backend","attachment")
42
43local context                  = context
44
45local texgetcount              = tex.getcount
46local getmacro                 = tokens.getters.macro
47
48local hpacknode                = nodes.hpack
49
50local pdfbackend               = backends.registered.pdf
51local nodeinjections           = pdfbackend.nodeinjections
52local codeinjections           = pdfbackend.codeinjections
53local registrations            = pdfbackend.registrations
54
55local executers                = structures.references.executers
56local variables                = interfaces.variables
57
58local v_hidden                 <const> = variables.hidden
59local v_auto                   <const> = variables.auto
60local v_embed                  <const> = variables.embed
61local v_max                    <const> = variables.max
62local v_yes                    <const> = variables.yes
63local v_no                     <const> = variables.no
64local v_compress               <const> = variables.compress
65local v_list                   <const> = variables.list
66local v_title                  <const> = variables.title
67
68local lpdf                     = lpdf
69
70local pdfconstant              = lpdf.constant
71local pdfnull                  = lpdf.null
72local pdfdictionary            = lpdf.dictionary
73local pdfarray                 = lpdf.array
74local pdfreference             = lpdf.reference
75local pdfunicode               = lpdf.unicode
76local pdfstring                = lpdf.string
77local pdfboolean               = lpdf.boolean
78local pdfaction                = lpdf.action
79local pdfborder                = lpdf.border
80
81local pdftransparencyvalue     = lpdf.transparencyvalue
82local pdfcolorvalues           = lpdf.colorvalues
83
84local pdfflushobject           = lpdf.flushobject
85local pdfflushstreamobject     = lpdf.flushstreamobject
86local pdfflushstreamfileobject = lpdf.flushstreamfileobject
87local pdfreserveobject         = lpdf.reserveobject
88local pdfpagereference         = lpdf.pagereference
89local pdfshareobjectreference  = lpdf.shareobjectreference
90
91-- symbols
92
93local presets = { } -- xforms
94
95local function registersymbol(name,n)
96    presets[name] = pdfreference(n)
97end
98
99local function registeredsymbol(name)
100    return presets[name]
101end
102
103local function presetsymbol(symbol)
104    if not presets[symbol] then
105        context.predefinesymbol { symbol }
106    end
107end
108
109local function presetsymbollist(list)
110    if list then
111        for symbol in gmatch(list,"[^, ]+") do
112            presetsymbol(symbol)
113        end
114    end
115end
116
117codeinjections.registersymbol   = registersymbol
118codeinjections.registeredsymbol = registeredsymbol
119codeinjections.presetsymbol     = presetsymbol
120codeinjections.presetsymbollist = presetsymbollist
121
122-- comments
123
124-- local symbols = {
125--     Addition     = pdfconstant("NewParagraph"),
126--     Attachment   = pdfconstant("Attachment"),
127--     Balloon      = pdfconstant("Comment"),
128--     Check        = pdfconstant("Check Mark"),
129--     CheckMark    = pdfconstant("Check Mark"),
130--     Circle       = pdfconstant("Circle"),
131--     Cross        = pdfconstant("Cross"),
132--     CrossHairs   = pdfconstant("Cross Hairs"),
133--     Graph        = pdfconstant("Graph"),
134--     InsertText   = pdfconstant("Insert Text"),
135--     New          = pdfconstant("Insert"),
136--     Paperclip    = pdfconstant("Paperclip"),
137--     RightArrow   = pdfconstant("Right Arrow"),
138--     RightPointer = pdfconstant("Right Pointer"),
139--     Star         = pdfconstant("Star"),
140--     Tag          = pdfconstant("Tag"),
141--     Text         = pdfconstant("Note"),
142--     TextNote     = pdfconstant("Text Note"),
143--     UpArrow      = pdfconstant("Up Arrow"),
144--     UpLeftArrow  = pdfconstant("Up-Left Arrow"),
145-- }
146
147local attachment_symbols = {
148    Graph     = pdfconstant("Graph"),
149    Paperclip = pdfconstant("Paperclip"),
150    Pushpin   = pdfconstant("PushPin"),
151}
152
153attachment_symbols.PushPin = attachment_symbols.Pushpin
154attachment_symbols.Default = attachment_symbols.Pushpin
155
156function lpdf.attachmentsymbols()
157    return sortedkeys(comment_symbols)
158end
159
160local comment_symbols = {
161    Comment      = pdfconstant("Comment"),
162    Help         = pdfconstant("Help"),
163    Insert       = pdfconstant("Insert"),
164    Key          = pdfconstant("Key"),
165    Newparagraph = pdfconstant("NewParagraph"),
166    Note         = pdfconstant("Note"),
167    Paragraph    = pdfconstant("Paragraph"),
168}
169
170comment_symbols.NewParagraph = Newparagraph
171comment_symbols.Default      = Note
172
173function lpdf.commentsymbols()
174    return sortedkeys(comment_symbols)
175end
176
177local function analyzesymbol(symbol,collection)
178    if not symbol or symbol == "" then
179        return collection and collection.Default, nil
180    elseif collection and collection[symbol] then
181        return collection[symbol], nil
182    else
183        local setn, setr, setd
184        local set = settings_to_array(symbol)
185        if #set == 1 then
186            setn, setr, setd = set[1], set[1], set[1]
187        elseif #set == 2 then
188            setn, setr, setd = set[1], set[1], set[2]
189        else
190            setn, setr, setd = set[1], set[2], set[3]
191        end
192        local appearance = pdfdictionary {
193            N = setn and registeredsymbol(setn),
194            R = setr and registeredsymbol(setr),
195            D = setd and registeredsymbol(setd),
196        }
197        local appearanceref = pdfshareobjectreference(appearance)
198        return nil, appearanceref
199    end
200end
201
202local function analyzenormalsymbol(symbol)
203    local appearance = pdfdictionary {
204        N = registeredsymbol(symbol),
205    }
206    local appearanceref = pdfshareobjectreference(appearance)
207    return appearanceref
208end
209
210codeinjections.analyzesymbol       = analyzesymbol
211codeinjections.analyzenormalsymbol = analyzenormalsymbol
212
213local function analyzelayer(layer)
214    -- todo:  (specification.layer ~= "" and pdfreference(specification.layer)) or nil, -- todo: ref to layer
215end
216
217local function analyzecolor(colorvalue,colormodel)
218    local cvalue = colorvalue and tonumber(colorvalue)
219    local cmodel = colormodel and tonumber(colormodel) or 3
220    return cvalue and pdfarray { pdfcolorvalues(cmodel,cvalue) } or nil
221end
222
223local function analyzetransparency(transparencyvalue)
224    local tvalue = transparencyvalue and tonumber(transparencyvalue)
225    return tvalue and pdftransparencyvalue(tvalue) or nil
226end
227
228-- Attachments
229
230local nofattachments    = 0
231local attachments       = { }
232local filestreams       = { }
233local referenced        = { }
234local ignorereferenced  = true -- fuzzy pdf spec .. twice in attachment list, can become an option
235local tobesavedobjrefs  = utilities.storage.allocate()
236local collectedobjrefs  = utilities.storage.allocate()
237local permitted         = true
238local enabled           = true
239
240function codeinjections.setattachmentsupport(option)
241    if option == false then
242        permitted = false
243        enabled   = false
244    end
245end
246
247local fileobjreferences = {
248    collected = collectedobjrefs,
249    tobesaved = tobesavedobjrefs,
250}
251
252job.fileobjreferences = fileobjreferences
253
254local function initializer()
255    collectedobjrefs = job.fileobjreferences.collected or { }
256    tobesavedobjrefs = job.fileobjreferences.tobesaved or { }
257end
258
259job.register('job.fileobjreferences.collected', tobesavedobjrefs, initializer)
260
261local function flushembeddedfiles()
262    if enabled and next(filestreams) then
263        local e = pdfarray()
264        local f = pdfarray()
265        for tag, reference in sortedhash(filestreams) do
266            if not reference then
267                report_attachment("unreferenced file, tag %a",tag)
268            elseif referenced[tag] == "hidden" or referenced[tag] == "forced" then
269                e[#e+1] = pdfstring(tag)
270                e[#e+1] = reference -- already a reference
271                f[#f+1] = reference -- collect all file description references
272            else
273                -- messy spec ... when annot not in named else twice in menu list acrobat
274                f[#f+1] = reference
275            end
276        end
277        if #e > 0 then
278            lpdf.addtonames("EmbeddedFiles",pdfreference(pdfflushobject(pdfdictionary{ Names = e })))
279        end
280        if #f > 0 then -- PDF/A-2|3: all associated files must have a relationship to the PDF document (global or part)
281            lpdf.addtocatalog("AF", pdfreference(pdfflushobject(f))) -- global (Catalog)
282        end
283    end
284end
285
286lpdf.registerdocumentfinalizer(flushembeddedfiles,"embeddedfiles")
287
288-- relation: Supplement, Data, Source, Alternative, Data
289
290
291local hashed = sha2.HEX256
292local shared = table.setmetatableindex("table")
293
294function codeinjections.embedfile(specification)
295    if enabled or specification.force then
296        local data      = specification.data
297        local filename  = specification.file
298        local name      = specification.name or ""
299        local title     = specification.title or ""
300        local hash      = specification.hash or filename
301        local keepdir   = specification.keepdir -- can change
302        local usedname  = specification.usedname
303        local filetype  = specification.filetype
304        local compress  = specification.compress
305        local index     = specification.index
306        local mimetype  = specification.mimetype or specification.mime
307        local relation  = specification.relation or "Unspecified"
308        if filename == "" then
309            filename = nil
310        end
311        if compress == nil then
312            compress = true
313        end
314        if data then
315            local r = filestreams[hash]
316            if r == false then
317                return nil
318            elseif r then
319                return r
320            elseif not filename then
321                filename = specification.tag
322                if not filename or filename == "" then
323                    filename = specification.registered
324                end
325                if not filename or filename == "" then
326                    filename = hash
327                end
328            end
329        else
330            if not filename then
331                return nil
332            end
333            local r = filestreams[hash]
334            if r == false then
335                return nil
336            elseif r then
337                return r
338            else
339                local foundname = resolvers.findbinfile(filename) or ""
340                if foundname == "" or not isfile(foundname) then
341                    filestreams[filename] = false
342                    return nil
343                else
344                    specification.foundname = foundname
345                end
346            end
347        end
348        -- needs to be cleaned up:
349        usedname = usedname ~= "" and usedname or filename or name
350        local basename  = keepdir == true and usedname or basefilename(usedname)
351        local basename  = gsub(basename,"%./","")
352        local savename  = name ~= "" and name or basename
353        local foundname = specification.foundname or filename
354        if not filetype or filetype == "" then
355            filetype = name and (filename and filesuffix(filename)) or "txt"
356        end
357        savename = addfilesuffix(savename,filetype) -- type is mandate for proper working in viewer
358
359-- local share = data and specification.share and shared[mimetype or ""] -- shared data with different possible filenames
360-- local shash = nil
361local fdict = nil
362-- print(data)
363-- if share then
364--     shash = hashed(data)
365--     fdict = share[shash]
366-- end
367-- if fdict then
368--     -- reuse f
369-- else
370
371        local a = pdfdictionary {
372            Type    = pdfconstant("EmbeddedFile"),
373            Subtype = mimetype and mimetype ~= "" and pdfconstant(mimetype) or nil,
374        }
375        if data then
376            fdict = pdfflushstreamobject(data,a)
377            specification.data = true -- signal that still data but already flushed
378        else
379            local attributes   = lfs.attributes(foundname)
380            local modification = modificationtime(foundname)
381            a.Params = {
382                Size    = attributes.size,
383                ModDate = lpdf.pdftimestamp(modification),
384            }
385            fdict = pdfflushstreamfileobject(foundname,a,compress)
386        end
387
388        fdict = pdfdictionary { F = pdfreference(fdict) }
389--         if shash then
390--             share[shash] = fdict
391--         end
392-- end
393        if title == "" and lpdf.majorversion() > 1 then
394            title = basefilename(savename)
395        end
396        local d = pdfdictionary {
397            Type           = pdfconstant("Filespec"),
398            F              = pdfstring(savename),
399         -- UF             = pdfstring(savename),
400            UF             = pdfunicode(savename),
401         -- EF             = pdfdictionary { F = pdfreference(f) },
402            EF             = fdict,
403            Desc           = title ~= "" and pdfunicode(title) or nil,
404            AFRelationship = pdfconstant(relation),
405            LMTX_Index     = index or nil,
406        }
407        local r = pdfreference(pdfflushobject(d))
408        filestreams[hash] = r
409        if specification.forcereference == true then
410            referenced[hash] = "forced"
411        end
412        return r
413    end
414end
415
416function nodeinjections.attachfile(specification)
417    if enabled then
418        local registered = specification.registered or "<unset>"
419        local data = specification.data
420        local hash
421        local filename
422        if data then
423            hash = md5.HEX(data)
424        else
425            filename = specification.file
426            if not filename or filename == "" then
427                report_attachment("no file specified, using registered %a instead",registered)
428                filename = registered
429                specification.file = registered
430            end
431            local foundname = resolvers.findbinfile(filename) or ""
432            if foundname == "" or not isfile(foundname) then
433                report_attachment("invalid filename %a, ignoring registered %a",filename,registered)
434                return nil
435            else
436                specification.foundname = foundname
437            end
438            hash = filename
439        end
440        specification.hash = hash
441        nofattachments = nofattachments + 1
442        local registered = specification.registered or ""
443        local title      = specification.title      or ""
444        local subtitle   = specification.subtitle   or ""
445        local author     = specification.author     or ""
446        local onlyname   = filename and filenameonly(filename) or ""
447        if registered == "" then
448            registered = filename
449        end
450        if author == "" and title ~= "" then
451            author = title
452            title  = onlyname or ""
453        end
454        if author == "" then
455            author = onlyname or "<unknown>"
456        end
457        if title == "" then
458            title = registered
459        end
460        if title == "" and filename then
461            title = onlyname
462        end
463        local aref = attachments[registered]
464        if not aref then
465            aref = codeinjections.embedfile(specification)
466            attachments[registered] = aref
467        end
468        local reference = specification.reference
469        if reference and aref then
470            tobesavedobjrefs[reference] = aref[1]
471        end
472        if not aref then
473            report_attachment("skipping attachment, registered %a",registered)
474            -- already reported
475        elseif specification.method == v_hidden then
476            referenced[hash] = "hidden"
477        else
478            referenced[hash] = "annotation"
479            local name, appearance = analyzesymbol(specification.symbol,attachment_symbols)
480            local flags = specification.flags or 0 -- to keep it expandable
481            local d = pdfdictionary {
482                Subtype  = pdfconstant("FileAttachment"),
483                FS       = aref,
484                Contents = pdfunicode(title),
485                Name     = name,
486                NM       = pdfstring("attachment:"..nofattachments),
487                T        = author ~= "" and pdfunicode(author) or nil,
488                Subj     = subtitle ~= "" and pdfunicode(subtitle) or nil,
489                C        = analyzecolor(specification.colorvalue,specification.colormodel),
490                CA       = analyzetransparency(specification.transparencyvalue),
491                AP       = appearance,
492                OC       = analyzelayer(specification.layer),
493             -- F        = pdfnull(), -- another rediculous need to satisfy validation
494                F        = (flags | 4) & (1023-1-2-32-256), -- set 3, clear 1,2,6,9; PDF 32000-1, p385
495            }
496            local width  = specification.width  or 0
497            local height = specification.height or 0
498            local depth  = specification.depth  or 0
499            local box    = hpacknode(nodeinjections.annotation(width,height,depth,d()))
500            box.width    = width
501            box.height   = height
502            box.depth    = depth
503            return box
504        end
505    end
506end
507
508function codeinjections.attachmentid(filename) -- not used in context
509    return filestreams[filename]
510end
511
512-- Comments
513
514local nofcomments      = 0
515local usepopupcomments = false
516
517local defaultattributes = {
518    ["xmlns"]           = "http://www.w3.org/1999/xhtml",
519    ["xmlns:xfa"]       = "http://www.xfa.org/schema/xfa-data/1.0/",
520    ["xfa:contentType"] = "text/html",
521    ["xfa:APIVersion"]  = "Acrobat:8.0.0",
522    ["xfa:spec"]        = "2.4",
523}
524
525local function checkcontent(text,option)
526    if option and option.xml then
527        local root = xml.convert(text)
528        if root and not root.er then
529            xml.checkbom(root)
530            local body = xml.first(root,"/body")
531            if body then
532                local at = body.at
533                for k, v in next, defaultattributes do
534                    if not at[k] then
535                        at[k] = v
536                    end
537                end
538             -- local content = xml.textonly(root)
539                local richcontent = xml.tostring(root)
540                return nil, pdfunicode(richcontent)
541            end
542        end
543    end
544    return pdfunicode(text)
545end
546
547function nodeinjections.comment(specification) -- brrr: seems to be done twice
548    nofcomments = nofcomments + 1
549    local text = specification.data or ""
550    if specification.space ~= v_yes then
551        text = stripstring(text)
552        text = gsub(text,"[\n\r] *","\n")
553    end
554    text = gsub(text,"\r","\n")
555    local name, appearance = analyzesymbol(specification.symbol,comment_symbols)
556    local tag      = specification.tag      or "" -- this is somewhat messy as recent
557    local title    = specification.title    or "" -- versions of acrobat see the title
558    local subtitle = specification.subtitle or "" -- as author
559    local author   = specification.author   or ""
560    local option   = settings_to_hash(specification.option or "")
561    if author ~= "" then
562        if subtitle == "" then
563            subtitle = title
564        elseif title ~= "" then
565            subtitle = subtitle .. ", " .. title
566        end
567        title = author
568    end
569    if title == "" then
570        title = tag
571    end
572    local content, richcontent = checkcontent(text,option)
573    local d = pdfdictionary {
574        Subtype   = pdfconstant("Text"),
575        Open      = option[v_max] and pdfboolean(true) or nil,
576        Contents  = content,
577        RC        = richcontent,
578        T         = title ~= "" and pdfunicode(title) or nil,
579        Subj      = subtitle ~= "" and pdfunicode(subtitle) or nil,
580        C         = analyzecolor(specification.colorvalue,specification.colormodel),
581        CA        = analyzetransparency(specification.transparencyvalue),
582        OC        = analyzelayer(specification.layer),
583        Name      = name,
584        NM        = pdfstring("comment:"..nofcomments),
585        AP        = appearance,
586    }
587    local width  = specification.width  or 0
588    local height = specification.height or 0
589    local depth  = specification.depth  or 0
590    local box
591    if usepopupcomments then
592        -- rather useless as we can hide/vide
593        local nd = pdfreserveobject()
594        local nc = pdfreserveobject()
595        local c = pdfdictionary {
596            Subtype = pdfconstant("Popup"),
597            Parent  = pdfreference(nd),
598        }
599        d.Popup = pdfreference(nc)
600        box = hpacknode(
601            nodeinjections.annotation(0,0,0,d(),nd),
602            nodeinjections.annotation(width,height,depth,c(),nc)
603        )
604    else
605        box = hpacknode(nodeinjections.annotation(width,height,depth,d()))
606    end
607    box.width  = width  -- redundant
608    box.height = height -- redundant
609    box.depth  = depth  -- redundant
610    return box
611end
612
613-- rendering stuff
614--
615-- object_1  -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >>
616-- object_2  -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >>
617-- rendering -> <</Type /Rendition /S /MS [objref_1 objref_2]>>
618--
619-- we only work foreward here (currently)
620-- annotation is to be packed at the tex end
621
622-- aiff audio/aiff
623-- au   audio/basic
624-- avi  video/avi
625-- mid  audio/midi
626-- mov  video/quicktime
627-- mp3  audio/x-mp3 (mpeg)
628-- mp4  audio/mp4
629-- mp4  video/mp4
630-- mpeg video/mpeg
631-- smil application/smil
632-- swf  application/x-shockwave-flash
633
634-- P  media play parameters (evt /BE for controls etc
635-- A  boolean (audio)
636-- C  boolean (captions)
637-- O  boolean (overdubs)
638-- S  boolean (subtitles)
639-- PL pdfconstant("ADBE_MCI"),
640
641-- F        = flags,
642-- T        = title,
643-- Contents = rubish,
644-- AP       = irrelevant,
645
646-- sound is different, no window (or zero) so we need to collect them and
647-- force them if not set
648
649local ms, mu, mf = { }, { }, { }
650
651local function delayed(label)
652    local reserved = mu[label]
653    if not reserved then
654        reserved = pdfreserveobject()
655        mu[label] = reserved
656    end
657    return pdfreference(reserved)
658end
659
660local function checkedreference(ref)
661    local set, bug = structures.references.identify("",ref)
662    if not bug and #set > 0 then
663        return pdfaction(set)
664    end
665end
666
667local function insertrenderingwindow(specification)
668    local actions   = nil
669    local label     = specification.label
670    local openpage  = specification.openpage
671    local closepage = specification.closepage
672    local option    = settings_to_hash(specification.option)
673    if option[v_auto] then
674        if not openpage or openpage == "" then
675            openpage = variables.StartCurrentRendering
676        end
677        if not closepage or closepage == "" then
678            closepage = variables.StopCurrentRendering
679        end
680    end
681    openpage  = checkedreference(openpage)
682    closepage = checkedreference(closepage)
683    if openpage or closepage then
684        actions = pdfdictionary {
685            PO = openpage,
686            PC = closepage,
687        }
688    end
689    local page = tonumber(specification.page) or texgetcount("realpageno") -- todo
690    local r = mu[label] or pdfreserveobject() -- reserve, child refers to parent
691    local a = pdfdictionary {
692        S  = pdfconstant("Rendition"),
693        R  = mf[label],
694        OP = 0,
695        AN = pdfreference(r),
696    }
697    local bs, bc = pdfborder()
698    local d = pdfdictionary {
699        Subtype = pdfconstant("Screen"),
700        P       = pdfreference(pdfpagereference(page)),
701        A       = a, -- needed in order to make the annotation clickable (i.e. don't bark)
702        T       = pdfunicode(label), -- for JS
703        Border  = bs,
704        C       = bc,
705        AA      = actions,
706    }
707    local width = specification.width or 0
708    local height = specification.height or 0
709    context(nodeinjections.annotation(width,height,0,d(),r)) -- save ref
710    return pdfreference(r)
711end
712
713-- some dictionaries can have a MH (must honor) or BE (best effort) capsule
714
715local function insertrendering(specification)
716    local label  = specification.label
717    local option = settings_to_hash(specification.option)
718    if not mf[label] then
719        local filename = specification.filename
720        if filename and filename ~= "" then
721            local isurl    = find(filename,"://",1,true)
722            local mimetype = specification.mimetype or specification.mime
723         -- local start = pdfdictionary {
724         --     Type = pdfconstant("MediaOffset"),
725         --     S = pdfconstant("T"), -- time
726         --     T = pdfdictionary { -- time
727         --         Type = pdfconstant("Timespan"),
728         --         S    = pdfconstant("S"),
729         --         V    = 3, -- time in seconds
730         --     },
731         -- }
732         -- local start = pdfdictionary {
733         --     Type = pdfconstant("MediaOffset"),
734         --     S = pdfconstant("F"), -- frame
735         --     F = 100 -- framenumber
736         -- }
737         -- local start = pdfdictionary {
738         --     Type = pdfconstant("MediaOffset"),
739         --     S = pdfconstant("M"), -- mark
740         --     M = "somemark",
741         -- }
742         -- local parameters = pdfdictionary {
743         --     BE = pdfdictionary {
744         --          B = start,
745         --     }
746         -- }
747            local parameters = pdfdictionary {
748                Type = pdfconstant("MediaPermissions"),
749                TF   = pdfstring("TEMPALWAYS"), -- TEMPNEVER TEMPEXTRACT TEMPACCESS TEMPALWAYS / needed for acrobat/wmp
750            }
751            local descriptor = pdfdictionary {
752                Type = pdfconstant("Filespec"),
753                F    = filename,
754            }
755            if isurl then
756                descriptor.FS = pdfconstant("URL")
757                descriptor = pdfreference(pdfflushobject(descriptor))
758            elseif option[v_embed] then
759                descriptor = codeinjections.embedfile {
760                    file           = filename,
761                    mimetype       = mimetype, -- yes or no
762                    title          = option[v_title],
763                    compress       = option[v_compress] or false,
764                    forcereference = option[v_list] ~= v_no,
765                }
766            end
767            local clip = pdfdictionary {
768                Type = pdfconstant("MediaClip"),
769                S    = pdfconstant("MCD"),
770                N    = label,
771                CT   = mimetype,
772                Alt  = pdfarray { "", "file not found" }, -- language id + message
773                D    = descriptor,
774                P    = pdfreference(pdfflushobject(parameters)),
775            }
776            local rendition = pdfdictionary {
777                Type = pdfconstant("Rendition"),
778                S    = pdfconstant("MR"),
779                N    = pdfunicode(label),
780                C    = pdfreference(pdfflushobject(clip)),
781            }
782            mf[label] = pdfreference(pdfflushobject(rendition))
783        end
784    end
785end
786
787function codeinjections.processrendering(label)
788    local specification = interactions.renderings.rendering(label)
789    if specification then
790        insertrendering(specification)
791    else
792        -- error
793    end
794end
795
796-- needed mapping for access from JS
797
798local function flushrenderings()
799    if next(mf) then
800        local r = pdfarray()
801        for label, reference in sortedhash(mf) do
802            r[#r+1] = pdfunicode(label)
803            r[#r+1] = reference -- already a reference
804        end
805        lpdf.addtonames("Renditions",pdfreference(pdfflushobject(pdfdictionary{ Names = r })))
806    end
807end
808
809lpdf.registerdocumentfinalizer(flushrenderings,"renderings")
810
811function codeinjections.insertrenderingwindow(specification)
812    local label = specification.label
813    codeinjections.processrendering(label)
814    ms[label] = insertrenderingwindow(specification)
815end
816
817local function set(operation,label)
818    if not label or label == "" then
819        -- This one is local to the window so unlikely to be set.
820        label = getmacro("currentrendering")
821    end
822    if label and label ~= "" then
823        codeinjections.processrendering(label)
824        return pdfdictionary {
825            S  = pdfconstant("Rendition"),
826            OP = operation,
827            R  = mf[label],
828            AN = ms[label] or delayed(label),
829        }
830    end
831end
832
833function executers.startrendering (label) return set(0,label) end
834function executers.stoprendering  (label) return set(1,label) end
835function executers.pauserendering (label) return set(2,label) end
836function executers.resumerendering(label) return set(3,label) end
837