qpdf_ruby 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +1,134 @@
1
1
  #include "struct_node.hpp"
2
2
 
3
- static void add_bbox(PDFStructWalker& walker, QPDFObjectHandle node) {
4
- int mcid = -std::numeric_limits<int>::infinity();
3
+ #include <optional>
4
+ #include <set>
5
5
 
6
- if (node.getKey("/K").isInteger()) {
7
- mcid = node.getKey("/K").getIntValue();
8
- }
9
-
10
- // Get the bbox from the walker or use defaults
11
-
12
- QPDFObjectHandle pageObj;
13
- QPDFObjectHandle currentNode = node;
14
- bool found = false;
6
+ namespace {
15
7
 
16
- // Try to find /Pg by walking up the parent chain
17
- while (currentNode.isDictionary()) {
18
- if (currentNode.hasKey("/Pg")) {
19
- pageObj = currentNode.getKey("/Pg");
20
- found = true;
21
- break;
22
- }
23
-
24
- if (currentNode.hasKey("/P")) {
25
- currentNode = currentNode.getKey("/P");
26
- } else {
27
- break; // No more parents to check
28
- }
8
+ // The page a structure element's content lives on: its own /Pg or the nearest ancestor's.
9
+ QPDFObjectHandle page_of(QPDFObjectHandle elem) {
10
+ for (int guard = 0; elem.isDictionary() && guard < 256; ++guard) {
11
+ if (elem.getKey("/Pg").isDictionary()) return elem.getKey("/Pg");
12
+ elem = elem.getKey("/P");
29
13
  }
14
+ return QPDFObjectHandle::newNull();
15
+ }
30
16
 
31
- if (!found && node.hasKey("/K") && node.getKey("/K").isArray()) {
32
- QPDFObjectHandle kids = node.getKey("/K");
33
- for (int i = 0; i < kids.getArrayNItems(); ++i) {
34
- QPDFObjectHandle kid = kids.getArrayItem(i);
35
- if (kid.isDictionary() && kid.hasKey("/Pg")) {
36
- pageObj = kid.getKey("/Pg");
37
- found = true;
38
- break;
39
- }
40
- }
17
+ // Unites the bounds of every piece of marked content below `elem`: direct MCIDs (in the content of
18
+ // the element's page), marked-content references (in their /Stm if given, else their own /Pg's
19
+ // content) and nested structure elements.
20
+ void collect_bounds(QPDFObjectHandle elem, qpdf_ruby::McidBounds const& bounds, std::optional<qpdf_ruby::Box>& out,
21
+ std::set<QPDFObjGen>& seen, int depth = 0) {
22
+ if (depth > 256 || (elem.isIndirect() && !seen.insert(elem.getObjGen()).second)) return;
23
+ QPDFObjectHandle page = page_of(elem);
24
+
25
+ auto add = [&](QPDFObjectHandle on_page, QPDFObjectHandle owner, int mcid) {
26
+ if (!on_page.isDictionary() || !(owner.isDictionary() || owner.isStream())) return;
27
+ auto it = bounds.find({on_page.getObjGen(), owner.getObjGen(), mcid});
28
+ if (it == bounds.end()) return;
29
+ out = out ? qpdf_ruby::unite(*out, it->second) : it->second;
30
+ };
31
+
32
+ QPDFObjectHandle kids = elem.getKey("/K");
33
+ std::vector<QPDFObjectHandle> list;
34
+ if (kids.isArray()) {
35
+ for (auto kid : kids.aitems()) list.push_back(kid);
36
+ } else if (!kids.isNull()) {
37
+ list.push_back(kids);
41
38
  }
42
39
 
43
- if (!found || !pageObj.isIndirect()) {
44
- std::cerr << "No /Pg key found for MCID " << mcid << ", cannot add BBox." << std::endl;
45
- return;
40
+ for (auto& kid : list) {
41
+ if (kid.isInteger()) {
42
+ add(page, page, kid.getIntValue());
43
+ } else if (kid.isDictionary() && kid.getKey("/Type").isNameAndEquals("/MCR") && kid.getKey("/MCID").isInteger()) {
44
+ QPDFObjectHandle on_page = kid.getKey("/Pg").isDictionary() ? kid.getKey("/Pg") : page;
45
+ // A /Stm that is not a stream names nothing we can look up: no bounds rather than the page's.
46
+ QPDFObjectHandle owner = kid.hasKey("/Stm") ? kid.getKey("/Stm") : on_page;
47
+ if (kid.hasKey("/Stm") && !owner.isStream()) continue;
48
+ add(on_page, owner, kid.getKey("/MCID").getIntValue());
49
+ } else if (kid.isDictionary() && kid.hasKey("/S")) {
50
+ collect_bounds(kid, bounds, out, seen, depth + 1);
51
+ }
46
52
  }
53
+ }
47
54
 
48
- auto media_box = walker.getPageCropBoxFor(pageObj);
49
-
50
- double llx = media_box[0], lly = media_box[1], urx = media_box[2], ury = media_box[3];
51
- auto const& bbox_map = walker.getMcidBboxMap();
55
+ bool is_layout(QPDFObjectHandle attrs) {
56
+ return attrs.isDictionary() && attrs.getKey("/O").isNameAndEquals("/Layout");
57
+ }
52
58
 
53
- // Use a try/catch to prevent failures when accessing the map
54
- try {
55
- if (bbox_map.count(mcid) > 0) {
56
- auto const& b = bbox_map.at(mcid);
57
- llx = b[0];
58
- lly = b[1];
59
- urx = b[2];
60
- ury = b[3];
59
+ // The element's Layout attribute dictionary - /A itself, or the first one in an /A array - or null.
60
+ QPDFObjectHandle layout_of(QPDFObjectHandle attrs) {
61
+ if (is_layout(attrs)) return attrs;
62
+ if (attrs.isArray()) {
63
+ for (auto item : attrs.aitems()) {
64
+ if (is_layout(item)) return item;
61
65
  }
62
- } catch (const std::exception& e) {
63
- std::cerr << "Error accessing mcid " << mcid << " in bbox map: " << e.what() << std::endl;
64
- // Continue with default values
65
66
  }
67
+ return QPDFObjectHandle::newNull();
68
+ }
66
69
 
67
- QPDFObjectHandle attrs;
68
- attrs = QPDFObjectHandle::newDictionary();
69
- attrs.replaceKey("/O", QPDFObjectHandle::newName("/Layout"));
70
-
71
- node.replaceKey("/A", attrs);
72
-
73
- if (!attrs.hasKey("/BBox")) {
74
- QPDFObjectHandle arr = QPDFObjectHandle::newArray();
75
- arr.appendItem(QPDFObjectHandle::newReal(llx));
76
- arr.appendItem(QPDFObjectHandle::newReal(lly));
77
- arr.appendItem(QPDFObjectHandle::newReal(urx));
78
- arr.appendItem(QPDFObjectHandle::newReal(ury));
79
-
80
- attrs.replaceKey("/BBox", arr);
70
+ // Structure types whose content is a line of text: a Figure among them is part of that line.
71
+ const std::set<std::string> kInlineParents = {"/P", "/H", "/H1", "/H2", "/H3", "/H4", "/H5",
72
+ "/H6", "/Lbl", "/Span", "/Quote", "/Note", "/Reference",
73
+ "/BibEntry", "/Code", "/Link", "/Annot", "/Ruby", "/Warichu",
74
+ "/Strong", "/Em"};
75
+
76
+ // True if the Figure sits in a line of text - its nearest parent that is not a NonStruct (which
77
+ // only groups, ISO 32000-1 14.8.4.2) is one of kInlineParents.
78
+ bool in_inline_context(QPDFObjectHandle elem) {
79
+ QPDFObjectHandle parent = elem.getKey("/P");
80
+ for (int guard = 0; guard < 64 && parent.isDictionary(); ++guard) {
81
+ if (!parent.getKey("/S").isNameAndEquals("/NonStruct")) break;
82
+ parent = parent.getKey("/P");
81
83
  }
84
+ QPDFObjectHandle type = parent.isDictionary() ? parent.getKey("/S") : QPDFObjectHandle::newNull();
85
+ return type.isName() && kInlineParents.count(type.getName()) > 0;
86
+ }
87
+
88
+ QPDFObjectHandle box_array(qpdf_ruby::Box const& box) {
89
+ QPDFObjectHandle array = QPDFObjectHandle::newArray();
90
+ for (double value : box) array.appendItem(QPDFObjectHandle::newReal(value, 4));
91
+ return array;
82
92
  }
83
93
 
94
+ } // namespace
95
+
96
+ // Gives the Figure a Layout attribute with a /BBox (its painted bounds) and, outside a line of text,
97
+ // /Placement /Block - without the placement PAC 2024 warns of a "possibly inappropriate use" of the
98
+ // Figure (checked 2026-10-02). Existing values are kept; an existing attribute object is extended.
84
99
  void FigureNode::ensureLayoutBBox(PDFStructWalker& walker) {
85
100
  StructElemNode::ensureLayoutBBox(walker);
86
101
 
87
- // Bail if a BBox is already present *anywhere* in /A
88
- if (node.hasKey("/A")) {
89
- QPDFObjectHandle A = node.getKey("/A");
90
- auto has_bbox = [](QPDFObjectHandle const& dict) {
91
- QPDFObjectHandle non_const_dict = dict;
92
- return non_const_dict.isDictionary() && non_const_dict.hasKey("/O") &&
93
- non_const_dict.getKey("/O").getName() == "/Layout" && non_const_dict.hasKey("/BBox");
94
- };
95
-
96
- if ((A.isDictionary() && has_bbox(A)) || (A.isArray() && [&]() -> bool {
97
- for (int i = 0; i < A.getArrayNItems(); ++i) {
98
- if (has_bbox(A.getArrayItem(i))) {
99
- return true;
100
- }
101
- }
102
- return false;
103
- }())) {
104
- return; // nothing to do
105
- }
102
+ QPDFObjectHandle attrs = node.getKey("/A");
103
+ QPDFObjectHandle layout = layout_of(attrs);
104
+ bool needs_bbox = !(layout.isDictionary() && layout.hasKey("/BBox"));
105
+ bool needs_placement = !(layout.isDictionary() && layout.hasKey("/Placement")) && !in_inline_context(node);
106
+ if (!needs_bbox && !needs_placement) return;
107
+
108
+ std::optional<qpdf_ruby::Box> box;
109
+ if (needs_bbox) {
110
+ std::set<QPDFObjGen> seen;
111
+ collect_bounds(node, walker.getMcidBounds(), box, seen);
112
+ QPDFObjectHandle page = page_of(node);
113
+ if (!box && page.isDictionary()) box = walker.getPageCropBoxFor(page); // else nothing to anchor it to
106
114
  }
115
+ if (!box && !needs_placement) return;
107
116
 
108
- add_bbox(walker, node);
109
- }
117
+ bool attached = layout.isDictionary();
118
+ if (!attached) {
119
+ layout = QPDFObjectHandle::newDictionary();
120
+ layout.replaceKey("/O", QPDFObjectHandle::newName("/Layout"));
121
+ }
122
+ if (box) layout.replaceKey("/BBox", box_array(*box));
123
+ if (needs_placement) layout.replaceKey("/Placement", QPDFObjectHandle::newName("/Block"));
124
+ if (attached) return;
125
+
126
+ // Keep every existing attribute object: append the new Layout dictionary to them.
127
+ if (attrs.isArray()) {
128
+ attrs.appendItem(layout);
129
+ } else if (attrs.isDictionary()) {
130
+ node.replaceKey("/A", QPDFObjectHandle::newArray({attrs, layout}));
131
+ } else {
132
+ node.replaceKey("/A", layout);
133
+ }
134
+ }
@@ -0,0 +1,317 @@
1
+ #include "mcid_bounds.hpp"
2
+ #include "stroke_bounds.hpp"
3
+
4
+ #include <qpdf/QPDFObjectHandle.hh>
5
+ #include <qpdf/QPDFPageObjectHelper.hh>
6
+
7
+ #include <algorithm>
8
+ #include <cmath>
9
+ #include <limits>
10
+ #include <optional>
11
+ #include <set>
12
+ #include <stack>
13
+ #include <vector>
14
+
15
+ namespace qpdf_ruby {
16
+
17
+ namespace {
18
+
19
+ using Matrix = std::array<double, 6>; // a b c d e f
20
+
21
+ constexpr Matrix kIdentity = {1, 0, 0, 1, 0, 0};
22
+
23
+ // m1 applied first, then m2 (PDF's row-vector convention: CTM' = m1 x CTM).
24
+ Matrix multiply(Matrix const& m1, Matrix const& m2) {
25
+ return {m1[0] * m2[0] + m1[1] * m2[2], m1[0] * m2[1] + m1[1] * m2[3],
26
+ m1[2] * m2[0] + m1[3] * m2[2], m1[2] * m2[1] + m1[3] * m2[3],
27
+ m1[4] * m2[0] + m1[5] * m2[2] + m2[4], m1[4] * m2[1] + m1[5] * m2[3] + m2[5]};
28
+ }
29
+
30
+ std::pair<double, double> transform(Matrix const& m, double x, double y) {
31
+ return {m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]};
32
+ }
33
+
34
+ struct Extent {
35
+ double llx = std::numeric_limits<double>::infinity();
36
+ double lly = std::numeric_limits<double>::infinity();
37
+ double urx = -std::numeric_limits<double>::infinity();
38
+ double ury = -std::numeric_limits<double>::infinity();
39
+
40
+ bool empty() const { return llx > urx || lly > ury; }
41
+ void add(double x, double y) {
42
+ llx = std::min(llx, x);
43
+ lly = std::min(lly, y);
44
+ urx = std::max(urx, x);
45
+ ury = std::max(ury, y);
46
+ }
47
+ void add_box(Matrix const& m, double x0, double y0, double x1, double y1) {
48
+ for (auto [x, y] : {transform(m, x0, y0), transform(m, x1, y0), transform(m, x1, y1), transform(m, x0, y1)}) {
49
+ add(x, y);
50
+ }
51
+ }
52
+ Box box() const { return {llx, lly, urx, ury}; }
53
+ };
54
+
55
+ std::optional<Matrix> matrix_of(QPDFObjectHandle array) {
56
+ if (!array.isArray() || array.getArrayNItems() != 6) return std::nullopt;
57
+ Matrix m;
58
+ for (int i = 0; i < 6; ++i) {
59
+ if (!array.getArrayItem(i).isNumber()) return std::nullopt;
60
+ m[i] = array.getArrayItem(i).getNumericValue();
61
+ }
62
+ return m;
63
+ }
64
+
65
+ int mcid_of(QPDFObjectHandle properties, QPDFObjectHandle resources) {
66
+ if (properties.isName() && resources.isDictionary()) {
67
+ QPDFObjectHandle named = resources.getKey("/Properties");
68
+ properties = named.isDictionary() ? named.getKey(properties.getName()) : QPDFObjectHandle::newNull();
69
+ }
70
+ if (properties.isDictionary() && properties.getKey("/MCID").isInteger()) {
71
+ return properties.getKey("/MCID").getIntValue();
72
+ }
73
+ return -1;
74
+ }
75
+
76
+ constexpr int kMaxFormDepth = 16;
77
+
78
+ // The parts of the graphics state that decide where paint lands; q/Q save and restore all of it.
79
+ struct GraphicsState {
80
+ Matrix ctm = kIdentity;
81
+ paths::StrokeStyle stroke;
82
+ };
83
+
84
+ class BoundsCollector : public QPDFObjectHandle::ParserCallbacks {
85
+ public:
86
+ // `owner` is the stream the content's MCIDs are numbered in (the page, or a Form XObject);
87
+ // `state` the graphics state it is drawn with; `active` the forms being walked (cycle guard).
88
+ BoundsCollector(QPDFObjGen page, QPDFObjGen owner, QPDFObjectHandle resources, McidBounds& out,
89
+ GraphicsState state, std::set<QPDFObjGen>& active, int depth = 0)
90
+ : page_(page), owner_(owner), resources_(std::move(resources)), out_(out), state_(state), active_(active),
91
+ depth_(depth) {}
92
+
93
+ void handleObject(QPDFObjectHandle obj, size_t, size_t) override {
94
+ if (!obj.isOperator()) {
95
+ operands_.push_back(obj);
96
+ return;
97
+ }
98
+ std::string const op = obj.getOperatorValue();
99
+
100
+ if (op == "q") {
101
+ state_stack_.push(state_);
102
+ } else if (op == "Q") {
103
+ if (!state_stack_.empty()) {
104
+ state_ = state_stack_.top();
105
+ state_stack_.pop();
106
+ }
107
+ } else if (op == "cm") {
108
+ std::vector<QPDFObjectHandle> six(operands_.end() - std::min<size_t>(6, operands_.size()), operands_.end());
109
+ if (auto m = matrix_of(QPDFObjectHandle::newArray(six))) state_.ctm = multiply(*m, state_.ctm);
110
+ } else if (op == "w" && number(1)) {
111
+ state_.stroke.width = *number(1);
112
+ } else if (op == "J" && number(1)) {
113
+ state_.stroke.cap = static_cast<int>(*number(1));
114
+ } else if (op == "j" && number(1)) {
115
+ state_.stroke.join = static_cast<int>(*number(1));
116
+ } else if (op == "M" && number(1)) {
117
+ state_.stroke.miter_limit = *number(1);
118
+ } else if (op == "gs" && !operands_.empty() && operands_.back().isName()) {
119
+ apply_ext_gstate(operands_.back().getName());
120
+ } else if (op == "BDC") {
121
+ mcids_.push(operands_.size() >= 2 ? mcid_of(operands_.back(), resources_) : -1);
122
+ } else if (op == "BMC") {
123
+ mcids_.push(-1);
124
+ } else if (op == "EMC") {
125
+ if (!mcids_.empty()) mcids_.pop();
126
+ } else if (op == "m" && point(1)) {
127
+ path_.push_back({*point(1), {}, false});
128
+ current_ = *point(1);
129
+ } else if (op == "l" && point(1)) {
130
+ add_segment(false, current_, current_, *point(1));
131
+ } else if (op == "c" && point(3)) {
132
+ add_segment(true, *point(3), *point(2), *point(1));
133
+ } else if (op == "v" && point(2)) {
134
+ add_segment(true, current_, *point(2), *point(1));
135
+ } else if (op == "y" && point(2)) {
136
+ add_segment(true, *point(2), *point(1), *point(1));
137
+ } else if (op == "h") {
138
+ close_subpath();
139
+ } else if (op == "re" && number(4)) {
140
+ double x = *number(4), y = *number(3), w = *number(2), h = *number(1);
141
+ path_.push_back({{x, y}, {}, false});
142
+ current_ = {x, y};
143
+ for (paths::Point p : {paths::Point{x + w, y}, paths::Point{x + w, y + h}, paths::Point{x, y + h}}) {
144
+ add_segment(false, current_, current_, p);
145
+ }
146
+ close_subpath();
147
+ } else if (op == "f" || op == "F" || op == "f*") {
148
+ paint(true, false);
149
+ } else if (op == "S") {
150
+ paint(false, true);
151
+ } else if (op == "s") {
152
+ close_subpath();
153
+ paint(false, true);
154
+ } else if (op == "B" || op == "B*") {
155
+ paint(true, true);
156
+ } else if (op == "b" || op == "b*") {
157
+ close_subpath();
158
+ paint(true, true);
159
+ } else if (op == "n") {
160
+ path_.clear();
161
+ } else if (op == "Do" && !operands_.empty() && operands_.back().isName()) {
162
+ draw_xobject(operands_.back().getName());
163
+ }
164
+ operands_.clear();
165
+ }
166
+
167
+ void handleEOF() override {}
168
+
169
+ private:
170
+ // The n-th numeric operand from the end (1 = the last), if there is one.
171
+ // (A copy of the handle: QPDF 11's accessors are not const.)
172
+ std::optional<double> number(size_t from_end) const {
173
+ if (operands_.size() < from_end) return std::nullopt;
174
+ QPDFObjectHandle operand = operands_[operands_.size() - from_end];
175
+ if (!operand.isNumber()) return std::nullopt;
176
+ return operand.getNumericValue();
177
+ }
178
+
179
+ // The n-th coordinate pair from the end (1 = the last two operands).
180
+ std::optional<paths::Point> point(size_t from_end) const {
181
+ auto x = number(2 * from_end);
182
+ auto y = number(2 * from_end - 1);
183
+ if (!x || !y) return std::nullopt;
184
+ return paths::Point{*x, *y};
185
+ }
186
+
187
+ // After h, a segment without a new m starts a new subpath at the closed one's start.
188
+ void add_segment(bool curve, paths::Point p1, paths::Point p2, paths::Point p3) {
189
+ if (path_.empty()) return; // no current point
190
+ if (path_.back().closed) path_.push_back({current_, {}, false});
191
+ path_.back().segments.push_back({curve, {current_, p1, p2, p3}});
192
+ current_ = p3;
193
+ }
194
+
195
+ void close_subpath() {
196
+ if (path_.empty() || path_.back().closed) return;
197
+ paths::Subpath& subpath = path_.back();
198
+ paths::Point start = subpath.start;
199
+ if (std::hypot(current_.x - start.x, current_.y - start.y) > 1e-9) add_segment(false, current_, current_, start);
200
+ path_.back().closed = true;
201
+ current_ = start;
202
+ }
203
+
204
+ void paint(bool fill, bool stroke) {
205
+ std::optional<Box> box;
206
+ auto unite_with = [&](std::optional<Box> const& more) {
207
+ if (more) box = box ? unite(*box, *more) : *more;
208
+ };
209
+ if (fill) unite_with(paths::fill_bounds(path_, state_.ctm));
210
+ if (stroke) unite_with(paths::stroke_bounds(path_, state_.ctm, state_.stroke));
211
+ if (box) record(*box);
212
+ path_.clear();
213
+ }
214
+
215
+ // Line width, cap, join and miter limit set through an ExtGState (/LW /LC /LJ /ML).
216
+ void apply_ext_gstate(std::string const& name) {
217
+ QPDFObjectHandle states = resources_.isDictionary() ? resources_.getKey("/ExtGState") : QPDFObjectHandle::newNull();
218
+ QPDFObjectHandle gs = states.isDictionary() ? states.getKey(name) : QPDFObjectHandle::newNull();
219
+ if (!gs.isDictionary()) return;
220
+ if (gs.getKey("/LW").isNumber()) state_.stroke.width = gs.getKey("/LW").getNumericValue();
221
+ if (gs.getKey("/LC").isInteger()) state_.stroke.cap = gs.getKey("/LC").getIntValueAsInt();
222
+ if (gs.getKey("/LJ").isInteger()) state_.stroke.join = gs.getKey("/LJ").getIntValueAsInt();
223
+ if (gs.getKey("/ML").isNumber()) state_.stroke.miter_limit = gs.getKey("/ML").getNumericValue();
224
+ }
225
+
226
+ void draw_xobject(std::string const& name) {
227
+ QPDFObjectHandle xobjects = resources_.isDictionary() ? resources_.getKey("/XObject") : QPDFObjectHandle::newNull();
228
+ QPDFObjectHandle xobject = xobjects.isDictionary() ? xobjects.getKey(name) : QPDFObjectHandle::newNull();
229
+ if (!xobject.isStream()) return;
230
+ QPDFObjectHandle dict = xobject.getDict();
231
+ Extent extent;
232
+ if (dict.getKey("/Subtype").isNameAndEquals("/Image")) {
233
+ extent.add_box(state_.ctm, 0, 0, 1, 1); // an image fills the unit square
234
+ } else if (dict.getKey("/Subtype").isNameAndEquals("/Form")) {
235
+ QPDFObjectHandle bbox = dict.getKey("/BBox");
236
+ if (!bbox.isArray() || bbox.getArrayNItems() != 4) return;
237
+ Matrix m = multiply(matrix_of(dict.getKey("/Matrix")).value_or(kIdentity), state_.ctm);
238
+ extent.add_box(m, bbox.getArrayItem(0).getNumericValue(), bbox.getArrayItem(1).getNumericValue(),
239
+ bbox.getArrayItem(2).getNumericValue(), bbox.getArrayItem(3).getNumericValue());
240
+ walk_form(xobject, m);
241
+ }
242
+ if (!extent.empty()) record(extent.box());
243
+ }
244
+
245
+ // The form's own MCIDs, keyed by the form; it starts from the graphics state it is drawn with,
246
+ // and its resources default to the ones it is drawn with.
247
+ void walk_form(QPDFObjectHandle form, Matrix const& m) {
248
+ if (depth_ >= kMaxFormDepth || !active_.insert(form.getObjGen()).second) return;
249
+ QPDFObjectHandle resources = form.getDict().getKey("/Resources");
250
+ GraphicsState inner_state = state_;
251
+ inner_state.ctm = m;
252
+ BoundsCollector inner(page_, form.getObjGen(), resources.isDictionary() ? resources : resources_, out_,
253
+ inner_state, active_, depth_ + 1);
254
+ form.parseAsContents(&inner);
255
+ active_.erase(form.getObjGen());
256
+ }
257
+
258
+ void record(Box const& box) {
259
+ int mcid = -1;
260
+ std::stack<int> copy = mcids_;
261
+ while (!copy.empty() && mcid < 0) {
262
+ mcid = copy.top();
263
+ copy.pop();
264
+ }
265
+ if (mcid < 0) return;
266
+ McidKey key{page_, owner_, mcid};
267
+ auto it = out_.find(key);
268
+ out_[key] = it == out_.end() ? box : unite(it->second, box);
269
+ }
270
+
271
+ QPDFObjGen page_;
272
+ QPDFObjGen owner_;
273
+ QPDFObjectHandle resources_;
274
+ McidBounds& out_;
275
+ std::vector<QPDFObjectHandle> operands_;
276
+ GraphicsState state_;
277
+ std::set<QPDFObjGen>& active_;
278
+ int depth_;
279
+ std::stack<GraphicsState> state_stack_;
280
+ std::stack<int> mcids_;
281
+ std::vector<paths::Subpath> path_;
282
+ paths::Point current_;
283
+ };
284
+
285
+ } // namespace
286
+
287
+ Box unite(Box const& a, Box const& b) {
288
+ return {std::min(a[0], b[0]), std::min(a[1], b[1]), std::max(a[2], b[2]), std::max(a[3], b[3])};
289
+ }
290
+
291
+ McidBounds find_mcid_bounds(QPDF& pdf) {
292
+ McidBounds bounds;
293
+ for (QPDFObjectHandle page : pdf.getAllPages()) {
294
+ QPDFPageObjectHelper helper(page);
295
+ McidBounds on_page;
296
+ std::set<QPDFObjGen> active;
297
+ BoundsCollector collector(page.getObjGen(), page.getObjGen(), helper.getAttribute("/Resources", false), on_page,
298
+ GraphicsState{}, active);
299
+ helper.parseContents(&collector);
300
+
301
+ QPDFObjectHandle crop = helper.getCropBox(); // falls back to the MediaBox; never copies
302
+ for (auto& [key, box] : on_page) {
303
+ if (crop.isArray() && crop.getArrayNItems() == 4) {
304
+ box[0] = std::max(box[0], crop.getArrayItem(0).getNumericValue());
305
+ box[1] = std::max(box[1], crop.getArrayItem(1).getNumericValue());
306
+ box[2] = std::min(box[2], crop.getArrayItem(2).getNumericValue());
307
+ box[3] = std::min(box[3], crop.getArrayItem(3).getNumericValue());
308
+ }
309
+ // Content entirely outside the crop box is invisible: no bounds rather than an inverted box.
310
+ if (box[0] > box[2] || box[1] > box[3]) continue;
311
+ bounds[key] = box;
312
+ }
313
+ }
314
+ return bounds;
315
+ }
316
+
317
+ } // namespace qpdf_ruby
@@ -0,0 +1,33 @@
1
+ #pragma once
2
+ #define POINTERHOLDER_TRANSITION 1
3
+
4
+ #include <qpdf/QPDF.hh>
5
+ #include <qpdf/QPDFObjGen.hh>
6
+
7
+ #include <array>
8
+ #include <map>
9
+ #include <tuple>
10
+
11
+ namespace qpdf_ruby {
12
+
13
+ // [llx, lly, urx, ury] in default user space.
14
+ using Box = std::array<double, 4>;
15
+
16
+ // MCIDs are unique per content stream only: the page's own content, or a Form XObject's (which a
17
+ // marked-content reference names with /Stm). Bounds are keyed by (page, stream, MCID); for the
18
+ // page's own content the stream is the page itself.
19
+ using McidKey = std::tuple<QPDFObjGen, QPDFObjGen, int>;
20
+ using McidBounds = std::map<McidKey, Box>;
21
+
22
+ // The area each piece of marked content paints: images, Form XObjects (their /BBox under their
23
+ // /Matrix), filled paths and stroked paths with their line width, caps and joins (stroke_bounds.hpp),
24
+ // under the full CTM (cm concatenates, q/Q save and restore the whole graphics state), clipped
25
+ // to the page's crop box (content wholly outside it gets no entry). Text adds nothing: its extent
26
+ // needs font metrics, so a Figure made only of text falls back to the crop box. Content belongs to
27
+ // the innermost enclosing MCID of its own stream; a Form XObject's content is also walked, under its
28
+ // /Matrix and the CTM it is drawn with, for the MCIDs numbered in that form.
29
+ McidBounds find_mcid_bounds(QPDF& pdf);
30
+
31
+ Box unite(Box const& a, Box const& b);
32
+
33
+ } // namespace qpdf_ruby
@@ -1,8 +1,8 @@
1
1
  #include "pdf_struct_walker.hpp"
2
2
  #include "struct_node.hpp"
3
3
 
4
- PDFStructWalker::PDFStructWalker(std::ostream& out, const std::unordered_map<int, std::array<double, 4>>& mcid2bbox)
5
- : out(out), mcid2bbox(const_cast<std::unordered_map<int, std::array<double, 4>>&>(mcid2bbox)) {}
4
+ PDFStructWalker::PDFStructWalker(std::ostream& out, qpdf_ruby::McidBounds bounds)
5
+ : out(out), mcid_bounds(std::move(bounds)) {}
6
6
 
7
7
  std::string PDFStructWalker::get_structure_as_string(QPDFObjectHandle const& node) {
8
8
  std::unique_ptr<StructNode> structNode = StructNode::fromQPDF(node);
@@ -19,28 +19,21 @@ void PDFStructWalker::ensureLayoutBBox(QPDFObjectHandle const& node) {
19
19
  void PDFStructWalker::buildPageObjectMap(QPDF& pdf) {
20
20
  pageObjToNumMap.clear();
21
21
  std::vector<QPDFObjectHandle> pages = pdf.getAllPages();
22
- for (int i = 0; i < pages.size(); ++i) {
22
+ for (size_t i = 0; i < pages.size(); ++i) {
23
23
  // Map the page's object ID to its 1-based page number
24
- pageObjToNumMap[pages.at(i).getObjGen()] = i + 1;
24
+ pageObjToNumMap[pages.at(i).getObjGen()] = static_cast<int>(i) + 1;
25
25
  }
26
26
  }
27
27
 
28
28
  const std::map<QPDFObjGen, int>& PDFStructWalker::getPageObjectMap() const { return pageObjToNumMap; }
29
29
 
30
30
  std::array<double, 4> PDFStructWalker::getPageCropBoxFor(QPDFObjectHandle const& page_oh) const {
31
- auto inherited = [](QPDFObjectHandle node, char const* key) -> QPDFObjectHandle {
32
- while (!node.isNull()) {
33
- if (auto val = node.getKey(key); !val.isNull()) return val;
34
- node = node.getKey("/Parent");
35
- }
36
- return QPDFObjectHandle(); // null ⇒ not found
37
- };
38
-
39
- QPDFObjectHandle crop = inherited(page_oh, "/CropBox");
40
- if (crop.isNull()) crop = inherited(page_oh, "/MediaBox"); // spec default
41
-
42
- std::array<double, 4> r;
43
- for (size_t i = 0; i < 4; ++i) r[i] = crop.getArrayItem(i).getNumericValue();
44
-
31
+ // getCropBox() follows inheritance through the page tree and falls back to the MediaBox. Its
32
+ // arguments would copy an inherited box into the page - a write in what is a read.
33
+ QPDFObjectHandle crop = QPDFPageObjectHelper(page_oh).getCropBox();
34
+ std::array<double, 4> r = {0, 0, 0, 0};
35
+ if (crop.isArray() && crop.getArrayNItems() == 4) {
36
+ for (int i = 0; i < 4; ++i) r[i] = crop.getArrayItem(i).getNumericValue();
37
+ }
45
38
  return r;
46
39
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  #pragma once
4
4
 
5
+ #define POINTERHOLDER_TRANSITION 1
6
+
5
7
  #include <qpdf/QPDF.hh>
6
8
  #include <qpdf/QPDFWriter.hh>
7
9
  #include <qpdf/QPDFPageObjectHelper.hh>
@@ -14,14 +16,16 @@
14
16
  #include <regex>
15
17
  #include <map>
16
18
 
19
+ #include "mcid_bounds.hpp"
20
+
17
21
  class PDFStructWalker {
18
22
  private:
19
23
  std::ostream& out;
20
24
  std::map<QPDFObjGen, int> pageObjToNumMap;
21
- std::unordered_map<int, std::array<double, 4>>& mcid2bbox;
25
+ qpdf_ruby::McidBounds mcid_bounds;
22
26
 
23
27
  public:
24
- PDFStructWalker(std::ostream& out = std::cout, const std::unordered_map<int, std::array<double, 4>>& mcid2bbox = {});
28
+ explicit PDFStructWalker(std::ostream& out = std::cout, qpdf_ruby::McidBounds bounds = {});
25
29
 
26
30
  void buildPageObjectMap(QPDF& pdf);
27
31
  std::string get_structure_as_string(QPDFObjectHandle const& node);
@@ -30,5 +34,5 @@ class PDFStructWalker {
30
34
  const std::map<QPDFObjGen, int>& getPageObjectMap() const;
31
35
  std::array<double, 4> getPageCropBoxFor(QPDFObjectHandle const& elem) const;
32
36
 
33
- const std::unordered_map<int, std::array<double, 4>>& getMcidBboxMap() const { return mcid2bbox; }
37
+ const qpdf_ruby::McidBounds& getMcidBounds() const { return mcid_bounds; }
34
38
  };