if not modules then modules = { } end modules ['mlib-run'] = { version = 1.001, comment = "companion to mlib-ctx.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files", } -- The directional helpers and pen analysis are more or less translated from the C -- code. In LuaTeX we spent quite some time on speeding up the Lua interface as well -- as the C code. There is not much to gain, especially if one keeps in mind that -- when integrated in TeX only a part of the time is spent in MetaPost. Of course an -- integrated approach is way faster than an external MetaPost and processing time -- nears zero. -- -- In LuaMetaTeX the MetaPost core has been cleaned up a it and as a result -- processing in double mode is now faster than in scaled mode. There are also extra -- features and interfaces, so the MkIV and MkXL (LMTX) implementation differ! local type, tostring, tonumber, next = type, tostring, tonumber, next local find, striplines = string.find, utilities.strings.striplines local concat, insert, remove, sortedkeys = table.concat, table.insert, table.remove, table.sortedkeys local emptystring = string.is_empty local trace_graphics = false trackers.register("metapost.graphics", function(v) trace_graphics = v end) local trace_tracingall = false trackers.register("metapost.tracingall", function(v) trace_tracingall = v end) local texerrormessage = logs.texerrormessage local report_metapost = logs.reporter("metapost") local report_terminal = logs.reporter("metapost","terminal") local report_tracer = logs.reporter("metapost","trace") local report_error = logs.reporter("metapost","error") local starttiming = statistics.starttiming local stoptiming = statistics.stoptiming local formatters = string.formatters local mplib = mplib metapost = metapost or { } local metapost = metapost metapost.showlog = false metapost.lastlog = "" metapost.texerrors = false metapost.exectime = metapost.exectime or { } -- hack metapost.nofruns = 0 local mpxformats = { } local nofformats = 0 local mpxpreambles = { } local mpxterminals = { } local mpxextradata = { } -- The flatten hack is needed because the library currently barks on \n\n and the -- collapse because mp cannot handle snippets due to grouping issues. -- todo: pass tables to executempx instead of preparing beforehand, -- as it's more efficient for the terminal local function flatten(source,target) for i=1,#source do local d = source[i] if type(d) == "table" then flatten(d,target) elseif d and d ~= "" then target[#target+1] = d end end return target end local function prepareddata(data) if data and data ~= "" then if type(data) == "table" then data = flatten(data,{ }) data = #data > 1 and concat(data,"\n") or data[1] end return data end end local execute = mplib.execute local function executempx(mpx,data) local terminal = mpxterminals[mpx] if terminal then terminal.writer(data) data = nil elseif type(data) == "table" then data = prepareddata(data,collapse) end metapost.nofruns = metapost.nofruns + 1 local result = execute(mpx,data) return result end directives.register("mplib.texerrors", function(v) metapost.texerrors = v end) trackers.register ("metapost.showlog", function(v) metapost.showlog = v end) function metapost.resetlastlog() metapost.lastlog = "" end local new_instance = mplib.new local find_file = mplib.finder function metapost.reporterror(result) if not result then report_metapost("error: no result object returned") return true elseif result.status == 0 then return false elseif mplib.realtimelogging then return false -- we already reported else local t = result.term local e = result.error local l = result.log local report = metapost.texerrors and texerrormessage or report_metapost if t and t ~= "" then report("mp error: %s",striplines(t)) end if e == "" or e == "no-error" then e = nil end if e then report("mp error: %s",striplines(e)) end if not t and not e and l then metapost.lastlog = metapost.lastlog .. "\n" .. l report_metapost("log: %s",l) else report_metapost("error: unknown, no error, terminal or log messages") end return true end end local f_preamble = formatters [ [[ boolean mplib ; mplib := true ; let dump = endinput ; input "%s" ; randomseed:=%s; ]] ] local methods = { double = "double", scaled = "scaled", -- binary = "binary", binary = "double", decimal = "decimal", posit = "posit", default = "scaled", } function metapost.runscript(code) return "" end function metapost.scripterror(str) report_metapost("script error: %s",str) end -- todo: random_seed local seed = nil local default_tolerance = 131/65536.0 -- a little below 0.001 * 0x7FFF/0x4000 local bend_tolerance = default_tolerance local move_tolerance = default_tolerance ----- bend_tolerance = 10/2000 ----- move_tolerance = bend_tolerance function metapost.setbendtolerance(t) bend_tolerance = t or default_tolerance end function metapost.setmovetolerance(t) move_tolerance = t or default_tolerance end function metapost.settolerance(t) bend_tolerance = t or default_tolerance move_tolerance = t or default_tolerance end function metapost.getbendtolerance() return bend_tolerance end function metapost.getmovetolerance() return move_tolerance end function metapost.gettolerance(t) return bend_tolerance, move_tolerance end function metapost.load(name,method) starttiming(mplib) if not seed then seed = job.getrandomseed() if seed <= 1 then seed = seed % 1000 elseif seed > 4095 then seed = seed % 4096 end end method = method and methods[method] or "scaled" local mpx, terminal = new_instance { bendtolerance = bend_tolerance, movetolerance = move_tolerance, mathmode = method, runscript = metapost.runscript, runinternal = metapost.runinternal, scripterror = metapost.scripterror, maketext = metapost.maketext, handlers = { log = metapost.newlogger(), -- warning = function(...) end, -- error = function(...) end, }, } report_metapost("initializing number mode %a",method) local result if not mpx then result = { status = 99, error = "out of memory"} else mpxterminals[mpx] = terminal -- pushing permits advanced features metapost.pushscriptrunner(mpx) result = executempx(mpx,f_preamble(file.addsuffix(name,"mp"),seed)) metapost.popscriptrunner() end stoptiming(mplib) metapost.reporterror(result) return mpx, result end function metapost.checkformat(mpsinput,method) local mpsinput = mpsinput or "metafun" local foundfile = "" if file.suffix(mpsinput) ~= "" then foundfile = find_file(mpsinput) or "" end -- if foundfile == "" then -- foundfile = find_file(file.replacesuffix(mpsinput,"mpvi")) or "" -- end if foundfile == "" then foundfile = find_file(file.replacesuffix(mpsinput,"mpxl")) or "" end if foundfile == "" then foundfile = find_file(file.replacesuffix(mpsinput,"mpiv")) or "" end if foundfile == "" then foundfile = find_file(file.replacesuffix(mpsinput,"mp")) or "" end if foundfile == "" then report_metapost("loading %a fails, format not found",mpsinput) else report_metapost("loading %a as %a using method %a",mpsinput,foundfile,method or "default") local mpx, result = metapost.load(foundfile,method) if mpx then return mpx else report_metapost("error in loading %a",mpsinput) metapost.reporterror(result) end end end function metapost.unload(mpx) starttiming(mplib) if mpx then mpx:finish() end stoptiming(mplib) end metapost.defaultformat = "metafun" metapost.defaultinstance = "metafun" metapost.defaultmethod = "default" function metapost.getextradata(mpx) return mpxextradata[mpx] end function metapost.pushformat(specification,f,m) -- was: instance, name, method if type(specification) ~= "table" then specification = { instance = specification, format = f, method = m, } end local instance = specification.instance local format = specification.format local method = specification.method local definitions = specification.definitions local extensions = specification.extensions local preamble = nil if not instance or instance == "" then instance = metapost.defaultinstance specification.instance = instance end if not format or format == "" then format = metapost.defaultformat specification.format = format end if not method or method == "" then method = metapost.defaultmethod specification.method = method end if definitions and definitions ~= "" then preamble = definitions end if extensions and extensions ~= "" then if preamble then preamble = preamble .. "\n" .. extensions else preamble = extensions end end nofformats = nofformats + 1 local usedinstance = instance .. ":" .. nofformats local mpx = mpxformats [usedinstance] local mpp = mpxpreambles[instance] or "" -- report_metapost("push instance %a (%S)",usedinstance,mpx) if preamble then preamble = prepareddata(preamble) mpp = mpp .. "\n" .. preamble mpxpreambles[instance] = mpp end if not mpx then report_metapost("initializing instance %a using format %a and method %a",usedinstance,format,method) mpx = metapost.checkformat(format,method) mpxformats [usedinstance] = mpx mpxextradata[mpx] = { } if mpp ~= "" then preamble = mpp end end if preamble then metapost.pushscriptrunner(mpx) executempx(mpx,preamble) metapost.popscriptrunner() end specification.mpx = mpx return mpx end -- luatex.wrapup(function() -- for k, mpx in next, mpxformats do -- mpx:finish() -- end -- end) function metapost.popformat() nofformats = nofformats - 1 end function metapost.reset(mpx) if not mpx then -- nothing elseif type(mpx) == "string" then if mpxformats[mpx] then local instance = mpxformats[mpx] instance:finish() mpxterminals[mpx] = nil mpxextradata[mpx] = nil mpxformats [mpx] = nil end else for name, instance in next, mpxformats do if instance == mpx then mpx:finish() mpxterminals[mpx] = nil mpxextradata[mpx] = nil mpxformats [mpx] = nil break end end end end if not metapost.process then function metapost.process(specification) metapost.run(specification) end end -- run, process, convert and flush all work with a specification with the -- following (often optional) fields -- -- mpx string or mp object -- data string or table of strings -- flusher table with flush methods -- askedfig string ("all" etc) or number -- incontext boolean -- plugmode boolean do local function makebeginbanner(specification) return formatters["%% begin graphic: n=%s\n\n"](metapost.n) end local function makeendbanner(specification) return "\n% end graphic\n\n" end -- This is somewhat complex. We want a logger that is bound to an instance and -- we implement the rest elsewhere so we need some hook. When we decide to move -- the mlib-fio code here we can avoid some of the fuzzyness. -- In the luatex lib we have log and error an dterm fields, but here we don't -- because we handle that ourselves. -- mplib.realtimelogging = false local mp_tra = { } local mp_tag = 0 local stack = { } local logger = false local logging = true local function pushlogger(mpx,tra) insert(stack,logger) logger = tra or false end local function poplogger(mpx) logger = remove(stack) or false end function metapost.checktracingonline(n) -- todo end function metapost.setlogging(state) logging = state end function metapost.newlogger() -- In a traditional scenario there are three states: terminal, log as well -- as both. The overhead of logging is large because metapost flushes each -- character (maybe that should be improved but caching at the libs end also -- has price, probably more than delegating to LUA). -- term=1 log=2 term+log =3 local l, nl, dl = { }, 0, false return function(target,str) if not logging then return elseif target == 4 then report_error(str) else if logger and (target == 2 or target == 3) then logger:write(str) end if target == 1 or target == 3 then if str == "\n" then mplib.realtimelogging = true if nl > 0 then report_tracer(concat(l,"",1,nl)) nl, dl = 0, false elseif not dl then report_tracer("") dl = true end else nl = nl + 1 l[nl] = str end end end end end function metapost.run(specification) local mpx = specification.mpx local data = specification.data local converted = false local result = { } local mpxdone = type(mpx) == "string" if mpxdone then mpx = metapost.pushformat { instance = mpx, format = mpx } end if mpx and data then local tra = false starttiming(metapost) -- why not at the outer level ... metapost.variables = { } -- todo also push / pop metapost.pushscriptrunner(mpx) if trace_graphics then tra = mp_tra[mpx] if not tra then mp_tag = mp_tag + 1 local jobname = tex.jobname tra = { inp = io.open(formatters["%s-mplib-run-%03i.mp"] (jobname,mp_tag),"w"), log = io.open(formatters["%s-mplib-run-%03i.log"](jobname,mp_tag),"w"), } mp_tra[mpx] = tra end local banner = makebeginbanner(specification) tra.inp:write(banner) tra.log:write(banner) pushlogger(mpx,tra and tra.log) else pushlogger(mpx,false) end if trace_tracingall then executempx(mpx,"tracingall;") end -- if data then if trace_graphics then if type(data) == "table" then for i=1,#data do tra.inp:write(data[i]) end else tra.inp:write(data) end end starttiming(metapost.exectime) result = executempx(mpx,data) stoptiming(metapost.exectime) if not metapost.reporterror(result) and result.fig then converted = metapost.convert(specification,result) end else report_metapost("error: invalid graphic") end -- if trace_graphics then local banner = makeendbanner(specification) tra.inp:write(banner) tra.log:write(banner) end stoptiming(metapost) poplogger() metapost.popscriptrunner() end if mpxdone then metapost.popformat() end return converted, result end end if not metapost.convert then function metapost.convert() report_metapost('warning: no converter set') end end function metapost.directrun(formatname,filename,outputformat,astable,mpdata) report_metapost("producing postscript and svg is no longer supported") end do local result = { } local width = 0 local height = 0 local depth = 0 local bbox = { 0, 0, 0, 0 } local flusher = { startfigure = function(n,llx,lly,urx,ury) result = { } width = urx - llx height = ury depth = -lly bbox = { llx, lly, urx, ury } end, flushfigure = function(t) local r = #result for i=1,#t do r = r + 1 result[r] = t[i] end end, stopfigure = function() end, } -- make table variant: function metapost.simple(instance,code,useextensions,dontwrap) -- can we pickup the instance ? local mpx = metapost.pushformat { instance = instance or "simplefun", format = "metafun", -- or: minifun method = "double", } metapost.process { mpx = mpx, flusher = flusher, askedfig = 1, useplugins = useextensions, data = dontwrap and { code } or { "beginfig(1);", code, "endfig;" }, incontext = false, } metapost.popformat() if result then local stream = concat(result," ") result = { } -- nil -- cleanup .. weird, we can have a dangling q return stream, width, height, depth, bbox else return "", 0, 0, 0, { 0, 0, 0, 0 } end end end local getstatistics = mplib.getstatistics function metapost.getstatistics(memonly) if memonly then local n, m = 0, 0 for name, mpx in next, mpxformats do n = n + 1 m = m + getstatistics(mpx).memory end return n, m else local t = { } for name, mpx in next, mpxformats do t[name] = getstatistics(mpx) end return t end end local gethashentries = mplib.gethashentries function metapost.gethashentries(name,full) if name then local mpx = mpxformats[name] or mpxformats[name .. ":1"] if mpx then return gethashentries(mpx,full) end else local t = { } for name, mpx in next, mpxformats do t[name] = gethashtokens(mpx,full) end return t end end local gethashtokens = mplib.gethashtokens function metapost.getinstancenames() return sortedkeys(mpxformats) end