2020-07-15 04:51:59 +02:00
local readfile = require ' luametalatex-readfile '
2019-07-17 21:14:34 +02:00
local format = string.format
local byte = string.byte
local pack = string.pack
local error = error
local pairs = pairs
local setmetatable = setmetatable
local assigned = { }
2020-05-31 09:30:49 +02:00
local delayed = { }
2020-06-13 14:34:53 +02:00
local compress = xzip.compress
local pdfvariable = pdf.variable
2020-08-13 22:24:13 +02:00
local digest = sha2.digest256
2020-06-03 23:59:59 +02:00
-- 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
2020-07-31 12:04:02 +02:00
-- raw: Pass on preencoded stream.
2020-06-07 22:42:53 +02:00
local function stream ( pdf , num , dict , content , isfile , raw )
2019-07-17 21:14:34 +02:00
if not num then num = pdf : getobj ( ) end
if pdf [ num ] ~= assigned then
error [[Invalid object]]
end
pdf [ num ] = { offset = pdf.file : seek ( ) }
2020-05-31 09:30:49 +02:00
if isfile then
2020-07-15 04:51:59 +02:00
local file < close > = readfile ( ' pdf_stream ' , content , nil )
content = file ( )
2020-05-31 09:30:49 +02:00
end
2020-06-13 14:34:53 +02:00
local level = not raw and pdfvariable.compresslevel or 0
local filter = ' '
if level > 0 then
local compressed = compress ( content , level )
if # compressed < # content + 19 then -- Filter has some overhead
filter = " /Filter/FlateDecode "
content = compressed
end
end
pdf.file : write ( format ( ' %i 0 obj \n <<%s%s/Length %i>>stream \n ' , num , dict , filter , # content ) )
2019-07-17 21:14:34 +02:00
pdf.file : write ( content )
pdf.file : write ' \n endstream \n endobj \n '
return num
end
2020-06-10 03:03:46 +02:00
local function delayedstream ( pdf , num , dict , content , isfile , raw )
2020-05-31 09:30:49 +02:00
if not num then num = pdf : getobj ( ) end
if pdf [ num ] ~= assigned then
error [[Invalid object]]
end
pdf [ num ] = delayed
2020-06-10 03:03:46 +02:00
pdf [ - num ] = { stream , dict , content , isfile , raw }
2020-05-31 09:30:49 +02:00
return num
end
2020-07-07 04:55:10 +02:00
local function indirect ( pdf , num , content , isfile , objstream )
2019-07-17 21:14:34 +02:00
if not num then num = pdf : getobj ( ) end
if pdf [ num ] ~= assigned then
error [[Invalid object]]
end
2020-05-31 09:30:49 +02:00
if isfile then
2020-07-15 04:51:59 +02:00
local file < close > = readfile ( ' pdf_dict ' , content , nil )
content = file ( )
2020-05-31 09:30:49 +02:00
end
2020-07-09 09:55:20 +02:00
if objstream ~= false and pdfvariable.objcompresslevel ~= 0 then
2020-07-07 04:55:10 +02:00
objstream = objstream or true
local objstr = pdf.objstream [ objstream ]
if not objstr then
objstr = { objnum = pdf : getobj ( ) , off = 0 , { } }
pdf.objstream [ objstream ] = objstr
end
local i = # objstr
pdf [ num ] = { objstr = objstr.objnum , i = i - 1 }
objstr [ 1 ] [ i ] = string.format ( " %i %i " , num , objstr.off )
objstr [ i + 1 ] = content
objstr.off = objstr.off + # content
else
pdf [ num ] = { offset = pdf.file : seek ( ) }
pdf.file : write ( format ( ' %i 0 obj \n ' , num ) )
pdf.file : write ( content )
pdf.file : write ' \n endobj \n '
end
2019-07-17 21:14:34 +02:00
return num
end
2020-06-06 04:09:56 +02:00
local function delay ( pdf , num , content , isfile )
2020-05-31 09:30:49 +02:00
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
2019-07-17 21:14:34 +02:00
local function getid ( pdf )
local id = pdf [ 0 ] + 1
pdf [ 0 ] = id
pdf [ id ] = assigned
return id
end
local function trailer ( pdf )
2020-07-07 04:55:10 +02:00
local linked = 0
for k , objstr in next , pdf.objstream do
objstr [ 1 ] = table.concat ( objstr [ 1 ] )
pdf : stream ( objstr.objnum , string.format ( " /Type/ObjStm/N %i/First %i " , # objstr - 1 , # objstr [ 1 ] ) , table.concat ( objstr ) )
end
2019-07-17 21:14:34 +02:00
local nextid = getid ( pdf )
local myoff = pdf.file : seek ( )
pdf [ nextid ] = { offset = myoff }
local offsets = { }
for i = 1 , nextid do
local off = pdf [ i ] . offset
if off then
2020-07-07 04:55:10 +02:00
offsets [ i + 1 ] = pack ( " >I1I3I2 " , 1 , off , 0 )
2019-07-17 21:14:34 +02:00
else
2020-07-07 04:55:10 +02:00
local objstr = pdf [ i ] . objstr
if objstr then
offsets [ i + 1 ] = pack ( " >I1I3I2 " , 2 , objstr , pdf [ i ] . i )
else
offsets [ linked + 1 ] = pack ( " >I1I3I2 " , 0 , i , 255 )
linked = i
end
2019-07-17 21:14:34 +02:00
end
end
2020-07-07 04:55:10 +02:00
offsets [ linked + 1 ] = ' \0 \0 \0 \0 \255 \255 '
2019-07-17 21:14:34 +02:00
pdf [ nextid ] = assigned
-- TODO: Add an /ID according to 14.4
2019-07-21 14:19:05 +02:00
local info = pdf.info and string.format ( " /Info %i 0 R " , pdf.info ) or " "
2020-07-07 04:55:10 +02:00
stream ( pdf , nextid , format ( [[/Type/XRef/Size %i/W[1 3 2]/Root %i 0 R%s]] , nextid + 1 , pdf.root , info ) , table.concat ( offsets ) )
2019-07-17 21:14:34 +02:00
pdf.file : write ( ' startxref \n ' , myoff , ' \n %%EOF ' )
end
local function close ( pdf )
trailer ( pdf )
2020-06-06 03:03:52 +02:00
local size = pdf.file : seek ( )
2019-07-17 21:14:34 +02:00
if # pdf.version ~= 3 then
error [[Invalid PDF version]]
end
pdf.file : seek ( ' set ' , 5 )
pdf.file : write ( pdf.version )
pdf.file : close ( )
2020-06-06 03:03:52 +02:00
return size
2019-07-17 21:14:34 +02:00
end
local pagetree = require ' luametalatex-pdf-pagetree '
local pdfmeta = {
close = close ,
getobj = getid ,
indirect = indirect ,
stream = stream ,
newpage = pagetree.newpage ,
2020-07-02 11:02:38 +02:00
reservepage = pagetree.reservepage ,
2019-07-17 21:14:34 +02:00
writepages = pagetree.write ,
2020-06-06 04:09:56 +02:00
delayed = delay ,
2020-05-31 09:30:49 +02:00
delayedstream = delayedstream ,
reference = reference ,
2020-06-03 23:59:59 +02:00
written = written ,
2019-07-17 21:14:34 +02:00
}
pdfmeta.__index = pdfmeta
local function open ( filename )
2020-07-12 19:30:16 +02:00
local file , msg = io.open ( filename , ' wb ' )
if not file then
tex.error ( ' Unable to open output file ' , string.format ( " Opening the output file %q failed. According to your system, the reason is: %q. If you continue, all output will be discarded. " , filename , msg ) )
file = assert ( io.tmpfile ( ) )
end
2020-07-12 02:15:08 +02:00
file : write " %PDF-X.X \n % \xC1 \xAC \xC1 \xB4 \n "
2020-07-07 04:55:10 +02:00
return setmetatable ( { file = file , version = ' 1.7 ' , [ 0 ] = 0 , pages = { } , objstream = { } } , pdfmeta )
2019-07-17 21:14:34 +02:00
end
return {
open = open ,
}