git @ Cat's Eye Technologies pdf.lua / 60fa885
Initial import of source code and license; expand on README. Chris Pressey 8 years ago
4 changed file(s) with 615 addition(s) and 1 deletion(s). Raw diff Collapse all Expand all
00 pdf.lua
11 =======
22
3 An "as-is" pure Lua library for low-level generation of PDF files [Public domain]
3 This is a pure Lua library for low-level generation of PDF files.
4
5 This is code I wrote, oh gosh, about 10 years ago now, and I have no plans to
6 develop it further. It is probably more valuable as a demonstration that it's
7 really not that difficult to create a PDF file, than as a serious piece of
8 equipment. However, I just tried the test script with Lua 5.1.5, using
9 Firefox 33.0 as the PDF viewer, and it still works.
10
11 This work is in the public domain; see the file UNLICENSE for details.
0 This is free and unencumbered software released into the public domain.
1
2 Anyone is free to copy, modify, publish, use, compile, sell, or
3 distribute this software, either in source code form or as a compiled
4 binary, for any purpose, commercial or non-commercial, and by any
5 means.
6
7 In jurisdictions that recognize copyright laws, the author or authors
8 of this software dedicate any and all copyright interest in the
9 software to the public domain. We make this dedication for the benefit
10 of the public at large and to the detriment of our heirs and
11 successors. We intend this dedication to be an overt act of
12 relinquishment in perpetuity of all present and future rights to this
13 software under copyright law.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
20 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
21 OTHER DEALINGS IN THE SOFTWARE.
22
23 For more information, please refer to <http://unlicense.org/>
0
1 PDF = {}
2 PDF.new = function()
3 local pdf = {} -- instance variable
4 local page = {} -- array of page descriptors
5 local object = {} -- array of object contents
6 local xref_table_offset -- byte offset of xref table
7
8 local catalog_obj -- global catalog object
9 local pages_obj -- global pages object
10 local procset_obj -- global procset object
11
12 --
13 -- Private functions.
14 --
15
16 local add = function(obj)
17 table.insert(object, obj)
18 obj.number = table.getn(object)
19 return obj
20 end
21
22 local get_ref = function(obj)
23 return string.format("%d 0 R", obj.number)
24 end
25
26 local write_object
27 local write_direct_object
28 local write_indirect_object
29
30 write_object = function(fh, obj)
31 if type(obj) == "table" and obj.datatype == "stream" then
32 write_indirect_object(fh, obj)
33 else
34 write_direct_object(fh, obj)
35 end
36 end
37
38 write_direct_object = function(fh, obj)
39 if type(obj) ~= "table" then
40 fh:write(obj .. "\n")
41 elseif obj.datatype == "dictionary" then
42 local k, v
43
44 fh:write("<<\n")
45 for k, v in pairs(obj.contents) do
46 fh:write(string.format("/%s ", k))
47 write_object(fh, v)
48 end
49 fh:write(">>\n")
50 elseif obj.datatype == "array" then
51 local v
52
53 fh:write("[\n")
54 for _, v in ipairs(obj.contents) do
55 write_object(fh, v)
56 end
57 fh:write("]\n")
58 elseif obj.datatype == "stream" then
59 local len = 0
60
61 if type(obj.contents) == "string" then
62 len = string.len(obj.contents)
63 else -- assume array
64 local i, str
65
66 for i, str in ipairs(obj.contents) do
67 len = len + string.len(str) + 1
68 end
69 end
70
71 fh:write(string.format("<< /Length %d >>\n", len))
72 fh:write("stream\n")
73
74 if type(obj.contents) == "string" then
75 fh:write(obj.contents)
76 else -- assume array
77 local i, str
78
79 for i, str in ipairs(obj.contents) do
80 fh:write(str)
81 fh:write("\n")
82 end
83 end
84
85 fh:write("endstream\n")
86 end
87 end
88
89 write_indirect_object = function(fh, obj)
90 obj.offset = fh:seek()
91 fh:write(string.format("%d %d obj\n", obj.number, 0))
92 write_direct_object(fh, obj)
93 fh:write("endobj\n")
94 end
95
96 local write_header = function(fh)
97 fh:write("%PDF-1.0\n")
98 end
99
100 local write_body = function(fh)
101 local i, obj
102
103 for i, obj in ipairs(object) do
104 write_indirect_object(fh, obj)
105 end
106 end
107
108 local write_xref_table = function(fh)
109 local i, obj
110
111 xref_table_offset = fh:seek()
112 fh:write("xref\n")
113 fh:write(string.format("%d %d\n", 1, table.getn(object)))
114 for i, obj in ipairs(object) do
115 fh:write(
116 string.format("%010d %05d n \n", obj.offset, 0)
117 )
118 end
119 end
120
121 local write_trailer = function(fh)
122 fh:write("trailer\n")
123 fh:write("<<\n")
124 fh:write(string.format("/Size %d\n", table.getn(object)))
125 fh:write("/Root " .. get_ref(catalog_obj) .. "\n")
126 fh:write(">>\n")
127 fh:write("startxref\n")
128 fh:write(string.format("%d\n", xref_table_offset))
129 fh:write("%%EOF\n")
130 end
131
132 --
133 -- Instance methods.
134 --
135
136 pdf.new_font = function(pdf, tab)
137 local subtype = tab.subtype or "Type1"
138 local name = tab.name or "Helvetica"
139 local font_obj = add {
140 datatype = "dictionary",
141 contents = {
142 Type = "/Font",
143 Subtype = "/" .. subtype,
144 BaseFont = "/" .. name,
145 }
146 }
147 return font_obj
148 end
149
150 pdf.new_page = function(pdf)
151 local pg = {} -- instance variable
152 local contents = {} -- array of operation strings
153 local used_font = {} -- fonts used on this page
154
155 --
156 -- Private functions.
157 --
158
159 local use_font = function(font_obj)
160 local i, f
161
162 for i, f in ipairs(used_font) do
163 if font_obj == f then
164 return "/F" .. i
165 end
166 end
167
168 table.insert(used_font, font_obj)
169 return "/F" .. table.getn(used_font)
170 end
171
172 --
173 -- Instance methods.
174 --
175
176 --
177 -- Text functions.
178 --
179
180 pg.begin_text = function(pg)
181 table.insert(contents, "BT")
182 end
183
184 pg.end_text = function(pg)
185 table.insert(contents, "ET")
186 end
187
188 pg.set_font = function(pg, font_obj, size)
189 table.insert(contents,
190 string.format("%s %f Tf",
191 use_font(font_obj), size)
192 )
193 end
194
195 pg.set_text_pos = function(pg, x, y)
196 table.insert(contents,
197 string.format("%f %f Td", x, y)
198 )
199 end
200
201 pg.show = function(pg, str)
202 table.insert(contents,
203 string.format("(%s) Tj", str)
204 )
205 end
206
207 pg.set_char_spacing = function(pg, spc)
208 table.insert(contents,
209 string.format("%f Tc", spc)
210 )
211 end
212
213 --
214 -- Graphics - path drawing functions.
215 --
216
217 pg.moveto = function(pg, x, y)
218 table.insert(contents,
219 string.format("%f %f m", x, y)
220 )
221 end
222
223 pg.lineto = function(pg, x, y)
224 table.insert(contents,
225 string.format("%f %f l", x, y)
226 )
227 end
228
229 pg.curveto = function(pg, x1, y1, x2, y2, x3, y3)
230 local str
231
232 if x3 and y3 then
233 str = string.format("%f %f %f %f %f %f c",
234 x1, y1, x2, y2, x3, y3)
235 else
236 str = string.format("%f %f %f %f v",
237 x1, y1, x2, y2)
238 end
239
240 table.insert(contents, str)
241 end
242
243 pg.rectangle = function(pg, x, y, w, h)
244 table.insert(contents,
245 string.format("%f %f %f %f re",
246 x, y, w, h)
247 )
248 end
249
250 --
251 -- Graphics - colours.
252 --
253
254 pg.setgray = function(pg, which, gray)
255 assert(which == "fill" or which == "stroke")
256 assert(gray >= 0 and gray <= 1)
257 if which == "fill" then
258 table.insert(contents,
259 string.format("%d g", gray)
260 )
261 else
262 table.insert(contents,
263 string.format("%d G", gray)
264 )
265 end
266 end
267
268 pg.setrgbcolor = function(pg, which, r, g, b)
269 assert(which == "fill" or which == "stroke")
270 assert(r >= 0 and r <= 1)
271 assert(g >= 0 and g <= 1)
272 assert(b >= 0 and b <= 1)
273 if which == "fill" then
274 table.insert(contents,
275 string.format("%f %f %f rg", r, g, b)
276 )
277 else
278 table.insert(contents,
279 string.format("%f %f %f RG", r, g, b)
280 )
281 end
282 end
283
284 pg.setcmykcolor = function(pg, which, c, m, y, k)
285 assert(which == "fill" or which == "stroke")
286 assert(c >= 0 and c <= 1)
287 assert(m >= 0 and m <= 1)
288 assert(y >= 0 and y <= 1)
289 assert(k >= 0 and k <= 1)
290 if which == "fill" then
291 table.insert(contents,
292 string.format("%f %f %f %f k", c, m, y, k)
293 )
294 else
295 table.insert(contents,
296 string.format("%f %f %f %f K", c, m, y, k)
297 )
298 end
299 end
300
301 --
302 -- Graphics - line options.
303 --
304
305 pg.setflat = function(pg, i)
306 assert(i >= 0 and i <= 100)
307 table.insert(contents,
308 string.format("%d i", i)
309 )
310 end
311
312 pg.setlinecap = function(pg, j)
313 assert(j == 0 or j == 1 or j == 2)
314 table.insert(contents,
315 string.format("%d J", j)
316 )
317 end
318
319 pg.setlinejoin = function(pg, j)
320 assert(j == 0 or j == 1 or j == 2)
321 table.insert(contents,
322 string.format("%d j", j)
323 )
324 end
325
326 pg.setlinewidth = function(pg, w)
327 table.insert(contents,
328 string.format("%d w", w)
329 )
330 end
331
332 pg.setmiterlimit = function(pg, m)
333 assert(m >= 1)
334 table.insert(contents,
335 string.format("%d M", m)
336 )
337 end
338
339 pg.setdash = function(pg, array, phase)
340 local str = ""
341 local v
342
343 for _, v in ipairs(array) do
344 str = str .. v .. " "
345 end
346
347 table.insert(contents,
348 string.format("[%s] %d d", str, phase)
349 )
350 end
351
352 --
353 -- Graphics - path-terminating functions.
354 --
355
356 pg.stroke = function(pg)
357 table.insert(contents, "S")
358 end
359
360 pg.closepath = function(pg)
361 table.insert(contents, "h")
362 end
363
364 pg.fill = function(pg)
365 table.insert(contents, "f")
366 end
367
368 pg.newpath = function(pg)
369 table.insert(contents, "n")
370 end
371
372 pg.clip = function(pg) -- no effect until next newpath
373 table.insert(contents, "W")
374 end
375
376 --
377 -- Graphics - state save/restore.
378 --
379
380 pg.save = function(pg)
381 table.insert(contents, "q")
382 end
383
384 pg.restore = function(pg)
385 table.insert(contents, "Q")
386 end
387
388 --
389 -- Graphics - CTM functions.
390 --
391 pg.transform = function(pg, a, b, c, d, e, f) -- aka concat
392 table.insert(contents,
393 string.format("%f %f %f %f %f %f cm",
394 a, b, c, d, e, f)
395 )
396 end
397
398 pg.translate = function(pg, x, y)
399 pg:transform(1, 0, 0, 1, x, y)
400 end
401
402 pg.scale = function(pg, x, y)
403 if not y then y = x end
404 pg:transform(x, 0, 0, y, 0, 0)
405 end
406
407 pg.rotate = function(pg, theta)
408 local c, s = math.cos(theta), math.sin(theta)
409 pg:transform(c, s, -1 * s, c, 0, 0)
410 end
411
412 pg.skew = function(pg, tha, thb)
413 local tana, tanb = math.tan(tha), math.tan(thb)
414 pg:transform(1, tana, tanb, 1, 0, 0)
415 end
416
417 pg.add = function(pg)
418 local contents_obj, this_obj, resources
419 local i, font_obj
420
421 contents_obj = add {
422 datatype = "stream",
423 contents = contents
424 }
425
426 resources = {
427 datatype = "dictionary",
428 contents = {
429 Font = {
430 datatype = "dictionary",
431 contents = {}
432 },
433 ProcSet = get_ref(procset_obj)
434 }
435 }
436
437 for i, font_obj in ipairs(used_font) do
438 resources.contents.Font.contents["F" .. i] =
439 get_ref(font_obj)
440 end
441
442 this_obj = add {
443 datatype = "dictionary",
444 contents = {
445 Type = "/Page",
446 Parent = get_ref(pages_obj),
447 Contents = get_ref(contents_obj),
448 Resources = resources
449 }
450 }
451
452 table.insert(pages_obj.contents.Kids.contents,
453 get_ref(this_obj))
454 pages_obj.contents.Count = pages_obj.contents.Count + 1
455 end
456
457 table.insert(page, pg)
458 return pg
459 end
460
461 pdf.write = function(pdf, file)
462 local fh
463
464 if type(file) == "string" then
465 fh = assert(io.open(file, "w"))
466 else
467 fh = file
468 end
469
470 write_header(fh)
471 write_body(fh)
472 write_xref_table(fh)
473 write_trailer(fh)
474
475 fh:close()
476 end
477
478 -- initialize... add a few objects that we know will exist.
479 pages_obj = add {
480 datatype = "dictionary",
481 contents = {
482 Type = "/Pages",
483 Kids = {
484 datatype = "array",
485 contents = {}
486 },
487 Count = 0
488 }
489 }
490
491 catalog_obj = add {
492 datatype = "dictionary",
493 contents = {
494 Type = "/Catalog",
495 Pages = get_ref(pages_obj)
496 }
497 }
498
499 procset_obj = add {
500 datatype = "array",
501 contents = { "/PDF", "/Text" }
502 }
503
504 return pdf
505 end
0 require "pdf"
1
2 p = PDF.new()
3
4 helv = p:new_font{ name = "Helvetica" }
5 times = p:new_font{ name = "Times-Roman" }
6
7 page = p:new_page()
8
9 page:setrgbcolor("stroke", 0.5, 0, 1)
10 page:moveto(150, 250)
11 page:lineto(150, 350)
12 page:stroke()
13
14 page:setrgbcolor("stroke", 0, 1, 0.3)
15 page:moveto(250, 250)
16 page:lineto(250, 350)
17 page:stroke()
18
19 page:save()
20
21 page:begin_text()
22 page:set_font(helv, 24)
23 page:set_text_pos(100, 100)
24 page:show("Hello, world!")
25 page:end_text()
26
27 page:begin_text()
28 page:set_font(times, 12)
29
30 for i = 1, 40 do
31 page:save()
32 page:translate(400, 400)
33 page:rotate((3.14159 / 40) * i)
34 page:moveto(0, 0)
35 page:lineto(25, 0)
36 page:stroke()
37 page:set_text_pos(0, 0)
38 page:show("<<and goodbye>>")
39 page:restore()
40 end
41
42 page:end_text()
43
44 page:restore()
45
46 page2 = p:new_page()
47 page2:moveto(250, 250)
48 page2:lineto(350, 250)
49 page2:stroke()
50
51 page2:begin_text()
52 page2:set_font(times, 24)
53 page2:set_text_pos(250, 250)
54 page2:show("Text")
55 page2:end_text()
56
57 draw_circle = function(p, x, y, r)
58 local k = 0.5522847498 * r
59
60 p:moveto(x - r, y)
61 p:curveto(x - r, y + k, x - k, y + r, x, y + r)
62 p:curveto(x + k, y + r, x + r, y + k, x + r, y)
63 p:curveto(x + r, y - k, x + k, y - r, x, y - r)
64 p:curveto(x - k, y - r, x - r, y - k, x - r, y)
65
66 end
67
68 draw_circle(page2, 300, 400, 100)
69 page2:stroke()
70
71 page:add()
72 page2:add()
73
74 p:write("test.pdf")
75