---- luapstricks.lua
-- Copyright 2021--2023 Marcel Krüger <[email protected]>
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3
-- of this license or (at your option) any later version.
-- The latest version of this license is in
--   http://www.latex-project.org/lppl.txt
-- and version 1.3 or later is part of all distributions of LaTeX
-- version 2005/12/01 or later.
--
-- This work has the LPPL maintenance status `maintained'.
--
-- The Current Maintainer of this work is M. Krüger
--
-- This work consists of the files luapstricks.lua and luapstricks-plugin-pstmarble.lua

if luatexbase then
 luatexbase.provides_module {
   name = 'luapstricks',
   version = 'v0.10',
   date = '2023-05-23',
   description = 'PSTricks backend for LuaLaTeX',
 }
end

local setwhatsitfield = node.setwhatsitfield or node.setfield
local late_lua_sub = node.subtype'late_lua'

local pdfprint = vf.pdf -- Set later to have the right mode
local function gobble() end
local function no_pdfprint_allowed()
 pdfprint = gobble -- Don't warn more than once for each code block
 tex.error("luapstricks: Graphics in immediate code segment", {
     "There was an attempt to trigger drawing commands in an immediate code block. \z
     This isn't allowed and will therefore be ignored."
 })
end

local pi = math.pi
local two_pi = 2*pi
local pi2_inv = 2/pi
local pi3_inv = 3/pi

local sin_table = {0, 1, 0, -1}

local l = lpeg

local whitespace = (l.S'\0\t\n\r\f ' + '%' * (1-l.P'\n')^0 * (l.P'\n' + -1))^1

local regular = 1 - l.S'\0\t\n\r\f %()<>[]{}/'

local exitmarker = {}
local lookup

-- local integer = l.S'+-'^-1 * l.R'09'^1 / tonumber
local real = l.S'+-'^-1 * (l.R'09'^1 * ('.' * l.R'09'^0)^-1 + '.' * l.R'09'^1) * (l.S'Ee' * l.S'+-'^-1 * l.R'09'^1)^-1 / tonumber
local radix_scanner = setmetatable({}, {__index = function(t, b)
 local digit
 if b < 10 then
   digit = l.R('0' .. string.char(string.byte'0' + b - 1))
 else
   digit = l.R'09'
   if b > 10 then
     digit = digit + l.R('A' .. string.char(string.byte'A' + b - 11))
     digit = digit + l.R('a' .. string.char(string.byte'a' + b - 11))
   end
 end
 digit = l.C(digit^1) * l.Cp()
 t[b] = digit
 return digit
end})
local radix = l.Cmt(l.R'09' * l.R'09'^-1 / tonumber * '#', function(subj, pos, radix)
 if radix < 2 or radix > 36 then return end
 local digits, pos = radix_scanner[radix]:match(subj, pos)
 if not digits then return end
 digits = tonumber(digits, radix)
 return pos, digits
end)
local number = radix + real -- + integer -- Every integer is also a real

local str_view do
 local meta = {
   __index = function(s, k)
     if k == 'value' then
       return string.sub(s.base.value, s.offset, s.last)
     end
   end,
   __newindex = function(s, k, v)
     if k == 'value' then
       s.base.value = string.sub(s.base.value, 1, s.offset-1) .. v .. string.sub(s.base.value, s.last+1)
       return
     end
     -- We could do rawset here, but there is no reason for setting keys anyway
     assert(false)
   end,
 }
 function str_view(base, offset, length)
   if getmetatable(base) == meta then
     offset = offset + base.offset - 1
     base = base.base
   end
   return setmetatable({
     kind = 'string',
     base = base,
     offset = offset,
     last = offset + length - 1,
   }, meta)
 end
end

local string_patt do
 local literal = '(' * l.Cs(l.P{(
     l.Cg('\\' * (
         'n' * l.Cc'\n'
       + 'r' * l.Cc'\r'
       + 't' * l.Cc'\t'
       + 'b' * l.Cc'\b'
       + 'f' * l.Cc'\f'
       + '\\' * l.Cc'\\'
       + '(' * l.Cc'('
       + ')' * l.Cc')'
       + l.R'07' * l.R'07'^-2 / function(s) return string.char(tonumber(s, 8) % 0x100) end
       + ('\r' * l.P'\n'^-1 + '\n')^-1 * l.Cc''
     ))
   + l.Cg('\r' * l.P'\n'^-1 * l.Cc'\n')
   + (1-l.S'()')
   + '(' * l.V(1) * ')'
 )^0}) * ')'
 local hexchar = l.R('09', 'af', 'AF')
 local hexbyte = hexchar * hexchar^-1 / function(s)
   local b = tonumber(s, 16)
   return #s == 1 and 16*b or b
 end
 local hex = '<' * (hexbyte^0 / string.char) * '>'
 string_patt = literal + hex -- TODO: Base85 is not implemented
end

local name = l.C(regular^1 + l.S'[]' + '<<' + '>>')
local literal_name = '/' * l.C(regular^0)
local imm_name = '//' * l.C(regular^0)

-- All objects are literal by default, except names represented as direct strings and operators
local any_object = l.P{whitespace^-1 * (
   number * -regular
 + l.Ct(l.Cg(string_patt, 'value') * l.Cg(l.Cc'string', 'kind'))
 + imm_name / function(name) return lookup(name) end
 + l.Ct(l.Cg(literal_name, 'value') * l.Cg(l.Cc'name', 'kind'))
 + name
 + l.Ct(l.Cg(l.Ct(l.Cg('{' * l.Ct(l.V(1)^0) * whitespace^-1 * '}', 'value') * l.Cg(l.Cc'array', 'kind')), 'value') * l.Cg(l.Cc'executable', 'kind'))
)}
local object_list = l.Ct(any_object^0) * whitespace^-1 * (-1 + l.Cp())

local function parse_ps(s)
 local tokens, fail_offset = object_list:match(s)
 if fail_offset then
   error(string.format('Failed to parse PS tokens at `%s\'', s:sub(fail_offset)))
 end
 return tokens
end

