meta-pdf.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"
}

-- This module is not used in practice but we keep it around for historic
-- reasons.

-- Finally we used an optimized version. The test code can be found in
-- meta-pdh.lua but since we no longer want to overload functione we use
-- more locals now. This module keeps changing as it is also a testbed.
--
-- We can make it even more efficient if needed, but as we don't use this
-- code often in \MKIV\ it makes no sense.

local tonumber = tonumber
local concat, unpack = table.concat, table.unpack
local gsub, find, byte, gmatch, match = string.gsub, string.find, string.byte, string.gmatch, string.match
local lpegmatch = lpeg.match
local round = math.round
local formatters, format = string.formatters, string.format

local mplib                     = mplib
local metapost                  = metapost
local lpdf                      = lpdf
local context                   = context

local report_mptopdf            = logs.reporter("graphics","mptopdf")

local texgetattribute           = tex.getattribute

local pdfrgbcode                = lpdf.rgbcode
local pdfcmykcode               = lpdf.cmykcode
local pdfgraycode               = lpdf.graycode
local pdfspotcode               = lpdf.spotcode
local pdftransparencycode       = lpdf.transparencycode
local pdffinishtransparencycode = lpdf.finishtransparencycode

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

mptopdf.nofconverted = 0

local f_translate = formatters["1 0 0 0 1 %.6N %.6N cm"]
local f_concat    = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]

local m_path, m_stack, m_texts, m_version, m_date, m_shortcuts = { }, { }, { }, 0, 0, false

local m_stack_close, m_stack_path, m_stack_concat = false, { }, nil
local extra_path_data, ignore_path = nil, false
local specials = { }

local function resetpath()
    m_stack_close, m_stack_path, m_stack_concat = false, { }, nil
end

local function resetall()
    m_path, m_stack, m_texts, m_version, m_shortcuts = { }, { }, { }, 0, false
    extra_path_data, ignore_path = nil, false
    specials = { }
    resetpath()
end

resetall()

local pdfcode = context.pdfliteral

local function mpscode(str)
    if ignore_path then
        pdfcode("h W n")
        if extra_path_data then
            pdfcode(extra_path_data)
            extra_path_data = nil
        end
        ignore_path = false
    else
        pdfcode(str)
    end
end

-- auxiliary functions

local function flushconcat()
    if m_stack_concat then
        mpscode(f_concat(unpack(m_stack_concat)))
        m_stack_concat = nil
    end
end

local function flushpath(cmd)
    if #m_stack_path > 0 then
        local path = { }
        if m_stack_concat then
            local sx = m_stack_concat[1]
            local sy = m_stack_concat[4]
            local rx = m_stack_concat[2]
            local ry = m_stack_concat[3]
            local tx = m_stack_concat[5]
            local ty = m_stack_concat[6]
            local d = (sx*sy) - (rx*ry)
            for k=1,#m_stack_path do
                local v  = m_stack_path[k]
                local px = v[1]
                local py = v[2]
                v[1] = (sy*(px-tx)-ry*(py-ty))/d
                v[2] = (sx*(py-ty)-rx*(px-tx))/d
                if #v == 7 then
                    px = v[3]
                    py = v[4]
                    v[3] = (sy*(px-tx)-ry*(py-ty))/d
                    v[4] = (sx*(py-ty)-rx*(px-tx))/d
                    px = v[5]
                    py = v[6]
                    v[5] = (sy*(px-tx)-ry*(py-ty))/d
                    v[6] = (sx*(py-ty)-rx*(px-tx))/d
                end
                path[k] = concat(v," ")
            end
        else
            for k=1,#m_stack_path do
                path[k] = concat(m_stack_path[k]," ")
            end
        end
        flushconcat()
        pdfcode(concat(path," "))
        if m_stack_close then
            mpscode("h " .. cmd)
        else
            mpscode(cmd)
        end
    end
    resetpath()
end

-- mp interface

local mps = { }

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

function mps.creationdate(a)
    m_date = a
end

function mps.newpath()
    m_stack_path = { }
end

function mps.boundingbox(llx, lly, urx, ury)
    context.setMPboundingbox(llx,lly,urx,ury)
end

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

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

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

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

function mps.translate(tx,ty)
    mpscode(f_translate(tx,ty))
end

function mps.scale(sx,sy)
    m_stack_concat = { sx, 0, 0, sy, 0, 0 }
end

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

function mps.setlinejoin(d)
    mpscode(d .. " j")
end

function mps.setlinecap(d)
    mpscode(d .. " J")
end

function mps.setmiterlimit(d)
    mpscode(d .. " M")
end

function mps.gsave()
    mpscode("q")
end

function mps.grestore()
    mpscode("Q")
end

function mps.setdash(...) -- can be made faster, operate on t = { ... }
    local n = select("#",...)
    mpscode("[" .. concat({...}," ",1,n-1) .. "] " .. select(n,...) .. " d")
 -- mpscode("[" .. concat({select(1,n-1)}," ") .. "] " .. select(n,...) .. " d")
