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