if not modules then modules = { } end modules ['luatex-mplib'] = { version = 1.001, comment = "companion to luatex-mplib.tex", author = "Hans Hagen & Taco Hoekwater", copyright = "ConTeXt Development Team", license = "public domain", } -- This module is a stripped down version of libraries that are used by ConTeXt. It -- can be used in other macro packages and/or serve as an example. Embedding in a -- macro package is upto others and normally boils down to inputting 'supp-mpl.tex'. if metapost and metapost.version then -- Let's silently quit and make sure that no one loads it manually in -- ConTeXt. else local format, match, gsub = string.format, string.match, string.gsub local concat = table.concat local abs = math.abs local mplib = require ('mplib') local kpse = require ('kpse') -- We create a namespace and some variables to it. If a namespace is already -- defined it wil not be initialized. This permits hooking in code beforehand. -- We don't make a format automatically. After all, distributions might have -- their own preferences and normally a format (mem) file will have some -- special place in the TeX tree. Also, there can already be format files, -- different memort settings and other nasty pitfalls that we don't want to -- interfere with. If you want, you can define a function -- -- metapost.make (name,mem_name) -- -- that does the job. metapost = metapost or { } metapost.version = 1.00 metapost.showlog = metapost.showlog or false metapost.lastlog = "" -- A few helpers, taken from 'l-file.lua'. local file = file or { } function file.replacesuffix(filename, suffix) return (string.gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix end function file.stripsuffix(filename) return (string.gsub(filename,"%.[%a%d]+$","")) end -- We use the KPSE library unless a finder is already defined. local mpkpse = kpse.new("luatex","mpost") metapost.finder = metapost.finder or function(name, mode, ftype) if mode == "w" then return name else return mpkpse:find_file(name,ftype) end end -- You can use your own reported if needed, as long as it handles multiple -- arguments and formatted strings. metapost.report = metapost.report or function(...) if logs.report then logs.report("metapost",...) else texio.write(format("",format(...))) end end -- The rest of this module is not documented. More info can be found in the -- LuaTeX manual, articles in user group journals and the files that ship -- with ConTeXt. function metapost.resetlastlog() metapost.lastlog = "" end local mplibone = tonumber(mplib.version()) <= 1.50 if mplibone then metapost.make = metapost.make or function(name,mem_name,dump) local t = os.clock() local mpx = mplib.new { ini_version = true, find_file = metapost.finder, job_name = file.stripsuffix(name) } mpx:execute(string.format("input %s ;",name)) if dump then mpx:execute("dump ;") metapost.report("format %s made and dumped for %s in %0.3f seconds",mem_name,name,os.clock()-t) else metapost.report("%s read in %0.3f seconds",name,os.clock()-t) end return mpx end function metapost.load(name) local mem_name = file.replacesuffix(name,"mem") local mpx = mplib.new { ini_version = false, mem_name = mem_name, find_file = metapost.finder } if not mpx and type(metapost.make) == "function" then -- when i have time i'll locate the format and dump mpx = metapost.make(name,mem_name) end if mpx then metapost.report("using format %s",mem_name,false) return mpx, nil else return nil, { status = 99, error = "out of memory or invalid format" } end end else local preamble = [[ boolean mplib ; mplib := true ; let dump = endinput ; input %s ; ]] metapost.make = metapost.make or function() end local template = [[ \pdfoutput=1 \pdfpkresolution600 \pdfcompresslevel=9 %s\relax \hsize=100in \vsize=\hsize \hoffset=-1in \voffset=\hoffset \topskip=0pt \setbox0=\hbox{%s}\relax \pageheight=\ht0 \pagewidth=\wd0 \box0 \bye ]] metapost.texrunner = "mtxrun --script plain" local texruns = 0 -- per document local texhash = { } -- per document function metapost.maketext(mpd,str,what) -- inefficient but one can always use metafun .. it's more a test -- feature local verbatimtex = mpd.verbatimtex if not verbatimtex then verbatimtex = { } mpd.verbatimtex = verbatimtex end if what == 1 then table.insert(verbatimtex,str) else local texcode = format(template,concat(verbatimtex,"\n"),str) local texdone = texhash[texcode] local jobname = tex.jobname if not texdone then texruns = texruns + 1 texdone = texruns texhash[texcode] = texdone local texname = format("%s-mplib-%s.tmp",jobname,texdone) local logname = format("%s-mplib-%s.log",jobname,texdone) local pdfname = format("%s-mplib-%s.pdf",jobname,texdone) io.savedata(texname,texcode) os.execute(format("%s %s",metapost.texrunner,texname)) os.remove(texname) os.remove(logname) end return format('"image::%s-mplib-%s.pdf" infont defaultfont',jobname,texdone) end end local function mpprint(buffer,...) for i=1,select("#",...) do local value = select(i,...) if value ~= nil then local t = type(value) if t == "number" then buffer[#buffer+1] = format("%.16f",value) elseif t == "string" then buffer[#buffer+1] = value elseif t == "table" then buffer[#buffer+1] = "(" .. concat(value,",") .. ")" else -- boolean or whatever buffer[#buffer+1] = tostring(value) end end end end function metapost.runscript(mpd,code) local code = loadstring(code) if type(code) == "function" then local buffer = { } function metapost.print(...) mpprint(buffer,...) end code() -- mpd.buffer = buffer -- for tracing return concat(buffer,"") end return "" end local modes = { scaled = true, decimal = true, binary = true, double = true, } function metapost.load(name,mode) local mpd = { buffer = { }, verbatim = { } } local mpx = mplib.new { ini_version = true, find_file = metapost.finder, make_text = function(...) return metapost.maketext (mpd,...) end, run_script = function(...) return metapost.runscript(mpd,...) end, extensions = 1, math_mode = mode and modes[mode] and mode or "scaled", } local result if not mpx then result = { status = 99, error = "out of memory"} else result = mpx:execute(format(preamble, file.replacesuffix(name,"mp"))) end metapost.reporterror(result) return mpx, result end end function metapost.unload(mpx) if mpx then mpx:finish() end end function metapost.reporterror(result) if not result then metapost.report("mp error: no result object returned") elseif result.status > 0 then local t, e, l = result.term, result.error, result.log if t then metapost.report("mp terminal: %s",t) end if e then metapost.report("mp error: %s", e) end if not t and not e and l then metapost.lastlog = metapost.lastlog .. "\n " .. l metapost.report("mp log: %s",l) else metapost.report("mp error: unknown, no error, terminal or log messages") end else return false end return true end function metapost.process(format,data,mode) local converted, result = false, {} local mpx = metapost.load(format,mode) if mpx and data then local result = mpx:execute(data) if not result then metapost.report("mp error: no result object returned") elseif result.status > 0 then metapost.report("mp error: %s",(result.term or "no-term") .. "\n" .. (result.error or "no-error")) elseif metapost.showlog then metapost.lastlog = metapost.lastlog .. "\n" .. result.term metapost.report("mp info: %s",result.term or "no-term") elseif result.fig then converted = metapost.convert(result) else metapost.report("mp error: unknown error, maybe no beginfig/endfig") end -- mpx:finish() -- mpx = nil else metapost.report("mp error: mem file not found") end return converted, result end local function getobjects(result,figure,f) return figure:objects() end function metapost.convert(result,flusher) metapost.flush(result,flusher) return true -- done end -- We removed some message and tracing code. We might even remove the -- flusher. local function pdf_startfigure(n,llx,lly,urx,ury) tex.sprint(format("\\startMPLIBtoPDF{%s}{%s}{%s}{%s}",llx,lly,urx,ury)) end local function pdf_stopfigure() tex.sprint("\\stopMPLIBtoPDF") end function pdf_literalcode(fmt,...) -- table tex.sprint(format("\\MPLIBtoPDF{%s}",format(fmt,...))) end function pdf_textfigure(font,size,text,width,height,depth) local how, what = match(text,"^(.-)::(.+)$") if how == "image" then tex.sprint(format("\\MPLIBpdftext{%s}{%s}",what,depth)) else -- text = gsub(text,".","\\hbox{%1}") -- kerning happens in metapost -- tex.sprint(format("\\MPLIBtextext{%s}{%s}{%s}{%s}",font,size,text,depth)) tex.sprint(format("\\MPLIBtextext{%s}{%s}{\\hpack{\\detokenize{%s}}}{%s}",font,size,text,depth)) end end local bend_tolerance = 131/65536 local rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1 local function pen_characteristics(object) local t = mplib.pen_info(object) rx, ry, sx, sy, tx, ty = t.rx, t.ry, t.sx, t.sy, t.tx, t.ty divider = sx*sy - rx*ry return not (sx==1 and rx==0 and ry==0 and sy==1 and tx==0 and ty==0), t.width end local function concatinated(px, py) -- no tx, ty here return (sy*px-ry*py)/divider,(sx*py-rx*px)/divider end local function curved(ith,pth) local d = pth.left_x - ith.right_x if abs(ith.right_x - ith.x_coord - d) <= bend_tolerance and abs(pth.x_coord - pth.left_x - d) <= bend_tolerance then d = pth.left_y - ith.right_y if abs(ith.right_y - ith.y_coord - d) <= bend_tolerance and abs(pth.y_coord - pth.left_y - d) <= bend_tolerance then return false end end return true end local function flushnormalpath(path,open) local pth, ith for i=1,#path do pth = path[i] if not ith then pdf_literalcode("%f %f m",pth.x_coord,pth.y_coord) elseif curved(ith,pth) then pdf_literalcode("%f %f %f %f %f %f c",ith.right_x,ith.right_y,pth.left_x,pth.left_y,pth.x_coord,pth.y_coord) else pdf_literalcode("%f %f l",pth.x_coord,pth.y_coord) end ith = pth end if not open then local one = path[1] if curved(pth,one) then pdf_literalcode("%f %f %f %f %f %f c",pth.right_x,pth.right_y,one.left_x,one.left_y,one.x_coord,one.y_coord ) else pdf_literalcode("%f %f l",one.x_coord,one.y_coord) end elseif #path == 1 then -- special case .. draw point local one = path[1] pdf_literalcode("%f %f l",one.x_coord,one.y_coord) end return t end local function flushconcatpath(path,open) pdf_literalcode("%f %f %f %f %f %f cm", sx, rx, ry, sy, tx ,ty) local pth, ith for i=1,#path do pth = path[i] if not ith then pdf_literalcode("%f %f m",concatinated(pth.x_coord,pth.y_coord)) elseif curved(ith,pth) then local a, b = concatinated(ith.right_x,ith.right_y) local c, d = concatinated(pth.left_x,pth.left_y) pdf_literalcode("%f %f %f %f %f %f c",a,b,c,d,concatinated(pth.x_coord, pth.y_coord)) else pdf_literalcode("%f %f l",concatinated(pth.x_coord, pth.y_coord)) end ith = pth end if not open then local one = path[1] if curved(pth,one) then local a, b = concatinated(pth.right_x,pth.right_y) local c, d = concatinated(one.left_x,one.left_y) pdf_literalcode("%f %f %f %f %f %f c",a,b,c,d,concatinated(one.x_coord, one.y_coord)) else pdf_literalcode("%f %f l",concatinated(one.x_coord,one.y_coord)) end elseif #path == 1 then -- special case .. draw point local one = path[1] pdf_literalcode("%f %f l",concatinated(one.x_coord,one.y_coord)) end return t end -- Support for specials has been removed. function metapost.flush(result,flusher) if result then local figures = result.fig if figures then for f=1, #figures do metapost.report("flushing figure %s",f) local figure = figures[f] local objects = getobjects(result,figure,f) local fignum = tonumber(match(figure:filename(),"([%d]+)$") or figure:charcode() or 0) local miterlimit, linecap, linejoin, dashed = -1, -1, -1, false local bbox = figure:boundingbox() local llx, lly, urx, ury = bbox[1], bbox[2], bbox[3], bbox[4] -- faster than unpack if urx < llx then -- invalid pdf_startfigure(fignum,0,0,0,0) pdf_stopfigure() else pdf_startfigure(fignum,llx,lly,urx,ury) pdf_literalcode("q") if objects then local savedpath = nil local savedhtap = nil for o=1,#objects do local object = objects[o] local objecttype = object.type if objecttype == "start_bounds" or objecttype == "stop_bounds" then -- skip elseif objecttype == "start_clip" then local evenodd = not object.istext and object.postscript == "evenodd" pdf_literalcode("q") flushnormalpath(object.path,t,false) pdf_literalcode("W n") pdf_literalcode(evenodd and "W* n" or "W n") elseif objecttype == "stop_clip" then pdf_literalcode("Q") miterlimit, linecap, linejoin, dashed = -1, -1, -1, false elseif objecttype == "special" then -- not supported elseif objecttype == "text" then local ot = object.transform -- 3,4,5,6,1,2 pdf_literalcode("q %f %f %f %f %f %f cm",ot[3],ot[4],ot[5],ot[6],ot[1],ot[2]) pdf_textfigure(object.font,object.dsize,object.text,object.width,object.height,object.depth) pdf_literalcode("Q") else local evenodd, collect, both = false, false, false local postscript = object.postscript if not object.istext then if postscript == "evenodd" then evenodd = true elseif postscript == "collect" then collect = true elseif postscript == "both" then both = true elseif postscript == "eoboth" then evenodd = true both = true end end if collect then if not savedpath then savedpath = { object.path or false } savedhtap = { object.htap or false } else savedpath[#savedpath+1] = object.path or false savedhtap[#savedhtap+1] = object.htap or false end else local cs = object.color local cr = false if cs and #cs > 0 then cs, cr = metapost.colorconverter(cs) pdf_literalcode(cs) end local ml = object.miterlimit if ml and ml ~= miterlimit then miterlimit = ml pdf_literalcode("%f M",ml) end local lj = object.linejoin if lj and lj ~= linejoin then linejoin = lj pdf_literalcode("%i j",lj) end local lc = object.linecap if lc and lc ~= linecap then linecap = lc pdf_literalcode("%i J",lc) end local dl = object.dash if dl then local d = format("[%s] %i d",concat(dl.dashes or {}," "),dl.offset) if d ~= dashed then dashed = d pdf_literalcode(dashed) end elseif dashed then pdf_literalcode("[] 0 d") dashed = false end local path = object.path local transformed, penwidth = false, 1 local open = path and path[1].left_type and path[#path].right_type local pen = object.pen if pen then if pen.type == 'elliptical' then transformed, penwidth = pen_characteristics(object) -- boolean, value pdf_literalcode("%f w",penwidth) if objecttype == 'fill' then objecttype = 'both' end else -- calculated by mplib itself objecttype = 'fill' end end if transformed then pdf_literalcode("q") end if path then if savedpath then for i=1,#savedpath do local path = savedpath[i] if transformed then flushconcatpath(path,open) else flushnormalpath(path,open) end end savedpath = nil end if transformed then flushconcatpath(path,open) else flushnormalpath(path,open) end if objecttype == "fill" then pdf_literalcode("h f") elseif objecttype == "outline" then if both then pdf_literalcode(evenodd and "h B*" or "h B") else pdf_literalcode(open and "S" or "h S") end elseif objecttype == "both" then pdf_literalcode(evenodd and "h B*" or "h B") end end if transformed then pdf_literalcode("Q") end local path = object.htap if path then if transformed then pdf_literalcode("q") end if savedhtap then for i=1,#savedhtap do local path = savedhtap[i] if transformed then flushconcatpath(path,open) else flushnormalpath(path,open) end end savedhtap = nil evenodd = true end if transformed then flushconcatpath(path,open) else flushnormalpath(path,open) end if objecttype == "fill" then pdf_literalcode("h f") elseif objecttype == "outline" then pdf_literalcode(evenodd and "h f*" or "h f") elseif objecttype == "both" then pdf_literalcode(evenodd and "h B*" or "h B") end if transformed then pdf_literalcode("Q") end end if cr then pdf_literalcode(cr) end end end end end pdf_literalcode("Q") pdf_stopfigure() end end end end end function metapost.colorconverter(cr) local n = #cr if n == 4 then local c, m, y, k = cr[1], cr[2], cr[3], cr[4] return format("%.3f %.3f %.3f %.3f k %.3f %.3f %.3f %.3f K",c,m,y,k,c,m,y,k), "0 g 0 G" elseif n == 3 then local r, g, b = cr[1], cr[2], cr[3] return format("%.3f %.3f %.3f rg %.3f %.3f %.3f RG",r,g,b,r,g,b), "0 g 0 G" else local s = cr[1] return format("%.3f g %.3f G",s,s), "0 g 0 G" end end end