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