+/*** 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 {