end

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

function mps.setlinewidth(d)
    mpscode(d .. " w")
end

function mps.closepath()
    m_stack_close = true
end

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

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

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

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

function mps.textext(font, scale, str) -- old parser
    local dx = 0
    local dy = 0
    if #m_stack_path > 0 then
        dx, dy = m_stack_path[1][1], m_stack_path[1][2]
    end
    flushconcat()
    context.MPtextext(font,scale,str,dx,dy)
    resetpath()
end

local handlers = { }

handlers[1] = function(s)
    pdfcode(pdffinishtransparencycode())
    pdfcode(pdfcmykcode(mps.colormodel,s[3],s[4],s[5],s[6]))
end
handlers[2] = function(s)
    pdfcode(pdffinishtransparencycode())
    pdfcode(pdfspotcode(mps.colormodel,s[3],s[4],s[5],s[6]))
end
handlers[3] = function(s)
    pdfcode(pdfrgbcode(mps.colormodel,s[4],s[5],s[6]))
    pdfcode(pdftransparencycode(s[2],s[3]))
end
handlers[4] = function(s)
    pdfcode(pdfcmykcode(mps.colormodel,s[4],s[5],s[6],s[7]))
    pdfcode(pdftransparencycode(s[2],s[3]))
end
handlers[5] = function(s)
    pdfcode(pdfspotcode(mps.colormodel,s[4],s[5],s[6],s[7]))
    pdfcode(pdftransparencycode(s[2],s[3]))
end

-- todo: color conversion

local nofshades, tn = 0, tonumber

local function linearshade(colorspace,domain,ca,cb,coordinates)
    pdfcode(pdffinishtransparencycode())
    nofshades = nofshades + 1
    local name = formatters["MpsSh%s"](nofshades)
    lpdf.linearshade(name,domain,ca,cb,1,colorspace,coordinates)
    extra_path_data, ignore_path = formatters["/%s sh Q"](name), true
    pdfcode("q /Pattern cs")
end

local function circularshade(colorspace,domain,ca,cb,coordinates)
    pdfcode(pdffinishtransparencycode())
    nofshades = nofshades + 1
    local name = formatters["MpsSh%s"](nofshades)
    lpdf.circularshade(name,domain,ca,cb,1,colorspace,coordinates)
    extra_path_data, ignore_path = formatters["/%s sh Q"](name), true
    pdfcode("q /Pattern cs")
end

handlers[30] = function(s)
    linearshade("DeviceRGB", { tn(s[ 2]), tn(s[ 3]) },
        { tn(s[ 5]), tn(s[ 6]), tn(s[ 7]) }, { tn(s[10]), tn(s[11]), tn(s[12]) },
        { tn(s[ 8]), tn(s[ 9]), tn(s[13]), tn(s[14]) } )
end

handlers[31] = function(s)
    circularshade("DeviceRGB", { tn(s[ 2]), tn(s[ 3]) },
        { tn(s[ 5]), tn(s[ 6]), tn(s[ 7]) }, { tn(s[11]), tn(s[12]), tn(s[13]) },
        { tn(s[ 8]), tn(s[ 9]), tn(s[10]), tn(s[14]), tn(s[15]), tn(s[16]) } )
end

handlers[32] = function(s)
    linearshade("DeviceCMYK", { tn(s[ 2]), tn(s[ 3]) },
        { tn(s[ 5]), tn(s[ 6]), tn(s[ 7]), tn(s[ 8]) }, { tn(s[11]), tn(s[12]), tn(s[13]), tn(s[14]) },
        { tn(s[ 9]), tn(s[10]), tn(s[15]), tn(s[16]) } )
end

handlers[33] = function(s)
    circularshade("DeviceCMYK", { tn(s[ 2]), tn(s[ 3]) },
        { tn(s[ 5]), tn(s[ 6]), tn(s[ 7]), tn(s[ 8]) }, { tn(s[12]), tn(s[13]), tn(s[14]), tn(s[15]) },
        { tn(s[ 9]), tn(s[10]), tn(s[11]), tn(s[16]), tn(s[17]), tn(s[18]) } )
end

handlers[34] = function(s) -- todo (after further cleanup)
    linearshade("DeviceGray", { tn(s[ 2]), tn(s[ 3]) }, { 0 }, { 1 }, { tn(s[9]), tn(s[10]), tn(s[15]), tn(s[16]) } )
end

handlers[35] = function(s) -- todo (after further cleanup)
    circularshade("DeviceGray",  { tn(s[ 2]), tn(s[ 3]) }, { 0 }, { 1 }, { tn(s[9]), tn(s[10]), tn(s[15]), tn(s[16]) } )
end

-- not supported in mkiv , use mplib instead

handlers[10] = function() report_mptopdf("skipping special %s",10) end
handlers[20] = function() report_mptopdf("skipping special %s",20) end
handlers[50] = function() report_mptopdf("skipping special %s",50) end

