]> mj.ucw.cz Git - paperjam.git/blobdiff - cmds.cc
One more <cstring>
[paperjam.git] / cmds.cc
diff --git a/cmds.cc b/cmds.cc
index fa4f615fadeedf28b62aa917422dca6927b4c8f8..7d0f9b659d934323bd02ef1b336c1811990867b6 100644 (file)
--- a/cmds.cc
+++ b/cmds.cc
+/*
+ *     PaperJam -- Commands
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
 #include <cassert>
 #include <cstdlib>
 #include <cstdio>
+#include <paper.h>
 
 #include "jam.h"
 
 /*** null ***/
 
 class null_cmd : public cmd_exec {
-  vector<page *> process(vector<page *> &pages) { return pages; }
+public:
+  null_cmd(cmd *c UNUSED) { }
+  vector<page *> process(vector<page *> &pages) override { return pages; }
 };
 
 static const arg_def no_args[] = {
-  { NULL,      0 }
+  { NULL,      0,      NULL }
 };
 
-static cmd_exec *null_ctor(cmd *c UNUSED)
-{
-  return new null_cmd;
-}
-
-/*** move ***/
+/*** Generic routines ***/
 
-class move_cmd : public cmd_exec {
-public:
-  double x, y;
-  vector<page *> process(vector<page *> &pages);
-};
+// Transformed page
 
 class xform_page : public page {
   page *orig_page;
-public:
   pdf_matrix xform;
-  void render(page_out *out, pdf_matrix xform);
-  xform_page(page *_orig, double _w, double _h) : page(_w, _h), orig_page(_orig) { }
+public:
+  void render(out_context *out, pdf_matrix xform) override;
+  void debug_dump() override
+    {
+      debug("Transform [%s]", xform.to_string().c_str());
+      orig_page->debug_dump();
+    }
+  xform_page(page *p, pdf_matrix xf);
 };
 
-void xform_page::render(page_out *out, pdf_matrix parent_xform)
+xform_page::xform_page(page *p, pdf_matrix xf)
+{
+  orig_page = p;
+  index = p->index;
+  xform = xf;
+
+  BBox media(p->width, p->height);
+  media.transform(xf);
+  width = media.width();
+  height = media.height();
+
+  image_box = p->image_box;
+  image_box.transform(xf);
+}
+
+void xform_page::render(out_context *out, pdf_matrix parent_xform)
 {
   orig_page->render(out, xform * parent_xform);
 }
 
