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