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