]> mj.ucw.cz Git - paperjam.git/blob - cmds.cc
Better formatting of PDF numbers
[paperjam.git] / cmds.cc
1 /*
2  *      PaperJam -- Commands
3  *
4  *      (c) 2018 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <cassert>
8 #include <cstdlib>
9 #include <cstdio>
10 #include <paper.h>
11
12 #include "jam.h"
13
14 /*** null ***/
15
16 class null_cmd : public cmd_exec {
17 public:
18   null_cmd(cmd *c UNUSED) { }
19   vector<page *> process(vector<page *> &pages) override { return pages; }
20 };
21
22 static const arg_def no_args[] = {
23   { NULL,       0 }
24 };
25
26 /*** Generic routines ***/
27
28 // Transformed page
29
30 class xform_page : public page {
31   page *orig_page;
32   pdf_matrix xform;
33 public:
34   void render(out_context *out, pdf_matrix xform) override;
35   xform_page(page *p, pdf_matrix xf);
36 };
37
38 xform_page::xform_page(page *p, pdf_matrix xf)
39 {
40   orig_page = p;
41   index = p->index;
42   xform = xf;
43
44   BBox media(p->width, p->height);
45   media.transform(xf);
46   width = media.width();
47   height = media.height();
48
49   image_box = p->image_box;
50   image_box.transform(xf);
51 }
52
53 void xform_page::render(out_context *out, pdf_matrix parent_xform)
54 {
55   orig_page->render(out, xform * parent_xform);
56 }
57
58 // Commands acting on individual pages
59
60 class cmd_exec_simple : public cmd_exec {
61   virtual page *process_page(page *p) = 0;
62   vector<page *> process(vector<page *> &pages) override;
63 };
64
65 vector<page *> cmd_exec_simple::process(vector<page *> &pages)
66 {
67   vector<page *> out;
68   for (auto p: pages)
69     out.push_back(process_page(p));
70   return out;
71 }
72
73 // Paper specifications
74
75 class paper_spec {
76 public:
77   double w, h;
78   paper_spec(cmd *c, bool maybe=true)
79     {
80       arg_val *aname = c->arg("paper");
81       arg_val *aw = c->arg("w");
82       arg_val *ah = c->arg("h");
83       if (!aname->given() && !aw->given() && !ah->given() && maybe)
84         {
85           w = h = 0;
86           return;
87         }
88       if (aw->given() != ah->given() || aname->given() == aw->given())
89         die("Either paper format name or width and height must be given");
90       if (aname->given())
91         {
92           const char *name = aname->as_string("").c_str();
93           const paper *pap = paperinfo(name);
94           if (!pap)
95             die("No paper called %s is known", name);
96           w = paperpswidth(pap);
97           h = paperpsheight(pap);
98         }
99       else
100         {
101           w = aw->as_double(0);
102           h = ah->as_double(0);
103         }
104     }
105 };
106
107 #define PAPER_ARGS \
108   { "paper",    AT_STRING | AT_POSITIONAL },    \
109   { "w",        AT_DIMEN },                     \
110   { "h",        AT_DIMEN }
111
112 // Position specification
113
114 class pos_spec {
115 public:
116   int h, v;
117   pos_spec() { v = h = 0; }
118   pos_spec(string s)
119     {
120       if (s.size() != 2)
121         die("Value of pos must have two characters");
122       if (s[0] == 't')
123         v = 1;
124       else if (s[0] == 'c')
125         v = 0;
126       else if (s[0] == 'b')
127         v = -1;
128       else
129         die("First character of pos must be t/c/b");
130       if (s[1] == 'l')
131         h = -1;
132       else if (s[1] == 'c')
133         h = 0;
134       else if (s[1] == 'r')
135         h = 1;
136       else
137         die("Second character of pos must be l/c/r");
138     }
139   pos_spec(cmd *c) : pos_spec(c->arg("pos")->as_string("cc")) { }
140   pdf_matrix place(BBox &inner, BBox &outer)
141     {
142       pdf_matrix m;
143       m.shift(-inner.x_min, -inner.y_min);
144       switch (h)
145         {
146         case -1:
147           break;
148         case 0:
149           m.shift((outer.width() - inner.width()) / 2, 0);
150           break;
151         case 1:
152           m.shift(outer.width() - inner.width(), 0);
153           break;
154         default:
155           abort();
156         }
157       switch (v)
158         {
159         case -1:
160           break;
161         case 0:
162           m.shift(0, (outer.height() - inner.height()) / 2);
163           break;
164         case 1:
165           m.shift(0, outer.height() - inner.height());
166           break;
167         default:
168           abort();
169         }
170       m.shift(outer.x_min, outer.y_min);
171       return m;
172     }
173 };
174
175 #define POS_ARGS \
176   { "pos",      AT_STRING }
177
178 // Margins
179
180 class margin_spec {
181 public:
182   double l, r, t, b;
183   margin_spec(cmd *c, string basic, string sx)
184     {
185       double m, h, v;
186       m = c->arg(basic)->as_double(0);
187       h = c->arg("h" + sx)->as_double(m);
188       v = c->arg("v" + sx)->as_double(m);
189       l = c->arg("l" + sx)->as_double(h);
190       r = c->arg("r" + sx)->as_double(h);
191       t = c->arg("t" + sx)->as_double(v);
192       b = c->arg("b" + sx)->as_double(v);
193     }
194   bool may_shrink(BBox *bb)
195     {
196       return (bb->width() > l+r && bb->height() > t+b);
197     }
198   void shrink_box(BBox *bb)
199     {
200       bb->x_min += l;
201       bb->x_max -= r;
202       bb->y_min += b;
203       bb->y_max -= t;
204       if (bb->x_min >= bb->x_max || bb->y_min >= bb->y_max)
205         die("Margins cannot be larger than the whole page");
206     }
207   void expand_box(BBox *bb)
208     {
209       bb->x_min -= l;
210       bb->x_max += r;
211       bb->y_min -= b;
212       bb->y_max += t;
213     }
214 };
215
216 #define MARGIN_ARGS1_NAMED(name)        \
217   { name,       AT_DIMEN }
218
219 #define MARGIN_ARGS1_POSNL(name)        \
220   { name,       AT_DIMEN | AT_POSITIONAL }
221
222 #define MARGIN_ARGS2(sx)                \
223   { "h" sx,     AT_DIMEN },             \
224   { "v" sx,     AT_DIMEN },             \
225   { "l" sx,     AT_DIMEN },             \
226   { "r" sx,     AT_DIMEN },             \
227   { "t" sx,     AT_DIMEN },             \
228   { "b" sx,     AT_DIMEN }
229
230 // Cropmarks
231
232 class cropmark_spec {
233   enum mark_type {
234     MARK_NONE,
235     MARK_BOX,
236     MARK_CROSS,
237     MARK_OUT,
238     MARK_IN,
239   } type;
240   double pen_width;
241   double arm_length;
242   double offset;
243   string crop_cross(double x, double y, uint mask);
244   QPDFObjectHandle egstate;
245 public:
246   cropmark_spec(cmd *c, const string prefix="", const string def_type="cross")
247     {
248       string t = c->arg(prefix + "mark")->as_string(def_type);
249       if (t == "none")
250         type = MARK_NONE;
251       else if (t == "box")
252         type = MARK_BOX;
253       else if (t == "cross")
254         type = MARK_CROSS;
255       else if (t == "in")
256         type = MARK_IN;
257       else if (t == "out")
258         type = MARK_OUT;
259       else
260         die("Invalid cropmark type %s", t.c_str());
261
262       pen_width = c->arg(prefix + "pen")->as_double(0.2);
263       arm_length = c->arg(prefix + "len")->as_double(5*mm);
264       offset = c->arg(prefix + "offset")->as_double(0);
265       egstate = QPDFObjectHandle::newNull();
266     }
267   string pdf_stream(out_context *out, BBox &box, pdf_matrix &xform);
268 };
269
270 string cropmark_spec::crop_cross(double x, double y, uint mask)
271 {
272   string s = "";
273
274   for (uint i=0; i<4; i++)
275     if (mask & (1U << i))
276       {
277         double x2 = x, y2 = y;
278         switch (i)
279           {
280           case 3: x2 -= arm_length; break;
281           case 2: x2 += arm_length; break;
282           case 1: y2 += arm_length; break;
283           case 0: y2 -= arm_length; break;
284           }
285         s += pdf_coord(x) + " " + pdf_coord(y) + " m " + pdf_coord(x2) + " " + pdf_coord(y2) + " l S ";
286       }
287
288   return s;
289 }
290
291 string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
292 {
293   if (type == MARK_NONE)
294     return "";
295
296   string s = "q ";
297   s += "0 0 0 RG ";
298   s += xform.to_string() + " cm ";
299
300   if (egstate.isNull())
301     {
302       auto egs = QPDFObjectHandle::newDictionary();
303       egs.replaceKey("/Type", QPDFObjectHandle::newName("/ExtGState"));
304       egs.replaceKey("/LW", QPDFObjectHandle::newReal(pen_width, 1));
305       egs.replaceKey("/LC", QPDFObjectHandle::newInteger(2));
306       egs.replaceKey("/LJ", QPDFObjectHandle::newInteger(0));
307       egstate = out->pdf->makeIndirectObject(egs);
308     }
309
310   string egs_res = out->new_resource("GS");
311   out->egstates.replaceKey(egs_res, egstate);
312   s += egs_res + " gs ";
313
314   BBox b = box;
315   b.x_min -= offset;
316   b.x_max += offset;
317   b.y_min -= offset;
318   b.y_max += offset;
319
320   switch (type)
321     {
322     case MARK_NONE:
323       break;
324     case MARK_BOX:
325        s += b.to_rect() + " re S ";
326        break;
327     case MARK_CROSS:
328        s += crop_cross(b.x_min, b.y_min, 0b1111);
329        s += crop_cross(b.x_max, b.y_min, 0b1111);
330        s += crop_cross(b.x_max, b.y_max, 0b1111);
331        s += crop_cross(b.x_min, b.y_max, 0b1111);
332        break;
333     case MARK_IN:
334        s += crop_cross(b.x_min, b.y_min, 0b0110);
335        s += crop_cross(b.x_max, b.y_min, 0b1010);
336        s += crop_cross(b.x_max, b.y_max, 0b1001);
337        s += crop_cross(b.x_min, b.y_max, 0b0101);
338        break;
339     case MARK_OUT:
340        s += crop_cross(b.x_min, b.y_min, 0b1001);
341        s += crop_cross(b.x_max, b.y_min, 0b0101);
342        s += crop_cross(b.x_max, b.y_max, 0b0110);
343        s += crop_cross(b.x_min, b.y_max, 0b1010);
344        break;
345     }
346
347   s += "Q ";
348   return s;
349 }
350
351 #define CROPMARK_ARGS(px)               \
352   { px "mark",  AT_STRING },            \
353   { px "pen",   AT_DIMEN },             \
354   { px "len",   AT_DIMEN },             \
355   { px "offset",AT_DIMEN }
356
357 // Scaling preserving aspect ratio
358
359 double scale_to_fit(BBox &from, BBox &to)
360 {
361   double fw = from.width(), fh = from.height();
362   double tw = to.width(), th = to.height();
363   if (is_zero(fw) || is_zero(fh) || is_zero(tw) || is_zero(th))
364     return 1;
365   else
366     return min(tw/fw, th/fh);
367 }
368
369 /*** move ***/
370
371 class move_cmd : public cmd_exec_simple {
372   double x, y;
373 public:
374   move_cmd(cmd *c)
375     {
376       x = c->arg("x")->as_double(0);
377       y = c->arg("y")->as_double(0);
378     }
379   page *process_page(page *p) override
380     {
381       pdf_matrix m;
382       m.shift(x, y);
383       return new xform_page(p, m);
384     }
385 };
386
387 static const arg_def move_args[] = {
388   { "x",        AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
389   { "y",        AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
390   { NULL,       0 }
391 };
392
393 /*** scale ***/
394
395 class scale_cmd : public cmd_exec_simple {
396   double x_factor, y_factor;
397 public:
398   scale_cmd(cmd *c)
399     {
400       x_factor = c->arg("x")->as_double(1);
401       y_factor = c->arg("y")->as_double(x_factor);
402     }
403   page *process_page(page *p) override
404     {
405       pdf_matrix m;
406       m.scale(x_factor, y_factor);
407       return new xform_page(p, m);
408     }
409 };
410
411 static const arg_def scale_args[] = {
412   { "x",        AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL },
413   { "y",        AT_DOUBLE | AT_POSITIONAL },
414   { NULL,       0 }
415 };
416
417 /*** rotate ***/
418
419 class rotate_cmd : public cmd_exec_simple {
420   int deg;
421 public:
422   rotate_cmd(cmd *c)
423     {
424       deg = c->arg("deg")->as_int(0) % 360;
425       if (deg < 0)
426         deg += 360;
427       if (deg % 90)
428         die("Rotate requires a multiple of 90 degrees");
429     }
430   page *process_page(page *p) override
431     {
432       pdf_matrix m;
433       switch (deg)
434         {
435         case 0:
436           break;
437         case 90:
438           m.rotate_deg(-90);
439           m.shift(0, p->width);
440           break;
441         case 180:
442           m.rotate_deg(180);
443           m.shift(p->width, p->height);
444           break;
445         case 270:
446           m.rotate_deg(90);
447           m.shift(p->height, 0);
448           break;
449         default:
450           abort();
451         }
452       return new xform_page(p, m);
453     }
454 };
455
456 static const arg_def rotate_args[] = {
457   { "angle",    AT_INT | AT_MANDATORY | AT_POSITIONAL },
458   { NULL,       0 }
459 };
460
461 /*** flip ***/
462
463 class flip_cmd : public cmd_exec_simple {
464   bool horizontal;
465   bool vertical;
466 public:
467   flip_cmd(cmd *c)
468     {
469       horizontal = c->arg("h")->as_int(0);
470       vertical = c->arg("v")->as_int(0);
471       if (!horizontal && !vertical)
472         die("Flip has no direction specified");
473     }
474   page *process_page(page *p) override
475     {
476       pdf_matrix m;
477       if (vertical)
478         {
479           m.scale(1, -1);
480           m.shift(0, p->height);
481         }
482       if (horizontal)
483         {
484           m.scale(-1, 1);
485           m.shift(p->width, 0);
486         }
487       return new xform_page(p, m);
488     }
489 };
490
491 static const arg_def flip_args[] = {
492   { "h",        AT_SWITCH },
493   { "v",        AT_SWITCH },
494   { NULL,       0 }
495 };
496
497 /*** select ***/
498
499 class select_cmd : public cmd_exec {
500   pipeline *pipe;
501 public:
502   select_cmd(cmd *c)
503     {
504       pipe = c->pipe;
505     }
506   vector<page *> process(vector<page *> &pages) override;
507 };
508
509 static int validate_page_index(vector<page *> &pages, int idx)
510 {
511   if (idx >= 1 && idx <= (int) pages.size())
512     return idx - 1;
513   if (idx <= -1 && idx >= (int) -pages.size())
514     return idx + pages.size();
515   die("Page index %d out of range", idx);
516 }
517
518 vector<page *> select_cmd::process(vector<page *> &pages)
519 {
520   vector<page *> out;
521   for (auto pb: pipe->branches)
522     {
523       vector<page *> selected;
524       for (auto ps: pb->selectors)
525         {
526           int f = validate_page_index(pages, ps.from);
527           int t = validate_page_index(pages, ps.to);
528           int step = (f <= t) ? 1 : -1;
529           for (int i=f; f<=t; f += step)
530             selected.push_back(pages[i]);
531         }
532       auto processed = run_command_list(pb->commands, selected);
533       for (auto p: processed)
534         out.push_back(p);
535     }
536   return out;
537 }
538
539 /*** apply ***/
540
541 class apply_cmd : public cmd_exec {
542   pipeline *pipe;
543 public:
544   apply_cmd(cmd *c)
545     {
546       pipe = c->pipe;
547     }
548   vector<page *> process(vector<page *> &pages) override;
549 };
550
551 static pipeline_branch *find_branch(pipeline *pipe, vector <page *> &pages, int idx)
552 {
553   for (auto pb: pipe->branches)
554     for (auto ps: pb->selectors)
555       {
556         int f = validate_page_index(pages, ps.from);
557         int t = validate_page_index(pages, ps.to);
558         if (f <= idx && idx <= t || t <= idx && idx <= f)
559           return pb;
560       }
561   return NULL;
562 }
563
564 vector<page *> apply_cmd::process(vector<page *> &pages)
565 {
566   vector<page *> out;
567
568   int cnt = 0;
569   for (auto p: pages)
570     {
571       pipeline_branch *pb = find_branch(pipe, pages, cnt);
572       if (pb)
573         {
574           vector<page *> tmp;
575           tmp.push_back(p);
576           auto processed = run_command_list(pb->commands, tmp);
577           for (auto q: processed)
578             out.push_back(q);
579         }
580       else
581         out.push_back(p);
582       cnt++;
583     }
584
585   return out;
586 }
587
588 /*** modulo ***/
589
590 class modulo_cmd : public cmd_exec {
591   pipeline *pipe;
592   int n;
593   bool half;
594 public:
595   modulo_cmd(cmd *c)
596     {
597       n = c->arg("n")->as_int(0);
598       if (n <= 0)
599         die("Modulo must have n > 0");
600       half = c->arg("half")->as_int(0);
601       pipe = c->pipe;
602     }
603   vector<page *> process(vector<page *> &pages) override;
604 };
605
606 vector<page *> modulo_cmd::process(vector<page *> &pages)
607 {
608   vector<page *> out;
609   int tuples = ((int) pages.size() + n - 1) / n;
610   int use_tuples = half ? tuples/2 : tuples;
611
612   for (int tuple=0; tuple < use_tuples; tuple++)
613     {
614       debug("# Tuple %d", tuple);
615       debug_indent += 4;
616       for (auto pb: pipe->branches)
617         {
618           vector<page *> tmp;
619           for (auto ps: pb->selectors)
620             {
621               int f = ps.from;
622               int t = ps.to;
623               int step = (f <= t) ? 1 : -1;
624               for (int i=f; i<=t; i += step)
625                 {
626                   int j;
627                   if (i > 0 && i <= n)
628                     j = tuple*n + i - 1;
629                   else if (i < 0 && i >= -n)
630                     j = (tuples-1-tuple)*n + (-i) - 1;
631                   else
632                     die("Modulo: invalid index %d", i);
633                   if (j < (int) pages.size())
634                     tmp.push_back(pages[j]);
635                   else
636                     {
637                       page *ref_page = pages[tuple*n];
638                       tmp.push_back(new empty_page(ref_page->width, ref_page->height));
639                     }
640                 }
641             }
642           auto processed = run_command_list(pb->commands, tmp);
643           for (auto q: processed)
644             out.push_back(q);
645         }
646       debug_indent -= 4;
647     }
648
649   return out;
650 }
651
652 static const arg_def modulo_args[] = {
653   { "n",        AT_INT | AT_MANDATORY | AT_POSITIONAL },
654   { "half",     AT_SWITCH },
655   { NULL,       0 }
656 };
657
658 /*** draw-bbox ***/
659
660 class draw_bbox_page : public page {
661   page *orig_page;
662 public:
663   void render(out_context *out, pdf_matrix xform) override;
664   draw_bbox_page(page *p) : page(p) { orig_page = p; }
665 };
666
667 class draw_bbox_cmd : public cmd_exec_simple {
668 public:
669   draw_bbox_cmd(cmd *c UNUSED) { }
670   page *process_page(page *p) override
671     {
672       return new draw_bbox_page(p);
673     }
674 };
675
676 void draw_bbox_page::render(out_context *out, pdf_matrix xform)
677 {
678   orig_page->render(out, xform);
679   out->contents +=
680      "q " +
681      xform.to_string() + " cm " +
682      "0 1 0 RG " +
683      image_box.to_rect() + " re S " +
684      "Q ";
685 }
686
687 /*** merge ***/
688
689 class merge_cmd : public cmd_exec {
690 public:
691   merge_cmd(cmd *c UNUSED) { }
692   vector<page *> process(vector<page *> &pages) override;
693 };
694
695 class merge_page : public page {
696   vector<page *> orig_pages;
697 public:
698   merge_page(vector<page *> &orig) : page(0, 0)
699     {
700       orig_pages = orig;
701       bool first = true;
702       for (auto p: orig)
703         {
704           if (first)
705             {
706               width = p->width;
707               height = p->height;
708               image_box = p->image_box;
709               first = false;
710             }
711           else
712             {
713               if (!is_equal(width, p->width) || !is_equal(height, p->height))
714                 die("All pages participating in a merge must have the same dimensions");
715               image_box.join(p->image_box);
716             }
717         }
718     }
719   void render(out_context *out, pdf_matrix xform) override
720     {
721       for (auto p: orig_pages)
722         p->render(out, xform);
723     }
724 };
725
726 vector<page *> merge_cmd::process(vector<page *> &pages)
727 {
728   vector<page *> out;
729   if (pages.size())
730     out.push_back(new merge_page(pages));
731   return out;
732 }
733
734 /*** paper ***/
735
736 class paper_cmd : public cmd_exec_simple {
737   paper_spec paper;
738   pos_spec pos;
739 public:
740   paper_cmd(cmd *c) : paper(c), pos(c) { }
741   page *process_page(page *p) override
742     {
743       BBox paper_box = BBox(paper.w, paper.h);
744       pdf_matrix xf = pos.place(p->image_box, paper_box);
745       page *q = new xform_page(p, xf);
746       q->width = paper.w;
747       q->height = paper.h;
748       return q;
749     }
750 };
751
752 static const arg_def paper_args[] = {
753   PAPER_ARGS,
754   POS_ARGS,
755   { NULL,       0 }
756 };
757
758 /*** scaleto ***/
759
760 class scaleto_cmd : public cmd_exec_simple {
761   paper_spec paper;
762   pos_spec pos;
763 public:
764   scaleto_cmd(cmd *c) : paper(c), pos(c) { }
765   page *process_page(page *p) override
766     {
767       BBox orig_box = BBox(p->width, p->height);
768       BBox paper_box = BBox(paper.w, paper.h);
769       pdf_matrix xf;
770       xf.scale(scale_to_fit(orig_box, paper_box));
771       orig_box.transform(xf);
772       xf.concat(pos.place(orig_box, paper_box));
773       page *q = new xform_page(p, xf);
774       q->width = paper.w;
775       q->height = paper.h;
776       return q;
777     }
778 };
779
780 static const arg_def scaleto_args[] = {
781   PAPER_ARGS,
782   POS_ARGS,
783   { NULL,       0 }
784 };
785
786 /*** fit ***/
787
788 class fit_cmd : public cmd_exec_simple {
789   paper_spec paper;
790   pos_spec pos;
791   margin_spec marg;
792 public:
793   fit_cmd(cmd *c) : paper(c, true), pos(c), marg(c, "margin", "margin") { }
794   page *process_page(page *p) override
795     {
796       pdf_matrix xf;
797       page *q;
798
799       if (!is_zero(paper.w) && !is_zero(paper.h))
800         {
801           // Paper given: scale image to fit paper
802           BBox orig_box = p->image_box;
803           BBox paper_box = BBox(paper.w, paper.h);
804           marg.shrink_box(&paper_box);
805           xf.scale(scale_to_fit(orig_box, paper_box));
806           orig_box.transform(xf);
807           xf.concat(pos.place(orig_box, paper_box));
808           q = new xform_page(p, xf);
809           q->width = paper.w;
810           q->height = paper.h;
811         }
812       else
813         {
814           // No paper given: adjust paper to fit image
815           xf.shift(-p->image_box.x_min, -p->image_box.y_min);
816           xf.shift(marg.l, marg.b);
817           q = new xform_page(p, xf);
818           q->width = p->image_box.width() + marg.l + marg.r;
819           q->height = p->image_box.height() + marg.t + marg.b;
820         }
821       return q;
822     }
823 };
824
825 static const arg_def fit_args[] = {
826   PAPER_ARGS,
827   POS_ARGS,
828   MARGIN_ARGS1_NAMED("margin"),
829   MARGIN_ARGS2("margin"),
830   { NULL,       0 }
831 };
832
833 /*** expand ***/
834
835 class expand_cmd : public cmd_exec_simple {
836   margin_spec marg;
837 public:
838   expand_cmd(cmd *c) : marg(c, "by", "") { }
839   page *process_page(page *p) override
840     {
841       pdf_matrix xf;
842       xf.shift(marg.l, marg.b);
843       page *q = new xform_page(p, xf);
844       q->width = p->width + marg.l + marg.r;
845       q->height = p->height + marg.t + marg.b;
846       if (q->width < 0.001 || q->height < 0.001)
847         die("Expansion must result in positive page dimensions");
848       return q;
849     }
850 };
851
852 static const arg_def expand_args[] = {
853   MARGIN_ARGS1_POSNL("by"),
854   MARGIN_ARGS2(""),
855   { NULL,       0 }
856 };
857
858 /*** margins ***/
859
860 class margins_cmd : public cmd_exec_simple {
861   margin_spec marg;
862 public:
863   margins_cmd(cmd *c) : marg(c, "size", "") { }
864   page *process_page(page *p) override
865     {
866       pdf_matrix xf;
867       xf.shift(-p->image_box.x_min, -p->image_box.y_min);
868       xf.shift(marg.l, marg.b);
869       page *q = new xform_page(p, xf);
870       q->width = p->image_box.width() + marg.l + marg.r;
871       q->height = p->image_box.height() + marg.t + marg.b;
872       if (q->width < 0.001 || q->height < 0.001)
873         die("Margins must result in positive page dimensions");
874       return q;
875     }
876 };
877
878 static const arg_def margins_args[] = {
879   MARGIN_ARGS1_POSNL("size"),
880   MARGIN_ARGS2(""),
881   { NULL,       0 }
882 };
883
884 /*** add-blank ***/
885
886 class add_blank_cmd : public cmd_exec {
887   int n;
888   paper_spec paper;
889 public:
890   add_blank_cmd(cmd *c) : paper(c, true)
891     {
892       n = c->arg("n")->as_int(1);
893     }
894   vector<page *> process(vector<page *> &pages) override;
895 };
896
897 vector<page *> add_blank_cmd::process(vector<page *> &pages)
898 {
899   vector<page *> out;
900
901   for (auto p: pages)
902     {
903       out.push_back(p);
904       for (int i=0; i<n; i++)
905         {
906           double w = paper.w, h = paper.h;
907           if (is_zero(w) || is_zero(h))
908             w = p->width, h = p->height;
909           out.push_back(new empty_page(w, h));
910         }
911     }
912
913   return out;
914 }
915
916 static const arg_def add_blank_args[] = {
917   { "n",        AT_INT | AT_POSITIONAL },
918   PAPER_ARGS,
919   { NULL,       0 }
920 };
921
922 /*** book ***/
923
924 class book_cmd : public cmd_exec {
925   int n;
926 public:
927   book_cmd(cmd *c)
928     {
929       n = c->arg("n")->as_int(0);
930       if (n % 4)
931         die("Number of pages per signature must be divisible by 4");
932     }
933   vector<page *> process(vector<page *> &pages) override;
934 };
935
936 vector<page *> book_cmd::process(vector<page *> &pages)
937 {
938   vector<page *> in, out;
939
940   in = pages;
941   while (in.size() % 4)
942     in.push_back(new empty_page(in[0]->width, in[0]->height));
943
944   int i = 0;
945   while (i < (int) in.size())
946     {
947       int sig = in.size() - i;
948       if (n)
949         sig = min(sig, n);
950       for (int j=0; j<sig/2; j+=2)
951         {
952           out.push_back(in[i + sig-1-j]);
953           out.push_back(in[i + j]);
954           out.push_back(in[i + j+1]);
955           out.push_back(in[i + sig-2-j]);
956         }
957       i += sig;
958     }
959
960   return out;
961 }
962
963 static const arg_def book_args[] = {
964   { "n",        AT_INT | AT_POSITIONAL },
965   { NULL,       0 }
966 };
967
968 /*** nup ***/
969
970 struct nup_state {
971   int rows, cols;
972   double tile_w, tile_h;
973   double paper_w, paper_h;
974   double fill_factor;
975   double scale;
976 };
977
978 class nup_cmd : public cmd_exec {
979   // Parameters
980   int grid_n, grid_m, num_tiles;
981   enum {
982     BY_ROWS,
983     BY_COLS,
984     BY_TILE,
985   } fill_by;
986   bool crop;
987   bool mixed;
988   int rotate;
989   double scale;
990   paper_spec paper;
991   margin_spec marg;
992   pos_spec pos;
993   pos_spec tpos;
994   double hspace, vspace;
995   cropmark_spec cmarks;
996
997   // Processing state
998   page *process_single(vector<page *> &in);
999   void find_config(vector<page *> &in, BBox *page_boxes);
1000   void try_config(nup_state &st);
1001   nup_state best;
1002   bool found_solution;
1003   BBox common_page_box;
1004
1005 public:
1006   nup_cmd(cmd *c) : paper(c), marg(c, "margin", "margin"), pos(c), tpos(c->arg("tpos")->as_string("tl")), cmarks(c, "c", "none")
1007     {
1008       grid_n = c->arg("n")->as_int(0);
1009       grid_m = c->arg("m")->as_int(0);
1010       if (grid_n > 0 && grid_m > 0)
1011         num_tiles = grid_n * grid_m;
1012       else if (grid_n > 0 && !grid_m)
1013         num_tiles = grid_n;
1014       else
1015         die("Grid size must be at least 1x1");
1016
1017       const string by = c->arg("by")->as_string("rows");
1018       if (by == "rows" || by == "row" || by == "r")
1019         fill_by = BY_ROWS;
1020       else if (by == "cols" || by == "cols" || by == "c")
1021         fill_by = BY_COLS;
1022       else if (by == "tile" || by == "t")
1023         fill_by = BY_TILE;
1024       else
1025         die("Parameter \"by\" must be rows/cols/tile");
1026
1027       crop = c->arg("crop")->as_int(0);
1028       mixed = c->arg("mixed")->as_int(0);
1029       rotate = c->arg("rotate")->as_int(-1);
1030       scale = c->arg("scale")->as_double(0);
1031
1032       double space = c->arg("space")->as_double(0);
1033       hspace = c->arg("hspace")->as_double(space);
1034       vspace = c->arg("vspace")->as_double(space);
1035
1036       if (!is_zero(scale) && (!is_zero(paper.w) || rotate >= 0))
1037         die("When nup is used with explicit scaling, paper size nor rotation may be given");
1038       if (!is_zero(scale) && !grid_m)
1039         die("When nup is used with explicit scaling, both grid sizes must be given");
1040     }
1041
1042   vector<page *> process(vector<page *> &pages) override;
1043 };
1044
1045 vector<page *> nup_cmd::process(vector<page *> &pages)
1046 {
1047   vector<page *> out;
1048
1049   // Unless mixed is given, find the common page size
1050   if (!mixed)
1051     {
1052       for (int i=0; i < (int) pages.size(); i++)
1053         {
1054           page *p = pages[i];
1055           BBox pb = crop ? p->image_box : BBox(p->width, p->height);
1056           if (!i)
1057             common_page_box = pb;
1058           else
1059             common_page_box.join(pb);
1060         }
1061       debug("NUP: Common page box [%.3f %.3f]-[%.3f %.3f]",
1062         common_page_box.x_min, common_page_box.y_min, common_page_box.x_max, common_page_box.y_max);
1063     }
1064
1065   // Process one tile set after another
1066   int i = 0;
1067   while (i < (int) pages.size())
1068     {
1069       vector<page *> in;
1070       if (fill_by == BY_TILE)
1071         {
1072           for (int j=0; j<num_tiles; j++)
1073             in.push_back(pages[i]);
1074           i++;
1075         }
1076       else
1077         {
1078           for (int j=0; j<num_tiles; j++)
1079             {
1080               if (i < (int) pages.size())
1081                 in.push_back(pages[i]);
1082               else
1083                 in.push_back(new empty_page(in[0]->width, in[0]->height));
1084               i++;
1085             }
1086         }
1087       out.push_back(process_single(in));
1088     }
1089
1090   return out;
1091 }
1092
1093 void nup_cmd::try_config(nup_state &st)
1094 {
1095   BBox window(st.paper_w, st.paper_h);
1096   if (!marg.may_shrink(&window))
1097     return;
1098   marg.shrink_box(&window);
1099   window.x_max -= (st.cols - 1) * hspace;
1100   window.y_max -= (st.rows - 1) * vspace;
1101   if (window.width() < 0 || window.height() < 0)
1102     return;
1103
1104   BBox image(st.cols * st.tile_w, st.rows * st.tile_h);
1105   st.scale = scale_to_fit(image, window);
1106   st.fill_factor = (st.scale*image.width() * st.scale*image.height()) / (st.paper_w * st.paper_h);
1107
1108   debug("Try: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
1109     st.cols, st.rows,
1110     st.paper_w, st.paper_h,
1111     st.scale, st.fill_factor);
1112
1113   if (!found_solution || best.fill_factor < st.fill_factor)
1114     {
1115       found_solution = true;
1116       best = st;
1117     }
1118 }
1119
1120 void nup_cmd::find_config(vector<page *> &in, BBox *page_boxes)
1121 {
1122   nup_state st;
1123
1124   // Determine tile size
1125   st.tile_w = st.tile_h = 0;
1126   for (int i=0; i<num_tiles; i++)
1127     {
1128       if (!mixed)
1129         page_boxes[i] = common_page_box;
1130       else if (crop)
1131         page_boxes[i] = in[i]->image_box;
1132       else
1133         page_boxes[i] = BBox(in[i]->width, in[i]->height);
1134       st.tile_w = max(st.tile_w, page_boxes[i].width());
1135       st.tile_h = max(st.tile_h, page_boxes[i].height());
1136     }
1137   debug("NUP: %d tiles of size [%.3f,%.3f]", num_tiles, st.tile_w, st.tile_h);
1138   debug_indent += 4;
1139
1140   // Try all possible configurations of tiles
1141   found_solution = false;
1142   if (!is_zero(scale))
1143     {
1144       // If explicit scaling is requested, we choose page size ourselves,
1145       // so there is just one configuration.
1146       st.rows = grid_n;
1147       st.cols = grid_m;
1148       st.paper_w = marg.l + st.cols*st.tile_w + (st.cols-1)*hspace + marg.r;
1149       st.paper_h = marg.t + st.rows*st.tile_h + (st.rows-1)*vspace + marg.b;
1150       try_config(st);
1151     }
1152   else
1153     {
1154       // Page size is fixed (either given or copied from the source pages),
1155       // but we can have freedom to rotate and/or to choose grid size.
1156       for (int rot=0; rot<=1; rot++)
1157         {
1158           if (rotate >= 0 && rot != rotate)
1159             continue;
1160
1161           // Establish paper size
1162           if (!is_zero(paper.w))
1163             {
1164               st.paper_w = paper.w;
1165               st.paper_h = paper.h;
1166             }
1167           else
1168             {
1169               st.paper_w = in[0]->width;
1170               st.paper_h = in[0]->height;
1171             }
1172           if (rot)
1173             swap(st.paper_w, st.paper_h);
1174
1175           // Try grid sizes
1176           if (grid_m)
1177             {
1178               st.rows = grid_n;
1179               st.cols = grid_m;
1180               try_config(st);
1181             }
1182           else
1183             {
1184               for (int r=1; r<=grid_n; r++)
1185                 if (!(grid_n % r))
1186                   {
1187                     st.rows = r;
1188                     st.cols = grid_n / r;
1189                     try_config(st);
1190                   }
1191             }
1192         }
1193     }
1194
1195   if (!found_solution)
1196     die("Nup did not find a feasible solution");
1197   debug("Best: %dx%d on %.3f x %.3f", best.cols, best.rows, best.paper_w, best.paper_h);
1198   debug_indent -= 4;
1199 }
1200
1201 class nup_page : public page {
1202 public:
1203   vector<page *> orig_pages;
1204   vector<pdf_matrix> xforms;
1205   vector<BBox> tile_boxes;
1206   cropmark_spec *cmarks;
1207   void render(out_context *out, pdf_matrix xform) override;
1208   nup_page(nup_state &st) : page(st.paper_w, st.paper_h) { }
1209 };
1210
1211 void nup_page::render(out_context *out, pdf_matrix parent_xform)
1212 {
1213   for (int i=0; i < (int) orig_pages.size(); i++)
1214     {
1215       orig_pages[i]->render(out, xforms[i] * parent_xform);
1216       out->contents += cmarks->pdf_stream(out, tile_boxes[i], parent_xform);
1217     }
1218 }
1219
1220 page *nup_cmd::process_single(vector<page *> &in)
1221 {
1222   BBox page_boxes[num_tiles];
1223   find_config(in, page_boxes);
1224   double tw = best.scale * best.tile_w;
1225   double th = best.scale * best.tile_h;
1226
1227   // Construct transform from paper to grid of tiles
1228   BBox paper_box(best.paper_w, best.paper_h);
1229   marg.shrink_box(&paper_box);
1230   BBox grid_box(best.cols * tw + (best.cols-1) * hspace,
1231                 best.rows * th + (best.rows-1) * vspace);
1232   pdf_matrix place_xform = pos.place(grid_box, paper_box);
1233
1234   nup_page *p = new nup_page(best);
1235   p->image_box = grid_box;
1236   p->image_box.transform(place_xform);
1237
1238   for (int i=0; i<num_tiles; i++)
1239     {
1240       int r, c;
1241       if (fill_by == BY_ROWS || fill_by == BY_TILE)
1242         {
1243           r = i / best.cols;
1244           c = i % best.cols;
1245         }
1246       else
1247         {
1248           c = i / best.rows;
1249           r = i % best.rows;
1250         }
1251
1252       pdf_matrix m;
1253       BBox &page_box = page_boxes[i];
1254       m.shift(-page_box.x_min, -page_box.y_min);
1255       m.scale(best.scale);
1256       page_box.transform(m);
1257
1258       double x = c * (tw + hspace);
1259       double y = (best.rows-1-r) * (th + vspace);
1260       BBox tile_box = BBox(x, y, x+tw, y+th);
1261       m.concat(tpos.place(page_box, tile_box));
1262
1263       p->orig_pages.push_back(in[i]);
1264       p->xforms.push_back(m * place_xform);
1265       p->tile_boxes.push_back(tile_box.transformed(place_xform));
1266       p->cmarks = &cmarks;
1267     }
1268
1269   return p;
1270 }
1271
1272 static const arg_def nup_args[] = {
1273   { "n",        AT_INT | AT_POSITIONAL | AT_MANDATORY },
1274   { "m",        AT_INT | AT_POSITIONAL },
1275   { "by",       AT_STRING },
1276   { "crop",     AT_SWITCH },
1277   { "mixed",    AT_SWITCH },
1278   { "rotate",   AT_SWITCH },
1279   { "scale",    AT_DOUBLE },
1280   PAPER_ARGS,
1281   MARGIN_ARGS1_NAMED("margin"),
1282   MARGIN_ARGS2("margin"),
1283   POS_ARGS,
1284   CROPMARK_ARGS("c"),
1285   { "tpos",     AT_STRING },
1286   { "space",    AT_DIMEN },
1287   { "hspace",   AT_DIMEN },
1288   { "vspace",   AT_DIMEN },
1289   { NULL,       0 }
1290 };
1291
1292 /*** cropmarks ***/
1293
1294 class cropmarks_page : public page {
1295   page *orig_page;
1296   cropmark_spec *cm;
1297 public:
1298   void render(out_context *out, pdf_matrix xform) override
1299     {
1300       orig_page->render(out, xform);
1301       out->contents += cm->pdf_stream(out, image_box, xform);
1302     }
1303   cropmarks_page(page *p, cropmark_spec *cms) : page(p), orig_page(p), cm(cms) { }
1304 };
1305
1306 class cropmarks_cmd : public cmd_exec_simple {
1307   cropmark_spec cm;
1308   page *process_page(page *p) override
1309     {
1310       return new cropmarks_page(p, &cm);
1311     }
1312
1313 public:
1314   cropmarks_cmd(cmd *c) : cm(c) { }
1315 };
1316
1317 static const arg_def cropmarks_args[] = {
1318   CROPMARK_ARGS(""),
1319   { NULL,       0 }
1320 };
1321
1322 /*** Command table ***/
1323
1324 template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
1325
1326 const cmd_def cmd_table[] = {
1327   { "null",     no_args,        0,      &ctor<null_cmd>         },
1328   { "move",     move_args,      0,      &ctor<move_cmd>         },
1329   { "scale",    scale_args,     0,      &ctor<scale_cmd>        },
1330   { "rotate",   rotate_args,    0,      &ctor<rotate_cmd>       },
1331   { "flip",     flip_args,      0,      &ctor<flip_cmd>         },
1332   { "select",   no_args,        1,      &ctor<select_cmd>       },
1333   { "apply",    no_args,        1,      &ctor<apply_cmd>        },
1334   { "modulo",   modulo_args,    1,      &ctor<modulo_cmd>       },
1335   { "draw-bbox",no_args,        0,      &ctor<draw_bbox_cmd>    },
1336   { "merge",    no_args,        0,      &ctor<merge_cmd>        },
1337   { "paper",    paper_args,     0,      &ctor<paper_cmd>        },
1338   { "scaleto",  scaleto_args,   0,      &ctor<scaleto_cmd>      },
1339   { "fit",      fit_args,       0,      &ctor<fit_cmd>          },
1340   { "expand",   expand_args,    0,      &ctor<expand_cmd>       },
1341   { "margins",  margins_args,   0,      &ctor<margins_cmd>      },
1342   { "add-blank",add_blank_args, 0,      &ctor<add_blank_cmd>    },
1343   { "book",     book_args,      0,      &ctor<book_cmd>         },
1344   { "nup",      nup_args,       0,      &ctor<nup_cmd>          },
1345   { "cropmarks",cropmarks_args, 0,      &ctor<cropmarks_cmd>    },
1346   { NULL,       NULL,           0,      NULL    }
1347 };