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