local scan_int = token.scan_integer local scan_token = token.scan_token local scan_keyword = token.scan_keyword local scan_string = token.scan_string local scan_word = token.scan_word local scan_dimen = token.scan_dimen local scan_box = token.scan_box token.scan_list = scan_box -- They are equal if no parameter is present local pdf = pdf local pdfvariable = pdf.variable local callbacks = require'luametalatex-callbacks' local writer = require'luametalatex-nodewriter' local newpdf = require'luametalatex-pdf' local nametree = require'luametalatex-pdf-nametree' local build_fontdir = require'luametalatex-pdf-font' local prepare_node_font = require'luametalatex-pdf-font-node'.prepare local fontmap = require'luametalatex-pdf-font-map' local utils = require'luametalatex-pdf-utils' local strip_floats = utils.strip_floats local to_bp = utils.to_bp local pdfname, pfile local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end}) local nodefont_meta = {} local usedglyphs = setmetatable({}, {__index=function(t, fid) local v if font.fonts[fid].format == 'type3node' then v = setmetatable({generation = 0, next_generation = 0}, nodefont_meta) else v = {} end t[fid] = v return v end}) local dests = {} local cur_page local declare_whatsit = require'luametalatex-whatsits'.new local whatsit_id = node.id'whatsit' local whatsits = node.whatsits() local colorstacks = {{ page = true, mode = "direct", default = "0 g 0 G", page_stack = {"0 g 0 G"}, }} local spacer_cmd = token.command_id'spacer' local function get_pfile() if not pfile then pdfname = tex.jobname .. '.pdf' pfile = newpdf.open(tex.jobname .. '.pdf') end return pfile end local outline local build_outline = require'luametalatex-pdf-outline' local function get_outline() if not outline then outline = build_outline() end return outline end local properties = node.direct.properties local reset_deadcycles do local tokens = { token.primitive_tokens.global, token.primitive_tokens.deadcycles,token.create(0x30), token.primitive_tokens.relax, } function reset_deadcycles() token.put_next(tokens) end end token.luacmd("shipout", function() local pfile = get_pfile() local total_voffset, total_hoffset = tex.voffset + pdfvariable.vorigin, tex.hoffset + pdfvariable.horigin local voff = node.new'kern' voff.kern = total_voffset voff.next = scan_box() voff.next.shift = total_hoffset local list = node.direct.tonode(node.direct.vpack(node.direct.todirect(voff))) local pageheight, pagewidth = tex.pageheight, tex.pagewidth -- In the following, the total_[hv]offset represents a symmetric offset applied on the right/bottom. -- The upper/left one is already included in the box dimensions list.height = pageheight ~= 0 and pageheight or list.height + list.depth + total_voffset list.width = pagewidth ~= 0 and pagewidth or list.width + total_hoffset local page, parent = pfile:newpage() cur_page = page local out, resources, annots = writer(pfile, list, fontdirs, usedglyphs, colorstacks) cur_page = nil local content = pfile:stream(nil, '', out) pfile:indirect(page, string.format([[<>]], parent, content, -math.ceil(to_bp(list.depth)), math.ceil(to_bp(list.width)), math.ceil(to_bp(list.height)), resources(pdfvariable.pageresources .. pdf.pageresources), annots, pdfvariable.pageattr, pdf.pageattributes)) node.flush_list(list) tex.runlocal(reset_deadcycles) end, 'force', 'protected') local infodir = "" local namesdir = "" local catalogdir = "" local catalog_openaction local creationdate = os.date("D:%Y%m%d%H%M%S") do local time0 = os.time() local tz = os.date('%z', time0) if tz:match'^[+-]%d%d%d%d$' then if tz:sub(1) == '0000' then tz = 'Z' else tz = tz:sub(1,3) .. "'" .. tz:sub(4) end else local utc_time = os.date('!*t') utc_time.isdst = nil local time1 = os.time(utc_time) local offset = time1-time0 if offset == 0 then tz = 'Z' else if offset > 0 then tz = '-' else tz = '+' offset = -offset end offset = offset // 60 tz = string.format("%s%02i'%02i", tz, offset//60, offset%60) end end creationdate = creationdate .. tz end local function write_infodir(p) local additional = "" if not string.find(infodir, "/CreationDate", 1, false) then additional = string.format("/CreationDate(%s)", creationdate) end if not string.find(infodir, "/ModDate", 1, false) then additional = string.format("%s/ModDate(%s)", additional, creationdate) end if not string.find(infodir, "/Producer", 1, false) then additional = string.format("%s/Producer(LuaMetaLaTeX)", additional) end if not string.find(infodir, "/Creator", 1, false) then additional = string.format("%s/Creator(TeX)", additional) end if not string.find(infodir, "/PTEX.Fullbanner", 1, false) then additional = string.format("%s/PTEX.Fullbanner(%s)", additional, status.enginestate.banner) end return p:indirect(nil, string.format("<<%s%s>>", infodir, additional)) end local pdf_escape = require'luametalatex-pdf-escape' local pdf_bytestring = pdf_escape.escape_bytes local pdf_text = pdf_escape.escape_text local function nodefont_newindex(t, k, v) t.generation = t.next_generation return rawset(t, k, v) end function callbacks.stop_run() local user_callback = callbacks.stop_run if user_callback then user_callback() end if not pfile then return end do nodefont_meta.__newindex = nodefont_newindex -- Start recording generations local need_new_run = true while need_new_run do need_new_run = nil for fid, glyphs in pairs(usedglyphs) do local next_gen = glyphs.next_generation if next_gen and next_gen == glyphs.generation then glyphs.next_generation = next_gen+1 need_new_run = true local f = font.getfont(fid) or font.fonts[fid] prepare_node_font(f, glyphs, pfile, fontdirs, usedglyphs) -- Might become fid, glyphs end end end end for fid, id in pairs(fontdirs) do local f = font.getfont(fid) or font.fonts[fid] local sorted = {} local used = usedglyphs[fid] used.generation, used.next_generation = nil, nil for k,v in pairs(usedglyphs[fid]) do sorted[#sorted+1] = v end table.sort(sorted, function(a,b) return a[1] < b[1] end) pfile:indirect(id, build_fontdir(pfile, f, sorted)) end pfile.root = pfile:getobj() pfile.version = string.format("%i.%i", pdfvariable.majorversion, pdfvariable.minorversion) local destnames = {} for k,obj in next, dests do if pfile:written(obj) then if type(k) == 'string' then destnames[k] = obj .. ' 0 R' end else texio.write_nl(string.format("Warning: Undefined destination %q", tostring(k))) end end if next(destnames) then namesdir = string.format("/Dests %i 0 R%s", nametree(destnames, pfile), namesdir or '') end if namesdir then catalogdir = string.format("/Names<<%s>>%s", namesdir, catalogdir) end local pages = #pfile.pages if outline then catalogdir = string.format("/Outlines %i 0 R%s", outline:write(pfile), catalogdir) end if catalog_openaction then catalogdir = catalogdir .. '/OpenAction' .. catalog_openaction end pfile:indirect(pfile.root, string.format([[<>]], pfile.version, pfile:writepages(), catalogdir)) pfile.info = write_infodir(pfile) local size = pfile:close() texio.write_nl("term", "(see the transcript file for additional information)") -- TODO: Additional logging, epecially targeting the log file texio.write_nl("term and log", " node memory still in use:") -- texio.write_nl("term and log", string.format(" %d words of node memory still in use:", status.nodestate.use)) local by_type, by_sub = {}, {} for n, id, sub in node.traverse(node.usedlist()) do if id == whatsit_id then by_sub[sub] = (by_sub[sub] or 0) + 1 else by_type[id] = (by_type[id] or 0) + 1 end end local nodestat = {} local types = node.types() for id, c in next, by_type do nodestat[#nodestat + 1] = string.format("%d %s", c, types[id]) end for id, c in next, by_sub do nodestat[#nodestat + 1] = string.format("%d %s", c, whatsits[id]) end texio.write_nl(" " .. table.concat(nodestat, ', ')) texio.write_nl(string.format("Output written on %s (%d pages, %d bytes).", pdfname, pages, size)) texio.write_nl(string.format("Transcript written on %s.\n", status.enginestate.logfilename)) end callbacks.__freeze('stop_run', true) token.luacmd("pdfvariable", function() for _, n in ipairs(pdf.variable_names) do if scan_keyword(n) then return token.put_next(token.create('pdfvariable ' .. n)) end end -- The following error message gobbles the next word as a side effect. -- This is intentional to make error-recovery easier. --[[ error(string.format("Unknown PDF variable %s", scan_word())) ]] -- Delay the error to ensure luatex85.sty compatibility texio.write_nl(string.format("Unknown PDF variable %s", scan_word())) tex.sprint"\\unexpanded{\\undefinedpdfvariable}" end) local lastobj = -1 local lastannot = -1 function pdf.newcolorstack(default, mode, page) local idx = #colorstacks colorstacks[idx + 1] = { page = page, mode = mode or "origin", default = default, page_stack = {default}, } return idx end local function projected(m, x, y, w) w = w or 1 return x*m[1] + y*m[3] + w*m[5], x*m[2] + y*m[4] + w*m[6] end local function get_action_attr(p, action, is_link) local action_type = action.action_type if action_type == 3 then return action.data end local action_attr = is_link and "/Subtype/Link/A<<" or "<<" local file = action.file if file then action_attr = action_attr .. '/F' .. pdf_bytestring(file) local newwindow = action.new_window if newwindow and newwindow > 0 then action_attr = action_attr .. '/NewWindow ' .. (newwindow == 1 and 'true' or 'false') end end if action_type == 2 then error[[FIXME: Threads are currently unsupported]] -- TODO elseif action_type == 0 then local page = assert(action.page, 'Page action must contain a page') local tokens = action.tokens if file then action_attr = string.format("%s/S/GoToR/D[%i %s]>>", action_attr, page-1, tokens) else local page_objnum = pfile:reservepage(page) action_attr = string.format("%s/S/GoTo/D[%i 0 R %s]>>", action_attr, page_objnum, tokens) end elseif action_type == 1 then -- GoTo local id = assert(action.id, 'GoTo action must contain an id') if file then assert(type(id) == "string") action_attr = action_attr .. "/S/GoToR/D" .. pdf_bytestring(id) .. ">>" else local dest = dests[id] if not dest then dest = pfile:getobj() dests[id] = dest end if type(id) == "string" then action_attr = action_attr .. "/S/GoTo/D" .. pdf_bytestring(id) .. ">>" else action_attr = string.format("%s/S/GoTo/D %i 0 R>>", action_attr, dest) end end end return action_attr end local function write_link(p, link) local quads = link.quads local minX, maxX, minY, maxY = math.huge, -math.huge, math.huge, -math.huge local attr = link.attr .. get_action_attr(p, link.action, true) assert(#quads%8==0) local quadStr = {} for i=1,#quads,8 do local x1, y1, x4, y4, x2, y2, x3, y3 = table.unpack(quads, i, i+7) x1, y1, x2, y2, x3, y3, x4, y4 = to_bp(x1), to_bp(y1), to_bp(x2), to_bp(y2), to_bp(x3), to_bp(y3), to_bp(x4), to_bp(y4) quadStr[i//8+1] = string.format("%f %f %f %f %f %f %f %f", x1, y1, x2, y2, x3, y3, x4, y4) minX = math.min(minX, x1, x2, x3, x4) minY = math.min(minY, y1, y2, y3, y4) maxX = math.max(maxX, x1, x2, x3, x4) maxY = math.max(maxY, y1, y2, y3, y4) end local boxes = strip_floats(string.format("/Rect[%f %f %f %f]/QuadPoints[%s]", minX-.2, minY-.2, maxX+.2, maxY+.2, table.concat(quadStr, ' '))) pfile:indirect(link.objnum, string.format("<>", boxes, attr)) for i=1,#quads do quads[i] = nil end link.objnum = nil end local function addlinkpoint(p, link, x, y, list, kind) local quads = link.quads local off = pdfvariable.linkmargin x = kind == 'start' and x-off or x+off if link.annots and link.annots ~= p.annots then -- We started on another page, let's finish that before starting the new page write_link(p, link) link.annots = nil end if not link.annots then link.annots = p.annots -- Just a marker to indicate the page link.objnum = link.objnum or p.file:getobj() p.annots[#p.annots+1] = link.objnum .. " 0 R" end local m = p.matrix local lx, ly = projected(m, x, y-off-(link.depth or node.direct.getdepth(list))) local ux, uy = projected(m, x, y+off+(link.height or node.direct.getheight(list))) local n = #quads quads[n+1], quads[n+2], quads[n+3], quads[n+4] = lx, ly, ux, uy if kind == 'final' or (link.force_separate and (n+4)%8 == 0) then write_link(p, link) link.annots = nil end end local function linkcontext_set(linkcontext, p, x, y, list, level, kind) if not p.is_page then return end for _,l in ipairs(linkcontext) do if l.level == level then addlinkpoint(p, l, x, y, list, level, kind) end end end 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 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 links = {set = linkcontext_set} p.linkcontext = links end 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) local end_link_whatsit = declare_whatsit('pdf_end_link', function(prop, p, n, x, y, outer, _, level) if not p.is_page then 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 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 addlinkpoint(p, link, x, y, outer, 'final') end) 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 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 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 -- and translate back, but this is more direct) pdf.write_matrix(a, b, c, d, e, f, p) a, b = projected(m, a, b, 0) 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 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 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 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 local off = pdfvariable.linkmargin local data if dest_type == "xyz" then local x, y = projected(p.matrix, x, y) local zoom = prop.xyz_zoom if zoom then data = string.format("[%i 0 R/XYZ %.5f %.5f %.3f]", cur_page, to_bp(x-off), to_bp(y+off), prop.zoom/1000) else data = string.format("[%i 0 R/XYZ %.5f %.5f null]", cur_page, to_bp(x-off), to_bp(y+off)) end elseif dest_type == "fitr" then local m = p.matrix local llx, lly = projected(x, x - prop.depth) local lrx, lry = projected(x+prop.width, x - prop.depth) local ulx, uly = projected(x, x + prop.height) local urx, ury = projected(x+prop.width, x + prop.height) local left, lower, right, upper = math.min(llx, lrx, ulx, urx), math.min(lly, lry, uly, ury), math.max(llx, lrx, ulx, urx), math.max(lly, lry, uly, ury) data = string.format("[%i 0 R/FitR %.5f %.5f %.5f %.5f]", cur_page, to_bp(left-off), to_bp(lower-off), to_bp(right+off), to_bp(upper+off)) elseif dest_type == "fit" then data = string.format("[%i 0 R/Fit]", cur_page) elseif dest_type == "fith" then local x, y = projected(p.matrix, x, y) data = string.format("[%i 0 R/FitH %.5f]", cur_page, to_bp(y+off)) elseif dest_type == "fitv" then local x, y = projected(p.matrix, x, y) data = string.format("[%i 0 R/FitV %.5f]", cur_page, to_bp(x-off)) elseif dest_type == "fitb" then data = string.format("[%i 0 R/FitB]", cur_page) elseif dest_type == "fitbh" then local x, y = projected(p.matrix, x, y) data = string.format("[%i 0 R/FitBH %.5f]", cur_page, to_bp(y+off)) elseif dest_type == "fitbv" then local x, y = projected(p.matrix, x, y) data = string.format("[%i 0 R/FitBV %.5f]", cur_page, to_bp(x-off)) end if pfile:written(dests[id]) then texio.write_nl(string.format("Duplicate destination %q", id)) else dests[id] = pfile:indirect(dests[id], strip_floats(data)) end 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 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 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 stack = colorstack.page_stack elseif colorstack.last_form == p.resources then stack = colorstack.form_stack else colorstack.last_form = p.resources stack = {prop.default} colorstack.form_stack = stack end if prop.action == "push" then stack[#stack+1] = prop.data elseif prop.action == "pop" then assert(#stack > 1) stack[#stack] = nil elseif prop.action == "set" then stack[#stack] = prop.data end pdf.write(colorstack.mode, stack[#stack], x, y, p) end) local function write_colorstack() local idx = scan_int() local colorstack = colorstacks[idx + 1] if not colorstack then 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 = scan_keyword'pop' and 'pop' or scan_keyword'set' and 'set' or scan_keyword'current' and 'current' or scan_keyword'push' and 'push' if not action then 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 = scan_string() -- text = token.serialize(token.scan_tokenlist()) -- Attention! This should never be executed in an expand-only context end local whatsit = node.new(whatsit_id, colorstack_whatsit) node.setproperty(whatsit, { colorstack = colorstack, action = action, data = text, }) node.write(whatsit) end local function scan_action() local action_type if scan_keyword'user' then return {action_type = 3, data = scan_string()} elseif scan_keyword'thread' then error[[FIXME: Unsupported]] -- TODO elseif scan_keyword'goto' then action_type = 1 else error[[Unsupported action]] end local action = { action_type = action_type, file = scan_keyword'file' and scan_string(), } if scan_keyword'page' then assert(action_type == 1) action_type = 0 action.action_type = 0 local page = scan_int() if page <= 0 then error[[page must be positive in action specification]] end action.page = page action.tokens = scan_string() elseif scan_keyword'num' then if action.file and action_type == 1 then error[[num style GoTo actions must be internal]] end action.id = scan_int() if action.id <= 0 then error[[id must be positive]] end elseif scan_keyword'name' then action.id = scan_string() else error[[Unsupported id type]] end action.new_window = scan_keyword'newwindow' and 1 or scan_keyword'nonewwindow' and 2 if action.new_window and not action.file then error[[newwindow is only supported for external files]] end return action end local function scan_literal_mode() return scan_keyword"direct" and "direct" or scan_keyword"page" and "page" or scan_keyword"text" and "text" or scan_keyword"direct" and "direct" or scan_keyword"raw" and "raw" or "origin" end local function maybe_gobble_cmd(cmd) local t = scan_token() if t.command ~= cmd then token.put_next(t) end end token.luacmd("pdffeedback", function() if scan_keyword"colorstackinit" then local page = scan_keyword'page' or (scan_keyword'nopage' and false) -- If you want to pass "page" as mode local mode = scan_literal_mode() local default = scan_string() tex.sprint(tostring(pdf.newcolorstack(default, mode, page))) elseif scan_keyword"creationdate" then tex.sprint(creationdate) elseif scan_keyword"lastannot" then tex.sprint(tostring(lastannot)) elseif scan_keyword"lastobj" then tex.sprint(tostring(lastobj)) else -- The following error message gobbles the next word as a side effect. -- This is intentional to make error-recovery easier. error(string.format("Unknown PDF feedback %s", scan_word())) end end) token.luacmd("pdfextension", function(_, immediate) if immediate == "value" then return end if immediate and immediate & 0x7 ~= 0 then immediate = immediate & 0x8 tex.error("Unexpected prefix", "You used \\pdfextension with a prefix that doesn't belong there. I will ignore it for now.") end if scan_keyword"colorstack" then write_colorstack() elseif scan_keyword"literal" then local mode = scan_literal_mode() local literal = scan_string() local whatsit = node.new(whatsit_id, literal_whatsit) node.setproperty(whatsit, { mode = mode, data = literal, }) node.write(whatsit) elseif scan_keyword"startlink" then local pfile = get_pfile() local whatsit = node.new(whatsit_id, start_link_whatsit) local attr = scan_keyword'attr' and scan_string() or '' local action = scan_action() local objnum = pfile:getobj() lastannot = num node.setproperty(whatsit, { link_attr = attr, action = action, objnum = objnum, }) node.write(whatsit) elseif scan_keyword"endlink" then local whatsit = node.new(whatsit_id, end_link_whatsit) node.write(whatsit) elseif scan_keyword"save" then local whatsit = node.new(whatsit_id, save_whatsit) node.write(whatsit) elseif scan_keyword"setmatrix" then local matrix = scan_string() local whatsit = node.new(whatsit_id, setmatrix_whatsit) node.setproperty(whatsit, { data = matrix, }) node.write(whatsit) elseif scan_keyword"restore" then local whatsit = node.new(whatsit_id, restore_whatsit) node.write(whatsit) elseif scan_keyword"info" then infodir = infodir .. scan_string() elseif scan_keyword"catalog" then catalogdir = catalogdir .. ' ' .. scan_string() if scan_keyword'openaction' then if catalog_openaction then tex.error("Duplicate openaction", {"Only one use of \\pdfextension catalog is allowed to \z have an openaction."}) else local action = scan_action() catalog_openaction = get_action_attr(get_pfile(), action) end end elseif scan_keyword"names" then namesdir = namesdir .. ' ' .. scan_string() elseif scan_keyword"obj" then local pfile = get_pfile() if scan_keyword"reserveobjnum" then lastobj = pfile:getobj() else local num = scan_keyword'useobjnum' and scan_int() or pfile:getobj() lastobj = num local attr = scan_keyword'stream' and (scan_keyword'attr' and scan_string() or '') local isfile = scan_keyword'file' local content = scan_string() if immediate == 8 then if attr then pfile:stream(num, attr, content, isfile) else pfile:indirect(num, content, isfile) end else if attr then pfile:delayedstream(num, attr, content, isfile) else pfile:delayed(num, attr, content, isfile) end end end elseif scan_keyword"refobj" then local num = scan_int() local whatsit = node.new(whatsit_id, refobj_whatsit) node.setproperty(whatsit, { obj = num, }) node.write(whatsit) elseif scan_keyword"outline" then local pfile = get_pfile() local attr = scan_keyword'attr' and scan_string() or '' local action if scan_keyword"useobjnum" then action = scan_int() else local actionobj = scan_action() action = pfile:indirect(nil, get_action_attr(pfile, actionobj)) end local outline = get_outline() if scan_keyword'level' then local level = scan_int() local open = scan_keyword'open' local title = scan_string() outline:add(pdf_text(title), action, level, open, attr) else local count = scan_keyword'count' and scan_int() or 0 local title = scan_string() outline:add_legacy(pdf_text(title), action, count, attr) end elseif scan_keyword"dest" then local id if scan_keyword'num' then id = scan_int() if id <= 0 then error[[id must be positive]] end elseif scan_keyword'name' then id = scan_string() else error[[Unsupported id type]] end local whatsit = node.new(whatsit_id, dest_whatsit) local prop = { dest_id = id, } node.setproperty(whatsit, prop) if scan_keyword'xyz' then prop.dest_type = 'xyz' prop.xyz_zoom = scan_keyword'zoom' and scan_int() maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fitr' then prop.dest_type = 'fitr' maybe_gobble_cmd(spacer_cmd) while true do if scan_keyword'width' then prop.width = scan_dimen() elseif scan_keyword'height' then prop.height = scan_dimen() elseif scan_keyword'depth' then prop.depth = scan_dimen() else break end end elseif scan_keyword'fitbh' then prop.dest_type = 'fitbh' maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fitbv' then prop.dest_type = 'fitbv' maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fitb' then prop.dest_type = 'fitb' maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fith' then prop.dest_type = 'fith' maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fitv' then prop.dest_type = 'fitv' maybe_gobble_cmd(spacer_cmd) elseif scan_keyword'fit' then prop.dest_type = 'fit' maybe_gobble_cmd(spacer_cmd) else error[[Unsupported dest type]] end node.write(whatsit) elseif scan_keyword'mapline' then fontmap.mapline(scan_string()) else -- The following error message gobbles the next word as a side effect. -- This is intentional to make error-recovery easier. error(string.format("Unknown PDF extension %s", scan_word())) end end, "value") local imglib = require'luametalatex-pdf-image' local imglib_node = imglib.node local imglib_write = imglib.write local imglib_immediatewrite = imglib.immediatewrite img = { new = imglib.new, scan = imglib.scan, node = function(img, pfile) return imglib_node(pfile or get_pfile(), img) end, write = function(img, pfile) return imglib_write(pfile or get_pfile(), img) end, immediatewrite = function(img, pfile) return imglib_immediatewrite(pfile or get_pfile(), img) end, } local lastimage = -1 local lastimagepages = -1 -- These are very minimal right now but LaTeX isn't using the scaling etc. stuff anyway. token.luacmd("saveimageresource", function(_, immediate) if immediate == "value" then return end if immediate and immediate & 0x7 ~= 0 then print(immediate) immediate = immediate & 0x8 tex.error("Unexpected prefix", "You used \\saveimageresource with a prefix that doesn't belong there. I will ignore it for now.") end local attr = scan_keyword'attr' and scan_string() or nil local page = scan_keyword'page' and scan_int() or nil local userpassword = scan_keyword'userpassword' and scan_string() or nil local ownerpassword = scan_keyword'ownerpassword' and scan_string() or nil -- local colorspace = scan_keyword'colorspace' and scan_int() or nil -- Doesn't make sense for PDF local pagebox = scan_keyword'mediabox' and 'media' or scan_keyword'cropbox' and 'crop' or scan_keyword'bleedbox' and 'bleed' or scan_keyword'trimbox' and 'trim' or scan_keyword'artbox' and 'art' or nil local filename = scan_string() local img = imglib.scan{ attr = attr, page = page, userpassword = userpassword, ownerpassword = ownerpassword, pagebox = pagebox, filename = filename, } local pfile = get_pfile() lastimage = imglib.get_num(pfile, img) lastimagepages = img.pages or 1 if immediate == 8 then imglib_immediatewrite(pfile, img) end end, "value") token.luacmd("useimageresource", function() local pfile = get_pfile() local img = assert(imglib.from_num(scan_int())) imglib_write(pfile, img) end, "protected") local integer_code = token.value.integer token.luacmd("lastsavedimageresourceindex", function() return integer_code, lastimage end, "value") token.luacmd("lastsavedimageresourcepages", function() return integer_code, lastimagepages end, "value") local savedbox = require'luametalatex-pdf-savedbox' local savedbox_save = savedbox.save function tex.saveboxresource(n, attr, resources, immediate, type, margin, pfile) if not node.type(n) then n = tonumber(n) if not n then error[[Invalid argument to saveboxresource]] end token.put_next(token.create'box', token.new(n, token.command_id'char_given')) n = scan_box() end margin = margin or pdfvariable.xformmargin return savedbox_save(pfile or get_pfile(), n, attr, resources, immediate, type, margin, fontdirs, usedglyphs) end tex.useboxresource = savedbox.use local lastbox = -1 token.luacmd("saveboxresource", function(_, immediate) if immediate == "value" then return end if immediate and immediate & 0x7 ~= 0 then immediate = immediate & 0x8 tex.error("Unexpected prefix", "You used \\saveboxresource with a prefix that doesn't belong there. I will ignore it for now.") end local type if scan_keyword'type' then texio.write_nl('XForm type attribute ignored') type = scan_int() end local attr = scan_keyword'attr' and scan_string() or nil local resources = scan_keyword'resources' and scan_string() or nil local margin = scan_keyword'margin' and scan_dimen() or nil local box = scan_int() local index = tex.saveboxresource(box, attr, resources, immediate == 8, type, margin) lastbox = index end, "value") token.luacmd("useboxresource", function() local width, height, depth while true do if scan_keyword'width' then width = scan_dimen() elseif scan_keyword'height' then height = scan_dimen() elseif scan_keyword'depth' then depth = scan_dimen() else break end end local index = scan_int() node.write((tex.useboxresource(index, width, height, depth))) end, "protected") token.luacmd("lastsavedboxresourceindex", function() return integer_code, lastbox end, "value") local saved_pos_x, saved_pos_y = -1, -1 local save_pos_whatsit = declare_whatsit('save_pos', function(_, _, _, x, y) saved_pos_x, saved_pos_y = assert(math.tointeger(x)), assert(math.tointeger(y)) end) token.luacmd("savepos", function() -- \savepos return node.direct.write(node.direct.new(whatsit_id, save_pos_whatsit)) end, "protected") token.luacmd("lastxpos", function() return integer_code, (saved_pos_x+.5)//1 end, "value") token.luacmd("lastypos", function() return integer_code, (saved_pos_y+.5)//1 end, "value") local function pdf_register_funcs(name) pdf[name] = "" pdf['get' .. name] = function() return pdf[name] end pdf['set' .. name] = function(s) pdf[name] = assert(s) end end pdf_register_funcs'pageattributes' pdf_register_funcs'pageresources' pdf_register_funcs'pagesattributes'