X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;ds=sidebyside;f=cmds.cc;h=7fad3028cccd0d462b3ba2d7cd72c7d258319c1a;hb=c3e865fc387ff75bc960a63f0bc434765c41b6eb;hp=8a5c0b4434e782a7c30de5d0c90480c30c7d8e3e;hpb=0527588aa3555c88b096f0ad2ab6147c3ec395d6;p=paperjam.git diff --git a/cmds.cc b/cmds.cc index 8a5c0b4..7fad302 100644 --- a/cmds.cc +++ b/cmds.cc @@ -1,167 +1,563 @@ +/* + * PaperJam -- Commands + * + * (c) 2018 Martin Mares + */ + #include #include #include +#include #include "jam.h" /*** null ***/ class null_cmd : public cmd_exec { - vector process(vector &pages) { return pages; } +public: + null_cmd(cmd *c UNUSED) { } + vector process(vector &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 process(vector &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 move_cmd::process(vector &pages) +// Commands acting on individual pages + +class cmd_exec_simple : public cmd_exec { + virtual page *process_page(page *p) = 0; + vector process(vector &pages) override; +}; + +vector cmd_exec_simple::process(vector &pages) { vector 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 process(vector &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 scale_cmd::process(vector &pages) -{ - vector 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]); + } }; -static cmd_exec *scale_ctor(cmd *c) +// 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; +}; + +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); + + 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)" } -class rotate_cmd : public cmd_exec { +// 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 process(vector &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 rotate_cmd::process(vector &pages) -{ - vector 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 process(vector &pages); +public: + select_cmd(cmd *c) + { + pipe = c->pipe; + } + vector process(vector &pages) override; }; static int validate_page_index(vector &pages, int idx) @@ -170,7 +566,7 @@ static int validate_page_index(vector &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 select_cmd::process(vector &pages) @@ -184,7 +580,7 @@ vector select_cmd::process(vector &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 + step; i += step) selected.push_back(pages[i]); } auto processed = run_command_list(pb->commands, selected); @@ -194,20 +590,1010 @@ vector select_cmd::process(vector &pages) return out; } -static cmd_exec *select_ctor(cmd *c) +/*** apply ***/ + +class apply_cmd : public cmd_exec { + pipeline *pipe; +public: + apply_cmd(cmd *c) + { + pipe = c->pipe; + } + vector process(vector &pages) override; +}; + +static pipeline_branch *find_branch(pipeline *pipe, vector &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); + if (f <= idx && idx <= t || t <= idx && idx <= f) + return pb; + } + return NULL; +} + +vector apply_cmd::process(vector &pages) +{ + vector out; + + int cnt = 0; + for (auto p: pages) + { + pipeline_branch *pb = find_branch(pipe, pages, cnt); + if (pb) + { + vector 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; +} + +/*** 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 process(vector &pages) override; +}; + +vector modulo_cmd::process(vector &pages) +{ + vector 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 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 process(vector &pages) override; +}; + +class merge_page : public page { + vector orig_pages; +public: + merge_page(vector &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 merge_cmd::process(vector &pages) +{ + vector 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 process(vector &pages) override; +}; + +vector add_blank_cmd::process(vector &pages) +{ + vector out; + + for (auto p: pages) + { + out.push_back(p); + for (int i=0; iwidth, 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 process(vector &pages) override; +}; + +vector book_cmd::process(vector &pages) { - select_cmd *r = new select_cmd; - r->pipe = c->pipe; - return r; + vector 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 &in); + void find_config(vector &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 process(vector &pages) override; +}; + +vector nup_cmd::process(vector &pages) +{ + vector 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 in; + if (fill_by == BY_TILE) + { + for (int j=0; jwidth, 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 &in, BBox *page_boxes) +{ + nup_state st; + + // Determine tile size + st.tile_w = st.tile_h = 0; + for (int i=0; iimage_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 orig_pages; + vector xforms; + vector 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 &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; iorig_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 process(vector &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 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 process(vector &pages) override; +}; + +vector slice_cmd::process(vector &pages) +{ + vector 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; rwidth = 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 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 page(s) after each page" }, + { "apply", no_args, 1, &ctor, + "Apply commands to selected pages" }, + { "book", book_args, 0, &ctor, + "Prepare booklets for book binding" }, + { "clip", clip_args, 0, &ctor, + "Suppress page contents drawn outside the image box" }, + { "common", no_args, 0, &ctor, + "Use a common page size and image box for all pages" }, + { "cropmarks",cropmarks_args, 0, &ctor, + "Draw cropping marks around the image box" }, + { "debug", no_args, 0, &ctor, + "Draw debugging information on the page)" }, + { "expand", expand_args, 0, &ctor, + "Expand paper around the image" }, + { "fit", fit_args, 0, &ctor, + "Fit image to a given paper" }, + { "flip", flip_args, 0, &ctor, + "Flip page horizontally and/or vertically" }, + { "margins", margins_args, 0, &ctor, + "Define image box by dimensions of margins around it" }, + { "merge", no_args, 0, &ctor, + "Merge all pages to one by placing them one over another" }, + { "modulo", modulo_args, 1, &ctor, + "Act on n-tuples of pages" }, + { "move", move_args, 0, &ctor, + "Shift contents on the page" }, + { "null", no_args, 0, &ctor, + "Do nothing" }, + { "nup", nup_args, 0, &ctor, + "Combine multiple pages to one (n-up printing)" }, + { "paper", paper_args, 0, &ctor, + "Place image on a given paper" }, + { "rotate", rotate_args, 0, &ctor, + "Rotate the page by multiples of 90 degrees" }, + { "scale", scale_args, 0, &ctor, + "Scale the page by a given factor" }, + { "scaleto", scaleto_args, 0, &ctor, + "Scale the page to a given size" }, + { "select", no_args, 1, &ctor, + "Select a subset of pages" }, + { "slice", slice_args, 0, &ctor, + "Slice to smaller pages" }, + { NULL, NULL, 0, NULL, + NULL, } };