luametalatex/luametalatex-pdf.lua

141 lines
3.7 KiB
Lua

local format = string.format
local gsub = string.gsub
local byte = string.byte
local pack = string.pack
local error = error
local pairs = pairs
local setmetatable = setmetatable
local assigned = {}
local delayed = {}
-- slightly tricky interface: No/nil return means that the objects content
-- isn't known yet, while false indicates a delayed object.
local function written(pdf, num)
num = pdf[num]
if not num or num == assigned then return end
return num ~= delayed
end
-- raw: Pass on preencoded stream. Currently ignored.
local function stream(pdf, num, dict, content, isfile, raw)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = {offset = pdf.file:seek()}
if isfile then
local f = io.open(content)
content = f:read'a'
f:close()
end
pdf.file:write(format('%i 0 obj\n<<%s/Length %i>>stream\n', num, dict, #content))
pdf.file:write(content)
pdf.file:write'\nendstream\nendobj\n'
return num
end
local function delayedstream(pdf, num, dict, content, isfile)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = delayed
pdf[-num] = {stream, dict, content, isfile}
return num
end
local function indirect(pdf, num, content, isfile)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = {offset = pdf.file:seek()}
pdf.file:write(format('%i 0 obj\n', num))
if isfile then
local f = io.open(content)
content = f:read'a'
f:close()
end
pdf.file:write(content)
pdf.file:write'\nendobj\n'
return num
end
local function delay(pdf, num, content, isfile)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
pdf[num] = delayed
pdf[-num] = {indirect, content, isfile}
return num
end
local function reference(pdf, num)
local status = pdf[num]
if status == delayed then
local saved = pdf[-num]
pdf[-num] = nil
pdf[num] = assigned
return saved[1](pdf, num, table.unpack(saved, 2))
elseif status == assigned or not status then
error[[Invalid object]]
-- else -- Already written
end
end
local function getid(pdf)
local id = pdf[0] + 1
pdf[0] = id
pdf[id] = assigned
return id
end
local function trailer(pdf)
local nextid = getid(pdf)
local myoff = pdf.file:seek()
pdf[nextid] = {offset = myoff}
local linked = 0
local offsets = {}
for i=1,nextid do
local off = pdf[i].offset
if off then
offsets[i+1] = pack(">I1I3I1", 1, off, 0)
else
offsets[linked+1] = pack(">I1I3I1", 0, i, 255)
linked = i
end
end
offsets[linked+1] = '\0\0\0\0\255'
pdf[nextid] = assigned
-- TODO: Add an /ID according to 14.4
local info = pdf.info and string.format("/Info %i 0 R", pdf.info) or ""
stream(pdf, nextid, format([[/Type/XRef/Size %i/W[1 3 1]/Root %i 0 R%s]], nextid+1, pdf.root, info), table.concat(offsets))
pdf.file:write('startxref\n', myoff, '\n%%EOF')
end
local function close(pdf)
trailer(pdf)
local size = pdf.file:seek()
if #pdf.version ~= 3 then
error[[Invalid PDF version]]
end
pdf.file:seek('set', 5)
pdf.file:write(pdf.version)
pdf.file:close()
return size
end
local pagetree = require'luametalatex-pdf-pagetree'
local pdfmeta = {
close = close,
getobj = getid,
indirect = indirect,
stream = stream,
newpage = pagetree.newpage,
writepages = pagetree.write,
delayed = delay,
delayedstream = delayedstream,
reference = reference,
written = written,
}
pdfmeta.__index = pdfmeta
local function open(filename)
local file = io.open(filename, 'w')
file:write"%PDF-X.X\n%🖋\n"
return setmetatable({file = file, version = '1.7', [0] = 0, pages = {}}, pdfmeta)
end
return {
open = open,
}