-
Notifications
You must be signed in to change notification settings - Fork 3
/
luametalatex-pdf.lua
179 lines (178 loc) · 5.34 KB
/
luametalatex-pdf.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
local readfile = require'luametalatex-readfile'
local format = string.format
local byte = string.byte
local pack = string.pack
local error = error
local pairs = pairs
local setmetatable = setmetatable
local assigned = {}
local delayed = {}
local compress = xzip.compress
local pdfvariable = pdf.variable
local digest = sha2.digest256
-- 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 or uncompressed stream.
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 file <close> = readfile('data', content, nil)
content = file()
end
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))
pdf.file:write(content)
pdf.file:write'\nendstream\nendobj\n'
return num
end
local function delayedstream(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] = delayed
pdf[-num] = {stream, dict, content, isfile, raw}
return num
end
local function indirect(pdf, num, content, isfile, objstream)
if not num then num = pdf:getobj() end
if pdf[num] ~= assigned then
error[[Invalid object]]
end
if isfile then
local file <close> = readfile('data', content, nil)
content = file()
end
if objstream ~= false and pdfvariable.objcompresslevel ~= 0 then
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'\nendobj\n'
end
return num
end
local function delay(pdf, num, content, isfile, objstream)
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, objstream}
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 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
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
offsets[i+1] = pack(">I1I3I2", 1, off, 0)
else
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
end
end
offsets[linked+1] = '\0\0\0\0\255\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 2]/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,
reservepage = pagetree.reservepage,
writepages = pagetree.write,
delayed = delay,
delayedstream = delayedstream,
reference = reference,
written = written,
}
pdfmeta.__index = pdfmeta
local function open(filename)
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
file:write"%PDF-X.X\n%\xC1\xAC\xC1\xB4\n"
return setmetatable({file = file, version = '1.7', [0] = 0, pages = {}, objstream = {}}, pdfmeta)
end
return {
open = open,
}