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:
parent
c83b7ffc5e
commit
7e3a4e3c74
@ -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
|
||||||
|
53
luametalatex-pdf-nametree.lua
Normal file
53
luametalatex-pdf-nametree.lua
Normal 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
|
100
luametalatex-pdf-outline.lua
Normal file
100
luametalatex-pdf-outline.lua
Normal 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
|
Loading…
Reference in New Issue
Block a user