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