]> mj.ucw.cz Git - paperjam.git/blob - pdf-tools.cc
TODO: a4r paper
[paperjam.git] / pdf-tools.cc
1 /*
2  *      Auxiliary functions for processing PDF files
3  *
4  *      (c) 2018--2022 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <cstdio>
8 #include <cstdlib>
9 #include <cstring>
10
11 #include <iconv.h>
12
13 #include "jam.h"
14
15 #include <qpdf/QUtil.hh>
16 #include <qpdf/Pl_Concatenate.hh>
17
18 /*** Transformation matrices ***/
19
20 // Construct string representation of a transformation matrix
21 string pdf_matrix::to_string() {
22         string s;
23         for (int i=0; i<6; i++) {
24                 if (i)
25                         s += " ";
26                 s += pdf_coord(m[i], 6);
27         }
28         return s;
29 }
30
31 /*** Bounding boxes ***/
32
33 QPDFObjectHandle BBox::to_array()
34 {
35         QPDFObjectHandle a = QPDFObjectHandle::newArray();
36         a.appendItem(QPDFObjectHandle::newReal(x_min, 1));
37         a.appendItem(QPDFObjectHandle::newReal(y_min, 1));
38         a.appendItem(QPDFObjectHandle::newReal(x_max, 1));
39         a.appendItem(QPDFObjectHandle::newReal(y_max, 1));
40         return a;
41 }
42
43 string BBox::to_rect()
44 {
45         return
46                 pdf_coord(x_min) + " " +
47                 pdf_coord(y_min) + " " +
48                 pdf_coord(width()) + " " +
49                 pdf_coord(height());
50 }
51
52 bool BBox::parse(QPDFObjectHandle h)
53 {
54         if (!h.isArray() || h.getArrayNItems() != 4)
55                 return false;
56         double x[4];
57         for (int i=0; i<4; i++) {
58                 QPDFObjectHandle item = h.getArrayItem(i);
59                 if (!item.isNumber())
60                         return false;
61                 x[i] = item.getNumericValue();
62         }
63         x_min = x[0];
64         y_min = x[1];
65         x_max = x[2];
66         y_max = x[3];
67         return true;
68 }
69
70 BBox::BBox(QPDFObjectHandle box) {
71         if (!parse(box)) {
72                 warn("Invalid bounding box, falling back to A4");
73                 x_min = 0, x_max = A4_WIDTH;
74                 y_min = 0, y_max = A4_HEIGHT;
75         }
76 }
77
78 void BBox::transform(pdf_matrix &m)
79 {
80         m.apply(&x_min, &y_min);
81         m.apply(&x_max, &y_max);
82         if (x_max < x_min)
83                 swap(x_min, x_max);
84         if (y_max < y_min)
85                 swap(y_min, y_max);
86 }
87
88 BBox BBox::transformed(pdf_matrix &m)
89 {
90         BBox b = *this;
91         b.transform(m);
92         return b;
93 }
94
95 void BBox::join(BBox &with)
96 {
97         x_min = min(x_min, with.x_min);
98         x_max = max(x_max, with.x_max);
99         y_min = min(y_min, with.y_min);
100         y_max = max(y_max, with.y_max);
101 }
102
103 static double clamp(double x, double min, double max)
104 {
105         if (x < min)
106                 return min;
107         if (x > max)
108                 return max;
109         return x;
110 }
111
112 void BBox::intersect(BBox &with)
113 {
114         x_min = clamp(x_min, with.x_min, with.x_max);
115         x_max = clamp(x_max, with.x_min, with.x_max);
116         y_min = clamp(y_min, with.y_min, with.y_max);
117         y_max = clamp(y_max, with.y_min, with.y_max);
118 }
119
120 void BBox::enlarge(double by)
121 {
122         x_min -= by;
123         x_max += by;
124         y_min -= by;
125         y_max += by;
126 }
127
128 BBox BBox::enlarged(double by)
129 {
130         BBox b = *this;
131         b.enlarge(by);
132         return b;
133 }
134
135 /*** Unicode strings ***/
136
137 // Construct PDF representation of a UTF-8 string
138 QPDFObjectHandle unicode_string(string s)
139 {
140         // If it is ASCII only, use the string directly
141         bool ascii_only = true;
142         for (char c: s)
143                 if (c < 0x20 || c > 0x7e)
144                         ascii_only = false;
145         if (ascii_only)
146                 return QPDFObjectHandle::newString(s);
147
148         // Use iconv to convert the string to big-endian UTF-16
149         iconv_t conv = iconv_open("UTF-16BE", "UTF-8");
150         if (conv == (iconv_t) -1)
151                 die("Cannot initialize iconv: %m");
152
153         char *in_ptr = (char *) s.c_str();      // Work around bad API of iconv()
154         size_t in_len = strlen(in_ptr);
155         size_t out_len = 2*in_len + 2;          // Worst case (including the BOM)
156         char out_buf[out_len];
157         char *out_ptr = out_buf;
158         size_t res = iconv(conv, &in_ptr, &in_len, &out_ptr, &out_len);
159         if (res == (size_t) -1)
160                 die("iconv failed: %m");
161         if (in_len)
162                 die("iconv stopped before the end of input");
163
164         iconv_close(conv);
165
166         // Package UTF-16 in a PDF string
167         string out;
168         out += 0xfe;
169         out += 0xff;
170         for (char *p = out_buf; p < out_ptr; p++)
171                 out += *p;
172         return QPDFObjectHandle::newString(out);
173 }
174
175 /*** Conversion of pages to XObjects ***/
176
177 static BBox get_trim_box(QPDFObjectHandle page)
178 {
179         static const char * const boxes[] = { "/TrimBox", "/CropBox", "/MediaBox", NULL };
180         for (int i=0; boxes[i]; i++)
181                 if (page.hasKey(boxes[i]))
182                         return BBox(page.getKey(boxes[i]));
183         warn("Page has no trimbox, falling back to A4");
184         return BBox(0, 0, A4_WIDTH, A4_HEIGHT);
185 }
186
187 /* Conversion of pages to XObjects is inspired by CUPS's pdftopdf filter. */
188 class CombineFromContents_Provider : public QPDFObjectHandle::StreamDataProvider {
189         private:
190                 vector<QPDFObjectHandle> contents;
191         public:
192                 CombineFromContents_Provider(const vector<QPDFObjectHandle> &contents) : contents(contents) { }
193                 void provideStreamData(int objid UNUSED, int generation UNUSED, Pipeline* pipeline) {
194                         Pl_Concatenate concat("concat", pipeline);
195                         for (int i=0; i < (int)contents.size(); i++)
196                                 contents[i].pipeStreamData(&concat, true, false, false);
197                         concat.manualFinish();
198                 }
199 };
200
201 QPDFObjectHandle page_to_xobject(QPDF *out, QPDFObjectHandle page)
202 {
203         page.assertPageObject();
204
205         QPDFObjectHandle xo_stream = QPDFObjectHandle::newStream(out);
206         QPDFObjectHandle xo_dict = xo_stream.getDict();
207
208         xo_dict.replaceKey("/Type", QPDFObjectHandle::newName("/XObject"));
209         xo_dict.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form"));
210         xo_dict.replaceKey("/FormType", QPDFObjectHandle::newInteger(1));
211
212         BBox box = get_trim_box(page);
213         xo_dict.replaceKey("/BBox", box.to_array());
214
215         xo_dict.replaceKey("/Resources", page.getKey("/Resources"));
216         if (page.hasKey("/Group"))
217                 xo_dict.replaceKey("/Group", page.getKey("/Group"));
218
219         if (page.hasKey("/UserUnit")) {
220                 double u = page.getKey("/UserUnit").getNumericValue();
221                 QPDFObjectHandle m = QPDFObjectHandle::newArray();
222                 m.appendItem(QPDFObjectHandle::newReal(u, 3));
223                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
224                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
225                 m.appendItem(QPDFObjectHandle::newReal(u, 3));
226                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
227                 m.appendItem(QPDFObjectHandle::newReal(0, 0));
228                 xo_dict.replaceKey("/Matrix", m);
229         }
230
231         vector<QPDFObjectHandle> contents = page.getPageContents();
232         auto ph = PointerHolder<QPDFObjectHandle::StreamDataProvider>(new CombineFromContents_Provider(contents));
233         xo_stream.replaceStreamData(ph, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull());
234         return xo_stream;
235 }
236
237 /*** Formatting of coordinates ***/
238
239 string pdf_coord(double x, uint digits)
240 {
241         char buf[16];
242         snprintf(buf, sizeof(buf), "%.*f", digits, x);
243         int n = strlen(buf);
244         while (n > 0 && buf[n-1] == '0')
245                 buf[--n] = 0;
246         if (n > 0 && buf[n-1] == '.')
247                 buf[--n] = 0;
248         return buf;
249 }
250
251 /*** Rotation matrix construction ***/
252
253 pdf_matrix pdf_rotation_matrix(int deg, double width, double height)
254 {
255       pdf_matrix m;
256       switch (deg) {
257               case 0:
258                       break;
259               case 90:
260                       m.rotate_deg(-90);
261                       m.shift(0, width);
262                       break;
263               case 180:
264                       m.rotate_deg(180);
265                       m.shift(width, height);
266                       break;
267               case 270:
268                       m.rotate_deg(90);
269                       m.shift(height, 0);
270                       break;
271               default:
272                       abort();
273       }
274       return m;
275 }