]> 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 b6c8eed7cdb355bd1f4f793a8e2be268bcb52b3a..7d0f9b659d934323bd02ef1b336c1811990867b6 100644 (file)
--- a/cmds.cc
+++ b/cmds.cc
@@ -20,7 +20,7 @@ public:
 };
 
 static const arg_def no_args[] = {
-  { NULL,      0 }
+  { NULL,      0,      NULL }
 };
 
 /*** Generic routines ***/
@@ -32,6 +32,11 @@ class xform_page : public page {
   pdf_matrix xform;
 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);
 };
 
@@ -86,13 +91,13 @@ public:
          return;
        }
       if (aw->given() != ah->given() || aname->given() == aw->given())
-       die("Either paper format name or width and height must be 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)
-           die("No paper called %s is known", name);
+           err("No paper called \"%s\" is known", name);
          w = paperpswidth(pap);
          h = paperpsheight(pap);
        }
@@ -105,9 +110,9 @@ public:
 };
 
 #define PAPER_ARGS \
-  { "paper",   AT_STRING | AT_POSITIONAL },    \
-  { "w",       AT_DIMEN },                     \
-  { "h",       AT_DIMEN }
+  { "paper",   AT_STRING | AT_POSITIONAL,              "Paper format name (e.g., a4)" },       \
+  { "w",       AT_DIMEN,                               "Paper width" },                        \
+  { "h",       AT_DIMEN,                               "Paper height" }
 
 // Position specification
 
@@ -118,7 +123,7 @@ public:
   pos_spec(string s)
     {
       if (s.size() != 2)
-        die("Value of pos must have two characters");
+        err("Value of pos must have two characters");
       if (s[0] == 't')
        v = 1;
       else if (s[0] == 'c')
@@ -126,7 +131,7 @@ public:
       else if (s[0] == 'b')
        v = -1;
       else
-       die("First character of pos must be t/c/b");
+       err("First character of pos must be t/c/b");
       if (s[1] == 'l')
        h = -1;
       else if (s[1] == 'c')
@@ -134,7 +139,7 @@ public:
       else if (s[1] == 'r')
        h = 1;
       else
-       die("Second character of pos must be l/c/r");
+       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)
@@ -173,7 +178,7 @@ public:
 };
 
 #define POS_ARGS \
-  { "pos",     AT_STRING }
+  { "pos",     AT_STRING,                      "Position on the page: (t|c|b)(l|c|r)" }
 
 // Margins
 
@@ -199,43 +204,47 @@ public:
     {
       bb->x_min += l;
       bb->x_max -= r;
-      bb->y_min += b;
-      bb->y_max -= t;
+      bb->y_min += t;
+      bb->y_max -= b;
       if (bb->x_min >= bb->x_max || bb->y_min >= bb->y_max)
-       die("Margins cannot be larger than the whole page");
+       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 -= b;
-      bb->y_max += t;
+      bb->y_min -= t;
+      bb->y_max += b;
     }
 };
 
-#define MARGIN_ARGS1_NAMED(name)       \
-  { name,      AT_DIMEN }
+#define MARGIN_ARGS1_NAMED(name)                                                       \
+  { name,      AT_DIMEN,                       "Size of all margins (default: 0)" }
 
-#define MARGIN_ARGS1_POSNL(name)       \
-  { name,      AT_DIMEN | AT_POSITIONAL }
+#define MARGIN_ARGS1_POSNL(name)                                                       \
+  { name,      AT_DIMEN | AT_POSITIONAL,       "Size of all margins (default: 0)" }
 
-#define MARGIN_ARGS2(sx)               \
-  { "h" sx,    AT_DIMEN },             \
-  { "v" sx,    AT_DIMEN },             \
-  { "l" sx,    AT_DIMEN },             \
-  { "r" sx,    AT_DIMEN },             \
-  { "t" sx,    AT_DIMEN },             \
-  { "b" sx,    AT_DIMEN }
+#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 {
-  double rgb[3];
 public:
