]> mj.ucw.cz Git - paperjam.git/blob - pdf.cc
TODO: a4r paper
[paperjam.git] / pdf.cc
1 /*
2  *      PaperJam -- Low-level handling of PDFs
3  *
4  *      (c) 2018--2022 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <cassert>
8 #include <cstdlib>
9 #include <cstdio>
10 #include <cstring>
11 #include <unistd.h>
12 #include <sys/wait.h>
13
14 #include "jam.h"
15
16 #include <qpdf/QPDFWriter.hh>
17
18 static QPDF in_pdf;
19 static QPDF out_pdf;
20
21 static void do_recalc_bbox(vector<page *> &pages, const char *in_name);
22
23 string out_context::new_resource(const string type)
24 {
25   return "/" + type + to_string(++res_cnt);
26 }
27
28 class in_page : public page {
29   QPDFObjectHandle pdf_page;
30   QPDFObjectHandle xobject;
31 public:
32   BBox media_box;
33   void render(out_context *out, pdf_matrix xform);
34   void debug_dump() { debug("Input page %d", index); }
35   in_page(QPDFObjectHandle inpg, int idx);
36   int get_rotate();
37 };
38
39 in_page::in_page(QPDFObjectHandle inpg, int idx)
40 {
41   pdf_page = inpg;
42   xobject = QPDFObjectHandle::newNull();
43   index = idx;
44
45   media_box = BBox(inpg.getKey("/MediaBox"));
46   width = media_box.width();
47   height = media_box.height();
48
49   QPDFObjectHandle art_box = inpg.getKey("/ArtBox");
50   if (art_box.isNull())
51     art_box = inpg.getKey("/CropBox");
52   if (art_box.isNull())
53     image_box = BBox(width, height);
54   else
55     {
56       image_box = BBox(art_box);
57       image_box.x_min -= media_box.x_min;
58       image_box.x_max -= media_box.x_min;
59       image_box.y_min -= media_box.y_min;
60       image_box.y_max -= media_box.y_min;
61     }
62 }
63
64 void in_page::render(out_context *out, pdf_matrix xform)
65 {
66   // Convert page to xobject
67   if (xobject.isNull())
68     xobject = out->pdf->makeIndirectObject( page_to_xobject(out->pdf, out->pdf->copyForeignObject(pdf_page)) );
69   string xobj_res = out->new_resource("XO");
70   out->xobjects.replaceKey(xobj_res, xobject);
71
72   pdf_matrix m;
73   m.shift(-media_box.x_min, -media_box.y_min);
74   m.concat(xform);
75
76   out->contents += "q " + m.to_string() + " cm " + xobj_res + " Do Q ";
77 }
78
79 int in_page::get_rotate()
80 {
81   QPDFObjectHandle rotate = pdf_page.getKey("/Rotate");
82   if (rotate.isNull())
83     return 0;
84   else if (rotate.isInteger())
85     {
86       long long deg = rotate.getIntValue();
87       if (deg < 0 || deg >= 360 || deg % 90)
88         {
89           warn("Page #%d: /Rotate must be 0, 90, 180 or 270", index);
90           return 0;
91         }
92       else
93         return deg;
94     }
95   else
96     {
97       warn("Page #%d: /Rotate is not an integer", index);
98       return 0;
99     }
100 }
101
102 void debug_pages(vector<page *> &pages)
103 {
104   if (!debug_level)
105     return;
106
107   for (auto pg: pages)
108     {
109       debug("Page #%d: media[%.3f %.3f] image[%.3f %.3f %.3f %.3f][%.3f %.3f]",
110         pg->index,
111         pg->width, pg->height,
112         pg->image_box.x_min, pg->image_box.y_min, pg->image_box.x_max, pg->image_box.y_max,
113         pg->image_box.width(), pg->image_box.height());
114       if (debug_level > 2)
115         {
116           debug_indent += 4;
117           pg->debug_dump();
118           debug_indent -= 4;
119         }
120     }
121 }
122
123 static vector<page *> apply_input_xforms(vector<page *> in_pages)
124 {
125   vector<page *> out_pages;
126
127   for (auto pg: in_pages)
128     {
129       in_page * in_pg = dynamic_cast<in_page *>(pg);
130       if (in_pg)
131         {
132           int deg = in_pg->get_rotate();
133           if (deg)
134             pg = new xform_page(pg, "/Rotate", pdf_rotation_matrix(deg, pg->width, pg->height));
135         }
136       out_pages.push_back(pg);
137     }
138
139   return out_pages;
140 }
141
142 vector<page *> run_command_list(list<cmd *> &cmds, vector<page *> &pages)
143 {
144   debug("# Input");
145   debug_pages(pages);
146
147   for (auto c: cmds)
148     {
149       debug("# Executing %s", c->def->name);
150       debug_indent += 4;
151       try
152         {
153           pages = c->exec->process(pages);
154         }
155       catch (exception &e)
156         {
157           die("Error in %s: %s", c->def->name, e.what());
158         }
159       debug_indent -= 4;
160       debug_pages(pages);
161     }
162
163   return pages;
164 }
165
166 static void make_info_dict()
167 {
168   // Create info dictionary if it did not exist yet
169   QPDFObjectHandle trailer = out_pdf.getTrailer();
170   QPDFObjectHandle info = trailer.getKey("/Info");
171   if (info.isNull())
172     {
173       info = QPDFObjectHandle::newDictionary();
174       trailer.replaceKey("/Info", info);
175     }
176   else
177     assert(info.isDictionary());
178
179   info.replaceKey("/Producer", unicode_string("PaperJam"));
180
181   // Copy entries from the source file's info dictionary
182   QPDFObjectHandle orig_trailer = in_pdf.getTrailer();
183   QPDFObjectHandle orig_info = orig_trailer.getKey("/Info");
184   if (!orig_info.isNull())
185     {
186       const string to_copy[] = { "/Title", "/Author", "/Subject", "/Keywords", "/Creator", "/CreationDate" };
187       for (string key: to_copy)
188         info.replaceOrRemoveKey(key, orig_info.getKey(key));
189     }
190 }
191
192 void process(list<cmd *> &cmds)
193 {
194   debug("### Reading input");
195   in_pdf.processFile(in_name);
196   in_pdf.pushInheritedAttributesToPage();
197   out_pdf.emptyPDF();
198
199   vector<QPDFObjectHandle> const &in_pages = in_pdf.getAllPages();
200   vector<page *> pages;
201
202   QPDFObjectHandle page_copy = out_pdf.copyForeignObject(in_pages[0]);
203
204   int cnt = 0;
205   for (auto inpg: in_pages)
206     pages.push_back(new in_page(inpg, ++cnt));
207
208   if (recalc_bbox)
209     do_recalc_bbox(pages, in_name);
210
211   if (!no_auto_transforms)
212     {
213       debug("### Applying input transforms");
214       pages = apply_input_xforms(pages);
215     }
216
217   debug("### Running commands");
218   pages = run_command_list(cmds, pages);
219
220   debug("### Writing output");
221   int out_page = 0;
222   for (auto pg: pages)
223     {
224       ++out_page;
225       if (debug_level > 1)
226         {
227           debug("Page #%d", out_page);
228           debug_indent += 4;
229           pg->debug_dump();
230           debug_indent -= 4;
231         }
232
233       out_context out;
234       out.pdf = &out_pdf;
235       out.resources = QPDFObjectHandle::newDictionary();
236       out.resources.replaceKey("/ProcSet", QPDFObjectHandle::parse("[/PDF]"));
237       out.xobjects = QPDFObjectHandle::newDictionary();
238       out.egstates = QPDFObjectHandle::newDictionary();
239       pg->render(&out, pdf_matrix());
240
241       QPDFObjectHandle contents = QPDFObjectHandle::newStream(&out_pdf, out.contents);
242
243       // Create the page object
244       QPDFObjectHandle out_page = out_pdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
245       out_page.replaceKey("/Type", QPDFObjectHandle::newName("/Page"));
246       out_page.replaceKey("/MediaBox", BBox(pg->width, pg->height).to_array());
247       // FIXME:
248       // out_page.replaceKey("/CropBox", pg->image_box.to_array());
249       out_page.replaceKey("/Contents", contents);
250       if (!out.xobjects.getKeys().empty())
251         out.resources.replaceKey("/XObject", out.xobjects);
252       if (!out.egstates.getKeys().empty())
253         out.resources.replaceKey("/ExtGState", out.egstates);
254       out_page.replaceKey("/Resources", out.resources);
255       out_pdf.addPage(out_page, false);
256     }
257
258   // Produce info dictionary
259   make_info_dict();
260
261   // Write the output file
262   QPDFWriter writer(out_pdf, out_name);
263   writer.write();
264   debug("### Done");
265 }
266
267 /*** Re-calculation of bboxes ***/
268
269 vector<BBox> gs_bboxes(const char *in)
270 {
271   int pipes[2];
272   if (pipe(pipes) < 0)
273     die("Cannot create pipe: %m");
274
275   pid_t pid = fork();
276   if (pid < 0)
277     die("Cannot fork: %m");
278
279   if (!pid)
280     {
281       close(pipes[0]);
282       dup2(pipes[1], 1);
283       dup2(pipes[1], 2);
284       close(pipes[1]);
285       execlp("gs", "gs", "-sDEVICE=bbox", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-q", in, NULL);
286       die("Cannot execute gs: %m");
287     }
288
289   close(pipes[1]);
290   FILE *f = fdopen(pipes[0], "r");
291   if (!f)
292     die("fdopen failed: %m");
293
294   char line[1024];
295   vector<BBox> bboxes;
296   while (fgets(line, sizeof(line), f))
297     {
298       char *eol = strchr(line, '\n');
299       if (!eol)
300         die("Ghostscript produced too long lines");
301       *eol = 0;
302
303       if (!strncmp(line, "%%HiResBoundingBox: ", 20))
304         {
305           double x1, y1, x2, y2;
306           if (sscanf(line+20, "%lf%lf%lf%lf", &x1, &y1, &x2, &y2) != 4)
307             die("Cannot parse Ghostscript output: %s", line);
308           bboxes.push_back(BBox(x1, y1, x2, y2));
309         }
310       else if (line[0] != '%')
311         fprintf(stderr, "%s\n", line);
312     }
313   fclose(f);
314
315   int stat;
316   if (waitpid(pid, &stat, 0) < 0)
317     die("wait failed: %m");
318   if (!WIFEXITED(stat) || WEXITSTATUS(stat))
319     die("Ghostscript failed");
320
321   return bboxes;
322 }
323
324 static void do_recalc_bbox(vector<page *> &pages, const char *in_name)
325 {
326   debug("Calling Ghostscript to re-calculate bounding boxes");
327   vector<BBox> bboxes = gs_bboxes(in_name);
328   if (pages.size() != bboxes.size())
329     die("Ghostscript failed to produce the right number of bboxes");
330
331   for (size_t i=0; i<pages.size(); i++)
332     pages[i]->image_box = bboxes[i];
333 }
334
335 // Transformed page
336
337 xform_page::xform_page(page *p, const char *desc, pdf_matrix xf)
338 {
339   orig_page = p;
340   index = p->index;
341   description = desc;
342   xform = xf;
343
344   BBox media(p->width, p->height);
345   media.transform(xf);
346   width = media.width();
347   height = media.height();
348
349   image_box = p->image_box;
350   image_box.transform(xf);
351 }
352
353 void xform_page::debug_dump()
354 {
355   debug("Transform (%s): [%s]", description, xform.to_string().c_str());
356   orig_page->debug_dump();
357 }
358
359 void xform_page::render(out_context *out, pdf_matrix parent_xform)
360 {
361   orig_page->render(out, xform * parent_xform);
362 }