From 7e3a4e3c74c1b9e2c03248b52f7153a52a2121c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Fabian=20Kr=C3=BCger?= Date: Sun, 7 Jun 2020 03:22:07 +0200 Subject: [PATCH] Named destinations and improved outline interface There is no support for PDFTeX's outline command yet, but the new one is much easier to use. --- luametalatex-back-pdf.lua | 57 +++++++++++++++++-- luametalatex-pdf-nametree.lua | 53 ++++++++++++++++++ luametalatex-pdf-outline.lua | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 luametalatex-pdf-nametree.lua create mode 100644 luametalatex-pdf-outline.lua diff --git a/luametalatex-back-pdf.lua b/luametalatex-back-pdf.lua index 46ccd34..ea39983 100644 --- a/luametalatex-back-pdf.lua +++ b/luametalatex-back-pdf.lua @@ -1,6 +1,7 @@ local pdf = pdf local writer = require'luametalatex-nodewriter' local newpdf = require'luametalatex-pdf' +local nametree = require'luametalatex-pdf-nametree' local pdfname, pfile local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end}) local usedglyphs = {} @@ -23,6 +24,13 @@ local function get_pfile() end return pfile end +local outline +local function get_outline() + if not outline then + outline = require'luametalatex-pdf-outline'() + end + return outline +end local properties = node.direct.properties token.luacmd("shipout", function() local pfile = get_pfile() @@ -44,6 +52,7 @@ token.luacmd("shipout", function() token.scan_token() end, 'force', 'protected') local infodir = "" +local namesdir = "" local catalogdir = "" local creationdate = os.date("D:%Y%m%d%H%M%S%z"):gsub("+0000$", "Z"):gsub("%d%d$", "'%0") local function write_infodir(p) @@ -87,7 +96,26 @@ callback.register("stop_run", function() end pfile.root = pfile:getobj() pfile.version = string.format("%i.%i", pdf.variable.majorversion, pdf.variable.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("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 pfile:indirect(pfile.root, string.format([[<>]], pfile.version, pfile:writepages(), catalogdir)) pfile.info = write_infodir(pfile) local size = pfile:close() @@ -151,10 +179,10 @@ local function projected(m, x, y, w) 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) +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 = "/Subtype/Link/A<<" + local action_attr = is_link and "/Subtype/Link/A<<" or "<<" local file = action.file if file then action_attr = action_attr .. '/F' .. pdf_string(file) @@ -190,7 +218,7 @@ 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) + local attr = link.attr .. get_action_attr(p, link.action, true) assert(#quads%8==0) local quadStr = {} for i=1,#quads,8 do @@ -421,7 +449,7 @@ end local function maybe_gobble_cmd(cmd) local t = token.scan_token() if t.command ~= cmd then - token.put_next(cmd) + token.put_next(t) end end token.luacmd("pdffeedback", function() @@ -500,6 +528,8 @@ token.luacmd("pdfextension", function(_, imm) infodir = infodir .. token.scan_string() elseif token.scan_keyword"catalog" then catalogdir = catalogdir .. ' ' .. token.scan_string() + elseif token.scan_keyword"names" then + namesdir = namesdir .. ' ' .. token.scan_string() elseif token.scan_keyword"obj" then local pfile = get_pfile() if token.scan_keyword"reserveobjnum" then @@ -532,6 +562,25 @@ token.luacmd("pdfextension", function(_, imm) handle = do_refobj, }) node.write(whatsit) + elseif token.scan_keyword"outline" then + local pfile = get_pfile() + local attr = token.scan_keyword'attr' and token.scan_string() or '' + local action + if token.scan_keyword"useobjnum" then + action = token.scan_int() + else + local actionobj = scan_action() + action = pfile:indirect(nil, get_action_attr(pfile, actionobj)) + end + if token.scan_keyword'level' then + local level = token.scan_int() + local open = token.scan_keyword'open' + local outline = get_outline() + local title = token.scan_string() + outline:add(pdf_string(title), action, level, open, attr) + else + error[[Legacy outline not yet supported]] + end elseif token.scan_keyword"dest" then local id if token.scan_keyword'num' then diff --git a/luametalatex-pdf-nametree.lua b/luametalatex-pdf-nametree.lua new file mode 100644 index 0000000..8ddab33 --- /dev/null +++ b/luametalatex-pdf-nametree.lua @@ -0,0 +1,53 @@ +local min = math.min +local format = string.format +local concat = table.concat +local move = table.move +local function write(pdf, tree, escaped, step) + local nextcount = (#tree-1)//6+1 + for i=1, nextcount do + if #tree > 6 then + tree[i] = pdf:indirect(nil, format('<>', escaped[step*(i-1)+1], escaped[step*i] or escaped[#escaped], concat(tree, ' 0 R ', 6*i-5, min(#tree, 6*i)))) + else + return pdf:indirect(nil, format('<>', concat(tree, ' 0 R ', 6*i-5, #tree))) + end + end + move(tree, #tree+1, 2*#tree-nextcount, nextcount+1) + return write(pdf, tree, escaped, step*6) +end +local function pdf_string(s) + -- Emulate other engines here: If looks like an escaped string, treat it as such. Otherwise, add parenthesis. + return s:match("^%(.*%)$") or s:match("^<.*>$") or '(' .. s .. ')' +end +local serialized = {} +return function(values, pdf) + local tree = {} + for k in next, values do + if type(k) ~= "string" then + error[[Invalid entry in nametree]] -- Might get ignored in a later version + end + tree[#tree+1] = k + end + table.sort(tree) + local total = #tree + local newtree = {} + for i=0,(total-1)//6 do + for j=1, 6 do + local key = tree[6*i+j] + if key then + local value = values[key] + key = pdf_string(key) + tree[6*i+j] = key + serialized[2*j-1] = key + serialized[2*j] = value + else + serialized[2*j-1], serialized[2*j] = nil, nil + end + end + if total > 6 then + newtree[i+1] = pdf:indirect(nil, format('<>', tree[6*i+1], tree[6*i+6] or tree[total], concat(serialized, ' '))) + else + return pdf:indirect(nil, format('<>', concat(serialized, ' '))) + end + end + return write(pdf, newtree, tree, 36) +end diff --git a/luametalatex-pdf-outline.lua b/luametalatex-pdf-outline.lua new file mode 100644 index 0000000..62b6f69 --- /dev/null +++ b/luametalatex-pdf-outline.lua @@ -0,0 +1,100 @@ +local outline = { + { title = "title 1", attr = "", open = true, level = 5, + { ... }, + } +} +local function add_outline(outline, title, action, level, open, attr) + local entry = {title = title, action = action, attr = attr, open = open, level = level} + -- Now find the right nesting level. We have to deal with non-continuous + -- levels, so we search the last entry which still had a smaller level + -- and append under that + local parent + repeat + parent = outline + outline = outline[#outline] + until not outline or outline.level >= level + parent[#parent + 1] = entry +end +local function assign_objnum(pdf, outline) + local objnum = pdf:getobj() + outline.objnum = objnum + local cur + for i=1,#outline do + local prev = cur + cur = outline[i] + cur.parent = objnum + assign_objnum(pdf, cur) + if prev then + cur.prev = prev.objnum + prev.next = cur.objnum + end + end + if outline[1] then + outline.first, outline.last = outline[1].objnum, outline[#outline].objnum + end +end +local function get_count(pdf, outline) + local count = 0 + for i=1,#outline do + local child = outline[i] + local sub = get_count(pdf, child) + local open = child.open + child.count = sub ~= 0 and (open and sub or -sub) or nil + count = count + 1 + (open and sub or 0) + end + return count +end +local function write_objects(pdf, outline) + local content = "<<" + local title = outline.title + if title then + content = string.format("%s/Title%s", content, title) + end + local parent = outline.parent + if parent then + content = string.format("%s/Parent %i 0 R", content, parent) + end + local prev = outline.prev + if prev then + content = string.format("%s/Prev %i 0 R", content, prev) + end + local next = outline.next + if next then + content = string.format("%s/Next %i 0 R", content, next) + end + local first = outline.first + if first then + content = string.format("%s/First %i 0 R", content, first) + end + local last = outline.last + if last then + content = string.format("%s/Last %i 0 R", content, last) + end + local action = outline.action + if action then + content = string.format("%s/A %i 0 R", content, action) + end + local count = outline.count + if count then + content = string.format("%s/Count %i", content, count) + end + content = content .. (outline.attr or '') .. ">>" + pdf:indirect(outline.objnum, content) + for i=1,#outline do + write_objects(pdf, outline[i]) + end +end +local function write_outline(outline, pdf) + assign_objnum(pdf, outline) + local count = get_count(pdf, outline) + outline.count = count == #outline and count or nil + write_objects(pdf, outline) + return outline.objnum +end +local meta = {__index = { + write = write_outline, + add = add_outline, +}} +return function() + return setmetatable({}, meta) +end