+  double rgb[3];
+  color_spec()
+    {
+      rgb[0] = rgb[1] = rgb[2] = 0;
+    }
   color_spec(string s)
     {
       if (s.length() != 6)
-       die("Invalid color specification \"%s\": expecting 6 hex digits", s.c_str());
+       err("Invalid color specification \"%s\": expecting 6 hex digits", s.c_str());
       for (int i=0; i<3; i++)
        {
          int x = 0;
@@ -261,6 +270,7 @@ public:
 // Cropmarks
 
 class cropmark_spec {
+public:
   enum mark_type {
     MARK_NONE,
     MARK_BOX,
@@ -272,10 +282,15 @@ class cropmark_spec {
   double pen_width;
   double arm_length;
   double offset;
-  string crop_cross(double x, double y, uint mask);
-  QPDFObjectHandle egstate;
   color_spec color;
-public:
+  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);
@@ -292,7 +307,7 @@ public:
       else if (t == "bg")
        type = MARK_BG;
       else
-       die("Invalid cropmark type %s", t.c_str());
+       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);
@@ -301,6 +316,9 @@ public:
     }
   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;
 };
 
 string cropmark_spec::crop_cross(double x, double y, uint mask)
@@ -347,11 +365,7 @@ string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
   out->egstates.replaceKey(egs_res, egstate);
   s += egs_res + " gs ";
 
-  BBox b = box;
-  b.x_min -= offset;
-  b.x_max += offset;
-  b.y_min -= offset;
-  b.y_max += offset;
+  BBox b = box.enlarged(offset);
 
   switch (type)
     {
@@ -387,12 +401,12 @@ string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
   return s;
 }
 
-#define CROPMARK_ARGS(px)              \
-  { px "mark", AT_STRING },            \
-  { px "pen",  AT_DIMEN },             \
-  { px "len",  AT_DIMEN },             \
-  { px "offset",AT_DIMEN },            \
-  { px "color",        AT_STRING }
+#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
 
@@ -425,9 +439,9 @@ public:
 };
 
 static const arg_def move_args[] = {
-  { "x",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
-  { "y",       AT_DIMEN | AT_MANDATORY | AT_POSITIONAL },
-  { NULL,      0 }
+  { "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 ***/
@@ -449,9 +463,9 @@ public:
 };
 
 static const arg_def scale_args[] = {
-  { "x",       AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL },
-  { "y",       AT_DOUBLE | AT_POSITIONAL },
-  { NULL,      0 }
+  { "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 ***/
@@ -461,11 +475,11 @@ class rotate_cmd : public cmd_exec_simple {
 public:
   rotate_cmd(cmd *c)
     {
-      deg = c->arg("deg")->as_int(0) % 360;
+      deg = c->arg("angle")->as_int(0) % 360;
       if (deg < 0)
        deg += 360;
       if (deg % 90)
-       die("Rotate requires a multiple of 90 degrees");
+       err("The angle must be a multiple of 90 degrees");
     }
   page *process_page(page *p) override
     {
@@ -494,8 +508,8 @@ public:
 };
 
 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 }
 };
 
 /*** flip ***/
@@ -509,7 +523,7 @@ public:
       horizontal = c->arg("h")->as_int(0);
       vertical = c->arg("v")->as_int(0);
       if (!horizontal && !vertical)
-       die("Flip has no direction specified");
+       err("No direction specified");
     }
   page *process_page(page *p) override
     {
@@ -529,9 +543,9 @@ public:
 };
 
 static const arg_def flip_args[] = {
-  { "h",       AT_SWITCH },
-  { "v",       AT_SWITCH },
-  { NULL,      0 }
+  { "h",       AT_SWITCH,                              "Flip horizontally" },
+  { "v",       AT_SWITCH,                              "Flip vertically" },
+  { NULL,      0,                                      NULL }
 };
 
 /*** select ***/
@@ -552,7 +566,7 @@ 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)
@@ -566,7 +580,7 @@ vector<page *> select_cmd::process(vector<page *> &pages)
          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)
+         for (int i=f; i<=t; i += step)
            selected.push_back(pages[i]);
        }
       auto processed = run_command_list(pb->commands, selected);
@@ -636,7 +650,7 @@ public:
     {
       n = c->arg("n")->as_int(0);
       if (n <= 0)
-       die("Modulo must have n > 0");
+       err("Modulo must have n > 0");
       half = c->arg("half")->as_int(0);
       pipe = c->pipe;
     }
@@ -669,7 +683,7 @@ vector<page *> modulo_cmd::process(vector<page *> &pages)
                  else if (i < 0 && i >= -n)
                    j = (tuples-1-tuple)*n + (-i) - 1;
                  else
-                   die("Modulo: invalid index %d", i);
+                   err("Invalid index %d", i);
                  if (j < (int) pages.size())
                    tmp.push_back(pages[j]);
                  else
