qpdf_ruby 0.1.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -5
- data/README.md +95 -54
- data/ext/qpdf_ruby/document_handle.cpp +1 -1
- data/ext/qpdf_ruby/figure_node.cpp +112 -87
- data/ext/qpdf_ruby/mcid_bounds.cpp +317 -0
- data/ext/qpdf_ruby/mcid_bounds.hpp +33 -0
- data/ext/qpdf_ruby/pdf_struct_walker.cpp +11 -18
- data/ext/qpdf_ruby/pdf_struct_walker.hpp +5 -3
- data/ext/qpdf_ruby/pdfua.cpp +1005 -0
- data/ext/qpdf_ruby/pdfua.hpp +118 -0
- data/ext/qpdf_ruby/qpdf_ruby.cpp +424 -212
- data/ext/qpdf_ruby/qpdf_ruby.hpp +1 -4
- data/ext/qpdf_ruby/ruby_guard.hpp +71 -0
- data/ext/qpdf_ruby/stroke_bounds.cpp +155 -0
- data/ext/qpdf_ruby/stroke_bounds.hpp +49 -0
- data/ext/qpdf_ruby/xmp_scan.cpp +329 -0
- data/ext/qpdf_ruby/xmp_scan.hpp +49 -0
- data/lib/qpdf_ruby/version.rb +1 -1
- data/lib/qpdf_ruby.rb +3 -2
- data/sig/qpdf_ruby.rbs +82 -1
- metadata +11 -12
- data/.clang-format +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -88
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/Rakefile +0 -25
- data/cliff.toml +0 -138
- data/docker/Dockerfile +0 -46
- data/ext/qpdf_ruby/pdf_image_mapper.cpp +0 -294
- data/ext/qpdf_ruby/pdf_image_mapper.hpp +0 -68
|
@@ -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,
|
|
5
|
-
: out(out),
|
|
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 (
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
#include <regex>
|
|
17
17
|
#include <map>
|
|
18
18
|
|
|
19
|
+
#include "mcid_bounds.hpp"
|
|
20
|
+
|
|
19
21
|
class PDFStructWalker {
|
|
20
22
|
private:
|
|
21
23
|
std::ostream& out;
|
|
22
24
|
std::map<QPDFObjGen, int> pageObjToNumMap;
|
|
23
|
-
|
|
25
|
+
qpdf_ruby::McidBounds mcid_bounds;
|
|
24
26
|
|
|
25
27
|
public:
|
|
26
|
-
PDFStructWalker(std::ostream& out = std::cout,
|
|
28
|
+
explicit PDFStructWalker(std::ostream& out = std::cout, qpdf_ruby::McidBounds bounds = {});
|
|
27
29
|
|
|
28
30
|
void buildPageObjectMap(QPDF& pdf);
|
|
29
31
|
std::string get_structure_as_string(QPDFObjectHandle const& node);
|
|
@@ -32,5 +34,5 @@ class PDFStructWalker {
|
|
|
32
34
|
const std::map<QPDFObjGen, int>& getPageObjectMap() const;
|
|
33
35
|
std::array<double, 4> getPageCropBoxFor(QPDFObjectHandle const& elem) const;
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const qpdf_ruby::McidBounds& getMcidBounds() const { return mcid_bounds; }
|
|
36
38
|
};
|