if not modules then modules = { } end modules ['mlib-pdf'] = { 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", } local gsub = string.gsub local concat, insert, remove, sortedkeys = table.concat, table.insert, table.remove, table.sortedkeys local sqrt, round = math.sqrt, math.round local setmetatable, rawset, tostring, tonumber, type = setmetatable, rawset, tostring, tonumber, type local P, S, C, Ct, Cc, Cg, Cf, Carg = lpeg.P, lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cf, lpeg.Carg local lpegmatch = lpeg.match local formatters = string.formatters local report_metapost = logs.reporter("metapost") local trace_variables = false trackers.register("metapost.variables",function(v) trace_variables = v end) local mplib = mplib local context = context local allocate = utilities.storage.allocate local peninfo = mplib.peninfo local getfields = mplib.getfields or mplib.fields -- todo: in lmtx get them once and then use gettype local save_table = false local force_stroke = false metapost = metapost or { } local metapost = metapost metapost.flushers = metapost.flushers or { } local pdfflusher = { } metapost.flushers.pdf = pdfflusher metapost.n = 0 local mpsliteral = nodes.pool.originliteral local f_f = formatters["%.6N"] local f_m = formatters["%.6N %.6N m"] local f_c = formatters["%.6N %.6N %.6N %.6N %.6N %.6N c"] local f_l = formatters["%.6N %.6N l"] local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"] local f_M = formatters["%.6N M"] local f_j = formatters["%i j"] local f_J = formatters["%i J"] local f_d = formatters["[%s] %.6N d"] local f_w = formatters["%.6N w"] local f_r = formatters["%.6N %.6N %.6N %.6N re"] directives.register("metapost.savetable",function(v) if type(v) == "string" then save_table = file.addsuffix(v,"mpl") elseif v then save_table = file.addsuffix(environment.jobname .. "-graphic","mpl") else save_table = false end end) trackers.register("metapost.forcestroke",function(v) force_stroke = v end) -- local function gettolerance(objects) -- return objects:tolerance() -- end function metapost.convert(specification,result) local flusher = specification.flusher local askedfig = specification.askedfig if save_table then table.save(save_table,metapost.totable(result,1)) -- direct end metapost.flush(specification,result) return true -- done end function pdfflusher.tocomment(message) if message then return formatters["%% mps graphic %s: %s"](metapost.n,message) else return formatters["%% mps graphic %s"](metapost.n) end end function pdfflusher.startfigure(n,llx,lly,urx,ury,message,figure,plugmode) metapost.n = metapost.n + 1 context.startMPLIBtoPDF(f_f(llx),f_f(lly),f_f(urx),f_f(ury),plugmode and "1" or "0") end function pdfflusher.stopfigure(message) context.stopMPLIBtoPDF() end function pdfflusher.flushfigure(pdfliterals) -- table if #pdfliterals > 0 then pdfliterals = concat(pdfliterals,"\n") context(mpsliteral(pdfliterals)) end end function pdfflusher.textfigure(font,size,text,width,height,depth) -- we could save the factor text = gsub(text,".","\\hbox{%1}") -- kerning happens in metapost (i have to check if this is true for mplib) context.MPtextext(font,size,text,0,-number.dimenfactors.bp*depth) end local rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1 local function pen_characteristics(object) local t = peninfo(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 mpconcat(px, py) -- no tx, ty here / we can move this one inline if needed return (sy*px-ry*py)/divider,(sx*py-rx*px)/divider end local getbendtolerance = metapost.getbendtolerance local curved = metapost.hascurvature local function flushnormalpath(path,t,open,tolerance) local pth, ith, nt local length = #path if t then nt = #t else t = { } nt = 0 end if length == 4 and path.cycle then local p1 = path[1] local p2 = path[2] local p3 = path[3] local p4 = path[4] if not p1.curved and not p2.curved and not p3.curved and not p4.curved then local p1x = p1.x_coord local p1y = p1.y_coord local p3x = p3.x_coord local p3y = p3.y_coord if p1y == p2.y_coord and p1x == p4.x_coord and p2.x_coord == p3x and p3y == p4.y_coord then nt = nt + 1 t[nt] = f_r(p1.x_coord,p1y,p3x-p1x,p3y-p1y) path.rectangle = true return end end end for i=1,length do nt = nt + 1 pth = path[i] if not ith or pth.state == 1 then t[nt] = f_m(pth.x_coord,pth.y_coord) -- elseif curved(ith,pth,tolerance) then elseif pth.curved then t[nt] = f_c(ith.right_x,ith.right_y,pth.left_x,pth.left_y,pth.x_coord,pth.y_coord) else t[nt] = f_l(pth.x_coord,pth.y_coord) end ith = pth end if not open then nt = nt + 1 local one = path[1] local len = path[length] if len.state == 2 then -- special case (potrace-001.tex) elseif one.state == 1 then t[nt] = f_m(one.x_coord,one.y_coord) -- elseif curved(pth,one,tolerance) then elseif one.curved then t[nt] = f_c(pth.right_x,pth.right_y,one.left_x,one.left_y,one.x_coord,one.y_coord) else t[nt] = f_l(one.x_coord,one.y_coord) end elseif length == 1 then -- special case .. draw point local one = path[1] nt = nt + 1 t[nt] = f_l(one.x_coord,one.y_coord) end return t end local function flushconcatpath(path,t,open,tolerance,transform) local pth, ith, nt local length = #path if t then nt = #t else t = { } nt = 0 end if transform then nt = nt + 1 t[nt] = f_cm(sx,rx,ry,sy,tx,ty) end for i=1,length do nt = nt + 1 pth = path[i] if not ith or pth.state == 1 then t[nt] = f_m(mpconcat(pth.x_coord,pth.y_coord)) -- elseif curved(ith,pth,tolerance) then elseif pth.curved then local a, b = mpconcat(ith.right_x,ith.right_y) local c, d = mpconcat(pth.left_x,pth.left_y) t[nt] = f_c(a,b,c,d,mpconcat(pth.x_coord,pth.y_coord)) else t[nt] = f_l(mpconcat(pth.x_coord, pth.y_coord)) end ith = pth end if not open then nt = nt + 1 local one = path[1] local len = path[length] if len.state == 2 then -- special case (potrace-001.tex) elseif one.state == 1 then t[nt] = f_m(one.x_coord,one.y_coord) -- elseif curved(pth,one,tolerance) then elseif one.curved then local a, b = mpconcat(pth.right_x,pth.right_y) local c, d = mpconcat(one.left_x,one.left_y) t[nt] = f_c(a,b,c,d,mpconcat(one.x_coord, one.y_coord)) else t[nt] = f_l(mpconcat(one.x_coord,one.y_coord)) end elseif length == 1 then -- special case .. draw point nt = nt + 1 local one = path[1] t[nt] = f_l(mpconcat(one.x_coord,one.y_coord)) end return t end local function toboundingbox(path) local size = #path if size == 4 then local pth = path[1] local x = pth.x_coord local y = pth.y_coord local llx, lly, urx, ury = x, y, x, y for i=2,size do pth = path[i] x = pth.x_coord y = pth.y_coord if x < llx then llx = x elseif x > urx then urx = x end if y < lly then lly = y elseif y > ury then ury = y end end return { llx, lly, urx, ury } else return { 0, 0, 0, 0 } end end function metapost.flushnormalpath(path, t, open, tolerance) return flushnormalpath(path, t, open, tolerance or getbendtolerance()) end -- The flusher is pdf based, if another backend is used, we need to overload the -- flusher; this is beta code, the organization will change (already upgraded in -- sync with mplib) -- -- We can avoid the before table but I like symmetry. There is of course a small -- performance penalty, but so is passing extra arguments (result, flusher, after) -- and returning stuff. -- -- This variable stuff will change in lmtx. local ignore = function() end local space = P(" ") local equal = P("=") local key = C((1-equal)^1) * equal local newline = S("\n\r")^1 local number = (((1-space-newline)^1) / tonumber) * (space^0) local p_number = number local p_string = C((1-newline)^0) local p_boolean = P("false") * Cc(false) + P("true") * Cc(true) local p_set = Ct(number^1) local p_path = Ct(Ct(number * number^-5)^1) local variable = P("1:") * p_number + P("2:") * p_string + P("3:") * p_boolean + S("4568") * P(":") * p_set + P("7:") * p_path local pattern_tab = Cf ( Carg(1) * (Cg(variable * newline^0)^0), rawset) local variable = P("1:") * p_number + P("2:") * p_string + P("3:") * p_boolean + S("4568") * P(":") * number^1 + P("7:") * (number * number^-5)^1 local pattern_lst = (variable * newline^0)^0 metapost.variables = { } -- currently across instances metapost.properties = { } -- to be stacked function metapost.untagvariable(str,variables) -- will be redone if variables == false then return lpegmatch(pattern_lst,str) else return lpegmatch(pattern_tab,str,1,variables or { }) end end -- function metapost.processspecial(str) -- lpegmatch(pattern_key,object.prescript,1,variables) -- end -- function metapost.processspecial(str) -- local code = loadstring(str) -- if code then -- if trace_variables then -- report_metapost("executing special code: %s",str) -- end -- code() -- else -- report_metapost("invalid special code: %s",str) -- end -- end local stack = { } -- general stack (not related to stacking) local nostacking = { 0 } -- layers in figures local function pushproperties(figure) -- maybe there will be getters in lmtx local boundingbox = figure:boundingbox() local slot = figure:charcode() or 0 local properties = { llx = boundingbox[1], lly = boundingbox[2], urx = boundingbox[3], ury = boundingbox[4], slot = slot, width = figure:width(), height = figure:height(), depth = figure:depth(), italic = figure:italic(), number = slot, } insert(stack,properties) metapost.properties = properties return properties end local function popproperties() metapost.properties = remove(stack) end local optimizecolor = false directives.register("metapost.optimizecolor", function(v) optimizecolor = v end) local function enhancedobject(original) return setmetatable({ }, { __index = original }) end function metapost.flush(specification,result) if result then local flusher = specification.flusher local askedfig = specification.askedfig local incontext = specification.incontext local filtering = specification.filtering local figures = result.fig if figures then flusher = flusher or pdfflusher local plugmode = not metapost.getbackendoption(specification.mpx,"noplugins") local resetplugins = plugmode and metapost.resetplugins or ignore -- before figure local processplugins = plugmode and metapost.processplugins or ignore -- each object local synchronizeplugins = plugmode and metapost.synchronizeplugins or ignore local pluginactions = plugmode and metapost.pluginactions or ignore -- before / after local startfigure = flusher.startfigure local stopfigure = flusher.stopfigure local flushfigure = flusher.flushfigure local textfigure = flusher.textfigure -- local processspecial = flusher.processspecial or metapost.processspecial local tocomment = flusher.tocomment if type(filtering) ~= "table" or not next(filtering) then filtering = false end -- patterns: we always use image 1 and then can use patterns for 2..n (or one number) -- we can then do an intermediate flush for index=1,#figures do local figure = figures[index] local properties = pushproperties(figure) if askedfig == "direct" or askedfig == "all" or askedfig == properties.number then local stacking = figure:stacking() -- This has to happen before fetching objects! local objects = figure:objects() local tolerance = figure:tolerance() or getbendtolerance() local result = { } local miterlimit = -1 local linecap = -1 local linejoin = -1 local dashed = false local linewidth = false local llx = properties.llx local lly = properties.lly local urx = properties.urx local ury = properties.ury if urx < llx then -- invalid startfigure(properties.number,0,0,0,0,"invalid",figure,plugmode) if tocomment then result[#result+1] = tocomment("invalid") end stopfigure() else -- we need to be indirect if we want the one-pass solution local groupstack = { } local function processfigure() if tocomment then result[#result+1] = tocomment("begin") end result[#result+1] = "q" if objects then -- resetplugins(result) -- we should move the colorinitializer here local savedpath = nil local savedhtap = nil if stacking then stacking = { } for o=1,#objects do local stack = objects[o].stacking if stack then if filtering then stacking[stack] = filtering[stack] else stacking[stack] = true end end end stacking = sortedkeys(stacking) else stacking = nostacking end for i=1,#stacking do local stack = stacking[i] for o=1,#objects do local object = objects[o] if stack == object.stacking then local objecttype = object.type if objecttype == "fill" or objecttype == "outline" then -- we use an indirect table as we want to overload -- entries but this is not possible in userdata -- -- can be optimized if no path -- local original = object local object = { } setmetatable(object, { __index = original }) -- if object.bytemap then -- We trigger the bm plugin. There can be a prescript "bytemask=n" so we -- need to preserve that. In that plugin we use object.bytemap directly. local bm_trigger = "bytemap=true" if object.prescript and object.prescript ~= "" then object.prescript = bm_trigger .. "\n" .. object.prescript else object.prescript = bm_trigger end end -- local before, after, options = processplugins(object) local evenodd = false local collect = false local both = false local flush = false local outline = force_outline local envelope = false local postscript = object.postscript local tolerance = options and tonumber(options.tolerance) or tolerance -- if not object.istext then if postscript == "evenodd" then evenodd = true elseif postscript == "collect" then collect = true elseif postscript == "flush" then flush = true elseif postscript == "both" then both = true elseif postscript == "eoboth" then evenodd = true both = true elseif postscript == "envelope" then envelope = true end -- end -- if flush and not savedpath then -- forget about it elseif 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 objecttype = object.type -- can have changed if envelope then dashed, linewidth = "", 1 -- to be sure end if before then -- We inject too many color and transparency directives although in practice -- it's not that relevant. However, we can optimize this a bit because we know -- what we can get so we just intercept these two cases: -- -- { [1] = transparency, [2] = color } -- { [1] = color } -- -- It is anyway a rather ugly hack because we should keep some kind of state -- but as we mix all kind of features that is also tricky. Maybe if I'd write -- this from scratch ... but that doesn't pay off. Also tracking a transstate -- is kind of fragile so maybe it should be optional. -- if optimizecolor and after then local r = #result if result[r] == "0 g 0 G" and (after[1] == "0 g 0 G" or after[2] == "0 g 0 G") then result[r] = nil r = r - 1 end if result[r] == "/Tr0 gs" and (after[1] == "/Tr0 gs" or after[2] == "/Tr0 gs") then result[r] = nil end end -- result = pluginactions(before,result,flushfigure) end local ml = object.miterlimit if ml and ml ~= miterlimit then miterlimit = ml result[#result+1] = f_M(ml) end local lj = object.linejoin if lj and lj ~= linejoin then linejoin = lj result[#result+1] = f_j(lj) end local lc = object.linecap if lc and lc ~= linecap then linecap = lc result[#result+1] = f_J(lc) end if both then if dashed ~= false then -- was just dashed test result[#result+1] = "[] 0 d" dashed = false end else local dl = object.dash if dl then local d = f_d(concat(dl.dashes or {}," "),dl.offset) if d ~= dashed then dashed = d result[#result+1] = d end elseif dashed ~= false then -- was just dashed test result[#result+1] = "[] 0 d" dashed = false end end local path = object.path -- newpath local transformed = false local penwidth = 1 local open = path and path[1].left_type and path[#path].right_type -- at this moment only "end_point" local pen = object.pen if pen then if pen.type == "elliptical" or outline then transformed, penwidth = pen_characteristics(original) -- boolean, value if penwidth ~= linewidth then result[#result+1] = f_w(penwidth) linewidth = penwidth end if objecttype == "fill" then objecttype = "both" end else -- calculated by mplib itself objecttype = "fill" end end if transformed then result[#result+1] = "q" end if path then if savedpath then for i=1,#savedpath do local path = savedpath[i] local open = not path.cycle if transformed then flushconcatpath(path,result,open,tolerance,i==1) else flushnormalpath(path,result,open,tolerance) end end savedpath = nil end if flush then -- ignore this path elseif transformed then flushconcatpath(path,result,open,tolerance,true) else flushnormalpath(path,result,open,tolerance) end if path.rectangle then if outline or envelope then result[#result+1] = "S" elseif objecttype == "fill" then result[#result+1] = "f" elseif objecttype == "outline" then result[#result+1] = both and "B" or "S" elseif objecttype == "both" then result[#result+1] = "B" end elseif outline or envelope then result[#result+1] = open and "S" or "h S" elseif objecttype == "fill" then result[#result+1] = evenodd and "h f*" or "h f" -- f* = eo elseif objecttype == "outline" then if both then result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo else result[#result+1] = open and "S" or "h S" end elseif objecttype == "both" then result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo -- b includes closepath end end if transformed then result[#result+1] = "Q" end local path = object.htap if path then if transformed then result[#result+1] = "q" end if savedhtap then for i=1,#savedhtap do local path = savedhtap[i] local open = not path.cycle if transformed then flushconcatpath(path,result,open,tolerance,i==1) else flushnormalpath(path,result,open,tolerance) end end savedhtap = nil evenodd = true end if transformed then flushconcatpath(path,result,open,tolerance,true) else flushnormalpath(path,result,open,tolerance) end if outline or envelope then result[#result+1] = open and "S" or "h S" elseif objecttype == "fill" then result[#result+1] = evenodd and "h f*" or "h f" -- f* = eo elseif objecttype == "outline" then result[#result+1] = open and "S" or "h S" elseif objecttype == "both" then result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo -- b includes closepath end if transformed then result[#result+1] = "Q" end end if after then result = pluginactions(after,result,flushfigure) end end if object.grouped then -- can be qQ'd so changes can end up in groups miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false end elseif objecttype == "start_clip" then -- local evenodd = not object.istext and object.postscript == "evenodd" local evenodd = object.postscript == "evenodd" result[#result+1] = "q" flushnormalpath(object.path,result,false,tolerance) result[#result+1] = evenodd and "W* n" or "W n" elseif objecttype == "stop_clip" then result[#result+1] = "Q" miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false elseif objecttype == "start_bounds" or objecttype == "stop_bounds" then -- skip elseif objecttype == "start_group" then if lpdf.flushgroup then local object = enhancedobject(object) local before, after = processplugins(object) local detail = object.detail if detail or before then result[#result+1] = "q" result = pluginactions(before,result,flushfigure) insert(groupstack, { after = after, result = result, detail = detail, bbox = toboundingbox(object.path), }) result = { } miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false else insert(groupstack,false) end else insert(groupstack,false) end elseif objecttype == "stop_group" then local data = remove(groupstack) if data then local reference = lpdf.flushgroup(concat(result,"\r"),data.bbox,data.detail) -- crashes on after = false result = data.result result[#result+1] = reference result = pluginactions(data.after,result,flushfigure) result[#result+1] = "Q" miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false end -- if objecttype == "text" then -- result[#result+1] = "q" -- local ot = object.transform -- 3,4,5,6,1,2 -- result[#result+1] = f_cm(ot[3],ot[4],ot[5],ot[6],ot[1],ot[2]) -- flushfigure(result) -- flush accumulated literals -- result = { } -- textfigure(object.font,object.dsize,object.text,object.width,object.height,object.depth) -- result[#result+1] = "Q" -- elseif objecttype == "special" then -- if processspecial then -- processspecial(object.prescript) -- end -- else -- error end end end end end result[#result+1] = "Q" if tocomment then result[#result+1] = tocomment("end") end flushfigure(result) end startfigure(properties.number,llx,lly,urx,ury,"begin",figure,plugmode) if incontext then context(function() processfigure() end) else processfigure() end stopfigure("end") end if askedfig ~= "all" then break end end popproperties() end resetplugins(result) -- we should move the colorinitializer here end end end -- tracing: do local result = { } local flusher = { startfigure = function() result = { } context.startnointerference() end, flushfigure = function(literals) local n = #result for i=1,#literals do result[n+i] = literals[i] end end, stopfigure = function() context.stopnointerference() end } local specification = { flusher = flusher, -- incontext = true, } function metapost.pdfliterals(result) metapost.flush(specification,result) return result end end function metapost.totable(result,askedfig) local askedfig = askedfig or 1 local figure = result and result.fig and result.fig[1] if figure then local results = { } local objects = figure:objects() for o=1,#objects do local object = objects[o] local result = { } local fields = getfields(object) -- hm, is this the whole list, if so, we can get it once for f=1,#fields do local field = fields[f] result[field] = object[field] end results[o] = result end local boundingbox = figure:boundingbox() return { boundingbox = { llx = boundingbox[1], lly = boundingbox[2], urx = boundingbox[3], ury = boundingbox[4], }, objects = results } else return nil end end