@@ -690,40 +704,53 @@ vector<page *> modulo_cmd::process(vector<page *> &pages)
 }
 
 static const arg_def modulo_args[] = {
-  { "n",       AT_INT | AT_MANDATORY | AT_POSITIONAL },
-  { "half",    AT_SWITCH },
-  { NULL,      0 }
+  { "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 }
 };
 
-/*** draw-bbox ***/
+/*** debug ***/
 
-class draw_bbox_page : public page {
+class debug_page : public page {
   page *orig_page;
+  cropmark_spec *page_cm, *image_cm;
 public:
-  void render(out_context *out, pdf_matrix xform) override;
-  draw_bbox_page(page *p) : page(p) { orig_page = p; }
+  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 draw_bbox_cmd : public cmd_exec_simple {
+class debug_cmd : public cmd_exec_simple {
+  cropmark_spec page_cmarks;
+  cropmark_spec image_cmarks;
 public:
-  draw_bbox_cmd(cmd *c UNUSED) { }
+  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 draw_bbox_page(p);
+      return new debug_page(p, &page_cmarks, &image_cmarks);
     }
 };
 
-void draw_bbox_page::render(out_context *out, pdf_matrix xform)
-{
-  orig_page->render(out, xform);
-  out->contents +=
-     "q " +
-     xform.to_string() + " cm " +
-     "0 1 0 RG " +
-     image_box.to_rect() + " re S " +
-     "Q ";
-}
-
 /*** merge ***/
 
 class merge_cmd : public cmd_exec {
@@ -751,7 +778,7 @@ public:
          else
            {
              if (!is_equal(width, p->width) || !is_equal(height, p->height))
-               die("All pages participating in a merge must have the same dimensions");
+               err("All pages must have the same dimensions");
              image_box.join(p->image_box);
            }
        }
@@ -761,6 +788,14 @@ public:
       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)
@@ -792,7 +827,7 @@ public:
 static const arg_def paper_args[] = {
   PAPER_ARGS,
   POS_ARGS,
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** scaleto ***/
@@ -820,7 +855,7 @@ public:
 static const arg_def scaleto_args[] = {
   PAPER_ARGS,
   POS_ARGS,
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** fit ***/
@@ -867,7 +902,7 @@ static const arg_def fit_args[] = {
   POS_ARGS,
   MARGIN_ARGS1_NAMED("margin"),
   MARGIN_ARGS2("margin"),
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** expand ***/
@@ -884,7 +919,7 @@ public:
       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)
-       die("Expansion must result in positive page dimensions");
+       err("Expansion must result in positive page dimensions");
       return q;
     }
 };
@@ -892,7 +927,7 @@ public:
 static const arg_def expand_args[] = {
   MARGIN_ARGS1_POSNL("by"),
   MARGIN_ARGS2(""),
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** margins ***/
@@ -903,14 +938,10 @@ public:
   margins_cmd(cmd *c) : marg(c, "size", "") { }
   page *process_page(page *p) override
     {
-      pdf_matrix xf;
-      xf.shift(-p->image_box.x_min, -p->image_box.y_min);
-      xf.shift(marg.l, marg.b);
-      page *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;
-      if (q->width < 0.001 || q->height < 0.001)
-       die("Margins must result in positive page dimensions");
+      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;
     }
 };
@@ -918,7 +949,7 @@ public:
 static const arg_def margins_args[] = {
   MARGIN_ARGS1_POSNL("size"),
   MARGIN_ARGS2(""),
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** add-blank ***/
@@ -954,9 +985,9 @@ vector<page *> add_blank_cmd::process(vector<page *> &pages)
 }
 
 static const arg_def add_blank_args[] = {
-  { "n",       AT_INT | AT_POSITIONAL },
+  { "n",       AT_INT | AT_POSITIONAL,         "Number of blank pages to add (default: 1)" },
   PAPER_ARGS,
-  { NULL,      0 }
+  { NULL,      0,                              NULL }
 };
 
 /*** book ***/
@@ -968,7 +999,7 @@ public:
     {
       n = c->arg("n")->as_int(0);
       if (n % 4)
-        die("Number of pages per signature must be divisible by 4");
+        err("Number of pages per signature must be divisible by 4");
     }
   vector<page *> process(vector<page *> &pages) override;
 };
@@ -1001,8 +1032,8 @@ vector<page *> book_cmd::process(vector<page *> &pages)
 }
 
 static const arg_def book_args[] = {
-  { "n",       AT_INT | AT_POSITIONAL },
-  { NULL,      0 }
+  { "n",       AT_INT | AT_POSITIONAL,         "Number of pages in a single booklet" },
+  { NULL,      0,                              NULL }
 };
 
 /*** nup ***/
@@ -1052,7 +1083,7 @@ public:
       else if (grid_n > 0 && !grid_m)
        num_tiles = grid_n;
       else
-       die("Grid size must be at least 1x1");
+       err("Grid size must be at least 1x1");
 
       const string by = c->arg("by")->as_string("rows");
       if (by == "rows" || by == "row" || by == "r")
