+ 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;
+ }
+};
+
+#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)
+{
+ 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;
+}
+
+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)" }
+
+// 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 {