local serialize_pdf do
 function serialize_pdf(obj)
   local t = type(obj)
   if t == 'number' then
     return string.format(math.type(obj) == 'float' and '%.5f' or '%i', obj)
   elseif t == 'boolean' then
     return obj and 'true' or 'false'
   elseif t == 'string' then
     return '/' .. obj
   elseif t == 'table' then
     t = obj.kind
     if t == 'name' then
       return '/' .. obj.value
     elseif t == 'string' then
       return '(' .. obj.value .. ')' -- TODO: Escaping
     elseif t == 'dict' then
       local helper = {}
       for k, v in next, obj.value do
         helper[#helper+1] = serialize_pdf(k)
         helper[#helper+1] = serialize_pdf(v)
       end
       return '<<' .. table.concat(helper, ' ') .. '>>'
     elseif t == 'array' then
       local helper = {}
       for i, v in ipairs(obj.value) do
         helper[i] = serialize_pdf(v)
       end
       return '[' .. table.concat(helper, ' ') .. ']'
     else
       error'Unable to serialize object'
     end
   end
   error'Unable to serialize object'
 end
end

local srand, rrand, rand do
 local state
 function srand(s)
   state = s//1
   if state < 1 then
     state = -(state % 0x7ffffffe) + 1
   elseif state > 0x7ffffffe then
     state = 0x7ffffffe
   end
 end
 function rrand()
   return state
 end
 function rand()
   state = (16807 * state) % 0x7fffffff
   -- if state <= 0 then
   --   state = state + 0x7fffffff
   -- end
   return state
 end
 srand(math.random(1, 0x7ffffffe))
end

local maybe_decompress do
 local compressed_pattern = '%!PS\n\z
   currentfile<</Predictor 1' * l.R'05' * '/Columns ' * (l.R'09'^1/tonumber) * '>>/FlateDecode filter cvx exec\n'
   * l.C(l.P(1)^1)

 local stacklimit = 999000

 function maybe_decompress(data)
   local columns, compressed = compressed_pattern:match(data)
   if not columns then return data end

   data = zlib.decompress(compressed)
   local bytes do
     local size = #data
     if size < stacklimit then
       bytes = {data:byte(1, -1)}
     else
       bytes = {}
       local off = 1
       for i = 1, size, stacklimit do
         table.move({data:byte(i, i+stacklimit-1)}, 1, stacklimit, i, bytes)
       end
     end
   end
   local new_data = {}
   local start_row = 1
   local out_row = 1
   while true do
     local control = bytes[start_row]
     if not control then break end
     if control == 0 or (control == 2 and start_row == 1) then
       table.move(bytes, start_row + 1, start_row + columns, out_row, new_data)
     elseif control == 1 then
       local last = bytes[start_row + 1]
       new_data[out_row] = last
       for i = 2, columns do
         last = (bytes[start_row + i] + last) & 0xFF
         new_data[out_row + i - 1] = last
       end
     elseif control == 2 then
       for i = 1, columns do
         new_data[out_row + i - 1] = (bytes[start_row + i] + new_data[out_row - columns - 1 + i]) & 0xFF
       end
     else
       error'Unimplemented'
     end
     start_row = start_row + columns + 1
     out_row = out_row + columns
   end
   local result = ''
   local size = #new_data
   for i = 1, size, stacklimit do
     result = result .. string.char(table.unpack(new_data, i, i + stacklimit > size and size or i + stacklimit - 1))
   end
   return result
 end
end

local font_aliases = {
 -- First add some help to find the TeX Gyre names under the corresponding URW font names
 ['NimbusRoman-Regular'] = 'kpse:texgyretermes-regular.otf',
 ['NimbusRoman-Italic'] = 'kpse:texgyretermes-italic.otf',
 ['NimbusRoman-Bold'] = 'kpse:texgyretermes-bold.otf',
 ['NimbusRoman-BoldItalic'] = 'kpse:texgyretermes-bolditalic.otf',

 ['NimbusSans-Regular'] = 'kpse:texgyreheros-regular.otf',
 ['NimbusSans-Italic'] = 'kpse:texgyreheros-italic.otf',
 ['NimbusSans-Bold'] = 'kpse:texgyreheros-bold.otf',
 ['NimbusSans-BoldItalic'] = 'kpse:texgyreheros-bolditalic.otf',

 ['NimbusSansNarrow-Regular'] = 'kpse:texgyreheroscn-regular.otf',
 ['NimbusSansNarrow-Oblique'] = 'kpse:texgyreheroscn-italic.otf',
 ['NimbusSansNarrow-Bold'] = 'kpse:texgyreheroscn-bold.otf',
 ['NimbusSansNarrow-BoldOblique'] = 'kpse:texgyreheroscn-bolditalic.otf',

 ['NimbusMonoPS-Regular'] = 'kpse:texgyrecursor-regular.otf',
 ['NimbusMonoPS-Italic'] = 'kpse:texgyrecursor-italic.otf',
 ['NimbusMonoPS-Bold'] = 'kpse:texgyrecursor-bold.otf',
 ['NimbusMonoPS-BoldItalic'] = 'kpse:texgyrecursor-bolditalic.otf',

 ['URWBookman-Light'] = 'kpse:texgyrebonum-regular.otf',
 ['URWBookman-LightItalic'] = 'kpse:texgyrebonum-italic.otf',
 ['URWBookman-Demi'] = 'kpse:texgyrebonum-bold.otf',
 ['URWBookman-DemiItalic'] = 'kpse:texgyrebonum-bolditalic.otf',

 ['URWGothic-Book'] = 'kpse:texgyreadventor-regular.otf',
 ['URWGothic-BookOblique'] = 'kpse:texgyreadventor-italic.otf',
 ['URWGothic-Demi'] = 'kpse:texgyreadventor-bold.otf',
 ['URWGothic-DemiOblique'] = 'kpse:texgyreadventor-bolditalic.otf',

 -- These fonts have weird names in their URW variant, so we use the standard font names directly instead.
 ['NewCenturySchlbk-Roman'] = 'kpse:texgyreschola-regular.otf',
 ['NewCenturySchlbk-Italic'] = 'kpse:texgyreschola-italic.otf',
 ['NewCenturySchlbk-Bold'] = 'kpse:texgyreschola-bold.otf',
 ['NewCenturySchlbk-BoldItalic'] = 'kpse:texgyreschola-bolditalic.otf',

 ['Palatino-Roman'] = 'kpse:texgyrepagella-regular.otf',
 ['Palatino-Italic'] = 'kpse:texgyrepagella-italic.otf',
 ['Palatino-Bold'] = 'kpse:texgyrepagella-bold.otf',
 ['Palatino-BoldItalic'] = 'kpse:texgyrepagella-bolditalic.otf',

 ['ZapfChancery-MediumItalic'] = 'kpse:texgyrechorus-mediumitalic.otf',

 -- The two symbol fonts don't have OpenType equivalents in TeX Live
 -- so we use TFM based fonts instead
 ['StandardSymbolsPS'] = 'usyr',
 ['Dingbats'] = 'uzdr',
}
-- Then map the standard 35 font names to the URW names as done by GhostScript
-- (Except for New Century Schoolbook which got mapped directly before.
for psname, remapped in next, {
 ['Times-Roman'] = 'NimbusRoman-Regular',
 ['Times-Italic'] = 'NimbusRoman-Italic',
 ['Times-Bold'] = 'NimbusRoman-Bold',
 ['Times-BoldItalic'] = 'NimbusRoman-BoldItalic',

 ['Helvetica'] = 'NimbusSans-Regular',
 ['Helvetica-Oblique'] = 'NimbusSans-Italic',
 ['Helvetica-Bold'] = 'NimbusSans-Bold',
 ['Helvetica-BoldOblique'] = 'NimbusSans-BoldItalic',

 ['Helvetica-Narrow'] = 'NimbusSansNarrow-Regular',
 ['Helvetica-Narrow-Oblique'] = 'NimbusSansNarrow-Oblique',
 ['Helvetica-Narrow-Bold'] = 'NimbusSansNarrow-Bold',
 ['Helvetica-Narrow-BoldOblique'] = 'NimbusSansNarrow-BoldOblique',

 ['Courier'] = 'NimbusMonoPS-Regular',
 ['Courier-Oblique'] = 'NimbusMonoPS-Italic',
 ['Courier-Bold'] = 'NimbusMonoPS-Bold',
 ['Courier-BoldOblique'] = 'NimbusMonoPS-BoldItalic',

 ['Bookman-Light'] = 'URWBookman-Light',
 ['Bookman-LightItalic'] = 'URWBookman-LightItalic',
 ['Bookman-Demi'] = 'URWBookman-Demi',
 ['Bookman-DemiItalic'] = 'URWBookman-DemiItalic',

 ['AvantGarde-Book'] = 'URWGothic-Book',
 ['AvantGarde-BookOblique'] = 'URWGothic-BookOblique',
 ['AvantGarde-Demi'] = 'URWGothic-Demi',
 ['AvantGarde-DemiOblique'] = 'URWGothic-DemiOblique',

 ['Symbol'] = 'StandardSymbolsPS',
 ['StandardSymL'] = 'StandardSymbolsPS',

 ['ZapfDingbats'] = 'Dingbats',

 -- Some additional names needed for PSTricks
 ['NimbusRomNo9L-Regu'] = 'NimbusRoman-Regular',
 ['NimbusRomNo9L-ReguItal'] = 'NimbusRoman-Italic',
 ['NimbusRomNo9L-Medi'] = 'NimbusRoman-Bold',
 ['NimbusRomNo9L-MediItal'] = 'NimbusRoman-BoldItalic',
 ['NimbusRomNo9L-Bold'] = 'NimbusRoman-Bold',

 ['NimbusSanL-Regu'] = 'NimbusSans-Regular',
 ['NimbusSanL-ReguItal'] = 'NimbusSans-Italic',
 ['NimbusSanL-Bold'] = 'NimbusSans-Bold',
 ['NimbusSanL-BoldItal'] = 'NimbusSans-BoldItalic',

 ['NimbusSanL-BoldCond'] = 'NimbusSansNarrow-Bold',
 ['NimbusSanL-BoldCondItal'] = 'NimbusSansNarrow-BoldOblique',
 ['NimbusSanL-ReguCond'] = 'NimbusSansNarrow-Regular',
 ['NimbusSanL-ReguCondItal'] = 'NimbusSansNarrow-Oblique',

 ['NimbusMonL-Regu'] = 'NimbusMonoPS-Regular',
 ['NimbusMonL-ReguObli'] = 'NimbusMonoPS-Italic',
 ['NimbusMonL-Bold'] = 'NimbusMonoPS-Bold',
 ['NimbusMonL-BoldObli'] = 'NimbusMonoPS-BoldItalic',

 ['URWBookmanL-DemiBoldItal'] = 'URWBookman-DemiItalic',
 ['URWBookmanL-DemiBold'] = 'URWBookman-Demi',
 ['URWBookmanL-LighItal'] = 'URWBookman-LightItalic',
 ['URWBookmanL-Ligh'] = 'URWBookman-Light',

 ['URWGothicL-BookObli'] = 'URWGothic-BookOblique',
 ['URWGothicL-Book'] = 'URWGothic-Book',
 ['URWGothicL-DemiObli'] = 'URWGothic-DemiOblique',
 ['URWGothicL-Demi'] = 'URWGothic-Demi',

 ['CenturySchL-Roma'] = 'NewCenturySchlbk-Roman',
 ['CenturySchL-Ital'] = 'NewCenturySchlbk-Italic',
 ['CenturySchL-Bold'] = 'NewCenturySchlbk-Bold',
 ['CenturySchL-BoldItal'] = 'NewCenturySchlbk-BoldItalic',

 ['URWPalladioL-Roma'] = 'Palatino-Roman',
 ['URWPalladioL-Ital'] = 'Palatino-Italic',
 ['URWPalladioL-Bold'] = 'Palatino-Bold',
 ['URWPalladioL-BoldItal'] = 'Palatino-BoldItalic',

 ['URWChanceryL-MediItal'] = 'ZapfChancery-MediumItalic',
} do
 font_aliases[psname] = font_aliases[remapped] or remapped
end

local operand_stack = {}

local pushs do
 local function helper(height, args, arg, ...)
   if args == 0 then return end
   height = height + 1
   operand_stack[height] = arg
   return helper(height, args - 1, ...)
 end
 function pushs(...)
   return helper(#operand_stack, select('#', ...), ...)
 end
end
local function push(value)
 operand_stack[#operand_stack+1] = value
end

local function ps_error(kind, ...)
 pushs(...)
 return error{pserror = kind, trace = debug.traceback()}
end

local function pop(...)
 local height = #operand_stack
 if height == 0 then
   return ps_error('stackunderflow', ...)
 end
 local v = operand_stack[height]
 operand_stack[height] = nil
 return v, v
end
local function pop_num(...)
 local raw = pop(...)
 local n = raw
 local tn = type(n)
 if tn == 'table' and n.kind == 'executable' then
   n = n.value
   tn = type(n)
 end
 if tn ~= 'number' then
   ps_error('typecheck', raw, ...)
 end
 return n, raw
end
local pop_int = pop_num
local function pop_proc(...)
 local v = pop()
 if type(v) ~= 'table' or v.kind ~= 'executable' or type(v.value) ~= 'table' or v.value.kind ~= 'array' then
   ps_error('typecheck', v, ...)
 end
 return v.value.value, v
end
local pop_bool = pop
local function pop_dict()
 local orig = pop()
 local dict = orig
 if type(dict) ~= 'table' then
   ps_error('typecheck', orig)
 end
 if dict.kind == 'executable' then
   dict = dict.value
   if type(dict) ~= 'table' then
     ps_error('typecheck', orig)
   end
 end
 if dict.kind ~= 'dict' then
   ps_error('typecheck', orig)
 end
 return dict.value, orig, dict
end
local function pop_array()
 local orig = pop()
 local arr = orig
 if type(arr) == 'table' and arr.kind == 'executable' then
   arr = arr.value
 end
 if type(arr) ~= 'table' or arr.kind ~= 'array' then
   ps_error('typecheck', orig)
 end
 return arr
end
local pop_string = pop
local function pop_key()
 local key = pop()
 if type(key) == 'table' then
   local kind = key.kind
   if kind == 'executable' then
     key = key.value
     if type(key) ~= 'table' then return key end
     kind = key.kind
   end
   if kind == 'string' or kind == 'name' or kind == 'operator' then
     key = key.value
   end
 end
 return key
end

local execute_ps, execute_tok

local dictionary_stack

-- About the bbox entry:
--   - If the bounding box is not currently tracked, it is set to nil
--   - Otherwise it's a linked list linked with the .next field. Every entry is a "matrix level"
--   - if .bbox[1] is nil, the current matrix level does not have a set bounding box yet
--   - Otherside it's {min_x, min_y, max_x, max_y}
--   - If a .bbox.matrix entry is present then it describes the matrix which should be applied before the bbox gets added to the next "matrix level"
local graphics_stack = {{
 matrix = {10, 0, 0, 10, 0, 0}, -- Chosen for consistency with GhostScript's pdfwrite. Must be the same as defaultmatrix
 bbox = nil,
 linewidth = nil,
 current_path = nil,
 current_point = nil,
 color = {},
 fillconstantalpha = 1,
 strokeconstantalpha = 1,
 alphaisshape = nil,
 blendmode = nil,
 linejoin = nil,
 linecap = nil,
 strokeadjust = nil,
 font = nil,
 dash = nil,
 saved_delayed = nil, -- nil if the `gsave` of this graphic state is not delayed
 flatness = 1,
 miterlimit = nil,
}}

local lua_node_lookup = setmetatable({}, {__mode = 'k'})
local char_width_storage -- Non nil only at the beginning of a Type 3 glyph. Used to export the width.
local ExtGStateCount = 0
local pdfdict_gput = token.create'pdfdict_gput:nnn'
if pdfdict_gput.cmdname == 'undefined_cs' then
 pdfdict_gput = nil
end
local lbrace = token.create(string.byte'{')
local rbrace = token.create(string.byte'}')
local ExtGState = setmetatable({}, {__index = pdfdict_gput and function(t, k)
 ExtGStateCount = ExtGStateCount + 1
 local name = 'PSExtG' .. ExtGStateCount
 tex.runtoks(function()
   tex.write(pdfdict_gput, lbrace, 'g__pdf_Core/Page/Resources/ExtGState', rbrace, lbrace, name, rbrace, lbrace, k, rbrace)
 end)
 ltx.__pdf.Page.Resources.ExtGState = true
 ltx.pdf.Page_Resources_gpush(tex.count.g_shipout_readonly_int)
 name = '/' .. name .. ' gs'
 t[k] = name
 return name
end or function()
 texio.write_nl"Extended graphic state modifications dropped since `pdfmanagement-testphase' is not loaded."
 return ''
end})

local write_shading do
 local ShadingCount = 0
 if pdfdict_gput then
   function write_shading(attr, data)
     local obj = pdf.obj{
       type = 'stream',
       immediate = false,
       attr = attr,
       string = data,
     }
     pdf.refobj(obj)
     ShadingCount = ShadingCount + 1
     local name = 'PSShad' .. ShadingCount
     local k = obj .. ' 0 R'
     tex.runtoks(function()
       tex.write(pdfdict_gput, lbrace, 'g__pdf_Core/Page/Resources/Shading', rbrace, lbrace, name, rbrace, lbrace, k, rbrace)
     end)
     ltx.__pdf.Page.Resources.Shading = true
     ltx.pdf.Page_Resources_gpush(tex.count.g_shipout_readonly_int)
     name = '/' .. name
     return name
   end
 else
   function write_shading()
     texio.write_nl"Extended graphic state modifications dropped since `pdfmanagement-testphase' is not loaded."
     return ''
   end
 end
end

local function matrix_transform(x, y, xx, xy, yx, yy, dx, dy)
 return x * xx + y * yx + dx, x * xy + y * yy + dy
end
local function matrix_invert(xx, xy, yx, yy, dx, dy)
 local determinante = xx*yy - xy*yx
 xx, xy, yx, yy = yy/determinante, -xy/determinante, -yx/determinante, xx/determinante
 dx, dy = - dx * xx - dy * yx, - dx * xy - dy * yy
 return xx, xy, yx, yy, dx, dy
end
local delayed = {
 text = {},
 matrix = {1, 0, 0, 1, 0, 0},
}
local function update_matrix(xx, xy, yx, yy, dx, dy)
 local matrix = graphics_stack[#graphics_stack].matrix
 matrix[1], matrix[2],
 matrix[3], matrix[4],
 matrix[5], matrix[6]
   = xx * matrix[1] + xy * matrix[3],             xx * matrix[2] + xy * matrix[4],
     yx * matrix[1] + yy * matrix[3],             yx * matrix[2] + yy * matrix[4],
     dx * matrix[1] + dy * matrix[3] + matrix[5], dx * matrix[2] + dy * matrix[4] + matrix[6]

 local delayed_matrix = delayed.matrix
 delayed_matrix[1], delayed_matrix[2],
 delayed_matrix[3], delayed_matrix[4],
 delayed_matrix[5], delayed_matrix[6]
   = xx * delayed_matrix[1] + xy * delayed_matrix[3],                     xx * delayed_matrix[2] + xy * delayed_matrix[4],
     yx * delayed_matrix[1] + yy * delayed_matrix[3],                     yx * delayed_matrix[2] + yy * delayed_matrix[4],
     dx * delayed_matrix[1] + dy * delayed_matrix[3] + delayed_matrix[5], dx * delayed_matrix[2] + dy * delayed_matrix[4] + delayed_matrix[6]

 local current_path = graphics_stack[#graphics_stack].current_path
 if not current_path then return end

 local determinante = xx*yy - xy*yx
 xx, xy, yx, yy, dx, dy = matrix_invert(xx, xy, yx, yy, dx, dy)
 local i=1
 while current_path[i] do
   local entry = current_path[i]
   if type(entry) == 'number' then
     local after = current_path[i+1]
     assert(type(after) == 'number')
     current_path[i], current_path[i+1] = xx * entry + yx * after + dx, xy * entry + yy * after + dy
     i = i+2
   else
     i = i+1
   end
 end
 local current_point = graphics_stack[#graphics_stack].current_point
 local x, y = current_point[1], current_point[2]
 current_point[1], current_point[2] = xx * x + yx * y + dx, xy * x + yy * y + dy
end

local function delayed_print(str)
 local delayed_text = delayed.text
 delayed_text[#delayed_text + 1] = str
end

local function reset_delayed(delayed)
 local delayed_matrix = delayed.matrix
 local delayed_text = delayed.text
 for i=1, #delayed_text do
   delayed_text[i] = nil
 end
 delayed_matrix[1], delayed_matrix[2],
 delayed_matrix[3], delayed_matrix[4],
 delayed_matrix[5], delayed_matrix[6] = 1, 0, 0, 1, 0, 0
end

local function flush_delayed_table(delayed, state, force_start)
 local delayed_matrix = delayed.matrix
 local delayed_text = delayed.text

 local cm_string = string.format('%.5f %.5f %.5f %.5f %.5f %.5f cm', delayed_matrix[1], delayed_matrix[2],
                                                                     delayed_matrix[3], delayed_matrix[4],
                                                                     delayed_matrix[5], delayed_matrix[6])
 if cm_string == "1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm" then
   cm_string = nil
 else
   local bbox = state.bbox
   if bbox then
     state.bbox = { matrix = delayed_matrix, next = bbox }
     delayed.matrix = {} -- Will be initialized in reset_delayed
   end
 end

 -- Before flushing, make sure that the current graphics state has started.
 graphics_stack_height = graphics_stack_height or #graphics_stack
 local saved_delayed = state.saved_delayed
 if saved_delayed and(cm_string or delayed_text[1] or force_start) then
   state.saved_delayed = nil
   pdfprint'q'
 end
 for i=1, #delayed_text do
   pdfprint(delayed_text[i])
 end
 if cm_string then
   pdfprint((cm_string:gsub('%.?0+ ', ' ')))
 end
 return reset_delayed(delayed)
end

local function flush_delayed(force_start)
 local pre_first_delayed_group
 for i = #graphics_stack, 1, -1 do
   if not graphics_stack[i].saved_delayed then
     pre_first_delayed_group = i
     break
   end
 end
 for i = pre_first_delayed_group, #graphics_stack-1 do
   flush_delayed_table(graphics_stack[i+1].saved_delayed, graphics_stack[i]) -- No need for force_start here
 end
 return flush_delayed_table(delayed, graphics_stack[#graphics_stack], force_start)
end

local function register_point_bbox(bbox, x, y)
 local min_x, min_y, max_x, max_y = bbox[1], bbox[2], bbox[3], bbox[4]
 if min_x then
   if x < min_x then
     bbox[1] = x
   elseif x > max_x then
     bbox[3] = x
   end
   if y < min_y then
     bbox[2] = y
   elseif y > max_y then
     bbox[4] = y
   end
 else
   bbox[1], bbox[2], bbox[3], bbox[4] = x, y, x, y
 end
end

-- Only call after flush_delayed
local function register_point(state, x, y)
 local bbox = state.bbox
 if not bbox then return end
 return register_point_bbox(bbox, x, y)
end

local function merge_bbox(bbox, after)
 if bbox[1] then
   local matrix = bbox.matrix
   if matrix then
     register_point_bbox(after, matrix_transform(bbox[1], bbox[2], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]))
     register_point_bbox(after, matrix_transform(bbox[1], bbox[4], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]))
     register_point_bbox(after, matrix_transform(bbox[3], bbox[2], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]))
     register_point_bbox(after, matrix_transform(bbox[3], bbox[4], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]))
   else
     register_point_bbox(after, bbox[1], bbox[2])
     register_point_bbox(after, bbox[3], bbox[4])
   end
 end
 return after
end

function drawarc(xc, yc, r, a1, a2)
 a1, a2 = math.rad(a1), math.rad(a2)
 local dx, dy = r*math.cos(a1), r*math.sin(a1)
 local x, y = xc + dx, yc + dy
 local segments = math.ceil(math.abs(a2-a1)*pi2_inv)
 local da = (a2-a1)/segments
 local state = graphics_stack[#graphics_stack]
 local current_path = state.current_path
 local i
 if current_path then
   i = #current_path + 1
   current_path[i], current_path[i+1], current_path[i+2] = x, y, 'l'
   i = i + 3
 else
   current_path = {x, y, 'm'}
   i = 4
   state.current_path = current_path
   state.current_point = {}
 end
 local factor = 4*math.tan(da/4)/3
 dx, dy = factor*dy, -factor*dx
 for _=1, segments do
   current_path[i], current_path[i+1] = x - dx, y - dy
   a1 = a1 + da
   dx, dy = r*math.cos(a1), r*math.sin(a1)
   x, y = xc + dx, yc + dy
   dx, dy = factor*dy, -factor*dx
   current_path[i+2], current_path[i+3] = x + dx, y + dy
   current_path[i+4], current_path[i+5] = x, y
   current_path[i+6] = 'c'
   i = i + 7
 end
 state.current_point[1], state.current_point[2] = x, y
end

local function try_lookup(name)
 for i = #dictionary_stack, 1, -1 do
   local dict = dictionary_stack[i]
   local value = dict.value[name]
   if value ~= nil then
     return value
   end
 end
end
function lookup(name)
 local result = try_lookup(name)
 if result == nil then
   return error(string.format('Unknown name %q', name))
 end
 return result
end
local function bind(proc)
 for i=1, #proc do
   local entry = proc[i]
   local tentry = type(entry)
   if tentry == 'table' and entry.kind == 'executable' and type(entry.value) == 'table' and entry.value.kind == 'array' then
     bind(entry.value.value)
   elseif tentry == 'string' then
     local res = try_lookup(entry)
     if type(res) == 'function' then
       proc[i] = res
     end
   end
 end
end

local subdivide, flatten do
 function subdivide(t, x0, y0, x1, y1, x2, y2, x3, y3)
   local mt = 1-t
   local x01, y01 = mt * x0 + t * x1, mt * y0 + t * y1
   local x12, y12 = mt * x1 + t * x2, mt * y1 + t * y2
   local x23, y23 = mt * x2 + t * x3, mt * y2 + t * y3
   local x012, y012 = mt * x01 + t * x12, mt * y01 + t * y12
   local x123, y123 = mt * x12 + t * x23, mt * y12 + t * y23
   local x0123, y0123 = mt * x012 + t * x123, mt * y012 + t * y123
   return x01, y01, x012, y012, x0123, y0123, x123, y123, x23, y23, x3, y3
 end
 local function flatness(x0, y0, x1, y1, x2, y2, x3, y3)
   local dx, dy = x3-x0, y3-y0
   local dist = math.sqrt(dx*dx + dy*dy)
   local d1 = math.abs(dx * (x0-x1) - dy * (y0-y1)) / dist
   local d2 = math.abs(dx * (x0-x2) - dy * (y0-y2)) / dist
   return d1 > d2 and d1 or d2
 end
 function flatten(out, target, x0, y0, x1, y1, x2, y2, x3, y3)
   local current = flatness(x0, y0, x1, y1, x2, y2, x3, y3)
   if current <= target then
     local i = #out
     -- out[i+1], out[i+2],
     -- out[i+3], out[i+4],
     -- out[i+5], out[i+6], out[i+7]
     --   = x1, y1, x2, y2, x3, y3, 'c'
     out[i+1], out[i+2], out[i+3]
       = x3, y3, 'l'
     return
   end
   local a, b, c, d, e, f, g, h, i, j, k, l = subdivide(.5, x0, y0, x1, y1, x2, y2, x3, y3)
   flatten(out, target, x0, y0, a, b, c, d, e, f)
   return flatten(out, target, e, f, g, h, i, j, k, l)
 end
end

local function ps_to_string(a)
 local ta = type(a)
 if ta == 'table' and a.kind == 'executable' then
   a = a.value
   ta = type(a)
 end
 if ta == 'string' then
 elseif ta == 'boolean' then
   a = a and 'true' or 'false'
 elseif ta == 'number' then
   a = string.format(math.type(a) == 'float' and '%.6g' or '%i', a)
   -- a = tostring(a)
 elseif ta == 'function' then
   texio.write_nl'Warning: cvs on operators is unsupported. Replaced by dummy.'
   a = '--nostringval--'
 elseif ta == 'table' then
   local kind = a.kind
   if kind == 'string' or kind == 'name' then
     a = a.value
   elseif kind == 'operator' then
     texio.write_nl'Warning: cvs on operators is unsupported. Replaced by dummy.'
     a = '--nostringval--'
   else
     a = '--nostringval--'
   end
 elseif ta == 'userdata' and a.read then
   a = 'file'
 else
   assert(false)
 end
 return a
end

local mark = {kind = 'mark'}
local null = {kind = 'null'}
local statusdict = {kind = 'dict', value = {}}
local globaldict = {kind = 'dict', value = {}}
local userdict = {kind = 'dict', value = {
 SDict = {kind = 'dict', value = {
   normalscale = {kind = 'executable', value = {kind = 'array', value = {}}},
 }},
 TeXDict = {kind = 'dict', value = {
   Resolution = function() push((pdf.getpkresolution())) end,
 }},
 ['@beginspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}},
 ['@setspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}},
 ['@endspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}},
}}
userdict.value.TeXDict.value.VResolution = userdict.value.TeXDict.value.Resolution
local FontDirectory = {kind = 'dict', value = {}}
local ResourceCategories = {kind = 'dict', value = {}}

local function num_to_base(num, base, ...)
 if num == 0 then return string.char(...) end
 local remaining = num // base
 local digit = num - base * remaining
 if digit < 10 then
   digit = digit + 0x30
 else
   digit = digit + 0x37
 end
 return num_to_base(remaining, base, digit, ...)
end

local plugin_interface = {
 push = push,
 pop = pop,
 pop_num = pop_num,
 pop_dict = pop_dict,
 pop_array = pop_array,
 pop_key = pop_key,
 pop_proc = pop_proc,
 exec = nil, -- execute_tok, -- filled in later
}

local systemdict
local function generic_show(str, ax, ay)
 local state = graphics_stack[#graphics_stack]
 local current_point = state.current_point
 if not current_point then return nil, 'nocurrentpoint' end
 local rawpsfont = state.font
 if not rawpsfont then return nil, 'invalidfont' end
 local str = str.value
 local psfont = rawpsfont.value
 local fid = psfont.FID
 local matrix = psfont.FontMatrix.value
 local fonttype = psfont.FontType
 if fonttype ~= 0x1CA and fonttype ~= 3 then
   texio.write_nl'luapstricks: Attempt to use unsupported font type.'
   return nil, 'invalidfont'
 end
 local x0, y0 = current_point[1], current_point[2]
 update_matrix(
   matrix[1],      matrix[2],
   matrix[3],      matrix[4],
   matrix[5] + x0, matrix[6] + y0)
 local w = 0
 if fonttype == 0x1CA then
   local characters = assert(font.getfont(fid)).characters
   local max_d, max_h = 0, 0
   flush_delayed()
   if pdfprint ~= gobble then
     vf.push()
     vf.fontid(fid)
   end
   for b in string.bytes(str) do
     if pdfprint ~= gobble then
       vf.char(b)
       if ax then
         vf.right(ax)
         vf.down(-ay)
       end
     end
     local char = characters[b]
     if char then
       w = w + (char.width or 0)
       if char.depth and char.depth > max_d then
         max_d = char.depth
       end
       if char.height and char.height > max_h then
         max_h = char.height
       end
     end
   end
   w = w/65781.76
   if pdfprint ~= gobble then
     max_d = max_d/65781.76
     max_h = max_h/65781.76
     register_point(state, 0, -max_d)
     if ax then
       local count = #str
       register_point(state, w + count * ax, max_h + count * ay)
     else
       register_point(state, w, max_h)
     end
     vf.pop()
   end
 elseif fonttype == 3 then
   for b in string.bytes(str) do
     systemdict.value.gsave()
     local state = graphics_stack[#graphics_stack]
     state.current_point, state.current_path = nil
     push(rawpsfont)
     push(b)
     local this_w
     char_width_storage = function(width)
       this_w = width
     end
     execute_tok(psfont.BuildChar) -- FIXME(maybe): Switch to BuildGlyph?
     systemdict.value.grestore()
     w = w + assert(this_w, 'Type 3 character failed to set width')
     update_matrix(1, 0, 0, 1, this_w, 0)
     if ax then
       update_matrix(1, 0, 0, 1, ax, ay)
     end
   end
   update_matrix(1, 0, 0, 1, -w, 0)
   if ax then
     local count = #str
     update_matrix(1, 0, 0, 1, count * -ax, count * -ay)
   end
 else
   assert(false)
 end
 if ax then
   local count = #str
   push(w + count * ax)
   push(count * ay)
 else
   push(w)
   push(0)
 end
 systemdict.value.rmoveto()
 update_matrix(matrix_invert(
   matrix[1],      matrix[2],
   matrix[3],      matrix[4],
   matrix[5] + x0, matrix[6] + y0))
 return true
end

systemdict = {kind = 'dict', value = {
 dup = function()
   local v = pop()
   push(v)
   push(v)
 end,
 exch = function()
   local b = pop()
   local a = pop(b)
   push(b)
   push(a)
 end,
 pop = function()
   pop()
 end,
 clear = function()
   for i = 1, #operand_stack do
     operand_stack[i] = nil
   end
 end,
 copy = function()
   local arg, orig = pop()
   local exec
   if type(arg) == 'table' and arg.kind == 'executable' then
     exec = true
     arg = arg.value
   end
   if type(arg) == 'number' then
     local height = #operand_stack
     if arg > height then
       error'copy argument larger then stack'
     end
     table.move(operand_stack, height-arg+1, height, height+1)
   elseif type(arg) == 'table' then
     -- See remarks in getinterval about missing functionality
     local kind = arg.kind
     if kind == 'array' then
       local src = pop_array().value
       if #src ~= #arg.value then
         error'copy with different sized arrays is not implemented yet'
       end
       table.move(src, 1, #src, 1, arg.value)
     elseif kind == 'string' then
       local src = pop_string().value
       if #src == #arg.value then
       elseif #src < #arg.value then
         arg = str_view(arg, 1, #src)
       else
         ps_error'rangecheck'
       end
       arg.value = src
     elseif kind == 'dict' then
       local src = pop_dict()
       if next(arg.value) then
         error'Target dictionary must be empty'
       end
       for k, v in next, src do
         arg.value[k] = v
       end
     else
       ps_error'typecheck'
     end
     push(exec and {kind = 'executable', value = arg} or arg)
   else
     ps_error('typecheck', orig)
   end
 end,
 roll = function()
   local j, arg2 = pop_int()
   local n, arg1 = pop_int(arg2)
   if n < 0 then
     ps_error('rangecheck', arg1, arg2)
   end
   if n == 0 or j == 0 then return end
   local height = #operand_stack
   if j < 0 then
     j = (-j) % n
     local temp = table.move(operand_stack, height-n+1, height-n+j, 1, {})
     table.move(operand_stack, height-n+j+1, height, height-n+1)
     table.move(temp, 1, j, height-j+1, operand_stack)
   else
     j = j % n
     local temp = table.move(operand_stack, height-j+1, height, 1, {})
     table.move(operand_stack, height-n+1, height-j, height-n+j+1)
     table.move(temp, 1, j, height-n+1, operand_stack)
   end
 end,
 index = function()
   local i, arg1 = pop_int()
   local height = #operand_stack
   if i < 0 or height <= i then
     ps_error('rangecheck', arg1)
   end
   push(operand_stack[height - i])
 end,
 null = function()
   push(null)
 end,
 mark = function()
   push(mark)
 end,
 ['['] = function()
   push(mark)
 end,
 [']'] = function()
   systemdict.value.counttomark()
   systemdict.value.array()
   systemdict.value.astore()
   systemdict.value.exch()
   systemdict.value.pop()
 end,
 ['<<'] = function()
   push(mark)
 end,
 ['>>'] = function()
   local mark_pos
   for i = #operand_stack, 1, -1 do
     if operand_stack[i] == mark then
       mark_pos = i
       break
     end
   end
   if not mark_pos then error'unmatchedmark' end
   local dict = lua.newtable(0, (#operand_stack-mark_pos) // 2)
   for i = mark_pos + 1, #operand_stack - 1, 2 do
     push(operand_stack[i])
     local key = pop_key()
     dict[key] = operand_stack[i+1]
   end
   for i = mark_pos, #operand_stack do
     operand_stack[i] = nil
   end
   push{kind = 'dict', value = dict}
 end,
 count = function()
   push(#operand_stack)
 end,
 counttomark = function()
   local height = #operand_stack
   for i=height, 1, -1 do
     local entry = operand_stack[i]
     if type(entry) == 'table' and entry.kind == 'mark' then
       return push(height-i)
     end
   end
   error'unmatchedmark'
 end,
 cleartomark = function()
   local entry
   repeat
     entry = pop()
   until (not entry) or type(entry) == 'table' and entry.kind == 'mark'
   if not entry then error'unmatchedmark' end
 end,

 ['if'] = function()
   local proc, arg2 = pop_proc()
   local cond = pop_bool(arg2)
   if cond then
     execute_ps(proc)
   end
 end,
 ifelse = function()
   local proc_else, arg3 = pop_proc()
   local proc_then, arg2 = pop_proc(arg3)
   local cond = pop_bool(arg2, arg3)
   if cond then
     execute_ps(proc_then)
   else
     execute_ps(proc_else)
   end
 end,
 ['for'] = function()
   local proc, arg4 = pop_proc()
   local limit, arg3 = pop_num(arg4)
   local step, arg2 = pop_num(arg3, arg4)
   local initial = pop_num(arg2, arg3, arg4)
   local success, err = pcall(function()
     for i=initial, limit, step do
       push(i)
       execute_ps(proc)
     end
   end)
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,
 forall = function()
   local proc, arg2 = pop_proc()
   local obj, arg1 = pop()
   if type(obj) ~= 'table' then
     ps_error('typecheck', arg1, arg2)
   end
   if obj.kind == 'executable' then
     obj = obj.value
     if type(obj) ~= 'table' then
       ps_error('typecheck', arg1, arg2)
     end
   end
   local success, err = pcall(
        obj.kind == 'array' and function()
          for i=1, #obj.value do
            push(obj.value[i])
            execute_ps(proc)
          end
        end
     or obj.kind == 'string' and function()
          for b in string.bytes(obj.value) do
            push(b)
            execute_ps(proc)
          end
        end
     or obj.kind == 'dict' and function()
          for k, v in next, obj.value do
            pushs(k, v)
            execute_ps(proc)
          end
        end
     or ps_error('typecheck', arg1, arg2))
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,
 ['repeat'] = function()
   local proc, arg2 = pop_proc()
   local count = pop_int(arg2)
   local success, err = pcall(function()
     for i=1, count do
       execute_ps(proc)
     end
   end)
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,
 loop = function()
   local proc = pop_proc()
   local success, err = pcall(function()
     while true do
       execute_ps(proc)
     end
   end)
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,

 reversepath = function()
   local state = graphics_stack[#graphics_stack]
   local path = state.current_path
   if not path then return end
   local newpath = lua.newtable(#path, 0)
   local i = 1
   local out_ptr = 1
   -- Iterate over groups starting with "x y m". These can contain multiple subpaths separated with `h`.
   while path[i+2] == 'm' do
     local x0, y0 = path[i], path[i + 1]
     local after = i + 3
     while path[after] and path[after + 2] ~= 'm' do
       after = after + 1
     end
     local j = after
     out_ptr = out_ptr + 3 -- Leave space for the initial `x y m`
     newpath[out_ptr - 1] = 'm'
     local drop_closepath = true -- If this is true we do not end with a closepath and therefore have to remove the first one.
     while true do
       j = j - 1
       local cmd = path[j]
       if cmd == 'h' then
         if j ~= after - 1 then
           newpath[out_ptr - 3], newpath[out_ptr - 2] = x0, y0
           if not drop_closepath then
             newpath[out_ptr] = 'h'
             out_ptr = out_ptr + 1
           end
           out_ptr = out_ptr + 3 -- Leave space for the initial `x y m`
           newpath[out_ptr - 1] = 'm'
         end
         drop_closepath = false
       elseif cmd == 'm' then
         newpath[out_ptr - 3], newpath[out_ptr - 2] = path[j - 2], path[j - 1]
         break
       else
         if cmd == 'c' then
           newpath[out_ptr - 3], newpath[out_ptr - 2] = path[j - 2], path[j - 1]
           newpath[out_ptr], newpath[out_ptr + 1], newpath[out_ptr + 2], newpath[out_ptr + 3] = path[j - 4], path[j - 3], path[j - 6], path[j - 5]
           out_ptr = out_ptr + 6
           newpath[out_ptr] = 'c'
           j = j - 6
         elseif cmd == 'l' then
           newpath[out_ptr - 3], newpath[out_ptr - 2] = path[j - 2], path[j - 1]
           out_ptr = out_ptr + 2
           j = j - 2
         else
           assert(false)
         end
         newpath[out_ptr] = cmd
         out_ptr = out_ptr + 1
       end
     end
     if not drop_closepath then
       newpath[out_ptr] = 'h'
       out_ptr = out_ptr + 1
     end
     i = after
   end
   state.current_path = newpath
   local last_cmd = #newpath
   if newpath[last_cmd] == 'h' then
     last_cmd = last_cmd - 1
   end
   state.current_point[1], state.current_point[2] = newpath[last_cmd - 2], newpath[last_cmd - 1]
 end,

 pathforall = function()
   local close = pop_proc()
   local curve = pop_proc()
   local line = pop_proc()
   local move = pop_proc()
   local state = graphics_stack[#graphics_stack]
   local path = state.current_path
   if not path then return end
   path = table.move(path, 1, #path, 1, {}) -- We don't want to be affected by modifications
   local success, err = pcall( function()
     local i = 1
     while true do
       local entry = path[i]
       if type(entry) == 'string' then
         execute_ps(entry == 'm' and move or entry == 'l' and line or entry == 'c' and curve or entry == 'h' and close or error'Unexpected path operator')
       elseif entry then
         push(entry)
       else
         break
       end
       i = i + 1
     end
   end)
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,

 ['.texboxforall'] = function()
   local proc, arg2 = pop_proc()
   local boxop = pop()
   local box = lua_node_lookup[boxop]
   if not box then
     -- push(boxop)
     -- -- push(proc)
     ps_error('typecheck', boxop, arg2)
   end
   if node.direct.getid(box.box) ~= node.id'hlist' then
     -- push(boxop)
     -- push(proc)
     error'.texboxforall is currently only supported for hboxes'
   end

   local head = node.direct.getlist(box.box)
   head = node.direct.flatten_discretionaries(head)
   node.direct.setlist(box.box, head)
   local success, err = pcall(function()
     local x, y = 0, 0
     local n = head
     while n do
       local after = node.direct.getnext(n)
       local width = node.direct.rangedimensions(box.box, n, after)/65781.76
       push(mark)
       local id = node.type(node.direct.getid(n))
       local subbox = {box = n, parent = box} -- parent is needed for lifetime reasons
       local function op()
         flush_delayed()
         vf.push()
         local n = subbox.box -- Same as the outer box, but this preserves the lifetime of subbox
         local parent = subbox.parent.box
         local after = node.direct.getnext(n)
         local head = node.direct.getlist(parent)
         node.direct.setnext(n, nil)
         node.direct.setlist(parent, n)
         local state = graphics_stack[#graphics_stack]
         local w, h, d = node.direct.dimensions(n)
         register_point(state, 0, -d/65781.76)
         register_point(state, w/65781.76, h/65781.76)
         vf.node(parent)
         node.direct.setnext(n, after)
         node.direct.setlist(parent, head)
         vf.pop()
       end
       lua_node_lookup[subbox] = op
       push(op)
       push(x)
       push(y)
       push(width)
       push(0)
       push(id)
       execute_ps(proc)
       if width ~= 0 then
         x = x + width
       end
       n = after
     end
   end)
   if not success and err ~= exitmarker then
     error(err, 0)
   end
 end,
 pathbbox = function()
   local current_path = assert(graphics_stack[#graphics_stack].current_path, 'nocurrentpoint')
   local i=1
   local llx, lly, urx, ury
   while current_path[i] do
     local entry = current_path[i]
     if type(entry) == 'number' then
       local after = current_path[i+1]
       assert(type(after) == 'number')
       llx = llx and llx < entry and llx or entry
       lly = lly and lly < after and lly or after
       urx = urx and urx > entry and urx or entry
       ury = ury and ury > after and ury or after
       i = i+2
     else
       i = i+1
     end
   end
   push(llx)
   push(lly)
   push(urx)
   push(ury)
 end,

 ['not'] = function()
   local val, orig = pop()
   local tval = type(val)
   if tval == 'table' and val.kind == 'executable' then
     val = val.value
     local tval = type(val)
   end
   if tval == 'boolean' then
     push(not val)
   elseif tval == 'number' then
     push(~val)
   else
     ps_error('typecheck', orig)
   end
 end,
 ['and'] = function()
   local val, orig = pop()
   local tval = type(val)
   if tval == 'table' and val.kind == 'executable' then
     val = val.value
     local tval = type(val)
   end
   if tval == 'boolean' then
     push(pop_bool() and val)
   elseif tval == 'number' then
     push(val & pop_int())
   else
     ps_error('typecheck', orig)
   end
 end,
 ['or'] = function()
   local val, orig = pop()
   local tval = type(val)
   if tval == 'table' and val.kind == 'executable' then
     val = val.value
     local tval = type(val)
   end
   if tval == 'boolean' then
     push(pop_bool() or val)
   elseif tval == 'number' then
     push(val | pop_int())
   else
     ps_error('typecheck', orig)
   end
 end,
 ['xor'] = function()
   local val, orig = pop()
   local tval = type(val)
   if tval == 'table' and val.kind == 'executable' then
     val = val.value
     local tval = type(val)
   end
   if tval == 'boolean' then
     push(val ~= pop_bool())
   elseif tval == 'number' then
     push(val ~ pop_int())
   else
     ps_error('typecheck', orig)
   end
 end,
 bitshift = function()
   local shift, arg2 = pop_num()
   local val = pop_num(arg2)
   push(val << shift)
 end,

 eq = function()
   local b = pop()
   local a = pop(b)
   if type(a) == 'table' and (a.kind == 'executable' or a.kind == 'name' or a.kind == 'operator') then
     a = a.value
   end
   if type(a) == 'table' and a.kind == 'string' then
     a = a.value
   end
   if type(b) == 'table' and (b.kind == 'executable' or b.kind == 'name' or b.kind == 'operator') then
     b = b.value
   end
   if type(b) == 'table' and b.kind == 'string' then
     b = b.value
   end
   push(a==b)
 end,
 ne = function()
   local b = pop()
   local a = pop(b)
   if type(a) == 'table' and (a.kind == 'executable' or a.kind == 'name' or a.kind == 'operator') then
     a = a.value
   end
   if type(a) == 'table' and a.kind == 'string' then
     a = a.value
   end
   if type(b) == 'table' and (b.kind == 'executable' or b.kind == 'name' or b.kind == 'operator') then
     b = b.value
   end
   if type(b) == 'table' and b.kind == 'string' then
     b = b.value
   end
   push(a~=b)
 end,
 gt = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and ta.kind == 'string' then
     if tb ~= 'table' or tb.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a>b)
 end,
 ge = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and ta.kind == 'string' then
     if tb ~= 'table' or tb.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a>=b)
 end,
 le = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and ta.kind == 'string' then
     if tb ~= 'table' or tb.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a<=b)
 end,
 lt = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and a.kind == 'string' then
     if tb ~= 'table' or b.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a<b)
 end,

 -- The following two are GhostScript extensions
 max = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and ta.kind == 'string' then
     if tb ~= 'table' or tb.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a > b and a or b)
 end,
 min = function()
   local b, arg2 = pop()
   local a, arg1 = pop(arg2)
   local ta, tb = type(a), type(b)
   if ta == 'table' and a.kind == 'executable' then
     a = a.value ta = type(a)
   end
   if tb == 'table' and b.kind == 'executable' then
     b = b.value tb = type(b)
   end
   if ta == 'number' then
     if tb ~= 'number' then
       ps_error('typecheck', arg1, arg2)
     end
   elseif ta == 'table' and ta.kind == 'string' then
     if tb ~= 'table' or tb.kind ~= 'string' then
       ps_error('typecheck', arg1, arg2)
     end
     a, b = a.value, b.value
   else
     ps_error('typecheck', arg1, arg2)
   end
   push(a < b and a or b)
 end,

 add = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a+b)
 end,
 sub = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a-b)
 end,
 mul = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a*b)
 end,
 div = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a/b)
 end,
 idiv = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a//b)
 end,
 mod = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a%b)
 end,
 exp = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   push(a^b)
 end,
 sqrt = function()
   push(math.sqrt(pop_num()))
 end,
 sin = function()
   local x = pop_num()
   local i, f = math.modf(x/90)
   if f == 0 then
     push(sin_table[i % 4 + 1])
   else
     push(math.sin(math.rad(x)))
   end
 end,
 cos = function()
   local x = pop_num()
   local i, f = math.modf(x/90)
   if f == 0 then
     push(sin_table[(i+1) % 4 + 1])
   else
     push(math.cos(math.rad(x)))
   end
 end,
 atan = function()
   local b, arg2 = pop_num()
   local a = pop_num(arg2)
   local res = math.deg(math.atan(a, b))
   if res < 0 then res = res + 360 end
   push(res)
 end,
 arccos = function()
   push(math.deg(math.acos(pop_num())))
 end,
 arcsin = function()
   push(math.deg(math.asin(pop_num())))
 end,
 abs = function()
   push(math.abs(pop_num()))
 end,
 neg = function()
   push(-pop_num())
 end,
 round = function()
   return push(math.floor(pop_num()+.5))
 end,
 ceiling = function()
   return push(math.ceil(pop_num()))
 end,
 floor = function()
   return push(math.floor(pop_num()))
 end,
 ln = function()
   push(math.log((pop_num())))
 end,
 log = function()
   push(math.log(pop_num(), 10))
 end,
 truncate = function()
   push((math.modf(pop_num())))
 end,
 cvn = function()
   local a, raw = pop()
   if type(a) == 'table' and a.kind == 'executable' then
     local val = a.value
     if type(val) ~= 'table' or val.kind ~= 'string' then
       ps_error('typecheck', raw)
     end
     push(val.value)
   end
   if type(a) ~= 'table' or a.kind ~= 'string' then
     ps_error('typecheck', raw)
   end
   return push{kind = 'name', value = a.value}
 end,
 cvi = function()
   local a, raw = pop()
   if type(a) == 'table' and a.kind == 'executable' then
     a = a.value
   end
   if type(a) == 'table' and a.kind == 'string' then
     a = (number * -1):match(a.value)
     if not a then
       ps_error('syntaxerror', raw)
     end
   end
   if type(a) ~= 'number' then ps_error('typecheck', raw) end
   push(a//1)
 end,
 cvr = function()
   local a, raw = pop()
   if type(a) == 'table' and a.kind == 'executable' then
     a = a.value
   end
   if type(a) == 'table' and a.kind == 'string' then
     a = (number * -1):match(a.value)
     if not a then
       ps_error('syntaxerror', raw)
     end
   end
   if type(a) ~= 'number' then ps_error('typecheck', raw) end
   push(a*1.)
 end,
 cvs = function()
   local old_str, arg2 = pop_string()
   local a, arg1 = pop()
   a = ps_to_string(a)
   if #old_str.value < #a then ps_error('rangecheck', arg1, arg2) end
   old_str.value = a .. string.sub(old_str.value, #a+1, -1)
   return push{kind = 'string', value = a}
 end,
 cvrs = function()
   local old_str, arg3 = pop_string()
   local radix, arg2 = pop_num()
   local num, arg1 = pop_num()
   if radix == 10 then
     num = string.format(math.type(num) == 'float' and '%.6g' or '%i', num)
   else
     num = num//1
     if num < 0 and num >= -0x80000000 then
       num = num + 0x100000000
     end
     if num < 0 then
       ps_error('rangecheck', arg1, arg2, arg3)
     end
     num = num == 0 and '0' or num_to_base(num, radix)
   end
   if #old_str.value < #num then ps_error('rangecheck', arg1, arg2, arg3) end
   old_str.value = num .. string.sub(old_str.value, #num+1, -1)
   return push{kind = 'string', value = num}
 end,

 string = function()
   push{kind = 'string', value = string.rep('\0', (pop_int()))}
 end,
 search = function()
   local seek = pop_string()
   local str = pop_string()
   local start, stop = string.find(str.value, seek.value, 1, true)
   if start then
     push(str_view(str, stop + 1, #str.value - stop))
     push(str_view(str, start, stop - start + 1))
     push(str_view(str, 1, start - 1))
     push(true)
   else
     push(str)
     push(false)
   end
 end,

 array = function()
   local size = pop_int()
   local arr = lua.newtable(size, 0)
   for i=1, size do arr[i] = null end
   push{kind = 'array', value = arr}
 end,
 astore = function()
   local arr = pop_array()
   local size = #arr.value
   for i=size, 1, -1 do
     arr.value[i] = pop()
   end
   push(arr)
 end,
 aload = function()
   local arr = pop_array()
   table.move(arr.value, 1, #arr.value, #operand_stack + 1, operand_stack)
   push(arr)
 end,
 getinterval = function()
   local count, arg3 = pop_int()
   local index, arg2 = pop_int()
   local arr, arg1 = pop()
   if type(arr) ~= 'table' then ps_error('typecheck', arg1, arg2, arg3) end
   if arr.kind == 'executable' then
     arr = arr.value
     if type(arr) ~= 'table' then ps_error('typecheck', arg1, arg2, arg3) end
   end
   if arr.kind == 'string' then
     push(str_view(arr, index + 1, count))
   elseif arr.kind == 'array' then
     -- TODO: At least for the array case, we could use metamethods to make get element sharing behavior
     push{kind = 'array', value = table.move(arr.value, index + 1, index + count, 1, {})}
   else
     ps_error('typecheck', arg1, arg2, arg3)
   end
 end,
 putinterval = function()
   local from, arg2 = pop()
   local index, arg1 = pop_int()
   if type(from) ~= 'table' then ps_error('typecheck', arg1, arg2) end
   if from.kind == 'executable' then
     from = from.value
     if type(from) ~= 'table' then ps_error('typecheck', arg1, arg2) end
   end
   if from.kind == 'string' then
     local to = pop_string()
     from = from.value
     to.value = string.sub(to.value, 1, index) .. from .. string.sub(to.value, index + 1 + #from)
   elseif from.kind == 'array' then
     local to = pop_array()
     table.move(from.value, 1, #from.value, index + 1, to.value)
   else
     ps_error('typecheck', arg1, arg2)
   end
 end,

 dict = function()
   local size = pop_int()
   push{kind = 'dict', value = lua.newtable(0, size)}
 end,
 begin = function()
   local _
   _, _, dictionary_stack[#dictionary_stack + 1] = pop_dict()
 end,
 ['end'] = function()
   if #dictionary_stack <= 3 then
     ps_error'dictstackunderflow'
   end
   dictionary_stack[#dictionary_stack] = nil
 end,
 currentdict = function()
   push(dictionary_stack[#dictionary_stack])
 end,
 bind = function()
   local d = pop()
   push(d)
   if type(d) ~= 'table' then ps_error'typecheck' end
   if d.kind == 'executable' then
     d = d.value
     if type(d) ~= 'table' then ps_error'typecheck' end
   end
   if d.kind ~= 'array' then ps_error'typecheck' end
   bind(d.value)
 end,
 def = function()
   local value = pop()
   local key = pop_key()
   dictionary_stack[#dictionary_stack].value[key] = value
 end,
 store = function()
   local value = pop()
   local key = pop_key()
   for i=#dictionary_stack, 1, -1 do
     if dictionary_stack[i].value[key] ~= nil then
       dictionary_stack[i].value[key] = value
       return
     end
   end
   dictionary_stack[#dictionary_stack].value[key] = value
 end,
 known = function()
   local key = pop_key()
   local dict = pop()
   push(dict.value[key] ~= nil)
 end,
 where = function()
   local key = pop_key()
   for i = #dictionary_stack, 1, -1 do
     local dict = dictionary_stack[i]
     local value = dict.value[key]
     if value ~= nil then
       push(dict)
       return push(true)
     end
   end
   return push(false)
 end,
 load = function()
   push(lookup(pop_key()))
 end,
 get = function()
   local key = pop()
   local obj = pop()
   if type(obj) ~= 'table' then ps_error'typecheck' end
   if obj.kind == 'executable' then
     obj = obj.value
     if type(obj) ~= 'table' then ps_error'typecheck' end
   end
   local val = obj.value
   if obj.kind == 'string' then
     push(key) key = pop_int()
     if key < 0 or key >= #val then ps_error'rangecheck' end
     push(string.byte(val, key+1))
   elseif obj.kind == 'array' then
     push(key) key = pop_int()
     if key < 0 or key >= #val then ps_error'rangecheck' end
     push(val[key+1])
   elseif obj.kind == 'dict' then
     push(key) key = pop_key()
     push(val[key])
   else
     ps_error'typecheck'
   end
 end,
 put = function()
   local value = pop()
   local key = pop()
   local obj = pop()
   if type(obj) ~= 'table' then ps_error'typecheck' end
   if obj.kind == 'executable' then
     obj = obj.value
     if type(obj) ~= 'table' then ps_error'typecheck' end
   end
   local val = obj.value
   if obj.kind == 'string' then
     push(key) key = pop_int()
     if key < 0 or key >= #val then ps_error'rangecheck' end
     push(value) value = pop_int()
     obj.value = string.sub(val, 1, key) .. string.char(value) .. string.sub(val, key+2, #val)
   elseif obj.kind == 'array' then
     push(key) key = pop_int()
     if key < 0 or key >= #val then ps_error'rangecheck' end
     val[key+1] = value
   elseif obj.kind == 'dict' then
     push(key) key = pop_key()
     val[key] = value
   else
     ps_error'typecheck'
   end
 end,
 undef = function()
   local key = pop_key()
   local dict = pop_dict()
   dict[key] = nil
 end,
 length = function()
   local obj = pop()
   if type(obj) == 'string' then
     return push(#obj)
   elseif type(obj) ~= 'table' then
     ps_error'typecheck'
   end
   if obj.kind == 'executable' then
     obj = obj.value
     if type(obj) ~= 'table' then ps_error'typecheck' end
   end
   local val = obj.value
   if obj.kind == 'string' then
     push(#val)
   elseif obj.kind == 'name' then
     push(#val)
   elseif obj.kind == 'array' then
     push(#val)
   elseif obj.kind == 'dict' then
     local length = 0
     for _ in next, val do
       length = length + 1
     end
     push(length)
   else
     ps_error'typecheck'
   end
 end,

 matrix = function()
   push{kind = 'array', value = {1, 0, 0, 1, 0, 0}}
 end,
 defaultmatrix = function()
   local m = pop_array()
   local mm = m.value
   assert(#mm == 6)
   mm[1], mm[2], mm[3], mm[4], mm[5], mm[6] = 10, 0, 0, 10, 0, 0
   push(m)
 end,
 currentmatrix = function()
   local m = pop_array()
   assert(#m.value == 6)
   table.move(graphics_stack[#graphics_stack].matrix, 1, 6, 1, m.value)
   push(m)
 end,
 currentlinewidth = function()
   push(assert(graphics_stack[#graphics_stack].linewidth, 'linewidth has to be set before it is queried'))
 end,
 currentmiterlimit = function()
   push(assert(graphics_stack[#graphics_stack].miterlimit, 'miterlimit has to be set before it is queried'))
 end,
 currentflat = function()
   push(graphics_stack[#graphics_stack].flatness)
 end,
 setlinewidth = function()
   local lw = pop_num()
   graphics_stack[#graphics_stack].linewidth = lw
   delayed_print(string.format('%.3f w', lw))
 end,
 setlinejoin = function()
   local linejoin = pop_int()
   graphics_stack[#graphics_stack].linejoin = linejoin
   delayed_print(string.format('%i j', linejoin))
 end,
 setlinecap = function()
   local linecap = pop_int()
   graphics_stack[#graphics_stack].linecap = linecap
   delayed_print(string.format('%i J', linecap))
 end,
 setmiterlimit = function()
   local ml = pop_int()
   graphics_stack[#graphics_stack].miterlimit = ml
   delayed_print(string.format('%.3f M', ml))
 end,
 setstrokeadjust = function()
   local sa = pop_bool()
   graphics_stack[#graphics_stack].strokeadjust = sa
   delayed_print(ExtGState[sa and '<</SA true>>' or '<</SA false>>'])
 end,
 setdash = function()
   local offset = pop_num()
   local patt = pop_array().value
   graphics_stack[#graphics_stack].dash = {offset = offset, pattern = patt}
   local mypatt = {}
   for i=1, #patt do
     mypatt[i] = string.format('%.3f', patt[i])
   end
   delayed_print(string.format('[%s] %.3f d', table.concat(mypatt, ' '), offset))
 end,
 setflat = function()
   local flatness = pop_num()
   graphics_stack[#graphics_stack].flatness = flatness
   delayed_print(string.format('%.3f i', flatness))
 end,
 currentpoint = function()
   local current_point = assert(graphics_stack[#graphics_stack].current_point, 'nocurrentpoint')
   push(current_point[1])
   push(current_point[2])
 end,

 moveto = function()
   local y = pop_num()
   local x = pop_num()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if current_path then
     local i = #current_path
     if i ~= 1 and current_path[i] == 'm' then
       current_path[i-2], current_path[i-1] = x, y
     else
       current_path[i+1], current_path[i+2], current_path[i+3] = x, y, 'm'
     end
     local current_point = state.current_point
     current_point[1], current_point[2] = x, y
   else
     state.current_path = {x, y, 'm'}
     state.current_point = {x, y}
   end
 end,
 rmoveto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local y = pop_num()
   local x = pop_num()
   local current_point = state.current_point
   x, y = current_point[1] + x, current_point[2] + y
   local i = #current_path
   if i ~= 1 and current_path[i] == 'm' then
     current_path[i-2], current_path[i-1] = x, y
   else
     current_path[i+1], current_path[i+2], current_path[i+3] = x, y, 'm'
   end
   current_point[1], current_point[2] = x, y
 end,
 lineto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local y = pop_num()
   local x = pop_num()
   local i = #current_path + 1
   current_path[i], current_path[i+1], current_path[i+2] = x, y, 'l'
   local current_point = state.current_point
   current_point[1], current_point[2] = x, y
 end,
 rlineto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local y = pop_num()
   local x = pop_num()
   local current_point = state.current_point
   x, y = x + current_point[1], y + current_point[2]
   local i = #current_path + 1
   current_path[i], current_path[i+1], current_path[i+2] = x, y, 'l'
   current_point[1], current_point[2] = x, y
 end,
 curveto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local y3 = pop_num()
   local x3 = pop_num()
   local y2 = pop_num()
   local x2 = pop_num()
   local y1 = pop_num()
   local x1 = pop_num()
   local i = #current_path + 1
   current_path[i], current_path[i+1], current_path[i+2], current_path[i+3], current_path[i+4], current_path[i+5], current_path[i+6] = x1, y1, x2, y2, x3, y3, 'c'
   local current_point = state.current_point
   current_point[1], current_point[2] = x3, y3
 end,
 rcurveto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local current_point = state.current_point
   local x0, y0 = current_point[1], current_point[2]
   local y3 = pop_num() + y0
   local x3 = pop_num() + x0
   local y2 = pop_num() + y0
   local x2 = pop_num() + x0
   local y1 = pop_num() + y0
   local x1 = pop_num() + x0
   local i = #current_path + 1
   current_path[i], current_path[i+1], current_path[i+2], current_path[i+3], current_path[i+4], current_path[i+5], current_path[i+6] = x1, y1, x2, y2, x3, y3, 'c'
   local current_point = state.current_point
   current_point[1], current_point[2] = x3, y3
 end,
 closepath = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   local current_point = state.current_point
   if not current_path then return end
   if current_path[#current_path] == 'h' then return end
   local x, y
   for i=#current_path, 1, -1 do
     if current_path[i] == 'm' then
       x, y = assert(tonumber(current_path[i-2])), assert(tonumber(current_path[i-1]))
     end
   end
   current_point[1], current_point[2] = assert(x), y
   current_path[#current_path + 1] = 'h'
 end,

 arc = function()
   local a2 = pop_num()
   local a1 = pop_num()
   local r = pop_num()
   local yc = pop_num()
   local xc = pop_num()
   while a2 < a1 do
     a2 = a2 + 360
   end
   drawarc(xc, yc, r, a1, a2)
 end,
 arcn = function()
   local a2 = pop_num()
   local a1 = pop_num()
   local r = pop_num()
   local yc = pop_num()
   local xc = pop_num()
   while a1 < a2 do
     a1 = a1 + 360
   end
   drawarc(xc, yc, r, a1, a2)
 end,
 arcto = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = assert(state.current_path, 'nocurrentpoint')
   local current_point = state.current_point
   local x0, y0 = current_point[1], current_point[2]

   local r = pop_num()
   local y2 = pop_num()
   local x2 = pop_num()
   local y1 = pop_num()
   local x1 = pop_num()

   local dx1, dy1 = x1 - x0, y1 - y0
   local dx2, dy2 = x2 - x1, y2 - y1

   local a1 = math.atan(dy1, dx1)
   local a2 = math.atan(dy2, dx2)

   if a1 - pi > a2 then
     a1 = a1 - two_pi
   elseif a2 - pi > a1 then
     a2 = a2 - two_pi
   end

   if a1 > a2 then
     a1 = a1 + math.pi/2
     a2 = a2 + math.pi/2
   else
     a1 = a1 - math.pi/2
     a2 = a2 - math.pi/2
   end

   local ox1, oy1 = r * math.cos(a1), r * math.sin(a1)
   local ox2, oy2 = r * math.cos(a2), r * math.sin(a2)
   -- Now we need to calculate the intersection of the lines offset by o1/o2
   -- to determine the center. We inlin eth ematix inverse for performance and better handling of edge cases.
   -- local t1, t2 = matrix_transform(0, 0, matrix_invert(dx1, dy1, dx2, dy2, ox2-ox1, oy2-oy1))
   local det = dx1*dy2 - dy1*dx2
   if math.abs(det) < 0.0000001 then
     -- Just draw a line
     push(x1)
     push(y1)
     systemdict.value.lineto()
     push(x1)
     push(y1)
     push(x1)
     push(y1)
     return
   end
   local t1 = (ox1 - ox2) * dy2/det + (oy2 - oy1) * dx2/det
   local cx, cy = x1 - ox1 + t1 * dx1, y1 - oy1 + t1 * dy1
   -- local ccx, ccy = x1 - ox2 - t2 * dx2, y1 - oy2 + t2 * dy2
   drawarc(cx, cy, r, a1*180/pi, a2*180/pi)

   push(cx + ox1)
   push(cy + oy1)
   push(cx + ox2)
   push(cy + oy2)
 end,
 arct = function()
   systemdict.value.arcto()
   pop()
   pop()
   pop()
   pop()
 end,

 eoclip = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if not current_path then return end
   flush_delayed(true)
   for i = 1, #current_path do
     if type(current_path[i]) == 'number' then
       pdfprint(string.format('%.5f', current_path[i]))
     else
       pdfprint(current_path[i])
     end
   end
   pdfprint'W* n'
 end,
 clip = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if not current_path then return end
   flush_delayed(true)
   for i = 1, #current_path do
     if type(current_path[i]) == 'number' then
       pdfprint(string.format('%.5f', current_path[i]))
     else
       pdfprint(current_path[i])
     end
   end
   pdfprint'W n'
 end,
 eofill = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if not current_path then return end
   current_path[#current_path+1] = 'f*'
   flush_delayed()
   local x
   for i = 1, #current_path do
     local value = current_path[i]
     if type(value) == 'number' then
       current_path[i] = string.format('%.5f', value)
       if x then
         register_point(state, x, value)
         x = nil
       else
         x = value
       end
     end
   end
   pdfprint((table.concat(current_path, ' '):gsub('%.?0+ ', ' ')))
   state.current_path, state.current_point = nil
 end,
 fill = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if not current_path then return end
   current_path[#current_path+1] = 'f'
   flush_delayed()
   local x
   for i = 1, #current_path do
     local value = current_path[i]
     if type(value) == 'number' then
       current_path[i] = string.format('%.5f', value)
       if x then
         register_point(state, x, value)
         x = nil
       else
         x = value
       end
     end
   end
   pdfprint((table.concat(current_path, ' '):gsub('%.?0+ ', ' ')))
   state.current_path, state.current_point = nil
 end,
 stroke = function()
   local state = graphics_stack[#graphics_stack]
   local current_path = state.current_path
   if not current_path then return end
   current_path[#current_path+1] = 'S'
   flush_delayed()
   local x
   for i = 1, #current_path do
     local value = current_path[i]
     if type(value) == 'number' then
       current_path[i] = string.format('%.5f', value)
       if x then
         register_point(state, x, value)
         x = nil
       else
         x = value
       end
     end
   end
   pdfprint((table.concat(current_path, ' '):gsub('%.?0+ ', ' ')))
   state.current_path, state.current_point = nil
 end,
 flattenpath = function()
   local state = graphics_stack[#graphics_stack]
   local old_path = state.current_path
   if not old_path then return end
   local new_path = {}
   local last_x, last_y = nil, 0
   local saved_x, saved_y
   local subpath_x, subpath_y
   local last_op = 1
   local matrix = state.matrix
   local tolerance = state.flatness / math.sqrt(matrix[1]*matrix[4]-matrix[2]*matrix[3])
   for i=1, #old_path do
     local entry = old_path[i]
     if type(entry) == 'string' then
       if entry == 'c' then
         assert(i - last_op == 6)
         flatten(new_path, tolerance, saved_x, saved_y, table.unpack(old_path, last_op, i-1))
         table.move(old_path, last_op + 4, last_op + 5, #new_path + 1, new_path)
         new_path[#new_path+1] = 'l'
       else
         if entry == 'm' then
           subpath_x, subpath_y = last_x, last_y
         elseif entry == 'h' then
           last_x, last_y = subpath_x, subpath_y
         end
         table.move(old_path, last_op, i, #new_path + 1, new_path)
       end
       saved_x, saved_y = last_x, last_y
       last_op = i + 1
     else
       if last_y then
         last_x, last_y = entry
       else
         last_y = entry
       end
     end
   end
   assert(last_op == #old_path + 1)
   state.current_path = new_path
 end,

 rectclip = function()
   flush_delayed()
   local top = pop()
   if type(top) == 'table' and top.kind == 'executable' then
     top = top.value
   end
   if type(top) == 'number' then
     local h = top
     local w = pop_num()
     local y = pop_num()
     local x = pop_num()
     pdfprint((string.format('%.5f %.5f %.5f %.5f re W n', x, y, w, h):gsub('%.?0+ ', ' ')))
   else
     error'Unsupported rectclip variant'
   end
 end,
 rectstroke = function()
   flush_delayed()
   local top = pop()
   if type(top) == 'table' and top.kind == 'executable' then
     top = top.value
   end
   if type(top) == 'number' then
     local h = top
     local w = pop_num()
     local y = pop_num()
     local x = pop_num()
     pdfprint((string.format('%.5f %.5f %.5f %.5f re S', x, y, w, h):gsub('%.?0+ ', ' ')))
     local state = graphics_stack[#graphics_stack]
     register_point(state, x, y)
     register_point(state, x + w, y + h)
   else
     error'Unsupported rectstroke variant'
   end
 end,
 rectfill = function()
   flush_delayed()
   local top = pop()
   if type(top) == 'table' and top.kind == 'executable' then
     top = top.value
   end
   if type(top) == 'number' then
     local h = top
     local w = pop_num()
     local y = pop_num()
     local x = pop_num()
     pdfprint((string.format('%.5f %.5f %.5f %.5f re f', x, y, w, h):gsub('%.?0+ ', ' ')))
     local state = graphics_stack[#graphics_stack]
     register_point(state, x, y)
     register_point(state, x + w, y + h)
   else
     error'Unsupported rectfill variant'
   end
 end,

 shfill = function()
   local shading_dict, arg1 = pop_dict()
   flush_delayed()
   local data_src
   local pdf_dict = ''
   for k, v in next, shading_dict do
     if k == 'DataSource' then
       data_src = v
     else
       pdf_dict = pdf_dict .. serialize_pdf(k) .. ' ' .. serialize_pdf(v)
     end
   end
   if shading_dict.ShadingType == 4 then
     assert(data_src)
     if type(data_src) ~= 'table' then
       push(arg1)
       ps_error'typecheck'
     end
     if data_src.kind == 'string' then
       data_src = data_src.value
     elseif data_src.kind == 'array' then
       data_src = data_src.value
       local color_model = shading_dict.ColorSpace.value[1]
       if type(color_model) == 'table' and color_model.kind == 'name' then
         color_model = color_model.value
       end
       if color_model == 'DeviceRGB' then
         color_model = 3
       elseif color_model == 'DeviceCMYK' then
         color_model = 4
       elseif color_model == 'DeviceGray' then
         color_model = 1
       else
         error'Unsupported color model in Shading dictionary'
       end
       local components = color_model + 3
       pdf_dict = pdf_dict .. '/BitsPerCoordinate 24/BitsPerComponent 8/BitsPerFlag 8/Decode[-8192 8191 -8192 8191' .. string.rep(' 0 1', color_model) .. ']'
       local data = ''
       for i = 1, #data_src-components+1, components do
         data = data .. string.pack('>BI3I3', data_src[i], (data_src[i+1]*1024+.5)//1 + 8388608, (data_src[i+2]*1024+.5)//1 + 8388608)
         for j = i + 3, i + 2 + color_model do
           data = data .. string.pack('B', (data_src[j]*255+.5)//1)
         end
       end
       data_src = data
     else
       error'Unsupported DataSource variant'
     end
     local obj = write_shading(pdf_dict, data_src)
     pdfprint(string.format('%s sh', write_shading(pdf_dict, data_src)))
   end
 end,

 scale = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     local mv = m.value
     if #mv ~= 6 then error'Unexpected size of matrix' end
     local y = pop_num()
     local x = pop_num()
     mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = x, 0, 0, y, 0, 0
     push(m)
   else
     push(m)
     local y = pop_num()
     local x = pop_num()
     update_matrix(x, 0, 0, y, 0, 0)
   end
 end,
 translate = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     local mv = m.value
     if #mv ~= 6 then error'Unexpected size of matrix' end
     local y = pop_num()
     local x = pop_num()
     mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = 1, 0, 0, 1, x, y
     push(m)
   else
     push(m)
     local y = pop_num()
     local x = pop_num()
     update_matrix(1, 0, 0, 1, x, y)
   end
 end,
 rotate = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     local mv = m.value
     if #mv ~= 6 then error'Unexpected size of matrix' end
     local angle = math.rad(pop_num())
     local s, c = math.sin(angle), math.cos(angle)
     mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = c, s, -s, c, 0, 0
     push(m)
   else
     push(m)
     local angle = math.rad(pop_num())
     local s, c = math.sin(angle), math.cos(angle)
     update_matrix(c, s, -s, c, 0, 0)
   end
 end,
 transform = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     m = m.value
     if #m ~= 6 then error'Unexpected size of matrix' end
   else
     push(m)
     m = graphics_stack[#graphics_stack].matrix
   end
   local y = pop_num()
   local x = pop_num()
   x, y = matrix_transform(x, y, m[1], m[2], m[3], m[4], m[5], m[6])
   push(x)
   push(y)
 end,
 itransform = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     m = m.value
     if #m ~= 6 then error'Unexpected size of matrix' end
   else
     push(m)
     m = graphics_stack[#graphics_stack].matrix
   end
   local y = pop_num()
   local x = pop_num()
   x, y = matrix_transform(x, y, matrix_invert(m[1], m[2], m[3], m[4], m[5], m[6]))
   push(x)
   push(y)
 end,
 dtransform = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     m = m.value
     if #m ~= 6 then error'Unexpected size of matrix' end
   else
     push(m)
     m = graphics_stack[#graphics_stack].matrix
   end
   local y = pop_num()
   local x = pop_num()
   x, y = matrix_transform(x, y, m[1], m[2], m[3], m[4], 0, 0)
   push(x)
   push(y)
 end,
 idtransform = function()
   local m = pop()
   if type(m) == 'table' and m.kind == 'array' then
     m = m.value
     if #m ~= 6 then error'Unexpected size of matrix' end
   else
     push(m)
     m = graphics_stack[#graphics_stack].matrix
   end
   local y = pop_num()
   local x = pop_num()
   x, y = matrix_transform(x, y, matrix_invert(m[1], m[2], m[3], m[4], 0, 0))
   push(x)
   push(y)
 end,
 concatmatrix = function()
   local m3a = pop_array()
   local m3 = m3a.value
   if #m3 ~= 6 then error'Unexpected size of matrix' end
   local m2 = pop_array().value
   if #m2 ~= 6 then error'Unexpected size of matrix' end
   local m1 = pop_array().value
   if #m1 ~= 6 then error'Unexpected size of matrix' end
   m3[1], m3[2],
   m3[3], m3[4],
   m3[5], m3[6]
     = m1[1] * m2[1] + m1[2] * m2[3],         m1[1] * m2[2] + m1[2] * m2[4],
       m1[3] * m2[1] + m1[4] * m2[3],         m1[3] * m2[2] + m1[4] * m2[4],
       m1[5] * m2[1] + m1[6] * m2[3] + m2[5], m1[5] * m2[2] + m1[6] * m2[4] + m2[6]
   push(m3a)
 end,
 invertmatrix = function()
   local target = pop_array()
   local T = target.value
   assert(#T == 6)
   local M = pop_array().value
   assert(#M == 6)
   T[1], T[2], T[3], T[4], T[5], T[6]
     = matrix_invert(M[1], M[2], M[3], M[4], M[5], M[6])
   push(target)
 end,
 concat = function()
   local m = pop_array().value
   if #m ~= 6 then error'Unexpected size of matrix' end
   update_matrix(m[1], m[2], m[3], m[4], m[5], m[6])
 end,
 -- setmatrix is not supported in PDF, so we invert the old matrix first
 setmatrix = function()
   local m = pop()
   if type(m) ~= 'table' or m.kind ~= 'array' then
     ps_error'typecheck'
   end
   local m = m.value
   if #m ~= 6 then ps_error'rangecheck' end
   local old = graphics_stack[#graphics_stack].matrix
   local pt = graphics_stack[#graphics_stack].current_point
   local a, b, c, d, e, f = matrix_invert(old[1], old[2], old[3], old[4], old[5], old[6])
   update_matrix(a, b, c, d, e, f)
   update_matrix(m[1], m[2], m[3], m[4], m[5], m[6])
 end,
 setpdfcolor = function()
   local pdf = pop_string().value
   local color = graphics_stack[#graphics_stack].color
   delayed_print(pdf)
   color.space = {kind = 'array', value = {{kind = 'name', value = 'PDF'}}}
   for i=2, #color do color[i] = nil end
   color[1] = pdf
 end,
 setgray = function()
   local g = pop_num()
   local color = graphics_stack[#graphics_stack].color
   color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceGray'}}}
   for i=2, #color do color[i] = nil end
   color[1] = g
   delayed_print(string.format('%.3f g %.3f G', g, g))
 end,
 setrgbcolor = function()
   local b = pop_num()
   local g = pop_num()
   local r = pop_num()
   local color = graphics_stack[#graphics_stack].color
   color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceRGB'}}}
   for i=4, #color do color[i] = nil end
   color[1], color[2], color[3] = r, g, b
   delayed_print(string.format('%.3f %.3f %.3f rg %.3f %.3f %.3f RG', r, g, b, r, g, b))
 end,
 -- Conversion based on Wikipedia article about HSB colorspace
 sethsbcolor = function()
   local b = pop_num()
   local s = pop_num()
   local h = pop_num()
   if b < 0 then b = 0 elseif b > 1 then b = 1 end
   if s < 0 then s = 0 elseif s > 1 then s = 1 end
   if h < 0 then h = 0 elseif h > 1 then h = 1 end
   local hi, hf = math.modf(6 * h)
   local p, q, t = b * (1 - s), b * (1 - s*hf), b * (1 - s * (1-hf))
   if hi == 0 or hi == 6 then
     push(b) push(t) push(p)
   elseif hi == 1 then
     push(q) push(b) push(p)
   elseif hi == 2 then
     push(p) push(b) push(t)
   elseif hi == 3 then
     push(p) push(q) push(b)
   elseif hi == 4 then
     push(t) push(p) push(b)
   elseif hi == 5 then
     push(b) push(p) push(q)
   end
   return systemdict.value.setrgbcolor()
 end,
 setcmykcolor = function()
   local k = pop_num()
   local y = pop_num()
   local m = pop_num()
   local c = pop_num()
   local color = graphics_stack[#graphics_stack].color
   color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceCMYK'}}}
   for i=5, #color do color[i] = nil end
   color[1], color[2], color[3], color[4] = c, m, y, k
   delayed_print(string.format('%.3f %.3f %.3f %.3f k %.3f %.3f %.3f %.3f K', c, m, y, k, c, m, y, k))
 end,
 ['.setopacityalpha'] = function()
   error'Unsupported, use .setfillconstantalpha instead'
 end,
 ['.setfillconstantalpha'] = function()
   local alpha = pop_num()
   graphics_stack[#graphics_stack].fillconstantalpha = alpha
   delayed_print(ExtGState['<</ca ' .. alpha .. '>>'])
 end,
 ['.setstrokeconstantalpha'] = function()
   local alpha = pop_num()
   graphics_stack[#graphics_stack].strokeconstantalpha = alpha
   delayed_print(ExtGState['<</CA ' .. alpha .. '>>'])
 end,
 ['.currentalphaisshape'] = function()
   local ais = graphics_stack[#graphics_stack].alphaisshape
   if ais == nil then error'alphaisshape has to be set before it is queried' end
   push(ais)
 end,
 ['.setalphaisshape'] = function()
   local ais = pop_bool()
   graphics_stack[#graphics_stack].alphaisshape = ais
   delayed_print(ExtGState['<</AIS ' .. (ais and 'true' or 'false') .. '>>'])
 end,
 ['.currentblendmode'] = function()
   local blendmode = graphics_stack[#graphics_stack].blendmode
   if blendmode == nil then error'blendmode has to be set before it is queried' end
   push{kind = 'name', value = blendmode}
 end,
 ['.setblendmode'] = function()
   local blendmode = pop()
   if type(blendmode) == 'string' then
   elseif type(blendmode) == 'table' and blendmode.kind == 'name' then
     blendmode = blendmode.value
   else
     push(blendmode)
     ps_error'typecheck'
   end
   graphics_stack[#graphics_stack].blendmode = blendmode
   delayed_print(ExtGState['<</BM /' .. blendmode .. '>>'])
 end,
 newpath = function()
   local state = graphics_stack[#graphics_stack]
   state.current_point = nil
   state.current_path = nil
 end,

 currentcolorspace = function()
   local color = graphics_stack[#graphics_stack].color
   if not color then error'Color has to be set before it is queried' end
   push(color.space)
 end,
 currentcolor = function()
   local color = graphics_stack[#graphics_stack].color
   if not color then error'Color has to be set before it is queried' end
   for i = 1, #color do
     push(color[i])
   end
 end,
 currentcmykcolor = function()
   local c, m, y, k
   local color = graphics_stack[#graphics_stack].color
   if not color then error'Color has to be set before it is queried' end
   local space = color.space.value[1]
   if type(space) == 'table' and space.kind == 'name' then space = space.value end
   if space == 'DeviceRGB' then
     c, m, y = 1 - color[1], 1 - color[2], 1 - color[3]
     -- k = math.min(c, m, y)
     -- TODO: Undercolor removal/black generation
     -- local undercolor = undercolorremoval(k)
     -- local undercolor = 0
     -- k = blackgeneration(k)
     k = 0
     -- c, m, y = c - undercolor, y - undercolor, k - undercolor
   elseif space == 'DeviceGray' then
     c, m, y, k = 0, 0, 0, 1 - color[1]
   elseif space == 'DeviceCMYK' then
     c, m, y, k = color[1], color[2], color[3], color[4]
   elseif space == 'PDF' then
     c, m, y, k = 0, 0, 0, 1
     print('???', 'tocmyk', color[1])
   else
     r, g, b, k = 0, 0, 0, 1
   end
   push(r)
   push(g)
   push(b)
 end,
 currentgraycolor = function()
   local g
   local color = graphics_stack[#graphics_stack].color
   if not color then error'Color has to be set before it is queried' end
   local space = color.space.value[1]
   if type(space) == 'table' and space.kind == 'name' then space = space.value end
   if space == 'DeviceRGB' then
     g = 0.3 * color[1] + 0.59 * color[2], 0.11 * color[3]
   elseif space == 'DeviceGray' then
     g = color[1]
   elseif space == 'DeviceCMYK' then
     g = math.min(1, math.max(0, 0.3 * color[1] + 0.59 * color[2] + 0.11 * color[3] + color[4]))
   elseif space == 'PDF' then
     g = 1
     print('???', 'togray', color[1])
   else
     g = 1
   end
   push(g)
 end,
 currentrgbcolor = function()
   local r, g, b
   local color = graphics_stack[#graphics_stack].color
   if not color then error'Color has to be set before it is queried' end
   local space = color.space.value[1]
   if type(space) == 'table' and space.kind == 'name' then space = space.value end
   if space == 'DeviceRGB' then
     r, g, b = color[1], color[2], color[3]
   elseif space == 'DeviceGray' then
     r = color[1]
     g, b = r, r
   elseif space == 'DeviceCMYK' then
     local c, m, y, k = color[1], color[2], color[3], color[4]
     c, m, y = c+k, m+k, y+k
     r, g, b = c >= 1 and 0 or 1-c, m >= 1 and 0 or 1-m, y >= 1 and 0 or 1-y
   elseif space == 'PDF' then
     r, g, b = 0, 0, 0
     print('???', 'torgb', color[1])
   else
     r, g, b = 0, 0, 0
   end
   push(r)
   push(g)
   push(b)
 end,
 currenthsbcolor = function()
   systemdict.value.currentrgbcolor()
   local b = pop_num()
   local g = pop_num()
   local r = pop_num()
   local M, m = math.max(r, g, b), math.min(r, g, b)
   local H
   if M == m then
     H = 0
   elseif M == r then
     H = (g-b)/(M-m) / 6
     if H < 0 then H = H + 1 end
   elseif M == g then
     H = (b-r)/(M-m) / 6 + 1/3
   elseif assert(M == b) then
     H = (r-g)/(M-m) / 6 + 2/3
   end
   local S = M == 0 and 0 or (M-m)/M
   local B = M
   push(H)
   push(S)
   push(B)
 end,
 currentfont = function()
   local f = graphics_stack[#graphics_stack].font
   if f then
     push(f)
   else
     push{kind = 'dict', value = {
        FID = font.current(),
        FontMatrix = {kind = 'array', value = {1, 0, 0, 1, 0, 0}},
        FontName = {kind = 'name', value = tex.fontname(font.current())},
        FontType = 0x1CA,
      }}
   end
 end,

 gsave = function()
   local bbox = graphics_stack[#graphics_stack].bbox
   graphics_stack[#graphics_stack+1] = table.copy(graphics_stack[#graphics_stack], bbox and {[bbox] = {}})
   graphics_stack[#graphics_stack].saved_delayed = delayed
   delayed = {
     text = {},
     matrix = {1, 0, 0, 1, 0, 0},
   }
 end,
 grestore = function()
   local state = graphics_stack[#graphics_stack]
   local saved_delayed = state.saved_delayed
   if saved_delayed then
     delayed = saved_delayed
   else
     pdfprint'Q'
     reset_delayed(delayed)
   end
   local upper_state = graphics_stack[#graphics_stack-1]
   local upper_bbox = upper_state.bbox
   if upper_bbox then
     local bbox = assert(state.bbox)
     while upper_bbox ~= bbox do
       bbox = merge_bbox(bbox, bbox.next or upper_bbox)
     end
   end
   graphics_stack[#graphics_stack] = nil
 end,

 setglobal = pop_bool,

 flush = function()
   io.stdout:flush()
 end,
 print = function()
   local msg = pop_string()
   io.stdout:write(msg.value)
 end,
 stack = function()
   for i=#operand_stack, 1, -1 do
     texio.write_nl('term and log', ps_to_string(operand_stack[i]))
   end
 end,
 pstack = function()
   for i=#operand_stack, 1, -1 do
     texio.write_nl('term and log', ps_to_string(require'inspect'(operand_stack[i])))
   end
 end,
 ['='] = function()
   texio.write_nl('term and log', ps_to_string(pop()))
 end,
 ['=='] = function() -- FIXME: Should give a better representation
   texio.write_nl('term and log', require'inspect'((pop())))
 end,

 stringwidth = function()
   local state = graphics_stack[#graphics_stack]
   local rawpsfont = assert(state.font, 'invalidfont')
   local str = pop_string().value
   local psfont = rawpsfont.value
   local fid = psfont.FID
   local matrix = psfont.FontMatrix.value
   local fonttype = psfont.FontType
   if fonttype ~= 0x1CA and fonttype ~= 3 then
     texio.write_nl'luapstricks: Attempt to use unsupported font type.'
     ps_error('invalidfont')
   end
   local w = 0
   if fonttype == 0x1CA then
     local characters = assert(font.getfont(fid)).characters
     for b in string.bytes(str) do
       local char = characters[b]
       w = w + (char and char.width or 0)
     end
     w = w/65781.76
   elseif fonttype == 3 then
     local saved_delayed = delayed
     delayed = {
       text = {},
       matrix = {1, 0, 0, 1, 0, 0},
     }
     local saved_saved_delayed = state.saved_delayed
     state.saved_delayed = nil
     local saved_pdfprint = pdfprint
     pdfprint = gobble
     for b in string.bytes(str) do
       systemdict.value.gsave()
       local state = graphics_stack[#graphics_stack]
       state.current_point, state.current_path = nil
       push(rawpsfont)
       push(b)
       local this_w
       char_width_storage = function(width)
         this_w = width
       end
       execute_tok(psfont.BuildChar) -- FIXME(maybe): Switch to BuildGlyph?
       systemdict.value.grestore()
       w = w + assert(this_w, 'Type 3 character failed to set width')
       update_matrix(1, 0, 0, 1, this_w, 0)
     end
     update_matrix(1, 0, 0, 1, -w, 0)
     pdfprint = saved_pdfprint
     state.saved_delayed = saved_saved_delayed
     delayed = saved_delayed
   end
   local x, y = matrix_transform(w, 0,
     matrix[1], matrix[2],
     matrix[3], matrix[4],
     0, 0)
   push(x)
   push(y)
 end,
 ashow = function()
   local str, arg3 = pop_string()
   local ay, arg2 = pop_num(arg3)
   local ax, arg1 = pop_num(arg2, arg3)
   local res, err = generic_show(str, ax, ay)
   if not res then
     ps_error(err, arg1, arg2, arg3)
   end
 end,
 show = function()
   local str, orig = pop_string()
   local res, err = generic_show(str)
   if not res then
     ps_error(err, orig)
   end
 end,
 definefont = function()
   local fontdict, raw_fontdict = pop_dict()
   local fontkey = pop_key()
   fontdict.FontMatrix = fontdict.FontMatrix or {kind = 'array', value = {1, 0, 0, 1, 0, 0}}
   if assert(fontdict.FontType) == 0x1CA then
     local fontname = fontdict.FontName
     if type(fontname) == 'table' and fontname.kind == 'name' then
       fontname = fontname.value
     elseif type(fontname) ~= 'string' then
       pushs(fontkey, raw_fontdict)
       ps_error'typecheck'
     end
     local fid = fonts.definers.read(fontname, 65782)
     if not fid then ps_error'invalidfont' end
     if not tonumber(fid) then
       local data = fid
       fid = font.define(data)
       fonts.definers.register(data, fid)
     end
     fontdict.FID = fid
   elseif fontdict.FontType == 3 then
   else
     texio.write_nl'luapstricks: definefont has been called with a font type which is not supported by luapstricks. I will continue, but attempts to use this font will fail.'
   end
   FontDirectory[fontkey] = raw_fontdict
   push(raw_fontdict)
 end,
 makefont = function()
   local m = pop_array().value
   if #m ~= 6 then error'Unexpected size of matrix' end
   local fontdict = pop_dict()
   local new_fontdict = {}
   for k,v in next, fontdict do
     new_fontdict[k] = v
   end
   local old_m = assert(fontdict.FontMatrix, 'invalidfont').value
   new_fontdict.FontMatrix = {kind = 'array', value = {
     old_m[1] * m[1] + old_m[2] * m[3],        old_m[1] * m[2] + old_m[2] * m[4],
     old_m[3] * m[1] + old_m[4] * m[3],        old_m[3] * m[2] + old_m[4] * m[4],
     old_m[5] * m[1] + old_m[6] * m[3] + m[5], old_m[5] * m[2] + old_m[6] * m[4] + m[6],
   }}
   push{kind = 'dict', value = new_fontdict}
 end,
 scalefont = function()
   local factor = pop_num()
   local fontdict = pop_dict()
   local new_fontdict = {}
   for k,v in next, fontdict do
     new_fontdict[k] = v
   end
   local old_m = assert(fontdict.FontMatrix, 'invalidfont').value
   new_fontdict.FontMatrix = {kind = 'array', value = {
     factor * old_m[1], factor * old_m[2],
     factor * old_m[3], factor * old_m[4],
     factor * old_m[5], factor * old_m[6],
   }}
   push{kind = 'dict', value = new_fontdict}
 end,
 setfont = function()
   local _, _, fontdict = pop_dict()
   local state = graphics_stack[#graphics_stack]
   state.font = fontdict
 end,
 ['.findfontid'] = function()
   local fid = pop_int()

   if font.frozen(fid) == nil then
     push(fid)
     ps_error'invalidfont'
   end
   local fontsize_inv = 65782/pdf.getfontsize(fid)
   local fontname = tex.fontname(fid)
   return push{kind = 'dict', value = {
     FID = fid,
     FontMatrix = {kind = 'array', value = {fontsize_inv, 0, 0, fontsize_inv, 0, 0}},
     FontName = {kind = 'name', value = fontname},
     FontType = 0x1CA,
   }}
 end,
 findfont = function()
   local fontname = pop_key()
   local fontdict = FontDirectory[fontname]
   if fontdict then push(fontdict) return end

   fontname = font_aliases[fontname] or fontname
   local fid = fonts.definers.read(fontname, 65782)
   if not fid then ps_error'invalidfont' end
   if not tonumber(fid) then
     local data = fid
     fid = font.define(data)
     fonts.definers.register(data, fid)
   end
   return push{kind = 'dict', value = {
     FID = fid,
     FontMatrix = {kind = 'array', value = {1, 0, 0, 1, 0, 0}},
     FontName = {kind = 'name', value = fontname},
     FontType = 0x1CA,
   }}
 end,
 selectfont = function()
   systemdict.value.exch()
   systemdict.value.findfont()
   systemdict.value.exch()
   if type(operand_stack[#operand_stack]) == 'number' then
     systemdict.value.scalefont()
   else
     systemdict.value.makefont()
   end
   systemdict.value.setfont()
 end,

 setcharwidth = function()
   -- Pop and ignore the advance height -- FIXME(maybe)
   pop_num()
   assert(char_width_storage, 'undefined')(pop_num())
   char_width_storage = nil
 end,
 setcachedevice = function()
   -- First pop and ignore the bounding box
   pop_num()
   pop_num()
   pop_num()
   pop_num()
   -- Fallback to setcharwidth
   systemdict.value.setcharwidth()
 end,
 setcachedevice2 = function()
   -- First pop additional entries for setccachedevice2 -- TODO: Implement other writing modes
   pop_num()
   pop_num()
   pop_num()
   pop_num()
   -- Fallback to setcachedevice
   systemdict.value.setcachedevice()
 end,

 findresource = function()
   local category = pop_key()
   local catdict = ResourceCategories.value[category]
   if not catdict then
     push(category)
     print('undefined resource category', category)
     ps_error'undefined'
   end
   local dict_height = #dictionary_stack + 1
   dictionary_stack[dict_height] = catdict
   execute_tok'FindResource'
   if #dictionary_stack ~= dict_height or dictionary_stack[dict_height] ~= catdict then
     error'Messed up dictionary stack in custom resource'
   end
   dictionary_stack[dict_height] = nil
 end,
 resourcestatus = function()
   local category = pop_key()
   local catdict = ResourceCategories.value[category]
   if not catdict then
     push(category)
     print('undefined resource category', category)
     ps_error'undefined'
   end
   local dict_height = #dictionary_stack + 1
   dictionary_stack[dict_height] = catdict
   execute_tok'ResourceStatus'
   if #dictionary_stack ~= dict_height or dictionary_stack[dict_height] ~= catdict then
     error'Messed up dictionary stack in custom resource'
   end
   dictionary_stack[dict_height] = nil
 end,
 resourceforall = function()
   local category = pop_key()
   local catdict = ResourceCategories.value[category]
   if not catdict then
     push(category)
     print('undefined resource category', category)
     ps_error'undefined'
   end
   local dict_height = #dictionary_stack + 1
   dictionary_stack[dict_height] = catdict
   execute_tok'ResourceForAll'
   if #dictionary_stack ~= dict_height or dictionary_stack[dict_height] ~= catdict then
     error'Messed up dictionary stack in custom resource'
   end
   dictionary_stack[dict_height] = nil
 end,
 defineresource = function()
   local category = pop_key()
   local catdict = ResourceCategories.value[category]
   if not catdict then
     push(category)
     print('undefined resource category', category)
     ps_error'undefined'
   end
   local dict_height = #dictionary_stack + 1
   dictionary_stack[dict_height] = catdict
   execute_tok'DefineResource'
   if #dictionary_stack ~= dict_height or dictionary_stack[dict_height] ~= catdict then
     error'Messed up dictionary stack in custom resource'
   end
   dictionary_stack[dict_height] = nil
 end,
 undefineresource = function()
   local category = pop_key()
   local catdict = ResourceCategories.value[category]
   if not catdict then
     push(category)
     print('undefined resource category', category)
     ps_error'undefined'
   end
   local dict_height = #dictionary_stack + 1
   dictionary_stack[dict_height] = catdict
   execute_tok'UndefineResource'
   if #dictionary_stack ~= dict_height or dictionary_stack[dict_height] ~= catdict then
     error'Messed up dictionary stack in custom resource'
   end
   dictionary_stack[dict_height] = nil
 end,

 realtime = function()
   push(os.gettimeofday() * 1000 // 1)
 end,

 rrand = function()
   push(rrand())
 end,
 srand = function()
   srand(pop_int())
 end,
 rand = function()
   push(rand())
 end,

 readonly = function() end, -- Concept not implemented
 type = function()
   local val = pop()
   local tval = type(val)
   if tval == 'table' and val.kind == 'executable' then
     val = val.value
     tval = type(val)
   end
   local tname
   if tval == 'string' then
     tname = 'nametype'
   elseif tval == 'number' then
     tname = math.type(val) == 'integer' and 'integertype' or 'realtype'
   elseif tval == 'boolean' then
     tname = 'booleantype'
   elseif tval == 'function' then
     tname = 'operatortype'
   elseif tval == 'table' then
     local kind = val.kind
     if kind == 'name' then
       tname = 'nametype'
     elseif kind == 'operator' then
       tname = 'operatortype'
     elseif kind == 'array' then
       tname = 'arraytype'
     elseif kind == 'dict' then
       tname = 'dicttype'
     elseif kind == 'dict' then
       tname = 'dicttype'
     elseif kind == 'null' then
       tname = 'nulltype'
     elseif kind == 'mark' then
       tname = 'nulltype'
     elseif kind == 'string' then
       tname = 'stringtype'
     else
       assert(false, 'Unexpected type')
     end
   else
     assert(false, 'Unexpected type')
   end
   push(tname)
   -- filetype
   -- fonttype
   -- gstatetype (LanguageLevel 2)
   -- packedarraytype (LanguageLevel 2)
   -- savetype
 end,
 xcheck = function()
   local a = pop()
   local ta = type(a)
   push(ta == 'function' or ta == 'name' or (ta == 'table' and a.kind == 'executable'))
 end,
 cvlit = function()
   local a = pop()
   local ta = type(a)
   if (ta == 'table' and a.kind == 'executable') or ta == 'string' or ta == 'function' then
     return push(a.value)
   end
   if ta == 'string' then
     return push{kind = 'name', value = a}
   end
   if ta == 'function' then
     return push{kind = 'operator', value = a}
   end
   return push(a)
 end,
 cvx = function()
   local a = pop()
   local ta = type(a)
   if (ta == 'table' and a.kind == 'executable') or ta == 'string' or ta == 'function' then
     return push(a)
   elseif ta == 'table' and (a.kind == 'operator' or a.kind == 'name') then
     return push(a.value)
   else
     return push{kind = 'executable', value = a}
   end
 end,
 exec = function()
   return execute_tok((pop()))
 end,
 stopped = function()
   local proc = pop()
   local success, err = pcall(execute_tok, proc)
   if success then
     push(false)
   elseif err == 'stop' or true then -- Since we don implement error handlers, all errors act like their error handler included "stop"
     push(true)
   end
 end,
 stop = function()
   error'stop'
 end,
 exit = function()
   error(exitmarker)
 end,
 quit = function()
   os.exit()
 end,
 run = function()
   local filename = pop_string().value
   local resolved = kpse.find_file(filename, 'PostScript header')
   if not resolved then
     error(string.format('Unable to find file %q.', filename))
   end
   local f = assert(io.open(resolved, 'rb'))
   local data = maybe_decompress(f:read'a')
   f:close()
   return execute_tok{kind = 'executable', value = {kind = 'string', value = data}}
 end,

 -- We don't implement local/global separation, so we ignore setglobal and always report currentglobal as true
 setglobal = function()
   pop()
 end,
 currentglobal = function()
   push(true)
 end,

 closefile = function()
   local f = pop()
   f:close()
 end,
 file = function()
   local access = pop_string()
   local orig_filename = pop_string()
   local filename = orig_filename.value
   if access.value:sub(1, 1) == 'a' then
     filename = kpse.find_file(filename)
     if not filename then
       push(orig_filename)
       push(access)
       ps_error'undefinedfilename'
     end
   end
   if access.value == '' then
     push(orig_filename)
     push(access)
     ps_error'invalidfileaccess'
   end
   local f = io.open(filename, access.value)
   if not f then
     push(orig_filename)
     push(access)
     ps_error'invalidfileaccess'
   end
   push(f)
 end,
 write = function()
   local data = pop_num()
   local f = pop()
   data = data % 256
   f:write(string.char(data))
 end,
 writestring = function()
   local data = pop_string().value
   local f = pop()
   f:write(data)
 end,
 readstring = function()
   local target = pop_string()
   local f = pop()
   local data = f:read(#target.value)
   if #target.value == #data then
     target.value = data
     push(target)
     push(true)
     systemdict.value.stack()
   else
     target = str_view(target, 1, #data)
     target.value = data
     push(target)
     push(false)
     systemdict.value.stack()
   end
 end,
 readline = function()
   local target = pop_string()
   local f = pop()
   local data = f:read'L' -- TODO: \r should be accepted as EOL marker too
   if data then
     if #data > #target.value then
       push(f)
       push(target)
       ps_error'rangecheck'
     end
     target = str_view(target, 1, #data)
     target.value = data
     push(target)
     push(true)
   else
     push{kind = 'string', value = ''}
     push(false)
   end
 end,

 token = function()
   local arg = pop()
   if type(arg) ~= 'table' or arg.kind ~= 'string' then
     push(arg)
     if type(arg) == 'userdata' and arg.read then
       error'token applied to file arguments is no yet implemented'
     else
       ps_error'typecheck'
     end
   end
   local str = arg.value
   local tok, after = l.match(any_object * l.Cp(), str)
   if after == nil then
     if l.match(whitespace^-1 * -1, str) then
       push(false)
     else
       push(arg)
       ps_error'syntaxerror'
     end
   else
     push(str_view(arg, after, #str - after + 1))
     push(tok)
     push(true)
   end
 end,

 ['.trackbbox'] = function()
   local state = graphics_stack[#graphics_stack]
   flush_delayed()
   state.bbox = { next = state.bbox, start = true }
 end,
 -- Trackedbbox should only be invoked if the current matrix is essentially the same
 -- as in the corresponding .trackbbox, otherwise everything gets messed up.
 -- This isn't checked, mostly because we don't want a check to be too sensitive.
 ['.trackedbbox'] = function()
   local state = graphics_stack[#graphics_stack]
   local bbox = state.bbox
   if not bbox then
     error'trackedbbox without matching trackbbox'
   end
   while not bbox.start do
     if not bbox.next then
       error'Illegal nesting of trackbbox/trackedbbox and gsave/grestore'
     end
     bbox = merge_bbox(bbox, bbox.next)
   end
   state.bbox = bbox.next and merge_bbox(bbox, bbox.next)
   push(bbox[1] or 0)
   push(bbox[2] or 0)
   push(bbox[3] or 0)
   push(bbox[4] or 0)
 end,

 revision = 1000,
 ['true'] = true,
 ['false'] = false,
 systemdict = systemdict,
 statusdict = statusdict,
 globaldict = globaldict,
 FontDirectory = FontDirectory,

 ISOLatin1Encoding = {kind = 'array', value = {
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = 'space'},
   {kind = 'name', value = 'exclam'},
   {kind = 'name', value = 'quotedbl'},
   {kind = 'name', value = 'numbersign'},
   {kind = 'name', value = 'dollar'},
   {kind = 'name', value = 'percent'},
   {kind = 'name', value = 'ampersand'},
   {kind = 'name', value = 'quoteright'},
   {kind = 'name', value = 'parenleft'},
   {kind = 'name', value = 'parenright'},
   {kind = 'name', value = 'asterisk'},
   {kind = 'name', value = 'plus'},
   {kind = 'name', value = 'comma'},
   {kind = 'name', value = 'minus'},
   {kind = 'name', value = 'period'},
   {kind = 'name', value = 'slash'},
   {kind = 'name', value = 'zero'},
   {kind = 'name', value = 'one'},
   {kind = 'name', value = 'two'},
   {kind = 'name', value = 'three'},
   {kind = 'name', value = 'four'},
   {kind = 'name', value = 'five'},
   {kind = 'name', value = 'six'},
   {kind = 'name', value = 'seven'},
   {kind = 'name', value = 'eight'},
   {kind = 'name', value = 'nine'},
   {kind = 'name', value = 'colon'},
   {kind = 'name', value = 'semicolon'},
   {kind = 'name', value = 'less'},
   {kind = 'name', value = 'equal'},
   {kind = 'name', value = 'greater'},
   {kind = 'name', value = 'question'},
   {kind = 'name', value = 'at'},
   {kind = 'name', value = 'A'},
   {kind = 'name', value = 'B'},
   {kind = 'name', value = 'C'},
   {kind = 'name', value = 'D'},
   {kind = 'name', value = 'E'},
   {kind = 'name', value = 'F'},
   {kind = 'name', value = 'G'},
   {kind = 'name', value = 'H'},
   {kind = 'name', value = 'I'},
   {kind = 'name', value = 'J'},
   {kind = 'name', value = 'K'},
   {kind = 'name', value = 'L'},
   {kind = 'name', value = 'M'},
   {kind = 'name', value = 'N'},
   {kind = 'name', value = 'O'},
   {kind = 'name', value = 'P'},
   {kind = 'name', value = 'Q'},
   {kind = 'name', value = 'R'},
   {kind = 'name', value = 'S'},
   {kind = 'name', value = 'T'},
   {kind = 'name', value = 'U'},
   {kind = 'name', value = 'V'},
   {kind = 'name', value = 'W'},
   {kind = 'name', value = 'X'},
   {kind = 'name', value = 'Y'},
   {kind = 'name', value = 'Z'},
   {kind = 'name', value = 'bracketleft'},
   {kind = 'name', value = 'backslash'},
   {kind = 'name', value = 'bracketright'},
   {kind = 'name', value = 'asciicircum'},
   {kind = 'name', value = 'underscore'},
   {kind = 'name', value = 'quoteleft'},
   {kind = 'name', value = 'a'},
   {kind = 'name', value = 'b'},
   {kind = 'name', value = 'c'},
   {kind = 'name', value = 'd'},
   {kind = 'name', value = 'e'},
   {kind = 'name', value = 'f'},
   {kind = 'name', value = 'g'},
   {kind = 'name', value = 'h'},
   {kind = 'name', value = 'i'},
   {kind = 'name', value = 'j'},
   {kind = 'name', value = 'k'},
   {kind = 'name', value = 'l'},
   {kind = 'name', value = 'm'},
   {kind = 'name', value = 'n'},
   {kind = 'name', value = 'o'},
   {kind = 'name', value = 'p'},
   {kind = 'name', value = 'q'},
   {kind = 'name', value = 'r'},
   {kind = 'name', value = 's'},
   {kind = 'name', value = 't'},
   {kind = 'name', value = 'u'},
   {kind = 'name', value = 'v'},
   {kind = 'name', value = 'w'},
   {kind = 'name', value = 'x'},
   {kind = 'name', value = 'y'},
   {kind = 'name', value = 'z'},
   {kind = 'name', value = 'braceleft'},
   {kind = 'name', value = 'bar'},
   {kind = 'name', value = 'braceright'},
   {kind = 'name', value = 'asciitilde'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = 'dotlessi'},
   {kind = 'name', value = 'grave'},
   {kind = 'name', value = 'acute'},
   {kind = 'name', value = 'circumflex'},
   {kind = 'name', value = 'tilde'},
   {kind = 'name', value = 'macron'},
   {kind = 'name', value = 'breve'},
   {kind = 'name', value = 'dotaccent'},
   {kind = 'name', value = 'dieresis'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = 'ring'},
   {kind = 'name', value = 'cedilla'},
   {kind = 'name', value = '.notdef'},
   {kind = 'name', value = 'hungarumlaut'},
   {kind = 'name', value = 'ogonek'},
   {kind = 'name', value = 'caron'},
   {kind = 'name', value = 'space'},
   {kind = 'name', value = 'exclamdown'},
   {kind = 'name', value = 'cent'},
   {kind = 'name', value = 'sterling'},
   {kind = 'name', value = 'currency'},
   {kind = 'name', value = 'yen'},
   {kind = 'name', value = 'brokenbar'},
   {kind = 'name', value = 'section'},
   {kind = 'name', value = 'dieresis'},
   {kind = 'name', value = 'copyright'},
   {kind = 'name', value = 'ordfeminine'},
   {kind = 'name', value = 'guillemotleft'},
   {kind = 'name', value = 'logicalnot'},
   {kind = 'name', value = 'hyphen'},
   {kind = 'name', value = 'registered'},
   {kind = 'name', value = 'macron'},
   {kind = 'name', value = 'degree'},
   {kind = 'name', value = 'plusminus'},
   {kind = 'name', value = 'twosuperior'},
   {kind = 'name', value = 'threesuperior'},
   {kind = 'name', value = 'acute'},
   {kind = 'name', value = 'mu'},
   {kind = 'name', value = 'paragraph'},
   {kind = 'name', value = 'periodcentered'},
   {kind = 'name', value = 'cedilla'},
   {kind = 'name', value = 'onesuperior'},
   {kind = 'name', value = 'ordmasculine'},
   {kind = 'name', value = 'guillemotright'},
   {kind = 'name', value = 'onequarter'},
   {kind = 'name', value = 'onehalf'},
   {kind = 'name', value = 'threequarters'},
   {kind = 'name', value = 'questiondown'},
   {kind = 'name', value = 'Agrave'},
   {kind = 'name', value = 'Aacute'},
   {kind = 'name', value = 'Acircumflex'},
   {kind = 'name', value = 'Atilde'},
   {kind = 'name', value = 'Adieresis'},
   {kind = 'name', value = 'Aring'},
   {kind = 'name', value = 'AE'},
   {kind = 'name', value = 'Ccedilla'},
   {kind = 'name', value = 'Egrave'},
   {kind = 'name', value = 'Eacute'},
   {kind = 'name', value = 'Ecircumflex'},
   {kind = 'name', value = 'Edieresis'},
   {kind = 'name', value = 'Igrave'},
   {kind = 'name', value = 'Iacute'},
   {kind = 'name', value = 'Icircumflex'},
   {kind = 'name', value = 'Idieresis'},
   {kind = 'name', value = 'Eth'},
   {kind = 'name', value = 'Ntilde'},
   {kind = 'name', value = 'Ograve'},
   {kind = 'name', value = 'Oacute'},
   {kind = 'name', value = 'Ocircumflex'},
   {kind = 'name', value = 'Otilde'},
   {kind = 'name', value = 'Odieresis'},
   {kind = 'name', value = 'multiply'},
   {kind = 'name', value = 'Oslash'},
   {kind = 'name', value = 'Ugrave'},
   {kind = 'name', value = 'Uacute'},
   {kind = 'name', value = 'Ucircumflex'},
   {kind = 'name', value = 'Udieresis'},
   {kind = 'name', value = 'Yacute'},
   {kind = 'name', value = 'Thorn'},
   {kind = 'name', value = 'germandbls'},
   {kind = 'name', value = 'agrave'},
   {kind = 'name', value = 'aacute'},
   {kind = 'name', value = 'acircumflex'},
   {kind = 'name', value = 'atilde'},
   {kind = 'name', value = 'adieresis'},
   {kind = 'name', value = 'aring'},
   {kind = 'name', value = 'ae'},
   {kind = 'name', value = 'ccedilla'},
   {kind = 'name', value = 'egrave'},
   {kind = 'name', value = 'eacute'},
   {kind = 'name', value = 'ecircumflex'},
   {kind = 'name', value = 'edieresis'},
   {kind = 'name', value = 'igrave'},
   {kind = 'name', value = 'iacute'},
   {kind = 'name', value = 'icircumflex'},
   {kind = 'name', value = 'idieresis'},
   {kind = 'name', value = 'eth'},
   {kind = 'name', value = 'ntilde'},
   {kind = 'name', value = 'ograve'},
   {kind = 'name', value = 'oacute'},
   {kind = 'name', value = 'ocircumflex'},
   {kind = 'name', value = 'otilde'},
   {kind = 'name', value = 'odieresis'},
   {kind = 'name', value = 'divide'},
   {kind = 'name', value = 'oslash'},
   {kind = 'name', value = 'ugrave'},
   {kind = 'name', value = 'uacute'},
   {kind = 'name', value = 'ucircumflex'},
   {kind = 'name', value = 'udieresis'},
   {kind = 'name', value = 'yacute'},
   {kind = 'name', value = 'thorn'},
   {kind = 'name', value = 'ydieresis'},
 }},

 -- In internal interface to allow package specific commands to be defined in separate file.
 -- This does not provide a stable interface for external extensions
 ['.loadplugin'] = function()
   local name = pop_key()
   local found = kpse.find_file(string.format('luapstricks-plugin-%s', name), 'lua')
   if not found then
     return push(false)
   end
   local loader = assert(loadfile(found))
   local plugin, version = loader('luapstricks', 0, plugin_interface)
   push{kind = 'dict', value = plugin}
   push(version)
   push(true)
 end,

 ['.build-image'] = function()
   local y = pop_num()
   local x = pop_num()
   local image = pop_array().value
   for i = 1, x*y do
     local rgb = image[i].value
     image[i] = string.pack('BBB', (rgb[1] * 255 + .5) // 1, (rgb[2] * 255 + .5) // 1, (rgb[3] * 255 + .5) // 1)
   end
   local i = img.scan {
     stream = table.concat(image),
     attr = string.format("/Type /XObject /Subtype /Image /Width %i /Height %i /BitsPerComponent 8 /ColorSpace /DeviceRGB", x, y),
     notype = true,
     nobbox = true,
     bbox = {0, 0, 65781.76, 65781.76}
   }
   push(function()
     flush_delayed()
     local state = graphics_stack[#graphics_stack]
     register_point(state, 0, 0)
     register_point(state, 1, 1)
     vf.push()
     local n = node.new'hlist'
     n.dir = 'TLT'
     n.head = img.node(i)
     vf.node(node.direct.todirect(n))
     node.free(n)
     vf.pop()
   end)
 end,
}}
systemdict.value.systemdict = systemdict
dictionary_stack = {systemdict, globaldict, userdict, userdict.value.TeXDict}
-- local execution_stack = {} -- Currently not implemented

-- Quite some stuff is missing here since these aren't implemented yet. Anyway mostly useful for testing.
ResourceCategories.value.Font = {kind = 'dict', value = {
 Category = {kind = 'name', value = 'Font'},
 InstanceType = 'dicttype',
 DefineResource = systemdict.value.definefont,
 FindResource = systemdict.value.findfont,
}}

ResourceCategories.value.Generic = {kind = 'dict', value = {
 Category = {kind = 'name', value = 'Generic'},
 DefineResource = function()
   local instance = pop()
   local key = pop_key()
   execute_tok'.Instances'
   local instances = pop_dict()
   instances[key] = instance
   push(instance)
 end,
 UndefineResource = function()
   local key = pop_key()
   execute_tok'.Instances'
   local instances = pop_dict()
   instances[key] = nil
 end,
 FindResource = function()
   local key = pop_key()
   execute_tok'.Instances'
   local instances = pop_dict()
   local instance = instances[key]
   if instance then
     push(instance)
     return
   end
   push(key)
   ps_error'undefinedresource'
 end,
 -- ResourceStatus = function()
 --   local key = pop_key()
 --   execute_tok'.Instances'
 --   local instances = pop_dict()
 --   local instance = instances[key]
 --   if instance then
 --     push(instance)
 --     return
 --   end
 --   push(key)
 --   ps_error'undefinedresource'
 -- end,
 -- ResourceForAll = function()
 --   local key = pop_key()
 --   execute_tok'.Instances'
 --   local instances = pop_dict()
 --   local instance = instances[key]
 --   if instance then
 --     push(instance)
 --     return
 --   end
 --   push(key)
 --   ps_error'undefinedresource'
 -- end,
 ['.Instances'] = {kind = 'dict', value = {}},
}}

local register_texbox do
 local meta = {__gc = function(t) node.direct.free(t.box) end}
 local dict = {}
 ResourceCategories.value['.TeXBox'] = {kind = 'dict', value = {
   Category = {kind = 'name', value = '.TeXBox'},
   DefineResource = function()
     push{kind = 'name', value = '.TeXBox'}
     ps_error'undefined'
   end,
   UndefineResource = function()
     local key = pop_key()
     dict[key] = nil
   end,
   FindResource = function()
     local key = pop_key()
     local instance = dict[key]
     if instance then
       push(instance)
       return
     end
     push(key)
     ps_error'undefinedresource'
   end,
 }}
 local id = 0
 function register_texbox(box)
   id = id + 1
   box = setmetatable({box = node.direct.todirect(box)}, meta)
   local op = function()
     flush_delayed()
     local state = graphics_stack[#graphics_stack]
     local copied = node.direct.copy(box.box)
     local w, h, d = node.direct.dimensions(copied)
     register_point(state, 0, -d/65781.76)
     register_point(state, w/65781.76, h/65781.76)
     vf.push()
     vf.node(copied)
     vf.pop()
     node.direct.free(copied)
   end
   lua_node_lookup[op] = box
   dict[id] = op
   return id
 end
end

ResourceCategories.value.Category = {kind = 'dict', value = {
 Category = {kind = 'name', value = 'Generic'},
 InstanceType = 'dicttype',
 DefineResource = function()
   local instance = pop()
   local key = pop_key()
   ResourceCategories.value[key] = instance
   push(instance)
 end,
 UndefineResource = function()
   local key = pop_key()
   ResourceCategories.value[key] = nil
 end,
 FindResource = function()
   local key = pop_key()
   local instance = ResourceCategories.value[key]
   if instance then
     push(instance)
     return
   end
   push(key)
   ps_error'undefinedresource'
 end,
 -- ResourceStatus = function()
 --   local key = pop_key()
 --   execute_tok'.Instances'
 --   local instances = pop_dict()
 --   local instance = instances[key]
 --   if instance then
 --     push(instance)
 --     return
 --   end
 --   push(key)
 --   ps_error'undefinedresource'
 -- end,
 -- ResourceForAll = function()
 --   local key = pop_key()
 --   execute_tok'.Instances'
 --   local instances = pop_dict()
 --   local instance = instances[key]
 --   if instance then
 --     push(instance)
 --     return
 --   end
 --   push(key)
 --   ps_error'undefinedresource'
 -- end,
}}

function execute_tok(tok, suppress_proc)
 local ttok = type(tok)
 if ttok == 'string' then
   return execute_tok(lookup(tok))
 elseif ttok == 'function' then
   return tok()
 elseif ttok == 'table' and tok.kind == 'executable' then
   local vtok = tok.value
   ttok = type(vtok)
   if suppress_proc and ttok == 'table' and tok.value.kind == 'array' then
     return push(tok)
   end
   if ttok == 'table' then
     local kind = vtok.kind
     if kind == 'array' then
       return execute_ps(vtok.value)
     elseif kind == 'string' then
       return execute_ps(assert(parse_ps(vtok.value), 'syntaxerror'))
     else
       error'Unimplemented'
     end
   elseif ttok == 'number' then
     return push(tok)
   else
     error'Unimplemented'
   end
 else
   return push(tok)
 end
end
plugin_interface.exec = execute_tok

function execute_ps(tokens)
 for i=1, #tokens do
   execute_tok(tokens[i], true)
 end
end
local any_object_or_end = any_object * l.Cp() + whitespace^-1 * -1 * l.Cc(nil) + l.Cp() * l.Cc(false)
function execute_string(str, context)
 local pos = 1
 while true do
   local tok
   tok, pos = any_object_or_end:match(str, pos)
   if pos then
     local success, err = pcall(execute_tok, tok, true)
     if not success then
       if context and type(err) == 'table' and err.pserror and not err.context then
         err.tok = tok
         err.context = context
       end
       error(err)
     end
   elseif pos == false then
     ps_error'syntaxerror'
   else
     break
   end
 end
end

-- If x, y shall be present iff direct ~= 'immediate'
local function outer_execute(tokens, direct, context, x, y)
 local TeXDict = userdict.value.TeXDict.value
 local saved_ocount = TeXDict.ocount
 local height = #operand_stack
 TeXDict.ocount = height
 local graphics_height
 if direct ~= 'immediate' then
   operand_stack[height + 1], operand_stack[height + 2] = x/65781.76, y/65781.76
   if direct then
     systemdict.value.moveto()
   else
     graphics_height = #graphics_stack
     systemdict.value.gsave()
     systemdict.value.translate()
   end
 end
 local success, err = pcall(execute_string, tokens, context)
 if not success then
   if type(err) == 'table' and err.pserror then
     tex.error(string.format('luapstricks: %q error occured while executing PS code from %q', err.pserror, err.context), {
       string.format('The error occured while executing the PS command %q.\n%s', err.tok, err.trace)
     })
   else
     error(err, 0)
   end
 end
 flush_delayed()
 if not direct then
   systemdict.value.grestore()
   if graphics_height ~= #graphics_stack then
     if graphics_height < #graphics_stack then
       texio.write_nl"luapstricks: PS block contains unbalanced gsave. grestore will be executed to compensate."
       repeat
         systemdict.value.grestore()
       until graphics_height == #graphics_stack
     else
       texio.write_nl"luapstricks: PS block contains unbalanced grestore."
     end
   end
   height = TeXDict.ocount or height
   local new_height = #operand_stack
   assert(new_height >= height)
   for k = height + 1, new_height do
     operand_stack[k] = nil
   end
   TeXDict.ocount = saved_ocount
 end
end

local ps_tokens, ps_direct, ps_context, ps_pos_x, ps_pos_y
local fid = font.define{
 name = 'dummy virtual font for PS rendering',
 -- type = 'virtual',
 characters = {
   [0x1F3A8] = {
     commands = {
       {'lua', function(fid)
         local n = node.new('glyph', 256)
         n.font = fid
         n.char = 1
         assert(not ps_pos_x)
         ps_pos_x, ps_pos_y = pdf.getpos()
         n.xoffset = -ps_pos_x
         n.yoffset = -ps_pos_y
         n = node.hpack(n, 0, 1, 0) -- Default width, TLT
         vf.node(node.direct.todirect(n))
         node.free(n)
       end}
     }
   },
   [1] = {
     commands = {
       {'lua', function()
         local tokens, x, y = assert(ps_tokens), ps_pos_x, ps_pos_y
         ps_tokens, ps_pos_x, ps_pos_y = nil
         return outer_execute(tokens, ps_direct, ps_context, x, y)
       end}
     }
   },
 },
}

local func = luatexbase.new_luafunction'luaPST'
token.set_lua('luaPST', func, 'protected')
lua.get_functions_table()[func] = function()
 local readstate = status.readstate or status
 local context = string.format('%s:%i', readstate.filename, readstate.linenumber)
 local direct = token.scan_keyword'immediate' and 'immediate' or token.scan_keyword'direct' and 'direct'
 local file = token.scan_keyword'file'
 local tokens = token.scan_argument(true)
 if file then
   context = tokens
   local resolved, msg = kpse.find_file(tokens, 'PostScript header')
   if not resolved then
     return tex.error(string.format('luapstricks: Unable to open %q: %s', tokens, msg))
   end
   local f = io.open(resolved, 'r')
   tokens = f:read'a'
   f:close()
 end
 if direct == 'immediate' then
   local saved_pdfprint = pdfprint
   pdfprint = no_pdfprint_allowed
   outer_execute(tokens, direct, context)
   pdfprint = saved_pdfprint
 else
   local n = node.new('whatsit', late_lua_sub)
   setwhatsitfield(n, 'data', function(n)
     assert(not ps_tokens)
     ps_tokens = tokens
     ps_direct = direct
     ps_context = context

     local nn = node.new('glyph')
     nn.subtype = 256
     nn.font, nn.char = fid, 0x1F3A8
     local list = node.new('hlist')
     list.head = nn
     list.direction = 0
     node.insert_after(n, n, list)
   end)
   node.write(n)
 end
end
tex.runtoks(function()
 tex.sprint[[\protected\def\luaPSTheader{\luaPST direct file}]]
end)

do
 func = luatexbase.new_luafunction'luaPSTcolor'
 token.set_lua('luaPSTcolor', func)
 local ps_rgb = 'rgb ' * l.C(l.P(1)^0) * l.Cc' setrgbcolor' * l.Cc'rgb '
 local ps_cmyk = 'cmyk ' * l.C(l.P(1)^0) * l.Cc' setcmykcolor' * l.Cc'cmyk '
 local ps_gray = 'gray ' * l.C(l.P(1)^0) * l.Cc' setgray' * l.Cc'gray '
 local ps_hsb = 'hsb ' * l.C(l.P(1)^0) * l.Cc' sethsbcolor' * l.Cc'hsb '
 local pscolor = ps_rgb + ps_gray + ps_gray + ps_hsb
 local pdf_rgb = l.Cmt(l.C(number * whitespace * number * whitespace * number / 0) * whitespace * 'rg'
               * whitespace * l.C(number * whitespace * number * whitespace * number / 0) * whitespace * 'RG' * -1, function(s, p, a, b)
                 if a == b then
                   return true, a, ' setrgbcolor', 'rgb '
                 else
                   return false
                 end
               end)
 local pdf_cmyk = l.Cmt(l.C(number * whitespace * number * whitespace * number * whitespace * number / 0) * whitespace * 'k'
                * whitespace * l.C(number * whitespace * number * whitespace * number * whitespace * number / 0) * whitespace * 'K' * -1, function(s, p, a, b)
                 if a == b then
                   return true, a, ' setcmykcolor', 'cmyk '
                 else
                   return false
                 end
               end)
 local pdf_gray = l.Cmt(l.C(number / 0) * whitespace * 'g'
                * whitespace * l.C(number / 0) * whitespace * 'G' * -1, function(s, p, a, b)
                 if a == b then
                   return true, a, ' setgray', 'gray '
                 else
                   return false
                 end
               end)
 local pdf_other = l.Cs(l.Cc'(' * l.P(1)^0 * l.Cc')') * l.C' setpdfcolor' * l.C'gray '
 local pdfcolor = pdf_rgb + pdf_cmyk + pdf_gray + pdf_other
 local anycolor = pscolor + pdfcolor
 lua.get_functions_table()[func] = function()
   local dvips_format = token.scan_keyword'dvips'
   local result, suffix, prefix = anycolor:match(token.scan_argument())
   tex.sprint(-2, dvips_format and prefix .. result or result .. suffix)
 end
end

func = luatexbase.new_luafunction'luaPSTbox'
token.set_lua('luaPSTbox', func)
lua.get_functions_table()[func] = function()
 local box = register_texbox(token.scan_list())
 tex.sprint(-2, tostring(box))
end