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