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