@@ -1062,7 +1093,7 @@ public:
       else if (by == "tile" || by == "t")
        fill_by = BY_TILE;
       else
-       die("Parameter \"by\" must be rows/cols/tile");
+       err("Argument \"by\" must be rows/cols/tile");
 
       crop = c->arg("crop")->as_int(0);
       mixed = c->arg("mixed")->as_int(0);
@@ -1074,9 +1105,9 @@ public:
       vspace = c->arg("vspace")->as_double(space);
 
       if (!is_zero(scale) && (!is_zero(paper.w) || rotate >= 0))
-       die("When nup is used with explicit scaling, paper size nor rotation may be given");
+       err("When used with explicit scaling, paper size nor rotation may be given");
       if (!is_zero(scale) && !grid_m)
-       die("When nup is used with explicit scaling, both grid sizes must be given");
+       err("When used with explicit scaling, both grid sizes must be given");
     }
 
   vector<page *> process(vector<page *> &pages) override;
@@ -1145,10 +1176,11 @@ void nup_cmd::try_config(nup_state &st)
   st.scale = scale_to_fit(image, window);
   st.fill_factor = (st.scale*image.width() * st.scale*image.height()) / (st.paper_w * st.paper_h);
 
-  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 (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)
     {
@@ -1233,8 +1265,11 @@ void nup_cmd::find_config(vector<page *> &in, BBox *page_boxes)
     }
 
   if (!found_solution)
-    die("Nup did not find a feasible solution");
-  debug("Best: %dx%d on %.3f x %.3f", best.cols, best.rows, best.paper_w, best.paper_h);
+    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;
 }
 
@@ -1246,6 +1281,14 @@ public:
   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)
@@ -1313,23 +1356,23 @@ page *nup_cmd::process_single(vector<page *> &in)
 }
 
 static const arg_def nup_args[] = {
-  { "n",       AT_INT | AT_POSITIONAL | AT_MANDATORY },
-  { "m",       AT_INT | AT_POSITIONAL },
-  { "by",      AT_STRING },
-  { "crop",    AT_SWITCH },
-  { "mixed",   AT_SWITCH },
-  { "rotate",  AT_SWITCH },
-  { "scale",   AT_DOUBLE },
+  { "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 },
-  { "space",   AT_DIMEN },
-  { "hspace",  AT_DIMEN },
-  { "vspace",  AT_DIMEN },
-  { NULL,      0 }
+  { "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 ***/
@@ -1343,6 +1386,11 @@ public:
       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) { }
 };
 
@@ -1352,14 +1400,149 @@ class cropmarks_cmd : public cmd_exec_simple {
     {
       return new cropmarks_page(p, &cm);
     }
-
 public:
   cropmarks_cmd(cmd *c) : cm(c) { }
 };
 
 static const arg_def cropmarks_args[] = {
   CROPMARK_ARGS(""),
-  { NULL,      0 }
+  { 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 ***/
@@ -1367,24 +1550,50 @@ static const arg_def cropmarks_args[] = {
 template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
 
 const cmd_def cmd_table[] = {
-  { "null",    no_args,        0,      &ctor<null_cmd>         },
-  { "move",    move_args,      0,      &ctor<move_cmd>         },
-  { "scale",   scale_args,     0,      &ctor<scale_cmd>        },
-  { "rotate",  rotate_args,    0,      &ctor<rotate_cmd>       },
-  { "flip",    flip_args,      0,      &ctor<flip_cmd>         },
-  { "select",  no_args,        1,      &ctor<select_cmd>       },
-  { "apply",   no_args,        1,      &ctor<apply_cmd>        },
-  { "modulo",  modulo_args,    1,      &ctor<modulo_cmd>       },
-  { "draw-bbox",no_args,       0,      &ctor<draw_bbox_cmd>    },
-  { "merge",   no_args,        0,      &ctor<merge_cmd>        },
-  { "paper",   paper_args,     0,      &ctor<paper_cmd>        },
-  { "scaleto", scaleto_args,   0,      &ctor<scaleto_cmd>      },
-  { "fit",     fit_args,       0,      &ctor<fit_cmd>          },
-  { "expand",  expand_args,    0,      &ctor<expand_cmd>       },
-  { "margins", margins_args,   0,      &ctor<margins_cmd>      },
-  { "add-blank",add_blank_args,        0,      &ctor<add_blank_cmd>    },
-  { "book",    book_args,      0,      &ctor<book_cmd>         },
-  { "nup",     nup_args,       0,      &ctor<nup_cmd>          },
-  { "cropmarks",cropmarks_args,        0,      &ctor<cropmarks_cmd>    },
-  { 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, }
 };