meta-pdh.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['meta-pdf'] = {
    version   = 1.001,
    comment   = "companion to meta-pdf.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

if true then
    return -- or os.exit()
end

-- This file contains the history of the converter. We keep it around as it
-- relates to the development of luatex.

-- This is the third version. Version 1 converted to Lua code,
-- version 2 gsubbed the file into TeX code, and version 3 uses
-- the new lpeg functionality and streams the result into TeX.

-- We will move old stuff to edu.

--~                         old           lpeg 0.4       lpeg 0.5
--~ 100 times test graphic  2.45 (T:1.07) 0.72 (T:0.24)  0.580  (0.560  no table) -- 0.54 optimized for one space (T:0.19)
--~ 100 times big  graphic 10.44          4.30/3.35 nogb 2.914  (2.050  no table) -- 1.99 optimized for one space (T:0.85)
--~ 500 times test graphic                T:1.29         T:1.16 (T:1.10 no table) -- T:1.10

-- only needed for mp output on disk

local concat, format, find, gsub, gmatch = table.concat, string.format, string.find, string.gsub, string.gmatch
local tostring, tonumber, select = tostring, tonumber, select
local lpegmatch = lpeg.match

metapost         = metapost or { }
local metapost   = metapost
local context    = context

metapost.mptopdf = metapost.mptopdf or { }
local mptopdf    = metapost.mptopdf

mptopdf.parsers      = { }
mptopdf.parser       = 'none'
mptopdf.nofconverted = 0

function mptopdf.reset()
    mptopdf.data      = ""
    mptopdf.path      = { }
    mptopdf.stack     = { }
    mptopdf.texts     = { }
    mptopdf.version   = 0
    mptopdf.shortcuts = false
    mptopdf.resetpath()
end

function mptopdf.resetpath()
    mptopdf.stack.close   = false
    mptopdf.stack.path    = { }
    mptopdf.stack.concat  = nil
    mptopdf.stack.special = false
end

mptopdf.reset()

function mptopdf.parsers.none()
    -- no parser set
end

function mptopdf.parse()
    mptopdf.parsers[mptopdf.parser]()
end

-- old code

mptopdf.steps = { }

mptopdf.descapes = {
    ['('] = "\\\\char40 ",
    [')'] = "\\\\char41 ",
    ['"'] = "\\\\char92 "
}

function mptopdf.descape(str)
    str = gsub(str,"\\(%d%d%d)",function(n)
        return "\\char" .. tonumber(n,8) .. " "
    end)
    return gsub(str,"\\([%(%)\\])",mptopdf.descapes)
end

function mptopdf.steps.descape(str)
    str = gsub(str,"\\(%d%d%d)",function(n)
        return "\\\\char" .. tonumber(n,8) .. " "
    end)
    return gsub(str,"\\([%(%)\\])",mptopdf.descapes)
end

function mptopdf.steps.strip() -- .3 per expr
    mptopdf.data = gsub(mptopdf.data,"^(.-)%%+Page:.-%c+(.*)%s+%a+%s+%%+EOF.*$", function(preamble, graphic)
        local bbox = "0 0 0 0"
        for b in gmatch(preamble,"%%%%%a+oundingBox: +(.-)%c+") do
            bbox = b
        end
        local name, version = gmatch(preamble,"%%%%Creator: +(.-) +(.-) ")
        mptopdf.version = tostring(version or "0")
        if find(preamble,"/hlw{0 dtransform",1,true) then
            mptopdf.shortcuts = true
        end
        -- the boundingbox specification needs to come before data, well, not really
        return bbox .. " boundingbox\n" .. "\nbegindata\n" .. graphic .. "\nenddata\n"
    end, 1)
    mptopdf.data = gsub(mptopdf.data,"%%%%MetaPostSpecials: +(.-)%c+", "%1 specials\n", 1)
    mptopdf.data = gsub(mptopdf.data,"%%%%MetaPostSpecial: +(.-)%c+", "%1 special\n")
    mptopdf.data = gsub(mptopdf.data,"%%.-%c+", "")
end

function mptopdf.steps.cleanup()
    if not mptopdf.shortcuts then
        mptopdf.data = gsub(mptopdf.data,"gsave%s+fill%s+grestore%s+stroke", "both")
        mptopdf.data = gsub(mptopdf.data,"([%d%.]+)%s+([%d%.]+)%s+dtransform%s+exch%s+truncate%s+exch%s+idtransform%s+pop%s+setlinewidth", function(wx,wy)
            if tonumber(wx) > 0 then return wx .. " setlinewidth" else return wy .. " setlinewidth"  end
        end)
        mptopdf.data = gsub(mptopdf.data,"([%d%.]+)%s+([%d%.]+)%s+dtransform%s+truncate%s+idtransform%s+setlinewidth%s+pop", function(wx,wy)
            if tonumber(wx) > 0 then return wx .. " setlinewidth" else return wy .. " setlinewidth"  end
        end)
    end
end

function mptopdf.steps.convert()
    mptopdf.data = gsub(mptopdf.data,"%c%((.-)%) (.-) (.-) fshow", function(str,font,scale)
        mptopdf.texts[mptopdf.texts+1] = {mptopdf.steps.descape(str), font, scale}
        return "\n" .. #mptopdf.texts .. " textext"
    end)
    mptopdf.data = gsub(mptopdf.data,"%[%s*(.-)%s*%]", function(str)
        return gsub(str,"%s+"," ")
    end)
    local t
    mptopdf.data = gsub(mptopdf.data,"%s*([^%a]-)%s*(%a+)", function(args,cmd)
        if cmd == "textext" then
            t = mptopdf.texts[tonumber(args)]
            return "metapost.mps.textext(" ..  "\"" .. t[2] .. "\"," .. t[3] .. ",\"" .. t[1] .. "\")\n"
        else
            return "metapost.mps." .. cmd .. "(" .. gsub(args," +",",") .. ")\n"
        end
    end)
end

function mptopdf.steps.process()
    assert(loadstring(mptopdf.data))() -- () runs the loaded chunk
end

function mptopdf.parsers.gsub()
    mptopdf.steps.strip()
    mptopdf.steps.cleanup()
    mptopdf.steps.convert()
    mptopdf.steps.process()
end

-- end of old code

-- from lua to tex

function mptopdf.pdfcode(str)
    context.pdfliteral(str) -- \\MPScode
end

function mptopdf.texcode(str)
    context(str)
end

-- auxiliary functions

function mptopdf.flushconcat()
    if mptopdf.stack.concat then
        mptopdf.pdfcode(concat(mptopdf.stack.concat," ") .. " cm")
        mptopdf.stack.concat = nil
    end
end

function mptopdf.flushpath(cmd)
    -- faster: no local function and loop
    if #mptopdf.stack.path > 0 then
        local path = { }
        if mptopdf.stack.concat then
            local sx, sy = mptopdf.stack.concat[1], mptopdf.stack.concat[4]
            local rx, ry = mptopdf.stack.concat[2], mptopdf.stack.concat[3]
            local tx, ty = mptopdf.stack.concat[5], mptopdf.stack.concat[6]
            local d = (sx*sy) - (rx*ry)
            local function mpconcat(px, py)
                return (sy*(px-tx)-ry*(py-ty))/d, (sx*(py-ty)-rx*(px-tx))/d
            end
            local stackpath = mptopdf.stack.path
            for k=1,#stackpath do
                local v = stackpath[k]
                v[1],v[2] = mpconcat(v[1],v[2])
                if #v == 7 then
                    v[3],v[4] = mpconcat(v[3],v[4])
                    v[5],v[6] = mpconcat(v[5],v[6])
                end
                path[#path+1] = concat(v," ")
            end
        else
            local stackpath = mptopdf.stack.path
            for k=1,#stackpath do
                path[#path+1] = concat(stackpath[k]," ")
            end
        end
        mptopdf.flushconcat()
        mptopdf.texcode("\\MPSpath{" .. concat(path," ") .. "}")
        if mptopdf.stack.close then
            mptopdf.texcode("\\MPScode{h " .. cmd .. "}")
        else
            mptopdf.texcode("\\MPScode{" .. cmd .."}")
        end
    end
    mptopdf.resetpath()
end

function mptopdf.loaded(name)
    local ok, n
    mptopdf.reset()
    ok, mptopdf.data, n = resolvers.loadbinfile(name, 'tex') -- we need a binary load !
    return ok
end

if not mptopdf.parse then
    function mptopdf.parse() end -- forward declaration
end

function mptopdf.convertmpstopdf(name)
    if mptopdf.loaded(name) then
        mptopdf.nofconverted = mptopdf.nofconverted + 1
        statistics.starttiming(mptopdf)
        mptopdf.parse()
        mptopdf.reset()
        statistics.stoptiming(mptopdf)
    else
        context("file " .. name .. " not found")
    end
end

-- mp interface

metapost.mps = metapost.mps or { }
local mps    = metapost.mps or { }

function mps.creator(a, b, c)
    mptopdf.version = tonumber(b)
end

function mps.creationdate(a)
    mptopdf.date= a
end

function mps.newpath()
    mptopdf.stack.path = { }
end

function mps.boundingbox(llx, lly, urx, ury)
    mptopdf.texcode("\\MPSboundingbox{" .. llx .. "}{" .. lly .. "}{" .. urx .. "}{" .. ury .. "}")
end

function mps.moveto(x,y)
    mptopdf.stack.path[#mptopdf.stack.path+1] = {x,y,"m"}
end

function mps.curveto(ax, ay, bx, by, cx, cy)
    mptopdf.stack.path[#mptopdf.stack.path+1] = {ax,ay,bx,by,cx,cy,"c"}
end

function mps.lineto(x,y)
    mptopdf.stack.path[#mptopdf.stack.path+1] = {x,y,"l"}
end

function mps.rlineto(x,y)
    local dx, dy = 0, 0
    if #mptopdf.stack.path > 0 then
        dx, dy = mptopdf.stack.path[#mptopdf.stack.path][1], mptopdf.stack.path[#mptopdf.stack.path][2]
    end
    mptopdf.stack.path[#mptopdf.stack.path+1] = {dx,dy,"l"}
end

function mps.translate(tx,ty)
    mptopdf.pdfcode("1 0 0 0 1 " .. tx .. " " .. ty .. " cm")
end

function mps.scale(sx,sy)
    mptopdf.stack.concat = {sx,0,0,sy,0,0}
end

function mps.concat(sx, rx, ry, sy, tx, ty)
    mptopdf.stack.concat = {sx,rx,ry,sy,tx,ty}
end

function mps.setlinejoin(d)
    mptopdf.pdfcode(d .. " j")
end

function mps.setlinecap(d)
    mptopdf.pdfcode(d .. " J")
end

function mps.setmiterlimit(d)
    mptopdf.pdfcode(d .. " M")
end

function mps.gsave()
    mptopdf.pdfcode("q")
end

function mps.grestore()
    mptopdf.pdfcode("Q")
end

function mps.setdash(...)
    local n = select("#",...)
    mptopdf.pdfcode("[" .. concat({...}," ",1,n-1) .. "] " .. select(n,...) .. " d")
end

function mps.resetdash()
    mptopdf.pdfcode("[ ] 0 d")
end

function mps.setlinewidth(d)
    mptopdf.pdfcode(d .. " w")
end

function mps.closepath()
    mptopdf.stack.close = true
end

function mps.fill()
    mptopdf.flushpath('f')
end

function mps.stroke()
    mptopdf.flushpath('S')
end

function mps.both()
    mptopdf.flushpath('B')
end

function mps.clip()
    mptopdf.flushpath('W n')
end

function mps.textext(font, scale, str) -- old parser
    local dx, dy = 0, 0
    if #mptopdf.stack.path > 0 then
        dx, dy = mptopdf.stack.path[1][1], mptopdf.stack.path[1][2]
    end
    mptopdf.flushconcat()
    mptopdf.texcode("\\MPStextext{"..font.."}{"..scale.."}{"..str.."}{"..dx.."}{"..dy.."}")
    mptopdf.resetpath()
end

--~ function mps.handletext(font,scale.str,dx,dy)
--~     local one, two = string.match(str, "^(%d+)::::(%d+)")
--~     if one and two then
--~         mptopdf.texcode("\\MPTOPDFtextext{"..font.."}{"..scale.."}{"..one.."}{"..two.."}{"..dx.."}{"..dy.."}")
--~     else
--~         mptopdf.texcode("\\MPTOPDFtexcode{"..font.."}{"..scale.."}{"..str.."}{"..dx.."}{"..dy.."}")
--~     end
--~ end

function mps.setrgbcolor(r,g,b) -- extra check
    r, g = tonumber(r), tonumber(g) -- needed when we use lpeg
    if r == 0.0123 and g < 0.1 then
        mptopdf.texcode("\\MPSspecial{" .. g*10000 .. "}{" .. b*10000 .. "}")
    elseif r == 0.123 and g < 0.1 then
        mptopdf.texcode("\\MPSspecial{" .. g* 1000 .. "}{" .. b* 1000 .. "}")
    else
        mptopdf.texcode("\\MPSrgb{" .. r .. "}{" .. g .. "}{" .. b .. "}")
    end
end

function mps.setcmykcolor(c,m,y,k)
    mptopdf.texcode("\\MPScmyk{" .. c .. "}{" .. m .. "}{" .. y .. "}{" .. k .. "}")
end

function mps.setgray(s)
    mptopdf.texcode("\\MPSgray{" .. s .. "}")
end

function mps.specials(version,signal,factor) -- 2.0 123 1000
end

function mps.special(...) -- 7 1 0.5 1 0 0 1 3
    local n = select("#",...)
    mptopdf.texcode("\\MPSbegin\\MPSset{" .. concat({...},"}\\MPSset{",2,n) .. "}\\MPSend")
end

function mps.begindata()
end

function mps.enddata()
end

function mps.showpage()
end

mps.n   = mps.newpath       -- n
mps.p   = mps.closepath     -- h
mps.l   = mps.lineto        -- l
mps.r   = mps.rlineto       -- r
mps.m   = mps.moveto        -- m
mps.c   = mps.curveto       -- c
mps.hlw = mps.setlinewidth
mps.vlw = mps.setlinewidth

mps.C   = mps.setcmykcolor  -- k
mps.G   = mps.setgray       -- g
mps.R   = mps.setrgbcolor   -- rg

mps.lj  = mps.setlinejoin   -- j
mps.ml  = mps.setmiterlimit -- M
mps.lc  = mps.setlinecap    -- J
mps.sd  = mps.setdash       -- d
mps.rd  = mps.resetdash

mps.S   = mps.stroke        -- S
mps.F   = mps.fill          -- f
mps.B   = mps.both          -- B
mps.W   = mps.clip          -- W

mps.q   = mps.gsave         -- q
mps.Q   = mps.grestore      -- Q

mps.s   = mps.scale         -- (not in pdf)
mps.t   = mps.concat        -- (not the same as pdf anyway)

mps.P   = mps.showpage

-- experimental

function mps.attribute(id,value)
    mptopdf.texcode("\\attribute " .. id .. "=" .. value .. " ")
--  mptopdf.texcode("\\dompattribute{" .. id .. "}{" .. value .. "}")
end

-- lpeg parser

-- The lpeg based parser is rather optimized for the kind of output
-- that MetaPost produces. It's my first real lpeg code, which may
-- show. Because the parser binds to functions, we define it last.

do -- assumes \let\c\char

    local byte = string.byte
    local digit = lpeg.R("09")
    local spec = digit^2 * lpeg.P("::::") * digit^2
    local text = lpeg.Cc("{") * (
        lpeg.P("\\") * ( (digit * digit * digit) / function(n) return "c" .. tonumber(n,8) end) +
                          lpeg.P(" ")            / function(n) return "\\c32" end + -- never in new mp
                          lpeg.P(1)              / function(n) return "\\c" .. byte(n) end
    ) * lpeg.Cc("}")
    local package = lpeg.Cs(spec + text^0)

    function mps.fshow(str,font,scale) -- lpeg parser
        mps.textext(font,scale,lpegmatch(package,str))
    end

end

do

    local eol      = lpeg.S('\r\n')^1
    local sp       = lpeg.P(' ')^1
    local space    = lpeg.S(' \r\n')^1
    local number   = lpeg.S('0123456789.-+')^1
    local nonspace = lpeg.P(1-lpeg.S(' \r\n'))^1

    local cnumber = lpeg.C(number)
    local cstring = lpeg.C(nonspace)

    local specials           = (lpeg.P("%%MetaPostSpecials:") * sp * (cstring * sp^0)^0 * eol) / mps.specials
    local special            = (lpeg.P("%%MetaPostSpecial:")  * sp * (cstring * sp^0)^0 * eol) / mps.special
    local boundingbox        = (lpeg.P("%%BoundingBox:")      * sp * (cnumber * sp^0)^4 * eol) / mps.boundingbox
    local highresboundingbox = (lpeg.P("%%HiResBoundingBox:") * sp * (cnumber * sp^0)^4 * eol) / mps.boundingbox

    local setup              = lpeg.P("%%BeginSetup")  * (1 - lpeg.P("%%EndSetup") )^1
    local prolog             = lpeg.P("%%BeginProlog") * (1 - lpeg.P("%%EndProlog"))^1
    local comment            = lpeg.P('%')^1 * (1 - eol)^1

    local curveto            = ((cnumber * sp)^6 * lpeg.P("curveto")            ) / mps.curveto
    local lineto             = ((cnumber * sp)^2 * lpeg.P("lineto")             ) / mps.lineto
    local rlineto            = ((cnumber * sp)^2 * lpeg.P("rlineto")            ) / mps.rlineto
    local moveto             = ((cnumber * sp)^2 * lpeg.P("moveto")             ) / mps.moveto
    local setrgbcolor        = ((cnumber * sp)^3 * lpeg.P("setrgbcolor")        ) / mps.setrgbcolor
    local setcmykcolor       = ((cnumber * sp)^4 * lpeg.P("setcmykcolor")       ) / mps.setcmykcolor
    local setgray            = ((cnumber * sp)^1 * lpeg.P("setgray")            ) / mps.setgray
    local newpath            = (                   lpeg.P("newpath")            ) / mps.newpath
    local closepath          = (                   lpeg.P("closepath")          ) / mps.closepath
    local fill               = (                   lpeg.P("fill")               ) / mps.fill
    local stroke             = (                   lpeg.P("stroke")             ) / mps.stroke
    local clip               = (                   lpeg.P("clip")               ) / mps.clip
    local both               = (                   lpeg.P("gsave fill grestore")) / mps.both
    local showpage           = (                   lpeg.P("showpage")           )
    local setlinejoin        = ((cnumber * sp)^1 * lpeg.P("setlinejoin")        ) / mps.setlinejoin
    local setlinecap         = ((cnumber * sp)^1 * lpeg.P("setlinecap")         ) / mps.setlinecap
    local setmiterlimit      = ((cnumber * sp)^1 * lpeg.P("setmiterlimit")      ) / mps.setmiterlimit
    local gsave              = (                   lpeg.P("gsave")              ) / mps.gsave
    local grestore           = (                   lpeg.P("grestore")           ) / mps.grestore

    local setdash            = (lpeg.P("[") * (cnumber * sp^0)^0 * lpeg.P("]") * sp * cnumber * sp * lpeg.P("setdash")) / mps.setdash
    local concat             = (lpeg.P("[") * (cnumber * sp^0)^6 * lpeg.P("]")                * sp * lpeg.P("concat") ) / mps.concat
    local scale              = (              (cnumber * sp^0)^6                              * sp * lpeg.P("concat") ) / mps.concat

    local fshow              = (lpeg.P("(") * lpeg.C((1-lpeg.P(")"))^1) * lpeg.P(")") * space * cstring * space * cnumber * space * lpeg.P("fshow")) / mps.fshow
    local fshow              = (lpeg.P("(") *
                                    lpeg.Cs( ( lpeg.P("\\(")/"\\050" + lpeg.P("\\)")/"\\051" + (1-lpeg.P(")")) )^1 )
                                * lpeg.P(")") * space * cstring * space * cnumber * space * lpeg.P("fshow")) / mps.fshow

    local setlinewidth_x     = (lpeg.P("0") * sp * cnumber * sp * lpeg.P("dtransform truncate idtransform setlinewidth pop")) / mps.setlinewidth
    local setlinewidth_y     = (cnumber * sp * lpeg.P("0 dtransform exch truncate exch idtransform pop setlinewidth")  ) / mps.setlinewidth

    local c   = ((cnumber * sp)^6 * lpeg.P("c")  ) / mps.curveto -- ^6 very inefficient, ^1 ok too
    local l   = ((cnumber * sp)^2 * lpeg.P("l")  ) / mps.lineto
    local r   = ((cnumber * sp)^2 * lpeg.P("r")  ) / mps.rlineto
    local m   = ((cnumber * sp)^2 * lpeg.P("m")  ) / mps.moveto
    local vlw = ((cnumber * sp)^1 * lpeg.P("vlw")) / mps.setlinewidth
    local hlw = ((cnumber * sp)^1 * lpeg.P("hlw")) / mps.setlinewidth

    local R   = ((cnumber * sp)^3 * lpeg.P("R")  ) / mps.setrgbcolor
    local C   = ((cnumber * sp)^4 * lpeg.P("C")  ) / mps.setcmykcolor
    local G   = ((cnumber * sp)^1 * lpeg.P("G")  ) / mps.setgray

    local lj  = ((cnumber * sp)^1 * lpeg.P("lj") ) / mps.setlinejoin
    local ml  = ((cnumber * sp)^1 * lpeg.P("ml") ) / mps.setmiterlimit
    local lc  = ((cnumber * sp)^1 * lpeg.P("lc") ) / mps.setlinecap

    local n   = lpeg.P("n") / mps.newpath
    local p   = lpeg.P("p") / mps.closepath
    local S   = lpeg.P("S") / mps.stroke
    local F   = lpeg.P("F") / mps.fill
    local B   = lpeg.P("B") / mps.both
    local W   = lpeg.P("W") / mps.clip
    local P   = lpeg.P("P") / mps.showpage

    local q   = lpeg.P("q") / mps.gsave
    local Q   = lpeg.P("Q") / mps.grestore

    local sd  = (lpeg.P("[") * (cnumber * sp^0)^0 * lpeg.P("]") * sp * cnumber * sp * lpeg.P("sd")) / mps.setdash
    local rd  = (                                                                     lpeg.P("rd")) / mps.resetdash

    local s   = (              (cnumber * sp^0)^2                    * lpeg.P("s") ) / mps.scale
    local t   = (lpeg.P("[") * (cnumber * sp^0)^6 * lpeg.P("]") * sp * lpeg.P("t") ) / mps.concat

    -- experimental

    local attribute = ((cnumber * sp)^2 * lpeg.P("attribute")) / mps.attribute
    local A         = ((cnumber * sp)^2 * lpeg.P("A"))         / mps.attribute

    local preamble = (
        prolog + setup +
        boundingbox + highresboundingbox + specials + special +
        comment
    )

    local procset = (
        lj + ml + lc +
        c + l + m + n + p + r +
        A  +
        R + C + G +
        S + F + B + W +
        vlw + hlw +
        Q + q +
        sd + rd +
        t + s +
        fshow +
        P
    )

    local verbose = (
        curveto + lineto + moveto + newpath + closepath + rlineto +
        setrgbcolor + setcmykcolor + setgray +
        attribute +
        setlinejoin + setmiterlimit + setlinecap +
        stroke + fill + clip + both +
        setlinewidth_x + setlinewidth_y +
        gsave + grestore +
        concat + scale +
        fshow +
        setdash + -- no resetdash
        showpage
    )

    -- order matters in terms of speed / we could check for procset first

    local captures_old = ( space + verbose + preamble           )^0
    local captures_new = ( space + procset + preamble + verbose )^0

    function mptopdf.parsers.lpeg()
        if find(mptopdf.data,"%%BeginResource: procset mpost",1,true) then
            lpegmatch(captures_new,mptopdf.data)
        else
            lpegmatch(captures_old,mptopdf.data)
        end
    end

end

mptopdf.parser = 'lpeg'

-- status info

statistics.register("mps conversion time",function()
    local n = mptopdf.nofconverted
    if n > 0 then
        return format("%s seconds, %s conversions", statistics.elapsedtime(mptopdf),n)
    else
        return nil
    end
end)