--- Drawing primitives, frames, transformations and MetaPost bridge.
--
--  drawing.lua
--  speedata publisher
--
--  For a list of authors see `git blame'
--  See file COPYING in the root directory for license info.

file_start("drawing.lua")

local M = {}

local colors_module = require("publisher.colors")
local metapost = require("publisher.metapost")

function M.pdf_draw_pos(x,y)
    x = sp_to_bp(x)
    y = sp_to_bp(y)
    local wd = 0.1
    return string.format("q 0 g 0.2 w %g %g m %g %g l %g %g l %g %g l h f Q ",x - wd,y - wd,x - wd,y + wd,x + wd,y + wd,x + wd,y - wd)
end
function M.pdf_circle_pos(x,y)
    return M.circle_pdfstring(x,y,10000,10000,"0G 0g","0G 0g",0)
end
function M.pdf_circle_pos_big(x,y)
    return M.circle_pdfstring(x,y,100000,100000,"0G 0g","0G 0g",0)
end
function M.pdf_curveto(x1,y1,x2,y2,x3,y3)
    x1 = sp_to_bp(x1)
    y1 = sp_to_bp(y1)
    x2 = sp_to_bp(x2)
    y2 = sp_to_bp(y2)
    x3 = sp_to_bp(x3)
    y3 = sp_to_bp(y3)
    return string.format("%g %g %g %g %g %g c",x1,y1,x2,y2,x3,y3)
end
function M.pdf_moveto( x,y )
    x = sp_to_bp(x)
    y = sp_to_bp(y)
    return string.format("%g %g m",x,y)
end
function M.pdf_lineto( x,y )
    x = sp_to_bp(x)
    y = sp_to_bp(y)
    return string.format("%g %g l",x,y)
end


function M.pdf_draw_pos_bp(x,y)
    local wd = 0.1
    return string.format("q 0 g 0.2 w %g %g m %g %g l %g %g l %g %g l h f Q ",x - wd,y - wd,x - wd,y + wd,x + wd,y + wd,x + wd,y - wd)
end

function M.pdf_circle_pos_bp(x,y)
    return M.circle_pdfstring_bp(x,y,0.5,0.5,"0G 0g","0G 0g",0)
end
function M.pdf_circle_pos_big_bp(x,y)
    return M.circle_pdfstring_bp(x,y,3,3,"0G 0g","0G 0g",0)
end
function M.pdf_curveto_bp(x1,y1,x2,y2,x3,y3)
    return string.format("%g %g %g %g %g %g c",x1,y1,x2,y2,x3,y3)
end
function M.pdf_moveto_bp( x,y )
    return string.format("%g %g m",x,y)
end

function M.pdf_lineto_bp( x,y )
    return string.format("%g %g l",x,y)
end

function M.transparentcolorstack()
    if publisher.defaultcolorstack == 0 then
        publisher.defaultcolorstack = pdf.newcolorstack("0 g 0 G/TRP1 gs","direct",true)
    end
end

function M.concat_transformation( a, b )
    local c = {}
    c[1] = a[1] * b[1] + a[2] * b[3]
    c[2] = a[1] * b[2] + a[2] * b[4]
    c[3] = a[3] * b[1] + a[4] * b[3]
    c[4] = a[3] * b[2] + a[4] * b[4]
    c[5] = a[5] * b[1] + a[6] * b[3] + b[5]
    c[6] = a[5] * b[2] + a[6] * b[4] + b[6]
    return c
end

-- Place a text in the background
function M.bgtext( box, textstring, angle, colorname, fontfamily, bgsize)
    local colorindex = colors_module.colors[colorname].index
    local boxheight, boxwidth = box.height, box.width
    local angle_rad = -1 * math.rad(angle)
    local sin = math.sin(angle_rad)
    local cos = math.cos(angle_rad)

    local a = publisher.par:new(nil,"bgtext")
    a:append(textstring, {fontfamily = fontfamily,color = colorindex})
    a:mknodelist(publisher.data)
    local textbox = node.hpack(a.objects[1])
    local rotated_height = sin * textbox.width  + cos * textbox.height
    local scale
    local shift_up = 0
    if bgsize == "contain" then
        scale = boxheight  / rotated_height
    else
        scale = 1
        shift_up = sp_to_bp((boxheight - rotated_height) / 2)
    end
    local rotated_width  = sin * textbox.height + cos * textbox.width
    local shift_right = sp_to_bp( (boxwidth - rotated_width * scale ) / 2)

    -- rotate: [cos θ sin θ −sin θ cos θ 0 0 ]
    local rotate_matrix = {   cos, sin, -1 * sin,   cos,           0, 0        }
    local scale_matrix  = { scale,   0,        0, scale,           0, 0        }
    local shift_matrix  = {     1,   0,        0,     1, shift_right, shift_up }
    local result_matrix
    result_matrix = M.concat_transformation(rotate_matrix,scale_matrix)
    result_matrix = M.concat_transformation(result_matrix,shift_matrix)
    local matrixstring = string.format("%g %g %g %g %d %g",math.round(result_matrix[1],3),math.round(result_matrix[2],3),math.round(result_matrix[3],3),math.round(result_matrix[4],3),math.round(result_matrix[5],3),math.round(result_matrix[6],3))
    local x = M.matrix( textbox, matrixstring,0,0 )
    x = node.hpack(x)
    x.width = 0
    x.height = 0
    box = node.insert_before(box,box,x)
    box = node.hpack(box)
    return box
end

--- Draw a background behind the rectangular (box) object.
function M.background( box, colorname,origin )
    -- color '-' means 'no color'
    if colorname == "-" then return box end
    if not colors_module.colors[colorname] then
        if origin then
            main.log("warn","Background: color is not defined","name",colorname,"from",origin)
        else
            main.log("warn","Background: color is not defined","name",colorname)
        end
        return box
    end
    local colentry = colors_module.colors[colorname]

    local pdfcolorstring = colentry.pdfstring
    local wd, ht, dp = sp_to_bp(box.width),sp_to_bp(box.height),sp_to_bp(box.depth)
    local n = node.new("whatsit","pdf_literal")
    publisher.setprop(n,"origin","background")
    publisher.setprop(n,"role",publisher.get_rolenum("Artifact"))
    n.data = string.format("q %s 0 -%g %g %g re f Q",pdfcolorstring,dp,wd,ht + dp)
    n.mode = 0
    if colentry.alpha then
        publisher.setprop(n,"opacity",colentry.alpha)
    end
    if node.type(box.id) == "hlist" then
        -- pdfliteral does not use up any space, so we can add it to the already packed box.
        n.next = box.list
        box.list.prev = n
        box.list = n
        return box
    else
        n.next = box
        box.prev = n
        n = node.hpack(n)
        return n
    end
end

--- Draw a colored frame around a given TeX box.
--- The control points of the frame are illustrated in
--- doc/img/roundedcorners.svg.
---
--- `obj` is a table with the keys:
---   box: the TeX box
---   colorname: the name of a color (defaults to "black")
---   width: the width of the border (defaults to 0)
---   clip: should the outside be clipped?
---   b_x_y_radius (x = b|t, y = r|l): the radius of the corners
---
--- Returns a hbox.
function M.frame(obj)
    local  box, width
    box          = obj.box
    width        = obj.rulewidth or 0
    local b_b_r_radius = sp_to_bp(obj.b_b_r_radius)
    local b_t_r_radius = sp_to_bp(obj.b_t_r_radius)
    local b_t_l_radius = sp_to_bp(obj.b_t_l_radius)
    local b_b_l_radius = sp_to_bp(obj.b_b_l_radius)

    local b_b_r_radius_inner = math.round(math.max(sp_to_bp(obj.b_b_r_radius) - width / publisher.factor,0),3)
    local b_t_r_radius_inner = math.round(math.max(sp_to_bp(obj.b_t_r_radius) - width / publisher.factor,0),3)
    local b_t_l_radius_inner = math.round(math.max(sp_to_bp(obj.b_t_l_radius) - width / publisher.factor,0),3)
    local b_b_l_radius_inner = math.round(math.max(sp_to_bp(obj.b_b_l_radius) - width / publisher.factor,0),3)

    -- See https://en.wikipedia.org/wiki/File:Circle_and_cubic_bezier.svg
    -- https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve
    -- 0.5522847498
    -- http://spencermortensen.com/articles/bezier-circle/
    -- 0.551915024494
    local circle_bezier = 0.551915024494

    local colentry = colors_module.get_colentry_from_name(obj.colorname,"black")
    if not colentry then
        main.log("error","Color is not defined","name",tostring(obj.colorname))
        colentry = colors_module.colors["black"]
    end

    local pdfcolorstring = colentry.pdfstring
    local wd, ht, dp = sp_to_bp(box.width),sp_to_bp(box.height),sp_to_bp(box.depth)
    local rw = math.round(width / publisher.factor,3) -- width of stroke
    if obj.colorname == "-" then
        rw = 0
    end

    -- outer boundary
    local x1, y1   = -rw + b_b_l_radius                     , -rw - dp
    local x2, y2   =  rw + wd - b_b_r_radius                , -rw - dp
    local x3, y3   =  rw + wd - circle_bezier * b_b_r_radius, -rw - dp
    local x4, y4   =  rw + wd                               , -rw - dp + circle_bezier * b_b_r_radius
    local x5, y5   =  rw + wd                               , -rw + b_b_r_radius
    local x6, y6   =  rw + wd                               ,  rw + ht - b_t_r_radius
    local x7, y7   =  rw + wd                               ,  rw + ht - circle_bezier * b_t_r_radius
    local x8, y8   =  rw + wd - circle_bezier * b_t_r_radius,  rw + ht
    local x9, y9   =  rw + wd - b_t_r_radius                ,  rw + ht
    local x10, y10 = -rw + b_t_l_radius                     ,  rw + ht
    local x11, y11 = -rw + circle_bezier * b_t_l_radius     ,  rw + ht
    local x12, y12 = -rw                                    ,  rw + ht - circle_bezier * b_t_l_radius
    local x13, y13 = -rw                                    ,  rw + ht - b_t_l_radius
    local x14, y14 = -rw                                    , -rw - dp + b_b_l_radius
    local x15, y15 = -rw                                    , -rw -dp + circle_bezier * b_b_l_radius
    local x16, y16 = -rw + circle_bezier * b_b_l_radius     , -rw - dp

    x1,  y1  = math.round(x1,3),  math.round(y1,3)
    x2,  y2  = math.round(x2,3),  math.round(y2,3)
    x3,  y3  = math.round(x3,3),  math.round(y3,3)
    x4,  y4  = math.round(x4,3),  math.round(y4,3)
    x5,  y5  = math.round(x5,3),  math.round(y5,3)
    x6,  y6  = math.round(x6,3),  math.round(y6,3)
    x7,  y7  = math.round(x7,3),  math.round(y7,3)
    x8,  y8  = math.round(x8,3),  math.round(y8,3)
    x9,  y9  = math.round(x9,3),  math.round(y9,3)
    x10, y10 = math.round(x10,3), math.round(y10,3)
    x11, y11 = math.round(x11,3), math.round(y11,3)
    x12, y12 = math.round(x12,3), math.round(y12,3)
    x13, y13 = math.round(x13,3), math.round(y13,3)
    x14, y14 = math.round(x14,3), math.round(y14,3)
    x15, y15 = math.round(x15,3), math.round(y15,3)
    x16, y16 = math.round(x16,3), math.round(y16,3)


    -- inner boundary
    local xx1, yy1   =   b_b_l_radius_inner                      , -dp
    local xx2, yy2   =   wd - b_b_r_radius_inner                 , -dp
    local xx3, yy3   =   wd - circle_bezier * b_b_r_radius_inner , -dp
    local xx4, yy4   =   wd                                      , -dp + circle_bezier * b_b_r_radius_inner
    local xx5, yy5   =   wd                                      , b_b_r_radius_inner
    local xx6, yy6   =   wd                                      , ht - b_t_r_radius_inner
    local xx7, yy7   =   wd                                      , ht - circle_bezier * b_t_r_radius_inner
    local xx8, yy8   =   wd - circle_bezier * b_t_r_radius_inner , ht
    local xx9, yy9   =   wd - b_t_r_radius_inner                 , ht
    local xx10, yy10 =   b_t_l_radius_inner                      , ht
    local xx11, yy11 =   circle_bezier * b_t_l_radius_inner      , ht
    local xx12, yy12 =   0                                       , ht - circle_bezier * b_t_l_radius_inner
    local xx13, yy13 =   0                                       , ht - b_t_l_radius_inner
    local xx14, yy14 =   0                                       , -dp + b_b_l_radius_inner
    local xx15, yy15 =   0                                       , -dp + circle_bezier * b_b_l_radius_inner
    local xx16, yy16 =   circle_bezier * b_b_l_radius_inner      , -dp

    xx1,  yy1  = math.round(xx1,3),  math.round(yy1,3)
    xx2,  yy2  = math.round(xx2,3),  math.round(yy2,3)
    xx3,  yy3  = math.round(xx3,3),  math.round(yy3,3)
    xx4,  yy4  = math.round(xx4,3),  math.round(yy4,3)
    xx5,  yy5  = math.round(xx5,3),  math.round(yy5,3)
    xx6,  yy6  = math.round(xx6,3),  math.round(yy6,3)
    xx7,  yy7  = math.round(xx7,3),  math.round(yy7,3)
    xx8,  yy8  = math.round(xx8,3),  math.round(yy8,3)
    xx9,  yy9  = math.round(xx9,3),  math.round(yy9,3)
    xx10, yy10 = math.round(xx10,3), math.round(yy10,3)
    xx11, yy11 = math.round(xx11,3), math.round(yy11,3)
    xx12, yy12 = math.round(xx12,3), math.round(yy12,3)
    xx13, yy13 = math.round(xx13,3), math.round(yy13,3)
    xx14, yy14 = math.round(xx14,3), math.round(yy14,3)
    xx15, yy15 = math.round(xx15,3), math.round(yy15,3)
    xx16, yy16 = math.round(xx16,3), math.round(yy16,3)

    local n_clip, rule_clip

    if obj.clip then
        n_clip = node.new("whatsit","pdf_literal")
        publisher.setprop(n_clip,"origin","obj.clip")
        rule_clip = {}
        rule_clip[#rule_clip + 1] = string.format("%g %g m",xx1,yy1)
        rule_clip[#rule_clip + 1] = string.format("%g %g l",xx2,yy2)
        rule_clip[#rule_clip + 1] = string.format("%g %g %g %g %g %g c", xx3,yy3, xx4,yy4, xx5, yy5 )
        rule_clip[#rule_clip + 1] = string.format("%g %g l",xx6, yy6)
        rule_clip[#rule_clip + 1] = string.format("%g %g %g %g %g %g c", xx7,yy7,xx8,yy8, xx9,yy9  )
        rule_clip[#rule_clip + 1] = string.format("%g %g l",xx10, yy10)
        rule_clip[#rule_clip + 1] = string.format("%g %g %g %g %g %g c", xx11,yy11,xx12,yy12, xx13,yy13  )
        rule_clip[#rule_clip + 1] = string.format("%g %g l",xx14,yy14 )
        rule_clip[#rule_clip + 1] = string.format("%g %g %g %g %g %g c W n ", xx15,yy15,xx16,yy16, xx1,yy1  )
    end

    local n = node.new("whatsit","pdf_literal")
    publisher.setprop(n,"origin","publisher.frame")
    local rule = {}

    -- We need to add q .. Q because the color would leak into the inner objects (#55)
    rule[#rule + 1] = string.format("q %s",pdfcolorstring)
    rule[#rule + 1] = string.format("%g w",rw)           -- rule width

    rule[#rule + 1] = string.format("%g %g m",xx1,yy1)
    rule[#rule + 1] = string.format("%g %g l",xx2,yy2)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", xx3,yy3, xx4,yy4, xx5, yy5 )
    rule[#rule + 1] = string.format("%g %g l",xx6, yy6)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", xx7,yy7,xx8,yy8, xx9,yy9  )
    rule[#rule + 1] = string.format("%g %g l",xx10, yy10)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", xx11,yy11,xx12,yy12, xx13,yy13  )
    rule[#rule + 1] = string.format("%g %g l",xx14,yy14 )
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c ", xx15,yy15,xx16,yy16, xx1,yy1  )

    rule[#rule + 1] = string.format("%g %g m",x1,y1)
    rule[#rule + 1] = string.format("%g %g l",x2,y2)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", x3,y3, x4,y4, x5, y5 )
    rule[#rule + 1] = string.format("%g %g l",x6, y6)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", x7,y7,x8,y8, x9,y9  )
    rule[#rule + 1] = string.format("%g %g l",x10, y10)
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", x11,y11,x12,y12, x13,y13  )
    rule[#rule + 1] = string.format("%g %g l",x14,y14 )
    rule[#rule + 1] = string.format("%g %g %g %g %g %g c", x15,y15,x16,y16, x1,y1  )
    if rw == 0 then
        rule[#rule + 1] = "n"
    else
        rule[#rule + 1] = "f*"
    end
    rule[#rule + 1] = "Q"

    n.data = table.concat(rule," ")
    if colentry.alpha then
        publisher.setprop(n,"opacity",colentry.alpha)
    end

    publisher.setprop(n,"origin","frame")
    publisher.setprop(n,"role",publisher.get_rolenum("Artifact"))


    if obj.clip then
        n_clip.data = table.concat(rule_clip, " ")
        node.setproperty(n_clip,{origin = "frame/clip"})
    end

    if n_clip and node.is_node(n_clip) then
        publisher.setprop(n_clip,"origin","frame")
        publisher.setprop(n_clip,"role",publisher.get_rolenum("Artifact"))
    end


    local pdf_save    = node.new("whatsit","pdf_save")
    local pdf_restore = node.new("whatsit","pdf_restore")

    node.insert_after(pdf_save,pdf_save,n)
    if obj.clip then
        node.insert_after(n,n,n_clip)
        node.insert_after(n_clip,n_clip,box)
    else
        node.insert_after(n,n,box)
    end

    local hvbox = node.hpack(pdf_save)
    local savedp = hvbox.depth
    hvbox.depth = 0
    node.insert_after(hvbox,node.tail(hvbox),pdf_restore)
    hvbox = node.vpack(hvbox)
    hvbox = node.vpack(hvbox)
    hvbox.depth = savedp
    return hvbox
end

function M.clip(obj)
    local box = obj.box
    local wd, ht, dp = sp_to_bp(box.width),sp_to_bp(box.height),sp_to_bp(box.depth)

    local kern_left = node.new("kern")
    local kern_top = node.new("kern")

    if obj.clip_width_sp ~= 0 then
        if obj.clip_left_sp ~= 0 or obj.clip_right_sp == 0 then
            obj.clip_right_sp = box.width - obj.clip_width_sp - obj.clip_left_sp
        else
            obj.clip_left_sp = box.width - obj.clip_width_sp - obj.clip_right_sp
        end
    end

    if obj.clip_height_sp ~= 0 then
        if obj.clip_top_sp ~= 0 or obj.clip_bottom_sp == 0  then
            obj.clip_bottom_sp = box.height - obj.clip_height_sp - obj.clip_top_sp
        else
            obj.clip_top_sp = box.height - obj.clip_height_sp - obj.clip_bottom_sp
        end
    end



    local clip_top_bp = sp_to_bp(obj.clip_top_sp)
    local clip_bottom_bp = sp_to_bp(obj.clip_bottom_sp)
    local clip_left_bp = sp_to_bp(obj.clip_left_sp)
    local clip_right_bp = sp_to_bp(obj.clip_right_sp)

    if obj.method == "clip" then
        kern_left.kern = -1 * obj.clip_left_sp
        kern_top.kern = -1 * obj.clip_top_sp
    end

    node.insert_after(kern_left,kern_left,box)
    box = node.hpack(kern_left)
    node.insert_after(kern_top,kern_top,box)
    box = node.vpack(kern_top)

    local n_clip, rule_clip
    n_clip = node.new("whatsit","pdf_literal")
    publisher.setprop(n_clip,"origin","obj.clip")
    rule_clip = {}
    if obj.method == "clip" then
        rule_clip[#rule_clip + 1] = string.format(" %g %g %g %g re W n ", 0,  -1 * dp + clip_bottom_bp, wd - clip_right_bp - clip_left_bp,ht+dp - clip_bottom_bp - clip_top_bp )
    elseif obj.method == "frame" then
        rule_clip[#rule_clip + 1] = string.format(" %g %g %g %g re W n ", clip_left_bp,  -1 * dp + clip_bottom_bp, wd - clip_right_bp - clip_left_bp,ht+dp - clip_bottom_bp - clip_top_bp )
    else
        err("Clip: method %s not implemented",obj.method)
    end

    n_clip = node.new("whatsit","pdf_literal")
    n_clip.data = table.concat(rule_clip, " ")
    node.setproperty(n_clip,{origin = "frame/clip (2)"})

    local pdf_save    = node.new("whatsit","pdf_save")
    local pdf_restore = node.new("whatsit","pdf_restore")

    node.insert_after(pdf_save,pdf_save,n_clip)
    node.insert_after(n_clip,n_clip,box)

    local hvbox = node.hpack(pdf_save)
    local savedp = hvbox.depth
    hvbox.depth = 0
    node.insert_after(hvbox,node.tail(hvbox),pdf_restore)
    hvbox = node.vpack(hvbox)
    if obj.method == "clip" then
        hvbox.width = hvbox.width - obj.clip_right_sp
        hvbox.height = hvbox.height - obj.clip_bottom_sp
    end
    hvbox.depth = savedp

    return hvbox
end

--- Get a PDF string for a circle. Control points are illustrated in
--- doc/img/circlepoints.svg.
function M.circle_pdfstring(center_x, center_y, radiusx_sp, radiusy_sp, stroke_colorstring, fill_colorstring, rulewidth_sp )
    local circle_bezier = 0.551915024494

    local shift_dn, shift_rt = -radiusy_sp + center_y, -radiusx_sp + center_x
    local dx = radiusx_sp * (1 - circle_bezier)
    local dy = radiusy_sp * (1 - circle_bezier)

    local x1 = shift_rt
    local y1 = shift_dn + radiusy_sp
    local x2 = x1
    local y2 = shift_dn + radiusy_sp * 2 - dy
    local x3 = shift_rt + dx
    local y3 = shift_dn + radiusy_sp * 2
    local x4 = shift_rt + radiusx_sp
    local y4 = shift_dn + radiusy_sp * 2
    local x5 = shift_rt + radiusx_sp * 2 - dx
    local y5 = y3
    local x6 = shift_rt + radiusx_sp * 2
    local y6 = y2
    local x7 = x6
    local y7 = y1
    local x8 = x6
    local y8 = shift_dn + dy
    local x9 = x5
    local y9 = shift_dn
    local x10 = x4
    local y10 = y9
    local x11 = x3
    local y11 = y9
    local x12 = x1
    local y12 = y8
    local circle = {}
    circle[#circle + 1] = "q"
    circle[#circle + 1] = string.format("%g w %s %s", sp_to_bp(rulewidth_sp), stroke_colorstring, fill_colorstring)
    circle[#circle + 1] = M.pdf_moveto(x1,y1)
    circle[#circle + 1] = M.pdf_curveto(x2,y2,x3,y3,x4,y4)
    circle[#circle + 1] = M.pdf_curveto(x5,y5,x6,y6,x7,y7)
    circle[#circle + 1] = M.pdf_curveto(x8,y8,x9,y9,x10,y10)
    circle[#circle + 1] = M.pdf_curveto(x11,y11,x12,y12,x1,y1)
    if fill_colorstring == "" then
        circle[#circle + 1] = "s"
    else
        circle[#circle + 1] = "b"
    end
    circle[#circle + 1] = "Q"
    return table.concat(circle, " ")
end

function M.circle_pdfstring_bp(center_x, center_y, radiusx_bp, radiusy_bp, stroke_colorstring, fill_colorstring, rulewidth_bp )
    local circle_bezier = 0.551915024494

    local shift_dn, shift_rt = -radiusy_bp + center_y, -radiusx_bp + center_x
    local dx = radiusx_bp * (1 - circle_bezier)
    local dy = radiusy_bp * (1 - circle_bezier)

    local x1 = shift_rt
    local y1 = shift_dn + radiusy_bp
    local x2 = x1
    local y2 = shift_dn + radiusy_bp * 2 - dy
    local x3 = shift_rt + dx
    local y3 = shift_dn + radiusy_bp * 2
    local x4 = shift_rt + radiusx_bp
    local y4 = shift_dn + radiusy_bp * 2
    local x5 = shift_rt + radiusx_bp * 2 - dx
    local y5 = y3
    local x6 = shift_rt + radiusx_bp * 2
    local y6 = y2
    local x7 = x6
    local y7 = y1
    local x8 = x6
    local y8 = shift_dn + dy
    local x9 = x5
    local y9 = shift_dn
    local x10 = x4
    local y10 = y9
    local x11 = x3
    local y11 = y9
    local x12 = x1
    local y12 = y8
    local circle = {}
    circle[#circle + 1] = "q"
    circle[#circle + 1] = string.format("%g w %s %s", rulewidth_bp, stroke_colorstring, fill_colorstring)
    circle[#circle + 1] = M.pdf_moveto_bp(x1,y1)
    circle[#circle + 1] = M.pdf_curveto_bp(x2,y2,x3,y3,x4,y4)
    circle[#circle + 1] = M.pdf_curveto_bp(x5,y5,x6,y6,x7,y7)
    circle[#circle + 1] = M.pdf_curveto_bp(x8,y8,x9,y9,x10,y10)
    circle[#circle + 1] = M.pdf_curveto_bp(x11,y11,x12,y12,x1,y1)
    if fill_colorstring == "" then
        circle[#circle + 1] = "s"
    else
        circle[#circle + 1] = "b"
    end
    circle[#circle + 1] = "Q"
    return table.concat(circle, " ")
end

--- Draw a circle
function M.circle( radiusx_sp, radiusy_sp, colorname,framecolorname,rulewidth_sp)
    if rulewidth_sp < 5 then
        framecolorname = colorname
    end
    local colentry = colors_module.get_colentry_from_name(colorname)
    if not colentry then
        err("Color %q unknown, reverting to black",colorname or "(no color name given)")
        colentry = colors_module.colors["black"]
    end
    local framecolentry = colors_module.get_colentry_from_name(framecolorname)

    if not framecolentry then
        err("Color %q unknown, reverting to black",framecolorname or "(no color name given)")
        framecolentry = colors_module.colors["black"]
    end
    local fillcolor   = colentry.pdfstring_fill
    local bordercolor = framecolentry.pdfstring_stroking

    local paint = node.new("whatsit","pdf_literal")
    paint.data = M.circle_pdfstring(0,0,radiusx_sp, radiusy_sp, bordercolor, fillcolor, rulewidth_sp)
    if colentry.alpha then
        publisher.setprop(paint,"opacity",colentry.alpha)
    end
    local v = node.vpack(paint)
    return v
end

function M.do_metapostimage(dataxml,txt,width,height,clip)
    publisher.metapostgraphics["_image"] = txt
    local cp = publisher.current_page
    local width_sp, height_sp
    if tonumber(width) then
        width_sp = cp.grid:width_sp(tonumber(width))
    else
        width_sp = tex.sp(width)
    end
    if tonumber(height) then
        height_sp = cp.grid:height_sp(tonumber(height))
    else
        height_sp = tex.sp(height)
    end

    local box, bbox = metapost.boxgraphic(width_sp,height_sp,"_image",{},{})
    if not box then
        main.log("error","Could not create metapost image")
        return
    end
    local image = {
        xsize = publisher.bp_to_sp(bbox[3] - bbox[1]),
        ysize = publisher.bp_to_sp(bbox[4] - bbox[2])
    }

    image.width = image.xsize
    image.height = image.ysize

    height    = publisher.set_image_length(dataxml,height,   "height") or image.height
    width     = publisher.set_image_length(dataxml,width,    "width" ) or image.width
    publisher.minheight = publisher.set_image_length(dataxml,publisher.minheight,"height") or 0
    publisher.minwidth  = publisher.set_image_length(dataxml,publisher.minwidth, "width" ) or 0
    publisher.maxheight = publisher.set_image_length(dataxml,publisher.maxheight,"height") or publisher.maxdimen
    publisher.maxwidth  = publisher.set_image_length(dataxml,publisher.maxwidth, "width" ) or publisher.maxdimen

    if not clip then
        width, height = publisher.calculate_image_width_height( image, width,height,publisher.minwidth,publisher.minheight,publisher.maxwidth, publisher.maxheight,publisher.stretch)

        box = node.hpack(box)
        box.width = width
        box.height = height
        box = node.vpack(box)

        local scaler      = node.new("whatsit","pdf_literal")
        local pdf_save    = node.new("whatsit","pdf_save")
        local pdf_restore = node.new("whatsit","pdf_restore")

        local scaleX = math.round(width / image.xsize,3)
        local scaleY = math.round(height / image.ysize,3)
        local shiftX = -bbox[1] * scaleX
        local shiftY = -bbox[2] * scaleY + sp_to_bp(height) * ( scaleY - 1 )
        local scalerdata = string.format("%g 0 0 %g %g %g cm ", scaleX,scaleY,shiftX,shiftY )
        scaler.data = scalerdata

        box.head = node.insert_before(box.head,box.head, scaler)
        box.head = node.insert_before(box.head,box.head,pdf_save)
        box.height = 0
        box.depth = 0
        box = node.vpack(box)
        node.insert_after(box.head,box.head,pdf_restore)
        box.height = height
    end

    return {box,nil}
end

function M.mpbox(parameter,width,height)
    local width_sp = width
    local height_sp = height
    local extra_parameter = {}

    extra_parameter.bordertopwidth = sp_to_bp(parameter.border_top_width) .. "bp"
    extra_parameter.borderbottomwidth = sp_to_bp(parameter.border_bottom_width) .. "bp"
    extra_parameter.borderleftwidth = sp_to_bp(parameter.border_left_width) .. "bp"
    extra_parameter.borderrightwidth = sp_to_bp(parameter.border_right_width) .. "bp"
    -- no width if style is none
    if parameter.border_top_style == "none" then
        extra_parameter.bordertopwidth = "0bp"
    end
    if parameter.border_bottom_style == "none" then
        extra_parameter.borderbottomwidth = "0bp"
    end
    if parameter.border_left_style == "none" then
        extra_parameter.borderleftwidth = "0bp"
    end
    if parameter.border_right_style == "none" then
        extra_parameter.borderrightwidth = "0bp"
    end

    extra_parameter.paddingtop = sp_to_bp(parameter.padding_top or 0) .. "bp"
    extra_parameter.paddingbottom = sp_to_bp(parameter.padding_bottom or 0) .. "bp"
    extra_parameter.paddingleft = sp_to_bp(parameter.padding_left or 0) .. "bp"
    extra_parameter.paddingright = sp_to_bp(parameter.padding_right or 0) .. "bp"

    extra_parameter.colors = {
        bordertopcolor = parameter.border_top_color,
        borderbottomcolor = parameter.border_bottom_color,
        borderleftcolor = parameter.border_left_color,
        borderrightcolor = parameter.border_right_color
    }
    extra_parameter.strings = {
        bordertopstyle = parameter.border_top_style,
        borderbottomstyle = parameter.border_bottom_style,
        borderleftstyle = parameter.border_left_style,
        borderrightstyle = parameter.border_right_style
    }
    extra_parameter.boolean = {
        debug = parameter.debug == true,
    }

    local mptext = [[
        wd = box.width  ;
        ht = box.height - bordertopwidth - borderbottomwidth ;
        if ht < 0: ht := 0; fi;
        if wd < 0: wd := 0; fi;
        z1 = (0,-borderbottomwidth - paddingbottom);
        x2 = borderleftwidth;
        x3 = x2 + wd + paddingleft + paddingright;
        x4 = x3 + borderrightwidth;

        y2 = y1 + borderbottomwidth;
        y3 = y2 + ht + borderbottomwidth + paddingtop + paddingbottom;
        y4 = y3 + bordertopwidth;

        % draw z1 -- (x4,y1) -- z4 -- (x1,y4) -- cycle;
        % draw z2 -- (x3,y2) -- z3 -- (x2,y3) -- cycle;

        picture border; border = nullpicture;

        path clip_top, clip_bottom, clip_left, clip_right;
        clip_top = (x1,y4) -- (x2,y3) -- z3 -- z4 -- cycle;
        clip_bottom = (x1,y1) -- (x2,y2) -- (x3,y2) -- (x4,y1) -- cycle;
        clip_left = (x1,y1) -- (x2,y2) -- (x2,y3) -- (x1,y4) -- cycle;
        clip_right = (x4,y1) -- (x3,y2) -- (x3,y3) -- (x4,y4) -- cycle;
        def isdarkcolor(expr c) =
            (redpart c < 0.2) and (greenpart c < 0.2) and (bluepart c < 0.2 )
        enddef;

        def drawborder(expr bordercolor, bwd, style, a, b, clippath, pos) =
            begingroup;
            interim linecap := butt;

            color col; col = bordercolor;
            string str;
            str = "withcolor col withpen pencircle scaled " & decimal bwd ;
            if style = "dashed":
                str := str & " dashed dashpattern(on 4bp off 5bp)"
            elseif ( style = "inset" )  and  (  (pos = "top" )  or (pos = "left") ):
                if isdarkcolor(col):
                    str := str & " withcolor 0.2[col, white] ";
                else:
                    str := str & " withcolor 0.5[col, black] ";
                fi;
            elseif ( style = "inset" ) and isdarkcolor(col) and ( (pos = "bottom" ) or (pos = "right") ):
                str := str & " withcolor 0.5[col, white] ";
            elseif ( style = "outset" )  and  (  (pos = "right" ) or (pos = "bottom") ):
                str := str & " withcolor 0.5[col, black] ";
            fi;
            drawoptions(scantokens(str));

            draw a -- b ;
            clip currentpicture to clippath ;
            addto border also currentpicture ;
            endgroup;
        enddef;

        y34 = 0.5[y3,y4];
        y12 = 0.5[y1,y2];
        x12 = 0.5[x1,x2];
        x34 = 0.5[x3,x4];

        drawborder(bordertopcolor,bordertopwidth, bordertopstyle, (x1,y34),(x4,y34), clip_top, "top" );
        drawborder(borderbottomcolor,borderbottomwidth,borderbottomstyle, (x1,y12),(x4,y12), clip_bottom, "bottom");
        drawborder(borderleftcolor,borderleftwidth, borderleftstyle,(x12,y1),(x12,y4), clip_left, "left" );
        drawborder(borderrightcolor,borderrightwidth, borderrightstyle,(x34,y1),(x34,y4), clip_right, "right" );

        currentpicture := border;
        if debug:
            drawoptions(scaled 3bp withcolor black );
            drawdot(z1) withcolor blue;
            drawdot(z2) withcolor red;
            drawdot(z3) withcolor red;
            drawdot(z4) withcolor blue;
        fi;
    ]]
    publisher.metapostgraphics.__htmlbox = mptext

    local instr,bbox = metapost.boxgraphic(width_sp,height_sp,"__htmlbox",extra_parameter)
    if not instr then
        main.log("error","Could not create metapost image")
        return
    end

    local ret = node.hpack(instr,width,"exactly")
    ret.height = height
    if parameter.shiftdown then
        ret.height = ret.height + parameter.shiftdown
    end
    ret = node.vpack(ret)

    node.set_attribute(ret,publisher.att_dontadjustlineheight,1)
    ret.height = 0
    ret.depth = 0
    ret.shift = parameter.margin_left
    return ret
end

--- Create a colored area. width and height are in scaled points.
--- Optional border_color (color name) and border_width (in sp) draw a stroke
--- rectangle at the outer edge; the fill area shrinks inward by border_width.
function M.box( width_sp,height_sp,colorname,border_color,border_width_sp )
    local h,v
    local _width   = sp_to_bp(width_sp)
    local _height  = sp_to_bp(height_sp)
    local pdfcmds = {}

    -- Border stroke (full size, centered on edge → offset inward by half line width)
    if border_color and border_width_sp and border_width_sp > 0 then
        local bw = sp_to_bp(border_width_sp)
        local half = bw / 2
        local border_colentry = colors_module.colors[border_color]
        if not border_colentry then
            err("Color %q unknown, reverting to black",border_color or "(no color name given)")
            border_colentry = colors_module.colors["black"]
        end
        pdfcmds[#pdfcmds+1] = string.format("q %s %g w %g %g %g %g re S Q",
            border_colentry.pdfstring, bw,
            half, -half, _width - bw, -(_height - bw))
    end

    -- Fill (shrunk by border width on each side)
    if colorname ~= "-" then
        local colentry = colors_module.colors[colorname]
        if not colentry then
            err("Color %q unknown, reverting to black",colorname or "(no color name given)")
            colentry = colors_module.colors["black"]
        end
        local bw = border_width_sp and sp_to_bp(border_width_sp) or 0
        local fill_x = bw
        local fill_y = -bw
        local fill_w = _width - 2 * bw
        local fill_h = _height - 2 * bw
        if fill_w > 0 and fill_h > 0 then
            pdfcmds[#pdfcmds+1] = string.format("q %s 1 0 0 1 0 0 cm %g %g %g %g re f Q",
                colentry.pdfstring, fill_x, fill_y, fill_w, -fill_h)
        end
    end

    if #pdfcmds > 0 then
        local paint = node.new("whatsit","pdf_literal")
        publisher.setprop(paint,"role",publisher.get_rolenum("Artifact"))
        paint.data = table.concat(pdfcmds, " ")
        paint.mode = 0
        if colorname ~= "-" then
            local colentry = colors_module.colors[colorname]
            if colentry and colentry.alpha then
                publisher.set_attribute(paint,"color",colors_module.get_colorindex_from_name(colorname))
            end
        end
        local hglue = set_glue(nil,{width = 0, stretch = 2^16, stretch_order = 3 })
        h = node.insert_after(paint,paint,hglue)
        h = node.hpack(h,width_sp,"exactly")
    else
        h = publisher.create_empty_hbox_with_width(width_sp)
    end

    local vglue = set_glue(nil,{width = 0, stretch = 2^16, stretch_order = 3 })
    v = node.insert_after(h,h,vglue)
    v = node.vpack(h,height_sp,"exactly")
    return v
end

function M.addhrule(hbox)
    local n = node.new("whatsit","pdf_literal")
    n.data = string.format("q 0.3 w [2 1] 0 d 0 0 1 RG 0 %g  m %g %g l S Q",  sp_to_bp(hbox.height),  -sp_to_bp(hbox.width) ,  sp_to_bp(hbox.height) )
    local tail = node.tail(hbox)
    hbox = node.insert_after(hbox,tail,n)
    hbox = node.hpack(hbox)
    return hbox
end

function M.boxit( box )
    local box = node.hpack(box)

    local rule_width = 0.1
    local wd = box.width                 / publisher.factor - rule_width
    local ht = (box.height + box.depth)  / publisher.factor - rule_width
    local dp = box.depth                 / publisher.factor - rule_width / 2

    local wbox = node.new("whatsit","pdf_literal")
    wbox.data = string.format("q 0.1 G %g w %g %g %g %g re s Q", rule_width, rule_width / 2, -dp, -wd, ht)
    wbox.mode = 0
    -- Draw box at the end so its contents gets "below" it.
    local tmp = node.tail(box.list)
    tmp.next = wbox
    return box
end

function M.colorbar( wd,ht,dp,color,origin,orientation)
    local colorname = color
    if color == "-" then
        -- ok, ignore
    else
        if not colorname or colorname == "" then
            colorname = "black"
        end
        if not colors_module.colors[colorname] then
            err("Color %q not found",color)
            colorname = "black"
        end
    end

    local rule_start = node.new("whatsit","pdf_literal")
    publisher.setprop(rule_start,"origin","colorbar")
    if colorname ~= "-" then
        local ht_bp = sp_to_bp(ht)
        local wd_bp = sp_to_bp(wd)
        rule_start.mode = 0
        local data = "q ".. colors_module.colors[colorname].pdfstring
        local rule_width_bp, rule_length_bp

        if not publisher.options.tablerulefix then
            if orientation == "horizontal" then
                -- draw horizontal line
                rule_width_bp = ht_bp
                rule_length_bp = wd_bp
                data = data .. string.format(" %g w 0 %g m %g %g l s Q ", rule_width_bp, rule_width_bp / 2, rule_length_bp, rule_width_bp / 2)
            else
                rule_width_bp = wd_bp
                rule_length_bp = ht_bp
                data = data .. string.format(" %g w %g %g m %g %g l s Q ", rule_width_bp, rule_width_bp /2, rule_length_bp, rule_width_bp / 2, 0)
            end
        else
            if orientation == "horizontal" then
                rule_width_bp = ht_bp
                rule_length_bp = wd_bp
                -- draw horiztontal line a bit lower
                data = data .. string.format(" %g w %g %g m %g %g l s Q ", rule_width_bp, 0 , rule_width_bp / -2, rule_length_bp, rule_width_bp / -2)
            else
                rule_width_bp = wd_bp
                rule_length_bp = ht_bp
                -- draw the vertical borders in negative direction
                data = data .. string.format(" %g w %g %g m %g %g l s Q ", rule_width_bp, rule_width_bp / 2 , -1 * rule_length_bp, rule_width_bp / 2 , 0)
            end
        end
        rule_start.data = data
        if publisher.options.tablerulefix then
            publisher.setprop(rule_start,"data", data)
        end
        publisher.setprop(rule_start,"role",publisher.get_rolenum("Artifact"))
    end
    local h = node.hpack(rule_start)
    h.width = wd
    h.depth = dp
    h.height = ht
    origin = origin or "origin_colorbar"
    publisher.setprop(h,"origin",origin)
    return h
end

local explode = function(s,p)
   local t = { }
   for s in string.gmatch(s,p) do
       if s ~= "" then
           t[#t+1] = s
       end
   end
   return t
end

function M.montage( nodelist_background,nodelist_foreground, origin_x, origin_y )
    local wd_bg = nodelist_background.width
    local ht_bg = nodelist_background.height + nodelist_background.depth
    local dp_bg = nodelist_background.depth
    local wd_fg = nodelist_foreground.width
    local ht_fg = nodelist_foreground.height + nodelist_foreground.depth
    local wd = wd_bg - wd_fg
    local ht = ht_bg - ht_fg
    origin_x = 100 - origin_x
    origin_y = 100 - origin_y
    local x = math.round(  sp_to_bp(wd - (wd * origin_x) / 100  ), 3 )
    local y = math.round(  sp_to_bp(ht - (ht * origin_y) / 100  ), 3 )

    local pdf_literal_q = node.new("whatsit","pdf_literal")
    pdf_literal_q.data = string.format("1 0 0 1 %g %g cm ",x, y)

    local pdf_literal_Q = node.new("whatsit","pdf_literal")
    pdf_literal_Q.data = string.format("1 0 0 1 %g 0 cm ",-1 * math.round(sp_to_bp(wd_bg),3))

    local pdf_save    = node.new("whatsit","pdf_save")
    local pdf_restore = node.new("whatsit","pdf_restore")
    local hbox

    hbox = node.insert_before(nodelist_background,nodelist_background, pdf_save)
    hbox = node.insert_after(hbox,node.tail(hbox),pdf_literal_Q)
    hbox = node.insert_after(hbox,node.tail(hbox),pdf_literal_q)
    hbox = node.insert_after(hbox,node.tail(hbox),nodelist_foreground)

    hbox = node.hpack(hbox)
    hbox.depth = 0
    hbox = node.insert_after(hbox,node.tail(hbox),pdf_restore)

    hbox = node.vpack(hbox)
    hbox.width = wd_bg
    hbox.height = ht_bg - dp_bg
    hbox.depth = dp_bg
    return hbox
end

--- Apply transformation matrix to object given at _nodelist_. Called from commands#transformation.
function M.matrix( nodelist,matrix,origin_x,origin_y )
    local wd,ht = nodelist.width, nodelist.height
    local tbl = explode(matrix,"[^\t ]+")

    origin_x = 100 - origin_x
    origin_y = 100 - origin_y
    local x = math.round(  sp_to_bp(wd - (wd * origin_x) / 100  )  , 3 )
    local y = math.round(  sp_to_bp(ht - (ht * origin_y) / 100  )  , 3 )

    local pdf_literal_q = node.new("whatsit","pdf_literal")
    local pdf_literal_Q = node.new("whatsit","pdf_literal")

    pdf_literal_q.data = string.format("q 1 0 0 1 %g %g cm q %g %g %g %g %g %g cm q 1 0 0 1 %g %g cm ",x,-1 * y,tbl[1],tbl[2],tbl[3],tbl[4],tbl[5],tbl[6],x * -1,y )
    pdf_literal_Q.data = "Q Q Q"

    local pdf_save    = node.new("whatsit","pdf_save")
    local pdf_restore = node.new("whatsit","pdf_restore")

    local hbox
    hbox = node.insert_before(nodelist,nodelist,pdf_literal_q)
    node.insert_after(nodelist,nodelist,pdf_literal_Q)
    hbox = node.insert_before(hbox,pdf_literal_q,pdf_save)
    hbox = node.hpack(hbox)

    hbox.depth = 0
    node.insert_after(hbox,node.tail(hbox),pdf_restore)

    local newbox = node.vpack(hbox)
    return newbox
end

--- Rotate an object clockwise with a given angle (in degrees).
---
--- First rotate the object at the top left corner (default)
--- If the origin is not top left, we need to shift the object
function M.rotate( nodelist,angle,origin_x,origin_y )
    local wd,ht = nodelist.width, nodelist.height + nodelist.depth
    nodelist.width = 0
    nodelist.height = 0
    nodelist.depth = 0

    -- positive would be counter clockwise, but CSS is clockwise. So we multiply by -1
    local angle_rad = -1 * math.rad(angle)
    local sin = math.round(math.sin(angle_rad),3)
    local cos = math.round(math.cos(angle_rad),3)
    local q = node.new("whatsit","pdf_literal")
    q.mode = 0

    origin_x = 100 - origin_x
    origin_y = 100 - origin_y
    local x = math.round(  sp_to_bp(wd - (wd * origin_x) / 100  )  , 3 )
    local y = math.round(  sp_to_bp(ht - (ht * origin_y) / 100  )  , 3 )
    q.data = string.format("q 1 0 0 1 %g -%g cm  q %g %g %g %g 0 0 cm q 1 0 0 1 -%g %g cm ",x,y,cos,sin, -1 * sin,cos,x,y )
    q.next = nodelist
    local tail = node.tail(nodelist)
    local Q = node.new("whatsit","pdf_literal")
    Q.data = "Q Q Q"
    tail.next = Q
    local tmp = node.vpack(q)
    tmp.width = 0
    tmp.height = 0
    tmp.depth = 0
    return tmp
end

--- Rotate a table cell clockwise with a given angle (in degrees).
-- This is a simple and very basic implementation which needs to be extended in the future.
function M.rotateTd( nodelist,angle, width_sp)
    if angle % 360 == 0 then return nodelist end

    -- positive would be counter clockwise, but CSS is clockwise. So we multiply by -1
    local angle_rad = -1 * math.rad(angle)

    -- With multi paragraph table cells it is easier if we have only one node to deal with.
    if nodelist.next then
        nodelist = node.vpack(nodelist)
    end

    -- When text is rotated, it needs to get shifted to the right and to the bottom
    local _wd,_ht,_dp = nodelist.width, nodelist.height,nodelist.depth
    local ht = _ht + _dp

    nodelist.width = 0
    nodelist.height = 0
    nodelist.depth = 0

    local sin = math.round(math.sin(angle_rad),3)
    local cos = math.round(math.cos(angle_rad),3)

    local q = node.new("whatsit","pdf_literal")
    q.mode = 0

    local shift_x, shift_y

    local shift_x_wd = cos * _wd
    local shift_x_ht = sin * ht
    if shift_x_wd > 0 then shift_x_wd = 0 end
    if shift_x_ht > 0 then shift_x_ht = 0 end

    local shift_y_wd = -1 * sin * _wd
    local shift_y_ht = cos * ht
    if shift_y_wd > 0 then shift_y_wd = 0 end
    if shift_y_ht > 0 then shift_y_ht = 0 end

    shift_x =  sp_to_bp( shift_x_ht + shift_x_wd) * -1
    shift_y =  sp_to_bp( shift_y_ht + shift_y_wd)

    q.data = string.format("q %g %g %g %g %g %g cm ",cos,sin, -1 * sin,cos,shift_x,shift_y)
    local Q = node.new("whatsit","pdf_literal")
    Q.data = "Q"

    _,q = node.insert_before(nodelist,nodelist,q)
    _,Q = node.insert_after(q,nodelist,Q)
    q = node.vpack(q)

    q.width  = math.abs(_wd * cos) + math.abs(ht * sin)
    q.height = math.abs(ht * cos) + math.abs(_wd * sin)
    q.depth  = 0

    return q
end

--- Rotate a text on a given angle (`angle` on textblock).
function M.rotate_textblock( nodelist,angle )
    local wd,ht = nodelist.width, nodelist.height + nodelist.depth
    nodelist.width = 0
    nodelist.height = 0
    nodelist.depth = 0
    local angle_rad = math.rad(angle)
    local sin = math.round(math.sin(angle_rad),3)
    local cos = math.round(math.cos(angle_rad),3)
    local q = node.new("whatsit","pdf_literal")
    q.mode = 0
    local shift_x = math.round(math.min(0,math.sin(angle_rad) * sp_to_bp(ht)) + math.min(0,     math.cos(angle_rad) * sp_to_bp(wd)),3)
    local shift_y = math.round(math.max(0,math.sin(angle_rad) * sp_to_bp(wd)) + math.max(0,-1 * math.cos(angle_rad) * sp_to_bp(ht)),3)
    q.data = string.format("q %g %g %g %g %g %g cm",cos,sin, -1 * sin,cos, -1 * shift_x ,-1 * shift_y )
    q.next = nodelist
    local tail = node.tail(nodelist)
    local Q = node.new("whatsit","pdf_literal")
    Q.data = "Q"
    tail.next = Q
    local tmp = node.vpack(q)
    tmp.width  = math.abs(wd * cos) + math.abs(ht * math.cos(math.rad(90 - angle)))
    tmp.height = math.abs(ht * math.sin(math.rad(90 - angle))) + math.abs(wd * sin)
    tmp.depth = 0
    return tmp
end

file_end("drawing.lua")

return M
