if not modules then modules = { } end modules ['mlib-pdf'] = { version = 1.001, comment = "companion to mlib-ctx.tex", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files", } local format, join = string.format, table.concat local sprint = tex.sprint local abs, sqrt, round = math.abs, math.sqrt, math.round metapost = metapost or { } metapost.multipass = false metapost.n = 0 metapost.optimize = true -- false --~ Because in MKiV we always have two passes, we save the objects. When an extra --~ mp run is done (due to for instance texts identifier in the parse pass), we --~ get a new result table and the stored objects are forgotten. Otherwise they --~ are reused. local function getobjects(result,figure,f) if metapost.optimize then local objects = result.objects if not objects then result.objects = { } end objects = result.objects[f] if not objects then objects = figure:objects() result.objects[f] = objects end return objects else return figure:objects() end end function metapost.convert(result, trialrun, flusher, multipass) if trialrun then metapost.multipass = false metapost.parse(result, flusher) if multipass and not metapost.multipass and metapost.optimize then metapost.flush(result, flusher) -- saves a run else return false end else metapost.flush(result, flusher) end return true -- done end function metapost.comment(message) if message then sprint(tex.ctxcatcodes,format("\\MPLIBtoPDF{\\letterpercent\\space mps graphic %s: %s}", metapost.n, message)) end end metapost.flushers = { } metapost.flushers.pdf = { } function metapost.flushers.pdf.startfigure(n,llx,lly,urx,ury,message) metapost.n = metapost.n + 1 sprint(tex.ctxcatcodes,format("\\startMPLIBtoPDF{%s}{%s}{%s}{%s}",llx,lly,urx,ury)) if message then metapost.comment(message) end end function metapost.flushers.pdf.stopfigure(message) if message then metapost.comment(message) end sprint(tex.ctxcatcodes,"\\stopMPLIBtoPDF") end function metapost.flushers.pdf.flushfigure(pdfliterals) -- table if #pdfliterals > 0 then sprint(tex.ctxcatcodes,"\\MPLIBtoPDF{",join(pdfliterals,"\n"),"}") end end function metapost.flushers.pdf.textfigure(font,size,text,width,height,depth) -- we could save the factor text = text:gsub(".","\\hbox{%1}") -- kerning happens in metapost (i have to check if this is true for mplib) sprint(tex.ctxcatcodes,format("\\MPLIBtextext{%s}{%s}{%s}{%s}{%s}",font,size,text,0,-number.dimenfactors.bp*depth)) 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) if mplib.pen_info then 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 else rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1 return false, 1 end end local function concat(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, t, open) local pth, ith for i=1,#path do pth = path[i] if not ith then t[#t+1] = format("%f %f m",pth.x_coord,pth.y_coord) elseif curved(ith,pth) then t[#t+1] = format("%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 t[#t+1] = format("%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 t[#t+1] = format("%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 t[#t+1] = format("%f %f l",one.x_coord,one.y_coord) end elseif #path == 1 then -- special case .. draw point local one = path[1] t[#t+1] = format("%f %f l",one.x_coord,one.y_coord) end return t end local function flushconcatpath(path, t, open) t[#t+1] = format("%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 t[#t+1] = format("%f %f m",concat(pth.x_coord,pth.y_coord)) elseif curved(ith,pth) then local a, b = concat(ith.right_x,ith.right_y) local c, d = concat(pth.left_x,pth.left_y) t[#t+1] = format("%f %f %f %f %f %f c",a,b,c,d,concat(pth.x_coord, pth.y_coord)) else t[#t+1] = format("%f %f l",concat(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 = concat(pth.right_x,pth.right_y) local c, d = concat(one.left_x,one.left_y) t[#t+1] = format("%f %f %f %f %f %f c",a,b,c,d,concat(one.x_coord, one.y_coord)) else t[#t+1] = format("%f %f l",concat(one.x_coord,one.y_coord)) end elseif #path == 1 then -- special case .. draw point local one = path[1] t[#t+1] = format("%f %f l",concat(one.x_coord,one.y_coord)) end return t end metapost.specials = metapost.specials or { } -- we have two extension handlers, one for pre and postscripts, and one for colors -- the flusher is pdf based, if another backend is used, we need to overload the -- flusher; this is beta code, the organization will change function metapost.flush(result,flusher) -- pdf flusher, table en dan concat is sneller, 1 literal if result then local figures = result.fig if figures then flusher = flusher or metapost.flushers.pdf local colorconverter = metapost.colorconverter() -- function ! local colorhandler = metapost.colorhandler for f=1, #figures do local figure = figures[f] local objects = getobjects(result,figure,f) local fignum = tonumber((figure:filename()):match("([%d]+)$") or figure:charcode() or 0) local t = { } 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 flusher.startfigure(fignum,0,0,0,0,"invalid",figure) flusher.stopfigure() else flusher.startfigure(fignum,llx,lly,urx,ury,"begin",figure) t[#t+1] = "q" if objects then 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 t[#t+1] = "q" flushnormalpath(object.path,t,false) t[#t+1] = "W n" elseif objecttype == "stop_clip" then t[#t+1] = "Q" miterlimit, linecap, linejoin, dashed = -1, -1, -1, false elseif objecttype == "special" then metapost.specials.register(object.prescript) elseif objecttype == "text" then t[#t+1] = "q" local ot = object.transform -- 3,4,5,6,1,2 t[#t+1] = format("%f %f %f %f %f %f cm",ot[3],ot[4],ot[5],ot[6],ot[1],ot[2]) -- TH: format("%f %f m %f %f %f %f 0 0 cm",unpack(ot)) flusher.flushfigure(t) t = { } flusher.textfigure(object.font,object.dsize,object.text,object.width,object.height,object.depth) t[#t+1] = "Q" else -- alternatively we can pass on the stack, could be a helper local currentobject = { -- not needed when no extensions type = object.type, miterlimit = object.miterlimit, linejoin = object.linejoin, linecap = object.linecap, color = object.color, dash = object.dash, path = object.path, htap = object.htap, pen = object.pen, prescript = object.prescript, postscript = object.postscript, } -- local before, inbetween, after = nil, nil, nil -- local cs, cr = currentobject.color, nil -- todo document why ... if cs and colorhandler and #cs > 0 and round(cs[1]*10000) == 123 then -- test in function currentobject, cr = colorhandler(cs,currentobject,t,colorconverter) objecttype = currentobject.type end -- local prescript = currentobject.prescript if prescript then -- move test to function local special = metapost.specials[prescript] if special then currentobject, before, inbetween, after = special(currentobject.postscript,currentobject,t,flusher) objecttype = currentobject.type end end -- cs = currentobject.color if cs and #cs > 0 then t[#t+1], cr = colorconverter(cs) end -- if before then currentobject, t = before() end local ml = currentobject.miterlimit if ml and ml ~= miterlimit then miterlimit = ml t[#t+1] = format("%f M",ml) end local lj = currentobject.linejoin if lj and lj ~= linejoin then linejoin = lj t[#t+1] = format("%i j",lj) end local lc = currentobject.linecap if lc and lc ~= linecap then linecap = lc t[#t+1] = format("%i J",lc) end local dl = currentobject.dash if dl then local d = format("[%s] %i d",join(dl.dashes or {}," "),dl.offset) if d ~= dashed then dashed = d t[#t+1] = dashed end elseif dashed then t[#t+1] = "[] 0 d" dashed = false end if inbetween then currentobject, t = inbetween() end local path = currentobject.path local transformed, penwidth = false, 1 local open = path and path[1].left_type and path[#path].right_type -- at this moment only "end_point" local pen = currentobject.pen if pen then if pen.type == 'elliptical' then transformed, penwidth = pen_characteristics(object) -- boolean, value t[#t+1] = format("%f w",penwidth) -- todo: only if changed if objecttype == 'fill' then objecttype = 'both' end else -- calculated by mplib itself objecttype = 'fill' end end if transformed then t[#t+1] = "q" end if path then if transformed then flushconcatpath(path,t,open) else flushnormalpath(path,t,open) end if objecttype == "fill" then t[#t+1] = "h f" elseif objecttype == "outline" then t[#t+1] = (open and "S") or "h S" elseif objecttype == "both" then t[#t+1] = "h B" end end if transformed then t[#t+1] = "Q" end local path = currentobject.htap if path then if transformed then t[#t+1] = "q" end if transformed then flushconcatpath(path,t,open) else flushnormalpath(path,t,open) end if objecttype == "fill" then t[#t+1] = "h f" elseif objecttype == "outline" then t[#t+1] = (open and "S") or "h S" elseif objecttype == "both" then t[#t+1] = "h B" end if transformed then t[#t+1] = "Q" end end if cr then t[#t+1] = cr end if after then currentobject, t = after() end end end end t[#t+1] = "Q" flusher.flushfigure(t) flusher.stopfigure("end") end end end end end function metapost.parse(result) if result then local figures = result.fig if figures then for f=1, #figures do local figure = figures[f] local objects = getobjects(result,figure,f) if objects then for o=1,#objects do local object = objects[o] if object.type == "outline" then local prescript = object.prescript if prescript then local special = metapost.specials[prescript] if special then special(object.postscript,object) end end end end end end end end end do -- just tracing local t = { } local flusher = { startfigure = function() t = { } sprint(tex.ctxcatcodes,"\\startnointerference") end, flushfigure = function(literals) for i=1, #literals do t[#t+1] = literals[i] end end, stopfigure = function() sprint(tex.ctxcatcodes,"\\stopnointerference") end } function metapost.pdfliterals(result) metapost.flush(result,flusher) return t end end function metapost.totable(result) local figure = result and result.fig and result.fig[1] if figure then local t = { } local objects = figure:objects() for _, object in ipairs(objects) do local tt = { } for _, field in ipairs(mplib.fields(object)) do tt[field] = object[field] end t[#t+1] = tt end local b = figure:boundingbox() return { boundingbox = { llx = b[1], lly = b[2], urx = b[3], ury = b[4] }, objects = t } else return nil end end function metapost.colorconverter() return function(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 --~ -- obsolete code --~ --~ -- the pen calculations are taken from metapost, first converted by --~ -- taco from c to lua, and then optimized by hans, so all errors are his --~ --~ local aspect_bound = 10/65536 --~ local aspect_default = 1/65536 --~ local eps = 0.0001 --~ --~ local function pyth(a,b) --~ return sqrt(a*a + b*b) -- much faster than sqrt(a^2 + b^2) --~ end --~ --~ local function coord_range_x(h, dz) -- direction x --~ local zlo, zhi = 0, 0 --~ for i=1, #h do --~ local p = h[i] --~ local z = p.x_coord --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ z = p.right_x --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ z = p.left_x --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ end --~ return (zhi - zlo <= dz and aspect_bound) or aspect_default --~ end --~ --~ local function coord_range_y(h, dz) -- direction y --~ local zlo, zhi = 0, 0 --~ for i=1, #h do --~ local p = h[i] --~ local z = p.y_coord --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ z = p.right_y --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ z = p.left_y --~ if z < zlo then zlo = z elseif z > zhi then zhi = z end --~ end --~ return (zhi - zlo <= dz and aspect_bound) or aspect_default --~ end --~ --~ local function pen_characteristics(object) --~ local p = object.pen[1] --~ local x_coord, y_coord, left_x, left_y, right_x, right_y = p.x_coord, p.y_coord, p.left_x, p.left_y, p.right_x, p.right_y --~ local wx, wy, width --~ if right_x == x_coord and left_y == y_coord then --~ wx = abs(left_x - x_coord) --~ wy = abs(right_y - y_coord) --~ else --~ wx = pyth(left_x - x_coord, right_x - x_coord) --~ wy = pyth(left_y - y_coord, right_y - y_coord) --~ end --~ if wy/coord_range_x(object.path, wx) >= wx/coord_range_y(object.path, wy) then --~ width = wy --~ else --~ width = wx --~ end --~ sx, rx, ry, sy, tx, ty = left_x, left_y, right_x, right_y, x_coord, y_coord --~ sx, rx, ry, sy = (sx-tx), (rx-ty), (ry-tx), (sy-ty) -- combine with previous --~ if width ~= 1 then --~ if width == 0 then --~ sx, sy = 1, 1 --~ else --~ rx, ry, sx, sy = rx/width, ry/width, sx/width, sy/width --~ end --~ end --~ -- sx rx ry sy tx ty -> 1 0 0 1 0 0 is ok, but 0 0 0 0 0 0 not --~ if true then --~ if abs(sx) < eps then sx = eps end --~ if abs(sy) < eps then sy = eps end --~ else --~ -- this block looks complicated but it only captures invalid transforms --~ -- to be checked rx vs sx and so --~ local det = sx/sy - ry/rx --~ local aspect = 4*aspect_bound + aspect_default --~ if abs(det) < aspect then --~ local s --~ if det >= 0 then --~ s, aspect = 1, aspect - det --~ else --~ s, aspect = -1, -aspect - det -- - ? --~ end --~ local absrx, absry, abssy, abssx = abs(rx), abs(ry), abs(sy), abs(sx) --~ if abssx + abssy >= absry + absrx then -- was yy --~ if abssx > abssy then --~ sy = sy + (aspect + s*abssx) / sx --~ else --~ sx = sx + (aspect + s*abssy) / sy --~ end --~ else --~ if absry > absrx then --~ rx = rx + (aspect + s*absry) / ry --~ else --~ ry = ry + (aspect + s*absrx) / rx --~ end --~ end --~ end --~ end --~ divider = sx*sy - rx*ry --~ return not (sx==1 and rx==0 and ry==0 and sy==1 and tx==0 and ty==0), width --~ end