/*
* PaperJam -- Commands
*
- * (c) 2018 Martin Mares <mj@ucw.cz>
+ * (c) 2018--2022 Martin Mares <mj@ucw.cz>
*/
#include <cassert>
#include <cstdlib>
#include <cstdio>
+#include <paper.h>
#include "jam.h"
};
static const arg_def no_args[] = {
- { NULL, 0 }
+ { NULL, 0, NULL }
};
-/*** Generic transformed page ***/
+/*** Generic routines ***/
-class xform_page : public page {
- page *orig_page;
- pdf_matrix xform;
+// Commands acting on individual pages
+
+class cmd_exec_simple : public cmd_exec {
+ virtual page *process_page(page *p) = 0;
+ vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> cmd_exec_simple::process(vector<page *> &pages)
+{
+ vector<page *> out;
+ for (auto p: pages)
+ out.push_back(process_page(p));
+ return out;
+}
+
+// Paper specifications
+
+class paper_spec {
public:
- void render(out_context *out, pdf_matrix xform) override;
- xform_page(page *p, pdf_matrix xf);
+ 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);
+ }
+ }
+};
+
+#define PAPER_ARGS \
+ { "paper", AT_STRING | AT_POSITIONAL, "Paper format name (e.g., a4)" }, \
+ { "w", AT_DIMEN, "Paper width" }, \
+ { "h", AT_DIMEN, "Paper height" }
+
+// Position specification
+
+class pos_spec {
+public:
+ 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;
+ }
};
-xform_page::xform_page(page *p, pdf_matrix xf)
+#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)
+ {
+ 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);
+ }
+ 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;
+ }
+};
+
+#define MARGIN_ARGS1_NAMED(name) \
+ { name, AT_DIMEN, "Size of all margins (default: 0)" }
+
+#define MARGIN_ARGS1_POSNL(name) \
+ { name, AT_DIMEN | AT_POSITIONAL, "Size of all margins (default: 0)" }
+
+#define MARGIN_ARGS2(sx) \
+ { "h" sx, AT_DIMEN, "Size of horizontal margins" }, \
+ { "v" sx, AT_DIMEN, "Size of vertical margins" }, \
+ { "l" sx, AT_DIMEN, "Size of left margin" }, \
+ { "r" sx, AT_DIMEN, "Size of right margin" }, \
+ { "t" sx, AT_DIMEN, "Size of top margin" }, \
+ { "b" sx, AT_DIMEN, "Size of bottom margin" }
+
+// Colors
+
+class color_spec {
+public:
+ double rgb[3];
+ color_spec()
+ {
+ rgb[0] = rgb[1] = rgb[2] = 0;
+ }
+ color_spec(string s)
+ {
+ if (s.length() != 6)
+ err("Invalid color specification \"%s\": expecting 6 hex digits", s.c_str());
+ for (int i=0; i<3; i++)
+ {
+ int x = 0;
+ for (int j=0; j<2; j++)
+ {
+ char c = s[2*i+j];
+ if (c >= '0' && c <= '9')
+ x = (x << 4) | (c - '0');
+ else if (c >= 'a' && c <= 'f')
+ x = (x << 4) | (c - 'a' + 10);
+ else if (c >= 'A' && c <= 'F')
+ x = (x << 4) | (c - 'A' + 10);
+ }
+ rgb[i] = x / 255.;
+ }
+ }
+ string to_string()
+ {
+ return pdf_coord(rgb[0]) + " " + pdf_coord(rgb[1]) + " " + pdf_coord(rgb[2]);
+ }
+};
+
+// Cropmarks
+
+class cropmark_spec {
+public:
+ enum mark_type {
+ MARK_NONE,
+ MARK_BOX,
+ MARK_CROSS,
+ MARK_OUT,
+ MARK_IN,
+ MARK_BG,
+ } type;
+ double pen_width;
+ double arm_length;
+ double offset;
+ color_spec color;
+ cropmark_spec()
+ {
+ type = MARK_NONE;
+ pen_width = 0.2;
+ arm_length = 5*mm;
+ offset = 0;
+ egstate = QPDFObjectHandle::newNull();
+ }
+ cropmark_spec(cmd *c, const string prefix="", const string def_type="cross") : color(c->arg(prefix + "color")->as_string("000000"))
+ {
+ string t = c->arg(prefix + "mark")->as_string(def_type);
+ if (t == "none")
+ type = MARK_NONE;
+ else if (t == "box")
+ type = MARK_BOX;
+ else if (t == "cross")
+ type = MARK_CROSS;
+ else if (t == "in")
+ type = MARK_IN;
+ else if (t == "out")
+ type = MARK_OUT;
+ else if (t == "bg")
+ type = MARK_BG;
+ else
+ err("Invalid cropmark type %s", t.c_str());
+
+ pen_width = c->arg(prefix + "pen")->as_double(0.2);
+ arm_length = c->arg(prefix + "len")->as_double(5*mm);
+ offset = c->arg(prefix + "offset")->as_double(0);
+ egstate = QPDFObjectHandle::newNull();
+ }
+ bool is_bg() { return (type == MARK_BG); }
+ string pdf_stream(out_context *out, BBox &box, pdf_matrix &xform);
+private:
+ string crop_cross(double x, double y, uint mask);
+ QPDFObjectHandle egstate;
+};
+
+string cropmark_spec::crop_cross(double x, double y, uint mask)
{
- orig_page = p;
- index = p->index;
- xform = xf;
+ string s = "";
- BBox media(p->width, p->height);
- media.transform(xf);
- width = media.width();
- height = media.height();
+ 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 ";
+ }
- bbox = p->bbox;
- bbox.transform(xf);
+ return s;
}
-void xform_page::render(out_context *out, pdf_matrix parent_xform)
+string cropmark_spec::pdf_stream(out_context *out, BBox &box, pdf_matrix &xform)
{
- orig_page->render(out, xform * parent_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;
}
-class cmd_exec_simple : public cmd_exec {
- virtual page *process_page(page *p) = 0;
- vector<page *> process(vector<page *> &pages) override;
-};
+#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)" }
-vector<page *> cmd_exec_simple::process(vector<page *> &pages)
+// Scaling preserving aspect ratio
+
+double scale_to_fit(BBox &from, BBox &to)
{
- vector<page *> out;
- for (auto p: pages)
- out.push_back(process_page(p));
- return out;
+ 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 ***/
{
pdf_matrix m;
m.shift(x, y);
- return new xform_page(p, m);
+ return new xform_page(p, "move", m);
}
};
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 ***/
{
pdf_matrix m;
m.scale(x_factor, y_factor);
- return new xform_page(p, m);
+ return new xform_page(p, "scale", m);
}
};
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 ***/
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
{
- pdf_matrix m;
- switch (deg)
- {
- case 0:
- break;
- case 90:
- m.rotate_deg(-90);
- m.shift(0, p->width);
- break;
- case 180:
- m.rotate_deg(180);
- m.shift(p->width, p->height);
- break;
- case 270:
- m.rotate_deg(90);
- m.shift(p->height, 0);
- break;
- default:
- abort();
- }
- return new xform_page(p, m);
+ return new xform_page(p, "rotate", pdf_rotation_matrix(deg, p->width, p->height));
}
};
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 ***/
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
{
m.scale(-1, 1);
m.shift(p->width, 0);
}
- return new xform_page(p, m);
+ return new xform_page(p, "flip", m);
}
};
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 ***/
{
pipe = c->pipe;
}
- vector<page *> process(vector<page *> &pages);
+ vector<page *> process(vector<page *> &pages) override;
};
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)
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);
{
pipe = c->pipe;
}
- vector<page *> process(vector<page *> &pages);
+ vector<page *> process(vector<page *> &pages) override;
};
static pipeline_branch *find_branch(pipeline *pipe, vector <page *> &pages, int idx)
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)
- die("Modulo must have n > 0");
+ err("Modulo must have n > 0");
+ half = c->arg("half")->as_int(0);
pipe = c->pipe;
}
- vector<page *> process(vector<page *> &pages);
+ vector<page *> process(vector<page *> &pages) override;
};
vector<page *> modulo_cmd::process(vector<page *> &pages)
{
vector<page *> out;
int tuples = ((int) pages.size() + n - 1) / n;
+ int use_tuples = half ? tuples/2 : tuples;
- for (int tuple=0; tuple < tuples; tuple++)
+ for (int tuple=0; tuple < use_tuples; tuple++)
{
debug("# Tuple %d", tuple);
debug_indent += 4;
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
}
static const arg_def modulo_args[] = {
- { "n", AT_INT | AT_MANDATORY | AT_POSITIONAL },
- { 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 }
+};
+
+/*** 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);
+ }
};
-/*** draw_bbox ***/
+/*** merge ***/
-class draw_bbox_cmd : public cmd_exec {
+class merge_cmd : public cmd_exec {
public:
- draw_bbox_cmd(cmd *c UNUSED) { }
- vector<page *> process(vector<page *> &pages);
+ merge_cmd(cmd *c UNUSED) { }
+ vector<page *> process(vector<page *> &pages) override;
};
-class draw_bbox_page : public page {
- page *orig_page;
+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, "paper", 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, "scaleto", 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, "fit", 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, "fit", 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, "expand", 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, "margins", pdf_matrix());
+ q->image_box = BBox(marg.l, marg.t, p->width - marg.r, p->height - marg.b);
+ if (q->image_box.width() < 0.001 || q->image_box.height() < 0.001)
+ err("Margins must result in positive image dimensions");
+ return q;
+ }
+};
+
+static const arg_def margins_args[] = {
+ MARGIN_ARGS1_POSNL("size"),
+ MARGIN_ARGS2(""),
+ { NULL, 0, NULL }
+};
+
+/*** add-blank ***/
+
+class add_blank_cmd : public cmd_exec {
+ int n;
+ paper_spec paper;
+public:
+ add_blank_cmd(cmd *c) : paper(c, true)
+ {
+ n = c->arg("n")->as_int(1);
+ }
+ vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> add_blank_cmd::process(vector<page *> &pages)
+{
+ vector<page *> out;
+
+ for (auto p: pages)
+ {
+ out.push_back(p);
+ for (int i=0; i<n; i++)
+ {
+ double w = paper.w, h = paper.h;
+ if (is_zero(w) || is_zero(h))
+ w = p->width, h = p->height;
+ out.push_back(new empty_page(w, h));
+ }
+ }
+
+ return out;
+}
+
+static const arg_def add_blank_args[] = {
+ { "n", AT_INT | AT_POSITIONAL, "Number of blank pages to add (default: 1)" },
+ PAPER_ARGS,
+ { NULL, 0, NULL }
+};
+
+/*** book ***/
+
+class book_cmd : public cmd_exec {
+ int n;
+public:
+ book_cmd(cmd *c)
+ {
+ n = c->arg("n")->as_int(0);
+ if (n % 4)
+ err("Number of pages per signature must be divisible by 4");
+ }
+ vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> book_cmd::process(vector<page *> &pages)
+{
+ vector<page *> in, out;
+
+ in = pages;
+ while (in.size() % 4)
+ in.push_back(new empty_page(in[0]->width, in[0]->height));
+
+ int i = 0;
+ while (i < (int) in.size())
+ {
+ int sig = in.size() - i;
+ if (n)
+ sig = min(sig, n);
+ for (int j=0; j<sig/2; j+=2)
+ {
+ out.push_back(in[i + sig-1-j]);
+ out.push_back(in[i + j]);
+ out.push_back(in[i + j+1]);
+ out.push_back(in[i + sig-2-j]);
+ }
+ i += sig;
+ }
+
+ return out;
+}
+
+static const arg_def book_args[] = {
+ { "n", AT_INT | AT_POSITIONAL, "Number of pages in a single booklet" },
+ { NULL, 0, NULL }
+};
+
+/*** nup ***/
+
+struct nup_state {
+ int rows, cols;
+ double tile_w, tile_h;
+ double paper_w, paper_h;
+ double fill_factor;
+ double scale;
+};
+
+class nup_cmd : public cmd_exec {
+ // Parameters
+ int grid_n, grid_m, num_tiles;
+ enum {
+ BY_ROWS,
+ BY_COLS,
+ BY_TILE,
+ } fill_by;
+ bool crop;
+ bool mixed;
+ int rotate;
+ double scale;
+ paper_spec paper;
+ margin_spec marg;
+ pos_spec pos;
+ pos_spec tpos;
+ double hspace, vspace;
+ cropmark_spec cmarks;
+
+ // Processing state
+ page *process_single(vector<page *> &in);
+ void find_config(vector<page *> &in, BBox *page_boxes);
+ void try_config(nup_state &st);
+ nup_state best;
+ bool found_solution;
+ BBox common_page_box;
+
+public:
+ nup_cmd(cmd *c) : paper(c), marg(c, "margin", "margin"), pos(c), tpos(c->arg("tpos")->as_string("tl")), cmarks(c, "c", "none")
+ {
+ grid_n = c->arg("n")->as_int(0);
+ grid_m = c->arg("m")->as_int(0);
+ if (grid_n > 0 && grid_m > 0)
+ num_tiles = grid_n * grid_m;
+ else if (grid_n > 0 && !grid_m)
+ num_tiles = grid_n;
+ else
+ err("Grid size must be at least 1x1");
+
+ const string by = c->arg("by")->as_string("rows");
+ if (by == "rows" || by == "row" || by == "r")
+ fill_by = BY_ROWS;
+ else if (by == "cols" || by == "cols" || by == "c")
+ fill_by = BY_COLS;
+ else if (by == "tile" || by == "t")
+ fill_by = BY_TILE;
+ else
+ err("Argument \"by\" must be rows/cols/tile");
+
+ crop = c->arg("crop")->as_int(0);
+ mixed = c->arg("mixed")->as_int(0);
+ rotate = c->arg("rotate")->as_int(-1);
+ scale = c->arg("scale")->as_double(0);
+
+ double space = c->arg("space")->as_double(0);
+ hspace = c->arg("hspace")->as_double(space);
+ vspace = c->arg("vspace")->as_double(space);
+
+ if (!is_zero(scale) && (!is_zero(paper.w) || rotate >= 0))
+ err("When used with explicit scaling, paper size nor rotation may be given");
+ if (!is_zero(scale) && !grid_m)
+ err("When used with explicit scaling, both grid sizes must be given");
+ }
+
+ vector<page *> process(vector<page *> &pages) override;
+};
+
+vector<page *> nup_cmd::process(vector<page *> &pages)
+{
+ vector<page *> out;
+
+ // Unless mixed is given, find the common page size
+ if (!mixed)
+ {
+ for (int i=0; i < (int) pages.size(); i++)
+ {
+ page *p = pages[i];
+ BBox pb = crop ? p->image_box : BBox(p->width, p->height);
+ if (!i)
+ common_page_box = pb;
+ else
+ common_page_box.join(pb);
+ }
+ debug("NUP: Common page box [%.3f %.3f]-[%.3f %.3f]",
+ common_page_box.x_min, common_page_box.y_min, common_page_box.x_max, common_page_box.y_max);
+ }
+
+ // Process one tile set after another
+ int i = 0;
+ while (i < (int) pages.size())
+ {
+ vector<page *> in;
+ if (fill_by == BY_TILE)
+ {
+ for (int j=0; j<num_tiles; j++)
+ in.push_back(pages[i]);
+ i++;
+ }
+ else
+ {
+ for (int j=0; j<num_tiles; j++)
+ {
+ if (i < (int) pages.size())
+ in.push_back(pages[i]);
+ else
+ in.push_back(new empty_page(in[0]->width, in[0]->height));
+ i++;
+ }
+ }
+ out.push_back(process_single(in));
+ }
+
+ return out;
+}
+
+void nup_cmd::try_config(nup_state &st)
+{
+ BBox window(st.paper_w, st.paper_h);
+ if (!marg.may_shrink(&window))
+ return;
+ marg.shrink_box(&window);
+ window.x_max -= (st.cols - 1) * hspace;
+ window.y_max -= (st.rows - 1) * vspace;
+ if (window.width() < 0 || window.height() < 0)
+ return;
+
+ BBox image(st.cols * st.tile_w, st.rows * st.tile_h);
+ st.scale = scale_to_fit(image, window);
+ st.fill_factor = (st.scale*image.width() * st.scale*image.height()) / (st.paper_w * st.paper_h);
+
+ if (debug_level > 1)
+ debug("Try: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
+ st.cols, st.rows,
+ st.paper_w, st.paper_h,
+ st.scale, st.fill_factor);
+
+ if (!found_solution || best.fill_factor < st.fill_factor)
+ {
+ found_solution = true;
+ best = st;
+ }
+}
+
+void nup_cmd::find_config(vector<page *> &in, BBox *page_boxes)
+{
+ nup_state st;
+
+ // Determine tile size
+ st.tile_w = st.tile_h = 0;
+ for (int i=0; i<num_tiles; i++)
+ {
+ if (!mixed)
+ page_boxes[i] = common_page_box;
+ else if (crop)
+ page_boxes[i] = in[i]->image_box;
+ else
+ page_boxes[i] = BBox(in[i]->width, in[i]->height);
+ st.tile_w = max(st.tile_w, page_boxes[i].width());
+ st.tile_h = max(st.tile_h, page_boxes[i].height());
+ }
+ debug("NUP: %d tiles of size [%.3f,%.3f]", num_tiles, st.tile_w, st.tile_h);
+ debug_indent += 4;
+
+ // Try all possible configurations of tiles
+ found_solution = false;
+ if (!is_zero(scale))
+ {
+ // If explicit scaling is requested, we choose page size ourselves,
+ // so there is just one configuration.
+ st.rows = grid_n;
+ st.cols = grid_m;
+ st.paper_w = marg.l + st.cols*st.tile_w + (st.cols-1)*hspace + marg.r;
+ st.paper_h = marg.t + st.rows*st.tile_h + (st.rows-1)*vspace + marg.b;
+ try_config(st);
+ }
+ else
+ {
+ // Page size is fixed (either given or copied from the source pages),
+ // but we can have freedom to rotate and/or to choose grid size.
+ for (int rot=0; rot<=1; rot++)
+ {
+ if (rotate >= 0 && rot != rotate)
+ continue;
+
+ // Establish paper size
+ if (!is_zero(paper.w))
+ {
+ st.paper_w = paper.w;
+ st.paper_h = paper.h;
+ }
+ else
+ {
+ st.paper_w = in[0]->width;
+ st.paper_h = in[0]->height;
+ }
+ if (rot)
+ swap(st.paper_w, st.paper_h);
+
+ // Try grid sizes
+ if (grid_m)
+ {
+ st.rows = grid_n;
+ st.cols = grid_m;
+ try_config(st);
+ }
+ else
+ {
+ for (int r=1; r<=grid_n; r++)
+ if (!(grid_n % r))
+ {
+ st.rows = r;
+ st.cols = grid_n / r;
+ try_config(st);
+ }
+ }
+ }
+ }
+
+ if (!found_solution)
+ err("No feasible solution found");
+ debug("Best: %dx%d on %.3f x %.3f => scale %.3f, fill %.6f",
+ best.cols, best.rows,
+ best.paper_w, best.paper_h,
+ best.scale, best.fill_factor);
+ debug_indent -= 4;
+}
+
+class nup_page : public page {
public:
- void render(out_context *out, pdf_matrix xform);
- draw_bbox_page(page *p) : page(p) { orig_page = p; }
+ vector<page *> orig_pages;
+ vector<pdf_matrix> xforms;
+ vector<BBox> tile_boxes;
+ cropmark_spec *cmarks;
+ void render(out_context *out, pdf_matrix xform) override;
+ nup_page(nup_state &st) : page(st.paper_w, st.paper_h) { }
+ void debug_dump() override
+ {
+ debug("N-up printing");
+ debug_indent += 4;
+ for (auto p: orig_pages)
+ p->debug_dump();
+ debug_indent -= 4;
+ }
};
-void draw_bbox_page::render(out_context *out, pdf_matrix xform)
+void nup_page::render(out_context *out, pdf_matrix parent_xform)
{
- orig_page->render(out, xform);
- out->contents +=
- "q " +
- xform.to_string() + " cm " +
- "0 1 0 RG " +
- bbox.to_rect() + " re S " +
- "Q ";
+ 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);
+ }
}
-vector<page *> draw_bbox_cmd::process(vector<page *> &pages)
+page *nup_cmd::process_single(vector<page *> &in)
+{
+ BBox page_boxes[num_tiles];
+ find_config(in, page_boxes);
+ double tw = best.scale * best.tile_w;
+ double th = best.scale * best.tile_h;
+
+ // Construct transform from paper to grid of tiles
+ BBox paper_box(best.paper_w, best.paper_h);
+ marg.shrink_box(&paper_box);
+ BBox grid_box(best.cols * tw + (best.cols-1) * hspace,
+ best.rows * th + (best.rows-1) * vspace);
+ pdf_matrix place_xform = pos.place(grid_box, paper_box);
+
+ nup_page *p = new nup_page(best);
+ p->image_box = grid_box;
+ p->image_box.transform(place_xform);
+
+ for (int i=0; i<num_tiles; i++)
+ {
+ int r, c;
+ if (fill_by == BY_ROWS || fill_by == BY_TILE)
+ {
+ r = i / best.cols;
+ c = i % best.cols;
+ }
+ else
+ {
+ c = i / best.rows;
+ r = i % best.rows;
+ }
+
+ pdf_matrix m;
+ BBox &page_box = page_boxes[i];
+ m.shift(-page_box.x_min, -page_box.y_min);
+ m.scale(best.scale);
+ page_box.transform(m);
+
+ double x = c * (tw + hspace);
+ double y = (best.rows-1-r) * (th + vspace);
+ BBox tile_box = BBox(x, y, x+tw, y+th);
+ m.concat(tpos.place(page_box, tile_box));
+
+ p->orig_pages.push_back(in[i]);
+ p->xforms.push_back(m * place_xform);
+ p->tile_boxes.push_back(tile_box.transformed(place_xform));
+ p->cmarks = &cmarks;
+ }
+
+ return p;
+}
+
+static const arg_def nup_args[] = {
+ { "n", AT_INT | AT_POSITIONAL | AT_MANDATORY, "Number of tiles on a page" },
+ { "m", AT_INT | AT_POSITIONAL, "If both n and m are given, produce n x m tiles on a page" },
+ { "by", AT_STRING, "Tile filling order: rows/cols/tile (default: rows)" },
+ { "crop", AT_SWITCH, "Crop input pages to their image box" },
+ { "mixed", AT_SWITCH, "Allow input pages of mixed sizes" },
+ { "rotate", AT_SWITCH, "Force (non-)rotation" },
+ { "scale", AT_DOUBLE, "Force specific scaling factor" },
+ PAPER_ARGS,
+ MARGIN_ARGS1_NAMED("margin"),
+ MARGIN_ARGS2("margin"),
+ POS_ARGS,
+ CROPMARK_ARGS("c"),
+ { "tpos", AT_STRING, "Position of images inside tiles (default: tl)" },
+ { "space", AT_DIMEN, "Space between tiles (default: 0)" },
+ { "hspace", AT_DIMEN, "Horizontal space between tiles (default: space)" },
+ { "vspace", AT_DIMEN, "Vertical space between tiles (default: space)" },
+ { NULL, 0, NULL }
+};
+
+/*** cropmarks ***/
+
+class cropmarks_page : public page {
+ page *orig_page;
+ cropmark_spec *cm;
+public:
+ void render(out_context *out, pdf_matrix xform) override
+ {
+ orig_page->render(out, xform);
+ out->contents += cm->pdf_stream(out, image_box, xform);
+ }
+ void debug_dump() override
+ {
+ debug("Add cropmarks");
+ orig_page->debug_dump();
+ }
+ cropmarks_page(page *p, cropmark_spec *cms) : page(p), orig_page(p), cm(cms) { }
+};
+
+class cropmarks_cmd : public cmd_exec_simple {
+ cropmark_spec cm;
+ page *process_page(page *p) override
+ {
+ return new cropmarks_page(p, &cm);
+ }
+public:
+ cropmarks_cmd(cmd *c) : cm(c) { }
+};
+
+static const arg_def cropmarks_args[] = {
+ CROPMARK_ARGS(""),
+ { NULL, 0, NULL }
+};
+
+/*** clip ***/
+
+class clip_page : public page {
+ page *orig_page;
+ BBox clip_to;
+public:
+ void render(out_context *out, pdf_matrix xform) override
+ {
+ out->contents += "q " + clip_to.transformed(xform).to_rect() + " re W n ";
+ orig_page->render(out, xform);
+ out->contents += "Q ";
+ }
+ void debug_dump() override
+ {
+ debug("Clip [%.3f %.3f %.3f %.3f]", clip_to.x_min, clip_to.y_min, clip_to.x_max, clip_to.y_max);
+ orig_page->debug_dump();
+ }
+ clip_page(page *p, BBox &to) : page(p), orig_page(p), clip_to(to) { }
+};
+
+class clip_cmd : public cmd_exec_simple {
+ double bleed;
+ page *process_page(page *p) override
+ {
+ BBox to = p->image_box.enlarged(bleed);
+ return new clip_page(p, to);
+ }
+public:
+ clip_cmd(cmd *c)
+ {
+ bleed = c->arg("bleed")->as_double(0);
+ }
+};
+
+static const arg_def clip_args[] = {
+ { "bleed", AT_DIMEN, "Allow bleeding of image outside its box" },
+ { NULL, 0, NULL }
+};
+
+/*** common ***/
+
+class common_cmd : public cmd_exec {
+ vector<page *> process(vector<page *> &pages) override
+ {
+ if (!pages.size())
+ return pages;
+
+ const page *first = pages[0];
+ BBox pbox(first->width, first->height);
+ BBox ibox = first->image_box;
+ for (auto p: pages)
+ {
+ BBox pg(p->width, p->height);
+ pbox.join(pg);
+ ibox.join(p->image_box);
+ }
+
+ vector<page *> out;
+ for (auto p: pages)
+ {
+ page *q = new xform_page(p, "common", 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)
- out.push_back(new draw_bbox_page(p));
+ {
+ 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, "slice", xf);
+ q->width = paper.w;
+ q->height = paper.h;
+ BBox slice_box = BBox(margin.l, margin.t, paper.w - margin.r, paper.h - margin.b);
+ q->image_box = p->image_box.transformed(xf);
+ q->image_box.intersect(slice_box);
+ BBox bleeding_slice = slice_box.enlarged(bleed);
+ out.push_back(new clip_page(q, bleeding_slice));
+ }
+ }
+
return out;
}
+static const arg_def slice_args[] = {
+ PAPER_ARGS,
+ POS_ARGS,
+ MARGIN_ARGS1_NAMED("margin"),
+ MARGIN_ARGS2("margin"),
+ { "bleed", AT_DIMEN, "Allow bleeding of image outside its box" },
+ { NULL, 0, NULL }
+};
+
/*** Command table ***/
template<typename T> cmd_exec *ctor(cmd *c) { return new T(c); }
const cmd_def cmd_table[] = {
- { "null", no_args, 0, &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> },
- { 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, }
};