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.
This commit is contained in:
Marcel Krüger 2020-06-07 03:22:07 +02:00
parent c83b7ffc5e
commit 7e3a4e3c74
3 changed files with 206 additions and 4 deletions

View File

@ -1,6 +1,7 @@
local pdf = pdf local pdf = pdf
local writer = require'luametalatex-nodewriter' local writer = require'luametalatex-nodewriter'
local newpdf = require'luametalatex-pdf' local newpdf = require'luametalatex-pdf'
local nametree = require'luametalatex-pdf-nametree'
local pdfname, pfile local pdfname, pfile
local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end}) local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end})
local usedglyphs = {} local usedglyphs = {}
@ -23,6 +24,13 @@ local function get_pfile()
end end
return pfile return pfile
end 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 local properties = node.direct.properties
token.luacmd("shipout", function() token.luacmd("shipout", function()
local pfile = get_pfile() local pfile = get_pfile()
@ -44,6 +52,7 @@ token.luacmd("shipout", function()
token.scan_token() token.scan_token()
end, 'force', 'protected') end, 'force', 'protected')
local infodir = "" local infodir = ""
local namesdir = ""
local catalogdir = "" local catalogdir = ""
local creationdate = os.date("D:%Y%m%d%H%M%S%z"):gsub("+0000$", "Z"):gsub("%d%d$", "'%0") local creationdate = os.date("D:%Y%m%d%H%M%S%z"):gsub("+0000$", "Z"):gsub("%d%d$", "'%0")
local function write_infodir(p) local function write_infodir(p)
@ -87,7 +96,26 @@ callback.register("stop_run", function()
end end
pfile.root = pfile:getobj() pfile.root = pfile:getobj()
pfile.version = string.format("%i.%i", pdf.variable.majorversion, pdf.variable.minorversion) 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 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([[<</Type/Catalog/Version/%s/Pages %i 0 R%s>>]], pfile.version, pfile:writepages(), catalogdir)) pfile:indirect(pfile.root, string.format([[<</Type/Catalog/Version/%s/Pages %i 0 R%s>>]], pfile.version, pfile:writepages(), catalogdir))
pfile.info = write_infodir(pfile) pfile.info = write_infodir(pfile)
local size = pfile:close() 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] return x*m[1] + y*m[3] + w*m[5], x*m[2] + y*m[4] + w*m[6]
end end
local function get_action_attr(p, action) local function get_action_attr(p, action, is_link)
local action_type = action.action_type local action_type = action.action_type
if action_type == 3 then return action.data end 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 local file = action.file
if file then if file then
action_attr = action_attr .. '/F' .. pdf_string(file) action_attr = action_attr .. '/F' .. pdf_string(file)
@ -190,7 +218,7 @@ end
local function write_link(p, link) local function write_link(p, link)
local quads = link.quads local quads = link.quads
local minX, maxX, minY, maxY = math.huge, -math.huge, math.huge, -math.huge 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) assert(#quads%8==0)
local quadStr = {} local quadStr = {}
for i=1,#quads,8 do for i=1,#quads,8 do
@ -421,7 +449,7 @@ end
local function maybe_gobble_cmd(cmd) local function maybe_gobble_cmd(cmd)
local t = token.scan_token() local t = token.scan_token()
if t.command ~= cmd then if t.command ~= cmd then
token.put_next(cmd) token.put_next(t)
end end
end end
token.luacmd("pdffeedback", function() token.luacmd("pdffeedback", function()
@ -500,6 +528,8 @@ token.luacmd("pdfextension", function(_, imm)
infodir = infodir .. token.scan_string() infodir = infodir .. token.scan_string()
elseif token.scan_keyword"catalog" then elseif token.scan_keyword"catalog" then
catalogdir = catalogdir .. ' ' .. token.scan_string() catalogdir = catalogdir .. ' ' .. token.scan_string()
elseif token.scan_keyword"names" then
namesdir = namesdir .. ' ' .. token.scan_string()
elseif token.scan_keyword"obj" then elseif token.scan_keyword"obj" then
local pfile = get_pfile() local pfile = get_pfile()
if token.scan_keyword"reserveobjnum" then if token.scan_keyword"reserveobjnum" then
@ -532,6 +562,25 @@ token.luacmd("pdfextension", function(_, imm)
handle = do_refobj, handle = do_refobj,
}) })
node.write(whatsit) 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 elseif token.scan_keyword"dest" then
local id local id
if token.scan_keyword'num' then if token.scan_keyword'num' then

View File

@ -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('<</Limits[%s %s]/Kids[%s 0 R]>>', 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('<</Kids[%s 0 R]>>', 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('<</Limits[%s %s]/Names[%s]>>', tree[6*i+1], tree[6*i+6] or tree[total], concat(serialized, ' ')))
else
return pdf:indirect(nil, format('<</Names[%s]>>', concat(serialized, ' ')))
end
end
return write(pdf, newtree, tree, 36)
end

View File

@ -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