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