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