lpdf-wid.lmt /size: 29 Kb    last modification: 2021-10-28 13:51
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                 = variables.hidden
59local v_auto                   = variables.auto
60local v_embed                  = variables.embed
61local v_max                    = variables.max
62local v_yes                    = variables.yes
63local v_no                     = variables.no
64local v_compress               = variables.compress
65local v_list                   = variables.list
66local v_title                  = 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
288function codeinjections.embedfile(specification)
289    if enabled then
290        local data      = specification.data
291        local filename  = specification.file
292        local name      = specification.name or ""
293        local title     = specification.title or ""
294        local hash      = specification.hash or filename
295        local keepdir   = specification.keepdir -- can change
296        local usedname  = specification.usedname
297        local filetype  = specification.filetype
298        local compress  = specification.compress
299        local mimetype  = specification.mimetype or specification.mime
300        if filename == "" then
301            filename = nil
302        end
303        if compress == nil then
304            compress = true
305        end
306        if data then
307            local r = filestreams[hash]
308            if r == false then
309                return nil
310            elseif r then
311                return r
312            elseif not filename then
313                filename = specification.tag
314                if not filename or filename == "" then
315                    filename = specification.registered
316                end
317                if not filename or filename == "" then
318                    filename = hash
319                end
320            end
321        else
322            if not filename then
323                return nil
324            end
325            local r = filestreams[hash]
326            if r == false then
327                return nil
328            elseif r then
329                return r
330            else
331                local foundname = resolvers.findbinfile(filename) or ""
332                if foundname == "" or not isfile(foundname) then
333                    filestreams[filename] = false
334                    return nil
335                else
336                    specification.foundname = foundname
337                end
338            end
339        end
340        -- needs to be cleaned up:
341        usedname = usedname ~= "" and usedname or filename or name
342        local basename  = keepdir == true and usedname or basefilename(usedname)
343        local basename  = gsub(basename,"%./","")
344        local savename  = name ~= "" and name or basename
345        local foundname = specification.foundname or filename
346        if not filetype or filetype == "" then
347            filetype = name and (filename and filesuffix(filename)) or "txt"
348        end
349        savename = addfilesuffix(savename,filetype) -- type is mandate for proper working in viewer
350        local a = pdfdictionary {
351            Type    = pdfconstant("EmbeddedFile"),
352            Subtype = mimetype and mimetype ~= "" and pdfconstant(mimetype) or nil,
353        }
354        local f
355        if data then
356            f = pdfflushstreamobject(data,a)
357            specification.data = true -- signal that still data but already flushed
358        else
359            local attributes   = lfs.attributes(foundname)
360            local modification = modificationtime(foundname)
361            a.Params = {
362                Size    = attributes.size,
363                ModDate = lpdf.pdftimestamp(modification),
364            }
365            f = pdfflushstreamfileobject(foundname,a,compress)
366        end
367        local d = pdfdictionary {
368            Type           = pdfconstant("Filespec"),
369            F              = pdfstring(savename),
370         -- UF             = pdfstring(savename),
371            UF             = pdfunicode(savename),
372            EF             = pdfdictionary { F = pdfreference(f) },
373            Desc           = title ~= "" and pdfunicode(title) or nil,
374            AFRelationship = pdfconstant("Unspecified"), -- Supplement, Data, Source, Alternative, Data
375        }
376        local r = pdfreference(pdfflushobject(d))
377        filestreams[hash] = r
378        if specification.forcereference == true then
379            referenced[hash] = "forced"
380        end
381        return r
382    end
383end
384
385function nodeinjections.attachfile(specification)
386    if enabled then
387        local registered = specification.registered or "<unset>"
388        local data = specification.data
389        local hash
390        local filename
391        if data then
392            hash = md5.HEX(data)
393        else
394            filename = specification.file
395            if not filename or filename == "" then
396                report_attachment("no file specified, using registered %a instead",registered)
397                filename = registered
398                specification.file = registered
399            end
400            local foundname = resolvers.findbinfile(filename) or ""
401            if foundname == "" or not isfile(foundname) then
402                report_attachment("invalid filename %a, ignoring registered %a",filename,registered)
403                return nil
404            else
405                specification.foundname = foundname
406            end
407            hash = filename
408        end
409        specification.hash = hash
410        nofattachments = nofattachments + 1
411        local registered = specification.registered or ""
412        local title      = specification.title      or ""
413        local subtitle   = specification.subtitle   or ""
414        local author     = specification.author     or ""
415        local onlyname   = filename and filenameonly(filename) or ""
416        if registered == "" then
417            registered = filename
418        end
419        if author == "" and title ~= "" then
420            author = title
421            title  = onlyname or ""
422        end
423        if author == "" then
424            author = onlyname or "<unknown>"
425        end
426        if title == "" then
427            title = registered
428        end
429        if title == "" and filename then
430            title = onlyname
431        end
432        local aref = attachments[registered]
433        if not aref then
434            aref = codeinjections.embedfile(specification)
435            attachments[registered] = aref
436        end
437        local reference = specification.reference
438        if reference and aref then
439            tobesavedobjrefs[reference] = aref[1]
440        end
441        if not aref then
442            report_attachment("skipping attachment, registered %a",registered)
443            -- already reported
444        elseif specification.method == v_hidden then
445            referenced[hash] = "hidden"
446        else
447            referenced[hash] = "annotation"
448            local name, appearance = analyzesymbol(specification.symbol,attachment_symbols)
449            local flags = specification.flags or 0 -- to keep it expandable
450            local d = pdfdictionary {
451                Subtype  = pdfconstant("FileAttachment"),
452                FS       = aref,
453                Contents = pdfunicode(title),
454                Name     = name,
455                NM       = pdfstring("attachment:"..nofattachments),
456                T        = author ~= "" and pdfunicode(author) or nil,
457                Subj     = subtitle ~= "" and pdfunicode(subtitle) or nil,
458                C        = analyzecolor(specification.colorvalue,specification.colormodel),
459                CA       = analyzetransparency(specification.transparencyvalue),
460                AP       = appearance,
461                OC       = analyzelayer(specification.layer),
462             -- F        = pdfnull(), -- another rediculous need to satisfy validation
463                F        = (flags | 4) & (1023-1-2-32-256), -- set 3, clear 1,2,6,9; PDF 32000-1, p385
464            }
465            local width  = specification.width  or 0
466            local height = specification.height or 0
467            local depth  = specification.depth  or 0
468            local box    = hpacknode(nodeinjections.annotation(width,height,depth,d()))
469            box.width    = width
470            box.height   = height
471            box.depth    = depth
472            return box
473        end
474    end
475end
476
477function codeinjections.attachmentid(filename) -- not used in context
478    return filestreams[filename]
479end
480
481-- Comments
482
483local nofcomments      = 0
484local usepopupcomments = false
485
486local defaultattributes = {
487    ["xmlns"]           = "http://www.w3.org/1999/xhtml",
488    ["xmlns:xfa"]       = "http://www.xfa.org/schema/xfa-data/1.0/",
489    ["xfa:contentType"] = "text/html",
490    ["xfa:APIVersion"]  = "Acrobat:8.0.0",
491    ["xfa:spec"]        = "2.4",
492}
493
494local function checkcontent(text,option)
495    if option and option.xml then
496        local root = xml.convert(text)
497        if root and not root.er then
498            xml.checkbom(root)
499            local body = xml.first(root,"/body")
500            if body then
501                local at = body.at
502                for k, v in next, defaultattributes do
503                    if not at[k] then
504                        at[k] = v
505                    end
506                end
507             -- local content = xml.textonly(root)
508                local richcontent = xml.tostring(root)
509                return nil, pdfunicode(richcontent)
510            end
511        end
512    end
513    return pdfunicode(text)
514end
515
516function nodeinjections.comment(specification) -- brrr: seems to be done twice
517    nofcomments = nofcomments + 1
518    local text = specification.data or ""
519    if specification.space ~= v_yes then
520        text = stripstring(text)
521        text = gsub(text,"[\n\r] *","\n")
522    end
523    text = gsub(text,"\r","\n")
524    local name, appearance = analyzesymbol(specification.symbol,comment_symbols)
525    local tag      = specification.tag      or "" -- this is somewhat messy as recent
526    local title    = specification.title    or "" -- versions of acrobat see the title
527    local subtitle = specification.subtitle or "" -- as author
528    local author   = specification.author   or ""
529    local option   = settings_to_hash(specification.option or "")
530    if author ~= "" then
531        if subtitle == "" then
532            subtitle = title
533        elseif title ~= "" then
534            subtitle = subtitle .. ", " .. title
535        end
536        title = author
537    end
538    if title == "" then
539        title = tag
540    end
541    local content, richcontent = checkcontent(text,option)
542    local d = pdfdictionary {
543        Subtype   = pdfconstant("Text"),
544        Open      = option[v_max] and pdfboolean(true) or nil,
545        Contents  = content,
546        RC        = richcontent,
547        T         = title ~= "" and pdfunicode(title) or nil,
548        Subj      = subtitle ~= "" and pdfunicode(subtitle) or nil,
549        C         = analyzecolor(specification.colorvalue,specification.colormodel),
550        CA        = analyzetransparency(specification.transparencyvalue),
551        OC        = analyzelayer(specification.layer),
552        Name      = name,
553        NM        = pdfstring("comment:"..nofcomments),
554        AP        = appearance,
555    }
556    local width  = specification.width  or 0
557    local height = specification.height or 0
558    local depth  = specification.depth  or 0
559    local box
560    if usepopupcomments then
561        -- rather useless as we can hide/vide
562        local nd = pdfreserveobject()
563        local nc = pdfreserveobject()
564        local c = pdfdictionary {
565            Subtype = pdfconstant("Popup"),
566            Parent  = pdfreference(nd),
567        }
568        d.Popup = pdfreference(nc)
569        box = hpacknode(
570            nodeinjections.annotation(0,0,0,d(),nd),
571            nodeinjections.annotation(width,height,depth,c(),nc)
572        )
573    else
574        box = hpacknode(nodeinjections.annotation(width,height,depth,d()))
575    end
576    box.width  = width  -- redundant
577    box.height = height -- redundant
578    box.depth  = depth  -- redundant
579    return box
580end
581
582-- rendering stuff
583--
584-- object_1  -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >>
585-- object_2  -> <</Type /Rendition /S /MR /C << /Type /MediaClip ... >> >>
586-- rendering -> <</Type /Rendition /S /MS [objref_1 objref_2]>>
587--
588-- we only work foreward here (currently)
589-- annotation is to be packed at the tex end
590
591-- aiff audio/aiff
592-- au   audio/basic
593-- avi  video/avi
594-- mid  audio/midi
595-- mov  video/quicktime
596-- mp3  audio/x-mp3 (mpeg)
597-- mp4  audio/mp4
598-- mp4  video/mp4
599-- mpeg video/mpeg
600-- smil application/smil
601-- swf  application/x-shockwave-flash
602
603-- P  media play parameters (evt /BE for controls etc
604-- A  boolean (audio)
605-- C  boolean (captions)
606-- O  boolean (overdubs)
607-- S  boolean (subtitles)
608-- PL pdfconstant("ADBE_MCI"),
609
610-- F        = flags,
611-- T        = title,
612-- Contents = rubish,
613-- AP       = irrelevant,
614
615-- sound is different, no window (or zero) so we need to collect them and
616-- force them if not set
617
618local ms, mu, mf = { }, { }, { }
619
620local function delayed(label)
621    local reserved = mu[label]
622    if not reserved then
623        reserved = pdfreserveobject()
624        mu[label] = reserved
625    end
626    return pdfreference(reserved)
627end
628
629local function checkedreference(ref)
630    local set, bug = structures.references.identify("",ref)
631    if not bug and #set > 0 then
632        return pdfaction(set)
633    end
634end
635
636local function insertrenderingwindow(specification)
637    local actions   = nil
638    local label     = specification.label
639    local openpage  = specification.openpage
640    local closepage = specification.closepage
641    local option    = settings_to_hash(specification.option)
642    if option[v_auto] then
643        if not openpage or openpage == "" then
644            openpage = variables.StartCurrentRendering
645        end
646        if not closepage or closepage == "" then
647            closepage = variables.StopCurrentRendering
648        end
649    end
650    openpage  = checkedreference(openpage)
651    closepage = checkedreference(closepage)
652    if openpage or closepage then
653        actions = pdfdictionary {
654            PO = openpage,
655            PC = closepage,
656        }
657    end
658    local page = tonumber(specification.page) or texgetcount("realpageno") -- todo
659    local r = mu[label] or pdfreserveobject() -- reserve, child refers to parent
660    local a = pdfdictionary {
661        S  = pdfconstant("Rendition"),
662        R  = mf[label],
663        OP = 0,
664        AN = pdfreference(r),
665    }
666    local bs, bc = pdfborder()
667    local d = pdfdictionary {
668        Subtype = pdfconstant("Screen"),
669        P       = pdfreference(pdfpagereference(page)),
670        A       = a, -- needed in order to make the annotation clickable (i.e. don't bark)
671        T       = pdfunicode(label), -- for JS
672        Border  = bs,
673        C       = bc,
674        AA      = actions,
675    }
676    local width = specification.width or 0
677    local height = specification.height or 0
678    context(nodeinjections.annotation(width,height,0,d(),r)) -- save ref
679    return pdfreference(r)
680end
681
682-- some dictionaries can have a MH (must honor) or BE (best effort) capsule
683
684local function insertrendering(specification)
685    local label  = specification.label
686    local option = settings_to_hash(specification.option)
687    if not mf[label] then
688        local filename = specification.filename
689        if filename and filename ~= "" then
690            local isurl    = find(filename,"://",1,true)
691            local mimetype = specification.mimetype or specification.mime
692         -- local start = pdfdictionary {
693         --     Type = pdfconstant("MediaOffset"),
694         --     S = pdfconstant("T"), -- time
695         --     T = pdfdictionary { -- time
696         --         Type = pdfconstant("Timespan"),
697         --         S    = pdfconstant("S"),
698         --         V    = 3, -- time in seconds
699         --     },
700         -- }
701         -- local start = pdfdictionary {
702         --     Type = pdfconstant("MediaOffset"),
703         --     S = pdfconstant("F"), -- frame
704         --     F = 100 -- framenumber
705         -- }
706         -- local start = pdfdictionary {
707         --     Type = pdfconstant("MediaOffset"),
708         --     S = pdfconstant("M"), -- mark
709         --     M = "somemark",
710         -- }
711         -- local parameters = pdfdictionary {
712         --     BE = pdfdictionary {
713         --          B = start,
714         --     }
715         -- }
716            local parameters = pdfdictionary {
717                Type = pdfconstant("MediaPermissions"),
718                TF   = pdfstring("TEMPALWAYS"), -- TEMPNEVER TEMPEXTRACT TEMPACCESS TEMPALWAYS / needed for acrobat/wmp
719            }
720            local descriptor = pdfdictionary {
721                Type = pdfconstant("Filespec"),
722                F    = filename,
723            }
724            if isurl then
725                descriptor.FS = pdfconstant("URL")
726                descriptor = pdfreference(pdfflushobject(descriptor))
727            elseif option[v_embed] then
728                descriptor = codeinjections.embedfile {
729                    file           = filename,
730                    mimetype       = mimetype, -- yes or no
731                    title          = option[v_title],
732                    compress       = option[v_compress] or false,
733                    forcereference = option[v_list] ~= v_no,
734                }
735            end
736            local clip = pdfdictionary {
737                Type = pdfconstant("MediaClip"),
738                S    = pdfconstant("MCD"),
739                N    = label,
740                CT   = mimetype,
741                Alt  = pdfarray { "", "file not found" }, -- language id + message
742                D    = descriptor,
743                P    = pdfreference(pdfflushobject(parameters)),
744            }
745            local rendition = pdfdictionary {
746                Type = pdfconstant("Rendition"),
747                S    = pdfconstant("MR"),
748                N    = pdfunicode(label),
749                C    = pdfreference(pdfflushobject(clip)),
750            }
751            mf[label] = pdfreference(pdfflushobject(rendition))
752        end
753    end
754end
755
756function codeinjections.processrendering(label)
757    local specification = interactions.renderings.rendering(label)
758    if specification then
759        insertrendering(specification)
760    else
761        -- error
762    end
763end
764
765-- needed mapping for access from JS
766
767local function flushrenderings()
768    if next(mf) then
769        local r = pdfarray()
770        for label, reference in sortedhash(mf) do
771            r[#r+1] = pdfunicode(label)
772            r[#r+1] = reference -- already a reference
773        end
774        lpdf.addtonames("Renditions",pdfreference(pdfflushobject(pdfdictionary{ Names = r })))
775    end
776end
777
778lpdf.registerdocumentfinalizer(flushrenderings,"renderings")
779
780function codeinjections.insertrenderingwindow(specification)
781    local label = specification.label
782    codeinjections.processrendering(label)
783    ms[label] = insertrenderingwindow(specification)
784end
785
786local function set(operation,label)
787    if not label or label == "" then
788        -- This one is local to the window so unlikely to be set.
789        label = getmacro("currentrendering")
790    end
791    if label and label ~= "" then
792        codeinjections.processrendering(label)
793        return pdfdictionary {
794            S  = pdfconstant("Rendition"),
795            OP = operation,
796            R  = mf[label],
797            AN = ms[label] or delayed(label),
798        }
799    end
800end
801
802function executers.startrendering (label) return set(0,label) end
803function executers.stoprendering  (label) return set(1,label) end
804function executers.pauserendering (label) return set(2,label) end
805function executers.resumerendering(label) return set(3,label) end
806