--end of not supported

function mps.setrgbcolor(r,g,b) -- extra check
    r = tonumber(r) -- needed when we use lpeg
    g = tonumber(g) -- needed when we use lpeg
    b = tonumber(b) -- needed when we use lpeg
    if r == 0.0123 and g < 0.1 then
        g = round(g*10000)
        b = round(b*10000)
        local s = specials[b]
        local h = round(s[#s])
        local handler = handlers[h]
        if handler then
            handler(s)
        else
            report_mptopdf("unknown special handler %s (1)",h)
        end
    elseif r == 0.123 and g < 0.1 then
        g = round(g*1000)
        b = round(b*1000)
        local s = specials[b]
        local h = round(s[#s])
        local handler = handlers[h]
        if handler then
            handler(s)
        else
            report_mptopdf("unknown special handler %s (2)",h)
        end
    else
        pdfcode(pdffinishtransparencycode())
        pdfcode(pdfrgbcode(mps.colormodel,r,g,b))
    end
end

function mps.setcmykcolor(c,m,y,k)
    pdfcode(pdffinishtransparencycode())
    pdfcode(pdfcmykcode(mps.colormodel,c,m,y,k))
end

function mps.setgray(s)
    pdfcode(pdffinishtransparencycode())
    pdfcode(pdfgraycode(mps.colormodel,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 t = { ... }
    local n = tonumber(t[#t-1])
    specials[n] = t
end

function mps.begindata()
end

function mps.enddata()
end

function mps.showpage()
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.

local lpegP, lpegR, lpegS, lpegC, lpegCc, lpegCs = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Cs

local digit    = lpegR("09")
local eol      = lpegS('\r\n')^1
local sp       = lpegP(' ')^1
local space    = lpegS(' \r\n')^1
local number   = lpegS('0123456789.-+')^1
local nonspace = lpegP(1-lpegS(' \r\n'))^1

local spec  = digit^2 * lpegP("::::") * digit^2
local text  = lpegCc("{") * (
        lpegP("\\") * ( (digit * digit * digit) / function(n) return "c" .. tonumber(n,8) end) +
                         lpegP(" ")             / function(n) return "\\c32" end + -- never in new mp
                         lpegP(1)               / function(n) return "\\c" .. byte(n) end
    ) * lpegCc("}")
local package = lpegCs(spec + text^0)

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

----- cnumber = lpegC(number)
local cnumber = number/tonumber -- we now expect numbers (feeds into %F)
local cstring = lpegC(nonspace)

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

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

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

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

local fshow              = (lpegP("(") * lpegC((1-lpegP(")"))^1) * lpegP(")") * space * cstring * space * cnumber * space * lpegP("fshow")) / mps.fshow
local fshow              = (lpegP("(") * lpegCs( ( lpegP("\\(")/"\\050" + lpegP("\\)")/"\\051" + (1-lpegP(")")) )^1 )
                            * lpegP(")") * space * cstring * space * cnumber * space * lpegP("fshow")) / mps.fshow

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

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

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

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

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

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

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

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

-- experimental

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

local procset = (
    lj + ml + lc +
    c + l + m + n + p + r +
    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 +
    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 + verbose + procset + preamble )^0

local function parse(m_data)
    if find(m_data,"%%BeginResource: procset mpost",1,true) then
     -- report_mptopdf("using sparse scanner, case 1")
        lpegmatch(captures_new,m_data)
    elseif find(m_data,"%%%%BeginProlog%s*%S+(.-)%%%%EndProlog") then
     -- report_mptopdf("using sparse scanner, case 2")
        lpegmatch(captures_new,m_data)
    else
     -- report_mptopdf("using verbose ps scanner")
        lpegmatch(captures_old,m_data)
    end
end

-- main converter

local a_colormodel = attributes.private('colormodel')

function mptopdf.convertmpstopdf(name)
    resetall()
    local ok, m_data, n = resolvers.loadbinfile(name, 'tex') -- we need a binary load !
    if ok then
        mps.colormodel = texgetattribute(a_colormodel)
        statistics.starttiming(mptopdf)
        mptopdf.nofconverted = mptopdf.nofconverted + 1
        pdfcode(formatters["\\letterpercent\\space mptopdf begin: n=%s, file=%s"](mptopdf.nofconverted,file.basename(name)))
        pdfcode("q 1 0 0 1 0 0 cm")
        parse(m_data)
        pdfcode(pdffinishtransparencycode())
        pdfcode("Q")
        pdfcode("\\letterpercent\\space mptopdf end")
        resetall()
        statistics.stoptiming(mptopdf)
    else
        report_mptopdf("file %a not found",name)
    end
end

-- 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)

-- interface

interfaces.implement {
    name      = "convertmpstopdf",
    arguments = "string",
    actions   = mptopdf.convertmpstopdf
}