2019-07-18 20:02:58 +02:00
|
|
|
|
local pdf = pdf
|
2020-06-13 14:34:53 +02:00
|
|
|
|
local pdfvariable = pdf.variable
|
2020-07-06 15:31:15 +02:00
|
|
|
|
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local writer = require'luametalatex-nodewriter'
|
|
|
|
|
local newpdf = require'luametalatex-pdf'
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local nametree = require'luametalatex-pdf-nametree'
|
2020-07-05 20:40:09 +02:00
|
|
|
|
local build_fontdir = require'luametalatex-pdf-font'
|
2020-07-08 22:20:08 +02:00
|
|
|
|
local prepare_node_font = require'luametalatex-pdf-font-node'.prepare
|
2020-07-07 04:54:27 +02:00
|
|
|
|
local fontmap = require'luametalatex-pdf-font-map'
|
2020-07-06 15:31:15 +02:00
|
|
|
|
|
|
|
|
|
local utils = require'luametalatex-pdf-utils'
|
|
|
|
|
local strip_floats = utils.strip_floats
|
|
|
|
|
local to_bp = utils.to_bp
|
|
|
|
|
|
2020-06-06 03:03:52 +02:00
|
|
|
|
local pdfname, pfile
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local fontdirs = setmetatable({}, {__index=function(t, k)t[k] = pfile:getobj() return t[k] end})
|
2020-07-08 22:20:08 +02:00
|
|
|
|
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})
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local dests = {}
|
|
|
|
|
local cur_page
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local declare_whatsit = require'luametalatex-whatsits'.new
|
2020-06-06 03:03:52 +02:00
|
|
|
|
local whatsit_id = node.id'whatsit'
|
|
|
|
|
local whatsits = node.whatsits()
|
2019-07-18 20:02:58 +02:00
|
|
|
|
local colorstacks = {{
|
|
|
|
|
page = true,
|
|
|
|
|
mode = "direct",
|
|
|
|
|
default = "0 g 0 G",
|
|
|
|
|
page_stack = {"0 g 0 G"},
|
|
|
|
|
}}
|
2020-01-02 04:14:39 +01:00
|
|
|
|
token.scan_list = token.scan_box -- They are equal if no parameter is present
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local spacer_cmd = token.command_id'spacer'
|
2020-05-31 09:30:49 +02:00
|
|
|
|
local function get_pfile()
|
2020-05-28 14:37:19 +02:00
|
|
|
|
if not pfile then
|
2020-06-06 03:03:52 +02:00
|
|
|
|
pdfname = tex.jobname .. '.pdf'
|
2020-05-28 14:37:19 +02:00
|
|
|
|
pfile = newpdf.open(tex.jobname .. '.pdf')
|
|
|
|
|
end
|
2020-05-31 09:30:49 +02:00
|
|
|
|
return pfile
|
|
|
|
|
end
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local outline
|
2020-07-05 20:40:09 +02:00
|
|
|
|
local build_outline = require'luametalatex-pdf-outline'
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local function get_outline()
|
|
|
|
|
if not outline then
|
2020-07-05 20:40:09 +02:00
|
|
|
|
outline = build_outline()
|
2020-06-07 03:22:07 +02:00
|
|
|
|
end
|
|
|
|
|
return outline
|
|
|
|
|
end
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local properties = node.direct.properties
|
2020-07-03 03:54:30 +02:00
|
|
|
|
local immediateassignment = token.new(5, token.command_id'convert')
|
|
|
|
|
local global = token.new(0, token.command_id'prefix')
|
|
|
|
|
local deadcycles = token.new(8, token.command_id'set_page_property')
|
|
|
|
|
local zero_tok = token.create(0x30)
|
|
|
|
|
local relax = token.new(0, 0)
|
|
|
|
|
local reset_deadcycles = {
|
|
|
|
|
immediateassignment,
|
|
|
|
|
global,
|
|
|
|
|
deadcycles,
|
|
|
|
|
zero_tok,
|
|
|
|
|
relax,
|
|
|
|
|
}
|
2020-05-31 09:30:49 +02:00
|
|
|
|
token.luacmd("shipout", function()
|
|
|
|
|
local pfile = get_pfile()
|
2020-07-09 18:59:04 +02:00
|
|
|
|
local total_voffset, total_hoffset = tex.voffset + pdfvariable.vorigin, tex.hoffset + pdfvariable.horigin
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local voff = node.new'kern'
|
2020-07-09 18:59:04 +02:00
|
|
|
|
voff.kern = total_voffset
|
2019-07-17 21:14:34 +02:00
|
|
|
|
voff.next = token.scan_list()
|
2020-07-09 18:59:04 +02:00
|
|
|
|
voff.next.shift = total_hoffset
|
2020-01-02 04:14:39 +01:00
|
|
|
|
local list = node.direct.tonode(node.direct.vpack(node.direct.todirect(voff)))
|
2020-07-09 18:59:04 +02:00
|
|
|
|
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
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local page, parent = pfile:newpage()
|
2020-06-02 01:22:59 +02:00
|
|
|
|
cur_page = page
|
|
|
|
|
local out, resources, annots = writer(pfile, list, fontdirs, usedglyphs, colorstacks)
|
|
|
|
|
cur_page = nil
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local content = pfile:stream(nil, '', out)
|
2020-07-08 22:20:08 +02:00
|
|
|
|
pfile:indirect(page, string.format([[<</Type/Page/Parent %i 0 R/Contents %i 0 R/MediaBox[0 %i %i %i]/Resources%s%s%s>>]], parent, content, -math.ceil(to_bp(list.depth)), math.ceil(to_bp(list.width)), math.ceil(to_bp(list.height)), resources(pdfvariable.pageresources), annots, pdfvariable.pageattr))
|
2020-06-06 03:03:52 +02:00
|
|
|
|
node.flush_list(list)
|
2020-07-03 03:54:30 +02:00
|
|
|
|
token.put_next(reset_deadcycles)
|
2019-07-17 21:14:34 +02:00
|
|
|
|
token.scan_token()
|
2020-05-28 14:37:19 +02:00
|
|
|
|
end, 'force', 'protected')
|
2020-06-29 19:04:09 +02:00
|
|
|
|
|
2019-07-21 14:19:05 +02:00
|
|
|
|
local infodir = ""
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local namesdir = ""
|
2020-05-31 09:30:49 +02:00
|
|
|
|
local catalogdir = ""
|
2020-07-02 11:02:38 +02:00
|
|
|
|
local catalog_openaction
|
2019-07-21 14:19:05 +02:00
|
|
|
|
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 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.banner)
|
|
|
|
|
end
|
|
|
|
|
return p:indirect(nil, string.format("<<%s%s>>", infodir, additional))
|
|
|
|
|
end
|
2020-06-05 17:38:30 +02:00
|
|
|
|
|
2020-06-07 22:42:53 +02:00
|
|
|
|
local pdf_escape = require'luametalatex-pdf-escape'
|
|
|
|
|
local pdf_bytestring = pdf_escape.escape_bytes
|
|
|
|
|
local pdf_text = pdf_escape.escape_text
|
2020-06-05 17:38:30 +02:00
|
|
|
|
|
2020-07-08 22:20:08 +02:00
|
|
|
|
local function nodefont_newindex(t, k, v)
|
|
|
|
|
t.generation = t.next_generation
|
|
|
|
|
return rawset(t, k, v)
|
|
|
|
|
end
|
|
|
|
|
|
2019-07-17 21:14:34 +02:00
|
|
|
|
callback.register("stop_run", function()
|
2020-05-28 14:37:19 +02:00
|
|
|
|
if not pfile then
|
|
|
|
|
return
|
|
|
|
|
end
|
2020-07-08 22:20:08 +02:00
|
|
|
|
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
|
2019-07-17 21:14:34 +02:00
|
|
|
|
for fid, id in pairs(fontdirs) do
|
2020-06-19 18:58:01 +02:00
|
|
|
|
local f = font.getfont(fid) or font.fonts[fid]
|
2019-07-17 21:14:34 +02:00
|
|
|
|
local sorted = {}
|
2020-07-08 22:20:08 +02:00
|
|
|
|
local used = usedglyphs[fid]
|
|
|
|
|
used.generation, used.next_generation = nil, nil
|
2019-07-17 21:14:34 +02:00
|
|
|
|
for k,v in pairs(usedglyphs[fid]) do
|
2020-07-03 03:54:30 +02:00
|
|
|
|
sorted[#sorted+1] = v
|
2019-07-17 21:14:34 +02:00
|
|
|
|
end
|
|
|
|
|
table.sort(sorted, function(a,b) return a[1] < b[1] end)
|
2020-07-05 20:40:09 +02:00
|
|
|
|
pfile:indirect(id, build_fontdir(pfile, f, sorted))
|
2019-07-17 21:14:34 +02:00
|
|
|
|
end
|
|
|
|
|
pfile.root = pfile:getobj()
|
2020-06-13 14:34:53 +02:00
|
|
|
|
pfile.version = string.format("%i.%i", pdfvariable.majorversion, pdfvariable.minorversion)
|
2020-06-07 03:22:07 +02:00
|
|
|
|
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
|
2020-07-02 02:06:58 +02:00
|
|
|
|
texio.write_nl(string.format("Warning: Undefined destination %q", tostring(k)))
|
2020-06-07 03:22:07 +02:00
|
|
|
|
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
|
2020-06-06 03:03:52 +02:00
|
|
|
|
local pages = #pfile.pages
|
2020-06-07 03:22:07 +02:00
|
|
|
|
if outline then
|
|
|
|
|
catalogdir = string.format("/Outlines %i 0 R%s", outline:write(pfile), catalogdir)
|
|
|
|
|
end
|
2020-07-02 11:02:38 +02:00
|
|
|
|
if catalog_openaction then
|
|
|
|
|
catalogdir = catalogdir .. '/OpenAction' .. catalog_openaction
|
|
|
|
|
end
|
2020-06-02 01:22:59 +02:00
|
|
|
|
pfile:indirect(pfile.root, string.format([[<</Type/Catalog/Version/%s/Pages %i 0 R%s>>]], pfile.version, pfile:writepages(), catalogdir))
|
2019-07-21 14:19:05 +02:00
|
|
|
|
pfile.info = write_infodir(pfile)
|
2020-06-06 03:03:52 +02:00
|
|
|
|
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", string.format(" %d words of node memory still in use:", status.var_used))
|
|
|
|
|
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, ', '))
|
2020-06-06 04:09:56 +02:00
|
|
|
|
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.log_name))
|
2019-07-17 21:14:34 +02:00
|
|
|
|
end, "Finish PDF file")
|
2019-07-18 13:10:33 +02:00
|
|
|
|
token.luacmd("pdfvariable", function()
|
2020-06-28 04:52:06 +02:00
|
|
|
|
for _, n in ipairs(pdf.variable_names) do
|
2019-07-18 13:10:33 +02:00
|
|
|
|
if token.scan_keyword(n) then
|
2020-06-28 04:52:06 +02:00
|
|
|
|
return token.put_next(token.create('pdfvariable ' .. n))
|
2019-07-18 13:10:33 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
-- The following error message gobbles the next word as a side effect.
|
|
|
|
|
-- This is intentional to make error-recovery easier.
|
2019-07-21 14:19:05 +02:00
|
|
|
|
--[[
|
2019-07-18 20:02:58 +02:00
|
|
|
|
error(string.format("Unknown PDF variable %s", token.scan_word()))
|
2019-07-21 14:19:05 +02:00
|
|
|
|
]] -- Delay the error to ensure luatex85.sty compatibility
|
|
|
|
|
texio.write_nl(string.format("Unknown PDF variable %s", token.scan_word()))
|
|
|
|
|
tex.sprint"\\unexpanded{\\undefinedpdfvariable}"
|
2019-07-18 20:02:58 +02:00
|
|
|
|
end)
|
2020-05-31 09:30:49 +02:00
|
|
|
|
|
|
|
|
|
local lastobj = -1
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local lastannot = -1
|
2020-05-31 09:30:49 +02:00
|
|
|
|
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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
|
2020-06-04 23:30:46 +02:00
|
|
|
|
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
|
2020-06-05 04:12:29 +02:00
|
|
|
|
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local function get_action_attr(p, action, is_link)
|
2020-06-05 17:38:30 +02:00
|
|
|
|
local action_type = action.action_type
|
|
|
|
|
if action_type == 3 then return action.data end
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local action_attr = is_link and "/Subtype/Link/A<<" or "<<"
|
2020-06-05 17:38:30 +02:00
|
|
|
|
local file = action.file
|
|
|
|
|
if file then
|
2020-06-07 22:42:53 +02:00
|
|
|
|
action_attr = action_attr .. '/F' .. pdf_bytestring(file)
|
2020-06-05 17:38:30 +02:00
|
|
|
|
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
|
|
|
|
|
error[[FIXME]]
|
|
|
|
|
elseif action_type == 1 then -- GoTo
|
|
|
|
|
local id = action.id
|
2020-07-02 11:02:38 +02:00
|
|
|
|
if id then
|
|
|
|
|
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
|
2020-06-05 17:38:30 +02:00
|
|
|
|
end
|
2020-07-02 11:02:38 +02:00
|
|
|
|
else
|
|
|
|
|
id = assert(action.page, 'GoTo action must contain either an id or a page')
|
|
|
|
|
local tokens = action.tokens
|
|
|
|
|
if file then
|
|
|
|
|
action_attr = string.format("%s/S/GoToR/D[%i %s]>>", action_attr, id-1, tokens)
|
2020-06-05 17:38:30 +02:00
|
|
|
|
else
|
2020-07-02 11:02:38 +02:00
|
|
|
|
local page_objnum = pfile:reservepage(id)
|
|
|
|
|
action_attr = string.format("%s/S/GoTo/D[%i 0 R %s]>>", action_attr, page_objnum, tokens)
|
2020-06-05 17:38:30 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return action_attr
|
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local function write_link(p, link)
|
|
|
|
|
local quads = link.quads
|
|
|
|
|
local minX, maxX, minY, maxY = math.huge, -math.huge, math.huge, -math.huge
|
2020-06-07 03:22:07 +02:00
|
|
|
|
local attr = link.attr .. get_action_attr(p, link.action, true)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
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)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
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)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
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
|
2020-07-06 15:31:15 +02:00
|
|
|
|
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("<</Type/Annot%s%s>>", boxes, attr))
|
2020-06-05 04:12:29 +02:00
|
|
|
|
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
|
2020-06-13 14:34:53 +02:00
|
|
|
|
local off = pdfvariable.linkmargin
|
2020-06-05 04:12:29 +02:00
|
|
|
|
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
|
2020-06-05 23:49:03 +02:00
|
|
|
|
local function linkcontext_set(linkcontext, p, x, y, list, level, kind)
|
2020-06-29 19:04:09 +02:00
|
|
|
|
if not p.is_page then return end
|
2020-06-05 23:49:03 +02:00
|
|
|
|
for _,l in ipairs(linkcontext) do if l.level == level then
|
|
|
|
|
addlinkpoint(p, l, x, y, list, level, kind)
|
|
|
|
|
end end
|
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-29 19:04:09 +02:00
|
|
|
|
if not p.is_page then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-29 19:04:09 +02:00
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
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')
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
|
|
|
|
local end_link_whatsit = declare_whatsit('pdf_end_link', function(prop, p, n, x, y, outer, _, level)
|
2020-06-29 19:04:09 +02:00
|
|
|
|
if not p.is_page then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-29 19:04:09 +02:00
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local links = p.linkcontext
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local link = links[#links]
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-05 04:12:29 +02:00
|
|
|
|
links[#links] = nil
|
2020-06-29 19:04:09 +02:00
|
|
|
|
if not links[1] then p.linkcontext = nil end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
addlinkpoint(p, link, x, y, outer, 'final')
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local setmatrix_whatsit do
|
2020-06-05 00:26:41 +02:00
|
|
|
|
local numberpattern = (lpeg.P'-'^-1 * lpeg.R'09'^0 * ('.' * lpeg.R'09'^0)^-1)/tonumber
|
2020-06-04 23:30:46 +02:00
|
|
|
|
local matrixpattern = numberpattern * ' ' * numberpattern * ' ' * numberpattern * ' ' * numberpattern
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-04 23:30:46 +02:00
|
|
|
|
local m = p.matrix
|
|
|
|
|
local a, b, c, d = matrixpattern:match(prop.data)
|
2020-06-05 00:26:41 +02:00
|
|
|
|
if not a then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-05 00:26:41 +02:00
|
|
|
|
end
|
2020-06-04 23:30:46 +02:00
|
|
|
|
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)
|
2020-06-05 00:26:41 +02:00
|
|
|
|
pdf.write_matrix(a, b, c, d, e, f, p)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
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
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
end
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local save_whatsit = declare_whatsit('pdf_save', function(prop, p, n, x, y, outer)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
pdf.write('page', 'q', x, y, p)
|
|
|
|
|
local lastmatrix = p.matrix
|
|
|
|
|
p.matrix = {[0] = lastmatrix, table.unpack(lastmatrix)}
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
|
|
|
|
local restore_whatsit = declare_whatsit('pdf_restore', function(prop, p, n, x, y, outer)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
-- TODO: Check x, y
|
|
|
|
|
pdf.write('page', 'Q', x, y, p)
|
|
|
|
|
p.matrix = p.matrix[0]
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-06-02 01:22:59 +02:00
|
|
|
|
assert(cur_page, "Destinations can not appear outside of a page")
|
|
|
|
|
local id = prop.dest_id
|
|
|
|
|
local dest_type = prop.dest_type
|
2020-06-30 15:14:24 +02:00
|
|
|
|
local off = pdfvariable.linkmargin
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local data
|
|
|
|
|
if dest_type == "xyz" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
local x, y = projected(p.matrix, x, y)
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local zoom = prop.xyz_zoom
|
|
|
|
|
if zoom then
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/XYZ %.5f %.5f %.3f]", cur_page, to_bp(x-off), to_bp(y+off), prop.zoom/1000)
|
2020-06-02 01:22:59 +02:00
|
|
|
|
else
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/XYZ %.5f %.5f null]", cur_page, to_bp(x-off), to_bp(y+off))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
end
|
|
|
|
|
elseif dest_type == "fitr" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
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)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
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))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
elseif dest_type == "fit" then
|
|
|
|
|
data = string.format("[%i 0 R/Fit]", cur_page)
|
|
|
|
|
elseif dest_type == "fith" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
local x, y = projected(p.matrix, x, y)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/FitH %.5f]", cur_page, to_bp(y+off))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
elseif dest_type == "fitv" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
local x, y = projected(p.matrix, x, y)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/FitV %.5f]", cur_page, to_bp(x-off))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
elseif dest_type == "fitb" then
|
|
|
|
|
data = string.format("[%i 0 R/FitB]", cur_page)
|
|
|
|
|
elseif dest_type == "fitbh" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
local x, y = projected(p.matrix, x, y)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/FitBH %.5f]", cur_page, to_bp(y+off))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
elseif dest_type == "fitbv" then
|
2020-06-24 02:47:02 +02:00
|
|
|
|
local x, y = projected(p.matrix, x, y)
|
2020-07-06 15:31:15 +02:00
|
|
|
|
data = string.format("[%i 0 R/FitBV %.5f]", cur_page, to_bp(x-off))
|
2020-06-02 01:22:59 +02:00
|
|
|
|
end
|
2020-06-03 23:59:59 +02:00
|
|
|
|
if pfile:written(dests[id]) then
|
|
|
|
|
texio.write_nl(string.format("Duplicate destination %q", id))
|
|
|
|
|
else
|
2020-07-06 15:31:15 +02:00
|
|
|
|
dests[id] = pfile:indirect(dests[id], strip_floats(data))
|
2020-06-03 23:59:59 +02:00
|
|
|
|
end
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2020-05-31 09:30:49 +02:00
|
|
|
|
pfile:reference(prop.obj)
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
|
|
|
|
local literal_whatsit = declare_whatsit('pdf_literal', function(prop, p, n, x, y)
|
|
|
|
|
if not prop then
|
2020-07-08 22:20:08 +02:00
|
|
|
|
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?")
|
2020-07-01 19:47:25 +02:00
|
|
|
|
return
|
|
|
|
|
end
|
2019-07-20 14:53:24 +02:00
|
|
|
|
pdf.write(prop.mode, prop.data, x, y, p)
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2019-07-18 20:02:58 +02:00
|
|
|
|
local colorstack = prop.colorstack
|
|
|
|
|
local stack
|
|
|
|
|
if p.is_page then
|
|
|
|
|
stack = colorstack.page_stack
|
2020-06-29 19:04:09 +02:00
|
|
|
|
elseif colorstack.last_form == p.resources then
|
2019-07-18 20:02:58 +02:00
|
|
|
|
stack = colorstack.form_stack
|
|
|
|
|
else
|
2020-06-29 19:04:09 +02:00
|
|
|
|
colorstack.last_form = p.resources
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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)
|
2020-07-01 19:47:25 +02:00
|
|
|
|
end)
|
2019-07-18 20:02:58 +02:00
|
|
|
|
local function write_colorstack()
|
|
|
|
|
local idx = token.scan_int()
|
|
|
|
|
local colorstack = colorstacks[idx + 1]
|
|
|
|
|
if not colorstack then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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]
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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
|
2020-07-01 19:47:25 +02:00
|
|
|
|
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
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, colorstack_whatsit)
|
2019-07-18 20:02:58 +02:00
|
|
|
|
node.setproperty(whatsit, {
|
|
|
|
|
colorstack = colorstack,
|
|
|
|
|
action = action,
|
|
|
|
|
data = text,
|
|
|
|
|
})
|
|
|
|
|
node.write(whatsit)
|
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local function scan_action()
|
|
|
|
|
local action_type
|
|
|
|
|
|
|
|
|
|
if token.scan_keyword'user' then
|
|
|
|
|
return {action_type = 3, data = token.scan_string()}
|
|
|
|
|
elseif token.scan_keyword'thread' then
|
|
|
|
|
error[[FIXME: Unsupported]] -- TODO
|
|
|
|
|
elseif token.scan_keyword'goto' then
|
|
|
|
|
action_type = 1
|
|
|
|
|
else
|
|
|
|
|
error[[Unsupported action]]
|
|
|
|
|
end
|
|
|
|
|
local action = {
|
|
|
|
|
action_type = action_type,
|
|
|
|
|
file = token.scan_keyword'file' and token.scan_string(),
|
|
|
|
|
}
|
|
|
|
|
if token.scan_keyword'page' then
|
2020-07-02 11:02:38 +02:00
|
|
|
|
assert(action_type == 1)
|
|
|
|
|
local page = token.scan_int()
|
|
|
|
|
if page <= 0 then
|
|
|
|
|
error[[page must be positive in action specified]]
|
|
|
|
|
end
|
|
|
|
|
action.page = page
|
|
|
|
|
action.tokens = token.scan_string()
|
2020-06-05 04:12:29 +02:00
|
|
|
|
elseif token.scan_keyword'num' then
|
2020-07-02 11:02:38 +02:00
|
|
|
|
if action.file and action_type == 1 then
|
2020-06-05 04:12:29 +02:00
|
|
|
|
error[[num style GoTo actions must be internal]]
|
|
|
|
|
end
|
|
|
|
|
action.id = token.scan_int()
|
2020-06-05 23:49:03 +02:00
|
|
|
|
if action.id <= 0 then
|
2020-06-05 04:12:29 +02:00
|
|
|
|
error[[id must be positive]]
|
|
|
|
|
end
|
|
|
|
|
elseif token.scan_keyword'name' then
|
|
|
|
|
action.id = token.scan_string()
|
|
|
|
|
else
|
|
|
|
|
error[[Unsupported id type]]
|
|
|
|
|
end
|
|
|
|
|
action.new_window = token.scan_keyword'newwindow' and 1
|
|
|
|
|
or token.scan_keyword'nonewwindow' and 2
|
2020-06-05 17:38:30 +02:00
|
|
|
|
if action.new_window and not action.file then
|
|
|
|
|
error[[newwindow is only supported for external files]]
|
|
|
|
|
end
|
2020-06-05 04:12:29 +02:00
|
|
|
|
return action
|
|
|
|
|
end
|
2019-07-20 14:53:24 +02:00
|
|
|
|
local function scan_literal_mode()
|
|
|
|
|
return token.scan_keyword"direct" and "direct"
|
|
|
|
|
or token.scan_keyword"page" and "page"
|
|
|
|
|
or token.scan_keyword"text" and "text"
|
|
|
|
|
or token.scan_keyword"direct" and "direct"
|
|
|
|
|
or token.scan_keyword"raw" and "raw"
|
|
|
|
|
or "origin"
|
|
|
|
|
end
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local function maybe_gobble_cmd(cmd)
|
|
|
|
|
local t = token.scan_token()
|
|
|
|
|
if t.command ~= cmd then
|
2020-06-07 03:22:07 +02:00
|
|
|
|
token.put_next(t)
|
2020-06-02 01:22:59 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2019-07-18 20:02:58 +02:00
|
|
|
|
token.luacmd("pdffeedback", function()
|
|
|
|
|
if token.scan_keyword"colorstackinit" then
|
|
|
|
|
local page = token.scan_keyword'page'
|
|
|
|
|
or (token.scan_keyword'nopage' and false) -- If you want to pass "page" as mode
|
2019-07-20 14:53:24 +02:00
|
|
|
|
local mode = scan_literal_mode()
|
2019-07-18 20:02:58 +02:00
|
|
|
|
local default = token.scan_string()
|
|
|
|
|
tex.sprint(tostring(pdf.newcolorstack(default, mode, page)))
|
2019-07-21 14:19:05 +02:00
|
|
|
|
elseif token.scan_keyword"creationdate" then
|
|
|
|
|
tex.sprint(creationdate)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
elseif token.scan_keyword"lastannot" then
|
|
|
|
|
tex.sprint(tostring(lastannot))
|
2020-05-31 09:30:49 +02:00
|
|
|
|
elseif token.scan_keyword"lastobj" then
|
|
|
|
|
tex.sprint(tostring(lastobj))
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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", token.scan_word()))
|
|
|
|
|
end
|
|
|
|
|
end)
|
2020-05-31 09:30:49 +02:00
|
|
|
|
token.luacmd("pdfextension", function(_, imm)
|
2019-07-18 20:02:58 +02:00
|
|
|
|
if token.scan_keyword"colorstack" then
|
|
|
|
|
write_colorstack()
|
2019-07-20 14:53:24 +02:00
|
|
|
|
elseif token.scan_keyword"literal" then
|
|
|
|
|
local mode = scan_literal_mode()
|
|
|
|
|
local literal = token.scan_string()
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, literal_whatsit)
|
2019-07-20 14:53:24 +02:00
|
|
|
|
node.setproperty(whatsit, {
|
|
|
|
|
mode = mode,
|
|
|
|
|
data = literal,
|
|
|
|
|
})
|
|
|
|
|
node.write(whatsit)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
elseif token.scan_keyword"startlink" then
|
|
|
|
|
local pfile = get_pfile()
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, start_link_whatsit)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
local attr = token.scan_keyword'attr' and token.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 token.scan_keyword"endlink" then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, end_link_whatsit)
|
2020-06-05 04:12:29 +02:00
|
|
|
|
node.write(whatsit)
|
2020-06-05 00:26:41 +02:00
|
|
|
|
elseif token.scan_keyword"save" then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, save_whatsit)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
node.write(whatsit)
|
|
|
|
|
elseif token.scan_keyword"setmatrix" then
|
|
|
|
|
local matrix = token.scan_string()
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, setmatrix_whatsit)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
node.setproperty(whatsit, {
|
|
|
|
|
data = matrix,
|
|
|
|
|
})
|
|
|
|
|
node.write(whatsit)
|
2020-06-05 00:26:41 +02:00
|
|
|
|
elseif token.scan_keyword"restore" then
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, restore_whatsit)
|
2020-06-04 23:30:46 +02:00
|
|
|
|
node.write(whatsit)
|
2019-07-21 14:19:05 +02:00
|
|
|
|
elseif token.scan_keyword"info" then
|
|
|
|
|
infodir = infodir .. token.scan_string()
|
2020-05-31 09:30:49 +02:00
|
|
|
|
elseif token.scan_keyword"catalog" then
|
2020-06-02 01:22:59 +02:00
|
|
|
|
catalogdir = catalogdir .. ' ' .. token.scan_string()
|
2020-07-02 11:02:38 +02:00
|
|
|
|
if token.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
|
2020-06-07 03:22:07 +02:00
|
|
|
|
elseif token.scan_keyword"names" then
|
|
|
|
|
namesdir = namesdir .. ' ' .. token.scan_string()
|
2020-05-31 09:30:49 +02:00
|
|
|
|
elseif token.scan_keyword"obj" then
|
|
|
|
|
local pfile = get_pfile()
|
|
|
|
|
if token.scan_keyword"reserveobjnum" then
|
|
|
|
|
lastobj = pfile:getobj()
|
|
|
|
|
else
|
|
|
|
|
local num = token.scan_keyword'useobjnum' and token.scan_int() or pfile:getobj()
|
|
|
|
|
lastobj = num
|
|
|
|
|
local attr = token.scan_keyword'stream' and (token.scan_keyword'attr' and token.scan_string() or '')
|
|
|
|
|
local isfile = token.scan_keyword'file'
|
|
|
|
|
local content = token.scan_string()
|
|
|
|
|
if immediate then
|
|
|
|
|
if attr then
|
|
|
|
|
pfile:stream(num, attr, content, isfile)
|
|
|
|
|
else
|
2020-06-02 01:22:59 +02:00
|
|
|
|
pfile:indirect(num, content, isfile)
|
2020-05-31 09:30:49 +02:00
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
if attr then
|
|
|
|
|
pfile:delayedstream(num, attr, content, isfile)
|
|
|
|
|
else
|
|
|
|
|
pfile:delayed(num, attr, content, isfile)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
elseif token.scan_keyword"refobj" then
|
|
|
|
|
local num = token.scan_int()
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, refobj_whatsit)
|
2020-05-31 09:30:49 +02:00
|
|
|
|
node.setproperty(whatsit, {
|
|
|
|
|
obj = num,
|
|
|
|
|
})
|
|
|
|
|
node.write(whatsit)
|
2020-06-07 03:22:07 +02:00
|
|
|
|
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
|
2020-06-07 13:10:33 +02:00
|
|
|
|
local outline = get_outline()
|
2020-06-07 03:22:07 +02:00
|
|
|
|
if token.scan_keyword'level' then
|
|
|
|
|
local level = token.scan_int()
|
|
|
|
|
local open = token.scan_keyword'open'
|
|
|
|
|
local title = token.scan_string()
|
2020-06-07 22:42:53 +02:00
|
|
|
|
outline:add(pdf_text(title), action, level, open, attr)
|
2020-06-07 03:22:07 +02:00
|
|
|
|
else
|
2020-06-07 13:10:33 +02:00
|
|
|
|
local count = token.scan_keyword'count' and token.scan_int() or 0
|
|
|
|
|
local title = token.scan_string()
|
2020-06-07 22:42:53 +02:00
|
|
|
|
outline:add_legacy(pdf_text(title), action, count, attr)
|
2020-06-07 03:22:07 +02:00
|
|
|
|
end
|
2020-06-02 01:22:59 +02:00
|
|
|
|
elseif token.scan_keyword"dest" then
|
|
|
|
|
local id
|
|
|
|
|
if token.scan_keyword'num' then
|
|
|
|
|
id = token.scan_int()
|
2020-06-05 23:49:03 +02:00
|
|
|
|
if id <= 0 then
|
2020-06-02 01:22:59 +02:00
|
|
|
|
error[[id must be positive]]
|
|
|
|
|
end
|
|
|
|
|
elseif token.scan_keyword'name' then
|
|
|
|
|
id = token.scan_string()
|
|
|
|
|
else
|
|
|
|
|
error[[Unsupported id type]]
|
|
|
|
|
end
|
2020-07-01 19:47:25 +02:00
|
|
|
|
local whatsit = node.new(whatsit_id, dest_whatsit)
|
2020-06-02 01:22:59 +02:00
|
|
|
|
local prop = {
|
|
|
|
|
dest_id = id,
|
|
|
|
|
}
|
|
|
|
|
node.setproperty(whatsit, prop)
|
|
|
|
|
if token.scan_keyword'xyz' then
|
|
|
|
|
prop.dest_type = 'xyz'
|
|
|
|
|
prop.xyz_zoom = token.scan_keyword'zoom' and token.scan_int()
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fitr' then
|
|
|
|
|
prop.dest_type = 'fitr'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
while true do
|
|
|
|
|
if token.scan_keyword'width' then
|
|
|
|
|
prop.width = token.scan_dimen()
|
|
|
|
|
elseif token.scan_keyword'height' then
|
|
|
|
|
prop.height = token.scan_dimen()
|
|
|
|
|
elseif token.scan_keyword'depth' then
|
|
|
|
|
prop.depth = token.scan_dimen()
|
|
|
|
|
else
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
elseif token.scan_keyword'fitbh' then
|
|
|
|
|
prop.dest_type = 'fitbh'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fitbv' then
|
|
|
|
|
prop.dest_type = 'fitbv'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fitb' then
|
|
|
|
|
prop.dest_type = 'fitb'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fith' then
|
|
|
|
|
prop.dest_type = 'fith'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fitv' then
|
|
|
|
|
prop.dest_type = 'fitv'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
elseif token.scan_keyword'fit' then
|
|
|
|
|
prop.dest_type = 'fit'
|
|
|
|
|
maybe_gobble_cmd(spacer_cmd)
|
|
|
|
|
else
|
|
|
|
|
error[[Unsupported dest type]]
|
|
|
|
|
end
|
|
|
|
|
node.write(whatsit)
|
2020-07-07 04:54:27 +02:00
|
|
|
|
elseif token.scan_keyword'mapline' then
|
|
|
|
|
fontmap.mapline(token.scan_string())
|
2019-07-18 20:02:58 +02:00
|
|
|
|
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", token.scan_word()))
|
|
|
|
|
end
|
2019-07-20 14:53:24 +02:00
|
|
|
|
end, "protected")
|
2020-07-05 20:40:09 +02:00
|
|
|
|
local imglib = require'luametalatex-pdf-image'
|
2020-06-11 12:44:37 +02:00
|
|
|
|
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,
|
|
|
|
|
}
|
2020-06-13 02:53:08 +02:00
|
|
|
|
|
2020-06-13 03:31:25 +02:00
|
|
|
|
local lastimage = -1
|
|
|
|
|
local lastimagepages = -1
|
|
|
|
|
|
2020-06-13 02:53:08 +02:00
|
|
|
|
-- These are very minimal right now but LaTeX isn't using the scaling etc. stuff anyway.
|
|
|
|
|
token.luacmd("saveimageresource", function(imm)
|
|
|
|
|
local attr = token.scan_keyword'attr' and token.scan_string() or nil
|
|
|
|
|
local page = token.scan_keyword'page' and token.scan_int() or nil
|
|
|
|
|
local userpassword = token.scan_keyword'userpassword' and token.scan_string() or nil
|
|
|
|
|
local ownerpassword = token.scan_keyword'ownerpassword' and token.scan_string() or nil
|
|
|
|
|
-- local colorspace = token.scan_keyword'colorspace' and token.scan_int() or nil -- Doesn't make sense for PDF
|
|
|
|
|
local pagebox = token.scan_keyword'mediabox' and 'media'
|
|
|
|
|
or token.scan_keyword'cropbox' and 'crop'
|
|
|
|
|
or token.scan_keyword'bleedbox' and 'bleed'
|
|
|
|
|
or token.scan_keyword'trimbox' and 'trim'
|
|
|
|
|
or token.scan_keyword'artbox' and 'art'
|
|
|
|
|
or nil
|
|
|
|
|
local filename = token.scan_string()
|
|
|
|
|
local img = imglib.scan{
|
|
|
|
|
attr = attr,
|
|
|
|
|
page = page,
|
|
|
|
|
userpassword = userpassword,
|
|
|
|
|
ownerpassword = ownerpassword,
|
|
|
|
|
pagebox = pagebox,
|
2020-06-13 03:31:25 +02:00
|
|
|
|
filename = filename,
|
2020-06-13 02:53:08 +02:00
|
|
|
|
}
|
|
|
|
|
local pfile = get_pfile()
|
|
|
|
|
lastimage = imglib.get_num(pfile, img)
|
2020-06-13 03:31:25 +02:00
|
|
|
|
lastimagepages = img.pages or 1
|
2020-06-13 02:53:08 +02:00
|
|
|
|
if imm == 'immediate' then
|
|
|
|
|
imglib_immediatewrite(pfile, img)
|
|
|
|
|
end
|
|
|
|
|
end, "protected")
|
2020-06-13 03:31:25 +02:00
|
|
|
|
|
2020-06-13 02:53:08 +02:00
|
|
|
|
token.luacmd("useimageresource", function()
|
|
|
|
|
local pfile = get_pfile()
|
|
|
|
|
local img = assert(imglib.from_num(token.scan_int()))
|
|
|
|
|
imglib_write(pfile, img)
|
|
|
|
|
end, "protected")
|
2020-06-13 03:31:25 +02:00
|
|
|
|
|
|
|
|
|
local value_values = token.values'value'
|
|
|
|
|
for i=0,#value_values do
|
|
|
|
|
value_values[value_values[i]] = i
|
|
|
|
|
end
|
|
|
|
|
local integer_code = value_values.integer
|
|
|
|
|
|
|
|
|
|
token.luacmd("lastsavedimageresourceindex", function()
|
|
|
|
|
return integer_code, lastimage
|
|
|
|
|
end, "protected", "value")
|
|
|
|
|
|
|
|
|
|
token.luacmd("lastsavedimageresourcepages", function()
|
|
|
|
|
return integer_code, lastimagepages
|
|
|
|
|
end, "protected", "value")
|
2020-06-30 11:48:46 +02:00
|
|
|
|
|
|
|
|
|
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 = token.scan_list()
|
|
|
|
|
end
|
|
|
|
|
margin = margin or tex.sp'1bp' -- FIXME: default margin variable
|
|
|
|
|
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(imm)
|
|
|
|
|
local type
|
|
|
|
|
if token.scan_keyword'type' then
|
|
|
|
|
texio.write_nl('XForm type attribute ignored')
|
|
|
|
|
type = token.scan_int()
|
|
|
|
|
end
|
|
|
|
|
local attr = token.scan_keyword'attr' and token.scan_string() or nil
|
|
|
|
|
local resources = token.scan_keyword'resources' and token.scan_string() or nil
|
2020-07-01 13:49:38 +02:00
|
|
|
|
local margin = token.scan_keyword'margin' and token.scan_dimen() or nil
|
2020-06-30 11:48:46 +02:00
|
|
|
|
local box = token.scan_int()
|
|
|
|
|
|
|
|
|
|
local index = tex.saveboxresource(box, attr, resources, imm == 'immediate', type, margin)
|
|
|
|
|
lastbox = index
|
|
|
|
|
end, "protected")
|
|
|
|
|
|
|
|
|
|
token.luacmd("useboxresource", function()
|
|
|
|
|
local width, height, depth
|
|
|
|
|
while true do
|
|
|
|
|
if token.scan_keyword'width' then
|
|
|
|
|
width = token.scan_dimen()
|
|
|
|
|
elseif token.scan_keyword'height' then
|
|
|
|
|
height = token.scan_dimen()
|
|
|
|
|
elseif token.scan_keyword'depth' then
|
|
|
|
|
depth = token.scan_dimen()
|
|
|
|
|
else
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
local index = token.scan_int()
|
|
|
|
|
node.write((tex.useboxresource(index, width, height, depth)))
|
|
|
|
|
end, "protected")
|
|
|
|
|
|
|
|
|
|
token.luacmd("lastsavedboxresourceindex", function()
|
|
|
|
|
return integer_code, lastbox
|
|
|
|
|
end, "protected", "value")
|
2020-07-09 17:32:42 +02:00
|
|
|
|
|
|
|
|
|
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, "protected", "value")
|
|
|
|
|
|
|
|
|
|
token.luacmd("lastypos", function()
|
|
|
|
|
return integer_code, (saved_pos_y+.5)//1
|
|
|
|
|
end, "protected", "value")
|