]> mj.ucw.cz Git - paperjam.git/blob - cmds.cc
Implemented book
[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   bbox = p->bbox;
50   bbox.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   void shrink_box(BBox *bb)
195     {
196       bb->x_min += l;
197       bb->x_max -= r;
198       bb->y_min += b;
199       bb->y_max -= t;
200       if (bb->x_min >= bb->x_max || bb->y_min >= bb->y_max)
201         die("Margins cannot be larger than the whole page");
202     }
203   void expand_box(BBox *bb)
204     {
205       bb->x_min -= l;
206       bb->x_max += r;
207       bb->y_min -= b;
208       bb->y_max += t;
209     }
210 };
211
212 #define MARGIN_ARGS1_NAMED(name)        \
213   { name,       AT_DIMEN }
214
215 #define MARGIN_ARGS1_POSNL(name)        \
216   { name,       AT_DIMEN | AT_POSITIONAL }
217
218 #define MARGIN_ARGS2(sx)                \
219   { "h" sx,     AT_DIMEN },             \
220   { "v" sx,     AT_DIMEN },             \
221   { "l" sx,     AT_DIMEN },             \
222   { "r" sx,     AT_DIMEN },             \
223   { "t" sx,     AT_DIMEN },             \
224   { "b" sx,     AT_DIMEN }
225
226 // Scaling preserving aspect ratio
227
228 double scale_to_fit(BBox &from, BBox &to)
229 {
230   double fw = from.width(), fh = from.height();
231   double tw = to.width(), th = to.height();
232   if (is_zero(fw) || is_zero(fh) || is_zero(tw) || is_zero(th))
233     return 1;
234   else
235     return min(tw/fw, th/fh);
236 }
237
238 /*** move ***/
239
240 class move_cmd : public cmd_exec_simple {
241   double x, y;
242 public:
243   move_cmd(cmd *c)
244     {
245       x = c->arg("x")->as_double(0);
246       y = c->arg("y")->as_double(0);
247     }
248   page *process_page(page *p) override
249     {
250       pdf_matrix m;
251       m.shift(x, y);
252       return new xform_page(p, m);
253     }
254 };
255
256 static const arg_def move_args[] = {
257   { "x",        AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
258   { "y",        AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
259   { NULL,       0 }
260 };
261
262 /*** scale ***/
263
264 class scale_cmd : public cmd_exec_simple {
265   double x_factor, y_factor;
266 public:
267   scale_cmd(cmd *c)
268     {
269       x_factor = c->arg("x")->as_double(1);
270       y_factor = c->arg("y")->as_double(x_factor);
271     }
272   page *process_page(page *p) override
273     {
274       pdf_matrix m;
275       m.scale(x_factor, y_factor);
276       return new xform_page(p, m);
277     }
278 };
279
280 static const arg_def scale_args[] = {
281   { "x",        AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL },
282   { "y",        AT_DOUBLE | AT_POSITIONAL },
283   { NULL,       0 }
284 };
285
286 /*** rotate ***/
287
288 class rotate_cmd : public cmd_exec_simple {
289   int deg;
290 public:
291   rotate_cmd(cmd *c)
292     {
293       deg = c->arg("deg")->as_int(0) % 360;
294       if (deg < 0)
295         deg += 360;
296       if (deg % 90)
297         die("Rotate requires a multiple of 90 degrees");
298     }
299   page *process_page(page *p) override
300     {
301       pdf_matrix m;
302       switch (deg)
303         {
304         case 0:
305           break;
306         case 90:
307           m.rotate_deg(-90);
308           m.shift(0, p->width);
309           break;
310         case 180:
311           m.rotate_deg(180);
312           m.shift(p->width, p->height);
313           break;
314         case 270:
315           m.rotate_deg(90);
316           m.shift(p->height, 0);
317           break;
318         default:
319           abort();
320         }
321       return new xform_page(p, m);
322     }
323 };
324
325 static const arg_def rotate_args[] = {
326   { "angle",    AT_INT | AT_MANDATORY | AT_POSITIONAL },
327   { NULL,       0 }
328 };
329
330 /*** flip ***/
331
332 class flip_cmd : public cmd_exec_simple {
333   bool horizontal;
334   bool vertical;
335 public:
336   flip_cmd(cmd *c)
337     {
338       horizontal = c->arg("h")->as_int(0);
339       vertical = c->arg("v")->as_int(0);
340       if (!horizontal && !vertical)
341         die("Flip has no direction specified");
342     }
343   page *process_page(page *p) override
344     {
345       pdf_matrix m;
346       if (vertical)
347         {
348           m.scale(1, -1);
349           m.shift(0, p->height);
350         }
351       if (horizontal)
352         {
353           m.scale(-1, 1);
354           m.shift(p->width, 0);
355         }
356       return new xform_page(p, m);
357     }
358 };
359
360 static const arg_def flip_args[] = {
361   { "h",        AT_SWITCH },
362   { "v",        AT_SWITCH },
363   { NULL,       0 }
364 };
365
366 /*** select ***/
367
368 class select_cmd : public cmd_exec {
369   pipeline *pipe;
370 public:
371   select_cmd(cmd *c)
372     {
373       pipe = c->pipe;
374     }
375   vector<page *> process(vector<page *> &pages) override;
376 };
377
378 static int validate_page_index(vector<page *> &pages, int idx)
379 {
380   if (idx >= 1 && idx <= (int) pages.size())
381     return idx - 1;
382   if (idx <= -1 && idx >= (int) -pages.size())
383     return idx + pages.size();
384   die("Page index %d out of range", idx);
385 }
386
387 vector<page *> select_cmd::process(vector<page *> &pages)
388 {
389   vector<page *> out;
390   for (auto pb: pipe->branches)
391     {
392       vector<page *> selected;
393       for (auto ps: pb->selectors)
394         {
395           int f = validate_page_index(pages, ps.from);
396           int t = validate_page_index(pages, ps.to);
397           int step = (f <= t) ? 1 : -1;
398           for (int i=f; f<=t; f += step)
399             selected.push_back(pages[i]);
400         }
401       auto processed = run_command_list(pb->commands, selected);
402       for (auto p: processed)
403         out.push_back(p);
404     }
405   return out;
406 }
407
408 /*** apply ***/
409
410 class apply_cmd : public cmd_exec {
411   pipeline *pipe;
412 public:
413   apply_cmd(cmd *c)
414     {
415       pipe = c->pipe;
416     }
417   vector<page *> process(vector<page *> &pages) override;
418 };
419
420 static pipeline_branch *find_branch(pipeline *pipe, vector <page *> &pages, int idx)
421 {
422   for (auto pb: pipe->branches)
423     for (auto ps: pb->selectors)
424       {
425         int f = validate_page_index(pages, ps.from);
426         int t = validate_page_index(pages, ps.to);
427         if (f <= idx && idx <= t || t <= idx && idx <= f)
428           return pb;
429       }
430   return NULL;
431 }
432
433 vector<page *> apply_cmd::process(vector<page *> &pages)
434 {
435   vector<page *> out;
436
437   int cnt = 0;
438   for (auto p: pages)
439     {
440       pipeline_branch *pb = find_branch(pipe, pages, cnt);
441       if (pb)
442         {
443           vector<page *> tmp;
444           tmp.push_back(p);
445           auto processed = run_command_list(pb->commands, tmp);
446           for (auto q: processed)
447             out.push_back(q);
448         }
449       else
450         out.push_back(p);
451       cnt++;
452     }
453
454   return out;
455 }
456
457 /*** modulo ***/
458
459 class modulo_cmd : public cmd_exec {
460   pipeline *pipe;
461   int n;
462 public:
463   modulo_cmd(cmd *c)
464     {
465       n = c->arg("n")->as_int(0);
466       if (n <= 0)
467         die("Modulo must have n > 0");
468       pipe = c->pipe;
469     }
470   vector<page *> process(vector<page *> &pages) override;
471 };
472
473 vector<page *> modulo_cmd::process(vector<page *> &pages)
474 {
475   vector<page *> out;
476   int tuples = ((int) pages.size() + n - 1) / n;
477
478   for (int tuple=0; tuple < tuples; tuple++)
479     {
480       debug("# Tuple %d", tuple);
481       debug_indent += 4;
482       for (auto pb: pipe->branches)
483         {
484           vector<page *> tmp;
485           for (auto ps: pb->selectors)
486             {
487               int f = ps.from;
488               int t = ps.to;
489               int step = (f <= t) ? 1 : -1;
490               for (int i=f; i<=t; i += step)
491                 {
492                   int j;
493                   if (i > 0 && i <= n)
494                     j = tuple*n + i - 1;
495                   else if (i < 0 && i >= -n)
496                     j = (tuples-1-tuple)*n + (-i) - 1;
497                   else
498                     die("Modulo: invalid index %d", i);
499                   if (j < (int) pages.size())
500                     tmp.push_back(pages[j]);
501                   else
502                     {
503                       page *ref_page = pages[tuple*n];
504                       tmp.push_back(new empty_page(ref_page->width, ref_page->height));
505                     }
506                 }
507             }
508           auto processed = run_command_list(pb->commands, tmp);
509           for (auto q: processed)
510             out.push_back(q);
511         }
512       debug_indent -= 4;
513     }
514
515   return out;
516 }
517
518 static const arg_def modulo_args[] = {
519   { "n",        AT_INT | AT_MANDATORY | AT_POSITIONAL },
520   { NULL,       0 }
521 };
522
523 /*** draw-bbox ***/
524
525 class draw_bbox_cmd : public cmd_exec {
526 public:
527   draw_bbox_cmd(cmd *c UNUSED) { }
528   vector<page *> process(vector<page *> &pages) override;
529 };
530
531 class draw_bbox_page : public page {
532   page *orig_page;
533 public:
534   void render(out_context *out, pdf_matrix xform) override;
535   draw_bbox_page(page *p) : page(p) { orig_page = p; }
536 };
537
538 void draw_bbox_page::render(out_context *out, pdf_matrix xform)
539 {
540   orig_page->render(out, xform);
541   out->contents +=
542      "q " +
543      xform.to_string() + " cm " +
544      "0 1 0 RG " +
545      bbox.to_rect() + " re S " +
546      "Q ";
547 }
548
549 vector<page *> draw_bbox_cmd::process(vector<page *> &pages)
550 {
551   vector<page *> out;
552   for (auto p: pages)
553     out.push_back(new draw_bbox_page(p));
554   return out;
555 }
556
557 /*** merge ***/
558
559 class merge_cmd : public cmd_exec {
560 public:
561   merge_cmd(cmd *c UNUSED) { }
562   vector<page *> process(vector<page *> &pages) override;
563 };
564
565 class merge_page : public page {
566   vector<page *> orig_pages;
567 public:
568   merge_page(vector<page *> &orig) : page(0, 0)
569     {
570       orig_pages = orig;
571       bool first = true;
572       for (auto p: orig)
573         {
574           if (first)
575             {
576               width = p->width;
577               height = p->height;
578               bbox = p->bbox;
579               first = false;
580             }
581           else
582             {
583               if (!is_equal(width, p->width) || !is_equal(height, p->height))
584                 die("All pages participating in a merge must have the same dimensions");
585               bbox.join(p->bbox);
586             }
587         }
588     }
589   void render(out_context *out, pdf_matrix xform) override
590     {
591       for (auto p: orig_pages)
592         p->render(out, xform);
593     }
594 };
595
596 vector<page *> merge_cmd::process(vector<page *> &pages)
597 {
598   vector<page *> out;
599   if (pages.size())
600     out.push_back(new merge_page(pages));
601   return out;
602 }
603
604 /*** paper ***/
605
606 class paper_cmd : public cmd_exec_simple {
607   paper_spec paper;
608   pos_spec pos;
609 public:
610   paper_cmd(cmd *c) : paper(c), pos(c) { }
611   page *process_page(page *p) override
612     {
613       BBox paper_box = BBox(paper.w, paper.h);
614       pdf_matrix xf = pos.place(p->bbox, paper_box);
615       page *q = new xform_page(p, xf);
616       q->width = paper.w;
617       q->height = paper.h;
618       return q;
619     }
620 };
621
622 static const arg_def paper_args[] = {
623   PAPER_ARGS,
624   POS_ARGS,
625   { NULL,       0 }
626 };
627
628 /*** scaleto ***/
629
630 class scaleto_cmd : public cmd_exec_simple {
631   paper_spec paper;
632   pos_spec pos;
633 public:
634   scaleto_cmd(cmd *c) : paper(c), pos(c) { }
635   page *process_page(page *p) override
636     {
637       BBox orig_box = BBox(p->width, p->height);
638       BBox paper_box = BBox(paper.w, paper.h);
639       pdf_matrix xf;
640       xf.scale(scale_to_fit(orig_box, paper_box));
641       orig_box.transform(xf);
642       xf.concat(pos.place(orig_box, paper_box));
643       page *q = new xform_page(p, xf);
644       q->width = paper.w;
645       q->height = paper.h;
646       return q;
647     }
648 };
649
650 static const arg_def scaleto_args[] = {
651   PAPER_ARGS,
652   POS_ARGS,
653   { NULL,       0 }
654 };
655
656 /*** fit ***/
657
658 class fit_cmd : public cmd_exec_simple {
659   paper_spec paper;
660   pos_spec pos;
661   margin_spec marg;
662 public:
663   fit_cmd(cmd *c) : paper(c, true), pos(c), marg(c, "margin", "margin") { }
664   page *process_page(page *p) override
665     {
666       pdf_matrix xf;
667       page *q;
668
669       if (!is_zero(paper.w) && !is_zero(paper.h))
670         {
671           // Paper given: scale image to fit paper
672           BBox orig_box = p->bbox;
673           BBox paper_box = BBox(paper.w, paper.h);
674           marg.shrink_box(&paper_box);
675           xf.scale(scale_to_fit(orig_box, paper_box));
676           orig_box.transform(xf);
677           xf.concat(pos.place(orig_box, paper_box));
678           q = new xform_page(p, xf);
679           q->width = paper.w;
680           q->height = paper.h;
681         }
682       else
683         {
684           // No paper given: adjust paper to fit image
685           xf.shift(-p->bbox.x_min, -p->bbox.y_min);
686           xf.shift(marg.l, marg.b);
687           q = new xform_page(p, xf);
688           q->width = p->bbox.width() + marg.l + marg.r;
689           q->height = p->bbox.height() + marg.t + marg.b;
690         }
691       return q;
692     }
693 };
694
695 static const arg_def fit_args[] = {
696   PAPER_ARGS,
697   POS_ARGS,
698   MARGIN_ARGS1_NAMED("margin"),
699   MARGIN_ARGS2("margin"),
700   { NULL,       0 }
701 };
702
703 /*** expand ***/
704
705 class expand_cmd : public cmd_exec_simple {
706   margin_spec marg;
707 public:
708   expand_cmd(cmd *c) : marg(c, "by", "") { }
709   page *process_page(page *p) override
710     {
711       pdf_matrix xf;
712       xf.shift(marg.l, marg.b);
713       page *q = new xform_page(p, xf);
714       q->width = p->width + marg.l + marg.r;
715       q->height = p->height + marg.t + marg.b;
716       if (q->width < 0.001 || q->height < 0.001)
717         die("Expansion must result in positive page dimensions");
718       return q;
719     }
720 };
721
722 static const arg_def expand_args[] = {
723   MARGIN_ARGS1_POSNL("by"),
724   MARGIN_ARGS2(""),
725   { NULL,       0 }
726 };
727
728 /*** margins ***/
729
730 class margins_cmd : public cmd_exec_simple {
731   margin_spec marg;
732 public:
733   margins_cmd(cmd *c) : marg(c, "size", "") { }
734   page *process_page(page *p) override
735     {
736       pdf_matrix xf;
737       xf.shift(-p->bbox.x_min, -p->bbox.y_min);
738       xf.shift(marg.l, marg.b);
739       page *q = new xform_page(p, xf);
740       q->width = p->bbox.width() + marg.l + marg.r;
741       q->height = p->bbox.height() + marg.t + marg.b;
742       if (q->width < 0.001 || q->height < 0.001)
743         die("Margins must result in positive page dimensions");
744       return q;
745     }
746 };
747
748 static const arg_def margins_args[] = {
749   MARGIN_ARGS1_POSNL("size"),
750   MARGIN_ARGS2(""),
751   { NULL,       0 }
752 };
753
754 /*** add-blank ***/
755
756 class add_blank_cmd : public cmd_exec {
757   int n;
758   paper_spec paper;
759 public:
760   add_blank_cmd(cmd *c) : paper(c, true)
761     {
762       n = c->arg("n")->as_int(1);
763     }
764   vector<page *> process(vector<page *> &pages) override;
765 };
766
767 vector<page *> add_blank_cmd::process(vector<page *> &pages)
768 {
769   vector<page *> out;
770
771   for (auto p: pages)
772     {
773       out.push_back(p);
774       for (int i=0; i<n; i++)
775         {
776           double w = paper.w, h = paper.h;
777           if (is_zero(w) || is_zero(h))
778             w = p->width, h = p->height;
779           out.push_back(new empty_page(w, h));
780         }
781     }
782
783   return out;
784 }
785
786 static const arg_def add_blank_args[] = {
787   { "n",        AT_INT | AT_POSITIONAL },
788   PAPER_ARGS,
789   { NULL,       0 }
790 };
791
792 /*** book ***/
793
794 class book_cmd : public cmd_exec {
795   int n;
796 public:
797   book_cmd(cmd *c)
798     {
799       n = c->arg("n")->as_int(0);
800       if (n % 4)
801         die("Number of pages per signature must be divisible by 4");
802     }
803   vector<page *> process(vector<page *> &pages) override;
804 };
805
806 vector<page *> book_cmd::process(vector<page *> &pages)
807 {
808   vector<page *> in, out;
809
810   in = pages;
811   while (in.size() % 4)
812     in.push_back(new empty_page(in[0]->width, in[0]->height));
813
814   int i = 0;
815   while (i < (int) in.size())
816     {
817       int sig = in.size() - i;
818       if (n)
819         sig = min(sig, n);
820       for (int j=0; j<sig/2; j+=2)
821         {
822           out.push_back(in[i + sig-1-j]);
823           out.push_back(in[i + j]);
824           out.push_back(in[i + j+1]);
825           out.push_back(in[i + sig-2-j]);
826         }
827       i += sig;
828     }
829
830   return out;
831 }
832
833 static const arg_def book_args[] = {
834   { "n",        AT_INT | AT_POSITIONAL },
835   { NULL,       0 }
836 };
837
838 /*** Command table ***/
839
840 template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
841
842 const cmd_def cmd_table[] = {
843   { "null",     no_args,        0,      &ctor<null_cmd>         },
844   { "move",     move_args,      0,      &ctor<move_cmd>         },
845   { "scale",    scale_args,     0,      &ctor<scale_cmd>        },
846   { "rotate",   rotate_args,    0,      &ctor<rotate_cmd>       },
847   { "flip",     flip_args,      0,      &ctor<flip_cmd>         },
848   { "select",   no_args,        1,      &ctor<select_cmd>       },
849   { "apply",    no_args,        1,      &ctor<apply_cmd>        },
850   { "modulo",   modulo_args,    1,      &ctor<modulo_cmd>       },
851   { "draw-bbox",no_args,        0,      &ctor<draw_bbox_cmd>    },
852   { "merge",    no_args,        0,      &ctor<merge_cmd>        },
853   { "paper",    paper_args,     0,      &ctor<paper_cmd>        },
854   { "scaleto",  scaleto_args,   0,      &ctor<scaleto_cmd>      },
855   { "fit",      fit_args,       0,      &ctor<fit_cmd>          },
856   { "expand",   expand_args,    0,      &ctor<expand_cmd>       },
857   { "margins",  margins_args,   0,      &ctor<margins_cmd>      },
858   { "add-blank",add_blank_args, 0,      &ctor<add_blank_cmd>    },
859   { "book",     book_args,      0,      &ctor<book_cmd>         },
860   { NULL,       NULL,           0,      NULL    }
861 };