diff options
Diffstat (limited to 'tex/context/base/grph-inc.lua')
-rw-r--r-- | tex/context/base/grph-inc.lua | 3218 |
1 files changed, 1609 insertions, 1609 deletions
diff --git a/tex/context/base/grph-inc.lua b/tex/context/base/grph-inc.lua index 9603419ae..ae4d5642d 100644 --- a/tex/context/base/grph-inc.lua +++ b/tex/context/base/grph-inc.lua @@ -1,1609 +1,1609 @@ -if not modules then modules = { } end modules ['grph-inc'] = { - version = 1.001, - comment = "companion to grph-inc.mkiv", - author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", - copyright = "PRAGMA ADE / ConTeXt Development Team", - license = "see context related readme files" -} - --- todo: empty filename or only suffix always false (not found) --- lowercase types --- mps tex tmp svg --- partly qualified --- dimensions --- use metatables --- figures.boxnumber can go as we now can use names --- avoid push --- move some to command namespace - ---[[ -The ConTeXt figure inclusion mechanisms are among the oldest code -in ConTeXt and evolved into a complex whole. One reason is that we -deal with backend in an abstract way. What complicates matters is -that we deal with internal graphics as well: TeX code, MetaPost code, -etc. Later on figure databases were introduced, which resulted in -a plug in model for locating images. On top of that runs a conversion -mechanism (with caching) and resource logging. - -Porting that to Lua is not that trivial because quite some -status information is kept between al these stages. Of course, image -reuse also has some price, and so I decided to implement the graphics -inclusion in several layers: detection, loading, inclusion, etc. - -Object sharing and scaling can happen at each stage, depending on the -way the resource is dealt with. - -The TeX-Lua mix is suboptimal. This has to do with the fact that we cannot -run TeX code from within Lua. Some more functionality will move to Lua. -]]-- - -local format, lower, find, match, gsub, gmatch = string.format, string.lower, string.find, string.match, string.gsub, string.gmatch -local texbox = tex.box -local contains = table.contains -local concat, insert, remove = table.concat, table.insert, table.remove -local todimen = string.todimen -local collapsepath = file.collapsepath -local formatters = string.formatters -local longtostring = string.longtostring -local expandfilename = dir.expandname - -local P, lpegmatch = lpeg.P, lpeg.match - -local settings_to_array = utilities.parsers.settings_to_array -local settings_to_hash = utilities.parsers.settings_to_hash -local allocate = utilities.storage.allocate -local setmetatableindex = table.setmetatableindex -local replacetemplate = utilities.templates.replace - -local variables = interfaces.variables -local codeinjections = backends.codeinjections -local nodeinjections = backends.nodeinjections - -local trace_figures = false trackers.register("graphics.locating", function(v) trace_figures = v end) -local trace_bases = false trackers.register("graphics.bases", function(v) trace_bases = v end) -local trace_programs = false trackers.register("graphics.programs", function(v) trace_programs = v end) -local trace_conversion = false trackers.register("graphics.conversion", function(v) trace_conversion = v end) -local trace_inclusion = false trackers.register("graphics.inclusion", function(v) trace_inclusion = v end) - -local report_inclusion = logs.reporter("graphics","inclusion") - -local context, img = context, img - -local f_hash_part = formatters["%s->%s->%s"] -local f_hash_full = formatters["%s->%s->%s->%s->%s->%s->%s"] - -local v_yes = variables.yes -local v_low = variables.low -local v_medium = variables.medium -local v_high = variables.high -local v_global = variables["global"] -local v_local = variables["local"] -local v_default = variables.default - -local maxdimen = 2^30-1 - -function img.check(figure) - if figure then - local width = figure.width - local height = figure.height - if height > width then - if height > maxdimen then - figure.height = maxdimen - figure.width = width * maxdimen/height - report_inclusion("limiting natural dimensions of %a (%s)",figure.filename,"height") - end - elseif width > maxdimen then - figure.width = maxdimen - figure.height = height * maxdimen/width - report_inclusion("limiting natural dimensions of %a (%s)",figure.filename,"width") - end - return figure - end -end - ---- some extra img functions --- can become luat-img.lua - -local imgkeys = img.keys() - -function img.totable(imgtable) - local result = { } - for k=1,#imgkeys do - local key = imgkeys[k] - result[key] = imgtable[key] - end - return result -end - -function img.serialize(i,...) - return table.serialize(img.totable(i),...) -end - -function img.print(i,...) - return table.print(img.totable(i),...) -end - -function img.clone(i,data) - i.width = data.width or i.width - i.height = data.height or i.height - -- attr etc - return i -end - -local validsizes = table.tohash(img.boxes()) -local validtypes = table.tohash(img.types()) - -function img.checksize(size) - if size then - size = gsub(size,"box","") - return validsizes[size] and size or "crop" - else - return "crop" - end -end - -local indexed = { } - -function img.ofindex(n) - return indexed[n] -end - ---- we can consider an grph-ini file - -figures = figures or { } -local figures = figures - -figures.boxnumber = figures.boxnumber or 0 -figures.defaultsearch = true -figures.defaultwidth = 0 -figures.defaultheight = 0 -figures.defaultdepth = 0 -figures.nofprocessed = 0 -figures.preferquality = true -- quality over location - -local figures_loaded = allocate() figures.loaded = figures_loaded -local figures_used = allocate() figures.used = figures_used -local figures_found = allocate() figures.found = figures_found -local figures_suffixes = allocate() figures.suffixes = figures_suffixes -local figures_patterns = allocate() figures.patterns = figures_patterns -local figures_resources = allocate() figures.resources = figures_resources - -local existers = allocate() figures.existers = existers -local checkers = allocate() figures.checkers = checkers -local includers = allocate() figures.includers = includers -local converters = allocate() figures.converters = converters -local identifiers = allocate() figures.identifiers = identifiers -local programs = allocate() figures.programs = programs - -local defaultformat = "pdf" -local defaultprefix = "m_k_i_v_" - -figures.localpaths = allocate { - ".", "..", "../.." -} - -figures.cachepaths = allocate { - prefix = "", - path = ".", - subpath = ".", -} - -local figure_paths = allocate(table.copy(figures.localpaths)) -figures.paths = figure_paths - -local figures_order = allocate { - "pdf", "mps", "jpg", "png", "jp2", "jbig", "svg", "eps", "tif", "gif", "mov", "buffer", "tex", "cld", "auto", -} - -local figures_formats = allocate { -- magic and order will move here - ["pdf"] = { list = { "pdf" } }, - ["mps"] = { patterns = { "mps", "%d+" } }, - ["jpg"] = { list = { "jpg", "jpeg" } }, - ["png"] = { list = { "png" } }, - ["jp2"] = { list = { "jp2" } }, - ["jbig"] = { list = { "jbig", "jbig2", "jb2" } }, - ["svg"] = { list = { "svg", "svgz" } }, - ["eps"] = { list = { "eps", "ai" } }, - ["gif"] = { list = { "gif" } }, - ["tif"] = { list = { "tif", "tiff" } }, - ["mov"] = { list = { "mov", "flv", "mp4" } }, -- "avi" is not supported - ["buffer"] = { list = { "tmp", "buffer", "buf" } }, - ["tex"] = { list = { "tex" } }, - ["cld"] = { list = { "cld" } }, - ["auto"] = { list = { "auto" } }, -} - -local figures_magics = allocate { - { format = "png", pattern = P("\137PNG\013\010\026\010") }, -- 89 50 4E 47 0D 0A 1A 0A, - { format = "jpg", pattern = P("\255\216\255") }, -- FF D8 FF - { format = "jp2", pattern = P("\000\000\000\012\106\080\032\032\013\010"), }, -- 00 00 00 0C 6A 50 20 20 0D 0A }, - { format = "gif", pattern = P("GIF") }, - { format = "pdf", pattern = (1 - P("%PDF"))^0 * P("%PDF") }, -} - -figures.formats = figures_formats -- frozen -figures.magics = figures_magics -- frozen -figures.order = figures_order -- frozen - --- We can set the order but only indirectly so that we can check for support. - -function figures.setorder(list) -- can be table or string - if type(list) == "string" then - list = settings_to_array(list) - end - if list and #list > 0 then - figures_order = allocate() - figures.order = figures_order - local done = { } -- just to be sure in case the list is generated - for i=1,#list do - local l = lower(list[i]) - if figures_formats[l] and not done[l] then - figures_order[#figures_order+1] = l - done[l] = true - end - end - report_inclusion("lookup order % a",figures_order) - else - -- invalid list - end -end - -function figures.guess(filename) - local f = io.open(filename,'rb') - if f then - local str = f:read(100) - f:close() - if str then - for i=1,#figures_magics do - local pattern = figures_magics[i] - if lpegmatch(pattern.pattern,str) then - local format = pattern.format - if trace_figures then - report_inclusion("file %a has format %a",filename,format) - end - return format - end - end - end - end -end - -local function setlookups() -- tobe redone .. just set locals - figures_suffixes = allocate() - figures_patterns = allocate() - for _, format in next, figures_order do - local data = figures_formats[format] - local list = data.list - if list then - for i=1,#list do - figures_suffixes[list[i]] = format -- hash - end - else - figures_suffixes[format] = format - end - local patterns = data.patterns - if patterns then - for i=1,#patterns do - figures_patterns[#figures_patterns+1] = { patterns[i], format } -- array - end - end - end - figures.suffixes = figures_suffixes - figures.patterns = figures_patterns -end - -setlookups() - -figures.setlookups = setlookups - -function figures.registerresource(t) - local n = #figures_resources + 1 - figures_resources[n] = t - return n -end - -local function register(tag,target,what) - local data = figures_formats[target] -- resolver etc - if not data then - data = { } - figures_formats[target] = data - end - local d = data[tag] -- list or pattern - if d and not contains(d,what) then - d[#d+1] = what -- suffix or patternspec - else - data[tag] = { what } - end - if not contains(figures_order,target) then - figures_order[#figures_order+1] = target - end - setlookups() -end - -function figures.registersuffix (suffix, target) register('list', target,suffix ) end -function figures.registerpattern(pattern,target) register('pattern',target,pattern) end - -local last_locationset = last_locationset or nil -local last_pathlist = last_pathlist or nil - -function figures.setpaths(locationset,pathlist) - if last_locationset == locationset and last_pathlist == pathlist then - -- this function can be called each graphic so we provide this optimization - return - end - local t, h = figure_paths, settings_to_hash(locationset) - if last_locationset ~= locationset then - -- change == reset (actually, a 'reset' would indeed reset - if h[v_local] then - t = table.fastcopy(figures.localpaths or { }) - else - t = { } - end - figures.defaultsearch = h[v_default] - last_locationset = locationset - end - if h[v_global] then - local list = settings_to_array(pathlist) - for i=1,#list do - local s = list[i] - if not contains(t,s) then - t[#t+1] = s - end - end - end - figure_paths = t - last_pathlist = pathlist - figures.paths = figure_paths - if trace_figures then - report_inclusion("using locations %a",last_locationset) - report_inclusion("using paths % a",figure_paths) - end -end - --- check conversions and handle it here - -function figures.hash(data) - local status = data and data.status - return (status and status.hash or tostring(status.private)) or "nohash" -- the <img object> -end - --- interfacing to tex - -local function new() -- we could use metatables status -> used -> request but it needs testing - local request = { - name = false, - label = false, - format = false, - page = false, - width = false, - height = false, - preview = false, - ["repeat"] = false, - controls = false, - display = false, - mask = false, - conversion = false, - resolution = false, - cache = false, - prefix = false, - size = false, - } - local used = { - fullname = false, - format = false, - name = false, - path = false, - suffix = false, - width = false, - height = false, - } - local status = { - status = 0, - converted = false, - cached = false, - fullname = false, - format = false, - } - -- this needs checking because we might check for nil, the test case - -- is getfiguredimensions which then should return ~= 0 - -- setmetatableindex(status, used) - -- setmetatableindex(used, request) - return { - request = request, - used = used, - status = status, - } -end - --- use table.insert|remove - -local lastfiguredata = nil -- will be topofstack or last so no { } (else problems with getfiguredimensions) -local callstack = { } - -function figures.initialize(request) - local figuredata = new() - if request then - -- request.width/height are strings and are only used when no natural dimensions - -- can be determined; at some point the handlers might set them to numbers instead - local w = tonumber(request.width) or 0 - local h = tonumber(request.height) or 0 - request.width = w > 0 and w or nil - request.height = h > 0 and h or nil - -- - request.page = math.max(tonumber(request.page) or 1,1) - request.size = img.checksize(request.size) - request.object = request.object == v_yes - request["repeat"] = request["repeat"] == v_yes - request.preview = request.preview == v_yes - request.cache = request.cache ~= "" and request.cache - request.prefix = request.prefix ~= "" and request.prefix - request.format = request.format ~= "" and request.format - table.merge(figuredata.request,request) - end - return figuredata -end - -function figures.push(request) - statistics.starttiming(figures) - local figuredata = figures.initialize(request) - insert(callstack,figuredata) - lastfiguredata = figuredata - return figuredata -end - -function figures.pop() - lastfiguredata = remove(callstack) or lastfiguredata - statistics.stoptiming(figures) -end - -function figures.current() - return callstack[#callstack] or lastfiguredata -end - -local function get(category,tag,default) - local value = lastfiguredata and lastfiguredata[category] - value = value and value[tag] - if not value or value == "" or value == true then - return default or "" - else - return value - end -end - -figures.get = get - -function commands.figurevariable(category,tag,default) - context(get(category,tag,default)) -end - -function commands.figurestatus (tag,default) context(get("status", tag,default)) end -function commands.figurerequest(tag,default) context(get("request",tag,default)) end -function commands.figureused (tag,default) context(get("used", tag,default)) end - -function commands.figurefilepath() context(file.dirname (get("used","fullname"))) end -function commands.figurefilename() context(file.nameonly(get("used","fullname"))) end -function commands.figurefiletype() context(file.extname (get("used","fullname"))) end - --- todo: local path or cache path - -local function forbiddenname(filename) - if not filename or filename == "" then - return false - end - local expandedfullname = collapsepath(filename,true) - local expandedinputname = collapsepath(file.addsuffix(environment.jobfilename,environment.jobfilesuffix),true) - if expandedfullname == expandedinputname then - report_inclusion("skipping graphic with same name as input filename %a, enforce suffix",expandedinputname) - return true - end - local expandedoutputname = collapsepath(codeinjections.getoutputfilename(),true) - if expandedfullname == expandedoutputname then - report_inclusion("skipping graphic with same name as output filename %a, enforce suffix",expandedoutputname) - return true - end -end - -local function register(askedname,specification) - if not specification then - specification = { } - elseif forbiddenname(specification.fullname) then - specification = { } - else - local format = specification.format - if format then - local conversion = specification.conversion - local resolution = specification.resolution - if conversion == "" then - conversion = nil - end - if resolution == "" then - resolution = nil - end - local newformat = conversion - if not newformat or newformat == "" then - newformat = defaultformat - end - if trace_conversion then - report_inclusion("checking conversion of %a, fullname %a, old format %a, new format %a, conversion %a, resolution %a", - askedname,specification.fullname,format,newformat,conversion or "default",resolution or "default") - end - -- quick hack - local converter = (newformat ~= format or resolution) and converters[format] - if converter then - if converter[newformat] then - converter = converter[newformat] - else - newformat = defaultformat - if converter[newformat] then - converter = converter[newformat] - else - converter = nil - newformat = defaultformat - end - end - elseif trace_conversion then - report_inclusion("no converter for %a to %a",format,newformat) - end - if converter then - local oldname = specification.fullname - local newpath = file.dirname(oldname) - local oldbase = file.basename(oldname) - -- - -- problem: we can have weird filenames, like a.b.c (no suffix) and a.b.c.gif - -- so we cannot safely remove a suffix (unless we do that for known suffixes) - -- - -- local newbase = file.removesuffix(oldbase) -- assumes a known suffix - -- - -- so we now have (also see *): - -- - local newbase = oldbase - -- - local fc = specification.cache or figures.cachepaths.path - if fc and fc ~= "" and fc ~= "." then - newpath = fc - else - newbase = defaultprefix .. newbase - end - if not file.is_writable(newpath) then - if trace_conversion then - report_inclusion("path %a is not writable, forcing conversion path %a",newpath,".") - end - newpath = "." - end - local subpath = specification.subpath or figures.cachepaths.subpath - if subpath and subpath ~= "" and subpath ~= "." then - newpath = newpath .. "/" .. subpath - end - local prefix = specification.prefix or figures.cachepaths.prefix - if prefix and prefix ~= "" then - newbase = prefix .. newbase - end - if resolution and resolution ~= "" then -- the order might change - newbase = newbase .. "_" .. resolution - end - -- - -- see *, we had: - -- - -- local newbase = file.addsuffix(newbase,newformat) - -- - -- but now have (result of Aditya's web image testing): - -- - -- as a side effect we can now have multiple fetches with different - -- original figures_formats, not that it matters much (apart from older conversions - -- sticking around) - -- - local newbase = newbase .. "." .. newformat - -- - local newname = file.join(newpath,newbase) - dir.makedirs(newpath) - oldname = collapsepath(oldname) - newname = collapsepath(newname) - local oldtime = lfs.attributes(oldname,'modification') or 0 - local newtime = lfs.attributes(newname,'modification') or 0 - if newtime == 0 or oldtime > newtime then - if trace_conversion then - report_inclusion("converting %a (%a) from %a to %a",askedname,oldname,format,newformat) - end - converter(oldname,newname,resolution or "") - else - if trace_conversion then - report_inclusion("no need to convert %a (%a) from %a to %a",askedname,oldname,format,newformat) - end - end - if io.exists(newname) and io.size(newname) > 0 then - specification.foundname = oldname - specification.fullname = newname - specification.prefix = prefix - specification.subpath = subpath - specification.converted = true - format = newformat - if not figures_suffixes[format] then - -- maybe the new format is lowres.png (saves entry in suffixes) - -- so let's do thsi extra check - local suffix = file.suffix(newformat) - if figures_suffixes[suffix] then - if trace_figures then - report_inclusion("using suffix %a as format for %a",suffix,format) - end - format = suffix - end - end - elseif io.exists(oldname) then - specification.fullname = oldname -- was newname - specification.converted = false - end - end - end - local found = figures_suffixes[format] -- validtypes[format] - if not found then - specification.found = false - if trace_figures then - report_inclusion("format %a is not supported",format) - end - else - specification.found = true - if trace_figures then - if validtypes[format] then -- format? - report_inclusion("format %a natively supported by backend",format) - else - report_inclusion("format %a supported by output file format",format) - end - end - end - end - specification.foundname = specification.foundname or specification.fullname - local askedhash = f_hash_part(askedname,specification.conversion or "default",specification.resolution or "default") - figures_found[askedhash] = specification - return specification -end - -local resolve_too = false -- true - -local internalschemes = { - file = true, -} - -local function locate(request) -- name, format, cache - -- not resolvers.cleanpath(request.name) as it fails on a!b.pdf and b~c.pdf - -- todo: more restricted cleanpath - local askedname = request.name - local askedhash = f_hash_part(askedname,request.conversion or "default",request.resolution or "default") - local foundname = figures_found[askedhash] - if foundname then - return foundname - end - -- - local askedcache = request.cache - local askedconversion = request.conversion - local askedresolution = request.resolution - -- - if request.format == "" or request.format == "unknown" then - request.format = nil - end - -- protocol check - local hashed = url.hashed(askedname) - if not hashed then - -- go on - elseif internalschemes[hashed.scheme] then - local path = hashed.path - if path and path ~= "" then - askedname = path - end - else - local foundname = resolvers.findbinfile(askedname) - if not foundname or not lfs.isfile(foundname) then -- foundname can be dummy - if trace_figures then - report_inclusion("unknown url %a",askedname) - end - -- url not found - return register(askedname) - end - local askedformat = request.format or file.suffix(askedname) or "" - local guessedformat = figures.guess(foundname) - if askedformat ~= guessedformat then - if trace_figures then - report_inclusion("url %a has unknown format",askedname) - end - -- url found, but wrong format - return register(askedname) - else - if trace_figures then - report_inclusion("url %a is resolved to %a",askedname,foundname) - end - return register(askedname, { - askedname = askedname, - fullname = foundname, - format = askedformat, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - -- we could use the hashed data instead - local askedpath= file.is_rootbased_path(askedname) - local askedbase = file.basename(askedname) - local askedformat = request.format or file.suffix(askedname) or "" - if askedformat ~= "" then - askedformat = lower(askedformat) - if trace_figures then - report_inclusion("forcing format %a",askedformat) - end - local format = figures_suffixes[askedformat] - if not format then - for i=1,#figures_patterns do - local pattern = figures_patterns[i] - if find(askedformat,pattern[1]) then - format = pattern[2] - break - end - end - end - if format then - local foundname, quitscanning, forcedformat = figures.exists(askedname,format,resolve_too) -- not askedformat - if foundname then - return register(askedname, { - askedname = askedname, - fullname = foundname, -- askedname, - format = forcedformat or format, - cache = askedcache, - -- foundname = foundname, -- no - conversion = askedconversion, - resolution = askedresolution, - }) - elseif quitscanning then - return register(askedname) - end - elseif trace_figures then - report_inclusion("unknown format %a",askedformat) - end - if askedpath then - -- path and type given, todo: strip pieces of path - local foundname, quitscanning, forcedformat = figures.exists(askedname,askedformat,resolve_too) - if foundname then - return register(askedname, { - askedname = askedname, - fullname = foundname, -- askedname, - format = forcedformat or askedformat, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - else - -- type given - for i=1,#figure_paths do - local path = figure_paths[i] - local check = path .. "/" .. askedname - -- we pass 'true' as it can be an url as well, as the type - -- is given we don't waste much time - local foundname, quitscanning, forcedformat = figures.exists(check,askedformat,resolve_too) - if foundname then - return register(check, { - askedname = askedname, - fullname = check, - format = askedformat, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - if figures.defaultsearch then - local check = resolvers.findfile(askedname) - if check and check ~= "" then - return register(askedname, { - askedname = askedname, - fullname = check, - format = askedformat, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - end - elseif askedpath then - if trace_figures then - report_inclusion("using rootbased path") - end - for i=1,#figures_order do - local format = figures_order[i] - local list = figures_formats[format].list or { format } - for j=1,#list do - local suffix = list[j] - local check = file.addsuffix(askedname,suffix) - local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too) - if foundname then - return register(askedname, { - askedname = askedname, - fullname = foundname, -- check, - format = forcedformat or format, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - end - else - if figures.preferquality then - if trace_figures then - report_inclusion("unknown format, quality preferred") - end - for j=1,#figures_order do - local format = figures_order[j] - local list = figures_formats[format].list or { format } - for k=1,#list do - local suffix = list[k] - -- local name = file.replacesuffix(askedbase,suffix) - local name = file.replacesuffix(askedname,suffix) - for i=1,#figure_paths do - local path = figure_paths[i] - local check = path .. "/" .. name - local isfile = url.hashed(check).scheme == "file" - if not isfile then - if trace_figures then - report_inclusion("warning: skipping path %a",path) - end - else - local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too) -- true) - if foundname then - return register(askedname, { - askedname = askedname, - fullname = foundname, -- check - format = forcedformat or format, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - end - end - end - else -- 'location' - if trace_figures then - report_inclusion("unknown format, using path strategy") - end - for i=1,#figure_paths do - local path = figure_paths[i] - for j=1,#figures_order do - local format = figures_order[j] - local list = figures_formats[format].list or { format } - for k=1,#list do - local suffix = list[k] - local check = path .. "/" .. file.replacesuffix(askedbase,suffix) - local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too) - if foundname then - return register(askedname, { - askedname = askedname, - fullname = foudname, -- check, - format = forcedformat or format, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - end - end - end - if figures.defaultsearch then - if trace_figures then - report_inclusion("using default tex path") - end - for j=1,#figures_order do - local format = figures_order[j] - local list = figures_formats[format].list or { format } - for k=1,#list do - local suffix = list[k] - local check = resolvers.findfile(file.replacesuffix(askedname,suffix)) - if check and check ~= "" then - return register(askedname, { - askedname = askedname, - fullname = check, - format = format, - cache = askedcache, - conversion = askedconversion, - resolution = askedresolution, - }) - end - end - end - end - end - return register(askedname, { -- these two are needed for hashing 'found' - conversion = askedconversion, - resolution = askedresolution, - }) -end - --- -- -- plugins -- -- -- - -function identifiers.default(data) - local dr, du, ds = data.request, data.used, data.status - local l = locate(dr) - local foundname = l.foundname - local fullname = l.fullname or foundname - if fullname then - du.format = l.format or false - du.fullname = fullname -- can be cached - ds.fullname = foundname -- original - ds.format = l.format - ds.status = (l.found and 10) or 0 - end - return data -end - -function figures.identify(data) - data = data or callstack[#callstack] or lastfiguredata - local list = identifiers.list -- defined at the end - for i=1,#list do - local identifier = list[i] - data = identifier(data) - if data.status.status > 0 then - break - end - end - return data -end - -function figures.exists(askedname,format,resolve) - return (existers[format] or existers.generic)(askedname,resolve) -end - -function figures.check(data) - data = data or callstack[#callstack] or lastfiguredata - return (checkers[data.status.format] or checkers.generic)(data) -end - -function figures.include(data) - data = data or callstack[#callstack] or lastfiguredata - return (includers[data.status.format] or includers.generic)(data) -end - -function figures.scale(data) -- will become lua code - context.doscalefigure() - return data -end - -function figures.done(data) - figures.nofprocessed = figures.nofprocessed + 1 - data = data or callstack[#callstack] or lastfiguredata - local dr, du, ds, nr = data.request, data.used, data.status, figures.boxnumber - local box = texbox[nr] - ds.width = box.width - ds.height = box.height - ds.xscale = ds.width /(du.width or 1) - ds.yscale = ds.height/(du.height or 1) - ds.page = ds.page or du.page or dr.page -- sort of redundant but can be limited - return data -end - -function figures.dummy(data) - data = data or callstack[#callstack] or lastfiguredata - local dr, du, nr = data.request, data.used, figures.boxnumber - local box = node.hpack(node.new("hlist")) -- we need to set the dir (luatex 0.60 buglet) - du.width = du.width or figures.defaultwidth - du.height = du.height or figures.defaultheight - du.depth = du.depth or figures.defaultdepth - -- box.dir = "TLT" - box.width = du.width - box.height = du.height - box.depth = du.depth - texbox[nr] = box -- hm, should be global (to be checked for consistency) -end - --- -- -- generic -- -- -- - -function existers.generic(askedname,resolve) - -- not findbinfile - local result - if lfs.isfile(askedname) then - result = askedname - elseif resolve then - result = resolvers.findbinfile(askedname) or "" - if result == "" then result = false end - end - if trace_figures then - if result then - report_inclusion("%a resolved to %a",askedname,result) - else - report_inclusion("%a cannot be resolved",askedname) - end - end - return result -end - -function checkers.generic(data) - local dr, du, ds = data.request, data.used, data.status - local name = du.fullname or "unknown generic" - local page = du.page or dr.page - local size = dr.size or "crop" - local color = dr.color or "natural" - local mask = dr.mask or "none" - local conversion = dr.conversion - local resolution = dr.resolution - if not conversion or conversion == "" then - conversion = "unknown" - end - if not resolution or resolution == "" then - resolution = "unknown" - end - local hash = f_hash_full(name,page,size,color,conversion,resolution,mask) - local figure = figures_loaded[hash] - if figure == nil then - figure = img.new { - filename = name, - page = page, - pagebox = dr.size, - -- visiblefilename = "", -- this prohibits the full filename ending up in the file - } - codeinjections.setfigurecolorspace(data,figure) - codeinjections.setfiguremask(data,figure) - figure = figure and img.check(img.scan(figure)) or false - local f, d = codeinjections.setfigurealternative(data,figure) - figure, data = f or figure, d or data - figures_loaded[hash] = figure - if trace_conversion then - report_inclusion("new graphic, using hash %a",hash) - end - else - if trace_conversion then - report_inclusion("existing graphic, using hash %a",hash) - end - end - if figure then - du.width = figure.width - du.height = figure.height - du.pages = figure.pages - du.depth = figure.depth or 0 - du.colordepth = figure.colordepth or 0 - du.xresolution = figure.xres or 0 - du.yresolution = figure.yres or 0 - du.xsize = figure.xsize or 0 - du.ysize = figure.ysize or 0 - ds.private = figure - ds.hash = hash - end - return data -end - -function includers.generic(data) - local dr, du, ds = data.request, data.used, data.status - -- here we set the 'natural dimensions' - dr.width = du.width - dr.height = du.height - local hash = figures.hash(data) - local figure = figures_used[hash] - -- figures.registerresource { - -- filename = du.fullname, - -- width = dr.width, - -- height = dr.height, - -- } - if figure == nil then - figure = ds.private - if figure then - figure = img.copy(figure) - figure = figure and img.clone(figure,data.request) or false - end - figures_used[hash] = figure - end - if figure then - local nr = figures.boxnumber - -- it looks like we have a leak in attributes here .. todo - local box = node.hpack(img.node(figure)) -- img.node(figure) not longer valid - indexed[figure.index] = figure - box.width, box.height, box.depth = figure.width, figure.height, 0 -- new, hm, tricky, we need to do that in tex (yet) - texbox[nr] = box - ds.objectnumber = figure.objnum - context.relocateexternalfigure() - end - return data -end - --- -- -- nongeneric -- -- -- - -local function checkers_nongeneric(data,command) -- todo: macros and context.* - local dr, du, ds = data.request, data.used, data.status - local name = du.fullname or "unknown nongeneric" - local hash = name - if dr.object then - -- hm, bugged ... waiting for an xform interface - if not job.objects.get("FIG::"..hash) then - if type(command) == "function" then - command() - end - context.dosetfigureobject(hash) - end - context.doboxfigureobject(hash) - elseif type(command) == "function" then - command() - end - return data -end - -local function includers_nongeneric(data) - return data -end - -checkers.nongeneric = checkers_nongeneric -includers.nongeneric = includers_nongeneric - --- -- -- mov -- -- -- - -function checkers.mov(data) - local dr, du, ds = data.request, data.used, data.status - local width = todimen(dr.width or figures.defaultwidth) - local height = todimen(dr.height or figures.defaultheight) - local foundname = du.fullname - dr.width, dr.height = width, height - du.width, du.height, du.foundname = width, height, foundname - if trace_inclusion then - report_inclusion("including movie %a, width %p, height %p",foundname,width,height) - end - -- we need to push the node.write in between ... we could make a shared helper for this - context.startfoundexternalfigure(width .. "sp",height .. "sp") - context(function() - nodeinjections.insertmovie { - width = width, - height = height, - factor = number.dimenfactors.bp, - ["repeat"] = dr["repeat"], - controls = dr.controls, - preview = dr.preview, - label = dr.label, - foundname = foundname, - } - end) - context.stopfoundexternalfigure() - return data -end - -includers.mov = includers.nongeneric - --- -- -- mps -- -- -- - -internalschemes.mprun = true - -local function internal(askedname) - local spec, mprun, mpnum = match(lower(askedname),"mprun([:%.]?)(.-)%.(%d+)") - if spec ~= "" then - return mprun, mpnum - else - return "", mpnum - end -end - -function existers.mps(askedname) - local mprun, mpnum = internal(askedname) - if mpnum then - return askedname - else - return existers.generic(askedname) - end -end - -function checkers.mps(data) - local mprun, mpnum = internal(data.used.fullname) - if mpnum then - return checkers_nongeneric(data,function() context.docheckfiguremprun(mprun,mpnum) end) - else - return checkers_nongeneric(data,function() context.docheckfiguremps(data.used.fullname) end) - end -end - -includers.mps = includers.nongeneric - --- -- -- tex -- -- -- - -function existers.tex(askedname) - askedname = resolvers.findfile(askedname) - return askedname ~= "" and askedname or false -end - -function checkers.tex(data) - return checkers_nongeneric(data,function() context.docheckfiguretex(data.used.fullname) end) -end - -includers.tex = includers.nongeneric - --- -- -- buffer -- -- -- - -function existers.buffer(askedname) - local name = file.nameonly(askedname) - local okay = buffers.exists(name) - return okay and name, true -- always quit scanning -end - -function checkers.buffer(data) - return checkers_nongeneric(data,function() context.docheckfigurebuffer(file.nameonly(data.used.fullname)) end) -end - -includers.buffers = includers.nongeneric - --- -- -- auto -- -- -- - -function existers.auto(askedname) - local name = gsub(askedname, ".auto$", "") - local format = figures.guess(name) - if format then - report_inclusion("format guess %a for %a",format,name) - else - report_inclusion("format guess for %a is not possible",name) - end - return format and name, true, format -end - -checkers.auto = checkers.generic -includers.auto = includers.generic - --- -- -- cld -- -- -- - -existers.cld = existers.tex - -function checkers.cld(data) - return checkers_nongeneric(data,function() context.docheckfigurecld(data.used.fullname) end) -end - -includers.cld = includers.nongeneric - --- -- -- converters -- -- -- - -local function makeoptions(options) - local to = type(options) - return (to == "table" and concat(options," ")) or (to == "string" and options) or "" -end - --- programs.makeoptions = makeoptions - -local function runprogram(binary,argument,variables) - local binary = match(binary,"[%S]+") -- to be sure - if type(argument) == "table" then - argument = concat(argument," ") -- for old times sake - end - if not os.which(binary) then - report_inclusion("program %a is not installed, not running command: %s",binary,command) - elseif not argument or argument == "" then - report_inclusion("nothing to run, unknown program %a",binary) - else - local command = format([["%s" %s]],binary,replacetemplate(longtostring(argument),variables)) - if trace_conversion or trace_programs then - report_inclusion("running command: %s",command) - end - os.spawn(command) - end -end - -programs.run = runprogram - --- -- -- eps & pdf -- -- -- --- --- \externalfigure[cow.eps] --- \externalfigure[cow.pdf][conversion=stripped] - -local epsconverter = converters.eps or { } -converters.eps = epsconverter -converters.ps = epsconverter - -local epstopdf = { - resolutions = { - [v_low] = "screen", - [v_medium] = "ebook", - [v_high] = "prepress", - }, - command = os.type == "windows" and "gswin32c" or "gs", - -- -dProcessDSCComments=false - argument = [[ - -q - -sDEVICE=pdfwrite - -dNOPAUSE - -dNOCACHE - -dBATCH - -dAutoRotatePages=/None - -dPDFSETTINGS=/%presets% - -dEPSCrop - -sOutputFile=%newname% - %oldname% - -c quit - ]], -} - -programs.epstopdf = epstopdf -programs.gs = epstopdf - -function epsconverter.pdf(oldname,newname,resolution) -- the resolution interface might change - local epstopdf = programs.epstopdf -- can be changed - local presets = epstopdf.resolutions[resolution or ""] or epstopdf.resolutions.high - runprogram(epstopdf.command, epstopdf.argument, { - newname = newname, - oldname = oldname, - presets = presets, - } ) -end - -epsconverter.default = epsconverter.pdf - -local pdfconverter = converters.pdf or { } -converters.pdf = pdfconverter - -programs.pdftoeps = { - command = "pdftops", - argument = [[-eps "%oldname%" "%newname%]], -} - -pdfconverter.stripped = function(oldname,newname) - local pdftoeps = programs.pdftoeps -- can be changed - local epstopdf = programs.epstopdf -- can be changed - local presets = epstopdf.resolutions[resolution or ""] or epstopdf.resolutions.high - local tmpname = newname .. ".tmp" - runprogram(pdftoeps.command, pdftoeps.argument, { oldname = oldname, newname = tmpname, presets = presets }) - runprogram(epstopdf.command, epstopdf.argument, { oldname = tmpname, newname = newname, presets = presets }) - os.remove(tmpname) -end - -figures.registersuffix("stripped","pdf") - --- -- -- svg -- -- -- - -local svgconverter = { } -converters.svg = svgconverter -converters.svgz = svgconverter - --- inkscape on windows only works with complete paths - -programs.inkscape = { - command = "inkscape", - pdfargument = [[ - "%oldname%" - --export-dpi=600 - -A - "%newname%" - ]], - pngargument = [[ - "%oldname%" - --export-dpi=600 - --export-png="%newname%" - ]], -} - -function svgconverter.pdf(oldname,newname) - local inkscape = programs.inkscape -- can be changed - runprogram(inkscape.command, inkscape.pdfargument, { - newname = expandfilename(newname), - oldname = expandfilename(oldname), - } ) -end - -function svgconverter.png(oldname,newname) - local inkscape = programs.inkscape - runprogram(inkscape.command, inkscape.pngargument, { - newname = expandfilename(newname), - oldname = expandfilename(oldname), - } ) -end - -svgconverter.default = svgconverter.pdf - --- -- -- gif -- -- -- --- -- -- tif -- -- -- - -local gifconverter = converters.gif or { } -local tifconverter = converters.tif or { } -local bmpconverter = converters.bmp or { } - -converters.gif = gifconverter -converters.tif = tifconverter -converters.bmp = bmpconverter - -programs.convert = { - command = "gm", -- graphicmagick - argument = [[convert "%oldname%" "%newname%"]], -} - -local function converter(oldname,newname) - local convert = programs.convert - runprogram(convert.command, convert.argument, { - newname = newname, - oldname = oldname, - } ) -end - -tifconverter.pdf = converter -gifconverter.pdf = converter -bmpconverter.pdf = converter - -gifconverter.default = converter -tifconverter.default = converter -bmpconverter.default = converter - --- todo: lowres - --- -- -- bases -- -- -- - -local bases = allocate() -figures.bases = bases - -local bases_list = nil -- index => { basename, fullname, xmlroot } -local bases_used = nil -- [basename] => { basename, fullname, xmlroot } -- pointer to list -local bases_found = nil -local bases_enabled = false - -local function reset() - bases_list = allocate() - bases_used = allocate() - bases_found = allocate() - bases_enabled = false - bases.list = bases_list - bases.used = bases_used - bases.found = bases_found -end - -reset() - -function bases.use(basename) - if basename == "reset" then - reset() - else - basename = file.addsuffix(basename,"xml") - if not bases_used[basename] then - local t = { basename, nil, nil } - bases_used[basename] = t - bases_list[#bases_list+1] = t - if not bases_enabled then - bases_enabled = true - xml.registerns("rlx","http://www.pragma-ade.com/schemas/rlx") -- we should be able to do this per xml file - end - if trace_bases then - report_inclusion("registering base %a",basename) - end - end - end -end - -local function bases_find(basename,askedlabel) - if trace_bases then - report_inclusion("checking for %a in base %a",askedlabel,basename) - end - basename = file.addsuffix(basename,"xml") - local t = bases_found[askedlabel] - if t == nil then - local base = bases_used[basename] - local page = 0 - if base[2] == nil then - -- no yet located - for i=1,#figure_paths do - local path = figure_paths[i] - local xmlfile = path .. "/" .. basename - if io.exists(xmlfile) then - base[2] = xmlfile - base[3] = xml.load(xmlfile) - if trace_bases then - report_inclusion("base %a loaded",xmlfile) - end - break - end - end - end - t = false - if base[2] and base[3] then -- rlx:library - for e in xml.collected(base[3],"/(*:library|figurelibrary)/*:figure/*:label") do - page = page + 1 - if xml.text(e) == askedlabel then - t = { - base = file.replacesuffix(base[2],"pdf"), - format = "pdf", - name = xml.text(e,"../*:file"), -- to be checked - page = page, - } - bases_found[askedlabel] = t - if trace_bases then - report_inclusion("figure %a found in base %a",askedlabel,base[2]) - end - return t - end - end - if trace_bases and not t then - report_inclusion("figure %a not found in base %a",askedlabel,base[2]) - end - end - end - return t -end - --- we can access sequential or by name - -local function bases_locate(askedlabel) - for i=1,#bases_list do - local entry = bases_list[i] - local t = bases_find(entry[1],askedlabel) - if t then - return t - end - end - return false -end - -function identifiers.base(data) - if bases_enabled then - local dr, du, ds = data.request, data.used, data.status - local fbl = bases_locate(dr.name or dr.label) - if fbl then - du.page = fbl.page - du.format = fbl.format - du.fullname = fbl.base - ds.fullname = fbl.name - ds.format = fbl.format - ds.page = fbl.page - ds.status = 10 - end - end - return data -end - -bases.locate = bases_locate -bases.find = bases_find - -identifiers.list = { - identifiers.base, - identifiers.default -} - --- tracing - -statistics.register("graphics processing time", function() - local nofprocessed = figures.nofprocessed - if nofprocessed > 0 then - return format("%s seconds including tex, %s processed images", statistics.elapsedtime(figures),nofprocessed) - else - return nil - end -end) - --- helper - -function figures.applyratio(width,height,w,h) -- width and height are strings and w and h are numbers - if not width or width == "" then - if not height or height == "" then - return figures.defaultwidth, figures.defaultheight - else - height = todimen(height) - if w and h then - return height * w/h, height - else - return figures.defaultwidth, height - end - end - else - width = todimen(width) - if not height or height == "" then - if w and h then - return width, width * h/w - else - return width, figures.defaultheight - end - else - return width, todimen(height) - end - end -end - --- example of simple plugins: --- --- figures.converters.png = { --- png = function(oldname,newname,resolution) --- local command = string.format('gm convert -depth 1 "%s" "%s"',oldname,newname) --- logs.report(string.format("running command %s",command)) --- os.execute(command) --- end, --- } - --- local fig = figures.push { name = pdffile } --- figures.identify() --- figures.check() --- local nofpages = fig.used.pages --- figures.pop() - --- interfacing - -commands.setfigurelookuporder = figures.setorder +if not modules then modules = { } end modules ['grph-inc'] = {
+ version = 1.001,
+ comment = "companion to grph-inc.mkiv",
+ author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+ copyright = "PRAGMA ADE / ConTeXt Development Team",
+ license = "see context related readme files"
+}
+
+-- todo: empty filename or only suffix always false (not found)
+-- lowercase types
+-- mps tex tmp svg
+-- partly qualified
+-- dimensions
+-- use metatables
+-- figures.boxnumber can go as we now can use names
+-- avoid push
+-- move some to command namespace
+
+--[[
+The ConTeXt figure inclusion mechanisms are among the oldest code
+in ConTeXt and evolved into a complex whole. One reason is that we
+deal with backend in an abstract way. What complicates matters is
+that we deal with internal graphics as well: TeX code, MetaPost code,
+etc. Later on figure databases were introduced, which resulted in
+a plug in model for locating images. On top of that runs a conversion
+mechanism (with caching) and resource logging.
+
+Porting that to Lua is not that trivial because quite some
+status information is kept between al these stages. Of course, image
+reuse also has some price, and so I decided to implement the graphics
+inclusion in several layers: detection, loading, inclusion, etc.
+
+Object sharing and scaling can happen at each stage, depending on the
+way the resource is dealt with.
+
+The TeX-Lua mix is suboptimal. This has to do with the fact that we cannot
+run TeX code from within Lua. Some more functionality will move to Lua.
+]]--
+
+local format, lower, find, match, gsub, gmatch = string.format, string.lower, string.find, string.match, string.gsub, string.gmatch
+local texbox = tex.box
+local contains = table.contains
+local concat, insert, remove = table.concat, table.insert, table.remove
+local todimen = string.todimen
+local collapsepath = file.collapsepath
+local formatters = string.formatters
+local longtostring = string.longtostring
+local expandfilename = dir.expandname
+
+local P, lpegmatch = lpeg.P, lpeg.match
+
+local settings_to_array = utilities.parsers.settings_to_array
+local settings_to_hash = utilities.parsers.settings_to_hash
+local allocate = utilities.storage.allocate
+local setmetatableindex = table.setmetatableindex
+local replacetemplate = utilities.templates.replace
+
+local variables = interfaces.variables
+local codeinjections = backends.codeinjections
+local nodeinjections = backends.nodeinjections
+
+local trace_figures = false trackers.register("graphics.locating", function(v) trace_figures = v end)
+local trace_bases = false trackers.register("graphics.bases", function(v) trace_bases = v end)
+local trace_programs = false trackers.register("graphics.programs", function(v) trace_programs = v end)
+local trace_conversion = false trackers.register("graphics.conversion", function(v) trace_conversion = v end)
+local trace_inclusion = false trackers.register("graphics.inclusion", function(v) trace_inclusion = v end)
+
+local report_inclusion = logs.reporter("graphics","inclusion")
+
+local context, img = context, img
+
+local f_hash_part = formatters["%s->%s->%s"]
+local f_hash_full = formatters["%s->%s->%s->%s->%s->%s->%s"]
+
+local v_yes = variables.yes
+local v_low = variables.low
+local v_medium = variables.medium
+local v_high = variables.high
+local v_global = variables["global"]
+local v_local = variables["local"]
+local v_default = variables.default
+
+local maxdimen = 2^30-1
+
+function img.check(figure)
+ if figure then
+ local width = figure.width
+ local height = figure.height
+ if height > width then
+ if height > maxdimen then
+ figure.height = maxdimen
+ figure.width = width * maxdimen/height
+ report_inclusion("limiting natural dimensions of %a (%s)",figure.filename,"height")
+ end
+ elseif width > maxdimen then
+ figure.width = maxdimen
+ figure.height = height * maxdimen/width
+ report_inclusion("limiting natural dimensions of %a (%s)",figure.filename,"width")
+ end
+ return figure
+ end
+end
+
+--- some extra img functions --- can become luat-img.lua
+
+local imgkeys = img.keys()
+
+function img.totable(imgtable)
+ local result = { }
+ for k=1,#imgkeys do
+ local key = imgkeys[k]
+ result[key] = imgtable[key]
+ end
+ return result
+end
+
+function img.serialize(i,...)
+ return table.serialize(img.totable(i),...)
+end
+
+function img.print(i,...)
+ return table.print(img.totable(i),...)
+end
+
+function img.clone(i,data)
+ i.width = data.width or i.width
+ i.height = data.height or i.height
+ -- attr etc
+ return i
+end
+
+local validsizes = table.tohash(img.boxes())
+local validtypes = table.tohash(img.types())
+
+function img.checksize(size)
+ if size then
+ size = gsub(size,"box","")
+ return validsizes[size] and size or "crop"
+ else
+ return "crop"
+ end
+end
+
+local indexed = { }
+
+function img.ofindex(n)
+ return indexed[n]
+end
+
+--- we can consider an grph-ini file
+
+figures = figures or { }
+local figures = figures
+
+figures.boxnumber = figures.boxnumber or 0
+figures.defaultsearch = true
+figures.defaultwidth = 0
+figures.defaultheight = 0
+figures.defaultdepth = 0
+figures.nofprocessed = 0
+figures.preferquality = true -- quality over location
+
+local figures_loaded = allocate() figures.loaded = figures_loaded
+local figures_used = allocate() figures.used = figures_used
+local figures_found = allocate() figures.found = figures_found
+local figures_suffixes = allocate() figures.suffixes = figures_suffixes
+local figures_patterns = allocate() figures.patterns = figures_patterns
+local figures_resources = allocate() figures.resources = figures_resources
+
+local existers = allocate() figures.existers = existers
+local checkers = allocate() figures.checkers = checkers
+local includers = allocate() figures.includers = includers
+local converters = allocate() figures.converters = converters
+local identifiers = allocate() figures.identifiers = identifiers
+local programs = allocate() figures.programs = programs
+
+local defaultformat = "pdf"
+local defaultprefix = "m_k_i_v_"
+
+figures.localpaths = allocate {
+ ".", "..", "../.."
+}
+
+figures.cachepaths = allocate {
+ prefix = "",
+ path = ".",
+ subpath = ".",
+}
+
+local figure_paths = allocate(table.copy(figures.localpaths))
+figures.paths = figure_paths
+
+local figures_order = allocate {
+ "pdf", "mps", "jpg", "png", "jp2", "jbig", "svg", "eps", "tif", "gif", "mov", "buffer", "tex", "cld", "auto",
+}
+
+local figures_formats = allocate { -- magic and order will move here
+ ["pdf"] = { list = { "pdf" } },
+ ["mps"] = { patterns = { "mps", "%d+" } },
+ ["jpg"] = { list = { "jpg", "jpeg" } },
+ ["png"] = { list = { "png" } },
+ ["jp2"] = { list = { "jp2" } },
+ ["jbig"] = { list = { "jbig", "jbig2", "jb2" } },
+ ["svg"] = { list = { "svg", "svgz" } },
+ ["eps"] = { list = { "eps", "ai" } },
+ ["gif"] = { list = { "gif" } },
+ ["tif"] = { list = { "tif", "tiff" } },
+ ["mov"] = { list = { "mov", "flv", "mp4" } }, -- "avi" is not supported
+ ["buffer"] = { list = { "tmp", "buffer", "buf" } },
+ ["tex"] = { list = { "tex" } },
+ ["cld"] = { list = { "cld" } },
+ ["auto"] = { list = { "auto" } },
+}
+
+local figures_magics = allocate {
+ { format = "png", pattern = P("\137PNG\013\010\026\010") }, -- 89 50 4E 47 0D 0A 1A 0A,
+ { format = "jpg", pattern = P("\255\216\255") }, -- FF D8 FF
+ { format = "jp2", pattern = P("\000\000\000\012\106\080\032\032\013\010"), }, -- 00 00 00 0C 6A 50 20 20 0D 0A },
+ { format = "gif", pattern = P("GIF") },
+ { format = "pdf", pattern = (1 - P("%PDF"))^0 * P("%PDF") },
+}
+
+figures.formats = figures_formats -- frozen
+figures.magics = figures_magics -- frozen
+figures.order = figures_order -- frozen
+
+-- We can set the order but only indirectly so that we can check for support.
+
+function figures.setorder(list) -- can be table or string
+ if type(list) == "string" then
+ list = settings_to_array(list)
+ end
+ if list and #list > 0 then
+ figures_order = allocate()
+ figures.order = figures_order
+ local done = { } -- just to be sure in case the list is generated
+ for i=1,#list do
+ local l = lower(list[i])
+ if figures_formats[l] and not done[l] then
+ figures_order[#figures_order+1] = l
+ done[l] = true
+ end
+ end
+ report_inclusion("lookup order % a",figures_order)
+ else
+ -- invalid list
+ end
+end
+
+function figures.guess(filename)
+ local f = io.open(filename,'rb')
+ if f then
+ local str = f:read(100)
+ f:close()
+ if str then
+ for i=1,#figures_magics do
+ local pattern = figures_magics[i]
+ if lpegmatch(pattern.pattern,str) then
+ local format = pattern.format
+ if trace_figures then
+ report_inclusion("file %a has format %a",filename,format)
+ end
+ return format
+ end
+ end
+ end
+ end
+end
+
+local function setlookups() -- tobe redone .. just set locals
+ figures_suffixes = allocate()
+ figures_patterns = allocate()
+ for _, format in next, figures_order do
+ local data = figures_formats[format]
+ local list = data.list
+ if list then
+ for i=1,#list do
+ figures_suffixes[list[i]] = format -- hash
+ end
+ else
+ figures_suffixes[format] = format
+ end
+ local patterns = data.patterns
+ if patterns then
+ for i=1,#patterns do
+ figures_patterns[#figures_patterns+1] = { patterns[i], format } -- array
+ end
+ end
+ end
+ figures.suffixes = figures_suffixes
+ figures.patterns = figures_patterns
+end
+
+setlookups()
+
+figures.setlookups = setlookups
+
+function figures.registerresource(t)
+ local n = #figures_resources + 1
+ figures_resources[n] = t
+ return n
+end
+
+local function register(tag,target,what)
+ local data = figures_formats[target] -- resolver etc
+ if not data then
+ data = { }
+ figures_formats[target] = data
+ end
+ local d = data[tag] -- list or pattern
+ if d and not contains(d,what) then
+ d[#d+1] = what -- suffix or patternspec
+ else
+ data[tag] = { what }
+ end
+ if not contains(figures_order,target) then
+ figures_order[#figures_order+1] = target
+ end
+ setlookups()
+end
+
+function figures.registersuffix (suffix, target) register('list', target,suffix ) end
+function figures.registerpattern(pattern,target) register('pattern',target,pattern) end
+
+local last_locationset = last_locationset or nil
+local last_pathlist = last_pathlist or nil
+
+function figures.setpaths(locationset,pathlist)
+ if last_locationset == locationset and last_pathlist == pathlist then
+ -- this function can be called each graphic so we provide this optimization
+ return
+ end
+ local t, h = figure_paths, settings_to_hash(locationset)
+ if last_locationset ~= locationset then
+ -- change == reset (actually, a 'reset' would indeed reset
+ if h[v_local] then
+ t = table.fastcopy(figures.localpaths or { })
+ else
+ t = { }
+ end
+ figures.defaultsearch = h[v_default]
+ last_locationset = locationset
+ end
+ if h[v_global] then
+ local list = settings_to_array(pathlist)
+ for i=1,#list do
+ local s = list[i]
+ if not contains(t,s) then
+ t[#t+1] = s
+ end
+ end
+ end
+ figure_paths = t
+ last_pathlist = pathlist
+ figures.paths = figure_paths
+ if trace_figures then
+ report_inclusion("using locations %a",last_locationset)
+ report_inclusion("using paths % a",figure_paths)
+ end
+end
+
+-- check conversions and handle it here
+
+function figures.hash(data)
+ local status = data and data.status
+ return (status and status.hash or tostring(status.private)) or "nohash" -- the <img object>
+end
+
+-- interfacing to tex
+
+local function new() -- we could use metatables status -> used -> request but it needs testing
+ local request = {
+ name = false,
+ label = false,
+ format = false,
+ page = false,
+ width = false,
+ height = false,
+ preview = false,
+ ["repeat"] = false,
+ controls = false,
+ display = false,
+ mask = false,
+ conversion = false,
+ resolution = false,
+ cache = false,
+ prefix = false,
+ size = false,
+ }
+ local used = {
+ fullname = false,
+ format = false,
+ name = false,
+ path = false,
+ suffix = false,
+ width = false,
+ height = false,
+ }
+ local status = {
+ status = 0,
+ converted = false,
+ cached = false,
+ fullname = false,
+ format = false,
+ }
+ -- this needs checking because we might check for nil, the test case
+ -- is getfiguredimensions which then should return ~= 0
+ -- setmetatableindex(status, used)
+ -- setmetatableindex(used, request)
+ return {
+ request = request,
+ used = used,
+ status = status,
+ }
+end
+
+-- use table.insert|remove
+
+local lastfiguredata = nil -- will be topofstack or last so no { } (else problems with getfiguredimensions)
+local callstack = { }
+
+function figures.initialize(request)
+ local figuredata = new()
+ if request then
+ -- request.width/height are strings and are only used when no natural dimensions
+ -- can be determined; at some point the handlers might set them to numbers instead
+ local w = tonumber(request.width) or 0
+ local h = tonumber(request.height) or 0
+ request.width = w > 0 and w or nil
+ request.height = h > 0 and h or nil
+ --
+ request.page = math.max(tonumber(request.page) or 1,1)
+ request.size = img.checksize(request.size)
+ request.object = request.object == v_yes
+ request["repeat"] = request["repeat"] == v_yes
+ request.preview = request.preview == v_yes
+ request.cache = request.cache ~= "" and request.cache
+ request.prefix = request.prefix ~= "" and request.prefix
+ request.format = request.format ~= "" and request.format
+ table.merge(figuredata.request,request)
+ end
+ return figuredata
+end
+
+function figures.push(request)
+ statistics.starttiming(figures)
+ local figuredata = figures.initialize(request)
+ insert(callstack,figuredata)
+ lastfiguredata = figuredata
+ return figuredata
+end
+
+function figures.pop()
+ lastfiguredata = remove(callstack) or lastfiguredata
+ statistics.stoptiming(figures)
+end
+
+function figures.current()
+ return callstack[#callstack] or lastfiguredata
+end
+
+local function get(category,tag,default)
+ local value = lastfiguredata and lastfiguredata[category]
+ value = value and value[tag]
+ if not value or value == "" or value == true then
+ return default or ""
+ else
+ return value
+ end
+end
+
+figures.get = get
+
+function commands.figurevariable(category,tag,default)
+ context(get(category,tag,default))
+end
+
+function commands.figurestatus (tag,default) context(get("status", tag,default)) end
+function commands.figurerequest(tag,default) context(get("request",tag,default)) end
+function commands.figureused (tag,default) context(get("used", tag,default)) end
+
+function commands.figurefilepath() context(file.dirname (get("used","fullname"))) end
+function commands.figurefilename() context(file.nameonly(get("used","fullname"))) end
+function commands.figurefiletype() context(file.extname (get("used","fullname"))) end
+
+-- todo: local path or cache path
+
+local function forbiddenname(filename)
+ if not filename or filename == "" then
+ return false
+ end
+ local expandedfullname = collapsepath(filename,true)
+ local expandedinputname = collapsepath(file.addsuffix(environment.jobfilename,environment.jobfilesuffix),true)
+ if expandedfullname == expandedinputname then
+ report_inclusion("skipping graphic with same name as input filename %a, enforce suffix",expandedinputname)
+ return true
+ end
+ local expandedoutputname = collapsepath(codeinjections.getoutputfilename(),true)
+ if expandedfullname == expandedoutputname then
+ report_inclusion("skipping graphic with same name as output filename %a, enforce suffix",expandedoutputname)
+ return true
+ end
+end
+
+local function register(askedname,specification)
+ if not specification then
+ specification = { }
+ elseif forbiddenname(specification.fullname) then
+ specification = { }
+ else
+ local format = specification.format
+ if format then
+ local conversion = specification.conversion
+ local resolution = specification.resolution
+ if conversion == "" then
+ conversion = nil
+ end
+ if resolution == "" then
+ resolution = nil
+ end
+ local newformat = conversion
+ if not newformat or newformat == "" then
+ newformat = defaultformat
+ end
+ if trace_conversion then
+ report_inclusion("checking conversion of %a, fullname %a, old format %a, new format %a, conversion %a, resolution %a",
+ askedname,specification.fullname,format,newformat,conversion or "default",resolution or "default")
+ end
+ -- quick hack
+ local converter = (newformat ~= format or resolution) and converters[format]
+ if converter then
+ if converter[newformat] then
+ converter = converter[newformat]
+ else
+ newformat = defaultformat
+ if converter[newformat] then
+ converter = converter[newformat]
+ else
+ converter = nil
+ newformat = defaultformat
+ end
+ end
+ elseif trace_conversion then
+ report_inclusion("no converter for %a to %a",format,newformat)
+ end
+ if converter then
+ local oldname = specification.fullname
+ local newpath = file.dirname(oldname)
+ local oldbase = file.basename(oldname)
+ --
+ -- problem: we can have weird filenames, like a.b.c (no suffix) and a.b.c.gif
+ -- so we cannot safely remove a suffix (unless we do that for known suffixes)
+ --
+ -- local newbase = file.removesuffix(oldbase) -- assumes a known suffix
+ --
+ -- so we now have (also see *):
+ --
+ local newbase = oldbase
+ --
+ local fc = specification.cache or figures.cachepaths.path
+ if fc and fc ~= "" and fc ~= "." then
+ newpath = fc
+ else
+ newbase = defaultprefix .. newbase
+ end
+ if not file.is_writable(newpath) then
+ if trace_conversion then
+ report_inclusion("path %a is not writable, forcing conversion path %a",newpath,".")
+ end
+ newpath = "."
+ end
+ local subpath = specification.subpath or figures.cachepaths.subpath
+ if subpath and subpath ~= "" and subpath ~= "." then
+ newpath = newpath .. "/" .. subpath
+ end
+ local prefix = specification.prefix or figures.cachepaths.prefix
+ if prefix and prefix ~= "" then
+ newbase = prefix .. newbase
+ end
+ if resolution and resolution ~= "" then -- the order might change
+ newbase = newbase .. "_" .. resolution
+ end
+ --
+ -- see *, we had:
+ --
+ -- local newbase = file.addsuffix(newbase,newformat)
+ --
+ -- but now have (result of Aditya's web image testing):
+ --
+ -- as a side effect we can now have multiple fetches with different
+ -- original figures_formats, not that it matters much (apart from older conversions
+ -- sticking around)
+ --
+ local newbase = newbase .. "." .. newformat
+ --
+ local newname = file.join(newpath,newbase)
+ dir.makedirs(newpath)
+ oldname = collapsepath(oldname)
+ newname = collapsepath(newname)
+ local oldtime = lfs.attributes(oldname,'modification') or 0
+ local newtime = lfs.attributes(newname,'modification') or 0
+ if newtime == 0 or oldtime > newtime then
+ if trace_conversion then
+ report_inclusion("converting %a (%a) from %a to %a",askedname,oldname,format,newformat)
+ end
+ converter(oldname,newname,resolution or "")
+ else
+ if trace_conversion then
+ report_inclusion("no need to convert %a (%a) from %a to %a",askedname,oldname,format,newformat)
+ end
+ end
+ if io.exists(newname) and io.size(newname) > 0 then
+ specification.foundname = oldname
+ specification.fullname = newname
+ specification.prefix = prefix
+ specification.subpath = subpath
+ specification.converted = true
+ format = newformat
+ if not figures_suffixes[format] then
+ -- maybe the new format is lowres.png (saves entry in suffixes)
+ -- so let's do thsi extra check
+ local suffix = file.suffix(newformat)
+ if figures_suffixes[suffix] then
+ if trace_figures then
+ report_inclusion("using suffix %a as format for %a",suffix,format)
+ end
+ format = suffix
+ end
+ end
+ elseif io.exists(oldname) then
+ specification.fullname = oldname -- was newname
+ specification.converted = false
+ end
+ end
+ end
+ local found = figures_suffixes[format] -- validtypes[format]
+ if not found then
+ specification.found = false
+ if trace_figures then
+ report_inclusion("format %a is not supported",format)
+ end
+ else
+ specification.found = true
+ if trace_figures then
+ if validtypes[format] then -- format?
+ report_inclusion("format %a natively supported by backend",format)
+ else
+ report_inclusion("format %a supported by output file format",format)
+ end
+ end
+ end
+ end
+ specification.foundname = specification.foundname or specification.fullname
+ local askedhash = f_hash_part(askedname,specification.conversion or "default",specification.resolution or "default")
+ figures_found[askedhash] = specification
+ return specification
+end
+
+local resolve_too = false -- true
+
+local internalschemes = {
+ file = true,
+}
+
+local function locate(request) -- name, format, cache
+ -- not resolvers.cleanpath(request.name) as it fails on a!b.pdf and b~c.pdf
+ -- todo: more restricted cleanpath
+ local askedname = request.name
+ local askedhash = f_hash_part(askedname,request.conversion or "default",request.resolution or "default")
+ local foundname = figures_found[askedhash]
+ if foundname then
+ return foundname
+ end
+ --
+ local askedcache = request.cache
+ local askedconversion = request.conversion
+ local askedresolution = request.resolution
+ --
+ if request.format == "" or request.format == "unknown" then
+ request.format = nil
+ end
+ -- protocol check
+ local hashed = url.hashed(askedname)
+ if not hashed then
+ -- go on
+ elseif internalschemes[hashed.scheme] then
+ local path = hashed.path
+ if path and path ~= "" then
+ askedname = path
+ end
+ else
+ local foundname = resolvers.findbinfile(askedname)
+ if not foundname or not lfs.isfile(foundname) then -- foundname can be dummy
+ if trace_figures then
+ report_inclusion("unknown url %a",askedname)
+ end
+ -- url not found
+ return register(askedname)
+ end
+ local askedformat = request.format or file.suffix(askedname) or ""
+ local guessedformat = figures.guess(foundname)
+ if askedformat ~= guessedformat then
+ if trace_figures then
+ report_inclusion("url %a has unknown format",askedname)
+ end
+ -- url found, but wrong format
+ return register(askedname)
+ else
+ if trace_figures then
+ report_inclusion("url %a is resolved to %a",askedname,foundname)
+ end
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foundname,
+ format = askedformat,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ -- we could use the hashed data instead
+ local askedpath= file.is_rootbased_path(askedname)
+ local askedbase = file.basename(askedname)
+ local askedformat = request.format or file.suffix(askedname) or ""
+ if askedformat ~= "" then
+ askedformat = lower(askedformat)
+ if trace_figures then
+ report_inclusion("forcing format %a",askedformat)
+ end
+ local format = figures_suffixes[askedformat]
+ if not format then
+ for i=1,#figures_patterns do
+ local pattern = figures_patterns[i]
+ if find(askedformat,pattern[1]) then
+ format = pattern[2]
+ break
+ end
+ end
+ end
+ if format then
+ local foundname, quitscanning, forcedformat = figures.exists(askedname,format,resolve_too) -- not askedformat
+ if foundname then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foundname, -- askedname,
+ format = forcedformat or format,
+ cache = askedcache,
+ -- foundname = foundname, -- no
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ elseif quitscanning then
+ return register(askedname)
+ end
+ elseif trace_figures then
+ report_inclusion("unknown format %a",askedformat)
+ end
+ if askedpath then
+ -- path and type given, todo: strip pieces of path
+ local foundname, quitscanning, forcedformat = figures.exists(askedname,askedformat,resolve_too)
+ if foundname then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foundname, -- askedname,
+ format = forcedformat or askedformat,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ else
+ -- type given
+ for i=1,#figure_paths do
+ local path = figure_paths[i]
+ local check = path .. "/" .. askedname
+ -- we pass 'true' as it can be an url as well, as the type
+ -- is given we don't waste much time
+ local foundname, quitscanning, forcedformat = figures.exists(check,askedformat,resolve_too)
+ if foundname then
+ return register(check, {
+ askedname = askedname,
+ fullname = check,
+ format = askedformat,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ if figures.defaultsearch then
+ local check = resolvers.findfile(askedname)
+ if check and check ~= "" then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = check,
+ format = askedformat,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ end
+ elseif askedpath then
+ if trace_figures then
+ report_inclusion("using rootbased path")
+ end
+ for i=1,#figures_order do
+ local format = figures_order[i]
+ local list = figures_formats[format].list or { format }
+ for j=1,#list do
+ local suffix = list[j]
+ local check = file.addsuffix(askedname,suffix)
+ local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too)
+ if foundname then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foundname, -- check,
+ format = forcedformat or format,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ end
+ else
+ if figures.preferquality then
+ if trace_figures then
+ report_inclusion("unknown format, quality preferred")
+ end
+ for j=1,#figures_order do
+ local format = figures_order[j]
+ local list = figures_formats[format].list or { format }
+ for k=1,#list do
+ local suffix = list[k]
+ -- local name = file.replacesuffix(askedbase,suffix)
+ local name = file.replacesuffix(askedname,suffix)
+ for i=1,#figure_paths do
+ local path = figure_paths[i]
+ local check = path .. "/" .. name
+ local isfile = url.hashed(check).scheme == "file"
+ if not isfile then
+ if trace_figures then
+ report_inclusion("warning: skipping path %a",path)
+ end
+ else
+ local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too) -- true)
+ if foundname then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foundname, -- check
+ format = forcedformat or format,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ end
+ end
+ end
+ else -- 'location'
+ if trace_figures then
+ report_inclusion("unknown format, using path strategy")
+ end
+ for i=1,#figure_paths do
+ local path = figure_paths[i]
+ for j=1,#figures_order do
+ local format = figures_order[j]
+ local list = figures_formats[format].list or { format }
+ for k=1,#list do
+ local suffix = list[k]
+ local check = path .. "/" .. file.replacesuffix(askedbase,suffix)
+ local foundname, quitscanning, forcedformat = figures.exists(check,format,resolve_too)
+ if foundname then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = foudname, -- check,
+ format = forcedformat or format,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ end
+ end
+ end
+ if figures.defaultsearch then
+ if trace_figures then
+ report_inclusion("using default tex path")
+ end
+ for j=1,#figures_order do
+ local format = figures_order[j]
+ local list = figures_formats[format].list or { format }
+ for k=1,#list do
+ local suffix = list[k]
+ local check = resolvers.findfile(file.replacesuffix(askedname,suffix))
+ if check and check ~= "" then
+ return register(askedname, {
+ askedname = askedname,
+ fullname = check,
+ format = format,
+ cache = askedcache,
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+ end
+ end
+ end
+ end
+ end
+ return register(askedname, { -- these two are needed for hashing 'found'
+ conversion = askedconversion,
+ resolution = askedresolution,
+ })
+end
+
+-- -- -- plugins -- -- --
+
+function identifiers.default(data)
+ local dr, du, ds = data.request, data.used, data.status
+ local l = locate(dr)
+ local foundname = l.foundname
+ local fullname = l.fullname or foundname
+ if fullname then
+ du.format = l.format or false
+ du.fullname = fullname -- can be cached
+ ds.fullname = foundname -- original
+ ds.format = l.format
+ ds.status = (l.found and 10) or 0
+ end
+ return data
+end
+
+function figures.identify(data)
+ data = data or callstack[#callstack] or lastfiguredata
+ local list = identifiers.list -- defined at the end
+ for i=1,#list do
+ local identifier = list[i]
+ data = identifier(data)
+ if data.status.status > 0 then
+ break
+ end
+ end
+ return data
+end
+
+function figures.exists(askedname,format,resolve)
+ return (existers[format] or existers.generic)(askedname,resolve)
+end
+
+function figures.check(data)
+ data = data or callstack[#callstack] or lastfiguredata
+ return (checkers[data.status.format] or checkers.generic)(data)
+end
+
+function figures.include(data)
+ data = data or callstack[#callstack] or lastfiguredata
+ return (includers[data.status.format] or includers.generic)(data)
+end
+
+function figures.scale(data) -- will become lua code
+ context.doscalefigure()
+ return data
+end
+
+function figures.done(data)
+ figures.nofprocessed = figures.nofprocessed + 1
+ data = data or callstack[#callstack] or lastfiguredata
+ local dr, du, ds, nr = data.request, data.used, data.status, figures.boxnumber
+ local box = texbox[nr]
+ ds.width = box.width
+ ds.height = box.height
+ ds.xscale = ds.width /(du.width or 1)
+ ds.yscale = ds.height/(du.height or 1)
+ ds.page = ds.page or du.page or dr.page -- sort of redundant but can be limited
+ return data
+end
+
+function figures.dummy(data)
+ data = data or callstack[#callstack] or lastfiguredata
+ local dr, du, nr = data.request, data.used, figures.boxnumber
+ local box = node.hpack(node.new("hlist")) -- we need to set the dir (luatex 0.60 buglet)
+ du.width = du.width or figures.defaultwidth
+ du.height = du.height or figures.defaultheight
+ du.depth = du.depth or figures.defaultdepth
+ -- box.dir = "TLT"
+ box.width = du.width
+ box.height = du.height
+ box.depth = du.depth
+ texbox[nr] = box -- hm, should be global (to be checked for consistency)
+end
+
+-- -- -- generic -- -- --
+
+function existers.generic(askedname,resolve)
+ -- not findbinfile
+ local result
+ if lfs.isfile(askedname) then
+ result = askedname
+ elseif resolve then
+ result = resolvers.findbinfile(askedname) or ""
+ if result == "" then result = false end
+ end
+ if trace_figures then
+ if result then
+ report_inclusion("%a resolved to %a",askedname,result)
+ else
+ report_inclusion("%a cannot be resolved",askedname)
+ end
+ end
+ return result
+end
+
+function checkers.generic(data)
+ local dr, du, ds = data.request, data.used, data.status
+ local name = du.fullname or "unknown generic"
+ local page = du.page or dr.page
+ local size = dr.size or "crop"
+ local color = dr.color or "natural"
+ local mask = dr.mask or "none"
+ local conversion = dr.conversion
+ local resolution = dr.resolution
+ if not conversion or conversion == "" then
+ conversion = "unknown"
+ end
+ if not resolution or resolution == "" then
+ resolution = "unknown"
+ end
+ local hash = f_hash_full(name,page,size,color,conversion,resolution,mask)
+ local figure = figures_loaded[hash]
+ if figure == nil then
+ figure = img.new {
+ filename = name,
+ page = page,
+ pagebox = dr.size,
+ -- visiblefilename = "", -- this prohibits the full filename ending up in the file
+ }
+ codeinjections.setfigurecolorspace(data,figure)
+ codeinjections.setfiguremask(data,figure)
+ figure = figure and img.check(img.scan(figure)) or false
+ local f, d = codeinjections.setfigurealternative(data,figure)
+ figure, data = f or figure, d or data
+ figures_loaded[hash] = figure
+ if trace_conversion then
+ report_inclusion("new graphic, using hash %a",hash)
+ end
+ else
+ if trace_conversion then
+ report_inclusion("existing graphic, using hash %a",hash)
+ end
+ end
+ if figure then
+ du.width = figure.width
+ du.height = figure.height
+ du.pages = figure.pages
+ du.depth = figure.depth or 0
+ du.colordepth = figure.colordepth or 0
+ du.xresolution = figure.xres or 0
+ du.yresolution = figure.yres or 0
+ du.xsize = figure.xsize or 0
+ du.ysize = figure.ysize or 0
+ ds.private = figure
+ ds.hash = hash
+ end
+ return data
+end
+
+function includers.generic(data)
+ local dr, du, ds = data.request, data.used, data.status
+ -- here we set the 'natural dimensions'
+ dr.width = du.width
+ dr.height = du.height
+ local hash = figures.hash(data)
+ local figure = figures_used[hash]
+ -- figures.registerresource {
+ -- filename = du.fullname,
+ -- width = dr.width,
+ -- height = dr.height,
+ -- }
+ if figure == nil then
+ figure = ds.private
+ if figure then
+ figure = img.copy(figure)
+ figure = figure and img.clone(figure,data.request) or false
+ end
+ figures_used[hash] = figure
+ end
+ if figure then
+ local nr = figures.boxnumber
+ -- it looks like we have a leak in attributes here .. todo
+ local box = node.hpack(img.node(figure)) -- img.node(figure) not longer valid
+ indexed[figure.index] = figure
+ box.width, box.height, box.depth = figure.width, figure.height, 0 -- new, hm, tricky, we need to do that in tex (yet)
+ texbox[nr] = box
+ ds.objectnumber = figure.objnum
+ context.relocateexternalfigure()
+ end
+ return data
+end
+
+-- -- -- nongeneric -- -- --
+
+local function checkers_nongeneric(data,command) -- todo: macros and context.*
+ local dr, du, ds = data.request, data.used, data.status
+ local name = du.fullname or "unknown nongeneric"
+ local hash = name
+ if dr.object then
+ -- hm, bugged ... waiting for an xform interface
+ if not job.objects.get("FIG::"..hash) then
+ if type(command) == "function" then
+ command()
+ end
+ context.dosetfigureobject(hash)
+ end
+ context.doboxfigureobject(hash)
+ elseif type(command) == "function" then
+ command()
+ end
+ return data
+end
+
+local function includers_nongeneric(data)
+ return data
+end
+
+checkers.nongeneric = checkers_nongeneric
+includers.nongeneric = includers_nongeneric
+
+-- -- -- mov -- -- --
+
+function checkers.mov(data)
+ local dr, du, ds = data.request, data.used, data.status
+ local width = todimen(dr.width or figures.defaultwidth)
+ local height = todimen(dr.height or figures.defaultheight)
+ local foundname = du.fullname
+ dr.width, dr.height = width, height
+ du.width, du.height, du.foundname = width, height, foundname
+ if trace_inclusion then
+ report_inclusion("including movie %a, width %p, height %p",foundname,width,height)
+ end
+ -- we need to push the node.write in between ... we could make a shared helper for this
+ context.startfoundexternalfigure(width .. "sp",height .. "sp")
+ context(function()
+ nodeinjections.insertmovie {
+ width = width,
+ height = height,
+ factor = number.dimenfactors.bp,
+ ["repeat"] = dr["repeat"],
+ controls = dr.controls,
+ preview = dr.preview,
+ label = dr.label,
+ foundname = foundname,
+ }
+ end)
+ context.stopfoundexternalfigure()
+ return data
+end
+
+includers.mov = includers.nongeneric
+
+-- -- -- mps -- -- --
+
+internalschemes.mprun = true
+
+local function internal(askedname)
+ local spec, mprun, mpnum = match(lower(askedname),"mprun([:%.]?)(.-)%.(%d+)")
+ if spec ~= "" then
+ return mprun, mpnum
+ else
+ return "", mpnum
+ end
+end
+
+function existers.mps(askedname)
+ local mprun, mpnum = internal(askedname)
+ if mpnum then
+ return askedname
+ else
+ return existers.generic(askedname)
+ end
+end
+
+function checkers.mps(data)
+ local mprun, mpnum = internal(data.used.fullname)
+ if mpnum then
+ return checkers_nongeneric(data,function() context.docheckfiguremprun(mprun,mpnum) end)
+ else
+ return checkers_nongeneric(data,function() context.docheckfiguremps(data.used.fullname) end)
+ end
+end
+
+includers.mps = includers.nongeneric
+
+-- -- -- tex -- -- --
+
+function existers.tex(askedname)
+ askedname = resolvers.findfile(askedname)
+ return askedname ~= "" and askedname or false
+end
+
+function checkers.tex(data)
+ return checkers_nongeneric(data,function() context.docheckfiguretex(data.used.fullname) end)
+end
+
+includers.tex = includers.nongeneric
+
+-- -- -- buffer -- -- --
+
+function existers.buffer(askedname)
+ local name = file.nameonly(askedname)
+ local okay = buffers.exists(name)
+ return okay and name, true -- always quit scanning
+end
+
+function checkers.buffer(data)
+ return checkers_nongeneric(data,function() context.docheckfigurebuffer(file.nameonly(data.used.fullname)) end)
+end
+
+includers.buffers = includers.nongeneric
+
+-- -- -- auto -- -- --
+
+function existers.auto(askedname)
+ local name = gsub(askedname, ".auto$", "")
+ local format = figures.guess(name)
+ if format then
+ report_inclusion("format guess %a for %a",format,name)
+ else
+ report_inclusion("format guess for %a is not possible",name)
+ end
+ return format and name, true, format
+end
+
+checkers.auto = checkers.generic
+includers.auto = includers.generic
+
+-- -- -- cld -- -- --
+
+existers.cld = existers.tex
+
+function checkers.cld(data)
+ return checkers_nongeneric(data,function() context.docheckfigurecld(data.used.fullname) end)
+end
+
+includers.cld = includers.nongeneric
+
+-- -- -- converters -- -- --
+
+local function makeoptions(options)
+ local to = type(options)
+ return (to == "table" and concat(options," ")) or (to == "string" and options) or ""
+end
+
+-- programs.makeoptions = makeoptions
+
+local function runprogram(binary,argument,variables)
+ local binary = match(binary,"[%S]+") -- to be sure
+ if type(argument) == "table" then
+ argument = concat(argument," ") -- for old times sake
+ end
+ if not os.which(binary) then
+ report_inclusion("program %a is not installed, not running command: %s",binary,command)
+ elseif not argument or argument == "" then
+ report_inclusion("nothing to run, unknown program %a",binary)
+ else
+ local command = format([["%s" %s]],binary,replacetemplate(longtostring(argument),variables))
+ if trace_conversion or trace_programs then
+ report_inclusion("running command: %s",command)
+ end
+ os.spawn(command)
+ end
+end
+
+programs.run = runprogram
+
+-- -- -- eps & pdf -- -- --
+--
+-- \externalfigure[cow.eps]
+-- \externalfigure[cow.pdf][conversion=stripped]
+
+local epsconverter = converters.eps or { }
+converters.eps = epsconverter
+converters.ps = epsconverter
+
+local epstopdf = {
+ resolutions = {
+ [v_low] = "screen",
+ [v_medium] = "ebook",
+ [v_high] = "prepress",
+ },
+ command = os.type == "windows" and "gswin32c" or "gs",
+ -- -dProcessDSCComments=false
+ argument = [[
+ -q
+ -sDEVICE=pdfwrite
+ -dNOPAUSE
+ -dNOCACHE
+ -dBATCH
+ -dAutoRotatePages=/None
+ -dPDFSETTINGS=/%presets%
+ -dEPSCrop
+ -sOutputFile=%newname%
+ %oldname%
+ -c quit
+ ]],
+}
+
+programs.epstopdf = epstopdf
+programs.gs = epstopdf
+
+function epsconverter.pdf(oldname,newname,resolution) -- the resolution interface might change
+ local epstopdf = programs.epstopdf -- can be changed
+ local presets = epstopdf.resolutions[resolution or ""] or epstopdf.resolutions.high
+ runprogram(epstopdf.command, epstopdf.argument, {
+ newname = newname,
+ oldname = oldname,
+ presets = presets,
+ } )
+end
+
+epsconverter.default = epsconverter.pdf
+
+local pdfconverter = converters.pdf or { }
+converters.pdf = pdfconverter
+
+programs.pdftoeps = {
+ command = "pdftops",
+ argument = [[-eps "%oldname%" "%newname%]],
+}
+
+pdfconverter.stripped = function(oldname,newname)
+ local pdftoeps = programs.pdftoeps -- can be changed
+ local epstopdf = programs.epstopdf -- can be changed
+ local presets = epstopdf.resolutions[resolution or ""] or epstopdf.resolutions.high
+ local tmpname = newname .. ".tmp"
+ runprogram(pdftoeps.command, pdftoeps.argument, { oldname = oldname, newname = tmpname, presets = presets })
+ runprogram(epstopdf.command, epstopdf.argument, { oldname = tmpname, newname = newname, presets = presets })
+ os.remove(tmpname)
+end
+
+figures.registersuffix("stripped","pdf")
+
+-- -- -- svg -- -- --
+
+local svgconverter = { }
+converters.svg = svgconverter
+converters.svgz = svgconverter
+
+-- inkscape on windows only works with complete paths
+
+programs.inkscape = {
+ command = "inkscape",
+ pdfargument = [[
+ "%oldname%"
+ --export-dpi=600
+ -A
+ "%newname%"
+ ]],
+ pngargument = [[
+ "%oldname%"
+ --export-dpi=600
+ --export-png="%newname%"
+ ]],
+}
+
+function svgconverter.pdf(oldname,newname)
+ local inkscape = programs.inkscape -- can be changed
+ runprogram(inkscape.command, inkscape.pdfargument, {
+ newname = expandfilename(newname),
+ oldname = expandfilename(oldname),
+ } )
+end
+
+function svgconverter.png(oldname,newname)
+ local inkscape = programs.inkscape
+ runprogram(inkscape.command, inkscape.pngargument, {
+ newname = expandfilename(newname),
+ oldname = expandfilename(oldname),
+ } )
+end
+
+svgconverter.default = svgconverter.pdf
+
+-- -- -- gif -- -- --
+-- -- -- tif -- -- --
+
+local gifconverter = converters.gif or { }
+local tifconverter = converters.tif or { }
+local bmpconverter = converters.bmp or { }
+
+converters.gif = gifconverter
+converters.tif = tifconverter
+converters.bmp = bmpconverter
+
+programs.convert = {
+ command = "gm", -- graphicmagick
+ argument = [[convert "%oldname%" "%newname%"]],
+}
+
+local function converter(oldname,newname)
+ local convert = programs.convert
+ runprogram(convert.command, convert.argument, {
+ newname = newname,
+ oldname = oldname,
+ } )
+end
+
+tifconverter.pdf = converter
+gifconverter.pdf = converter
+bmpconverter.pdf = converter
+
+gifconverter.default = converter
+tifconverter.default = converter
+bmpconverter.default = converter
+
+-- todo: lowres
+
+-- -- -- bases -- -- --
+
+local bases = allocate()
+figures.bases = bases
+
+local bases_list = nil -- index => { basename, fullname, xmlroot }
+local bases_used = nil -- [basename] => { basename, fullname, xmlroot } -- pointer to list
+local bases_found = nil
+local bases_enabled = false
+
+local function reset()
+ bases_list = allocate()
+ bases_used = allocate()
+ bases_found = allocate()
+ bases_enabled = false
+ bases.list = bases_list
+ bases.used = bases_used
+ bases.found = bases_found
+end
+
+reset()
+
+function bases.use(basename)
+ if basename == "reset" then
+ reset()
+ else
+ basename = file.addsuffix(basename,"xml")
+ if not bases_used[basename] then
+ local t = { basename, nil, nil }
+ bases_used[basename] = t
+ bases_list[#bases_list+1] = t
+ if not bases_enabled then
+ bases_enabled = true
+ xml.registerns("rlx","http://www.pragma-ade.com/schemas/rlx") -- we should be able to do this per xml file
+ end
+ if trace_bases then
+ report_inclusion("registering base %a",basename)
+ end
+ end
+ end
+end
+
+local function bases_find(basename,askedlabel)
+ if trace_bases then
+ report_inclusion("checking for %a in base %a",askedlabel,basename)
+ end
+ basename = file.addsuffix(basename,"xml")
+ local t = bases_found[askedlabel]
+ if t == nil then
+ local base = bases_used[basename]
+ local page = 0
+ if base[2] == nil then
+ -- no yet located
+ for i=1,#figure_paths do
+ local path = figure_paths[i]
+ local xmlfile = path .. "/" .. basename
+ if io.exists(xmlfile) then
+ base[2] = xmlfile
+ base[3] = xml.load(xmlfile)
+ if trace_bases then
+ report_inclusion("base %a loaded",xmlfile)
+ end
+ break
+ end
+ end
+ end
+ t = false
+ if base[2] and base[3] then -- rlx:library
+ for e in xml.collected(base[3],"/(*:library|figurelibrary)/*:figure/*:label") do
+ page = page + 1
+ if xml.text(e) == askedlabel then
+ t = {
+ base = file.replacesuffix(base[2],"pdf"),
+ format = "pdf",
+ name = xml.text(e,"../*:file"), -- to be checked
+ page = page,
+ }
+ bases_found[askedlabel] = t
+ if trace_bases then
+ report_inclusion("figure %a found in base %a",askedlabel,base[2])
+ end
+ return t
+ end
+ end
+ if trace_bases and not t then
+ report_inclusion("figure %a not found in base %a",askedlabel,base[2])
+ end
+ end
+ end
+ return t
+end
+
+-- we can access sequential or by name
+
+local function bases_locate(askedlabel)
+ for i=1,#bases_list do
+ local entry = bases_list[i]
+ local t = bases_find(entry[1],askedlabel)
+ if t then
+ return t
+ end
+ end
+ return false
+end
+
+function identifiers.base(data)
+ if bases_enabled then
+ local dr, du, ds = data.request, data.used, data.status
+ local fbl = bases_locate(dr.name or dr.label)
+ if fbl then
+ du.page = fbl.page
+ du.format = fbl.format
+ du.fullname = fbl.base
+ ds.fullname = fbl.name
+ ds.format = fbl.format
+ ds.page = fbl.page
+ ds.status = 10
+ end
+ end
+ return data
+end
+
+bases.locate = bases_locate
+bases.find = bases_find
+
+identifiers.list = {
+ identifiers.base,
+ identifiers.default
+}
+
+-- tracing
+
+statistics.register("graphics processing time", function()
+ local nofprocessed = figures.nofprocessed
+ if nofprocessed > 0 then
+ return format("%s seconds including tex, %s processed images", statistics.elapsedtime(figures),nofprocessed)
+ else
+ return nil
+ end
+end)
+
+-- helper
+
+function figures.applyratio(width,height,w,h) -- width and height are strings and w and h are numbers
+ if not width or width == "" then
+ if not height or height == "" then
+ return figures.defaultwidth, figures.defaultheight
+ else
+ height = todimen(height)
+ if w and h then
+ return height * w/h, height
+ else
+ return figures.defaultwidth, height
+ end
+ end
+ else
+ width = todimen(width)
+ if not height or height == "" then
+ if w and h then
+ return width, width * h/w
+ else
+ return width, figures.defaultheight
+ end
+ else
+ return width, todimen(height)
+ end
+ end
+end
+
+-- example of simple plugins:
+--
+-- figures.converters.png = {
+-- png = function(oldname,newname,resolution)
+-- local command = string.format('gm convert -depth 1 "%s" "%s"',oldname,newname)
+-- logs.report(string.format("running command %s",command))
+-- os.execute(command)
+-- end,
+-- }
+
+-- local fig = figures.push { name = pdffile }
+-- figures.identify()
+-- figures.check()
+-- local nofpages = fig.used.pages
+-- figures.pop()
+
+-- interfacing
+
+commands.setfigurelookuporder = figures.setorder
|