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