grph-inc.lmt /size: 80 Kb    last modification: 2024-01-16 09:02
1if not modules then modules = { } end modules ['grph-inc'] = {
2    version   = 1.001,
3    comment   = "companion to grph-inc.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-- todo: in pdfe: pdfe.copyappearance(document,objnum)
10--
11-- local im = createimage { filename = fullname }
12-- local on = images.flushobject(im,document.__xrefs__[AP])
13
14-- todo: files are sometimes located twice
15-- todo: empty filename or only suffix always false (not found)
16-- lowercase types
17-- mps tex tmp svg
18-- partly qualified
19-- dimensions
20-- use metatables
21-- figures.boxnumber can go as we now can use names
22-- avoid push
23-- move some to command namespace
24
25--[[
26The ConTeXt figure inclusion mechanisms are among the oldest code
27in ConTeXt and evolved into a complex whole. One reason is that we
28deal with backend in an abstract way. What complicates matters is
29that we deal with internal graphics as well: TeX code, MetaPost code,
30etc. Later on figure databases were introduced, which resulted in
31a plug in model for locating images. On top of that runs a conversion
32mechanism (with caching) and resource logging.
33
34Porting that to Lua is not that trivial because quite some
35status information is kept between al these stages. Of course, image
36reuse also has some price, and so I decided to implement the graphics
37inclusion in several layers: detection, loading, inclusion, etc.
38
39Object sharing and scaling can happen at each stage, depending on the
40way the resource is dealt with.
41
42The TeX-Lua mix is suboptimal. This has to do with the fact that we cannot
43run TeX code from within Lua. Some more functionality will move to Lua.
44]]--
45
46-- todo: store loaded pages per pdf file someplace
47
48local tonumber, tostring, next, unpack = tonumber, tostring, next, unpack
49local format, lower, find, match, gsub = string.format, string.lower, string.find, string.match, string.gsub
50local longtostring, texhashed = string.longtostring, string.texhashed
51local contains = table.contains
52local sortedhash, sortedkeys = table.sortedhash, table.sortedkeys
53local concat, insert, remove, unique = table.concat, table.insert, table.remove, table.unique
54local todimen = string.todimen
55local collapsepath = file.collapsepath
56local formatters = string.formatters
57local odd = math.odd
58local isfile, isdir, modificationtime = lfs.isfile, lfs.isdir, lfs.modification
59local joinfile, is_qualified_path = file.join, file.is_qualified_path
60
61local P, R, S, Cc, C, Cs, Ct, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.Cc, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.match
62
63local settings_to_array = utilities.parsers.settings_to_array
64local settings_to_hash  = utilities.parsers.settings_to_hash
65local settings_to_set   = utilities.parsers.settings_to_set
66local allocate          = utilities.storage.allocate
67local setmetatableindex = table.setmetatableindex
68local replacetemplate   = utilities.templates.replace
69
70local bpfactor          = number.dimenfactors.bp
71
72images                  = images or { }
73local images            = images
74
75local hasscheme         = url.hasscheme
76local urlhashed         = url.hashed
77
78local resolveprefix     = resolvers.resolve
79
80local texgetbox         = tex.getbox
81local texsetbox         = tex.setbox
82
83local hpack             = nodes.hpack
84
85local new_latelua       = nodes.pool.latelua
86local new_hlist         = nodes.pool.hlist
87
88local context           = context
89
90local implement         = interfaces.implement
91local variables         = interfaces.variables
92
93local codeinjections    = backends.codeinjections
94local nodeinjections    = backends.nodeinjections
95
96local trace_figures     = false  trackers.register  ("graphics.locating",   function(v) trace_figures    = v end)
97local trace_bases       = false  trackers.register  ("graphics.bases",      function(v) trace_bases      = v end)
98local trace_programs    = false  trackers.register  ("graphics.programs",   function(v) trace_programs   = v end)
99local trace_conversion  = false  trackers.register  ("graphics.conversion", function(v) trace_conversion = v end)
100local trace_inclusion   = false  trackers.register  ("graphics.inclusion",  function(v) trace_inclusion  = v end)
101local trace_usage       = false  trackers.register  ("graphics.usage",      function(v) trace_usage      = v end)
102
103local extra_check       = false  directives.register("graphics.extracheck",    function(v) extra_check    = v end)
104local auto_transform    = true   directives.register("graphics.autotransform", function(v) auto_transform = v end)
105
106local report            = logs.reporter("graphics")
107local report_inclusion  = logs.reporter("graphics","inclusion")
108
109local f_hash_part       = formatters["%s->%s->%s->%s"]
110local f_hash_full       = formatters["%s->%s->%s->%s->%s->%s->%s->%s->%s"]
111
112local v_yes             = variables.yes
113local v_always          = variables.always
114local v_embed           = variables.embed
115local v_fix             = variables.fix
116local v_global          = variables["global"]
117local v_local           = variables["local"]
118local v_default         = variables.default
119local v_auto            = variables.auto
120
121local maxdimen          = tex.magicconstants.maxdimen -- 0x3FFFFFFF -- 2^30-1
122
123local ctx_doscalefigure            = context.doscalefigure
124local ctx_relocateexternalfigure   = context.relocateexternalfigure
125local ctx_startfoundexternalfigure = context.startfoundexternalfigure
126local ctx_stopfoundexternalfigure  = context.stopfoundexternalfigure
127local ctx_dosetfigureobject        = context.dosetfigureobject
128local ctx_doboxfigureobject        = context.doboxfigureobject
129
130-- extensions
131
132function checkimage(figure)
133    if figure then
134        local width  = figure.width or 0
135        local height = figure.height or 0
136        if width <= 0 or height <= 0 then
137            report_inclusion("image %a has bad dimensions (%p,%p), discarding",figure.filename or "?",width,height)
138            return false, "bad dimensions"
139        end
140     -- local xres    = figure.xres
141     -- local yres    = figure.yres
142        local changes = false
143        if height > width then
144            if height > maxdimen then
145                figure.height = maxdimen
146                figure.width  = width * maxdimen/height
147                changed       = true
148            end
149        elseif width > maxdimen then
150            figure.width  = maxdimen
151            figure.height = height * maxdimen/width
152            changed       = true
153        end
154        if changed then
155            report_inclusion("limiting natural dimensions of %a, old %p * %p, new %p * %p",
156                figure.filename,width,height,figure.width,figure.height)
157        end
158        if width >= maxdimen or height >= maxdimen then
159            report_inclusion("image %a is too large (%p,%p), discarding",
160                figure.filename,width,height)
161            return false, "dimensions too large"
162        end
163        return figure
164    end
165end
166
167-- This is a bit of abstraction. Fro a while we will follow the luatex image
168-- model.
169
170local imagekeys  = {
171    -- only relevant ones (permitted in luatex)
172    "width", "height", "depth", "bbox",
173    "colordepth", "colorspace",
174    "filename", "filepath", "visiblefilename",
175    "imagetype", "stream",
176    "index", "objnum",
177    "pagebox", "page", "pages",
178    "rotation", "transform",
179    "xsize", "ysize", "xres", "yres",
180}
181
182local imagesizes = {
183    art   = true, bleed = true, crop = true,
184    media = true, none  = true, trim = true,
185}
186
187local imagetypes = { [0] =
188    "none",
189    "pdf", "png", "jpg", "jp2", "jbig2",
190    "stream",
191}
192
193imagetypes   = table.swapped(imagetypes,imagetypes)
194
195images.keys  = imagekeys
196images.types = imagetypes
197images.sizes = imagesizes
198
199local codeinjections = backends.codeinjections
200
201local function createimage(specification)
202    return codeinjections.newimage(specification)
203end
204
205local function copyimage(specification)
206    return codeinjections.copyimage(specification)
207end
208
209local function scanimage(specification)
210    return codeinjections.scanimage(specification)
211end
212
213local function embedimage(specification)
214   -- write the image to file
215    return codeinjections.embedimage(specification)
216end
217
218local function wrapimage(specification)
219    -- create an image rule
220    return codeinjections.wrapimage(specification)
221end
222
223images.create = createimage
224images.scan   = scanimage
225images.copy   = copyimage
226images.wrap   = wrapimage
227images.embed  = embedimage
228
229-- If really needed we can provide:
230--
231-- img = {
232--     new                  = createimage,
233--     scan                 = scanimage,
234--     copy                 = copyimage,
235--     node                 = wrapimage,
236--     write                = function(specification) context(wrapimage(specification)) end,
237--     immediatewrite       = embedimage,
238--     immediatewriteobject = function() end, -- not upported, experimental anyway
239--     boxes                = function() return sortedkeys(imagesizes) end,
240--     fields               = function() return imagekeys end,
241--     types                = function() return { unpack(imagetypes,0,#imagetypes) } end,
242-- }
243
244-- end of copies / mapping
245
246local function imagetotable(imgtable)
247    if type(imgtable) == "table" then
248        return copy(imgtable)
249    end
250    local result = { }
251    for k=1,#imagekeys do
252        local key   = imagekeys[k]
253        result[key] = imgtable[key]
254    end
255    return result
256end
257
258function images.serialize(i,...)
259    return table.serialize(imagetotable(i),...)
260end
261
262function images.print(i,...)
263    return table.print(imagetotable(i),...)
264end
265
266local function checkimagesize(size)
267    if size then
268        size = gsub(size,"box","")
269        return imagesizes[size] and size or "crop"
270    else
271        return "crop"
272    end
273end
274
275images.check     = checkimage
276images.checksize = checkimagesize
277images.totable   = imagetotable
278
279-- local indexed = { }
280--
281-- function images.ofindex(n)
282--     return indexed[n]
283-- end
284
285--- we can consider an grph-ini file
286
287figures                 = figures or { }
288local figures           = figures
289
290figures.images          = images
291figures.boxnumber       = figures.boxnumber or 0
292figures.defaultsearch   = true
293figures.defaultwidth    = 0
294figures.defaultheight   = 0
295figures.defaultdepth    = 0
296figures.nofprocessed    = 0
297figures.nofmissing      = 0
298figures.preferquality   = true -- quality over location
299
300local figures_loaded    = allocate()   figures.loaded      = figures_loaded
301local figures_used      = allocate()   figures.used        = figures_used
302local figures_found     = allocate()   figures.found       = figures_found
303local figures_suffixes  = allocate()   figures.suffixes    = figures_suffixes
304local figures_patterns  = allocate()   figures.patterns    = figures_patterns
305local figures_resources = allocate()   figures.resources   = figures_resources
306
307local existers          = allocate()   figures.existers    = existers
308local checkers          = allocate()   figures.checkers    = checkers
309local includers         = allocate()   figures.includers   = includers
310local remappers         = allocate()   figures.remappers   = remappers
311local converters        = allocate()   figures.converters  = converters
312local identifiers       = allocate()   figures.identifiers = identifiers
313local programs          = allocate()   figures.programs    = programs
314local conversions       = allocate()   figures.conversions = conversions
315
316local defaultformat <const> = "pdf"
317local defaultprefix <const> = "m_k_i_v_"
318
319figures.localpaths = allocate {
320    ".", "..", "../.."
321}
322
323figures.cachepaths = allocate {
324    prefix  = "",
325    path    = ".",
326    subpath = ".",
327}
328
329local figure_paths = allocate(table.copy(figures.localpaths))
330figures.paths      = figure_paths
331
332local figures_order =  allocate {
333    -- "mov" is gone
334    "pdf", "mps", "jpg", "png", "jp2", "jbig", "svg", "eps", "tif", "gif", "buffer", "tex", "cld", "auto",
335}
336
337local figures_formats = allocate { -- magic and order will move here
338    ["pdf"]    = { list = { "pdf" } },
339    ["mps"]    = { patterns = { "^mps$", "^%d+$" } }, -- we need to anchor
340    ["jpg"]    = { list = { "jpg", "jpeg" } },
341    ["png"]    = { list = { "png" } },
342    ["jp2"]    = { list = { "jp2" } },
343    ["jbig"]   = { list = { "jbig", "jbig2", "jb2" } },
344    ["svg"]    = { list = { "svg", "svgz" } },
345    ["eps"]    = { list = { "eps", "ai" } },
346    ["gif"]    = { list = { "gif" } },
347    ["tif"]    = { list = { "tif", "tiff" } },
348 -- ["mov"]    = { list = { "mov", "flv", "mp4" } }, -- "avi" is not supported
349    ["buffer"] = { list = { "tmp", "buffer", "buf" } },
350    ["tex"]    = { list = { "tex" } },
351    ["cld"]    = { list = { "cld" } },
352    ["auto"]   = { list = { "auto" } },
353}
354
355local figures_magics = allocate {
356    { format = "png", pattern = P("\137PNG\013\010\026\010") },                   -- 89 50 4E 47 0D 0A 1A 0A,
357    { format = "jpg", pattern = P("\255\216\255") },                              -- FF D8 FF
358    { format = "jp2", pattern = P("\000\000\000\012\106\080\032\032\013\010"), }, -- 00 00 00 0C 6A 50 20 20 0D 0A },
359    { format = "gif", pattern = P("GIF") },
360    { format = "pdf", pattern = (1 - P("%PDF"))^0 * P("%PDF") },
361}
362
363local figures_native = allocate {
364    pdf = true,
365    jpg = true,
366    jp2 = true,
367    png = true,
368}
369
370figures.formats = figures_formats -- frozen
371figures.magics  = figures_magics  -- frozen
372figures.order   = figures_order   -- frozen
373
374-- name checker
375
376local okay = P("m_k_i_v_")
377
378local pattern = (R("az","AZ") * P(":"))^-1 * (                                      -- a-z : | A-Z :
379    (okay + R("az","09") + S("_/") - P("_")^2)^1 * (P(".") * R("az")^1)^0 * P(-1) + -- a-z | single _ | /
380    (okay + R("az","09") + S("-/") - P("-")^2)^1 * (P(".") * R("az")^1)^0 * P(-1) + -- a-z | single - | /
381    (okay + R("AZ","09") + S("_/") - P("_")^2)^1 * (P(".") * R("AZ")^1)^0 * P(-1) + -- A-Z | single _ | /
382    (okay + R("AZ","09") + S("-/") - P("-")^2)^1 * (P(".") * R("AZ")^1)^0 * P(-1)   -- A-Z | single - | /
383) * Cc(false) + Cc(true)
384
385function figures.badname(name)
386    if not name then
387        -- bad anyway
388    elseif not hasscheme(name) then
389        return lpegmatch(pattern,name)
390    else
391        return lpegmatch(pattern,file.basename(name))
392    end
393end
394
395logs.registerfinalactions(function()
396    local done = false
397    if trace_usage and figures.nofprocessed > 0 then
398        logs.startfilelogging(report,"names")
399        for _, data in sortedhash(figures_found) do
400            if done then
401                report()
402            else
403                done = true
404            end
405            report("asked   : %s",data.askedname)
406            if data.found then
407                report("format  : %s",data.format)
408                report("found   : %s",data.foundname)
409                report("used    : %s",data.fullname)
410                if data.badname then
411                    report("comment : %s","bad name")
412                elseif data.comment then
413                    report("comment : %s",data.comment)
414                end
415            else
416                report("comment : %s","not found")
417            end
418        end
419        logs.stopfilelogging()
420    end
421    if figures.nofmissing > 0 and logs.loggingerrors() then
422        logs.starterrorlogging(report,"missing figures")
423        for _, data in sortedhash(figures_found) do
424            report("%w%s",6,data.askedname)
425        end
426        logs.stoperrorlogging()
427    end
428end)
429
430-- We can set the order but only indirectly so that we can check for support.
431
432function figures.setorder(list) -- can be table or string
433    if type(list) == "string" then
434        list = settings_to_array(list)
435    end
436    if list and #list > 0 then
437        figures_order = allocate()
438        figures.order = figures_order
439        local done = { } -- just to be sure in case the list is generated
440        for i=1,#list do
441            local l = lower(list[i])
442            if figures_formats[l] and not done[l] then
443                figures_order[#figures_order+1] = l
444                done[l] = true
445            end
446        end
447        report_inclusion("lookup order % a",figures_order)
448    else
449        -- invalid list
450    end
451end
452
453local function guessfromstring(str,name)
454    if str then
455        for i=1,#figures_magics do
456            local pattern = figures_magics[i]
457            if lpegmatch(pattern.pattern,str) then
458                local format = pattern.format
459                if trace_figures and name then
460                    report_inclusion("file %a has format %a",name,format)
461                end
462                return format
463            end
464        end
465    end
466end
467
468figures.guessfromstring = guessfromstring
469
470function figures.guess(filename)
471    local f = io.open(filename,'rb')
472    if f then
473        local str = f:read(100)
474        f:close()
475        if str then
476            return guessfromstring(str)
477        end
478    end
479end
480
481local function setlookups() -- tobe redone .. just set locals
482    figures_suffixes = allocate()
483    figures_patterns = allocate()
484    for _, format in next, figures_order do
485        local data = figures_formats[format]
486        local list = data.list
487        if list then
488            for i=1,#list do
489                figures_suffixes[list[i]] = format -- hash
490            end
491        else
492            figures_suffixes[format] = format
493        end
494        local patterns = data.patterns
495        if patterns then
496            for i=1,#patterns do
497                figures_patterns[#figures_patterns+1] = { patterns[i], format } -- array
498            end
499        end
500    end
501    figures.suffixes = figures_suffixes
502    figures.patterns = figures_patterns
503end
504
505setlookups()
506
507figures.setlookups = setlookups
508
509function figures.registerresource(t)
510    local n = #figures_resources + 1
511    figures_resources[n] = t
512    return n
513end
514
515local function register(tag,what,target)
516    local data = figures_formats[target] -- resolver etc
517    if not data then
518        data = { }
519        figures_formats[target] = data
520    end
521    local d = data[tag] -- list or pattern
522    if d and not contains(d,what) then
523        d[#d+1] = what -- suffix or patternspec
524    else
525        data[tag] = { what }
526    end
527    if not contains(figures_order,target) then
528        figures_order[#figures_order+1] = target
529    end
530    setlookups()
531end
532
533function figures.registersuffix (suffix, target) register('list',suffix,target) end
534function figures.registerpattern(pattern,target) register('pattern',pattern,target) end
535
536implement { name = "registerfiguresuffix",  actions = register, arguments = { "'list'",    "string", "string" } }
537implement { name = "registerfigurepattern", actions = register, arguments = { "'pattern'", "string", "string" } }
538
539local last_locationset = last_locationset or nil
540local last_pathlist    = last_pathlist    or nil
541
542function figures.setpaths(locationset,pathlist)
543    if last_locationset == locationset and last_pathlist == pathlist then
544        -- this function can be called each graphic so we provide this optimization
545        return
546    end
547    local t = figure_paths
548    local h = settings_to_hash(locationset)
549    local p = environment.arguments.path
550    if last_locationset ~= locationset then
551        -- change == reset (actually, a 'reset' would indeed reset
552        if h[v_local] then
553            t = table.fastcopy(figures.localpaths or { })
554        else
555            t = { }
556        end
557        figures.defaultsearch = h[v_default]
558        last_locationset = locationset
559        if p then
560            for i=1,#t do
561                t[#t+1] = joinfile(p,t[i])
562            end
563        end
564    end
565    if h[v_global] then
566        local list = settings_to_array(pathlist)
567        for i=1,#list do
568            local s = list[i]
569            if not contains(t,s) then
570                t[#t+1] = s
571            end
572        end
573        if p then
574            for i=1,#t do
575                if not is_qualified_path(t[i]) then
576                    t[#t+1] = joinfile(p,t[i])
577                end
578            end
579        end
580    end
581    -- new
582    if p then
583        insert(t,1,p)
584    end
585    t = unique(t)
586    --
587    figure_paths  = t
588    last_pathlist = pathlist
589    figures.paths = figure_paths
590    if trace_figures then
591        report_inclusion("using locations %a",last_locationset)
592        report_inclusion("using paths % a",figure_paths)
593    end
594end
595
596implement { name = "setfigurepaths", actions = figures.setpaths, arguments = "2 strings" }
597
598-- check conversions and handle it here
599
600function figures.hash(data)
601    local status = data and data.status
602    return (status and status.hash or tostring(status.private)) or "nohash" -- the <img object>
603end
604
605-- interfacing to tex
606
607local function new() -- we could use metatables status -> used -> request but it needs testing
608    local request = {
609        name       = false,
610        label      = false,
611        format     = false,
612        page       = false,
613        width      = false,
614        height     = false,
615        preview    = false,
616        ["repeat"] = false,
617        controls   = false,
618        display    = false,
619        mask       = false,
620        crop       = false,
621        conversion = false,
622        resolution = false,
623        color      = false,
624        arguments  = false,
625        cache      = false,
626        prefix     = false,
627        size       = false,
628    }
629    local used = {
630        fullname   = false,
631        format     = false,
632        name       = false,
633        path       = false,
634        suffix     = false,
635        width      = false,
636        height     = false,
637    }
638    local status = {
639        status     = 0,
640        converted  = false,
641        cached     = false,
642        fullname   = false,
643        format     = false,
644    }
645    -- this needs checking because we might check for nil, the test case
646    -- is getfiguredimensions which then should return ~= 0
647 -- setmetatableindex(status, used)
648 -- setmetatableindex(used, request)
649    return {
650        request = request,
651        used    = used,
652        status  = status,
653    }
654end
655
656-- use table.insert|remove
657
658local lastfiguredata = nil -- will be topofstack or last so no { } (else problems with getfiguredimensions)
659local callstack      = { }
660
661function figures.initialize(request)
662    local figuredata = new()
663    if request then
664        -- request.width/height are strings and are only used when no natural dimensions
665        -- can be determined; at some point the handlers might set them to numbers instead
666        local w = tonumber(request.width) or 0
667        local h = tonumber(request.height) or 0
668        local p = tonumber(request.page) or 0
669        request.width  = w > 0 and w or nil
670        request.height = h > 0 and h or nil
671        --
672        request.page      = p > 0 and p or 1
673        request.keepopen  = p > 0
674        request.size      = checkimagesize(request.size)
675        request.object    = request.object == v_yes
676        request["repeat"] = request["repeat"] == v_yes
677        request.preview   = request.preview == v_yes
678        request.cache     = request.cache ~= "" and request.cache
679        request.prefix    = request.prefix ~= "" and request.prefix
680        request.format    = request.format ~= "" and request.format
681        request.compact   = backends.codeinjections.validcompactor(request.compact)
682        table.merge(figuredata.request,request)
683    end
684    return figuredata
685end
686
687function figures.push(request)
688    statistics.starttiming(figures)
689    local figuredata = figures.initialize(request) -- we could use table.sparse but we set them later anyway
690    insert(callstack,figuredata)
691    lastfiguredata = figuredata
692    return figuredata
693end
694
695function figures.pop()
696    remove(callstack)
697    lastfiguredata = callstack[#callstack] or lastfiguredata
698    statistics.stoptiming(figures)
699end
700
701function figures.current()
702    return callstack[#callstack] or lastfiguredata
703end
704
705function figures.setconversion(format,conversion)
706    if conversion == "reset" or conversion == "" then
707        conversion = nil
708    end
709    conversions[format] = conversion
710end
711
712local hash = { name = true, label = true, fullname = true }
713
714local function get(category,tag,default)
715    local value = lastfiguredata and lastfiguredata[category]
716    value = value and value[tag]
717    if not value or value == "" or value == true then
718        return default or ""
719    else
720        if hash[tag] then
721            value = texhashed(value)
722        end
723        return value
724    end
725end
726
727local function setdimensions(box)
728    local status = lastfiguredata and lastfiguredata.status
729    local used   = lastfiguredata and lastfiguredata.used
730    if status and used then
731        local b = texgetbox(box)
732        local w = b.width
733        local h = b.height + b.depth
734        status.width  = w
735        status.height = h
736        used.width    = w
737        used.height   = h
738        status.status = 10
739    end
740end
741
742figures.get = get
743figures.set = setdimensions
744
745implement { name = "figurestatus",   actions = { get, context }, arguments = { "'status'",  "string", "string" } }
746implement { name = "figurerequest",  actions = { get, context }, arguments = { "'request'", "string", "string" } }
747implement { name = "figureused",     actions = { get, context }, arguments = { "'used'",    "string", "string" } }
748
749implement { name = "figurefilepath", public = true, actions = { get, file.dirname,  context }, arguments = { "'used'", "'fullname'" } }
750implement { name = "figurefilename", public = true, actions = { get, file.nameonly, context }, arguments = { "'used'", "'fullname'" } }
751implement { name = "figurefiletype", public = true, actions = { get, file.extname,  context }, arguments = { "'used'", "'fullname'" } }
752
753implement { name = "figuresetdimensions", actions = setdimensions, arguments = "integer" }
754
755-- todo: local path or cache path
756
757local function forbiddenname(filename)
758    if not filename or filename == "" then
759        return false
760    end
761    local expandedfullname = collapsepath(filename,true)
762    local expandedinputname = collapsepath(file.addsuffix(environment.jobfilename,environment.jobfilesuffix),true)
763    if expandedfullname == expandedinputname then
764        report_inclusion("skipping graphic with same name as input filename %a, enforce suffix",expandedinputname)
765        return true
766    end
767    local expandedoutputname = collapsepath(codeinjections.getoutputfilename(),true)
768    if expandedfullname == expandedoutputname then
769        report_inclusion("skipping graphic with same name as output filename %a, enforce suffix",expandedoutputname)
770        return true
771    end
772end
773
774local function rejected(specification)
775    if extra_check then
776        local fullname = specification.fullname
777        if fullname and figures_native[file.suffix(fullname)] and not figures.guess(fullname) then
778            specification.comment = "probably a bad file"
779            specification.found   = false
780            specification.error   = true
781            report_inclusion("file %a looks bad",fullname)
782            return true
783        end
784    end
785end
786
787local function wipe(str)
788    if str == "" or str == "default" or str == "unknown" then
789        return nil
790    else
791        return str
792    end
793end
794
795local function register(askedname,specification)
796    if not specification then
797        specification = { askedname = askedname, comment = "invalid specification" }
798    elseif forbiddenname(specification.fullname) then
799        specification = { askedname = askedname, comment = "forbidden name" }
800    elseif specification.internal then
801        -- no filecheck needed
802        specification.found = true
803        if trace_figures then
804            report_inclusion("format %a internally supported by engine",specification.format)
805        end
806    elseif not rejected(specification) then
807        local format = specification.format
808        if format then
809            local conversion = wipe(specification.conversion)
810            local resolution = wipe(specification.resolution)
811            local arguments  = wipe(specification.arguments)
812            local crop       = wipe(specification.crop)
813            local newformat  = conversion
814            if not newformat or newformat == "" then
815                newformat = defaultformat
816            end
817            --
818            conversion = conversions[format] or conversion
819            --
820            if trace_conversion then
821                report_inclusion("checking conversion of %a, fullname %a, old format %a, new format %a, conversion %a, resolution %a, crop %a, arguments %a",
822                    askedname,
823                    specification.fullname,
824                    format,
825                    newformat,
826                    conversion or "default",
827                    resolution or "default",
828                    crop       or "default",
829                    arguments  or ""
830                )
831            end
832            -- begin of quick hack
833            local remapper = remappers[format]
834            if remapper then
835                remapper = remapper[conversion]
836                if remapper then
837                    specification = remapper(specification) or specification
838                    format        = specification.format
839                    newformat     = format
840                    conversion    = nil
841                end
842            end
843            -- end of quick hack
844            local converter = (not remapper) and (newformat ~= format or resolution or arguments) and converters[format] -- no crop here
845            if converter then
846                local okay = converter[newformat]
847                if okay then
848                    converter = okay
849                else
850                    newformat = defaultformat
851                    converter = converter[newformat]
852                end
853            elseif trace_conversion then
854                report_inclusion("no converter for %a to %a",format,newformat)
855            end
856            if converter then
857                -- todo: make this a function
858                --
859                -- todo: outline as helper function
860                --
861                local oldname = specification.fullname
862                local newpath = file.dirname(oldname)
863                local oldbase = file.basename(oldname)
864                local runpath = environment.arguments.runpath
865                if runpath and runpath ~= "" and newpath == environment.arguments.path then
866                    newpath = runpath
867                end
868                --
869                -- problem: we can have weird filenames, like a.b.c (no suffix) and a.b.c.gif
870                -- so we cannot safely remove a suffix (unless we do that for known suffixes)
871                --
872                -- local newbase = file.removesuffix(oldbase) -- assumes a known suffix
873                --
874                -- so we now have (also see *):
875                --
876                local newbase = oldbase
877                --
878                local fc = specification.cache or figures.cachepaths.path
879                if fc and fc ~= "" and fc ~= "." then
880                    newpath = gsub(fc,"%*",newpath) -- so cachedir can be "/data/cache/*"
881                else
882                    newbase = defaultprefix .. newbase
883                end
884                local subpath = specification.subpath or figures.cachepaths.subpath
885                if subpath and subpath ~= "" and subpath ~= "."  then
886                    newpath = newpath .. "/" .. subpath
887                end
888                if not isdir(newpath) then
889                    dir.makedirs(newpath)
890                    if not file.is_writable(newpath) then
891                        if trace_conversion then
892                            report_inclusion("path %a is not writable, forcing conversion path %a",newpath,".")
893                        end
894                        newpath = "."
895                    end
896                end
897                local prefix = specification.prefix or figures.cachepaths.prefix
898                if prefix and prefix ~= "" then
899                    newbase = prefix .. newbase
900                end
901                local hash = ""
902                if resolution then
903                    hash = hash .. "[r:" .. resolution .. "]"
904                end
905                if arguments then
906                    hash = hash .. "[a:" .. arguments .. "]"
907                end
908                if crop then
909                    hash = hash .. "[c:" .. crop .. "]"
910                end
911                newbase = gsub(newbase,"%.","_") -- nicer to have no suffix in the name
912                if hash ~= "" then
913                    newbase = newbase .. "_" .. md5.hex(hash)
914                end
915                --
916                -- see *, we had:
917                --
918                -- local newbase = file.addsuffix(newbase,newformat)
919                --
920                -- but now have (result of Aditya's web image testing):
921                --
922                -- as a side effect we can now have multiple fetches with different
923                -- original figures_formats, not that it matters much (apart from older conversions
924                -- sticking around)
925                --
926                local newbase = newbase .. "." .. newformat
927                local newname = file.join(newpath,newbase)
928                oldname = collapsepath(oldname)
929                newname = collapsepath(newname)
930                local oldtime = modificationtime(oldname) or 0
931                local newtime = modificationtime(newname) or 0
932                if newtime == 0 or oldtime > newtime then
933                    if trace_conversion then
934                        report_inclusion("converting %a (%a) from %a to %a",askedname,oldname,format,newformat)
935                    end
936                    converter(oldname,newname,resolution or "", arguments or "",specification) -- in retrospect a table
937                else
938                    if trace_conversion then
939                        report_inclusion("no need to convert %a (%a) from %a to %a",askedname,oldname,format,newformat)
940                    end
941                end
942                if io.exists(newname) and io.size(newname) > 0 then
943                    specification.foundname = oldname
944                    specification.fullname  = newname
945                    specification.prefix    = prefix
946                    specification.subpath   = subpath
947                    specification.converted = true
948                    format = newformat
949                    if not figures_suffixes[format] then
950                        -- maybe the new format is lowres.png (saves entry in suffixes)
951                        -- so let's do this extra check
952                        local suffix = file.suffix(newformat)
953                        if figures_suffixes[suffix] then
954                            if trace_figures then
955                                report_inclusion("using suffix %a as format for %a",suffix,format)
956                            end
957                            format = suffix
958                        end
959                    end
960                    specification.format = format
961                elseif io.exists(oldname) then
962                    report_inclusion("file %a is bugged",oldname)
963                    if format and imagetypes[format] then
964                        specification.fullname = oldname
965                    end
966                    specification.converted = false
967                    specification.bugged    = true
968                end
969            end
970        end
971        if format then
972            local found = figures_suffixes[format]
973            if not found then
974                specification.found = false
975                if trace_figures then
976                    report_inclusion("format %a is not supported",format)
977                end
978            elseif imagetypes[format] then
979                specification.found = true
980                if trace_figures then
981                    report_inclusion("format %a natively supported by backend",format)
982                end
983            else
984                specification.found = true -- else no foo.1 mps conversion
985                if trace_figures then
986                    report_inclusion("format %a supported by output file format",format)
987                end
988            end
989        else
990            specification.askedname = askedname
991            specification.found     = false
992        end
993    end
994    if specification.found then
995        specification.foundname = specification.foundname or specification.fullname
996    else
997        specification.foundname = nil
998    end
999    specification.badname   = figures.badname(askedname)
1000    local askedhash = f_hash_part(
1001        askedname,
1002        specification.conversion or "default",
1003        specification.resolution or "default",
1004        specification.arguments  or ""
1005    )
1006    figures_found[askedhash] = specification
1007    if not specification.found then
1008        figures.nofmissing = figures.nofmissing + 1
1009    end
1010    return specification
1011end
1012
1013local resolve_too = false -- true
1014
1015local internalschemes = {
1016    file    = true,
1017    tree    = true,
1018    dirfile = true,
1019    dirtree = true,
1020}
1021
1022local function locate(request) -- name, format, cache
1023    -- not resolvers.cleanpath(request.name) as it fails on a!b.pdf and b~c.pdf
1024    -- todo: more restricted cleanpath
1025    local askedname       = request.name or ""
1026    local askedcache      = request.cache
1027    local askedconversion = request.conversion
1028    local askedresolution = request.resolution
1029    local askedarguments  = request.arguments
1030    local askedcrop       = request.crop
1031    local askedhash       = f_hash_part(
1032        askedname,
1033        askedconversion or "default",
1034        askedresolution or "default",
1035        askedcrop       or "default",
1036        askedarguments  or ""
1037    )
1038    local foundname = figures_found[askedhash]
1039    if foundname then
1040        return foundname
1041    end
1042    --
1043    local askedformat = request.format
1044    if not askedformat or askedformat == "" or askedformat == "unknown" then
1045        askedformat = file.suffix(askedname) or ""
1046    elseif askedformat == v_auto then
1047        if trace_figures then
1048            report_inclusion("ignoring suffix of %a",askedname)
1049        end
1050        askedformat = ""
1051        askedname   = file.removesuffix(askedname)
1052    end
1053    -- protocol check
1054    local hashed = urlhashed(askedname)
1055    if not hashed then
1056        -- go on
1057    elseif internalschemes[hashed.scheme] then
1058        local path = hashed.path
1059        if path and path ~= "" then
1060            askedname = path
1061        end
1062    else
1063        local foundname = resolvers.findbinfile(askedname)
1064        -- we need a neutral isfile
1065        if foundname then
1066            if hashed.scheme == "file" and not isfile(foundname) then
1067                foundname = false
1068            end
1069        end
1070        if not foundname then -- foundname can be dummy
1071            if trace_figures then
1072                report_inclusion("unknown url %a",askedname)
1073            end
1074            -- url not found
1075            return register(askedname)
1076        end
1077        -- ok for now but we need a loadbinfile option to emulate a file
1078        -- so that we justy share it .. we now rely on os caching of files
1079        -- but normally scheme based files are not that large anyway
1080        local guessedformat = false
1081        if hashed.scheme == "file" then
1082            guessedformat = figures.guess(foundname)
1083        else
1084            -- kind of costly: todo pseudo file and just acccess
1085            local found, data = resolvers.loadbinfile(foundname)
1086            guessedformat = figures.guessfromstring(data)
1087        end
1088        if askedformat ~= guessedformat then
1089            if trace_figures then
1090                report_inclusion("url %a has unknown format",askedname)
1091            end
1092            -- url found, but wrong format
1093            return register(askedname)
1094        else
1095            if trace_figures then
1096                report_inclusion("url %a is resolved to %a",askedname,foundname)
1097            end
1098            return register(askedname, {
1099                askedname  = askedname,
1100                fullname   = foundname,
1101                format     = askedformat,
1102                cache      = askedcache,
1103                conversion = askedconversion,
1104                resolution = askedresolution,
1105                crop       = askedcrop,
1106                arguments  = askedarguments,
1107            })
1108        end
1109    end
1110    -- we could use the hashed data instead
1111    local askedpath = file.is_rootbased_path(askedname)
1112    local askedbase = file.basename(askedname)
1113    if askedformat ~= "" then
1114        askedformat = lower(askedformat)
1115        if trace_figures then
1116            report_inclusion("forcing format %a",askedformat)
1117        end
1118        local format = figures_suffixes[askedformat]
1119        if not format then
1120            for i=1,#figures_patterns do
1121                local pattern = figures_patterns[i]
1122                if find(askedformat,pattern[1]) then
1123                    format = pattern[2]
1124                    if trace_figures then
1125                        report_inclusion("asked format %a matches %a",askedformat,pattern[1])
1126                    end
1127                    break
1128                end
1129            end
1130        end
1131        if format then
1132            local foundname, quitscanning, forcedformat, internal = figures.exists(askedname,format,resolve_too) -- not askedformat
1133            if foundname then
1134                return register(askedname, {
1135                    askedname  = askedname,
1136                    fullname   = foundname, -- askedname,
1137                    format     = forcedformat or format,
1138                    cache      = askedcache,
1139                 -- foundname  = foundname, -- no
1140                    conversion = askedconversion,
1141                    resolution = askedresolution,
1142                    arguments  = askedarguments,
1143                    crop       = askedcrop,
1144                    internal   = internal,
1145                })
1146            elseif quitscanning then
1147                return register(askedname)
1148            end
1149            askedformat = format -- new per 2013-08-05
1150        elseif trace_figures then
1151            report_inclusion("unknown format %a",askedformat)
1152        end
1153        if askedpath then
1154            -- path and type given, todo: strip pieces of path
1155            local foundname, quitscanning, forcedformat = figures.exists(askedname,askedformat,resolve_too)
1156            if foundname then
1157                return register(askedname, {
1158                    askedname  = askedname,
1159                    fullname   = foundname, -- askedname,
1160                    format     = forcedformat or askedformat,
1161                    cache      = askedcache,
1162                    conversion = askedconversion,
1163                    resolution = askedresolution,
1164                    crop       = askedcrop,
1165                    arguments  = askedarguments,
1166                })
1167            end
1168        else
1169            -- type given
1170            for i=1,#figure_paths do
1171                local path = resolveprefix(figure_paths[i]) -- we resolve (e.g. jobfile:)
1172                local check = path .. "/" .. askedname
1173             -- we pass 'true' as it can be an url as well, as the type
1174             -- is given we don't waste much time
1175                local foundname, quitscanning, forcedformat = figures.exists(check,askedformat,resolve_too)
1176                if foundname then
1177                    return register(check, {
1178                        askedname  = askedname,
1179                        fullname   = foundname, -- check,
1180                        format     = askedformat,
1181                        cache      = askedcache,
1182                        conversion = askedconversion,
1183                        resolution = askedresolution,
1184                        crop       = askedcrop,
1185                        arguments  = askedarguments,
1186                    })
1187                end
1188            end
1189            if figures.defaultsearch then
1190                local check = resolvers.findfile(askedname)
1191                if check and check ~= "" then
1192                    return register(askedname, {
1193                        askedname  = askedname,
1194                        fullname   = check,
1195                        format     = askedformat,
1196                        cache      = askedcache,
1197                        conversion = askedconversion,
1198                        resolution = askedresolution,
1199                        crop       = askedcrop,
1200                        arguments  = askedarguments,
1201                    })
1202                end
1203            end
1204        end
1205    elseif askedpath then
1206        if trace_figures then
1207            report_inclusion("using rootbased path")
1208        end
1209        for i=1,#figures_order do
1210            local format = figures_order[i]
1211            local list = figures_formats[format].list or { format }
1212            for j=1,#list do
1213                local suffix = list[j]
1214                local check = file.addsuffix(askedname,suffix)
1215                local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too)
1216                if foundname then
1217                    return register(askedname, {
1218                        askedname  = askedname,
1219                        fullname   = foundname, -- check,
1220                        format     = forcedformat or format,
1221                        cache      = askedcache,
1222                        conversion = askedconversion,
1223                        resolution = askedresolution,
1224                        crop       = askedcrop,
1225                        arguments  = askedarguments,
1226                    })
1227                end
1228            end
1229        end
1230    else
1231        if figures.preferquality then
1232            if trace_figures then
1233                report_inclusion("unknown format, quality preferred")
1234            end
1235            for j=1,#figures_order do
1236                local format = figures_order[j]
1237                local list = figures_formats[format].list or { format }
1238                for k=1,#list do
1239                    local suffix = list[k]
1240                 -- local name = file.replacesuffix(askedbase,suffix)
1241                    local name = file.replacesuffix(askedname,suffix)
1242                    for i=1,#figure_paths do
1243                        local path = resolveprefix(figure_paths[i]) -- we resolve (e.g. jobfile:)
1244                        local check = path .. "/" .. name
1245                        local isfile = internalschemes[urlhashed(check).scheme]
1246                        if not isfile then
1247                            if trace_figures then
1248                                report_inclusion("warning: skipping path %a",path)
1249                            end
1250                        else
1251                            local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too) -- true)
1252                            if foundname then
1253                                return register(askedname, {
1254                                    askedname  = askedname,
1255                                    fullname   = foundname, -- check
1256                                    format     = forcedformat or format,
1257                                    cache      = askedcache,
1258                                    conversion = askedconversion,
1259                                    resolution = askedresolution,
1260                                    crop       = askedcrop,
1261                                    arguments  = askedarguments
1262                                })
1263                            end
1264                        end
1265                    end
1266                end
1267            end
1268        else -- 'location'
1269            if trace_figures then
1270                report_inclusion("unknown format, using path strategy")
1271            end
1272            for i=1,#figure_paths do
1273                local path = resolveprefix(figure_paths[i]) -- we resolve (e.g. jobfile:)
1274                for j=1,#figures_order do
1275                    local format = figures_order[j]
1276                    local list = figures_formats[format].list or { format }
1277                    for k=1,#list do
1278                        local suffix = list[k]
1279                        local check = path .. "/" .. file.replacesuffix(askedbase,suffix)
1280                        local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too)
1281                        if foundname then
1282                            return register(askedname, {
1283                                askedname  = askedname,
1284                                fullname   = foudname, -- check,
1285                                format     = forcedformat or format,
1286                                cache      = askedcache,
1287                                conversion = askedconversion,
1288                                resolution = askedresolution,
1289                                crop       = askedcrop,
1290                                arguments  = askedarguments,
1291                            })
1292                        end
1293                    end
1294                end
1295            end
1296        end
1297        if figures.defaultsearch then
1298            if trace_figures then
1299                report_inclusion("using default tex path")
1300            end
1301            for j=1,#figures_order do
1302                local format = figures_order[j]
1303                local list = figures_formats[format].list or { format }
1304                for k=1,#list do
1305                    local suffix = list[k]
1306                    local check = resolvers.findfile(file.replacesuffix(askedname,suffix))
1307                    if check and check ~= "" then
1308                        return register(askedname, {
1309                            askedname  = askedname,
1310                            fullname   = check,
1311                            format     = format,
1312                            cache      = askedcache,
1313                            conversion = askedconversion,
1314                            resolution = askedresolution,
1315                            crop       = askedcrop,
1316                            arguments  = askedarguments,
1317                        })
1318                    end
1319                end
1320            end
1321        end
1322    end
1323    return register(askedname, { -- these two are needed for hashing 'found'
1324        conversion = askedconversion,
1325        resolution = askedresolution,
1326        crop       = askedcrop,
1327        arguments  = askedarguments,
1328    })
1329end
1330
1331-- -- -- plugins -- -- --
1332
1333function identifiers.default(data)
1334    local dr, du, ds = data.request, data.used, data.status
1335    local l = locate(dr)
1336    local foundname = l.foundname
1337    local fullname  = l.fullname or foundname
1338    if fullname then
1339        du.format   = l.format or false
1340        du.fullname = fullname -- can be cached
1341        ds.fullname = foundname -- original
1342        ds.format   = l.format
1343        ds.status   = (l.bugged and 0) or (l.found and 10) or 0
1344    end
1345    return data
1346end
1347
1348function figures.identify(data)
1349    data = data or callstack[#callstack] or lastfiguredata
1350    if data then
1351        local list = identifiers.list -- defined at the end
1352        for i=1,#list do
1353            local identifier = list[i]
1354            local data = identifier(data)
1355            if data and data.status and (not data.status and data.status.status > 0) then
1356                break
1357            end
1358        end
1359    end
1360    return data
1361end
1362
1363function figures.exists(askedname,format,resolve)
1364    return (existers[format] or existers.generic)(askedname,resolve)
1365end
1366
1367function figures.check(data)
1368    data = data or callstack[#callstack] or lastfiguredata
1369    return (checkers[data.status.format] or checkers.generic)(data)
1370end
1371
1372local used_images = { }
1373
1374statistics.register("used graphics",function()
1375    if trace_usage then
1376        local filename = file.nameonly(environment.jobname) .. "-figures-usage.lua"
1377        if next(figures_found) then
1378            local found = { }
1379            for _, data in sortedhash(figures_found) do
1380                found[#found+1] = data
1381                for k, v in next, data do
1382                    if v == false or v == "" then
1383                        data[k] = nil
1384                    end
1385                end
1386            end
1387            for i=1,#used_images do
1388                local u = used_images[i]
1389                local s = u.status
1390                if s then
1391                    s.status = nil -- doesn't say much here
1392                    if s.error then
1393                        u.used = { } -- better show that it's not used
1394                    end
1395                end
1396                for _, t in next, u do
1397                    for k, v in next, t do
1398                        if v == false or v == "" or k == "private" then
1399                            t[k] = nil
1400                        end
1401                    end
1402                end
1403            end
1404            table.save(filename,{
1405                found = found,
1406                used  = used_images,
1407            } )
1408            return format("log saved in '%s'",filename)
1409        else
1410            os.remove(filename)
1411        end
1412    end
1413end)
1414
1415function figures.include(data)
1416    data = data or callstack[#callstack] or lastfiguredata
1417    if trace_usage then
1418        used_images[#used_images+1] = data
1419    end
1420    return (includers[data.status.format] or includers.generic)(data)
1421end
1422
1423function figures.scale(data) -- will become lua code
1424    data = data or callstack[#callstack] or lastfiguredata
1425    ctx_doscalefigure()
1426    return data
1427end
1428
1429function figures.done(data)
1430    figures.nofprocessed = figures.nofprocessed + 1
1431    data = data or callstack[#callstack] or lastfiguredata
1432    local dr, du, ds, nr = data.request, data.used, data.status, figures.boxnumber
1433    local box = texgetbox(nr)
1434    ds.width  = box.width
1435    ds.height = box.height
1436    -- somehow this fails on some of tacos files
1437 -- ds.xscale = ds.width /(du.width  or 1)
1438 -- ds.yscale = ds.height/(du.height or 1)
1439    -- du.width and du.height can be false
1440    if du.width and du.height and du.width > 0 and du.height > 0 then
1441        ds.xscale = ds.width /du.width
1442        ds.yscale = ds.height/du.height
1443    elseif du.xsize and du.ysize and du.xsize > 0 and du.ysize > 0 then
1444        ds.xscale = ds.width /du.xsize
1445        ds.yscale = ds.height/du.ysize
1446    else
1447        ds.xscale = 1
1448        ds.yscale = 1
1449    end
1450    -- sort of redundant but can be limited
1451    ds.page = ds.page or du.page or dr.page
1452    return data
1453end
1454
1455function figures.dummy(data)
1456    data = data or callstack[#callstack] or lastfiguredata
1457    local dr, du, nr = data.request, data.used, figures.boxnumber
1458    local box  = hpack(new_hlist()) -- we need to set the dir (luatex 0.60 buglet)
1459    du.width   = du.width  or figures.defaultwidth
1460    du.height  = du.height or figures.defaultheight
1461    du.depth   = du.depth  or figures.defaultdepth
1462    box.width  = du.width
1463    box.height = du.height
1464    box.depth  = du.depth
1465    texsetbox(nr,box) -- hm, should be global (to be checked for consistency)
1466end
1467
1468-- -- -- generic -- -- --
1469
1470function existers.generic(askedname,resolve)
1471    -- not findbinfile
1472    local result
1473    if hasscheme(askedname) then
1474        result = resolvers.findbinfile(askedname)
1475    elseif isfile(askedname) then
1476        result = askedname
1477    elseif resolve then
1478        result = resolvers.findbinfile(askedname)
1479    end
1480    if not result or result == "" then
1481        result = false
1482    end
1483    if trace_figures then
1484        if result then
1485            report_inclusion("%a resolved to %a",askedname,result)
1486        else
1487            report_inclusion("%a cannot be resolved",askedname)
1488        end
1489    end
1490    return result
1491end
1492
1493-- pdf : 0-3: 0 90 180 270
1494-- jpeg: 0 unset 1-4: 0 90 180 270 5-8: flipped r/c
1495
1496local transforms = setmetatableindex (
1497    {
1498        ["orientation-1"] = 0, ["R0"]     = 0,
1499        ["orientation-2"] = 4, ["R0MH"]   = 4,
1500        ["orientation-3"] = 2, ["R180"]   = 2,
1501        ["orientation-4"] = 6, ["R0MV"]   = 6,
1502        ["orientation-5"] = 5, ["R270MH"] = 5,
1503        ["orientation-6"] = 3, ["R90"]    = 3,
1504        ["orientation-7"] = 7, ["R90MH"]  = 7,
1505        ["orientation-8"] = 1, ["R270"]   = 1,
1506    },
1507    function(t,k) -- transforms are 0 .. 7
1508        local v = tonumber(k) or 0
1509        if v < 0 or v > 7 then
1510            v = 0
1511        end
1512        t[k] = v
1513        return v
1514    end
1515)
1516
1517local function checktransform(figure,forced)
1518    if auto_transform then
1519
1520        local orientation = (forced ~= "" and forced ~= v_auto and forced) or figure.orientation or 0
1521        local transform   = transforms["orientation-"..orientation]
1522        figure.transform = transform
1523        if odd(transform) then
1524            return figure.height, figure.width
1525        else
1526            return figure.width, figure.height
1527        end
1528    end
1529end
1530
1531local pagecount = { }
1532
1533function checkers.generic(data) -- called by checkers.pdf/png/jpg
1534    local dr, du, ds    = data.request, data.used, data.status
1535    local name          = du.fullname or "unknown generic"
1536    local page          = du.page or dr.page
1537    local size          = dr.size or "crop"
1538    local color         = dr.color or "natural"
1539    local mask          = dr.mask or "none"
1540    local range         = dr.range or "none"
1541    local crop          = dr.crop or "none"
1542    local conversion    = dr.conversion
1543    local resolution    = dr.resolution
1544    local arguments     = dr.arguments
1545    local scanimage     = dr.scanimage or scanimage
1546    local userpassword  = dr.userpassword
1547    local ownerpassword = dr.ownerpassword
1548    if not conversion or conversion == "" then
1549        conversion = "default"
1550    end
1551    if not resolution or resolution == "" then
1552        resolution = "default"
1553    end
1554    if not arguments or arguments == "" then
1555        arguments = "default"
1556    end
1557    local hash = f_hash_full(
1558        name,
1559        page,
1560        size,
1561        color,
1562        mask,
1563        range,
1564        crop,
1565        conversion,
1566        resolution,
1567        arguments
1568    )
1569    --
1570    local figure = figures_loaded[hash]
1571    if figure == nil then
1572        figure = createimage {
1573            filename        = name,
1574            page            = page,
1575            pagebox         = dr.size,
1576            keepopen        = dr.keepopen or false,
1577            userpassword    = userpassword,
1578            ownerpassword   = ownerpassword,
1579         -- visiblefilename = "", -- this prohibits the full filename ending up in the file
1580        }
1581        codeinjections.setfigurecolorspace(data,figure)
1582        codeinjections.setfiguremask(data,figure)
1583        if figure then
1584            -- new, bonus check (a bogus check in lmtx)
1585            if page and page > 1 then
1586                local f = scanimage {
1587                    filename      = name,
1588                    userpassword  = userpassword,
1589                    ownerpassword = ownerpassword,
1590                }
1591                if f and f.page and f.pages < page then
1592                    report_inclusion("no page %i in %a, using page 1",page,name)
1593                    page        = 1
1594                    figure.page = page
1595                end
1596            end
1597            -- till here
1598            local f, comment = checkimage(scanimage(figure))
1599            if not f then
1600                ds.comment = comment
1601                ds.found   = false
1602                ds.error   = true
1603            end
1604            if figure.attr and not f.attr then
1605                -- tricky as img doesn't allow it
1606                f.attr = figure.attr
1607            end
1608            if figure.newmask then
1609                f.newmask = figure.newmask
1610            end
1611            if figure.newranges then
1612                f.newranges = figure.newranges
1613            end
1614            if dr.cmyk == v_yes then
1615                f.enforcecmyk = true
1616            elseif dr.cmyk == v_auto and attributes.colors.model == "cmyk" then
1617                f.enforcecmyk = true
1618            end
1619            figure = f
1620        end
1621        local f, d = codeinjections.setfigurealternative(data,figure)
1622        figure = f or figure
1623        data   = d or data
1624        figures_loaded[hash] = figure
1625        if trace_conversion then
1626            report_inclusion("new graphic, using hash %a",hash)
1627        end
1628    else
1629        if trace_conversion then
1630            report_inclusion("existing graphic, using hash %a",hash)
1631        end
1632    end
1633    if figure then
1634        local width, height = checktransform(figure,dr.transform)
1635        --
1636        du.width       = width
1637        du.height      = height
1638        du.pages       = figure.pages
1639        du.depth       = figure.depth or 0
1640        du.colordepth  = figure.colordepth or 0
1641        du.xresolution = figure.xres or 0
1642        du.yresolution = figure.yres or 0
1643        du.xsize       = figure.xsize or 0
1644        du.ysize       = figure.ysize or 0
1645        du.rotation    = figure.rotation or 0    -- in pdf multiples or 90% in jpeg 1
1646        du.orientation = figure.orientation or 0 -- jpeg 1 2 3 4 (0=unset)
1647        ds.private     = figure
1648        ds.hash        = hash
1649    end
1650    return data
1651end
1652
1653local nofimages = 0
1654local pofimages = { }
1655
1656function figures.getrealpage(index)
1657    return pofimages[index] or 0
1658end
1659
1660local function updatepage(specification)
1661    local n = specification.n
1662    pofimages[n] = pofimages[n] or tex.count.realpageno -- so when reused we register the first one only
1663end
1664
1665function includers.generic(data)
1666    local dr, du, ds = data.request, data.used, data.status
1667    -- here we set the 'natural dimensions'
1668    dr.width     = du.width
1669    dr.height    = du.height
1670    local hash   = figures.hash(data)
1671    local figure = figures_used[hash]
1672 -- figures.registerresource {
1673 --     filename = du.fullname,
1674 --     width    = dr.width,
1675 --     height   = dr.height,
1676 -- }
1677    if figure == nil then
1678        figure = ds.private -- the img object
1679        if figure then
1680            figure = (dr.copyimage or copyimage)(figure)
1681            if figure then
1682                figure.width  = dr.width  or figure.width
1683                figure.height = dr.height or figure.height
1684            end
1685        end
1686        figures_used[hash] = figure
1687    end
1688    if figure then
1689        local nr     = figures.boxnumber
1690        nofimages    = nofimages + 1
1691        ds.pageindex = nofimages
1692        local image  = wrapimage(figure)
1693        local pager  = new_latelua { action = updatepage, n = nofimages }
1694        image.next   = pager
1695        pager.prev   = image
1696        local box    = hpack(image)
1697        box.width    = figure.width
1698        box.height   = figure.height
1699        box.depth    = 0
1700        texsetbox(nr,box)
1701        ds.objectnumber = figure.objnum
1702     -- indexed[figure.index] = figure
1703        ctx_relocateexternalfigure()
1704    end
1705    return data
1706end
1707
1708-- -- -- nongeneric -- -- --
1709
1710local function checkers_nongeneric(data,command) -- todo: macros and context.*
1711    local dr, du, ds = data.request, data.used, data.status
1712    local name = du.fullname or "unknown nongeneric"
1713    local hash = name
1714    if dr.object then
1715        -- hm, bugged ... waiting for an xform interface
1716        if not objects.data["FIG"][hash] then
1717            if type(command) == "function" then
1718                command()
1719            end
1720            ctx_dosetfigureobject("FIG",hash)
1721        end
1722        ctx_doboxfigureobject("FIG",hash)
1723    elseif type(command) == "function" then
1724        command()
1725    end
1726    return data
1727end
1728
1729local function includers_nongeneric(data)
1730    return data
1731end
1732
1733checkers.nongeneric  = checkers_nongeneric
1734includers.nongeneric = includers_nongeneric
1735
1736-- -- -- mov -- -- --
1737
1738-- function checkers.mov(data)
1739--     local dr, du, ds = data.request, data.used, data.status
1740--     local width = todimen(dr.width or figures.defaultwidth)
1741--     local height = todimen(dr.height or figures.defaultheight)
1742--     local foundname = du.fullname
1743--     dr.width, dr.height = width, height
1744--     du.width, du.height, du.foundname = width, height, foundname
1745--     if trace_inclusion then
1746--         report_inclusion("including movie %a, width %p, height %p",foundname,width,height)
1747--     end
1748--     -- we need to push the node.write in between ... we could make a shared helper for this
1749--     ctx_startfoundexternalfigure(width .. "sp",height .. "sp")
1750--     context(function()
1751--         nodeinjections.insertmovie {
1752--             width      = width,
1753--             height     = height,
1754--             factor     = bpfactor,
1755--             ["repeat"] = dr["repeat"],
1756--             controls   = dr.controls,
1757--             preview    = dr.preview,
1758--             label      = dr.label,
1759--             foundname  = foundname,
1760--         }
1761--     end)
1762--     ctx_stopfoundexternalfigure()
1763--     return data
1764-- end
1765--
1766-- includers.mov = includers.nongeneric
1767
1768-- -- -- mps -- -- --
1769
1770internalschemes.mprun = true
1771
1772-- mprun.foo.1 mprun.6 mprun:foo.2
1773
1774local ctx_docheckfiguremprun = context.docheckfiguremprun
1775local ctx_docheckfiguremps   = context.docheckfiguremps
1776
1777local function internal(askedname)
1778    local spec, mprun, mpnum = match(lower(askedname),"mprun([:%.]?)(.-)%.(%d+)")
1779 -- mpnum = tonumber(mpnum) or 0 -- can be string or number, fed to context anyway
1780    if spec ~= "" then
1781        return mprun, mpnum
1782    else
1783        return "", mpnum
1784    end
1785end
1786
1787function existers.mps(askedname)
1788    local mprun, mpnum = internal(askedname)
1789    if mpnum then
1790        return askedname, true, "mps", true
1791    else
1792        return existers.generic(askedname)
1793    end
1794end
1795
1796function checkers.mps(data)
1797    local mprun, mpnum = internal(data.used.fullname)
1798    if mpnum then
1799        return checkers_nongeneric(data,function() ctx_docheckfiguremprun(mprun,mpnum) end)
1800    else
1801        return checkers_nongeneric(data,function() ctx_docheckfiguremps(data.used.fullname) end)
1802    end
1803end
1804
1805includers.mps = includers.nongeneric
1806
1807-- -- -- tex -- -- --
1808
1809local ctx_docheckfiguretex = context.docheckfiguretex
1810
1811function existers.tex(askedname)
1812    askedname = resolvers.findfile(askedname)
1813    return askedname ~= "" and askedname or false, true, "tex", true
1814end
1815
1816function checkers.tex(data)
1817    return checkers_nongeneric(data,function() ctx_docheckfiguretex(data.used.fullname) end)
1818end
1819
1820includers.tex = includers.nongeneric
1821
1822-- -- -- buffer -- -- --
1823
1824local ctx_docheckfigurebuffer = context.docheckfigurebuffer
1825
1826function existers.buffer(askedname)
1827    local name = file.nameonly(askedname)
1828    local okay = buffers.exists(name)
1829    return okay and name, true, "buffer", true -- always quit scanning
1830end
1831
1832function checkers.buffer(data)
1833    return checkers_nongeneric(data,function() ctx_docheckfigurebuffer(file.nameonly(data.used.fullname)) end)
1834end
1835
1836includers.buffers = includers.nongeneric
1837
1838-- -- -- auto -- -- --
1839
1840function existers.auto(askedname)
1841    local name = gsub(askedname, ".auto$", "")
1842    local format = figures.guess(name)
1843 -- if format then
1844 --     report_inclusion("format guess %a for %a",format,name)
1845 -- else
1846 --     report_inclusion("format guess for %a is not possible",name)
1847 -- end
1848    return format and name, true, format
1849end
1850
1851checkers.auto  = checkers.generic
1852includers.auto = includers.generic
1853
1854-- -- -- cld -- -- --
1855
1856local ctx_docheckfigurecld = context.docheckfigurecld
1857
1858function existers.cld(askedname)
1859    askedname = resolvers.findfile(askedname)
1860    return askedname ~= "" and askedname or false, true, "cld", true
1861end
1862
1863function checkers.cld(data)
1864    return checkers_nongeneric(data,function() ctx_docheckfigurecld(data.used.fullname) end)
1865end
1866
1867includers.cld = includers.nongeneric
1868
1869-- -- -- converters -- -- --
1870
1871setmetatableindex(converters,"table")
1872
1873-- We keep this helper because it has been around for a while and therefore it can
1874-- be a depedency in an existing workflow.
1875
1876function programs.makeoptions(options)
1877    local to = type(options)
1878    return (to == "table" and concat(options," ")) or (to == "string" and options) or ""
1879end
1880
1881function programs.run(binary,argument,variables)
1882    local found = nil
1883    if type(binary) == "table" then
1884        for i=1,#binary do
1885            local b = binary[i]
1886            found = os.which(b)
1887            if found then
1888                binary = b
1889                break
1890            end
1891        end
1892        if not found then
1893            binary = concat(binary, " | ")
1894        end
1895    elseif binary then
1896        found = os.which(match(binary,"[%S]+"))
1897    end
1898    if type(argument) == "table" then
1899        argument = concat(argument," ") -- for old times sake
1900    end
1901    if not found then
1902        report_inclusion("program %a is not installed",binary or "?")
1903    elseif not argument or argument == "" then
1904        report_inclusion("nothing to run, no arguments for program %a",binary)
1905    else
1906        -- no need to use the full found filename (found) .. we also don't quote the program
1907        -- name any longer as in luatex there is too much messing with these names
1908        local command = format([[%s %s]],binary,replacetemplate(longtostring(argument),variables))
1909        if trace_conversion or trace_programs then
1910            report_inclusion("running command: %s",command)
1911        end
1912        os.execute(command)
1913    end
1914end
1915
1916-- the rest of the code has been moved to grph-con.lua
1917
1918-- -- -- bases -- -- --
1919
1920local bases         = allocate()
1921figures.bases       = bases
1922
1923local bases_list    = nil -- index      => { basename, fullname, xmlroot }
1924local bases_used    = nil -- [basename] => { basename, fullname, xmlroot } -- pointer to list
1925local bases_found   = nil
1926local bases_enabled = false
1927
1928local function reset()
1929    bases_list    = allocate()
1930    bases_used    = allocate()
1931    bases_found   = allocate()
1932    bases_enabled = false
1933    bases.list    = bases_list
1934    bases.used    = bases_used
1935    bases.found   = bases_found
1936end
1937
1938reset()
1939
1940function bases.use(basename)
1941    if basename == "reset" then
1942        reset()
1943    else
1944        basename = file.addsuffix(basename,"xml")
1945        if not bases_used[basename] then
1946            local t = { basename, nil, nil }
1947            bases_used[basename] = t
1948            bases_list[#bases_list+1] = t
1949            if not bases_enabled then
1950                bases_enabled = true
1951                xml.registerns("rlx","http://www.pragma-ade.com/schemas/rlx") -- we should be able to do this per xml file
1952            end
1953            if trace_bases then
1954                report_inclusion("registering base %a",basename)
1955            end
1956        end
1957    end
1958end
1959
1960implement { name = "usefigurebase", actions = bases.use, arguments = "string" }
1961
1962local function bases_find(basename,askedlabel)
1963    if trace_bases then
1964        report_inclusion("checking for %a in base %a",askedlabel,basename)
1965    end
1966    basename = file.addsuffix(basename,"xml")
1967    local t = bases_found[askedlabel]
1968    if t == nil then
1969        local base = bases_used[basename]
1970        local page = 0
1971        if base[2] == nil then
1972            -- no yet located
1973            for i=1,#figure_paths do
1974                local path = resolveprefix(figure_paths[i]) -- we resolve (e.g. jobfile:)
1975                local xmlfile = path .. "/" .. basename
1976                if io.exists(xmlfile) then
1977                    base[2] = xmlfile
1978                    base[3] = xml.load(xmlfile)
1979                    if trace_bases then
1980                        report_inclusion("base %a loaded",xmlfile)
1981                    end
1982                    break
1983                end
1984            end
1985        end
1986        t = false
1987        if base[2] and base[3] then -- rlx:library
1988            for e in xml.collected(base[3],"/(*:library|figurelibrary)/*:figure/*:label") do
1989                page = page + 1
1990                if xml.text(e) == askedlabel then
1991                    t = {
1992                        base   = file.replacesuffix(base[2],"pdf"),
1993                        format = "pdf",
1994                        name   = xml.text(e,"../*:file"), -- to be checked
1995                        page   = page,
1996                    }
1997                    bases_found[askedlabel] = t
1998                    if trace_bases then
1999                        report_inclusion("figure %a found in base %a",askedlabel,base[2])
2000                    end
2001                    return t
2002                end
2003            end
2004            if trace_bases and not t then
2005                report_inclusion("figure %a not found in base %a",askedlabel,base[2])
2006            end
2007        end
2008    end
2009    return t
2010end
2011
2012-- we can access sequential or by name
2013
2014local function bases_locate(askedlabel)
2015    for i=1,#bases_list do
2016        local entry = bases_list[i]
2017        local t = bases_find(entry[1],askedlabel,1,true)
2018        if t then
2019            return t
2020        end
2021    end
2022    return false
2023end
2024
2025function identifiers.base(data)
2026    if bases_enabled then
2027        local dr, du, ds = data.request, data.used, data.status
2028        local fbl = bases_locate(dr.name or dr.label)
2029        if fbl then
2030            du.page     = fbl.page
2031            du.format   = fbl.format
2032            du.fullname = fbl.base
2033            ds.fullname = fbl.name
2034            ds.format   = fbl.format
2035            ds.page     = fbl.page
2036            ds.status   = 10
2037        end
2038    end
2039    return data
2040end
2041
2042bases.locate = bases_locate
2043bases.find   = bases_find
2044
2045identifiers.list = {
2046    identifiers.base,
2047    identifiers.default
2048}
2049
2050-- tracing
2051
2052statistics.register("graphics processing time", function()
2053    local nofprocessed = figures.nofprocessed
2054    if nofprocessed > 0 then
2055        local nofnames, nofbadnames = 0, 0
2056        for hash, data in next, figures_found do
2057            nofnames = nofnames + 1
2058            if data.badname then
2059                nofbadnames = nofbadnames + 1
2060            end
2061        end
2062        return format("%s seconds including tex, %s processed images, %s unique asked, %s bad names",
2063            statistics.elapsedtime(figures),nofprocessed,nofnames,nofbadnames)
2064    else
2065        return nil
2066    end
2067end)
2068
2069-- helper
2070
2071function figures.applyratio(width,height,w,h) -- width and height are strings and w and h are numbers
2072    if not width or width == "" then
2073        if not height or height == "" then
2074            return figures.defaultwidth, figures.defaultheight
2075        else
2076            height = todimen(height)
2077            if w and h then
2078                return height * w/h, height
2079            else
2080                return figures.defaultwidth, height
2081            end
2082        end
2083    else
2084        width = todimen(width)
2085        if not height or height == "" then
2086            if w and h then
2087                return width, width * h/w
2088            else
2089                return width, figures.defaultheight
2090            end
2091        else
2092            return width, todimen(height)
2093        end
2094    end
2095end
2096
2097-- example of simple plugins:
2098--
2099-- figures.converters.png = {
2100--     png = function(oldname,newname,resolution)
2101--         local command = string.format('gm convert -depth 1 "%s" "%s"',oldname,newname)
2102--         logs.report(string.format("running command %s",command))
2103--         os.execute(command)
2104--     end,
2105-- }
2106
2107-- local n = "foo.pdf"
2108-- local d = figures.getinfo(n)
2109-- if d then
2110--     for i=1,d.used.pages do
2111--         local p = figures.getinfo(n,i)
2112--         if p then
2113--             local u = p.used
2114--             print(u.width,u.height,u.orientation)
2115--         end
2116--     end
2117-- end
2118
2119function figures.getinfo(name,page)
2120    if type(name) == "string" then
2121        name = { name = name, page = page }
2122    end
2123    if name.name then
2124        local data = figures.push(name)
2125        data = figures.identify(data)
2126        if data.status and data.status.status > 0 then
2127            data = figures.check(data)
2128        end
2129        figures.pop()
2130        return data
2131    end
2132end
2133
2134function figures.getpdfinfo(name,page,metadata)
2135    -- not that useful but as we have it for detailed inclusion we can as
2136    -- we expose it
2137    if type(name) ~= "table" then
2138        name = { name = name, page = page, metadata = metadata }
2139    end
2140    return codeinjections.getinfo(name)
2141end
2142
2143-- interfacing
2144
2145implement {
2146    name      = "figure_push",
2147    scope     = "private",
2148    actions   = figures.push,
2149    arguments = {
2150        {
2151            { "name" },
2152            { "label" },
2153            { "page" },
2154            { "file" },
2155            { "size" },
2156            { "object" },
2157            { "prefix" },
2158            { "cache" },
2159            { "format" },
2160            { "preset" },
2161            { "controls" },
2162            { "resources" },
2163            { "preview" },
2164            { "display" },
2165            { "mask" },
2166            { "range" },
2167            { "crop" },
2168            { "conversion" },
2169            { "resolution" },
2170            { "color" },
2171            { "cmyk" },
2172            { "arguments" },
2173            { "repeat" },
2174            { "transform" },
2175            { "compact" },
2176            { "metadata" },
2177            { "width", "dimen" },
2178            { "height", "dimen" },
2179            { "userpassword" },
2180            { "ownerpassword" },
2181        }
2182    }
2183}
2184
2185-- beware, we get a number passed by default
2186
2187implement { name = "figure_pop",      scope = "private", actions = figures.pop }
2188implement { name = "figure_done",     scope = "private", actions = figures.done }
2189implement { name = "figure_dummy",    scope = "private", actions = figures.dummy }
2190implement { name = "figure_identify", scope = "private", actions = figures.identify }
2191implement { name = "figure_scale",    scope = "private", actions = figures.scale }
2192implement { name = "figure_check",    scope = "private", actions = figures.check }
2193implement { name = "figure_include",  scope = "private", actions = figures.include }
2194
2195implement {
2196    name      = "setfigurelookuporder",
2197    actions   = figures.setorder,
2198    arguments = "string"
2199}
2200
2201implement {
2202    name      = "figure_reset",
2203    scope     = "private",
2204    arguments = { "integer", "dimen", "dimen" },
2205    actions   = function(box,width,height)
2206        figures.boxnumber     = box
2207        figures.defaultwidth  = width
2208        figures.defaultheight = height
2209    end
2210}
2211
2212-- require("util-lib-imp-gm")
2213--
2214-- figures.converters.tif.pdf = function(oldname,newname,resolution)
2215--     logs.report("graphics","using gm library to convert %a",oldname)
2216--     utilities.graphicmagick.convert {
2217--         inputname  = oldname,
2218--         outputname = newname,
2219--     }
2220-- end
2221--
2222-- \externalfigure[t:/sources/hakker1b.tiff]
2223
2224-- something relatively new:
2225
2226local registered = { }
2227
2228local ctx_doexternalfigurerepeat = context.doexternalfigurerepeat
2229
2230implement {
2231    name      = "figure_register_page",
2232    arguments = "3 strings",
2233    actions   = function(a,b,c)
2234        registered[#registered+1] = { a, b, c }
2235        context(#registered)
2236    end
2237}
2238
2239implement {
2240    name    = "figure_nof_registered_pages",
2241    actions = function()
2242        context(#registered)
2243    end
2244}
2245
2246implement {
2247    name      = "figure_flush_registered_pages",
2248    arguments = "string",
2249    actions   = function(n)
2250        local f = registered[tonumber(n)]
2251        if f then
2252            ctx_doexternalfigurerepeat(f[1],f[2],f[3],n)
2253        end
2254    end
2255}
2256
2257implement {
2258    name      = "setfigureconversion",
2259    arguments = "2 strings",
2260    actions   = figures.setconversion
2261}
2262
2263-- for the moment we keep this here:
2264
2265do
2266
2267    local stores       = setmetatableindex("table")
2268    local defaultwidth = 6553600 -- maybe bp
2269
2270    function images.resetstore(name)
2271        stores[name] = { }
2272    end
2273
2274    local function resetdata(name,n)
2275        local store = stores[name]
2276        store[n] = false
2277    end
2278
2279    local function storedata(name,data)
2280        local store = stores[name]
2281        store[#store+1] = data
2282        return #store
2283    end
2284
2285    local function fetchdata(name,n)
2286        local store = stores[name]
2287        return store and store[n]
2288    end
2289
2290    images.storedata = storedata
2291    images.fetchdata = fetchdata
2292    images.resetdata = resetdata
2293
2294 -- function images.filetostore(name,kind,filename)
2295 --     return storedata(name, {
2296 --         data = io.loaddata(filename),
2297 --         kind = kind or file.suffix(filename),
2298 --     })
2299 -- end
2300 --
2301 -- function images.datatostore(name,kind,data)
2302 --     return storedata(name, {
2303 --         data = data,
2304 --         kind = kind,
2305 --     })
2306 -- end
2307
2308 -- local embedimage = images.embed
2309 -- local wrapimage  = images.wrap
2310
2311    function images.embedfromstore(name,n,reset)
2312        local blob = fetchdata(name,n)
2313        if blob then
2314            local kind = blob.kind
2315            if kind then
2316                local identify = graphics.identifiers[kind]
2317                local inject   = backends.codeinjections[kind]
2318                if identify and inject then
2319                    local info = blob.info or identify(blob.data,"string") -- could already be done
2320                    if info then
2321                        info.width  = defaultwidth
2322                        info.height = (info.ysize /info.xsize) * defaultwidth
2323                        local image = blob.image
2324                        if not image then
2325                            image = inject(info,"string")
2326                            blob.image = image
2327                        end
2328                        image.width  = info.width
2329                        image.height = info.height
2330                        context(wrapimage(image))
2331                     -- if reset then
2332                     --     resetdata(name,n)
2333                     -- end
2334                    end
2335                end
2336            end
2337        end
2338    end
2339
2340    implement {
2341        name      = "embedimagefromstore",
2342        arguments = { "string", "integer", "boolean" },
2343        actions   = images.embedfromstore,
2344    }
2345
2346end
2347
2348local function register(name,specification,setter)
2349    -- could be a helper
2350    specification = loadstring("return " .. specification)
2351    if type(specification) == "function" then
2352        specification = specification()
2353        if specification then
2354            setter(name,specification)
2355        end
2356    end
2357end
2358
2359implement {
2360    name      = "registerfiguremask",
2361    public    = true,
2362    protected = true,
2363    arguments = { "optional", "string" }, -- "lua"
2364    actions   = function(name,specification)
2365        register(name,specification,codeinjections.registerfiguremask)
2366    end
2367}
2368
2369implement {
2370    name      = "registerfigurerange",
2371    public    = true,
2372    protected = true,
2373    arguments = { "optional", "string" }, -- "lua"
2374    actions   = function(name,specification)
2375        register(name,specification,codeinjections.registerfigurerange)
2376    end
2377}
2378