]> mj.ucw.cz Git - paperjam.git/blob - pdf-tools.cc
Rudimentary reading and writing PDFs
[paperjam.git] / pdf-tools.cc
1 /*
2  *      Auxiliary functions for processing PDF files
3  *
4  *      (c) 2018 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <cstdarg>
8 #include <cstdio>
9 #include <cstdlib>
10
11 #include <iconv.h>
12
13 using namespace std;
14
15 #include "pdf-tools.h"
16
17 #include <qpdf/QUtil.hh>
18 #include <qpdf/Pl_Concatenate.hh>
19
20 /*** Messages ***/
21
22 int debug_mode;
23
24 void debug(const char *msg, ...)
25 {
26         if (!debug_mode)
27                 return;
28         va_list args;
29         va_start(args, msg);
30         vfprintf(stderr, msg, args);
31         fputc('\n', stderr);
32         va_end(args);
33 }
34
35 void warn(const char *msg, ...)
36 {
37         va_list args;
38         va_start(args, msg);
39         fprintf(stderr, "WARNING: ");
40         vfprintf(stderr, msg, args);
41         fputc('\n', stderr);
42         va_end(args);
43 }
44
45 void die(const char *msg, ...)
46 {
47         va_list args;
48         va_start(args, msg);
49         fprintf(stderr, "ERROR: ");
50         vfprintf(stderr, msg, args);
51         fputc('\n', stderr);
52         va_end(args);
53         exit(1);
54 }
55
56 void bad(const char *msg, ...)
57 {
58         va_list args;
59         va_start(args, msg);
60         char buf[1024];
61         vsnprintf(buf, sizeof(buf), msg, args);
62         va_end(args);
63
64         printf("error: %s\n", buf);
65         die("BAD: %s", buf);
66 }
67
68 /*** Transformation matrices ***/
69
70 // Construct string representation of a transformation matrix
71 string pdf_matrix::to_string() {
72         string s;
73         for (int i=0; i<6; i++) {
74                 if (i)
75                         s += " ";
76                 char buf[16];
77                 snprintf(buf, sizeof(buf), "%.3f", m[i]);
78                 s += buf;
79         }
80         return s;
81 }
82
83 /*** Bounding boxes ***/
84
85 QPDFObjectHandle BBox::to_array()
86 {
87         QPDFObjectHandle a = QPDFObjectHandle::newArray();
88         a.appendItem(QPDFObjectHandle::newReal(x_min, 1));
89         a.appendItem(QPDFObjectHandle::newReal(y_min, 1));
90         a.appendItem(QPDFObjectHandle::newReal(x_max, 1));
91         a.appendItem(QPDFObjectHandle::newReal(y_max, 1));
92         return a;
93 }
94
95 bool BBox::parse(QPDFObjectHandle h)
96 {
97         if (!h.isArray() || h.getArrayNItems() != 4)
98                 return false;
99         double x[4];
100         for (int i=0; i<4; i++) {
101                 QPDFObjectHandle item = h.getArrayItem(i);
102                 if (!item.isNumber())
103                         return false;
104                 x[i] = item.getNumericValue();
105         }
106         x_min = x[0];
107         y_min = x[1];
108         x_max = x[2];
109         y_max = x[3];
110         return true;
111 }
112
113 /*** Unicode strings ***/
114
115 // Construct PDF representation of a UTF-8 string
116 QPDFObjectHandle unicode_string(string s)
117 {
118         // If it is ASCII only, use the string directly
119         bool ascii_only = true;
120         for (char c: s)
121                 if (c < 0x20 || c > 0x7e)
122                         ascii_only = false;
123         if (ascii_only)
124                 return QPDFObjectHandle::newString(s);
125
126         // Use iconv to convert the string to big-endian UTF-16
127         iconv_t conv = iconv_open("UTF-16BE", "UTF-8");
128         if (conv == (iconv_t) -1)
129                 die("Cannot initialize iconv: %m");
130
131         char *in_ptr = (char *) s.c_str();      // Work around bad API of iconv()
132         size_t in_len = strlen(in_ptr);
133         size_t out_len = 2*in_len + 2;          // Worst case (including the BOM)
134         char out_buf[out_len];
135         char *out_ptr = out_buf;
136         size_t res = iconv(conv, &in_ptr, &in_len, &out_ptr, &out_len);
137         if (res == (size_t) -1)
138                 die("iconv failed: %m");
139         if (in_len)
140                 die("iconv stopped before the end of input");
141
142         iconv_close(conv);
143
144         // Package UTF-16 in a PDF string
145         string out;
146         out += 0xfe;
147         out += 0xff;
148         for (char *p = out_buf; p < out_ptr; p++)
149                 out += *p;
150         return QPDFObjectHandle::newString(out);
151 }
152
153 /*** Conversion of pages to XObjects ***/
154
155 static BBox get_trim_box(QPDFObjectHandle page)
156 {
157         static const char * const boxes[] = { "/TrimBox", "/CropBox", "/MediaBox", NULL };
158         for (int i=0; boxes[i]; i++)
159                 if (page.hasKey(boxes[i]))
160                         return BBox(page.getKey(boxes[i]));
161         warn("Page has no trimbox, falling back to A4");
162         return BBox(0, 0, A4_WIDTH, A4_HEIGHT);
163 }
164
165 /* Conversion of pages to XObjects is inspired by CUPS's pdftopdf filter. */
166 class CombineFromContents_Provider : public QPDFObjectHandle::StreamDataProvider {
167         private:
168                 vector<QPDFObjectHandle> contents;
169         public:
170                 CombineFromContents_Provider(const vector<QPDFObjectHandle> &contents) : contents(contents) { }
171                 void provideStreamData(int objid UNUSED, int generation UNUSED, Pipeline* pipeline) {
172                         Pl_Concatenate concat("concat", pipeline);
173                         for (int i=0; i < (int)contents.size(); i++)
174                                 contents[i].pipeStreamData(&concat, true, false, false);
175                         concat.manualFinish();
176                 }
177 };
178
179 QPDFObjectHandle page_to_xobject(QPDF *out, QPDFObjectHandle page)
180 {
181         page.assertPageObject();
182
183         QPDFObjectHandle xo_stream = QPDFObjectHandle::newStream(out);
184         QPDFObjectHandle xo_dict = xo_stream.getDict();
185
186         xo_dict.replaceKey("/Type", QPDFObjectHandle::newName("/XObject"));
187         xo_dict.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form"));
188         xo_dict.replaceKey("/FormType", QPDFObjectHandle::newInteger(1));
189
190         BBox box = get_trim_box(page);
191         xo_dict.replaceKey("/BBox", box.to_array());
192
193         xo_dict.replaceKey("/Resources", page.getKey("/Resources"));
194         if (page.hasKey("/Group"))
195                 xo_dict.replaceKey("/Group", page.getKey("/Group"));
196
197         if (page.hasKey("/UserUnit")) {
198                 double u = page.getKey("/UserUnit").getNumericValue();
199                 QPDFObjectHandle m = QPDFObjectHandle::newArray();
200                 m.appendItem(QPDFObjectHandle::newReal(u, 3));
201                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
202                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
203                 m.appendItem(QPDFObjectHandle::newReal(u, 3));
204                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
205                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
206                 xo_dict.replaceKey("/Matrix", m);
207         }
208
209         vector<QPDFObjectHandle> contents = page.getPageContents();
210         auto ph = PointerHolder<QPDFObjectHandle::StreamDataProvider>(new CombineFromContents_Provider(contents));
211         xo_stream.replaceStreamData(ph, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull());
212         return xo_stream;
213 }