---- luapstricks.lua -- Copyright 2021--2023 Marcel Krüger -- -- 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<>/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 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 '<>' or '<>']) 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['<>']) end, ['.setstrokeconstantalpha'] = function() local alpha = pop_num() graphics_stack[#graphics_stack].strokeconstantalpha = alpha delayed_print(ExtGState['<>']) 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['<>']) 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['<>']) 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