diff --git a/luametalatex-back-pdf.lua b/luametalatex-back-pdf.lua index d80264e..74afa30 100644 --- a/luametalatex-back-pdf.lua +++ b/luametalatex-back-pdf.lua @@ -8,6 +8,7 @@ local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() r local usedglyphs = {} local dests = {} local cur_page +local declare_whatsit = require'luametalatex-whatsits'.new local whatsit_id = node.id'whatsit' local whatsits = node.whatsits() local colorstacks = {{ @@ -264,9 +265,16 @@ local function linkcontext_set(linkcontext, p, x, y, list, level, kind) end end end -function do_start_link(prop, p, n, x, y, outer, _, level) +local start_link_whatsit = declare_whatsit('pdf_start_link', function(prop, p, n, x, y, outer, _, level) + if not prop then + tex.error('Invalid pdf_start_link whatsit', {"A pdf_start_link whatsit did not contain all necessary \z + parameters. Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + return + end if not p.is_page then - error[[No link allowed here]] + tex.error('pdf_start_link outside of page', {"PDF links are not allowed in Type3 charstrings or Form XObjects. \z + The link will be ignored"}) + return end local links = p.linkcontext if not links then @@ -276,29 +284,45 @@ function do_start_link(prop, p, n, x, y, outer, _, level) local link = {quads = {}, attr = prop.link_attr, action = prop.action, level = level, force_separate = false} -- force_separate should become an option links[#links+1] = link addlinkpoint(p, link, x, y, outer, 'start') -end -function do_end_link(prop, p, n, x, y, outer, _, level) +end) +local end_link_whatsit = declare_whatsit('pdf_end_link', function(prop, p, n, x, y, outer, _, level) if not p.is_page then - error[[No link allowed here]] + tex.error('pdf_start_link outside of page', {"PDF links are not allowed in Type3 charstrings or Form XObjects. \z + The link will be ignored"}) + return end local links = p.linkcontext - if not links then error"No link here to end" end + if not links then + tex.error('No link here to end', {"You asked me to end a link, but currently there is no link active. \z + Maybe you forgot to run \\pdfextension startlink first?"}) + return + end local link = links[#links] + if link.level ~= level then + tex.error('Inconsistent link level', {"You asked me to end a link, but the most recent link had been started at another level. \z + I will continue with the link for now."}) + return + end links[#links] = nil if not links[1] then p.linkcontext = nil end - if link.level ~= level then error"Wrong link level" end addlinkpoint(p, link, x, y, outer, 'final') -end +end) -local do_setmatrix do +local setmatrix_whatsit do local numberpattern = (lpeg.P'-'^-1 * lpeg.R'09'^0 * ('.' * lpeg.R'09'^0)^-1)/tonumber local matrixpattern = numberpattern * ' ' * numberpattern * ' ' * numberpattern * ' ' * numberpattern - function do_setmatrix(prop, p, n, x, y, outer) + setmatrix_whatsit = declare_whatsit('pdf_setmatrix', function(prop, p, n, x, y, outer) + if not prop then + tex.error('Invalid pdf_setmatrix whatsit', {"A pdf_setmatrix whatsit did not contain a matrix value. \z + Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + return + end local m = p.matrix local a, b, c, d = matrixpattern:match(prop.data) if not a then - print(prop.data) - error[[No valid matrix found]] + tex.error('Invalid matrix', {"The matrix in this pdf_setmatrix whatsit does not have the expected structure and could not be parsed. \z + Did you provide enough parameters? The matrix needs exactly four decimal entries."}) + return end local e, f = (1-a)*x-c*y, (1-d)*y-b*x -- Emulate that the origin is at x, y for this transformation -- (We could also first translate by (-x, -y), then apply the matrix @@ -308,19 +332,23 @@ local do_setmatrix do c, d = projected(m, c, d, 0) e, f = projected(m, e, f, 1) m[1], m[2], m[3], m[4], m[5], m[6] = a, b, c, d, e, f - end + end) end -local function do_save(prop, p, n, x, y, outer) +local save_whatsit = declare_whatsit('pdf_save', function(prop, p, n, x, y, outer) pdf.write('page', 'q', x, y, p) local lastmatrix = p.matrix p.matrix = {[0] = lastmatrix, table.unpack(lastmatrix)} -end -local function do_restore(prop, p, n, x, y, outer) +end) +local restore_whatsit = declare_whatsit('pdf_restore', function(prop, p, n, x, y, outer) -- TODO: Check x, y pdf.write('page', 'Q', x, y, p) p.matrix = p.matrix[0] -end -local function do_dest(prop, p, n, x, y) +end) +local dest_whatsit = declare_whatsit('pdf_dest', function(prop, p, n, x, y) + if not prop then + tex.error('Invalid pdf_dest whatsit', {"A pdf_dest whatsit did not contain all necessary \z + parameters. Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + end assert(cur_page, "Destinations can not appear outside of a page") local id = prop.dest_id local dest_type = prop.dest_type @@ -365,14 +393,29 @@ local function do_dest(prop, p, n, x, y) else dests[id] = pfile:indirect(dests[id], data) end -end -local function do_refobj(prop, p, n, x, y) +end) +local refobj_whatsit = declare_whatsit('pdf_refobj', function(prop, p, n, x, y) + if not prop then + tex.error('Invalid pdf_refobj whatsit', {"A pdf_refobj whatsit did not reference any object. \z + Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + return + end pfile:reference(prop.obj) -end -local function do_literal(prop, p, n, x, y) +end) +local literal_whatsit = declare_whatsit('pdf_literal', function(prop, p, n, x, y) + if not prop then + tex.error('Invalid pdf_literal whatsit', {"A pdf_literal whatsit did not contain a literal to be inserted. \z + Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + return + end pdf.write(prop.mode, prop.data, x, y, p) -end -local function do_colorstack(prop, p, n, x, y) +end) +local colorstack_whatsit = declare_whatsit('pdf_colorstack', function(prop, p, n, x, y) + if not prop then + tex.error('Invalid pdf_colorstack whatsit', {"A pdf_colorstack whatsit did not contain all necessary \z + parameters. Maybe your code hasn't been adapted to LuaMetaLaTeX yet?"}) + return + end local colorstack = prop.colorstack local stack if p.is_page then @@ -393,28 +436,33 @@ local function do_colorstack(prop, p, n, x, y) stack[#stack] = prop.data end pdf.write(colorstack.mode, stack[#stack], x, y, p) -end +end) local function write_colorstack() local idx = token.scan_int() local colorstack = colorstacks[idx + 1] if not colorstack then - error[[Undefined colorstack]] + tex.error('Undefined colorstack', {"The requested colorstack is not initialized. \z + This probably means that you forgot to run \\pdffeedback colorstackinit or \z + that you specified the wrong index. I will continue with colorstack 0."}) + colorstack = colorstacks[1] end local action = token.scan_keyword'pop' and 'pop' or token.scan_keyword'set' and 'set' or token.scan_keyword'current' and 'current' or token.scan_keyword'push' and 'push' if not action then - error[[Missing action specifier for colorstack command]] + tex.error('Missing action specifier for colorstack', { + "I don't know what you want to do with this colorstack. I would have expected pop/set/current or push here. \z + I will ignore this command."}) + return end local text if action == "push" or "set" then text = token.scan_string() -- text = token.to_string(token.scan_tokenlist()) -- Attention! This should never be executed in an expand-only context end - local whatsit = node.new(whatsit_id, whatsits.pdf_colorstack) + local whatsit = node.new(whatsit_id, colorstack_whatsit) node.setproperty(whatsit, { - handle = do_colorstack, colorstack = colorstack, action = action, data = text, @@ -498,52 +546,40 @@ token.luacmd("pdfextension", function(_, imm) elseif token.scan_keyword"literal" then local mode = scan_literal_mode() local literal = token.scan_string() - local whatsit = node.new(whatsit_id, whatsits.pdf_literal) + local whatsit = node.new(whatsit_id, literal_whatsit) node.setproperty(whatsit, { - handle = do_literal, mode = mode, data = literal, }) node.write(whatsit) elseif token.scan_keyword"startlink" then local pfile = get_pfile() - local whatsit = node.new(whatsit_id, whatsits.pdf_start_link) + local whatsit = node.new(whatsit_id, start_link_whatsit) local attr = token.scan_keyword'attr' and token.scan_string() or '' local action = scan_action() local objnum = pfile:getobj() lastannot = num node.setproperty(whatsit, { - handle = do_start_link, link_attr = attr, action = action, objnum = objnum, }) node.write(whatsit) elseif token.scan_keyword"endlink" then - local whatsit = node.new(whatsit_id, whatsits.pdf_end_link) - node.setproperty(whatsit, { - handle = do_end_link, - }) + local whatsit = node.new(whatsit_id, end_link_whatsit) node.write(whatsit) elseif token.scan_keyword"save" then - local whatsit = node.new(whatsit_id, whatsits.pdf_save) - node.setproperty(whatsit, { - handle = do_save, - }) + local whatsit = node.new(whatsit_id, save_whatsit) node.write(whatsit) elseif token.scan_keyword"setmatrix" then local matrix = token.scan_string() - local whatsit = node.new(whatsit_id, whatsits.pdf_setmatrix) + local whatsit = node.new(whatsit_id, setmatrix_whatsit) node.setproperty(whatsit, { - handle = do_setmatrix, data = matrix, }) node.write(whatsit) elseif token.scan_keyword"restore" then - local whatsit = node.new(whatsit_id, whatsits.pdf_restore) - node.setproperty(whatsit, { - handle = do_restore, - }) + local whatsit = node.new(whatsit_id, restore_whatsit) node.write(whatsit) elseif token.scan_keyword"info" then infodir = infodir .. token.scan_string() @@ -577,10 +613,9 @@ token.luacmd("pdfextension", function(_, imm) end elseif token.scan_keyword"refobj" then local num = token.scan_int() - local whatsit = node.new(whatsit_id, whatsits.pdf_refobj) + local whatsit = node.new(whatsit_id, refobj_whatsit) node.setproperty(whatsit, { obj = num, - handle = do_refobj, }) node.write(whatsit) elseif token.scan_keyword"outline" then @@ -616,10 +651,9 @@ token.luacmd("pdfextension", function(_, imm) else error[[Unsupported id type]] end - local whatsit = node.new(whatsit_id, whatsits.pdf_dest) + local whatsit = node.new(whatsit_id, dest_whatsit) local prop = { dest_id = id, - handle = do_dest, } node.setproperty(whatsit, prop) if token.scan_keyword'xyz' then diff --git a/luametalatex-firstcode.lua b/luametalatex-firstcode.lua index 33a97d5..15318b8 100644 --- a/luametalatex-firstcode.lua +++ b/luametalatex-firstcode.lua @@ -63,50 +63,8 @@ end -- end -- }) +local new_whatsit = require'luametalatex-whatsits'.new local whatsit_id = node.id'whatsit' -local whatsits = { - [0] = "open", - "write", - "close", - "special", - nil, - nil, - "save_pos", - "late_lua", - "user_defined", - nil, - nil, - nil, - nil, - nil, - nil, - nil, - "pdf_literal", - "pdf_refobj", - "pdf_annot", - "pdf_start_link", - "pdf_end_link", - "pdf_dest", - "pdf_action", - "pdf_thread", - "pdf_start_thread", - "pdf_end_thread", - "pdf_thread_data", - "pdf_link_data", - "pdf_colorstack", - "pdf_setmatrix", - "pdf_save", - "pdf_restore", -} -whatsits[whatsits[0]] = 0 -for i = 0,#whatsits do - local v = whatsits[i] - if v then - whatsits[v] = i - end -end -function node.whatsits() return whatsits end -function node.subtype(s) return type(s) == "string" and whatsits[s] or nil end local spacer_cmd, relax_cmd = token.command_id'spacer', token.command_id'relax' local function scan_filename() local name = {} @@ -141,15 +99,16 @@ local function do_openout(p) end end end +local open_whatsit = new_whatsit('open', do_openout) token.luacmd("openout", function(_, immediate) -- \openout local file = token.scan_int() token.scan_keyword'=' local name = scan_filename() - local props = {file = file, name = name, handle = do_openout} + local props = {file = file, name = name} if immediate == "immediate" then do_openout(props) else - local whatsit = node.direct.new(whatsit_id, whatsits.open) + local whatsit = node.direct.new(whatsit_id, open_whatsit) properties[whatsit] = props node.direct.write(whatsit) end @@ -160,13 +119,14 @@ local function do_closeout(p) ofiles[p.file] = nil end end +local close_whatsit = new_whatsit('close', do_closeout) token.luacmd("closeout", function(_, immediate) -- \closeout local file = token.scan_int() - local props = {file = file, handle = do_closeout} + local props = {file = file} if immediate == "immediate" then do_closeout(props) else - local whatsit = node.direct.new(whatsit_id, whatsits.close) + local whatsit = node.direct.new(whatsit_id, close_whatsit) properties[whatsit] = props node.direct.write(whatsit) end @@ -180,14 +140,15 @@ local function do_write(p) texio.write_nl(p.file < 0 and "log" or "term and log", content) end end +local write_whatsit = new_whatsit('write', do_write) token.luacmd("write", function(_, immediate) -- \write local file = token.scan_int() local content = token.scan_tokenlist() - local props = {file = file, data = content, handle = do_write} + local props = {file = file, data = content} if immediate == "immediate" then do_write(props) else - local whatsit = node.direct.new(whatsit_id, whatsits.write) + local whatsit = node.direct.new(whatsit_id, write_whatsit) properties[whatsit] = props node.direct.write(whatsit) end @@ -199,7 +160,7 @@ token.luacmd("immediate", function() -- \immediate return token.put_next(next_tok) end local function_id = next_tok.mode - functions[function_id](function_id, 'immediate') + return functions[function_id](function_id, 'immediate') end, "protected") -- functions[43] = function() -- \pdfvariable -- local name = token.scan_string() diff --git a/luametalatex-init.lua b/luametalatex-init.lua index 6e140a8..7711f0c 100644 --- a/luametalatex-init.lua +++ b/luametalatex-init.lua @@ -98,7 +98,8 @@ callback_register('handle_error_hook', function() if line == "" then return 3 end local first = line:sub(1,1):upper() if first == 'H' then - texio.write(tex.gethelptext()) + texio.write(tex.gethelptext() or "Sorry, I don't know how to help in this situation.\n\z + Maybe you should try asking a human?") elseif first == 'I' then line = line:sub(2) tex.runtoks(function() diff --git a/luametalatex-nodewriter.lua b/luametalatex-nodewriter.lua index b6ce05d..07fd02d 100644 --- a/luametalatex-nodewriter.lua +++ b/luametalatex-nodewriter.lua @@ -31,6 +31,8 @@ local rangedimensions = direct.rangedimensions local traverse_id = direct.traverse_id local getdata = direct.getdata +local get_whatsit_handler = require'luametalatex-whatsits'.handler + local dir_id = node.id'dir' local function doublekeyed(t, id2name, name2id, index) @@ -470,9 +472,9 @@ function nodehandler.glyph(p, n, x, y, ...) p.pos.x = p.pos.x + math.floor(getwidth(n)*(1+getexpansion(n)/1000000)+.5) end function nodehandler.whatsit(p, n, ...) -- Whatsit? - local prop = properties[n]-- or node.getproperty(n) - if prop and prop.handle then - prop:handle(p, n, ...) + local handler, prop = get_whatsit_handler(n) + if handler then + handler(prop, p, n, ...) else write("Invalid whatsit found (missing handler).") end diff --git a/luametalatex-whatsits.lua b/luametalatex-whatsits.lua new file mode 100644 index 0000000..2bed3de --- /dev/null +++ b/luametalatex-whatsits.lua @@ -0,0 +1,96 @@ +local whatsit_id = node.id'whatsit' +local whatsits = { + [0] = "open", + "write", + "close", + "special", + nil, + nil, + "save_pos", + "late_lua", + "user_defined", + nil, + nil, + nil, + nil, + nil, + nil, + nil, + "pdf_literal", + "pdf_refobj", + "pdf_annot", + "pdf_start_link", + "pdf_end_link", + "pdf_dest", + "pdf_action", + "pdf_thread", + "pdf_start_thread", + "pdf_end_thread", + "pdf_thread_data", + "pdf_link_data", + "pdf_colorstack", + "pdf_setmatrix", + "pdf_save", + "pdf_restore", +} +local whatsithandler = {} +-- for i = 0,#whatsits do -- #whatsits isn't guaranteed to work because of the nil entries +for i = 0,31 do + local v = whatsits[i] + if v then + whatsits[v] = i + end +end +function node.whatsits() return whatsits end +function node.subtype(s) return type(s) == "string" and whatsits[s] or nil end + +local direct = node.direct +local getsubtype = direct.getsubtype +local properties = direct.get_properties_table() +local tonode, todirect = direct.tonode, direct.todirect + +local function get_handler(n, subtype) + local props = properties[n] + return props and props.handle or whatsithandler[subtype or getsubtype(n)], props +end +local function new(name, handler) + assert(type(name) == 'string') + local subtype = whatsits[name] + if subtype then + if whatsithandler[subtype] then + texio.write_nl'WARNING: Overwriting default whatsit handler' + end + else + subtype = #whatsits + 1 + whatsits[subtype] = name + whatsits[name] = subtype + end + whatsithandler[subtype] = handler + return subtype +end + +-- TODO: Some fields might expect different values +local function setwhatsitfield(n, name, value) + local props = properties[n] + if not props then + props = {} + properties[n] = props + end + props[name] = value +end +direct.setwhatsitfield = setwhatsitfield + +local function getwhatsitfield(n, name) + local props = properties[n] + return props and props[name] +end +direct.getwhatsitfield = getwhatsitfield + +-- TODO: Some fields might be nodes and therefore have to be converted +function node.setwhatsitfield(n, ...) return setwhatsitfield(todirect(n), ...) end +function node.getwhatsitfield(n, ...) return getwhatsitfield(todirect(n), ...) end + +return { + handler = get_handler, + new = new, +}