-vector<page *> move_cmd::process(vector<page *> &pages)
+// Commands acting on individual pages
+
+class cmd_exec_simple : public cmd_exec {
+  virtual page *process_page(page *p) = 0;
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> cmd_exec_simple::process(vector<page *> &pages)
 {
   vector<page *> out;
   for (auto p: pages)
-    {
-      xform_page *q = new xform_page(p, p->width, p->height);
-      q->xform.shift(x, y);
-      out.push_back(q);
-    }
+    out.push_back(process_page(p));
   return out;
 }
 
-static const arg_def move_args[] = {
-  { "x",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
-  { "y",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
-  { NULL,      0 }
+// Paper specifications
+
+class paper_spec {
+public:
+  double w, h;
+  paper_spec(cmd *c, bool maybe=true)
+    {
+      arg_val *aname = c->arg("paper");
+      arg_val *aw = c->arg("w");
+      arg_val *ah = c->arg("h");
+      if (!aname->given() && !aw->given() && !ah->given() && maybe)
+       {
+         w = h = 0;
+         return;
+       }
+      if (aw->given() != ah->given() || aname->given() == aw->given())
+       err("Either paper format name or width and height must be given");
+      if (aname->given())
+       {
+         const char *name = aname->as_string("").c_str();
+         const paper *pap = paperinfo(name);
+         if (!pap)
+           err("No paper called \"%s\" is known", name);
+         w = paperpswidth(pap);
+         h = paperpsheight(pap);
+       }
+      else
+       {
+         w = aw->as_double(0);
+         h = ah->as_double(0);
+       }
+    }
 };
 
-static cmd_exec *move_ctor(cmd *c)
-{
-  move_cmd *m = new move_cmd;
-  m->x = c->args.at(0)->as_double(0);
-  m->y = c->args.at(1)->as_double(0);
-  return m;
-}
+#define PAPER_ARGS \
+  { "paper",   AT_STRING | AT_POSITIONAL,              "Paper format name (e.g., a4)" },       \
+  { "w",       AT_DIMEN,                               "Paper width" },                        \
+  { "h",       AT_DIMEN,                               "Paper height" }
 
-/*** scale ***/
+// Position specification
 
-class scale_cmd : public cmd_exec {
+class pos_spec {
 public:
-  double x_factor, y_factor;
-  vector<page *> process(vector<page *> &pages);
+  int h, v;
+  pos_spec() { v = h = 0; }
+  pos_spec(string s)
+    {
+      if (s.size() != 2)
+        err("Value of pos must have two characters");
+      if (s[0] == 't')
+       v = 1;
+      else if (s[0] == 'c')
+       v = 0;
+      else if (s[0] == 'b')
+       v = -1;
+      else
+       err("First character of pos must be t/c/b");
+      if (s[1] == 'l')
+       h = -1;
+      else if (s[1] == 'c')
+       h = 0;
+      else if (s[1] == 'r')
+       h = 1;
+      else
+       err("Second character of pos must be l/c/r");
+    }
+  pos_spec(cmd *c) : pos_spec(c->arg("pos")->as_string("cc")) { }
+  pdf_matrix place(BBox &inner, BBox &outer)
+    {
+      pdf_matrix m;
+      m.shift(-inner.x_min, -inner.y_min);
+      switch (h)
+       {
+       case -1:
+         break;
+       case 0:
+         m.shift((outer.width() - inner.width()) / 2, 0);
+         break;
+       case 1:
+         m.shift(outer.width() - inner.width(), 0);
+         break;
+       default:
+         abort();
+       }
+      switch (v)
+       {
+       case -1:
+         break;
+       case 0:
+         m.shift(0, (outer.height() - inner.height()) / 2);
+         break;
+       case 1:
+         m.shift(0, outer.height() - inner.height());
+         break;
+       default:
+         abort();
+       }
+      m.shift(outer.x_min, outer.y_min);
+      return m;
+    }
 };
 
-vector<page *> scale_cmd::process(vector<page *> &pages)
-{
-  vector<page *> out;
-  for (auto p: pages)
+#define POS_ARGS \
+  { "pos",     AT_STRING,                      "Position on the page: (t|c|b)(l|c|r)" }
+
+// Margins
+
+class margin_spec {
+public:
+  double l, r, t, b;
+  margin_spec(cmd *c, string basic, string sx)
     {
-      xform_page *q = new xform_page(p, x_factor*p->width, y_factor*p->height);
-      q->xform.scale(x_factor, y_factor);
-      out.push_back(q);
+      double m, h, v;
+      m = c->arg(basic)->as_double(0);
+      h = c->arg("h" + sx)->as_double(m);
+      v = c->arg("v" + sx)->as_double(m);
+      l = c->arg("l" + sx)->as_double(h);
+      r = c->arg("r" + sx)->as_double(h);
+      t = c->arg("t" + sx)->as_double(v);
+      b = c->arg("b" + sx)->as_double(v);
     }
-  return out;
-}
+  bool may_shrink(BBox *bb)
+    {
+      return (bb->width() > l+r && bb->height() > t+b);
+    }
+  void shrink_box(BBox *bb)
+    {
+      bb->x_min += l;
+      bb->x_max -= r;
+      bb->y_min += t;
+      bb->y_max -= b;
+      if (bb->x_min >= bb->x_max || bb->y_min >= bb->y_max)
+       err("Margins cannot be larger than the whole page");
+    }
+  void expand_box(BBox *bb)
+    {
+      bb->x_min -= l;
+      bb->x_max += r;
+      bb->y_min -= t;
+      bb->y_max += b;
+    }
+};
 
-static const arg_def scale_args[] = {
-  { "x",       AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL },
-  { "y",       AT_DOUBLE | AT_POSITIONAL },
-  { NULL,      0 }
+#define MARGIN_ARGS1_NAMED(name)                                                       \
+  { name,      AT_DIMEN,                       "Size of all margins (default: 0)" }
+
+#define MARGIN_ARGS1_POSNL(name)                                                       \
+  { name,      AT_DIMEN | AT_POSITIONAL,       "Size of all margins (default: 0)" }
+
+#define MARGIN_ARGS2(sx)                                                               \
+  { "h" sx,    AT_DIMEN,                       "Size of horizontal margins" },         \
+  { "v" sx,    AT_DIMEN,                       "Size of vertical margins" },           \
+  { "l" sx,    AT_DIMEN,                       "Size of left margin" },                \
+  { "r" sx,    AT_DIMEN,                       "Size of right margin" },               \
+  { "t" sx,    AT_DIMEN,                       "Size of top margin" },                 \
+  { "b" sx,    AT_DIMEN,                       "Size of bottom margin" }
+
+// Colors
+
+class color_spec {
+public:
+  double rgb[3];
+  color_spec()
+    {
+      rgb[0] = rgb[1] = rgb[2] = 0;
+    }
+  color_spec(string s)
+    {
+      if (s.length() != 6)
+       err("Invalid color specification \"%s\": expecting 6 hex digits", s.c_str());
+      for (int i=0; i<3; i++)
+       {
+         int x = 0;
+         for (int j=0; j<2; j++)
+           {
+             char c = s[2*i+j];
+             if (c >= '0' && c <= '9')
+               x = (x << 4) | (c - '0');
+             else if (c >= 'a' && c <= 'f')
+               x = (x << 4) | (c - 'a' + 10);
+             else if (c >= 'A' && c <= 'F')
+               x = (x << 4) | (c - 'A' + 10);
+           }
+         rgb[i] = x / 255.;
+       }
+    }
+  string to_string()
+    {
+      return pdf_coord(rgb[0]) + " " + pdf_coord(rgb[1]) + " " + pdf_coord(rgb[2]);
+    }
+};
+
+// Cropmarks
+
+class cropmark_spec {
+public:
+  enum mark_type {
+    MARK_NONE,
+    MARK_BOX,
+    MARK_CROSS,
+    MARK_OUT,
+    MARK_IN,
+    MARK_BG,
+  } type;
+  double pen_width;
+  double arm_length;
+  double offset;
+  color_spec color;
+  cropmark_spec()
+    {
+      type = MARK_NONE;
+      pen_width = 0.2;
+      arm_length = 5*mm;
+      offset = 0;
+      egstate = QPDFObjectHandle::newNull();
+    }
+  cropmark_spec(cmd *c, const string prefix="", const string def_type="cross") : color(c->arg(prefix + "color")->as_string("000000"))
+    {
+      string t = c->arg(prefix + "mark")->as_string(def_type);
+      if (t == "none")
+       type = MARK_NONE;
+      else if (t == "box")
+       type = MARK_BOX;
+      else if (t == "cross")
+       type = MARK_CROSS;
+      else if (t == "in")
+       type = MARK_IN;
+      else if (t == "out")
+       type = MARK_OUT;
+      else if (t == "bg")
+       type = MARK_BG;
+      else
+       err("Invalid cropmark type %s", t.c_str());
+
+      pen_width = c->arg(prefix + "pen")->as_double(0.2);
+      arm_length = c->arg(prefix + "len")->as_double(5*mm);
+      offset = c->arg(prefix + "offset")->as_double(0);
+      egstate = QPDFObjectHandle::newNull();
+    }
+  bool is_bg() { return (type == MARK_BG); }
+  string pdf_stream(out_context *out, BBox &box, pdf_matrix &xform);
+private:
+  string crop_cross(double x, double y, uint mask);
+  QPDFObjectHandle egstate;
 };
 
-static cmd_exec *scale_ctor(cmd *c)
+string cropmark_spec::crop_cross(double x, double y, uint mask)
 {
-  scale_cmd *s = new scale_cmd;
-  s->x_factor = c->args.at(0)->as_double(1);
-  s->y_factor = c->args.at(1)->as_double(s->x_factor);
+  string s = "";
+
+  for (uint i=0; i<4; i++)
+    if (mask & (1U << i))
+      {
+       double x2 = x, y2 = y;
+       switch (i)
+         {
+         case 3: x2 -= arm_length; break;
+         case 2: x2 += arm_length; break;
+         case 1: y2 += arm_length; break;
+         case 0: y2 -= arm_length; break;
+         }
+       s += pdf_coord(x) + " " + pdf_coord(y) + " m " + pdf_coord(x2) + " " + pdf_coord(y2) + " l S ";
+      }
+
   return s;
 }
 
-/*** rotate ***/
+string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
+{
+  if (type == MARK_NONE)
+    return "";
+
+  string s = "q ";
+  s += color.to_string() + (type == MARK_BG ? " rg " : " RG ");
+  s += xform.to_string() + " cm ";
+
+  if (egstate.isNull())
+    {
+      auto egs = QPDFObjectHandle::newDictionary();
+      egs.replaceKey("/Type", QPDFObjectHandle::newName("/ExtGState"));
+      egs.replaceKey("/LW", QPDFObjectHandle::newReal(pen_width, 1));
+      egs.replaceKey("/LC", QPDFObjectHandle::newInteger(2));
+      egs.replaceKey("/LJ", QPDFObjectHandle::newInteger(0));
+      egstate = out->pdf->makeIndirectObject(egs);
+    }
+
+  string egs_res = out->new_resource("GS");
+  out->egstates.replaceKey(egs_res, egstate);
+  s += egs_res + " gs ";
+
+  BBox b = box.enlarged(offset);
 
-class rotate_cmd : public cmd_exec {
+  switch (type)
+    {
+    case MARK_NONE:
+      break;
+    case MARK_BOX:
+       s += b.to_rect() + " re S ";
+       break;
+    case MARK_CROSS:
+       s += crop_cross(b.x_min, b.y_min, 0b1111);
+       s += crop_cross(b.x_max, b.y_min, 0b1111);
+       s += crop_cross(b.x_max, b.y_max, 0b1111);
+       s += crop_cross(b.x_min, b.y_max, 0b1111);
+       break;
+    case MARK_IN:
+       s += crop_cross(b.x_min, b.y_min, 0b0110);
+       s += crop_cross(b.x_max, b.y_min, 0b1010);
+       s += crop_cross(b.x_max, b.y_max, 0b1001);
+       s += crop_cross(b.x_min, b.y_max, 0b0101);
+       break;
+    case MARK_OUT:
+       s += crop_cross(b.x_min, b.y_min, 0b1001);
+       s += crop_cross(b.x_max, b.y_min, 0b0101);
+       s += crop_cross(b.x_max, b.y_max, 0b0110);
+       s += crop_cross(b.x_min, b.y_max, 0b1010);
+       break;
+    case MARK_BG:
+       s += b.to_rect() + " re f ";
+       break;
+    }
+
+  s += "Q ";
+  return s;
+}
+
+#define CROPMARK_ARGS(px)                                                                      \
+  { px "mark", AT_STRING,              "Cropmark style: box/cross/in/out/bg" },                \
+  { px "pen",  AT_DIMEN,               "Cropmark pen width (default: 0.2pt)" },                \
+  { px "len",  AT_DIMEN,               "Cropmark arm length (default: 5mm)" },                 \
+  { px "offset",AT_DIMEN,              "Cropmark offset outside the box (default: 0)" },       \
+  { px "color",        AT_STRING,              "Cropmark color (RRGGBB, default: 000000)" }
+
+// Scaling preserving aspect ratio
+
+double scale_to_fit(BBox &from, BBox &to)
+{
+  double fw = from.width(), fh = from.height();
+  double tw = to.width(), th = to.height();
+  if (is_zero(fw) || is_zero(fh) || is_zero(tw) || is_zero(th))
+    return 1;
+  else
+    return min(tw/fw, th/fh);
+}
+
+/*** move ***/
+
+class move_cmd : public cmd_exec_simple {
+  double x, y;
 public:
-  int deg;
-  vector<page *> process(vector<page *> &pages);
+  move_cmd(cmd *c)
+    {
+      x = c->arg("x")->as_double(0);
+      y = c->arg("y")->as_double(0);
+    }
+  page *process_page(page *p) override
+    {
+      pdf_matrix m;
+      m.shift(x, y);
+      return new xform_page(p, m);
+    }
 };
 
-vector<page *> rotate_cmd::process(vector<page *> &pages)
-{
-  vector<page *> out;
-  for (auto p: pages)
+static const arg_def move_args[] = {
+  { "x",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL,        "Move right by this distance" },
+  { "y",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL,        "Move up by this distance" },
+  { NULL,      0,                              NULL }
+};
+
+/*** scale ***/
+
+class scale_cmd : public cmd_exec_simple {
+  double x_factor, y_factor;
+public:
+  scale_cmd(cmd *c)
+    {
+      x_factor = c->arg("x")->as_double(1);
+      y_factor = c->arg("y")->as_double(x_factor);
+    }
+  page *process_page(page *p) override
     {
-      xform_page *q = new xform_page(p, p->width, p->height);
+      pdf_matrix m;
+      m.scale(x_factor, y_factor);
+      return new xform_page(p, m);
+    }
+};
+
+static const arg_def scale_args[] = {
+  { "x",       AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL,       "Scale horizontally by this fraction" },
+  { "y",       AT_DOUBLE | AT_POSITIONAL,                      "Scale vertically by this fraction (default: x)" },
+  { NULL,      0,                                              NULL }
+};
+
+/*** rotate ***/
+
+class rotate_cmd : public cmd_exec_simple {
+  int deg;
+public:
+  rotate_cmd(cmd *c)
+    {
+      deg = c->arg("angle")->as_int(0) % 360;
+      if (deg < 0)
+       deg += 360;
+      if (deg % 90)
+       err("The angle must be a multiple of 90 degrees");
+    }
+  page *process_page(page *p) override
+    {
+      pdf_matrix m;
       switch (deg)
        {
        case 0:
          break;
        case 90:
-         q->xform.rotate_deg(-90);
-         q->xform.shift(0, p->width);
-         swap(q->width, q->height);
+         m.rotate_deg(-90);
+         m.shift(0, p->width);
          break;
        case 180:
-         q->xform.rotate_deg(180);
-         q->xform.shift(p->width, p->height);
+         m.rotate_deg(180);
+         m.shift(p->width, p->height);
          break;
        case 270:
-         q->xform.rotate_deg(90);
-         q->xform.shift(p->height, 0);
-         swap(q->width, q->height);
+         m.rotate_deg(90);
+         m.shift(p->height, 0);
          break;
        default:
          abort();
        }
-      out.push_back(q);
+      return new xform_page(p, m);
     }
-  return out;
-}
+};
 
 static const arg_def rotate_args[] = {
-  { "angle",   AT_INT | AT_MANDATORY | AT_POSITIONAL },
-  { NULL,      0 }
+  { "angle",   AT_INT | AT_MANDATORY | AT_POSITIONAL,  "Rotate clockwise by this angle" },
+  { NULL,      0,                                      NULL }
 };
 
-static cmd_exec *rotate_ctor(cmd *c)
-{
-  rotate_cmd *r = new rotate_cmd;
-  r->deg = c->args.at(0)->as_int(0) % 360;
-  if (r->deg < 0)
-    r->deg += 360;
-  if (r->deg % 90)
-    die("Rotate requires a multiple of 90 degrees");
-  return r;
-}
+/*** flip ***/
+
+class flip_cmd : public cmd_exec_simple {
+  bool horizontal;
+  bool vertical;
+public:
+  flip_cmd(cmd *c)
+    {
+      horizontal = c->arg("h")->as_int(0);
+      vertical = c->arg("v")->as_int(0);
+      if (!horizontal && !vertical)
+       err("No direction specified");
+    }
+  page *process_page(page *p) override
+    {
+      pdf_matrix m;
+      if (vertical)
+       {
+         m.scale(1, -1);
+         m.shift(0, p->height);
+       }
+      if (horizontal)
+       {
+         m.scale(-1, 1);
+         m.shift(p->width, 0);
+       }
+      return new xform_page(p, m);
+    }
+};
+
+static const arg_def flip_args[] = {
+  { "h",       AT_SWITCH,                              "Flip horizontally" },
+  { "v",       AT_SWITCH,                              "Flip vertically" },
+  { NULL,      0,                                      NULL }
+};
 
 /*** select ***/
 
 class select_cmd : public cmd_exec {
-public:
   pipeline *pipe;
-  vector<page *> process(vector<page *> &pages);
+public:
+  select_cmd(cmd *c)
+    {
+      pipe = c->pipe;
+    }
+  vector<page *> process(vector<page *> &pages) override;
 };
 
 static int validate_page_index(vector<page *> &pages, int idx)
@@ -170,44 +566,1034 @@ static int validate_page_index(vector<page *> &pages, int idx)
     return idx - 1;
   if (idx <= -1 && idx >= (int) -pages.size())
     return idx + pages.size();
-  die("Page index %d out of range", idx);
+  err("Page index %d out of range", idx);
 }
 
 vector<page *> select_cmd::process(vector<page *> &pages)
 {
   vector<page *> out;
+  for (auto pb: pipe->branches)
+    {
+      vector<page *> selected;
+      for (auto ps: pb->selectors)
+       {
+         int f = validate_page_index(pages, ps.from);
+         int t = validate_page_index(pages, ps.to);
+         int step = (f <= t) ? 1 : -1;
+         for (int i=f; i<=t; i += step)
+           selected.push_back(pages[i]);
+       }
+      auto processed = run_command_list(pb->commands, selected);
+      for (auto p: processed)
+       out.push_back(p);
+    }
+  return out;
+}
+
+/*** apply ***/
+
+class apply_cmd : public cmd_exec {
+  pipeline *pipe;
+public:
+  apply_cmd(cmd *c)
+    {
+      pipe = c->pipe;
+    }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+static pipeline_branch *find_branch(pipeline *pipe, vector <page *> &pages, int idx)
+{
   for (auto pb: pipe->branches)
     for (auto ps: pb->selectors)
       {
        int f = validate_page_index(pages, ps.from);
        int t = validate_page_index(pages, ps.to);
-       int step = (f <= t) ? 1 : -1;
-       for (int i=f; f<=t; f += step)
-         {
-           vector<page *> selected;
-           selected.push_back(pages[i]);
-           selected = run_command_list(pb->commands, selected);
-           for (auto p: selected)
-             out.push_back(p);
-         }
+       if (f <= idx && idx <= t || t <= idx && idx <= f)
+         return pb;
       }
+  return NULL;
+}
+
+vector<page *> apply_cmd::process(vector<page *> &pages)
+{
+  vector<page *> out;
+
+  int cnt = 0;
+  for (auto p: pages)
+    {
+      pipeline_branch *pb = find_branch(pipe, pages, cnt);
+      if (pb)
+       {
+         vector<page *> tmp;
+         tmp.push_back(p);
+         auto processed = run_command_list(pb->commands, tmp);
+         for (auto q: processed)
+           out.push_back(q);
+       }
+      else
+       out.push_back(p);
+      cnt++;
+    }
+
   return out;
 }
 
-static cmd_exec *select_ctor(cmd *c)
+/*** modulo ***/
+
+class modulo_cmd : public cmd_exec {
+  pipeline *pipe;
+  int n;
+  bool half;
+public:
+  modulo_cmd(cmd *c)
+    {
+      n = c->arg("n")->as_int(0);
+      if (n <= 0)
+       err("Modulo must have n > 0");
+      half = c->arg("half")->as_int(0);
+      pipe = c->pipe;
+    }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> modulo_cmd::process(vector<page *> &pages)
 {
-  select_cmd *r = new select_cmd;
-  r->pipe = c->pipe;
-  return r;
+  vector<page *> out;
+  int tuples = ((int) pages.size() + n - 1) / n;
+  int use_tuples = half ? tuples/2 : tuples;
+
+  for (int tuple=0; tuple < use_tuples; tuple++)
+    {
+      debug("# Tuple %d", tuple);
+      debug_indent += 4;
+      for (auto pb: pipe->branches)
+       {
+         vector<page *> tmp;
+         for (auto ps: pb->selectors)
+           {
+             int f = ps.from;
+             int t = ps.to;
+             int step = (f <= t) ? 1 : -1;
+             for (int i=f; i<=t; i += step)
+               {
+                 int j;
+                 if (i > 0 && i <= n)
+                   j = tuple*n + i - 1;
+                 else if (i < 0 && i >= -n)
+                   j = (tuples-1-tuple)*n + (-i) - 1;
+                 else
+                   err("Invalid index %d", i);
+                 if (j < (int) pages.size())
+                   tmp.push_back(pages[j]);
+                 else
+                   {
+                     page *ref_page = pages[tuple*n];
+                     tmp.push_back(new empty_page(ref_page->width, ref_page->height));
+                   }
+               }
+           }
+         auto processed = run_command_list(pb->commands, tmp);
+         for (auto q: processed)
+           out.push_back(q);
+       }
+      debug_indent -= 4;
+    }
+
+  return out;
 }
 
+static const arg_def modulo_args[] = {
+  { "n",       AT_INT | AT_MANDATORY | AT_POSITIONAL,          "Number of pages in a single tuple" },
+  { "half",    AT_SWITCH,                                      "Process only the first half of n-tuples" },
+  { NULL,      0,                                              NULL }
+};
+
+/*** debug ***/
+
+class debug_page : public page {
+  page *orig_page;
+  cropmark_spec *page_cm, *image_cm;
+public:
+  debug_page(page *p, cropmark_spec *page_cms, cropmark_spec *image_cms) : page(p), orig_page(p), page_cm(page_cms), image_cm(image_cms) { }
+  void render(out_context *out, pdf_matrix xform) override
+    {
+      orig_page->render(out, xform);
+      BBox page_bbox = BBox(0, 0, width, height);
+      out->contents += page_cm->pdf_stream(out, page_bbox, xform);
+      out->contents += image_cm->pdf_stream(out, image_box, xform);
+    }
+  void debug_dump() override
+    {
+      debug("Draw debugging boxes");
+      orig_page->debug_dump();
+    }
+};
+
+class debug_cmd : public cmd_exec_simple {
+  cropmark_spec page_cmarks;
+  cropmark_spec image_cmarks;
+public:
+  debug_cmd(cmd *c UNUSED)
+    {
+      page_cmarks.type = cropmark_spec::MARK_BOX;
+      page_cmarks.color.rgb[0] = 1;
+      page_cmarks.color.rgb[1] = 0;
+      page_cmarks.color.rgb[2] = 0;
+      image_cmarks.type = cropmark_spec::MARK_BOX;
+      image_cmarks.color.rgb[0] = 0;
+      image_cmarks.color.rgb[1] = 1;
+      image_cmarks.color.rgb[2] = 0;
+    }
+  page *process_page(page *p) override
+    {
+      return new debug_page(p, &page_cmarks, &image_cmarks);
+    }
+};
+
+/*** merge ***/
+
+class merge_cmd : public cmd_exec {
+public:
+  merge_cmd(cmd *c UNUSED) { }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+class merge_page : public page {
+  vector<page *> orig_pages;
+public:
+  merge_page(vector<page *> &orig) : page(0, 0)
+    {
+      orig_pages = orig;
+      bool first = true;
+      for (auto p: orig)
+       {
+         if (first)
+           {
+             width = p->width;
+             height = p->height;
+             image_box = p->image_box;
+             first = false;
+           }
+         else
+           {
+             if (!is_equal(width, p->width) || !is_equal(height, p->height))
+               err("All pages must have the same dimensions");
+             image_box.join(p->image_box);
+           }
+       }
+    }
+  void render(out_context *out, pdf_matrix xform) override
+    {
+      for (auto p: orig_pages)
+       p->render(out, xform);
+    }
+  void debug_dump() override
+    {
+      debug("Merge pages");
+      debug_indent += 4;
+      for (auto p: orig_pages)
+       p->debug_dump();
+      debug_indent -= 4;
+    }
+};
+
+vector<page *> merge_cmd::process(vector<page *> &pages)
+{
+  vector<page *> out;
+  if (pages.size())
+    out.push_back(new merge_page(pages));
+  return out;
+}
+
+/*** paper ***/
+
+class paper_cmd : public cmd_exec_simple {
+  paper_spec paper;
+  pos_spec pos;
+public:
+  paper_cmd(cmd *c) : paper(c), pos(c) { }
+  page *process_page(page *p) override
+    {
+      BBox paper_box = BBox(paper.w, paper.h);
+      pdf_matrix xf = pos.place(p->image_box, paper_box);
+      page *q = new xform_page(p, xf);
+      q->width = paper.w;
+      q->height = paper.h;
+      return q;
+    }
+};
+
+static const arg_def paper_args[] = {
+  PAPER_ARGS,
+  POS_ARGS,
+  { NULL,      0,                              NULL }
+};
+
+/*** scaleto ***/
+
+class scaleto_cmd : public cmd_exec_simple {
+  paper_spec paper;
+  pos_spec pos;
+public:
+  scaleto_cmd(cmd *c) : paper(c), pos(c) { }
+  page *process_page(page *p) override
+    {
+      BBox orig_box = BBox(p->width, p->height);
+      BBox paper_box = BBox(paper.w, paper.h);
+      pdf_matrix xf;
+      xf.scale(scale_to_fit(orig_box, paper_box));
+      orig_box.transform(xf);
+      xf.concat(pos.place(orig_box, paper_box));
+      page *q = new xform_page(p, xf);
+      q->width = paper.w;
+      q->height = paper.h;
+      return q;
+    }
+};
+
+static const arg_def scaleto_args[] = {
+  PAPER_ARGS,
+  POS_ARGS,
+  { NULL,      0,                              NULL }
+};
+
+/*** fit ***/
+
+class fit_cmd : public cmd_exec_simple {
+  paper_spec paper;
+  pos_spec pos;
+  margin_spec marg;
+public:
+  fit_cmd(cmd *c) : paper(c, true), pos(c), marg(c, "margin", "margin") { }
+  page *process_page(page *p) override
+    {
+      pdf_matrix xf;
+      page *q;
+
+      if (!is_zero(paper.w) && !is_zero(paper.h))
+       {
+         // Paper given: scale image to fit paper
+         BBox orig_box = p->image_box;
+         BBox paper_box = BBox(paper.w, paper.h);
+         marg.shrink_box(&paper_box);
+         xf.scale(scale_to_fit(orig_box, paper_box));
+         orig_box.transform(xf);
+         xf.concat(pos.place(orig_box, paper_box));
+         q = new xform_page(p, xf);
+         q->width = paper.w;
+         q->height = paper.h;
+       }
+      else
+       {
+         // No paper given: adjust paper to fit image
+         xf.shift(-p->image_box.x_min, -p->image_box.y_min);
+         xf.shift(marg.l, marg.b);
+         q = new xform_page(p, xf);
+         q->width = p->image_box.width() + marg.l + marg.r;
+         q->height = p->image_box.height() + marg.t + marg.b;
+       }
+      return q;
+    }
+};
+
+static const arg_def fit_args[] = {
+  PAPER_ARGS,
+  POS_ARGS,
+  MARGIN_ARGS1_NAMED("margin"),
+  MARGIN_ARGS2("margin"),
+  { NULL,      0,                              NULL }
+};
+
+/*** expand ***/
+
+class expand_cmd : public cmd_exec_simple {
+  margin_spec marg;
+public:
+  expand_cmd(cmd *c) : marg(c, "by", "") { }
+  page *process_page(page *p) override
+    {
+      pdf_matrix xf;
+      xf.shift(marg.l, marg.b);
+      page *q = new xform_page(p, xf);
+      q->width = p->width + marg.l + marg.r;
+      q->height = p->height + marg.t + marg.b;
+      if (q->width < 0.001 || q->height < 0.001)
+       err("Expansion must result in positive page dimensions");
+      return q;
+    }
+};
+
+static const arg_def expand_args[] = {
+  MARGIN_ARGS1_POSNL("by"),
+  MARGIN_ARGS2(""),
+  { NULL,      0,                              NULL }
+};
+
+/*** margins ***/
+
+class margins_cmd : public cmd_exec_simple {
+  margin_spec marg;
+public:
+  margins_cmd(cmd *c) : marg(c, "size", "") { }
+  page *process_page(page *p) override
+    {
+      page *q = new xform_page(p, pdf_matrix());
+      q->image_box = BBox(marg.l, marg.t, p->width - marg.r, p->height - marg.b);
+      if (q->image_box.width() < 0.001 || q->image_box.height() < 0.001)
+       err("Margins must result in positive image dimensions");
+      return q;
+    }
+};
+
+static const arg_def margins_args[] = {
+  MARGIN_ARGS1_POSNL("size"),
+  MARGIN_ARGS2(""),
+  { NULL,      0,                              NULL }
+};
+
+/*** add-blank ***/
+
+class add_blank_cmd : public cmd_exec {
+  int n;
+  paper_spec paper;
+public:
+  add_blank_cmd(cmd *c) : paper(c, true)
+    {
+      n = c->arg("n")->as_int(1);
+    }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> add_blank_cmd::process(vector<page *> &pages)
+{
+  vector<page *> out;
+
+  for (auto p: pages)
+    {
+      out.push_back(p);
+      for (int i=0; i<n; i++)
+       {
+         double w = paper.w, h = paper.h;
+         if (is_zero(w) || is_zero(h))
+           w = p->width, h = p->height;
+         out.push_back(new empty_page(w, h));
+       }
+    }
+
+  return out;
+}
+
+static const arg_def add_blank_args[] = {
+  { "n",       AT_INT | AT_POSITIONAL,         "Number of blank pages to add (default: 1)" },
+  PAPER_ARGS,
+  { NULL,      0,                              NULL }
+};
+
+/*** book ***/
+
+class book_cmd : public cmd_exec {
+  int n;
+public:
+  book_cmd(cmd *c)
+    {
+      n = c->arg("n")->as_int(0);
+      if (n % 4)
+        err("Number of pages per signature must be divisible by 4");
+    }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> book_cmd::process(vector<page *> &pages)
+{
+  vector<page *> in, out;
+
+  in = pages;
+  while (in.size() % 4)
+    in.push_back(new empty_page(in[0]->width, in[0]->height));
+
+  int i = 0;
+  while (i < (int) in.size())
+    {
+      int sig = in.size() - i;
+      if (n)
+       sig = min(sig, n);
+      for (int j=0; j<sig/2; j+=2)
+       {
+         out.push_back(in[i + sig-1-j]);
+         out.push_back(in[i + j]);
+         out.push_back(in[i + j+1]);
+         out.push_back(in[i + sig-2-j]);
+       }
+      i += sig;
+    }
+
+  return out;
+}
+
+static const arg_def book_args[] = {
+  { "n",       AT_INT | AT_POSITIONAL,         "Number of pages in a single booklet" },
+  { NULL,      0,                              NULL }
+};
+
+/*** nup ***/
+
+struct nup_state {
+  int rows, cols;
+  double tile_w, tile_h;
+  double paper_w, paper_h;
+  double fill_factor;
+  double scale;
+};
+
+class nup_cmd : public cmd_exec {
+  // Parameters
+  int grid_n, grid_m, num_tiles;
+  enum {
+    BY_ROWS,
+    BY_COLS,
+    BY_TILE,
+  } fill_by;
+  bool crop;
+  bool mixed;
+  int rotate;
+  double scale;
+  paper_spec paper;
+  margin_spec marg;
+  pos_spec pos;
+  pos_spec tpos;
+  double hspace, vspace;
+  cropmark_spec cmarks;
+
+  // Processing state
+  page *process_single(vector<page *> &in);
+  void find_config(vector<page *> &in, BBox *page_boxes);
+  void try_config(nup_state &st);
+  nup_state best;
+  bool found_solution;
+  BBox common_page_box;
+
+public:
+  nup_cmd(cmd *c) : paper(c), marg(c, "margin", "margin"), pos(c), tpos(c->arg("tpos")->as_string("tl")), cmarks(c, "c", "none")
+    {
+      grid_n = c->arg("n")->as_int(0);
+      grid_m = c->arg("m")->as_int(0);
+      if (grid_n > 0 && grid_m > 0)
+       num_tiles = grid_n * grid_m;
+      else if (grid_n > 0 && !grid_m)
+       num_tiles = grid_n;
+      else
+       err("Grid size must be at least 1x1");
+
+      const string by = c->arg("by")->as_string("rows");
+      if (by == "rows" || by == "row" || by == "r")
+       fill_by = BY_ROWS;
+      else if (by == "cols" || by == "cols" || by == "c")
+       fill_by = BY_COLS;
+      else if (by == "tile" || by == "t")
+       fill_by = BY_TILE;
+      else
+       err("Argument \"by\" must be rows/cols/tile");
+
+      crop = c->arg("crop")->as_int(0);
+      mixed = c->arg("mixed")->as_int(0);
+      rotate = c->arg("rotate")->as_int(-1);
+      scale = c->arg("scale")->as_double(0);
+
+      double space = c->arg("space")->as_double(0);
+      hspace = c->arg("hspace")->as_double(space);
+      vspace = c->arg("vspace")->as_double(space);
+
+      if (!is_zero(scale) && (!is_zero(paper.w) || rotate >= 0))
+       err("When used with explicit scaling, paper size nor rotation may be given");
+      if (!is_zero(scale) && !grid_m)
+       err("When used with explicit scaling, both grid sizes must be given");
+    }
+
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> nup_cmd::process(vector<page *> &pages)
+{
+  vector<page *> out;
+
+  // Unless mixed is given, find the common page size
+  if (!mixed)
+    {
+      for (int i=0; i < (int) pages.size(); i++)
+       {
+         page *p = pages[i];
+         BBox pb = crop ? p->image_box : BBox(p->width, p->height);
+         if (!i)
+           common_page_box = pb;
+         else
+           common_page_box.join(pb);
+       }
+      debug("NUP: Common page box [%.3f %.3f]-[%.3f %.3f]",
+       common_page_box.x_min, common_page_box.y_min, common_page_box.x_max, common_page_box.y_max);
+    }
+
+  // Process one tile set after another
+  int i = 0;
+  while (i < (int) pages.size())
+    {
+      vector<page *> in;
+      if (fill_by == BY_TILE)
+       {
+         for (int j=0; j<num_tiles; j++)
+           in.push_back(pages[i]);
+         i++;
+       }
+      else
+       {
+         for (int j=0; j<num_tiles; j++)
+           {
+             if (i < (int) pages.size())
+               in.push_back(pages[i]);
+             else
+               in.push_back(new empty_page(in[0]->width, in[0]->height));
+             i++;
+           }
+       }
+      out.push_back(process_single(in));
+    }
+
+  return out;
+}
+
+void nup_cmd::try_config(nup_state &st)
+{
+  BBox window(st.paper_w, st.paper_h);
+  if (!marg.may_shrink(&window))
+    return;
+  marg.shrink_box(&window);
+  window.x_max -= (st.cols - 1) * hspace;
+  window.y_max -= (st.rows - 1) * vspace;
+  if (window.width() < 0 || window.height() < 0)
+    return;
+
+  BBox image(st.cols * st.tile_w, st.rows * st.tile_h);
+  st.scale = scale_to_fit(image, window);
+  st.fill_factor = (st.scale*image.width() * st.scale*image.height()) / (st.paper_w * st.paper_h);
+
+  if (debug_level > 1)
+    debug("Try: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
+          st.cols, st.rows,
+         st.paper_w, st.paper_h,
+         st.scale, st.fill_factor);
+
+  if (!found_solution || best.fill_factor < st.fill_factor)
+    {
+      found_solution = true;
+      best = st;
+    }
+}
+
+void nup_cmd::find_config(vector<page *> &in, BBox *page_boxes)
+{
+  nup_state st;
+
+  // Determine tile size
+  st.tile_w = st.tile_h = 0;
+  for (int i=0; i<num_tiles; i++)
+    {
+      if (!mixed)
+       page_boxes[i] = common_page_box;
+      else if (crop)
+       page_boxes[i] = in[i]->image_box;
+      else
+       page_boxes[i] = BBox(in[i]->width, in[i]->height);
+      st.tile_w = max(st.tile_w, page_boxes[i].width());
+      st.tile_h = max(st.tile_h, page_boxes[i].height());
+    }
+  debug("NUP: %d tiles of size [%.3f,%.3f]", num_tiles, st.tile_w, st.tile_h);
+  debug_indent += 4;
+
+  // Try all possible configurations of tiles
+  found_solution = false;
+  if (!is_zero(scale))
+    {
+      // If explicit scaling is requested, we choose page size ourselves,
+      // so there is just one configuration.
+      st.rows = grid_n;
+      st.cols = grid_m;
+      st.paper_w = marg.l + st.cols*st.tile_w + (st.cols-1)*hspace + marg.r;
+      st.paper_h = marg.t + st.rows*st.tile_h + (st.rows-1)*vspace + marg.b;
+      try_config(st);
+    }
+  else
+    {
+      // Page size is fixed (either given or copied from the source pages),
+      // but we can have freedom to rotate and/or to choose grid size.
+      for (int rot=0; rot<=1; rot++)
+       {
+         if (rotate >= 0 && rot != rotate)
+           continue;
+
+         // Establish paper size
+         if (!is_zero(paper.w))
+           {
+             st.paper_w = paper.w;
+             st.paper_h = paper.h;
+           }
+         else
+           {
+             st.paper_w = in[0]->width;
+             st.paper_h = in[0]->height;
+           }
+         if (rot)
+           swap(st.paper_w, st.paper_h);
+
+         // Try grid sizes
+         if (grid_m)
+           {
+             st.rows = grid_n;
+             st.cols = grid_m;
+             try_config(st);
+           }
+         else
+           {
+             for (int r=1; r<=grid_n; r++)
+               if (!(grid_n % r))
+                 {
+                   st.rows = r;
+                   st.cols = grid_n / r;
+                   try_config(st);
+                 }
+           }
+       }
+    }
+
+  if (!found_solution)
+    err("No feasible solution found");
+  debug("Best: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
+        best.cols, best.rows,
+       best.paper_w, best.paper_h,
+       best.scale, best.fill_factor);
+  debug_indent -= 4;
+}
+
+class nup_page : public page {
+public:
+  vector<page *> orig_pages;
+  vector<pdf_matrix> xforms;
+  vector<BBox> tile_boxes;
+  cropmark_spec *cmarks;
+  void render(out_context *out, pdf_matrix xform) override;
+  nup_page(nup_state &st) : page(st.paper_w, st.paper_h) { }
+  void debug_dump() override
+    {
+      debug("N-up printing");
+      debug_indent += 4;
+      for (auto p: orig_pages)
+       p->debug_dump();
+      debug_indent -= 4;
+    }
+};
+
+void nup_page::render(out_context *out, pdf_matrix parent_xform)
+{
+  for (int i=0; i < (int) orig_pages.size(); i++)
+    {
+      if (cmarks->is_bg())
+       out->contents += cmarks->pdf_stream(out, tile_boxes[i], parent_xform);
+      orig_pages[i]->render(out, xforms[i] * parent_xform);
+      if (!cmarks->is_bg())
+       out->contents += cmarks->pdf_stream(out, tile_boxes[i], parent_xform);
+    }
+}
+
+page *nup_cmd::process_single(vector<page *> &in)
+{
+  BBox page_boxes[num_tiles];
+  find_config(in, page_boxes);
+  double tw = best.scale * best.tile_w;
+  double th = best.scale * best.tile_h;
+
+  // Construct transform from paper to grid of tiles
+  BBox paper_box(best.paper_w, best.paper_h);
+  marg.shrink_box(&paper_box);
+  BBox grid_box(best.cols * tw + (best.cols-1) * hspace,
+                best.rows * th + (best.rows-1) * vspace);
+  pdf_matrix place_xform = pos.place(grid_box, paper_box);
+
+  nup_page *p = new nup_page(best);
+  p->image_box = grid_box;
+  p->image_box.transform(place_xform);
+
+  for (int i=0; i<num_tiles; i++)
+    {
+      int r, c;
+      if (fill_by == BY_ROWS || fill_by == BY_TILE)
+       {
+         r = i / best.cols;
+         c = i % best.cols;
+       }
+      else
+       {
+         c = i / best.rows;
+         r = i % best.rows;
+       }
+
+      pdf_matrix m;
+      BBox &page_box = page_boxes[i];
+      m.shift(-page_box.x_min, -page_box.y_min);
+      m.scale(best.scale);
+      page_box.transform(m);
+
+      double x = c * (tw + hspace);
+      double y = (best.rows-1-r) * (th + vspace);
+      BBox tile_box = BBox(x, y, x+tw, y+th);
+      m.concat(tpos.place(page_box, tile_box));
+
+      p->orig_pages.push_back(in[i]);
+      p->xforms.push_back(m * place_xform);
+      p->tile_boxes.push_back(tile_box.transformed(place_xform));
+      p->cmarks = &cmarks;
+    }
+
+  return p;
+}
+
+static const arg_def nup_args[] = {
+  { "n",       AT_INT | AT_POSITIONAL | AT_MANDATORY,  "Number of tiles on a page" },
+  { "m",       AT_INT | AT_POSITIONAL,                 "If both n and m are given, produce n x m tiles on a page" },
+  { "by",      AT_STRING,                              "Tile filling order: rows/cols/tile (default: rows)" },
+  { "crop",    AT_SWITCH,                              "Crop input pages to their image box" },
+  { "mixed",   AT_SWITCH,                              "Allow input pages of mixed sizes" },
+  { "rotate",  AT_SWITCH,                              "Force (non-)rotation" },
+  { "scale",   AT_DOUBLE,                              "Force specific scaling factor" },
+  PAPER_ARGS,
+  MARGIN_ARGS1_NAMED("margin"),
+  MARGIN_ARGS2("margin"),
+  POS_ARGS,
+  CROPMARK_ARGS("c"),
+  { "tpos",    AT_STRING,                              "Position of images inside tiles (default: tl)" },
+  { "space",   AT_DIMEN,                               "Space between tiles (default: 0)" },
+  { "hspace",  AT_DIMEN,                               "Horizontal space between tiles (default: space)" },
+  { "vspace",  AT_DIMEN,                               "Vertical space between tiles (default: space)" },
+  { NULL,      0,                                      NULL }
+};
+
+/*** cropmarks ***/
+
+class cropmarks_page : public page {
+  page *orig_page;
+  cropmark_spec *cm;
+public:
+  void render(out_context *out, pdf_matrix xform) override
+    {
+      orig_page->render(out, xform);
+      out->contents += cm->pdf_stream(out, image_box, xform);
+    }
+  void debug_dump() override
+    {
+      debug("Add cropmarks");
+      orig_page->debug_dump();
+    }
+  cropmarks_page(page *p, cropmark_spec *cms) : page(p), orig_page(p), cm(cms) { }
+};
+
+class cropmarks_cmd : public cmd_exec_simple {
+  cropmark_spec cm;
+  page *process_page(page *p) override
+    {
+      return new cropmarks_page(p, &cm);
+    }
+public:
+  cropmarks_cmd(cmd *c) : cm(c) { }
+};
+
+static const arg_def cropmarks_args[] = {
+  CROPMARK_ARGS(""),
+  { NULL,      0,                              NULL }
+};
+
+/*** clip ***/
+
+class clip_page : public page {
+  page *orig_page;
+  BBox clip_to;
+public:
+  void render(out_context *out, pdf_matrix xform) override
+    {
+      out->contents += "q " + clip_to.transformed(xform).to_rect() + " re W n ";
+      orig_page->render(out, xform);
+      out->contents += "Q ";
+    }
+  void debug_dump() override
+    {
+      debug("Clip [%.3f %.3f %.3f %.3f]", clip_to.x_min, clip_to.y_min, clip_to.x_max, clip_to.y_max);
+      orig_page->debug_dump();
+    }
+  clip_page(page *p, BBox &to) : page(p), orig_page(p), clip_to(to) { }
+};
+
+class clip_cmd : public cmd_exec_simple {
+  double bleed;
+  page *process_page(page *p) override
+    {
+      BBox to = p->image_box.enlarged(bleed);
+      return new clip_page(p, to);
+    }
+public:
+  clip_cmd(cmd *c)
+    {
+      bleed = c->arg("bleed")->as_double(0);
+    }
+};
+
+static const arg_def clip_args[] = {
+  { "bleed",   AT_DIMEN,                       "Allow bleeding of image outside its box" },
+  { NULL,      0,                              NULL }
+};
+
+/*** common ***/
+
+class common_cmd : public cmd_exec {
+  vector<page *> process(vector<page *> &pages) override
+    {
+      if (!pages.size())
+        return pages;
+
+      const page *first = pages[0];
+      BBox pbox(first->width, first->height);
+      BBox ibox = first->image_box;
+      for (auto p: pages)
+       {
+         BBox pg(p->width, p->height);
+         pbox.join(pg);
+         ibox.join(p->image_box);
+       }
+
+      vector<page *> out;
+      for (auto p: pages)
+       {
+         page *q = new xform_page(p, pdf_matrix());
+         q->width = pbox.width();
+         q->height = pbox.height();
+         q->image_box = ibox;
+         out.push_back(q);
+       }
+
+      return out;
+    }
+public:
+  common_cmd(cmd *c UNUSED) { }
+};
+
+/*** slice ***/
+
+class slice_cmd : public cmd_exec {
+  paper_spec paper;
+  pos_spec pos;
+  margin_spec margin;
+  double bleed;
+public:
+  slice_cmd(cmd *c) : paper(c), pos(c), margin(c, "margin", "margin")
+    {
+      if (is_zero(paper.w) || is_zero(paper.h))
+       err("Paper format must be given");
+      bleed = c->arg("bleed")->as_double(0);
+    }
+  vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> slice_cmd::process(vector<page *> &pages)
+{
+  vector<page *> out;
+
+  for (auto p: pages)
+    {
+      double pw = paper.w - margin.l - margin.r;
+      double ph = paper.h - margin.t - margin.b;
+      if (pw < 0 || ph < 0)
+       err("Margins larger than paper");
+
+      int cols = (int) ceil((p->image_box.width() - 2*mm) / pw);
+      int rows = (int) ceil((p->image_box.height() - 2*mm) / ph);
+      BBox big_box(cols*pw, rows*ph);
+      pdf_matrix placement = pos.place(p->image_box, big_box);
+
+      debug("Slicing [%.3f,%.3f] to %dx%d [%.3f,%.3f]", p->image_box.width(), p->image_box.height(), rows, cols, pw, ph);
+      for (int r=0; r<rows; r++)
+       for (int c=0; c<cols; c++)
+         {
+           pdf_matrix xf = placement;
+           xf.shift(-c*pw, -(rows-1-r)*ph);
+           xf.shift(margin.l, margin.t);
+           page *q = new xform_page(p, xf);
+           q->width = paper.w;
+           q->height = paper.h;
+           BBox slice_box = BBox(margin.l, margin.t, paper.w - margin.r, paper.h - margin.b);
+           q->image_box = p->image_box.transformed(xf);
+           q->image_box.intersect(slice_box);
+           BBox bleeding_slice = slice_box.enlarged(bleed);
+           out.push_back(new clip_page(q, bleeding_slice));
+         }
+    }
+
+  return out;
+}
+
+static const arg_def slice_args[] = {
+  PAPER_ARGS,
+  POS_ARGS,
+  MARGIN_ARGS1_NAMED("margin"),
+  MARGIN_ARGS2("margin"),
+  { "bleed",   AT_DIMEN,                       "Allow bleeding of image outside its box" },
+  { NULL,      0,                              NULL }
+};
+
 /*** Command table ***/
 
+template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
+
 const cmd_def cmd_table[] = {
-  { "null",    no_args,        0,      null_ctor       },
-  { "move",    move_args,      0,      move_ctor       },
-  { "scale",   scale_args,     0,      scale_ctor      },
-  { "rotate",  rotate_args,    0,      rotate_ctor     },
-  { "select",  no_args,        1,      select_ctor     },
-  { NULL,      NULL,           0,      NULL    }
+  { "add-blank",add_blank_args,        0,      &ctor<add_blank_cmd>,
+               "Add blank page(s) after each page" },
+  { "apply",   no_args,        1,      &ctor<apply_cmd>,
+               "Apply commands to selected pages" },
+  { "book",    book_args,      0,      &ctor<book_cmd>,
+               "Prepare booklets for book binding" },
+  { "clip",    clip_args,      0,      &ctor<clip_cmd>,
+               "Suppress page contents drawn outside the image box" },
+  { "common",  no_args,        0,      &ctor<common_cmd>,
+               "Use a common page size and image box for all pages" },
+  { "cropmarks",cropmarks_args,        0,      &ctor<cropmarks_cmd>,
+               "Draw cropping marks around the image box" },
+  { "debug",   no_args,        0,      &ctor<debug_cmd>,
+               "Draw debugging information on the page)" },
+  { "expand",  expand_args,    0,      &ctor<expand_cmd>,
+               "Expand paper around the image" },
+  { "fit",     fit_args,       0,      &ctor<fit_cmd>,
+               "Fit image to a given paper" },
+  { "flip",    flip_args,      0,      &ctor<flip_cmd>,
+               "Flip page horizontally and/or vertically" },
+  { "margins", margins_args,   0,      &ctor<margins_cmd>,
+               "Define image box by dimensions of margins around it" },
+  { "merge",   no_args,        0,      &ctor<merge_cmd>,
+               "Merge all pages to one by placing them one over another" },
+  { "modulo",  modulo_args,    1,      &ctor<modulo_cmd>,
+               "Act on n-tuples of pages" },
+  { "move",    move_args,      0,      &ctor<move_cmd>,
+               "Shift contents on the page" },
+  { "null",    no_args,        0,      &ctor<null_cmd>,
+               "Do nothing" },
+  { "nup",     nup_args,       0,      &ctor<nup_cmd>,
+               "Combine multiple pages to one (n-up printing)" },
+  { "paper",   paper_args,     0,      &ctor<paper_cmd>,
+               "Place image on a given paper" },
+  { "rotate",  rotate_args,    0,      &ctor<rotate_cmd>,
+               "Rotate the page by multiples of 90 degrees" },
+  { "scale",   scale_args,     0,      &ctor<scale_cmd>,
+               "Scale the page by a given factor" },
+  { "scaleto", scaleto_args,   0,      &ctor<scaleto_cmd>,
+               "Scale the page to a given size" },
+  { "select",  no_args,        1,      &ctor<select_cmd>,
+               "Select a subset of pages" },
+  { "slice",   slice_args,     0,      &ctor<slice_cmd>,
+               "Slice to smaller pages" },
+  { NULL,      NULL,           0,      NULL,
+               NULL, }
 };