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