4 * (c) 2018 Martin Mares <mj@ucw.cz>
16 class null_cmd : public cmd_exec {
18 null_cmd(cmd *c UNUSED) { }
19 vector<page *> process(vector<page *> &pages) override { return pages; }
22 static const arg_def no_args[] = {
26 /*** Generic routines ***/
30 class xform_page : public page {
34 void render(out_context *out, pdf_matrix xform) override;
35 void debug_dump() override
37 debug("Transform [%s]", xform.to_string().c_str());
38 orig_page->debug_dump();
40 xform_page(page *p, pdf_matrix xf);
43 xform_page::xform_page(page *p, pdf_matrix xf)
49 BBox media(p->width, p->height);
51 width = media.width();
52 height = media.height();
54 image_box = p->image_box;
55 image_box.transform(xf);
58 void xform_page::render(out_context *out, pdf_matrix parent_xform)
60 orig_page->render(out, xform * parent_xform);
63 // Commands acting on individual pages
65 class cmd_exec_simple : public cmd_exec {
66 virtual page *process_page(page *p) = 0;
67 vector<page *> process(vector<page *> &pages) override;
70 vector<page *> cmd_exec_simple::process(vector<page *> &pages)
74 out.push_back(process_page(p));
78 // Paper specifications
83 paper_spec(cmd *c, bool maybe=true)
85 arg_val *aname = c->arg("paper");
86 arg_val *aw = c->arg("w");
87 arg_val *ah = c->arg("h");
88 if (!aname->given() && !aw->given() && !ah->given() && maybe)
93 if (aw->given() != ah->given() || aname->given() == aw->given())
94 err("Either paper format name or width and height must be given");
97 const char *name = aname->as_string("").c_str();
98 const paper *pap = paperinfo(name);
100 err("No paper called \"%s\" is known", name);
101 w = paperpswidth(pap);
102 h = paperpsheight(pap);
106 w = aw->as_double(0);
107 h = ah->as_double(0);
113 { "paper", AT_STRING | AT_POSITIONAL, "Paper format name (e.g., a4)" }, \
114 { "w", AT_DIMEN, "Paper width" }, \
115 { "h", AT_DIMEN, "Paper height" }
117 // Position specification
122 pos_spec() { v = h = 0; }
126 err("Value of pos must have two characters");
129 else if (s[0] == 'c')
131 else if (s[0] == 'b')
134 err("First character of pos must be t/c/b");
137 else if (s[1] == 'c')
139 else if (s[1] == 'r')
142 err("Second character of pos must be l/c/r");
144 pos_spec(cmd *c) : pos_spec(c->arg("pos")->as_string("cc")) { }
145 pdf_matrix place(BBox &inner, BBox &outer)
148 m.shift(-inner.x_min, -inner.y_min);
154 m.shift((outer.width() - inner.width()) / 2, 0);
157 m.shift(outer.width() - inner.width(), 0);
167 m.shift(0, (outer.height() - inner.height()) / 2);
170 m.shift(0, outer.height() - inner.height());
175 m.shift(outer.x_min, outer.y_min);
181 { "pos", AT_STRING, "Position on the page: (t|c|b)(l|c|r)" }
188 margin_spec(cmd *c, string basic, string sx)
191 m = c->arg(basic)->as_double(0);
192 h = c->arg("h" + sx)->as_double(m);
193 v = c->arg("v" + sx)->as_double(m);
194 l = c->arg("l" + sx)->as_double(h);
195 r = c->arg("r" + sx)->as_double(h);
196 t = c->arg("t" + sx)->as_double(v);
197 b = c->arg("b" + sx)->as_double(v);
199 bool may_shrink(BBox *bb)
201 return (bb->width() > l+r && bb->height() > t+b);
203 void shrink_box(BBox *bb)
209 if (bb->x_min >= bb->x_max || bb->y_min >= bb->y_max)
210 err("Margins cannot be larger than the whole page");
212 void expand_box(BBox *bb)
221 #define MARGIN_ARGS1_NAMED(name) \
222 { name, AT_DIMEN, "Size of all margins (default: 0)" }
224 #define MARGIN_ARGS1_POSNL(name) \
225 { name, AT_DIMEN | AT_POSITIONAL, "Size of all margins (default: 0)" }
227 #define MARGIN_ARGS2(sx) \
228 { "h" sx, AT_DIMEN, "Size of horizontal margins" }, \
229 { "v" sx, AT_DIMEN, "Size of vertical margins" }, \
230 { "l" sx, AT_DIMEN, "Size of left margin" }, \
231 { "r" sx, AT_DIMEN, "Size of right margin" }, \
232 { "t" sx, AT_DIMEN, "Size of top margin" }, \
233 { "b" sx, AT_DIMEN, "Size of bottom margin" }
242 rgb[0] = rgb[1] = rgb[2] = 0;
247 err("Invalid color specification \"%s\": expecting 6 hex digits", s.c_str());
248 for (int i=0; i<3; i++)
251 for (int j=0; j<2; j++)
254 if (c >= '0' && c <= '9')
255 x = (x << 4) | (c - '0');
256 else if (c >= 'a' && c <= 'f')
257 x = (x << 4) | (c - 'a' + 10);
258 else if (c >= 'A' && c <= 'F')
259 x = (x << 4) | (c - 'A' + 10);
266 return pdf_coord(rgb[0]) + " " + pdf_coord(rgb[1]) + " " + pdf_coord(rgb[2]);
272 class cropmark_spec {
292 egstate = QPDFObjectHandle::newNull();
294 cropmark_spec(cmd *c, const string prefix="", const string def_type="cross") : color(c->arg(prefix + "color")->as_string("000000"))
296 string t = c->arg(prefix + "mark")->as_string(def_type);
301 else if (t == "cross")
310 err("Invalid cropmark type %s", t.c_str());
312 pen_width = c->arg(prefix + "pen")->as_double(0.2);
313 arm_length = c->arg(prefix + "len")->as_double(5*mm);
314 offset = c->arg(prefix + "offset")->as_double(0);
315 egstate = QPDFObjectHandle::newNull();
317 bool is_bg() { return (type == MARK_BG); }
318 string pdf_stream(out_context *out, BBox &box, pdf_matrix &xform);
320 string crop_cross(double x, double y, uint mask);
321 QPDFObjectHandle egstate;
324 string cropmark_spec::crop_cross(double x, double y, uint mask)
328 for (uint i=0; i<4; i++)
329 if (mask & (1U << i))
331 double x2 = x, y2 = y;
334 case 3: x2 -= arm_length; break;
335 case 2: x2 += arm_length; break;
336 case 1: y2 += arm_length; break;
337 case 0: y2 -= arm_length; break;
339 s += pdf_coord(x) + " " + pdf_coord(y) + " m " + pdf_coord(x2) + " " + pdf_coord(y2) + " l S ";
345 string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
347 if (type == MARK_NONE)
351 s += color.to_string() + (type == MARK_BG ? " rg " : " RG ");
352 s += xform.to_string() + " cm ";
354 if (egstate.isNull())
356 auto egs = QPDFObjectHandle::newDictionary();
357 egs.replaceKey("/Type", QPDFObjectHandle::newName("/ExtGState"));
358 egs.replaceKey("/LW", QPDFObjectHandle::newReal(pen_width, 1));
359 egs.replaceKey("/LC", QPDFObjectHandle::newInteger(2));
360 egs.replaceKey("/LJ", QPDFObjectHandle::newInteger(0));
361 egstate = out->pdf->makeIndirectObject(egs);
364 string egs_res = out->new_resource("GS");
365 out->egstates.replaceKey(egs_res, egstate);
366 s += egs_res + " gs ";
368 BBox b = box.enlarged(offset);
375 s += b.to_rect() + " re S ";
378 s += crop_cross(b.x_min, b.y_min, 0b1111);
379 s += crop_cross(b.x_max, b.y_min, 0b1111);
380 s += crop_cross(b.x_max, b.y_max, 0b1111);
381 s += crop_cross(b.x_min, b.y_max, 0b1111);
384 s += crop_cross(b.x_min, b.y_min, 0b0110);
385 s += crop_cross(b.x_max, b.y_min, 0b1010);
386 s += crop_cross(b.x_max, b.y_max, 0b1001);
387 s += crop_cross(b.x_min, b.y_max, 0b0101);
390 s += crop_cross(b.x_min, b.y_min, 0b1001);
391 s += crop_cross(b.x_max, b.y_min, 0b0101);
392 s += crop_cross(b.x_max, b.y_max, 0b0110);
393 s += crop_cross(b.x_min, b.y_max, 0b1010);
396 s += b.to_rect() + " re f ";
404 #define CROPMARK_ARGS(px) \
405 { px "mark", AT_STRING, "Cropmark style: box/cross/in/out/bg" }, \
406 { px "pen", AT_DIMEN, "Cropmark pen width (default: 0.2pt)" }, \
407 { px "len", AT_DIMEN, "Cropmark arm length (default: 5mm)" }, \
408 { px "offset",AT_DIMEN, "Cropmark offset outside the box (default: 0)" }, \
409 { px "color", AT_STRING, "Cropmark color (RRGGBB, default: 000000)" }
411 // Scaling preserving aspect ratio
413 double scale_to_fit(BBox &from, BBox &to)
415 double fw = from.width(), fh = from.height();
416 double tw = to.width(), th = to.height();
417 if (is_zero(fw) || is_zero(fh) || is_zero(tw) || is_zero(th))
420 return min(tw/fw, th/fh);
425 class move_cmd : public cmd_exec_simple {
430 x = c->arg("x")->as_double(0);
431 y = c->arg("y")->as_double(0);
433 page *process_page(page *p) override
437 return new xform_page(p, m);
441 static const arg_def move_args[] = {
442 { "x", AT_DIMEN | AT_MANDATORY | AT_POSITIONAL, "Move right by this distance" },
443 { "y", AT_DIMEN | AT_MANDATORY | AT_POSITIONAL, "Move up by this distance" },
449 class scale_cmd : public cmd_exec_simple {
450 double x_factor, y_factor;
454 x_factor = c->arg("x")->as_double(1);
455 y_factor = c->arg("y")->as_double(x_factor);
457 page *process_page(page *p) override
460 m.scale(x_factor, y_factor);
461 return new xform_page(p, m);
465 static const arg_def scale_args[] = {
466 { "x", AT_DOUBLE | AT_MANDATORY | AT_POSITIONAL, "Scale horizontally by this fraction" },
467 { "y", AT_DOUBLE | AT_POSITIONAL, "Scale vertically by this fraction (default: x)" },
473 class rotate_cmd : public cmd_exec_simple {
478 deg = c->arg("angle")->as_int(0) % 360;
482 err("The angle must be a multiple of 90 degrees");
484 page *process_page(page *p) override
493 m.shift(0, p->width);
497 m.shift(p->width, p->height);
501 m.shift(p->height, 0);
506 return new xform_page(p, m);
510 static const arg_def rotate_args[] = {
511 { "angle", AT_INT | AT_MANDATORY | AT_POSITIONAL, "Rotate clockwise by this angle" },
517 class flip_cmd : public cmd_exec_simple {
523 horizontal = c->arg("h")->as_int(0);
524 vertical = c->arg("v")->as_int(0);
525 if (!horizontal && !vertical)
526 err("No direction specified");
528 page *process_page(page *p) override
534 m.shift(0, p->height);
539 m.shift(p->width, 0);
541 return new xform_page(p, m);
545 static const arg_def flip_args[] = {
546 { "h", AT_SWITCH, "Flip horizontally" },
547 { "v", AT_SWITCH, "Flip vertically" },
553 class select_cmd : public cmd_exec {
560 vector<page *> process(vector<page *> &pages) override;
563 static int validate_page_index(vector<page *> &pages, int idx)
565 if (idx >= 1 && idx <= (int) pages.size())
567 if (idx <= -1 && idx >= (int) -pages.size())
568 return idx + pages.size();
569 err("Page index %d out of range", idx);
572 vector<page *> select_cmd::process(vector<page *> &pages)
575 for (auto pb: pipe->branches)
577 vector<page *> selected;
578 for (auto ps: pb->selectors)
580 int f = validate_page_index(pages, ps.from);
581 int t = validate_page_index(pages, ps.to);
582 int step = (f <= t) ? 1 : -1;
583 for (int i=f; i<=t; i += step)
584 selected.push_back(pages[i]);
586 auto processed = run_command_list(pb->commands, selected);
587 for (auto p: processed)
595 class apply_cmd : public cmd_exec {
602 vector<page *> process(vector<page *> &pages) override;
605 static pipeline_branch *find_branch(pipeline *pipe, vector <page *> &pages, int idx)
607 for (auto pb: pipe->branches)
608 for (auto ps: pb->selectors)
610 int f = validate_page_index(pages, ps.from);
611 int t = validate_page_index(pages, ps.to);
612 if (f <= idx && idx <= t || t <= idx && idx <= f)
618 vector<page *> apply_cmd::process(vector<page *> &pages)
625 pipeline_branch *pb = find_branch(pipe, pages, cnt);
630 auto processed = run_command_list(pb->commands, tmp);
631 for (auto q: processed)
644 class modulo_cmd : public cmd_exec {
651 n = c->arg("n")->as_int(0);
653 err("Modulo must have n > 0");
654 half = c->arg("half")->as_int(0);
657 vector<page *> process(vector<page *> &pages) override;
660 vector<page *> modulo_cmd::process(vector<page *> &pages)
663 int tuples = ((int) pages.size() + n - 1) / n;
664 int use_tuples = half ? tuples/2 : tuples;
666 for (int tuple=0; tuple < use_tuples; tuple++)
668 debug("# Tuple %d", tuple);
670 for (auto pb: pipe->branches)
673 for (auto ps: pb->selectors)
677 int step = (f <= t) ? 1 : -1;
678 for (int i=f; i<=t; i += step)
683 else if (i < 0 && i >= -n)
684 j = (tuples-1-tuple)*n + (-i) - 1;
686 err("Invalid index %d", i);
687 if (j < (int) pages.size())
688 tmp.push_back(pages[j]);
691 page *ref_page = pages[tuple*n];
692 tmp.push_back(new empty_page(ref_page->width, ref_page->height));
696 auto processed = run_command_list(pb->commands, tmp);
697 for (auto q: processed)
706 static const arg_def modulo_args[] = {
707 { "n", AT_INT | AT_MANDATORY | AT_POSITIONAL, "Number of pages in a single tuple" },
708 { "half", AT_SWITCH, "Process only the first half of n-tuples" },
714 class debug_page : public page {
716 cropmark_spec *page_cm, *image_cm;
718 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) { }
719 void render(out_context *out, pdf_matrix xform) override
721 orig_page->render(out, xform);
722 BBox page_bbox = BBox(0, 0, width, height);
723 out->contents += page_cm->pdf_stream(out, page_bbox, xform);
724 out->contents += image_cm->pdf_stream(out, image_box, xform);
726 void debug_dump() override
728 debug("Draw debugging boxes");
729 orig_page->debug_dump();
733 class debug_cmd : public cmd_exec_simple {
734 cropmark_spec page_cmarks;
735 cropmark_spec image_cmarks;
737 debug_cmd(cmd *c UNUSED)
739 page_cmarks.type = cropmark_spec::MARK_BOX;
740 page_cmarks.color.rgb[0] = 1;
741 page_cmarks.color.rgb[1] = 0;
742 page_cmarks.color.rgb[2] = 0;
743 image_cmarks.type = cropmark_spec::MARK_BOX;
744 image_cmarks.color.rgb[0] = 0;
745 image_cmarks.color.rgb[1] = 1;
746 image_cmarks.color.rgb[2] = 0;
748 page *process_page(page *p) override
750 return new debug_page(p, &page_cmarks, &image_cmarks);
756 class merge_cmd : public cmd_exec {
758 merge_cmd(cmd *c UNUSED) { }
759 vector<page *> process(vector<page *> &pages) override;
762 class merge_page : public page {
763 vector<page *> orig_pages;
765 merge_page(vector<page *> &orig) : page(0, 0)
775 image_box = p->image_box;
780 if (!is_equal(width, p->width) || !is_equal(height, p->height))
781 err("All pages must have the same dimensions");
782 image_box.join(p->image_box);
786 void render(out_context *out, pdf_matrix xform) override
788 for (auto p: orig_pages)
789 p->render(out, xform);
791 void debug_dump() override
793 debug("Merge pages");
795 for (auto p: orig_pages)
801 vector<page *> merge_cmd::process(vector<page *> &pages)
805 out.push_back(new merge_page(pages));
811 class paper_cmd : public cmd_exec_simple {
815 paper_cmd(cmd *c) : paper(c), pos(c) { }
816 page *process_page(page *p) override
818 BBox paper_box = BBox(paper.w, paper.h);
819 pdf_matrix xf = pos.place(p->image_box, paper_box);
820 page *q = new xform_page(p, xf);
827 static const arg_def paper_args[] = {
835 class scaleto_cmd : public cmd_exec_simple {
839 scaleto_cmd(cmd *c) : paper(c), pos(c) { }
840 page *process_page(page *p) override
842 BBox orig_box = BBox(p->width, p->height);
843 BBox paper_box = BBox(paper.w, paper.h);
845 xf.scale(scale_to_fit(orig_box, paper_box));
846 orig_box.transform(xf);
847 xf.concat(pos.place(orig_box, paper_box));
848 page *q = new xform_page(p, xf);
855 static const arg_def scaleto_args[] = {
863 class fit_cmd : public cmd_exec_simple {
868 fit_cmd(cmd *c) : paper(c, true), pos(c), marg(c, "margin", "margin") { }
869 page *process_page(page *p) override
874 if (!is_zero(paper.w) && !is_zero(paper.h))
876 // Paper given: scale image to fit paper
877 BBox orig_box = p->image_box;
878 BBox paper_box = BBox(paper.w, paper.h);
879 marg.shrink_box(&paper_box);
880 xf.scale(scale_to_fit(orig_box, paper_box));
881 orig_box.transform(xf);
882 xf.concat(pos.place(orig_box, paper_box));
883 q = new xform_page(p, xf);
889 // No paper given: adjust paper to fit image
890 xf.shift(-p->image_box.x_min, -p->image_box.y_min);
891 xf.shift(marg.l, marg.b);
892 q = new xform_page(p, xf);
893 q->width = p->image_box.width() + marg.l + marg.r;
894 q->height = p->image_box.height() + marg.t + marg.b;
900 static const arg_def fit_args[] = {
903 MARGIN_ARGS1_NAMED("margin"),
904 MARGIN_ARGS2("margin"),
910 class expand_cmd : public cmd_exec_simple {
913 expand_cmd(cmd *c) : marg(c, "by", "") { }
914 page *process_page(page *p) override
917 xf.shift(marg.l, marg.b);
918 page *q = new xform_page(p, xf);
919 q->width = p->width + marg.l + marg.r;
920 q->height = p->height + marg.t + marg.b;
921 if (q->width < 0.001 || q->height < 0.001)
922 err("Expansion must result in positive page dimensions");
927 static const arg_def expand_args[] = {
928 MARGIN_ARGS1_POSNL("by"),
935 class margins_cmd : public cmd_exec_simple {
938 margins_cmd(cmd *c) : marg(c, "size", "") { }
939 page *process_page(page *p) override
941 page *q = new xform_page(p, pdf_matrix());
942 q->image_box = BBox(marg.l, marg.t, p->width - marg.r, p->height - marg.b);
943 if (q->image_box.width() < 0.001 || q->image_box.height() < 0.001)
944 err("Margins must result in positive image dimensions");
949 static const arg_def margins_args[] = {
950 MARGIN_ARGS1_POSNL("size"),
957 class add_blank_cmd : public cmd_exec {
961 add_blank_cmd(cmd *c) : paper(c, true)
963 n = c->arg("n")->as_int(1);
965 vector<page *> process(vector<page *> &pages) override;
968 vector<page *> add_blank_cmd::process(vector<page *> &pages)
975 for (int i=0; i<n; i++)
977 double w = paper.w, h = paper.h;
978 if (is_zero(w) || is_zero(h))
979 w = p->width, h = p->height;
980 out.push_back(new empty_page(w, h));
987 static const arg_def add_blank_args[] = {
988 { "n", AT_INT | AT_POSITIONAL, "Number of blank pages to add (default: 1)" },
995 class book_cmd : public cmd_exec {
1000 n = c->arg("n")->as_int(0);
1002 err("Number of pages per signature must be divisible by 4");
1004 vector<page *> process(vector<page *> &pages) override;
1007 vector<page *> book_cmd::process(vector<page *> &pages)
1009 vector<page *> in, out;
1012 while (in.size() % 4)
1013 in.push_back(new empty_page(in[0]->width, in[0]->height));
1016 while (i < (int) in.size())
1018 int sig = in.size() - i;
1021 for (int j=0; j<sig/2; j+=2)
1023 out.push_back(in[i + sig-1-j]);
1024 out.push_back(in[i + j]);
1025 out.push_back(in[i + j+1]);
1026 out.push_back(in[i + sig-2-j]);
1034 static const arg_def book_args[] = {
1035 { "n", AT_INT | AT_POSITIONAL, "Number of pages in a single booklet" },
1043 double tile_w, tile_h;
1044 double paper_w, paper_h;
1049 class nup_cmd : public cmd_exec {
1051 int grid_n, grid_m, num_tiles;
1065 double hspace, vspace;
1066 cropmark_spec cmarks;
1069 page *process_single(vector<page *> &in);
1070 void find_config(vector<page *> &in, BBox *page_boxes);
1071 void try_config(nup_state &st);
1073 bool found_solution;
1074 BBox common_page_box;
1077 nup_cmd(cmd *c) : paper(c), marg(c, "margin", "margin"), pos(c), tpos(c->arg("tpos")->as_string("tl")), cmarks(c, "c", "none")
1079 grid_n = c->arg("n")->as_int(0);
1080 grid_m = c->arg("m")->as_int(0);
1081 if (grid_n > 0 && grid_m > 0)
1082 num_tiles = grid_n * grid_m;
1083 else if (grid_n > 0 && !grid_m)
1086 err("Grid size must be at least 1x1");
1088 const string by = c->arg("by")->as_string("rows");
1089 if (by == "rows" || by == "row" || by == "r")
1091 else if (by == "cols" || by == "cols" || by == "c")
1093 else if (by == "tile" || by == "t")
1096 err("Argument \"by\" must be rows/cols/tile");
1098 crop = c->arg("crop")->as_int(0);
1099 mixed = c->arg("mixed")->as_int(0);
1100 rotate = c->arg("rotate")->as_int(-1);
1101 scale = c->arg("scale")->as_double(0);
1103 double space = c->arg("space")->as_double(0);
1104 hspace = c->arg("hspace")->as_double(space);
1105 vspace = c->arg("vspace")->as_double(space);
1107 if (!is_zero(scale) && (!is_zero(paper.w) || rotate >= 0))
1108 err("When used with explicit scaling, paper size nor rotation may be given");
1109 if (!is_zero(scale) && !grid_m)
1110 err("When used with explicit scaling, both grid sizes must be given");
1113 vector<page *> process(vector<page *> &pages) override;
1116 vector<page *> nup_cmd::process(vector<page *> &pages)
1120 // Unless mixed is given, find the common page size
1123 for (int i=0; i < (int) pages.size(); i++)
1126 BBox pb = crop ? p->image_box : BBox(p->width, p->height);
1128 common_page_box = pb;
1130 common_page_box.join(pb);
1132 debug("NUP: Common page box [%.3f %.3f]-[%.3f %.3f]",
1133 common_page_box.x_min, common_page_box.y_min, common_page_box.x_max, common_page_box.y_max);
1136 // Process one tile set after another
1138 while (i < (int) pages.size())
1141 if (fill_by == BY_TILE)
1143 for (int j=0; j<num_tiles; j++)
1144 in.push_back(pages[i]);
1149 for (int j=0; j<num_tiles; j++)
1151 if (i < (int) pages.size())
1152 in.push_back(pages[i]);
1154 in.push_back(new empty_page(in[0]->width, in[0]->height));
1158 out.push_back(process_single(in));
1164 void nup_cmd::try_config(nup_state &st)
1166 BBox window(st.paper_w, st.paper_h);
1167 if (!marg.may_shrink(&window))
1169 marg.shrink_box(&window);
1170 window.x_max -= (st.cols - 1) * hspace;
1171 window.y_max -= (st.rows - 1) * vspace;
1172 if (window.width() < 0 || window.height() < 0)
1175 BBox image(st.cols * st.tile_w, st.rows * st.tile_h);
1176 st.scale = scale_to_fit(image, window);
1177 st.fill_factor = (st.scale*image.width() * st.scale*image.height()) / (st.paper_w * st.paper_h);
1179 if (debug_level > 1)
1180 debug("Try: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
1182 st.paper_w, st.paper_h,
1183 st.scale, st.fill_factor);
1185 if (!found_solution || best.fill_factor < st.fill_factor)
1187 found_solution = true;
1192 void nup_cmd::find_config(vector<page *> &in, BBox *page_boxes)
1196 // Determine tile size
1197 st.tile_w = st.tile_h = 0;
1198 for (int i=0; i<num_tiles; i++)
1201 page_boxes[i] = common_page_box;
1203 page_boxes[i] = in[i]->image_box;
1205 page_boxes[i] = BBox(in[i]->width, in[i]->height);
1206 st.tile_w = max(st.tile_w, page_boxes[i].width());
1207 st.tile_h = max(st.tile_h, page_boxes[i].height());
1209 debug("NUP: %d tiles of size [%.3f,%.3f]", num_tiles, st.tile_w, st.tile_h);
1212 // Try all possible configurations of tiles
1213 found_solution = false;
1214 if (!is_zero(scale))
1216 // If explicit scaling is requested, we choose page size ourselves,
1217 // so there is just one configuration.
1220 st.paper_w = marg.l + st.cols*st.tile_w + (st.cols-1)*hspace + marg.r;
1221 st.paper_h = marg.t + st.rows*st.tile_h + (st.rows-1)*vspace + marg.b;
1226 // Page size is fixed (either given or copied from the source pages),
1227 // but we can have freedom to rotate and/or to choose grid size.
1228 for (int rot=0; rot<=1; rot++)
1230 if (rotate >= 0 && rot != rotate)
1233 // Establish paper size
1234 if (!is_zero(paper.w))
1236 st.paper_w = paper.w;
1237 st.paper_h = paper.h;
1241 st.paper_w = in[0]->width;
1242 st.paper_h = in[0]->height;
1245 swap(st.paper_w, st.paper_h);
1256 for (int r=1; r<=grid_n; r++)
1260 st.cols = grid_n / r;
1267 if (!found_solution)
1268 err("No feasible solution found");
1269 debug("Best: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
1270 best.cols, best.rows,
1271 best.paper_w, best.paper_h,
1272 best.scale, best.fill_factor);
1276 class nup_page : public page {
1278 vector<page *> orig_pages;
1279 vector<pdf_matrix> xforms;
1280 vector<BBox> tile_boxes;
1281 cropmark_spec *cmarks;
1282 void render(out_context *out, pdf_matrix xform) override;
1283 nup_page(nup_state &st) : page(st.paper_w, st.paper_h) { }
1284 void debug_dump() override
1286 debug("N-up printing");
1288 for (auto p: orig_pages)
1294 void nup_page::render(out_context *out, pdf_matrix parent_xform)
1296 for (int i=0; i < (int) orig_pages.size(); i++)
1298 if (cmarks->is_bg())
1299 out->contents += cmarks->pdf_stream(out, tile_boxes[i], parent_xform);
1300 orig_pages[i]->render(out, xforms[i] * parent_xform);
1301 if (!cmarks->is_bg())
1302 out->contents += cmarks->pdf_stream(out, tile_boxes[i], parent_xform);
1306 page *nup_cmd::process_single(vector<page *> &in)
1308 BBox page_boxes[num_tiles];
1309 find_config(in, page_boxes);
1310 double tw = best.scale * best.tile_w;
1311 double th = best.scale * best.tile_h;
1313 // Construct transform from paper to grid of tiles
1314 BBox paper_box(best.paper_w, best.paper_h);
1315 marg.shrink_box(&paper_box);
1316 BBox grid_box(best.cols * tw + (best.cols-1) * hspace,
1317 best.rows * th + (best.rows-1) * vspace);
1318 pdf_matrix place_xform = pos.place(grid_box, paper_box);
1320 nup_page *p = new nup_page(best);
1321 p->image_box = grid_box;
1322 p->image_box.transform(place_xform);
1324 for (int i=0; i<num_tiles; i++)
1327 if (fill_by == BY_ROWS || fill_by == BY_TILE)
1339 BBox &page_box = page_boxes[i];
1340 m.shift(-page_box.x_min, -page_box.y_min);
1341 m.scale(best.scale);
1342 page_box.transform(m);
1344 double x = c * (tw + hspace);
1345 double y = (best.rows-1-r) * (th + vspace);
1346 BBox tile_box = BBox(x, y, x+tw, y+th);
1347 m.concat(tpos.place(page_box, tile_box));
1349 p->orig_pages.push_back(in[i]);
1350 p->xforms.push_back(m * place_xform);
1351 p->tile_boxes.push_back(tile_box.transformed(place_xform));
1352 p->cmarks = &cmarks;
1358 static const arg_def nup_args[] = {
1359 { "n", AT_INT | AT_POSITIONAL | AT_MANDATORY, "Number of tiles on a page" },
1360 { "m", AT_INT | AT_POSITIONAL, "If both n and m are given, produce n x m tiles on a page" },
1361 { "by", AT_STRING, "Tile filling order: rows/cols/tile (default: rows)" },
1362 { "crop", AT_SWITCH, "Crop input pages to their image box" },
1363 { "mixed", AT_SWITCH, "Allow input pages of mixed sizes" },
1364 { "rotate", AT_SWITCH, "Force (non-)rotation" },
1365 { "scale", AT_DOUBLE, "Force specific scaling factor" },
1367 MARGIN_ARGS1_NAMED("margin"),
1368 MARGIN_ARGS2("margin"),
1371 { "tpos", AT_STRING, "Position of images inside tiles (default: tl)" },
1372 { "space", AT_DIMEN, "Space between tiles (default: 0)" },
1373 { "hspace", AT_DIMEN, "Horizontal space between tiles (default: space)" },
1374 { "vspace", AT_DIMEN, "Vertical space between tiles (default: space)" },
1380 class cropmarks_page : public page {
1384 void render(out_context *out, pdf_matrix xform) override
1386 orig_page->render(out, xform);
1387 out->contents += cm->pdf_stream(out, image_box, xform);
1389 void debug_dump() override
1391 debug("Add cropmarks");
1392 orig_page->debug_dump();
1394 cropmarks_page(page *p, cropmark_spec *cms) : page(p), orig_page(p), cm(cms) { }
1397 class cropmarks_cmd : public cmd_exec_simple {
1399 page *process_page(page *p) override
1401 return new cropmarks_page(p, &cm);
1404 cropmarks_cmd(cmd *c) : cm(c) { }
1407 static const arg_def cropmarks_args[] = {
1414 class clip_page : public page {
1418 void render(out_context *out, pdf_matrix xform) override
1420 out->contents += "q " + clip_to.transformed(xform).to_rect() + " re W n ";
1421 orig_page->render(out, xform);
1422 out->contents += "Q ";
1424 void debug_dump() override
1426 debug("Clip [%.3f %.3f %.3f %.3f]", clip_to.x_min, clip_to.y_min, clip_to.x_max, clip_to.y_max);
1427 orig_page->debug_dump();
1429 clip_page(page *p, BBox &to) : page(p), orig_page(p), clip_to(to) { }
1432 class clip_cmd : public cmd_exec_simple {
1434 page *process_page(page *p) override
1436 BBox to = p->image_box.enlarged(bleed);
1437 return new clip_page(p, to);
1442 bleed = c->arg("bleed")->as_double(0);
1446 static const arg_def clip_args[] = {
1447 { "bleed", AT_DIMEN, "Allow bleeding of image outside its box" },
1453 class common_cmd : public cmd_exec {
1454 vector<page *> process(vector<page *> &pages) override
1459 const page *first = pages[0];
1460 BBox pbox(first->width, first->height);
1461 BBox ibox = first->image_box;
1464 BBox pg(p->width, p->height);
1466 ibox.join(p->image_box);
1472 page *q = new xform_page(p, pdf_matrix());
1473 q->width = pbox.width();
1474 q->height = pbox.height();
1475 q->image_box = ibox;
1482 common_cmd(cmd *c UNUSED) { }
1487 class slice_cmd : public cmd_exec {
1493 slice_cmd(cmd *c) : paper(c), pos(c), margin(c, "margin", "margin")
1495 if (is_zero(paper.w) || is_zero(paper.h))
1496 err("Paper format must be given");
1497 bleed = c->arg("bleed")->as_double(0);
1499 vector<page *> process(vector<page *> &pages) override;
1502 vector<page *> slice_cmd::process(vector<page *> &pages)
1508 double pw = paper.w - margin.l - margin.r;
1509 double ph = paper.h - margin.t - margin.b;
1510 if (pw < 0 || ph < 0)
1511 err("Margins larger than paper");
1513 int cols = (int) ceil((p->image_box.width() - 2*mm) / pw);
1514 int rows = (int) ceil((p->image_box.height() - 2*mm) / ph);
1515 BBox big_box(cols*pw, rows*ph);
1516 pdf_matrix placement = pos.place(p->image_box, big_box);
1518 debug("Slicing [%.3f,%.3f] to %dx%d [%.3f,%.3f]", p->image_box.width(), p->image_box.height(), rows, cols, pw, ph);
1519 for (int r=0; r<rows; r++)
1520 for (int c=0; c<cols; c++)
1522 pdf_matrix xf = placement;
1523 xf.shift(-c*pw, -(rows-1-r)*ph);
1524 xf.shift(margin.l, margin.t);
1525 page *q = new xform_page(p, xf);
1527 q->height = paper.h;
1528 BBox slice_box = BBox(margin.l, margin.t, paper.w - margin.r, paper.h - margin.b);
1529 q->image_box = p->image_box.transformed(xf);
1530 q->image_box.intersect(slice_box);
1531 BBox bleeding_slice = slice_box.enlarged(bleed);
1532 out.push_back(new clip_page(q, bleeding_slice));
1539 static const arg_def slice_args[] = {
1542 MARGIN_ARGS1_NAMED("margin"),
1543 MARGIN_ARGS2("margin"),
1544 { "bleed", AT_DIMEN, "Allow bleeding of image outside its box" },
1548 /*** Command table ***/
1550 template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
1552 const cmd_def cmd_table[] = {
1553 { "add-blank",add_blank_args, 0, &ctor<add_blank_cmd>,
1554 "Add blank page(s) after each page" },
1555 { "apply", no_args, 1, &ctor<apply_cmd>,
1556 "Apply commands to selected pages" },
1557 { "book", book_args, 0, &ctor<book_cmd>,
1558 "Prepare booklets for book binding" },
1559 { "clip", clip_args, 0, &ctor<clip_cmd>,
1560 "Suppress page contents drawn outside the image box" },
1561 { "common", no_args, 0, &ctor<common_cmd>,
1562 "Use a common page size and image box for all pages" },
1563 { "cropmarks",cropmarks_args, 0, &ctor<cropmarks_cmd>,
1564 "Draw cropping marks around the image box" },
1565 { "debug", no_args, 0, &ctor<debug_cmd>,
1566 "Draw debugging information on the page)" },
1567 { "expand", expand_args, 0, &ctor<expand_cmd>,
1568 "Expand paper around the image" },
1569 { "fit", fit_args, 0, &ctor<fit_cmd>,
1570 "Fit image to a given paper" },
1571 { "flip", flip_args, 0, &ctor<flip_cmd>,
1572 "Flip page horizontally and/or vertically" },
1573 { "margins", margins_args, 0, &ctor<margins_cmd>,
1574 "Define image box by dimensions of margins around it" },
1575 { "merge", no_args, 0, &ctor<merge_cmd>,
1576 "Merge all pages to one by placing them one over another" },
1577 { "modulo", modulo_args, 1, &ctor<modulo_cmd>,
1578 "Act on n-tuples of pages" },
1579 { "move", move_args, 0, &ctor<move_cmd>,
1580 "Shift contents on the page" },
1581 { "null", no_args, 0, &ctor<null_cmd>,
1583 { "nup", nup_args, 0, &ctor<nup_cmd>,
1584 "Combine multiple pages to one (n-up printing)" },
1585 { "paper", paper_args, 0, &ctor<paper_cmd>,
1586 "Place image on a given paper" },
1587 { "rotate", rotate_args, 0, &ctor<rotate_cmd>,
1588 "Rotate the page by multiples of 90 degrees" },
1589 { "scale", scale_args, 0, &ctor<scale_cmd>,
1590 "Scale the page by a given factor" },
1591 { "scaleto", scaleto_args, 0, &ctor<scaleto_cmd>,
1592 "Scale the page to a given size" },
1593 { "select", no_args, 1, &ctor<select_cmd>,
1594 "Select a subset of pages" },
1595 { "slice", slice_args, 0, &ctor<slice_cmd>,
1596 "Slice to smaller pages" },
1597 { NULL, NULL, 0, NULL,