diff options
-rw-r--r-- | Master/texmf-dist/doc/latex/luapstricks/README.md | 59 | ||||
-rw-r--r-- | Master/texmf-dist/tex/latex/luapstricks/luapstricks.lua | 3946 | ||||
-rwxr-xr-x | Master/tlpkg/bin/tlpkg-ctan-check | 3 | ||||
-rwxr-xr-x | Master/tlpkg/libexec/ctan2tds | 1 | ||||
-rw-r--r-- | Master/tlpkg/tlpsrc/collection-pstricks.tlpsrc | 1 | ||||
-rw-r--r-- | Master/tlpkg/tlpsrc/luapstricks.tlpsrc | 0 |
6 files changed, 4009 insertions, 1 deletions
diff --git a/Master/texmf-dist/doc/latex/luapstricks/README.md b/Master/texmf-dist/doc/latex/luapstricks/README.md new file mode 100644 index 00000000000..d7c32789835 --- /dev/null +++ b/Master/texmf-dist/doc/latex/luapstricks/README.md @@ -0,0 +1,59 @@ +# `luapstricks` -- A PSTricks backend for LuaTeX +Support PSTricks directly in LuaTeX, without requiring external processes, special environments or similar. + +Since recent PSTricks versions load `luapstricks` automatically when LuaLaTeX is used, this does not require changes to the document. +Some advanced features require the `pdfmanagement-testphase` package to be loaded and activated. + +## Usage + 1. Make sure that you have the latest pstricks version installed. + 2. Run `l3build install` *or* copy `luapstricks.lua` from this repository into the directory of your TeX files or another directory in your TeX search path. + 3. Compile your document with `lualatex`. + 4. Given the current state of development of this project, the previous stage probably failed. Write a bug report to the author. Otherwise go to step 7. + 5. Wait for a fix. + 6. Go back to step 2 and try again. + 7. It worked? That's great. Feel free to inform the author anyway and share the awesome images you created. + +## Issues and feature requests +Should you find a bug or want to request a feature, please report it using the [issue tracker](https://github.com/zauguin/luapstricks/issues). + +## License +This code is licensed under the LaTeX Project Public License 1.3. + +## Acknowledgments +This project would never have reached a usable state without all the testing and suggestions by Pablo González Luengo. +Also many thanks to Herbert Voß for adapting PSTricks and related packages to work with luapstricks. + +## Demos +Documentation of PSTricks packages, created by `lualatex` and `luapstricks.lua`: + +- [pst-user](https://hvoss.org/PSTexa/pst-user.pdf) Main (historical) documentation without overlay stuff (is now in pst-ovl) +- [pstricks-add](https://hvoss.org/PSTexa/pstricks-add-doc.pdf) + + +- [pst-3d](https://hvoss.org/PSTexa/pst-3d-doc.pdf) +- [pst-3dplot](https://hvoss.org/PSTexa/pst-3dplot-doc.pdf) +- [pst-am](https://hvoss.org/PSTexa/pst-am-doc.pdf) +- [pst-antiprism](https://hvoss.org/PSTexa/pst-antiprism-doc.pdf) +- [pst-barcode](https://hvoss.org/PSTexa/pst-barcode-doc.pdf) +- [pst-bezier](https://hvoss.org/PSTexa/pst-bezier-doc.pdf) +- [pst-cie](https://hvoss.org/PSTexa/pst-cie-doc.pdf) +- [pst-circ](https://hvoss.org/PSTexa/pst-circ-doc.pdf) +- [pst-coil](https://hvoss.org/PSTexa/pst-coil-doc.pdf) +- [pst-dart](https://hvoss.org/PSTexa/pst-dart-doc.pdf) +- [pst-eucl](https://hvoss.org/PSTexa/pst-eucl-doc.pdf) +- [pst-func](https://hvoss.org/PSTexa/pst-func-doc.pdf) +- [pst-knot](https://hvoss.org/PSTexa/pst-knot-doc.pdf) +- [pst-node](https://hvoss.org/PSTexa/pst-node-doc.pdf) +- [pst-ovl](https://hvoss.org/PSTexa/pst-ovl-doc.pdf) +- [pst-platon](https://hvoss.org/PSTexa/pst-platon-doc.pdf) +- [pst-plot](https://hvoss.org/PSTexa/pst-plot-doc.pdf) +- [pst-poly](https://hvoss.org/PSTexa/pst-poly-doc.pdf) +- [pst-solarsystem](https://hvoss.org/PSTexa/pst-solarsystem-doc.pdf) +- [pst-soroban](https://hvoss.org/PSTexa/pst-soroban-doc.pdf) +- [pst-spinner](https://hvoss.org/PSTexa/pst-spinner-doc.pdf) +- [pst-stru](https://hvoss.org/PSTexa/pst-stru-doc.pdf) +- [pst-tools](https://hvoss.org/PSTexa/pst-tools-doc.pdf) +- [pst-tree](https://hvoss.org/PSTexa/pst-tree-doc.pdf) +- [pst-turtle](https://hvoss.org/PSTexa/pst-turtle-doc.pdf) +- [pst-vehicle](https://hvoss.org/PSTexa/pst-vehicle-doc.pdf) +- [pst-venn](https://hvoss.org/PSTexa/pst-venn-doc.pdf) diff --git a/Master/texmf-dist/tex/latex/luapstricks/luapstricks.lua b/Master/texmf-dist/tex/latex/luapstricks/luapstricks.lua new file mode 100644 index 00000000000..eebca868012 --- /dev/null +++ b/Master/texmf-dist/tex/latex/luapstricks/luapstricks.lua @@ -0,0 +1,3946 @@ +---- luapstricks.lua +-- Copyright 2021 Marcel Krüger <tex@2krueger.de> +-- +-- 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 file luapstricks.lua. + +if luatexbase then + luatexbase.provides_module { + name = 'luapstricks', + version = 'v0.1', + date = '2021-09-16', + description = 'PSTricks backend for LuaLaTeX', + } +end + +local pdfprint = vf.pdf -- Set later to have the right mode +local function gobble() end + +local pi = math.pi +local two_pi = 2*pi +local pi2_inv = 2/pi +local pi3_inv = 3/pi + +local sin_table = {0, 1, 0, -1} + +local l = lpeg + +local whitespace = (l.S'\0\t\n\r\f ' + '%' * (1-l.P'\n')^0 * (l.P'\n' + -1))^1 + +local regular = 1 - l.S'\0\t\n\r\f %()<>[]{}/' + +local exitmarker = {} +local lookup + +-- local integer = l.S'+-'^-1 * l.R'09'^1 / tonumber +local real = l.S'+-'^-1 * (l.R'09'^1 * ('.' * l.R'09'^0)^-1 + '.' * l.R'09'^1) * (l.S'Ee' * l.S'+-'^-1 * l.R'09'^1)^-1 / tonumber +local radix_scanner = setmetatable({}, {__index = function(t, b) + local digit + if b < 10 then + digit = l.R('0' .. string.char(string.byte'0' + b - 1)) + else + digit = l.R'09' + if b > 10 then + digit = digit + l.R('A' .. string.char(string.byte'A' + b - 11)) + digit = digit + l.R('a' .. string.char(string.byte'a' + b - 11)) + end + end + digit = l.C(digit^1) * l.Cp() + t[b] = digit + return digit +end}) +local radix = l.Cmt(l.R'09' * l.R'09'^-1 / tonumber * '#', function(subj, pos, radix) + if radix < 2 or radix > 36 then return end + local digits, pos = radix_scanner[radix]:match(subj, pos) + if not digits then return end + digits = tonumber(digits, radix) + return pos, digits +end) +local number = radix + real -- + integer -- Every integer is also a real + +local str_view do + local meta = { + __index = function(s, k) + if k == 'value' then + return string.sub(s.base.value, s.offset, s.last) + end + end, + __newindex = function(s, k, v) + if k == 'value' then + s.base.value = string.sub(s.base.value, 1, s.offset-1) .. v .. string.sub(s.base.value, s.last+1) + return + end + -- We could do rawset here, but there is no reason for setting keys anyway + assert(false) + end, + } + function str_view(base, offset, length) + if getmetatable(base) == meta then + offset = offset + base.offset - 1 + base = base.base + end + return setmetatable({ + kind = 'string', + base = base, + offset = offset, + last = offset + length - 1, + }, meta) + end +end + +local string_patt do + local literal = '(' * l.Cs(l.P{( + l.Cg('\\' * ( + 'n' * l.Cc'\n' + + 'r' * l.Cc'\r' + + 't' * l.Cc'\t' + + 'b' * l.Cc'\b' + + 'f' * l.Cc'\f' + + '\\' * l.Cc'\\' + + '(' * l.Cc'(' + + ')' * l.Cc')' + + l.R'07' * l.R'07'^-2 / function(s) return string.char(tonumber(s, 8) % 0x100) end + + ('\r' * l.P'\n'^-1 + '\n')^-1 * l.Cc'' + )) + + l.Cg('\r' * l.P'\n'^-1 * l.Cc'\n') + + (1-l.S'()') + + '(' * l.V(1) * ')' + )^0}) * ')' + local hexchar = l.R('09', 'af', 'AF') + local hexbyte = hexchar * hexchar^-1 / function(s) + local b = tonumber(s, 16) + return #s == 1 and 16*b or b + end + local hex = '<' * (hexbyte^0 / string.char) * '>' + string_patt = literal + hex -- TODO: Base85 is not implemented +end + +local name = l.C(regular^1 + l.S'[]' + '<<' + '>>') +local literal_name = '/' * l.C(regular^0) +local imm_name = '//' * l.C(regular^0) + +-- All objects are literal by default, except names represented as direct strings and operators +local any_object = l.P{whitespace^-1 * ( + number * -regular + + l.Ct(l.Cg(string_patt, 'value') * l.Cg(l.Cc'string', 'kind')) + + imm_name / function(name) return lookup(name) end + + l.Ct(l.Cg(literal_name, 'value') * l.Cg(l.Cc'name', 'kind')) + + name + + l.Ct(l.Cg(l.Ct(l.Cg('{' * l.Ct(l.V(1)^0) * whitespace^-1 * '}', 'value') * l.Cg(l.Cc'array', 'kind')), 'value') * l.Cg(l.Cc'executable', 'kind')) +)} +local object_list = l.Ct(any_object^0) * whitespace^-1 * (-1 + l.Cp()) + +local function parse_ps(s) + local tokens, fail_offset = object_list:match(s) + if fail_offset then + error(string.format('Failed to parse PS tokens at `%s\'', s:sub(fail_offset))) + end + return tokens +end + +local serialize_pdf do + function serialize_pdf(obj) + local t = type(obj) + if t == 'number' then + return string.format(math.type(obj) == 'float' and '%.5f' or '%i', obj) + elseif t == 'boolean' then + return obj and 'true' or 'false' + elseif t == 'string' then + return '/' .. obj + elseif t == 'table' then + t = obj.kind + if t == 'name' then + return '/' .. obj.value + elseif t == 'string' then + return '(' .. obj.value .. ')' -- TODO: Escaping + elseif t == 'dict' then + local helper = {} + for k, v in next, obj.value do + helper[#helper+1] = serialize_pdf(k) + helper[#helper+1] = serialize_pdf(v) + end + return '<<' .. table.concat(helper, ' ') .. '>>' + elseif t == 'array' then + local helper = {} + for i, v in ipairs(obj.value) do + helper[i] = serialize_pdf(v) + end + return '[' .. table.concat(helper, ' ') .. ']' + else + error'Unable to serialize object' + end + end + error'Unable to serialize object' + end +end + +local srand, rrand, rand do + local state + function srand(s) + state = s//1 + if state < 1 then + state = -(state % 0x7ffffffe) + 1 + elseif state > 0x7ffffffe then + state = 0x7ffffffe + end + end + function rrand() + return state + end + function rand() + state = (16807 * state) % 0x7fffffff + -- if state <= 0 then + -- state = state + 0x7fffffff + -- end + return state + end + srand(math.random(1, 0x7ffffffe)) +end + +local maybe_decompress do + local compressed_pattern = '%!PS\n\z + currentfile<</Predictor 1' * l.R'05' * '/Columns ' * (l.R'09'^1/tonumber) * '>>/FlateDecode filter cvx exec\n' + * l.C(l.P(1)^1) + + local stacklimit = 999000 + + function maybe_decompress(data) + local columns, compressed = compressed_pattern:match(data) + if not columns then return data end + + data = zlib.decompress(compressed) + local bytes do + local size = #data + if size < stacklimit then + bytes = {data:byte(1, -1)} + else + bytes = {} + local off = 1 + for i = 1, size, stacklimit do + table.move({data:byte(i, i+stacklimit-1)}, 1, stacklimit, i, bytes) + end + end + end + local new_data = {} + local start_row = 1 + local out_row = 1 + while true do + local control = bytes[start_row] + if not control then break end + if control == 0 or (control == 2 and start_row == 1) then + table.move(bytes, start_row + 1, start_row + columns, out_row, new_data) + elseif control == 1 then + local last = bytes[start_row + 1] + new_data[out_row] = last + for i = 2, columns do + last = (bytes[start_row + i] + last) & 0xFF + new_data[out_row + i - 1] = last + end + elseif control == 2 then + for i = 1, columns do + new_data[out_row + i - 1] = (bytes[start_row + i] + new_data[out_row - columns - 1 + i]) & 0xFF + end + else + error'Unimplemented' + end + start_row = start_row + columns + 1 + out_row = out_row + columns + end + local result = '' + local size = #new_data + for i = 1, size, stacklimit do + result = result .. string.char(table.unpack(new_data, i, i + stacklimit > size and size or i + stacklimit - 1)) + end + return result + end +end + +local font_aliases = { + -- First add some help to find the TeX Gyre names under the corresponding URW font names + ['NimbusRoman-Regular'] = 'kpse:texgyretermes-regular.otf', + ['NimbusRoman-Italic'] = 'kpse:texgyretermes-italic.otf', + ['NimbusRoman-Bold'] = 'kpse:texgyretermes-bold.otf', + ['NimbusRoman-BoldItalic'] = 'kpse:texgyretermes-bolditalic.otf', + + ['NimbusSans-Regular'] = 'kpse:texgyreheros-regular.otf', + ['NimbusSans-Italic'] = 'kpse:texgyreheros-italic.otf', + ['NimbusSans-Bold'] = 'kpse:texgyreheros-bold.otf', + ['NimbusSans-BoldItalic'] = 'kpse:texgyreheros-bolditalic.otf', + + ['NimbusSansNarrow-Regular'] = 'kpse:texgyreheroscn-regular.otf', + ['NimbusSansNarrow-Oblique'] = 'kpse:texgyreheroscn-italic.otf', + ['NimbusSansNarrow-Bold'] = 'kpse:texgyreheroscn-bold.otf', + ['NimbusSansNarrow-BoldOblique'] = 'kpse:texgyreheroscn-bolditalic.otf', + + ['NimbusMonoPS-Regular'] = 'kpse:texgyrecursor-regular.otf', + ['NimbusMonoPS-Italic'] = 'kpse:texgyrecursor-italic.otf', + ['NimbusMonoPS-Bold'] = 'kpse:texgyrecursor-bold.otf', + ['NimbusMonoPS-BoldItalic'] = 'kpse:texgyrecursor-bolditalic.otf', + + ['URWBookman-Light'] = 'kpse:texgyrebonum-regular.otf', + ['URWBookman-LightItalic'] = 'kpse:texgyrebonum-italic.otf', + ['URWBookman-Demi'] = 'kpse:texgyrebonum-bold.otf', + ['URWBookman-DemiItalic'] = 'kpse:texgyrebonum-bolditalic.otf', + + ['URWGothic-Book'] = 'kpse:texgyreadventor-regular.otf', + ['URWGothic-BookOblique'] = 'kpse:texgyreadventor-italic.otf', + ['URWGothic-Demi'] = 'kpse:texgyreadventor-bold.otf', + ['URWGothic-DemiOblique'] = 'kpse:texgyreadventor-bolditalic.otf', + + -- These fonts have weird names in their URW variant, so we use the standard font names directly instead. + ['NewCenturySchlbk-Roman'] = 'kpse:texgyreschola-regular.otf', + ['NewCenturySchlbk-Italic'] = 'kpse:texgyreschola-italic.otf', + ['NewCenturySchlbk-Bold'] = 'kpse:texgyreschola-bold.otf', + ['NewCenturySchlbk-BoldItalic'] = 'kpse:texgyreschola-bolditalic.otf', + + ['Palatino-Roman'] = 'kpse:texgyrepagella-regular.otf', + ['Palatino-Italic'] = 'kpse:texgyrepagella-italic.otf', + ['Palatino-Bold'] = 'kpse:texgyrepagella-bold.otf', + ['Palatino-BoldItalic'] = 'kpse:texgyrepagella-bolditalic.otf', + + ['ZapfChancery-MediumItalic'] = 'kpse:texgyrechorus-mediumitalic.otf', + + -- The two symbol fonts don't have OpenType equivalents in TeX Live + -- so we use TFM based fonts instead + ['StandardSymbolsPS'] = 'usyr', + ['Dingbats'] = 'uzdr', +} +-- Then map the standard 35 font names to the URW names as done by GhostScript +-- (Except for New Century Schoolbook which got mapped directly before. +for psname, remapped in next, { + ['Times-Roman'] = 'NimbusRoman-Regular', + ['Times-Italic'] = 'NimbusRoman-Italic', + ['Times-Bold'] = 'NimbusRoman-Bold', + ['Times-BoldItalic'] = 'NimbusRoman-BoldItalic', + + ['Helvetica'] = 'NimbusSans-Regular', + ['Helvetica-Oblique'] = 'NimbusSans-Italic', + ['Helvetica-Bold'] = 'NimbusSans-Bold', + ['Helvetica-BoldOblique'] = 'NimbusSans-BoldItalic', + + ['Helvetica-Narrow'] = 'NimbusSansNarrow-Regular', + ['Helvetica-Narrow-Oblique'] = 'NimbusSansNarrow-Oblique', + ['Helvetica-Narrow-Bold'] = 'NimbusSansNarrow-Bold', + ['Helvetica-Narrow-BoldOblique'] = 'NimbusSansNarrow-BoldOblique', + + ['Courier'] = 'NimbusMonoPS-Regular', + ['Courier-Oblique'] = 'NimbusMonoPS-Italic', + ['Courier-Bold'] = 'NimbusMonoPS-Bold', + ['Courier-BoldOblique'] = 'NimbusMonoPS-BoldItalic', + + ['Bookman-Light'] = 'URWBookman-Light', + ['Bookman-LightItalic'] = 'URWBookman-LightItalic', + ['Bookman-Demi'] = 'URWBookman-Demi', + ['Bookman-DemiItalic'] = 'URWBookman-DemiItalic', + + ['AvantGarde-Book'] = 'URWGothic-Book', + ['AvantGarde-BookOblique'] = 'URWGothic-BookOblique', + ['AvantGarde-Demi'] = 'URWGothic-Demi', + ['AvantGarde-DemiOblique'] = 'URWGothic-DemiOblique', + + ['Symbol'] = 'StandardSymbolsPS', + ['StandardSymL'] = 'StandardSymbolsPS', + + ['ZapfDingbats'] = 'Dingbats', + + -- Some additional names needed for PSTricks + ['NimbusRomNo9L-Regu'] = 'NimbusRoman-Regular', + ['NimbusRomNo9L-ReguItal'] = 'NimbusRoman-Italic', + ['NimbusRomNo9L-Medi'] = 'NimbusRoman-Bold', + ['NimbusRomNo9L-MediItal'] = 'NimbusRoman-BoldItalic', + ['NimbusRomNo9L-Bold'] = 'NimbusRoman-Bold', + + ['NimbusSanL-Regu'] = 'NimbusSans-Regular', + ['NimbusSanL-ReguItal'] = 'NimbusSans-Italic', + ['NimbusSanL-Bold'] = 'NimbusSans-Bold', + ['NimbusSanL-BoldItal'] = 'NimbusSans-BoldItalic', + + ['NimbusSanL-BoldCond'] = 'NimbusSansNarrow-Bold', + ['NimbusSanL-BoldCondItal'] = 'NimbusSansNarrow-BoldOblique', + ['NimbusSanL-ReguCond'] = 'NimbusSansNarrow-Regular', + ['NimbusSanL-ReguCondItal'] = 'NimbusSansNarrow-Oblique', + + ['NimbusMonL-Regu'] = 'NimbusMonoPS-Regular', + ['NimbusMonL-ReguObli'] = 'NimbusMonoPS-Italic', + ['NimbusMonL-Bold'] = 'NimbusMonoPS-Bold', + ['NimbusMonL-BoldObli'] = 'NimbusMonoPS-BoldItalic', + + ['URWBookmanL-DemiBoldItal'] = 'URWBookman-DemiItalic', + ['URWBookmanL-DemiBold'] = 'URWBookman-Demi', + ['URWBookmanL-LighItal'] = 'URWBookman-LightItalic', + ['URWBookmanL-Ligh'] = 'URWBookman-Light', + + ['URWGothicL-BookObli'] = 'URWGothic-BookOblique', + ['URWGothicL-Book'] = 'URWGothic-Book', + ['URWGothicL-DemiObli'] = 'URWGothic-DemiOblique', + ['URWGothicL-Demi'] = 'URWGothic-Demi', + + ['CenturySchL-Roma'] = 'NewCenturySchlbk-Roman', + ['CenturySchL-Ital'] = 'NewCenturySchlbk-Italic', + ['CenturySchL-Bold'] = 'NewCenturySchlbk-Bold', + ['CenturySchL-BoldItal'] = 'NewCenturySchlbk-BoldItalic', + + ['URWPalladioL-Roma'] = 'Palatino-Roman', + ['URWPalladioL-Ital'] = 'Palatino-Italic', + ['URWPalladioL-Bold'] = 'Palatino-Bold', + ['URWPalladioL-BoldItal'] = 'Palatino-BoldItalic', + + ['URWChanceryL-MediItal'] = 'ZapfChancery-MediumItalic', +} do + font_aliases[psname] = font_aliases[remapped] or remapped +end + +local operand_stack = {} + +local pushs do + local function helper(height, args, arg, ...) + if args == 0 then return end + height = height + 1 + operand_stack[height] = arg + return helper(height, args - 1, ...) + end + function pushs(...) + return helper(#operand_stack, select('#', ...), ...) + end +end +local function push(value) + operand_stack[#operand_stack+1] = value +end + +local function ps_error(kind, ...) + pushs(...) + return error{pserror = kind, trace = debug.traceback()} +end + +local function pop(...) + local height = #operand_stack + if height == 0 then + return ps_error('stackunderflow', ...) + end + local v = operand_stack[height] + operand_stack[height] = nil + return v, v +end +local function pop_num(...) + local raw = pop(...) + local n = raw + local tn = type(n) + if tn == 'table' and n.kind == 'executable' then + n = n.value + tn = type(n) + end + if tn ~= 'number' then + ps_error('typecheck', raw, ...) + end + return n, raw +end +local pop_int = pop_num +local function pop_proc(...) + local v = pop() + if type(v) ~= 'table' or v.kind ~= 'executable' or type(v.value) ~= 'table' or v.value.kind ~= 'array' then + ps_error('typecheck', v, ...) + end + return v.value.value, v +end +local pop_bool = pop +local function pop_dict() + local orig = pop() + local dict = orig + if type(dict) ~= 'table' then + ps_error('typecheck', orig) + end + if dict.kind == 'executable' then + dict = dict.value + if type(dict) ~= 'table' then + ps_error('typecheck', orig) + end + end + if dict.kind ~= 'dict' then + ps_error('typecheck', orig) + end + return dict.value, orig, dict +end +local function pop_array() + local orig = pop() + local arr = orig + if type(arr) == 'table' and arr.kind == 'executable' then + arr = arr.value + end + if type(arr) ~= 'table' or arr.kind ~= 'array' then + ps_error('typecheck', orig) + end + return arr +end +local pop_string = pop +local function pop_key() + local key = pop() + if type(key) == 'table' then + local kind = key.kind + if kind == 'executable' then + key = key.value + if type(key) ~= 'table' then return key end + kind = key.kind + end + if kind == 'string' or kind == 'name' or kind == 'operator' then + key = key.value + end + end + return key +end + +local execute_ps, execute_tok + +local dictionary_stack +local graphics_stack = {{ + matrix = {10, 0, 0, 10, 0, 0}, -- Chosen for consistency with GhostScript's pdfwrite. Must be the same as defaultmatrix + 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 + 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 + +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 globaldict = {kind = 'dict', value = {}} +local userdict = {kind = 'dict', value = { + SDict = {kind = 'dict', value = { + normalscale = {kind = 'executable', value = {kind = 'array', value = {}}}, + }}, + ['@beginspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}}, + ['@setspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}}, + ['@endspecial'] = {kind = 'executable', value = {kind = 'array', value = {}}}, +}} +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 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'Font support is not implemented' + return true + 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 + 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] + w = w + (char and char.width or 0) + end + if pdfprint ~= gobble then + vf.pop() + end + w = w/65781.76 + 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'Unmatched mark' 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'Unmatched mark' + end, + cleartomark = function() + local entry + repeat + entry = pop() + until (not entry) or type(entry) == 'table' and entry.kind == 'mark' + if not entry then error'Unmatched mark' 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, + + 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, subbox.box) + vf.node(parent) + node.direct.setnext(n, after) + node.direct.setlist(parent, head) + vf.pop() + end + lua_node_lookup[subbox] = op + push(op) + push(x) + push(y) + push(width) + push(0) + push(id) + execute_ps(proc) + if width ~= 0 then + x = x + width + end + n = after + end + end) + if not success and err ~= exitmarker then + error(err, 0) + end + end, + pathbbox = function() + local current_path = assert(graphics_stack[#graphics_stack].current_path, 'nocurrentpoint') + local i=1 + local llx, lly, urx, ury + while current_path[i] do + local entry = current_path[i] + if type(entry) == 'number' then + local after = current_path[i+1] + assert(type(after) == 'number') + llx = llx and llx < entry and llx or entry + lly = lly and lly < after and lly or after + urx = urx and urx > entry and urx or entry + ury = ury and ury > after and ury or after + i = i+2 + else + i = i+1 + end + end + push(llx) + push(lly) + push(urx) + push(ury) + end, + + ['not'] = function() + local val, orig = pop() + local tval = type(val) + if tval == 'table' and val.kind == 'executable' then + val = val.value + local tval = type(val) + end + if tval == 'boolean' then + push(not val) + elseif tval == 'number' then + push(~val) + else + ps_error('typecheck', orig) + end + end, + ['and'] = function() + local val, orig = pop() + local tval = type(val) + if tval == 'table' and val.kind == 'executable' then + val = val.value + local tval = type(val) + end + if tval == 'boolean' then + push(pop_bool() and val) + elseif tval == 'number' then + push(val & pop_int()) + else + ps_error('typecheck', orig) + end + end, + ['or'] = function() + local val, orig = pop() + local tval = type(val) + if tval == 'table' and val.kind == 'executable' then + val = val.value + local tval = type(val) + end + if tval == 'boolean' then + push(pop_bool() or val) + elseif tval == 'number' then + push(val | pop_int()) + else + ps_error('typecheck', orig) + end + end, + ['xor'] = function() + local val, orig = pop() + local tval = type(val) + if tval == 'table' and val.kind == 'executable' then + val = val.value + local tval = type(val) + end + if tval == 'boolean' then + push(val ~= pop_bool()) + elseif tval == 'number' then + push(val ~ pop_int()) + else + ps_error('typecheck', orig) + end + end, + bitshift = function() + local shift, arg2 = pop_num() + local val = pop_num(arg2) + push(val << shift) + end, + + eq = function() + local b = pop() + local a = pop(b) + if type(a) == 'table' and (a.kind == 'executable' or a.kind == 'name' or a.kind == 'operator') then + a = a.value + end + if type(a) == 'table' and a.kind == 'string' then + a = a.value + end + if type(b) == 'table' and (b.kind == 'executable' or b.kind == 'name' or b.kind == 'operator') then + b = b.value + end + if type(b) == 'table' and b.kind == 'string' then + b = b.value + end + push(a==b) + end, + ne = function() + local b = pop() + local a = pop(b) + if type(a) == 'table' and (a.kind == 'executable' or a.kind == 'name' or a.kind == 'operator') then + a = a.value + end + if type(a) == 'table' and a.kind == 'string' then + a = a.value + end + if type(b) == 'table' and (b.kind == 'executable' or b.kind == 'name' or b.kind == 'operator') then + b = b.value + end + if type(b) == 'table' and b.kind == 'string' then + b = b.value + end + push(a~=b) + end, + gt = function() + local b, arg2 = pop() + local a, arg1 = pop(arg2) + local ta, tb = type(a), type(b) + if ta == 'table' and a.kind == 'executable' then + a = a.value ta = type(a) + end + if tb == 'table' and b.kind == 'executable' then + b = b.value tb = type(b) + end + if ta == 'number' then + if tb ~= 'number' then + ps_error('typecheck', arg1, arg2) + end + elseif ta == 'table' and ta.kind == 'string' then + if tb ~= 'table' or tb.kind ~= 'string' then + ps_error('typecheck', arg1, arg2) + end + a, b = a.value, b.value + else + ps_error('typecheck', arg1, arg2) + end + push(a>b) + end, + ge = function() + local b, arg2 = pop() + local a, arg1 = pop(arg2) + local ta, tb = type(a), type(b) + if ta == 'table' and a.kind == 'executable' then + a = a.value ta = type(a) + end + if tb == 'table' and b.kind == 'executable' then + b = b.value tb = type(b) + end + if ta == 'number' then + if tb ~= 'number' then + ps_error('typecheck', arg1, arg2) + end + elseif ta == 'table' and ta.kind == 'string' then + if tb ~= 'table' or tb.kind ~= 'string' then + ps_error('typecheck', arg1, arg2) + end + a, b = a.value, b.value + else + ps_error('typecheck', arg1, arg2) + end + push(a>=b) + end, + le = function() + local b, arg2 = pop() + local a, arg1 = pop(arg2) + local ta, tb = type(a), type(b) + if ta == 'table' and a.kind == 'executable' then + a = a.value ta = type(a) + end + if tb == 'table' and b.kind == 'executable' then + b = b.value tb = type(b) + end + if ta == 'number' then + if tb ~= 'number' then + ps_error('typecheck', arg1, arg2) + end + elseif ta == 'table' and ta.kind == 'string' then + if tb ~= 'table' or tb.kind ~= 'string' then + ps_error('typecheck', arg1, arg2) + end + a, b = a.value, b.value + else + ps_error('typecheck', arg1, arg2) + end + push(a<=b) + end, + lt = function() + local b, arg2 = pop() + local a, arg1 = pop(arg2) + local ta, tb = type(a), type(b) + if ta == 'table' and a.kind == 'executable' then + a = a.value ta = type(a) + end + if tb == 'table' and b.kind == 'executable' then + b = b.value tb = type(b) + end + if ta == 'number' then + if tb ~= 'number' then + ps_error('typecheck', arg1, arg2) + end + elseif ta == 'table' and a.kind == 'string' then + if tb ~= 'table' or b.kind ~= 'string' then + ps_error('typecheck', arg1, arg2) + end + a, b = a.value, b.value + else + ps_error('typecheck', arg1, arg2) + end + push(a<b) + end, + + add = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a+b) + end, + sub = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a-b) + end, + mul = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a*b) + end, + div = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a/b) + end, + idiv = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a//b) + end, + mod = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a%b) + end, + exp = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + push(a^b) + end, + sqrt = function() + push(math.sqrt(pop_num())) + end, + sin = function() + local x = pop_num() + local i, f = math.modf(x/90) + if f == 0 then + push(sin_table[i % 4 + 1]) + else + push(math.sin(math.rad(x))) + end + end, + cos = function() + local x = pop_num() + local i, f = math.modf(x/90) + if f == 0 then + push(sin_table[(i+1) % 4 + 1]) + else + push(math.cos(math.rad(x))) + end + end, + atan = function() + local b, arg2 = pop_num() + local a = pop_num(arg2) + local res = math.deg(math.atan(a, b)) + if res < 0 then res = res + 360 end + push(res) + end, + arccos = function() + push(math.deg(math.acos(pop_num()))) + end, + arcsin = function() + push(math.deg(math.asin(pop_num()))) + end, + abs = function() + push(math.abs(pop_num())) + end, + neg = function() + push(-pop_num()) + end, + round = function() + return push(math.floor(pop_num()+.5)) + end, + ceiling = function() + return push(math.ceil(pop_num())) + end, + floor = function() + return push(math.floor(pop_num())) + end, + ln = function() + push(math.log((pop_num()))) + end, + log = function() + push(math.log(pop_num(), 10)) + end, + truncate = function() + push((math.modf(pop_num()))) + end, + cvn = function() + local a, raw = pop() + if type(a) == 'table' and a.kind == 'executable' then + local val = a.value + if type(val) ~= 'table' or val.kind ~= 'string' then + ps_error('typecheck', raw) + end + push(val.value) + end + if type(a) ~= 'table' or a.kind ~= 'string' then + ps_error('typecheck', raw) + end + return push{kind = 'name', value = a.value} + end, + cvi = function() + local a, raw = pop() + if type(a) == 'table' and a.kind == 'executable' then + a = a.value + end + if type(a) == 'table' and a.kind == 'string' then + a = (number * -1):match(a.value) + if not a then + ps_error('syntaxerror', raw) + end + end + if type(a) ~= 'number' then ps_error('typecheck', raw) end + push(a//1) + end, + cvr = function() + local a, raw = pop() + if type(a) == 'table' and a.kind == 'executable' then + a = a.value + end + if type(a) == 'table' and a.kind == 'string' then + a = (number * -1):match(a.value) + if not a then + ps_error('syntaxerror', raw) + end + end + if type(a) ~= 'number' then ps_error('typecheck', raw) end + push(a*1.) + end, + cvs = function() + local old_str, arg2 = pop_string() + local a, arg1 = pop() + a = ps_to_string(a) + if #old_str.value < #a then ps_error('rangecheck', arg1, arg2) end + old_str.value = a .. string.sub(old_str.value, #a+1, -1) + return push{kind = 'string', value = a} + end, + cvrs = function() + local old_str, arg3 = pop_string() + local radix, arg2 = pop_num() + local num, arg1 = pop_num() + if radix == 10 then + num = string.format(math.type(num) == 'float' and '%.6g' or '%i', num) + else + num = num//1 + if num < 0 and num >= -0x80000000 then + num = num + 0x100000000 + end + if num < 0 then + ps_error('rangecheck', arg1, arg2, arg3) + end + num = num == 0 and '0' or num_to_base(num, radix) + end + if #old_str.value < #num then ps_error('rangecheck', arg1, arg2, arg3) end + old_str.value = num .. string.sub(old_str.value, #num+1, -1) + return push{kind = 'string', value = num} + end, + + string = function() + push{kind = 'string', value = string.rep('\0', (pop_int()))} + end, + search = function() + local seek = pop_string() + local str = pop_string() + local start, stop = string.find(str.value, seek.value, 1, true) + if start then + push(str_view(str, stop + 1, #str.value - stop)) + push(str_view(str, start, stop - start + 1)) + push(str_view(str, 1, start - 1)) + push(true) + else + push(str) + push(false) + end + end, + + array = function() + local size = pop_int() + local arr = lua.newtable(size, 0) + for i=1, size do arr[i] = null end + push{kind = 'array', value = arr} + end, + astore = function() + local arr = pop_array() + local size = #arr.value + for i=size, 1, -1 do + arr.value[i] = pop() + end + push(arr) + end, + aload = function() + local arr = pop_array() + table.move(arr.value, 1, #arr.value, #operand_stack + 1, operand_stack) + push(arr) + end, + getinterval = function() + local count, arg3 = pop_int() + local index, arg2 = pop_int() + local arr, arg1 = pop() + if type(arr) ~= 'table' then ps_error('typecheck', arg1, arg2, arg3) end + if arr.kind == 'executable' then + arr = arr.value + if type(arr) ~= 'table' then ps_error('typecheck', arg1, arg2, arg3) end + end + if arr.kind == 'string' then + push(str_view(arr, index + 1, count)) + elseif arr.kind == 'array' then + -- TODO: At least for the array case, we could use metamethods to make get element sharing behavior + push{kind = 'array', value = table.move(arr.value, index + 1, index + count, 1, {})} + else + ps_error('typecheck', arg1, arg2, arg3) + end + end, + putinterval = function() + local from, arg2 = pop() + local index, arg1 = pop_int() + if type(from) ~= 'table' then ps_error('typecheck', arg1, arg2) end + if from.kind == 'executable' then + from = from.value + if type(from) ~= 'table' then ps_error('typecheck', arg1, arg2) end + end + if from.kind == 'string' then + local to = pop_string() + from = from.value + to.value = string.sub(to.value, 1, index) .. from .. string.sub(to.value, index + 1 + #from) + elseif from.kind == 'array' then + local to = pop_array() + table.move(from.value, 1, #from.value, index + 1, to.value) + else + ps_error('typecheck', arg1, arg2) + end + end, + + dict = function() + local size = pop_int() + push{kind = 'dict', value = lua.newtable(0, size)} + end, + begin = function() + local _ + _, _, dictionary_stack[#dictionary_stack + 1] = pop_dict() + end, + ['end'] = function() + if #dictionary_stack <= 3 then + ps_error'dictstackunderflow' + end + dictionary_stack[#dictionary_stack] = nil + end, + currentdict = function() + push(dictionary_stack[#dictionary_stack]) + end, + bind = function() + local d = pop() + push(d) + if type(d) ~= 'table' then ps_error'typecheck' end + if d.kind == 'executable' then + d = d.value + if type(d) ~= 'table' then ps_error'typecheck' end + end + if d.kind ~= 'array' then ps_error'typecheck' end + bind(d.value) + end, + def = function() + local value = pop() + local key = pop_key() + dictionary_stack[#dictionary_stack].value[key] = value + end, + store = function() + local value = pop() + local key = pop_key() + for i=#dictionary_stack, 1, -1 do + if dictionary_stack[i].value[key] ~= nil then + dictionary_stack[i].value[key] = value + return + end + end + dictionary_stack[#dictionary_stack].value[key] = value + end, + known = function() + local key = pop_key() + local dict = pop() + push(dict.value[key] ~= nil) + end, + where = function() + local key = pop_key() + for i = #dictionary_stack, 1, -1 do + local dict = dictionary_stack[i] + local value = dict.value[key] + if value ~= nil then + push(dict) + return push(true) + end + end + return push(false) + end, + load = function() + push(lookup(pop_key())) + end, + get = function() + local key = pop() + local obj = pop() + if type(obj) ~= 'table' then ps_error'typecheck' end + if obj.kind == 'executable' then + obj = obj.value + if type(obj) ~= 'table' then ps_error'typecheck' end + end + local val = obj.value + if obj.kind == 'string' then + push(key) key = pop_int() + if key < 0 or key >= #val then ps_error'rangecheck' end + push(string.byte(val, key+1)) + elseif obj.kind == 'array' then + push(key) key = pop_int() + if key < 0 or key >= #val then ps_error'rangecheck' end + push(val[key+1]) + elseif obj.kind == 'dict' then + push(key) key = pop_key() + push(val[key]) + else + ps_error'typecheck' + end + end, + put = function() + local value = pop() + local key = pop() + local obj = pop() + if type(obj) ~= 'table' then ps_error'typecheck' end + if obj.kind == 'executable' then + obj = obj.value + if type(obj) ~= 'table' then ps_error'typecheck' end + end + local val = obj.value + if obj.kind == 'string' then + push(key) key = pop_int() + if key < 0 or key >= #val then ps_error'rangecheck' end + push(value) value = pop_int() + obj.value = string.sub(val, 1, key) .. string.char(value) .. string.sub(val, key+2, #val) + elseif obj.kind == 'array' then + push(key) key = pop_int() + if key < 0 or key >= #val then ps_error'rangecheck' end + val[key+1] = value + elseif obj.kind == 'dict' then + push(key) key = pop_key() + val[key] = value + else + ps_error'typecheck' + end + end, + undef = function() + local key = pop_key() + local dict = pop_dict() + dict[key] = nil + end, + length = function() + local obj = pop() + if type(obj) == 'string' then + return push(#obj) + elseif type(obj) ~= 'table' then + ps_error'typecheck' + end + if obj.kind == 'executable' then + obj = obj.value + if type(obj) ~= 'table' then ps_error'typecheck' end + end + local val = obj.value + if obj.kind == 'string' then + push(#val) + elseif obj.kind == 'name' then + push(#val) + elseif obj.kind == 'array' then + push(#val) + elseif obj.kind == 'dict' then + local length = 0 + for _ in next, val do + length = length + 1 + end + push(length) + else + ps_error'typecheck' + end + end, + + matrix = function() + push{kind = 'array', value = {1, 0, 0, 1, 0, 0}} + end, + defaultmatrix = function() + local m = pop_array() + local mm = m.value + assert(#mm == 6) + mm[1], mm[2], mm[3], mm[4], mm[5], mm[6] = 10, 0, 0, 10, 0, 0 + push(m) + end, + currentmatrix = function() + local m = pop_array() + assert(#m.value == 6) + table.move(graphics_stack[#graphics_stack].matrix, 1, 6, 1, m.value) + push(m) + end, + currentlinewidth = function() + push(assert(graphics_stack[#graphics_stack].linewidth, 'linewidth has to be set before it is queried')) + end, + currentmiterlimit = function() + push(assert(graphics_stack[#graphics_stack].miterlimit, 'miterlimit has to be set before it is queried')) + end, + currentflat = function() + push(graphics_stack[#graphics_stack].flatness) + end, + setlinewidth = function() + local lw = pop_num() + graphics_stack[#graphics_stack].linewidth = lw + delayed_print(string.format('%.3f w', lw)) + end, + setlinejoin = function() + local linejoin = pop_int() + graphics_stack[#graphics_stack].linejoin = linejoin + delayed_print(string.format('%i j', linejoin)) + end, + setlinecap = function() + local linecap = pop_int() + graphics_stack[#graphics_stack].linecap = linecap + delayed_print(string.format('%i J', linecap)) + end, + setmiterlimit = function() + local ml = pop_int() + graphics_stack[#graphics_stack].miterlimit = ml + delayed_print(string.format('%.3f M', ml)) + end, + setstrokeadjust = function() + local sa = pop_bool() + graphics_stack[#graphics_stack].strokeadjust = sa + delayed_print(ExtGState[sa and '<</SA true>>' or '<</SA false>>']) + end, + setdash = function() + local offset = pop_num() + local patt = pop_array().value + graphics_stack[#graphics_stack].dash = {offset = offset, pattern = patt} + local mypatt = {} + for i=1, #patt do + mypatt[i] = string.format('%.3f', patt[i]) + end + delayed_print(string.format('[%s] %.3f d', table.concat(mypatt, ' '), offset)) + end, + setflat = function() + local flatness = pop_num() + graphics_stack[#graphics_stack].flatness = flatness + delayed_print(string.format('%.3f i', flatness)) + end, + currentpoint = function() + local current_point = assert(graphics_stack[#graphics_stack].current_point, 'nocurrentpoint') + push(current_point[1]) + push(current_point[2]) + end, + + moveto = function() + local y = pop_num() + local x = pop_num() + local state = graphics_stack[#graphics_stack] + local current_path = state.current_path + if current_path then + local i = #current_path + 1 + current_path[i], current_path[i+1], current_path[i+2] = x, y, 'm' + 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 + 1 + current_path[i], current_path[i+1], current_path[i+2] = x, y, 'm' + 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 + 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*' + for i = 1, #current_path do + if type(current_path[i]) == 'number' then + current_path[i] = string.format('%.5f', current_path[i]) + end + end + flush_delayed() + 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' + for i = 1, #current_path do + if type(current_path[i]) == 'number' then + current_path[i] = string.format('%.5f', current_path[i]) + end + end + flush_delayed() + 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' + for i = 1, #current_path do + if type(current_path[i]) == 'number' then + current_path[i] = string.format('%.5f', current_path[i]) + end + end + flush_delayed() + 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+ ', ' '))) + 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+ ', ' '))) + else + error'Unsupported rectfill variant' + end + end, + + shfill = function() + local shading_dict, arg1 = pop_dict() + flush_delayed() + local data_src + local pdf_dict = '' + for k, v in next, shading_dict do + if k == 'DataSource' then + data_src = v + else + pdf_dict = pdf_dict .. serialize_pdf(k) .. ' ' .. serialize_pdf(v) + end + end + if shading_dict.ShadingType == 4 then + assert(data_src) + if type(data_src) ~= 'table' then + push(arg1) + ps_error'typecheck' + end + if data_src.kind == 'string' then + data_src = data_src.value + elseif data_src.kind == 'array' then + data_src = data_src.value + local color_model = shading_dict.ColorSpace.value[1] + if type(color_model) == 'table' and color_model.kind == 'name' then + color_model = color_model.value + end + if color_model == 'DeviceRGB' then + color_model = 3 + elseif color_model == 'DeviceCMYK' then + color_model = 4 + elseif color_model == 'DeviceGray' then + color_model = 1 + else + error'Unsupported color model in Shading dictionary' + end + local components = color_model + 3 + pdf_dict = pdf_dict .. '/BitsPerCoordinate 24/BitsPerComponent 8/BitsPerFlag 8/Decode[-8192 8191 -8192 8191' .. string.rep(' 0 1', color_model) .. ']' + local data = '' + for i = 1, #data_src-components+1, components do + data = data .. string.pack('>BI3I3', data_src[i], (data_src[i+1]*1024+.5)//1 + 8388608, (data_src[i+2]*1024+.5)//1 + 8388608) + for j = i + 3, i + 2 + color_model do + data = data .. string.pack('B', (data_src[j]*255+.5)//1) + end + end + data_src = data + else + error'Unsupported DataSource variant' + end + local obj = write_shading(pdf_dict, data_src) + pdfprint(string.format('%s sh', write_shading(pdf_dict, data_src))) + end + end, + + scale = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + local mv = m.value + if #mv ~= 6 then error'Unexpected size of matrix' end + local y = pop_num() + local x = pop_num() + mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = x, 0, 0, y, 0, 0 + push(m) + else + push(m) + local y = pop_num() + local x = pop_num() + update_matrix(x, 0, 0, y, 0, 0) + end + end, + translate = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + local mv = m.value + if #mv ~= 6 then error'Unexpected size of matrix' end + local y = pop_num() + local x = pop_num() + mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = 1, 0, 0, 1, x, y + push(m) + else + push(m) + local y = pop_num() + local x = pop_num() + update_matrix(1, 0, 0, 1, x, y) + end + end, + rotate = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + local mv = m.value + if #mv ~= 6 then error'Unexpected size of matrix' end + local angle = math.rad(pop_num()) + local s, c = math.sin(angle), math.cos(angle) + mv[1], mv[2], mv[3], mv[4], mv[5], mv[6] = c, s, -s, c, 0, 0 + push(m) + else + push(m) + local angle = math.rad(pop_num()) + local s, c = math.sin(angle), math.cos(angle) + update_matrix(c, s, -s, c, 0, 0) + end + end, + transform = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + m = m.value + if #m ~= 6 then error'Unexpected size of matrix' end + else + push(m) + m = graphics_stack[#graphics_stack].matrix + end + local y = pop_num() + local x = pop_num() + x, y = matrix_transform(x, y, m[1], m[2], m[3], m[4], m[5], m[6]) + push(x) + push(y) + end, + itransform = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + m = m.value + if #m ~= 6 then error'Unexpected size of matrix' end + else + push(m) + m = graphics_stack[#graphics_stack].matrix + end + local y = pop_num() + local x = pop_num() + x, y = matrix_transform(x, y, matrix_invert(m[1], m[2], m[3], m[4], m[5], m[6])) + push(x) + push(y) + end, + dtransform = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + m = m.value + if #m ~= 6 then error'Unexpected size of matrix' end + else + push(m) + m = graphics_stack[#graphics_stack].matrix + end + local y = pop_num() + local x = pop_num() + x, y = matrix_transform(x, y, m[1], m[2], m[3], m[4], 0, 0) + push(x) + push(y) + end, + idtransform = function() + local m = pop() + if type(m) == 'table' and m.kind == 'array' then + m = m.value + if #m ~= 6 then error'Unexpected size of matrix' end + else + push(m) + m = graphics_stack[#graphics_stack].matrix + end + local y = pop_num() + local x = pop_num() + x, y = matrix_transform(x, y, matrix_invert(m[1], m[2], m[3], m[4], 0, 0)) + push(x) + push(y) + end, + concatmatrix = function() + local m3a = pop_array() + local m3 = m3a.value + if #m3 ~= 6 then error'Unexpected size of matrix' end + local m2 = pop_array().value + if #m2 ~= 6 then error'Unexpected size of matrix' end + local m1 = pop_array().value + if #m1 ~= 6 then error'Unexpected size of matrix' end + m3[1], m3[2], + m3[3], m3[4], + m3[5], m3[6] + = m1[1] * m2[1] + m1[2] * m2[3], m1[1] * m2[2] + m1[2] * m2[4], + m1[3] * m2[1] + m1[4] * m2[3], m1[3] * m2[2] + m1[4] * m2[4], + m1[5] * m2[1] + m1[6] * m2[3] + m2[5], m1[5] * m2[2] + m1[6] * m2[4] + m2[6] + push(m3a) + end, + invertmatrix = function() + local target = pop_array() + local T = target.value + assert(#T == 6) + local M = pop_array().value + assert(#M == 6) + T[1], T[2], T[3], T[4], T[5], T[6] + = matrix_invert(M[1], M[2], M[3], M[4], M[5], M[6]) + push(target) + end, + concat = function() + local m = pop_array().value + if #m ~= 6 then error'Unexpected size of matrix' end + update_matrix(m[1], m[2], m[3], m[4], m[5], m[6]) + end, + -- setmatrix is not supported in PDF, so we invert the old matrix first + setmatrix = function() + local m = pop() + if type(m) ~= 'table' or m.kind ~= 'array' then + ps_error'typecheck' + end + local m = m.value + if #m ~= 6 then ps_error'rangecheck' end + local old = graphics_stack[#graphics_stack].matrix + local pt = graphics_stack[#graphics_stack].current_point + local a, b, c, d, e, f = matrix_invert(old[1], old[2], old[3], old[4], old[5], old[6]) + update_matrix(a, b, c, d, e, f) + update_matrix(m[1], m[2], m[3], m[4], m[5], m[6]) + end, + setpdfcolor = function() + local pdf = pop_string().value + local color = graphics_stack[#graphics_stack].color + delayed_print(pdf) + color.space = {kind = 'array', value = {{kind = 'name', value = 'PDF'}}} + for i=2, #color do color[i] = nil end + color[1] = pdf + end, + setgray = function() + local g = pop_num() + local color = graphics_stack[#graphics_stack].color + color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceGray'}}} + for i=2, #color do color[i] = nil end + color[1] = g + delayed_print(string.format('%.3f g %.3f G', g, g)) + end, + setrgbcolor = function() + local b = pop_num() + local g = pop_num() + local r = pop_num() + local color = graphics_stack[#graphics_stack].color + color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceRGB'}}} + for i=4, #color do color[i] = nil end + color[1], color[2], color[3] = r, g, b + delayed_print(string.format('%.3f %.3f %.3f rg %.3f %.3f %.3f RG', r, g, b, r, g, b)) + end, + -- Conversion based on Wikipedia article about HSB colorspace + sethsbcolor = function() + local b = pop_num() + local s = pop_num() + local h = pop_num() + if b < 0 then b = 0 elseif b > 1 then b = 1 end + if s < 0 then s = 0 elseif s > 1 then s = 1 end + if h < 0 then h = 0 elseif h > 1 then h = 1 end + local hi, hf = math.modf(6 * h) + local p, q, t = b * (1 - s), b * (1 - s*hf), b * (1 - s * (1-hf)) + if hi == 0 or hi == 6 then + push(b) push(t) push(p) + elseif hi == 1 then + push(q) push(b) push(p) + elseif hi == 2 then + push(p) push(b) push(t) + elseif hi == 3 then + push(p) push(q) push(b) + elseif hi == 4 then + push(t) push(p) push(b) + elseif hi == 5 then + push(b) push(p) push(q) + end + return systemdict.value.setrgbcolor() + end, + setcmykcolor = function() + local k = pop_num() + local y = pop_num() + local m = pop_num() + local c = pop_num() + local color = graphics_stack[#graphics_stack].color + color.space = {kind = 'array', value = {{kind = 'name', value = 'DeviceCMYK'}}} + for i=5, #color do color[i] = nil end + color[1], color[2], color[3], color[4] = c, m, y, k + delayed_print(string.format('%.3f %.3f %.3f %.3f k %.3f %.3f %.3f %.3f K', c, m, y, k, c, m, y, k)) + end, + ['.setopacityalpha'] = function() + error'Unsupported, use .setfillconstantalpha instead' + end, + ['.setfillconstantalpha'] = function() + local alpha = pop_num() + graphics_stack[#graphics_stack].fillconstantalpha = alpha + delayed_print(ExtGState['<</ca ' .. alpha .. '>>']) + end, + ['.setstrokeconstantalpha'] = function() + local alpha = pop_num() + graphics_stack[#graphics_stack].strokeconstantalpha = alpha + delayed_print(ExtGState['<</CA ' .. alpha .. '>>']) + end, + ['.currentalphaisshape'] = function() + local ais = graphics_stack[#graphics_stack].alphaisshape + if ais == nil then error'alphaisshape has to be set before it is queried' end + push(ais) + end, + ['.setalphaisshape'] = function() + local ais = pop_bool() + graphics_stack[#graphics_stack].alphaisshape = ais + delayed_print(ExtGState['<</AIS ' .. (ais and 'true' or 'false') .. '>>']) + end, + ['.currentblendmode'] = function() + local blendmode = graphics_stack[#graphics_stack].blendmode + if blendmode == nil then error'blendmode has to be set before it is queried' end + push{kind = 'name', value = blendmode} + end, + ['.setblendmode'] = function() + local blendmode = pop() + if type(blendmode) == 'string' then + elseif type(blendmode) == 'table' and blendmode.kind == 'name' then + blendmode = blendmode.value + else + push(blendmode) + ps_error'typecheck' + end + graphics_stack[#graphics_stack].blendmode = blendmode + delayed_print(ExtGState['<</BM /' .. blendmode .. '>>']) + end, + newpath = function() + local state = graphics_stack[#graphics_stack] + state.current_point = nil + state.current_path = nil + end, + + currentcolorspace = function() + local color = graphics_stack[#graphics_stack].color + if not color then error'Color has to be set before it is queried' end + push(color.space) + end, + currentcolor = function() + local color = graphics_stack[#graphics_stack].color + if not color then error'Color has to be set before it is queried' end + for i = 1, #color do + push(color[i]) + end + end, + currentcmykcolor = function() + local c, m, y, k + local color = graphics_stack[#graphics_stack].color + if not color then error'Color has to be set before it is queried' end + local space = color.space.value[1] + if type(space) == 'table' and space.kind == 'name' then space = space.value end + if space == 'DeviceRGB' then + c, m, y = 1 - color[1], 1 - color[2], 1 - color[3] + -- k = math.min(c, m, y) + -- TODO: Undercolor removal/black generation + -- local undercolor = undercolorremoval(k) + -- local undercolor = 0 + -- k = blackgeneration(k) + k = 0 + -- c, m, y = c - undercolor, y - undercolor, k - undercolor + elseif space == 'DeviceGray' then + c, m, y, k = 0, 0, 0, 1 - color[1] + elseif space == 'DeviceCMYK' then + c, m, y, k = color[1], color[2], color[3], color[4] + elseif space == 'PDF' then + c, m, y, k = 0, 0, 0, 1 + print('???', 'tocmyk', color[1]) + else + r, g, b, k = 0, 0, 0, 1 + end + push(r) + push(g) + push(b) + end, + currentgraycolor = function() + local g + local color = graphics_stack[#graphics_stack].color + if not color then error'Color has to be set before it is queried' end + local space = color.space.value[1] + if type(space) == 'table' and space.kind == 'name' then space = space.value end + if space == 'DeviceRGB' then + g = 0.3 * color[1] + 0.59 * color[2], 0.11 * color[3] + elseif space == 'DeviceGray' then + g = color[1] + elseif space == 'DeviceCMYK' then + g = math.min(1, math.max(0, 0.3 * color[1] + 0.59 * color[2] + 0.11 * color[3] + color[4])) + elseif space == 'PDF' then + g = 1 + print('???', 'togray', color[1]) + else + g = 1 + end + push(g) + end, + currentrgbcolor = function() + local r, g, b + local color = graphics_stack[#graphics_stack].color + if not color then error'Color has to be set before it is queried' end + local space = color.space.value[1] + if type(space) == 'table' and space.kind == 'name' then space = space.value end + if space == 'DeviceRGB' then + r, g, b = color[1], color[2], color[3] + elseif space == 'DeviceGray' then + r = color[1] + g, b = r, r + elseif space == 'DeviceCMYK' then + local c, m, y, k = color[1], color[2], color[3], color[4] + c, m, y = c+k, m+k, y+k + r, g, b = c >= 1 and 0 or 1-c, m >= 1 and 0 or 1-m, y >= 1 and 0 or 1-y + elseif space == 'PDF' then + r, g, b = 0, 0, 0 + print('???', 'torgb', color[1]) + else + r, g, b = 0, 0, 0 + end + push(r) + push(g) + push(b) + end, + currenthsbcolor = function() + systemdict.value.currentrgbcolor() + local b = pop_num() + local g = pop_num() + local r = pop_num() + local M, m = math.max(r, g, b), math.min(r, g, b) + local H + if M == m then + H = 0 + elseif M == r then + H = (g-b)/(M-m) / 6 + if H < 0 then H = H + 1 end + elseif M == g then + H = (b-r)/(M-m) / 6 + 1/3 + elseif assert(M == b) then + H = (r-g)/(M-m) / 6 + 2/3 + end + local S = M == 0 and 0 or (M-m)/M + local B = M + push(H) + push(S) + push(B) + end, + currentfont = function() + local f = graphics_stack[#graphics_stack].font + if f then + push(f) + else + push{kind = 'dict', value = { + FID = font.current(), + FontMatrix = {kind = 'array', value = {1, 0, 0, 1, 0, 0}}, + FontName = {kind = 'name', value = tex.fontname(font.current())}, + FontType = 0x1CA, + }} + end + end, + + gsave = function() + graphics_stack[#graphics_stack+1] = table.copy(graphics_stack[#graphics_stack]) + graphics_stack[#graphics_stack].saved_delayed = delayed + delayed = { + text = {}, + matrix = {1, 0, 0, 1, 0, 0}, + } + end, + grestore = function() + local saved_delayed = graphics_stack[#graphics_stack].saved_delayed + if saved_delayed then + delayed = saved_delayed + else + pdfprint'Q' + reset_delayed(delayed) + 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, + ['='] = 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', ps_to_string(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'Font support is not implemented' + return + 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 + else + texio.write_nl'definefont is not implemnted. Pushing dummy font.' + 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, + + 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, + + revision = 1000, + ['true'] = true, + ['false'] = false, + systemdict = systemdict, + 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'}, + }} +}} +systemdict.value.systemdict = systemdict +dictionary_stack = {systemdict, globaldict, userdict} +-- 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() vf.push() vf.node(box.box) vf.pop() 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 + +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 + +local func = luatexbase.new_luafunction'luaPSTheader' +token.set_lua('luaPSTheader', func, 'protected') +lua.get_functions_table()[func] = function() + local stack_depth = #operand_stack + local filename = token.scan_argument() + local f = io.open(kpse.find_file(filename, 'PostScript header'), 'r') + local src = f:read'a' + f:close() + execute_string(src, filename) + if #operand_stack ~= stack_depth then + error'Unexpected values on operand stack' + end +end + +local func = luatexbase.new_luafunction'showPS' +token.set_lua('showPS', func, 'protected') +lua.get_functions_table()[func] = function() + local command = token.scan_argument(true) + local tokens = parse_ps(command) + execute_ps(tokens) + for i = 1, #operand_stack do + local op = operand_stack[i] + operand_stack[i] = nil + if type(op) == 'table' then + print(op.kind, op.value) + else + print(op) + end + 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) + vf.node(node.direct.todirect(n)) + node.free(n) + end} + } + }, + [1] = { + commands = { + {'lua', function() + local tokens, direct = assert(ps_tokens), ps_direct + ps_tokens = nil + local x, y = pdf.getpos() + local height = #operand_stack + operand_stack[height + 1], operand_stack[height + 2] = ps_pos_x/65781.76, ps_pos_y/65781.76 + ps_pos_x, ps_pos_y = nil + if direct then + systemdict.value.moveto() + else + systemdict.value.gsave() + systemdict.value.translate() + end + local success, err = pcall(execute_string, tokens, ps_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() + local new_height = #operand_stack + assert(new_height >= height) + for k = height + 1, new_height do + operand_stack[k] = nil + end + end + end} + } + }, + }, +} + +local modes = tex.getmodevalues() +local func = luatexbase.new_luafunction'luaPST' +token.set_lua('luaPST', func, 'protected') +lua.get_functions_table()[func] = function() + local context = string.format('%s:%i', status.filename, status.linenumber) + local direct = token.scan_keyword'direct' + local tokens = token.scan_argument(true) + local n = node.new('whatsit', 'late_lua') + function n.data() + assert(not ps_tokens) + ps_tokens = tokens + ps_direct = direct + ps_context = context + end + local nn = node.new('glyph') + nn.subtype = 256 + nn.font, nn.char = fid, 0x1F3A8 + n.next = nn + local modename = modes[math.abs(tex.nest.top.mode)] + if 'horizontal' ~= modename then + n = node.hpack(n) -- Glyphs can only appear in hmode + if 'math' == modename then + local d = node.new'disc' + d.penalty = 10000 + d.replace = n + n = d + end + end + if tex.nest.ptr == 0 then + -- Main vertical list. Here we might appear before the page starts properly + -- and should not freeze page specifications. Since we don't have any outer dimensions, + -- we can ensure this by sneaking our node into the current page list whithout going though + -- build_page. + tex.triggerbuildpage() -- First ensure that everything else is contributed properly. + tex.lists.page_head = node.insert_after(tex.lists.page_head, nil, n) + else + node.write(n) + end +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 pscolor = ps_rgb + ps_gray + ps_gray + 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 diff --git a/Master/tlpkg/bin/tlpkg-ctan-check b/Master/tlpkg/bin/tlpkg-ctan-check index 260e84699e3..9e032f4285e 100755 --- a/Master/tlpkg/bin/tlpkg-ctan-check +++ b/Master/tlpkg/bin/tlpkg-ctan-check @@ -491,7 +491,8 @@ my @TLP_working = qw( luaimageembed luaindex luainputenc luaintro luakeys lualatex-doc lualatex-doc-de lualatex-math lualatex-truncate lualibs - luamesh luamplib luaotfload luapackageloader luaprogtable luarandom + luamesh luamplib luaotfload luapackageloader luaprogtable luapstricks + luarandom luasseq luatex85 luatexbase luatexja luatexko luatextra luatodonotes luavlna luaxml lwarp lxfonts ly1 lyluatex diff --git a/Master/tlpkg/libexec/ctan2tds b/Master/tlpkg/libexec/ctan2tds index 03d15e33975..8a4ccdc017e 100755 --- a/Master/tlpkg/libexec/ctan2tds +++ b/Master/tlpkg/libexec/ctan2tds @@ -2080,6 +2080,7 @@ $standardtex 'luamesh', '\.sty', # not lltxdoc.cls 'luapackageloader', '\.lua|' . $standardtex, 'luaprogtable','\.lua|' . $standardtex, + 'luapstricks', '\.lua|' . $standardtex, 'luatexko', '\.lua|' . $standardtex, 'luatodonotes','\.lua|' . $standardtex, 'luavlna', 'luavlna.*\.lua|luavlna\.tex|' . $standardtex, diff --git a/Master/tlpkg/tlpsrc/collection-pstricks.tlpsrc b/Master/tlpkg/tlpsrc/collection-pstricks.tlpsrc index 0f2b6ab2bba..ee95939bc37 100644 --- a/Master/tlpkg/tlpsrc/collection-pstricks.tlpsrc +++ b/Master/tlpkg/tlpsrc/collection-pstricks.tlpsrc @@ -8,6 +8,7 @@ depend collection-plaingeneric depend auto-pst-pdf depend bclogo depend dsptricks +depend luapstricks depend makeplot depend pdftricks depend pdftricks2 diff --git a/Master/tlpkg/tlpsrc/luapstricks.tlpsrc b/Master/tlpkg/tlpsrc/luapstricks.tlpsrc new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/Master/tlpkg/tlpsrc/luapstricks.tlpsrc |