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.
@@ -3,9 +3,6 @@
3
3
 
4
4
  #include "ruby.h"
5
5
 
6
- VALUE rb_qpdf_mark_paths_as_artifacts(VALUE self);
7
- VALUE rb_qpdf_ensure_bboxs(VALUE self);
8
- VALUE rb_qpdf_get_structure_string(VALUE self);
9
- VALUE rb_qpdf_doc_set_encryption(int argc, VALUE* argv, VALUE self);
6
+ extern "C" void Init_qpdf_ruby(void);
10
7
 
11
8
  #endif /* QPDF_RUBY_H */
@@ -0,0 +1,71 @@
1
+ #pragma once
2
+
3
+ #include <ruby.h>
4
+
5
+ #include <cstring>
6
+ #include <exception>
7
+ #include <string>
8
+ #include <type_traits>
9
+ #include <utility>
10
+
11
+ extern VALUE rb_eQpdfRubyError; // QpdfRuby::Error, defined in Init_qpdf_ruby
12
+
13
+ namespace qpdf_ruby {
14
+
15
+ // Ruby raises by longjmp, which skips the destructors of every C++ object in the frames it leaves.
16
+ // So no Ruby exception may ever travel while a C++ object with a destructor is alive. The methods in
17
+ // qpdf_ruby.cpp keep to four rules:
18
+ // 1. Check and convert arguments as Ruby values first, before any C++ object exists (Check_Type,
19
+ // #to_s, NUM2INT may all raise).
20
+ // 2. Fetch the document handle (handle_of) only after the last conversion: #to_s or #to_int is
21
+ // Ruby code, and can call initialize on the same document, which frees the old handle.
22
+ // 3. Build C++ values (std::string, containers) only inside the lambda given to guarded.
23
+ // 4. Turn a C++ result into Ruby values only through guarded_to_ruby.
24
+ // Checked by bin/asan-rspec.sh (use-after-free, ordinary leaks) and by
25
+ // spec/qpdf_ruby/document_error_path_memory_spec.rb (destructors skipped by a raise, which
26
+ // LeakSanitizer cannot see).
27
+
28
+ // Runs C++ code from a Ruby method. A C++ exception becomes a QpdfRuby::Error only after the catch
29
+ // block - and every C++ object with a destructor - is gone.
30
+ template <typename Fn>
31
+ auto guarded(Fn&& fn) -> decltype(fn()) {
32
+ char message[1024] = {0};
33
+ {
34
+ std::string what;
35
+ try {
36
+ return std::forward<Fn>(fn)();
37
+ } catch (const std::exception& e) {
38
+ what = e.what();
39
+ } catch (...) {
40
+ what = "unknown C++ exception";
41
+ }
42
+ std::strncpy(message, what.c_str(), sizeof(message) - 1);
43
+ }
44
+ rb_raise(rb_eQpdfRubyError, "%s", message);
45
+ }
46
+
47
+ // guarded, then `build(result)` to make the Ruby return value. Allocating Ruby objects can raise
48
+ // (NoMemoryError); rb_protect stops that while the C++ result is alive, the result is destroyed, and
49
+ // only then is the exception re-raised.
50
+ template <typename Fn, typename Build>
51
+ VALUE guarded_to_ruby(Fn&& fn, Build&& build) {
52
+ int state = 0;
53
+ VALUE out = Qnil;
54
+ {
55
+ auto result = guarded(std::forward<Fn>(fn));
56
+ struct Call {
57
+ decltype(result)* value;
58
+ std::remove_reference_t<Build>* to_ruby;
59
+ } call{&result, &build};
60
+ out = rb_protect(
61
+ [](VALUE arg) -> VALUE {
62
+ auto* c = reinterpret_cast<Call*>(arg);
63
+ return (*c->to_ruby)(*c->value);
64
+ },
65
+ reinterpret_cast<VALUE>(&call), &state);
66
+ }
67
+ if (state) rb_jump_tag(state);
68
+ return out;
69
+ }
70
+
71
+ } // namespace qpdf_ruby
@@ -0,0 +1,155 @@
1
+ #include "stroke_bounds.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+ #include <limits>
6
+
7
+ namespace qpdf_ruby::paths {
8
+
9
+ namespace {
10
+
11
+ constexpr double kEpsilon = 1e-9;
12
+
13
+ Point operator+(Point a, Point b) { return {a.x + b.x, a.y + b.y}; }
14
+ Point operator-(Point a, Point b) { return {a.x - b.x, a.y - b.y}; }
15
+ Point operator*(Point a, double k) { return {a.x * k, a.y * k}; }
16
+ double length(Point a) { return std::hypot(a.x, a.y); }
17
+ bool same(Point a, Point b) { return length(a - b) < kEpsilon; }
18
+ Point unit(Point a) { return a * (1 / length(a)); }
19
+ Point left_normal(Point t) { return {-t.y, t.x}; }
20
+
21
+ class Bounds {
22
+ public:
23
+ explicit Bounds(Matrix const& m) : m_(m) {}
24
+
25
+ void point(Point p) {
26
+ double x = m_[0] * p.x + m_[2] * p.y + m_[4];
27
+ double y = m_[1] * p.x + m_[3] * p.y + m_[5];
28
+ add(x, y, 0, 0);
29
+ }
30
+
31
+ // A disk of radius r in user space becomes an ellipse; its box half-sizes follow from the matrix.
32
+ void disk(Point c, double r) {
33
+ double x = m_[0] * c.x + m_[2] * c.y + m_[4];
34
+ double y = m_[1] * c.x + m_[3] * c.y + m_[5];
35
+ add(x, y, r * std::hypot(m_[0], m_[2]), r * std::hypot(m_[1], m_[3]));
36
+ }
37
+
38
+ std::optional<Box> box() const {
39
+ if (llx_ > urx_) return std::nullopt;
40
+ return Box{llx_, lly_, urx_, ury_};
41
+ }
42
+
43
+ private:
44
+ void add(double x, double y, double dx, double dy) {
45
+ llx_ = std::min(llx_, x - dx);
46
+ lly_ = std::min(lly_, y - dy);
47
+ urx_ = std::max(urx_, x + dx);
48
+ ury_ = std::max(ury_, y + dy);
49
+ }
50
+
51
+ Matrix const& m_;
52
+ double llx_ = std::numeric_limits<double>::infinity();
53
+ double lly_ = std::numeric_limits<double>::infinity();
54
+ double urx_ = -std::numeric_limits<double>::infinity();
55
+ double ury_ = -std::numeric_limits<double>::infinity();
56
+ };
57
+
58
+ // Direction a segment leaves its start / enters its end; nullopt for a segment of zero length.
59
+ std::optional<Point> start_tangent(Segment const& s) {
60
+ for (int i = 1; i <= 3; ++i) {
61
+ if ((s.curve || i == 3) && !same(s.p[i], s.p[0])) return unit(s.p[i] - s.p[0]);
62
+ }
63
+ return std::nullopt;
64
+ }
65
+
66
+ std::optional<Point> end_tangent(Segment const& s) {
67
+ for (int i = 2; i >= 0; --i) {
68
+ if ((s.curve || i == 0) && !same(s.p[3], s.p[i])) return unit(s.p[3] - s.p[i]);
69
+ }
70
+ return std::nullopt;
71
+ }
72
+
73
+ void cap(Bounds& out, Point at, Point outward, double r, StrokeStyle const& style) {
74
+ if (style.cap == 1) {
75
+ out.disk(at, r);
76
+ } else if (style.cap == 2) {
77
+ Point n = left_normal(outward) * r;
78
+ Point tip = at + outward * r;
79
+ out.point(tip + n);
80
+ out.point(tip - n);
81
+ }
82
+ }
83
+
84
+ // The join between a segment arriving in direction `a` and the next leaving in direction `b`.
85
+ void join(Bounds& out, Point at, Point a, Point b, double r, StrokeStyle const& style) {
86
+ if (style.join == 1) {
87
+ out.disk(at, r);
88
+ return;
89
+ }
90
+ if (style.join != 0) return; // bevel: the segments' own corners already bound it
91
+ double cos_turn = std::clamp(a.x * b.x + a.y * b.y, -1.0, 1.0);
92
+ double half = std::sqrt((1 + cos_turn) / 2); // cos of half the turn angle
93
+ if (cos_turn > 1 - kEpsilon || half < kEpsilon || 1 / half > style.miter_limit) return; // straight, or beveled
94
+ Point bisector = left_normal(a) + left_normal(b);
95
+ if (length(bisector) < kEpsilon) return;
96
+ bisector = unit(bisector);
97
+ if (a.x * b.y - a.y * b.x > 0) bisector = bisector * -1; // a left turn has its point on the right
98
+ out.point(at + bisector * (r / half));
99
+ }
100
+
101
+ void stroke_subpath(Bounds& out, Subpath const& subpath, double r, StrokeStyle const& style) {
102
+ std::vector<Segment const*> drawn;
103
+ for (auto const& s : subpath.segments) {
104
+ if (start_tangent(s)) drawn.push_back(&s);
105
+ }
106
+ if (drawn.empty()) {
107
+ // A degenerate subpath is painted only with round caps, as a dot (ISO 32000-1, 8.5.3.2).
108
+ if (style.cap == 1 && (subpath.closed || !subpath.segments.empty())) out.disk(subpath.start, r);
109
+ return;
110
+ }
111
+
112
+ for (auto const* s : drawn) {
113
+ if (s->curve) {
114
+ for (auto const& p : s->p) out.disk(p, r);
115
+ } else {
116
+ Point n = left_normal(*start_tangent(*s)) * r;
117
+ for (Point p : {s->p[0], s->p[3]}) {
118
+ out.point(p + n);
119
+ out.point(p - n);
120
+ }
121
+ }
122
+ }
123
+ for (size_t i = 0; i + 1 < drawn.size(); ++i) {
124
+ join(out, drawn[i]->p[3], *end_tangent(*drawn[i]), *start_tangent(*drawn[i + 1]), r, style);
125
+ }
126
+ if (subpath.closed) {
127
+ join(out, drawn.back()->p[3], *end_tangent(*drawn.back()), *start_tangent(*drawn.front()), r, style);
128
+ } else {
129
+ cap(out, drawn.front()->p[0], *start_tangent(*drawn.front()) * -1, r, style);
130
+ cap(out, drawn.back()->p[3], *end_tangent(*drawn.back()), r, style);
131
+ }
132
+ }
133
+
134
+ } // namespace
135
+
136
+ std::optional<Box> fill_bounds(std::vector<Subpath> const& path, Matrix const& ctm) {
137
+ Bounds out(ctm);
138
+ for (auto const& subpath : path) {
139
+ if (subpath.segments.empty()) continue;
140
+ out.point(subpath.start);
141
+ for (auto const& s : subpath.segments) {
142
+ for (auto const& p : s.p) out.point(p);
143
+ }
144
+ }
145
+ return out.box();
146
+ }
147
+
148
+ std::optional<Box> stroke_bounds(std::vector<Subpath> const& path, Matrix const& ctm, StrokeStyle const& style) {
149
+ Bounds out(ctm);
150
+ double r = std::fabs(style.width) / 2;
151
+ for (auto const& subpath : path) stroke_subpath(out, subpath, r, style);
152
+ return out.box();
153
+ }
154
+
155
+ } // namespace qpdf_ruby::paths
@@ -0,0 +1,49 @@
1
+ #pragma once
2
+
3
+ #include "mcid_bounds.hpp"
4
+
5
+ #include <array>
6
+ #include <optional>
7
+ #include <vector>
8
+
9
+ namespace qpdf_ruby::paths {
10
+
11
+ using Matrix = std::array<double, 6>; // a b c d e f, PDF's row-vector convention
12
+
13
+ struct Point {
14
+ double x = 0;
15
+ double y = 0;
16
+ };
17
+
18
+ // A line uses `p[0]` and `p[3]`; a cubic Bezier curve all four.
19
+ struct Segment {
20
+ bool curve = false;
21
+ std::array<Point, 4> p;
22
+ };
23
+
24
+ struct Subpath {
25
+ Point start;
26
+ std::vector<Segment> segments;
27
+ bool closed = false;
28
+ };
29
+
30
+ // The graphics state parameters that shape a stroke (w, J, j, M).
31
+ struct StrokeStyle {
32
+ double width = 1;
33
+ int cap = 0; // 0 butt, 1 round, 2 projecting square
34
+ int join = 0; // 0 miter, 1 round, 2 bevel
35
+ double miter_limit = 10;
36
+ };
37
+
38
+ // The area a path covers in default user space when filled: the hull of its points (a curve
39
+ // lies inside the hull of its control points). Empty for a path that has no segment.
40
+ std::optional<Box> fill_bounds(std::vector<Subpath> const& path, Matrix const& ctm);
41
+
42
+ // The area a stroke of the path paints in default user space: every segment widened by half the
43
+ // line width, plus its caps and joins as the style draws them, all under the CTM (a round cap or
44
+ // join becomes an ellipse). Curves are bounded by a disk of half the line width around each
45
+ // control point - exact for lines, a little generous for curves. Dashes are ignored: they only
46
+ // remove paint. Empty for a path that paints nothing.
47
+ std::optional<Box> stroke_bounds(std::vector<Subpath> const& path, Matrix const& ctm, StrokeStyle const& style);
48
+
49
+ } // namespace qpdf_ruby::paths
@@ -0,0 +1,329 @@
1
+ #include "xmp_scan.hpp"
2
+
3
+ #include <map>
4
+ #include <vector>
5
+
6
+ namespace qpdf_ruby::xmp {
7
+
8
+ namespace {
9
+
10
+ constexpr char kRdfNs[] = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
11
+ constexpr char kXmlNs[] = "http://www.w3.org/XML/1998/namespace";
12
+
13
+ bool is_space(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
14
+
15
+ bool is_name_end(char c) { return is_space(c) || c == '/' || c == '>' || c == '='; }
16
+
17
+ void append_utf8(std::string& out, unsigned long cp) {
18
+ if (cp < 0x80) {
19
+ out += static_cast<char>(cp);
20
+ } else if (cp < 0x800) {
21
+ out += static_cast<char>(0xC0 | (cp >> 6));
22
+ out += static_cast<char>(0x80 | (cp & 0x3F));
23
+ } else if (cp < 0x10000) {
24
+ out += static_cast<char>(0xE0 | (cp >> 12));
25
+ out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
26
+ out += static_cast<char>(0x80 | (cp & 0x3F));
27
+ } else {
28
+ out += static_cast<char>(0xF0 | (cp >> 18));
29
+ out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
30
+ out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
31
+ out += static_cast<char>(0x80 | (cp & 0x3F));
32
+ }
33
+ }
34
+
35
+ // Attribute values with the predefined and numeric character references resolved - enough to
36
+ // compare namespace URIs. False for an unknown or broken reference.
37
+ bool decode(std::string const& raw, std::string& out) {
38
+ static const std::map<std::string, char> named = {
39
+ {"amp", '&'}, {"lt", '<'}, {"gt", '>'}, {"quot", '"'}, {"apos", '\''}};
40
+ out.clear();
41
+ for (size_t i = 0; i < raw.size(); ++i) {
42
+ if (raw[i] != '&') {
43
+ out += raw[i];
44
+ continue;
45
+ }
46
+ size_t semi = raw.find(';', i);
47
+ if (semi == std::string::npos) return false;
48
+ std::string ref = raw.substr(i + 1, semi - i - 1);
49
+ if (auto it = named.find(ref); it != named.end()) {
50
+ out += it->second;
51
+ } else if (ref.size() > 1 && ref[0] == '#') {
52
+ bool hex = ref[1] == 'x';
53
+ std::string digits = ref.substr(hex ? 2 : 1);
54
+ if (digits.empty() || digits.size() > 8) return false;
55
+ unsigned long cp = 0;
56
+ for (char d : digits) {
57
+ int v = (d >= '0' && d <= '9') ? d - '0'
58
+ : hex && d >= 'a' && d <= 'f' ? d - 'a' + 10
59
+ : hex && d >= 'A' && d <= 'F' ? d - 'A' + 10
60
+ : -1;
61
+ if (v < 0) return false;
62
+ cp = cp * (hex ? 16 : 10) + static_cast<unsigned long>(v);
63
+ }
64
+ if (cp == 0 || cp > 0x10FFFF) return false;
65
+ append_utf8(out, cp);
66
+ } else {
67
+ return false;
68
+ }
69
+ i = semi;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ using Bindings = std::map<std::string, std::string>; // prefix ("" = default) -> namespace URI
75
+
76
+ // Where an element sits: the first rdf:RDF, an rdf:Description directly under it, a property of
77
+ // that Description, or anything else.
78
+ enum class Kind { Other, RdfRoot, Description, Property };
79
+
80
+ struct Element {
81
+ std::string qname;
82
+ Bindings bindings;
83
+ Kind kind;
84
+ size_t start = 0; // the "<" of the start tag
85
+ size_t content_start = 0; // just after its ">"
86
+ bool simple = true; // no child element or CDATA inside
87
+ std::string ns, local;
88
+ };
89
+
90
+ // The namespace of a qualified name under `bindings`; false for an unbound prefix.
91
+ bool resolve(std::string const& qname, Bindings const& bindings, bool is_attribute, std::string& ns,
92
+ std::string& local) {
93
+ size_t colon = qname.find(':');
94
+ if (colon == std::string::npos) {
95
+ local = qname;
96
+ ns.clear();
97
+ if (!is_attribute) { // unprefixed attributes have no namespace; elements take the default
98
+ auto it = bindings.find("");
99
+ if (it != bindings.end()) ns = it->second;
100
+ }
101
+ return !local.empty();
102
+ }
103
+ std::string prefix = qname.substr(0, colon);
104
+ local = qname.substr(colon + 1);
105
+ if (prefix.empty() || local.empty() || local.find(':') != std::string::npos) return false;
106
+ if (prefix == "xml") {
107
+ ns = kXmlNs;
108
+ return true;
109
+ }
110
+ auto it = bindings.find(prefix);
111
+ if (it == bindings.end() || it->second.empty()) return false;
112
+ ns = it->second;
113
+ return true;
114
+ }
115
+
116
+ class Scanner {
117
+ public:
118
+ explicit Scanner(std::string const& xml) : xml_(xml) {}
119
+
120
+ Scan run() {
121
+ Scan out;
122
+ if (!walk(out)) return Scan{};
123
+ out.well_formed = stack_.empty() && seen_root_;
124
+ if (!out.well_formed) return Scan{};
125
+ return out;
126
+ }
127
+
128
+ private:
129
+ bool skip_past(std::string const& end) {
130
+ size_t at = xml_.find(end, pos_);
131
+ if (at == std::string::npos) return false;
132
+ pos_ = at + end.size();
133
+ return true;
134
+ }
135
+
136
+ bool starts(std::string const& s) const { return xml_.compare(pos_, s.size(), s) == 0; }
137
+
138
+ std::string name() {
139
+ size_t start = pos_;
140
+ while (pos_ < xml_.size() && !is_name_end(xml_[pos_])) ++pos_;
141
+ return xml_.substr(start, pos_ - start);
142
+ }
143
+
144
+ void spaces() {
145
+ while (pos_ < xml_.size() && is_space(xml_[pos_])) ++pos_;
146
+ }
147
+
148
+ bool walk(Scan& out) {
149
+ while (pos_ < xml_.size()) {
150
+ size_t lt = xml_.find('<', pos_);
151
+ if (lt == std::string::npos) return true;
152
+ pos_ = lt;
153
+ bool ok = starts("<!--") ? skip_past("-->")
154
+ : starts("<![CDATA[") ? !stack_.empty() && (stack_.back().simple = false, skip_past("]]>"))
155
+ : starts("<?") ? skip_past("?>")
156
+ : starts("<!DOCTYPE") ? doctype()
157
+ : starts("</") ? end_tag(out)
158
+ : starts("<!") ? false
159
+ : start_tag(out);
160
+ if (!ok) return false;
161
+ }
162
+ return true;
163
+ }
164
+
165
+ bool doctype() {
166
+ if (seen_root_ || !stack_.empty()) return false;
167
+ for (int brackets = 0; pos_ < xml_.size(); ++pos_) {
168
+ char c = xml_[pos_];
169
+ if (c == '[') ++brackets;
170
+ if (c == ']') --brackets;
171
+ if (c == '>' && brackets == 0) {
172
+ ++pos_;
173
+ return true;
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+
179
+ bool end_tag(Scan& out) {
180
+ size_t lt = pos_;
181
+ pos_ += 2;
182
+ std::string qname = name();
183
+ spaces();
184
+ if (pos_ >= xml_.size() || xml_[pos_] != '>' || stack_.empty() || stack_.back().qname != qname) return false;
185
+ ++pos_;
186
+ Element const& closed = stack_.back();
187
+ if (closed.kind == Kind::RdfRoot) out.rdf_close = lt;
188
+ if (closed.kind == Kind::Property) {
189
+ Property property{closed.ns, closed.local, closed.start, pos_, "", false};
190
+ if (closed.simple) property.has_value = decode(xml_.substr(closed.content_start, lt - closed.content_start), property.value);
191
+ if (!property.has_value) property.value.clear();
192
+ out.located.push_back(std::move(property));
193
+ }
194
+ stack_.pop_back();
195
+ return true;
196
+ }
197
+
198
+ bool start_tag(Scan& out) {
199
+ size_t lt = pos_;
200
+ if (!stack_.empty()) stack_.back().simple = false;
201
+ ++pos_;
202
+ std::string qname = name();
203
+ if (qname.empty()) return false;
204
+ if (stack_.empty() && seen_root_) return false; // a second root element
205
+
206
+ struct Attribute {
207
+ std::string qname, value;
208
+ size_t begin, end;
209
+ };
210
+ std::vector<Attribute> attributes;
211
+ Bindings bindings = stack_.empty() ? Bindings{} : stack_.back().bindings;
212
+ bool self_closing = false;
213
+ while (true) {
214
+ size_t before = pos_;
215
+ spaces();
216
+ if (pos_ >= xml_.size()) return false;
217
+ if (xml_[pos_] == '>') {
218
+ ++pos_;
219
+ break;
220
+ }
221
+ if (starts("/>")) {
222
+ pos_ += 2;
223
+ self_closing = true;
224
+ break;
225
+ }
226
+ if (pos_ == before) return false; // attributes must be separated by white space
227
+ size_t attribute_begin = before;
228
+ std::string attribute = name();
229
+ spaces();
230
+ if (attribute.empty() || pos_ >= xml_.size() || xml_[pos_] != '=') return false;
231
+ ++pos_;
232
+ spaces();
233
+ if (pos_ >= xml_.size() || (xml_[pos_] != '"' && xml_[pos_] != '\'')) return false;
234
+ char quote = xml_[pos_++];
235
+ size_t close = xml_.find(quote, pos_);
236
+ if (close == std::string::npos) return false;
237
+ std::string raw = xml_.substr(pos_, close - pos_), value;
238
+ pos_ = close + 1;
239
+ if (raw.find('<') != std::string::npos || !decode(raw, value)) return false;
240
+ if (attribute == "xmlns") {
241
+ bindings[""] = value;
242
+ } else if (attribute.rfind("xmlns:", 0) == 0) {
243
+ // An empty prefix is no name; undeclaring a prefix is XML 1.1 only.
244
+ if (attribute.size() == 6 || value.empty()) return false;
245
+ bindings[attribute.substr(6)] = value;
246
+ } else {
247
+ attributes.push_back({attribute, value, attribute_begin, pos_});
248
+ }
249
+ }
250
+
251
+ std::string ns, local;
252
+ if (!resolve(qname, bindings, false, ns, local)) return false;
253
+ Kind parent = stack_.empty() ? Kind::Other : stack_.back().kind;
254
+ Kind kind = Kind::Other;
255
+ if (ns == kRdfNs && local == "RDF" && !seen_rdf_) {
256
+ kind = Kind::RdfRoot;
257
+ seen_rdf_ = true;
258
+ } else if (parent == Kind::RdfRoot && ns == kRdfNs && local == "Description") {
259
+ kind = Kind::Description;
260
+ } else if (parent == Kind::Description) {
261
+ kind = Kind::Property;
262
+ out.properties.insert({ns, local});
263
+ }
264
+ for (auto const& attribute : attributes) {
265
+ std::string attribute_ns, attribute_local;
266
+ if (!resolve(attribute.qname, bindings, true, attribute_ns, attribute_local)) return false;
267
+ bool property = kind == Kind::Description && !attribute_ns.empty() && attribute_ns != kRdfNs && attribute_ns != kXmlNs;
268
+ if (!property) continue;
269
+ out.properties.insert({attribute_ns, attribute_local});
270
+ out.located.push_back({attribute_ns, attribute_local, attribute.begin, attribute.end, attribute.value, true});
271
+ }
272
+
273
+ if (stack_.empty()) seen_root_ = true;
274
+ if (self_closing) {
275
+ if (kind == Kind::Property) out.located.push_back({ns, local, lt, pos_, "", true});
276
+ return true;
277
+ }
278
+ stack_.push_back({qname, std::move(bindings), kind, lt, pos_, true, ns, local});
279
+ return true;
280
+ }
281
+
282
+ std::string const& xml_;
283
+ size_t pos_ = 0;
284
+ std::vector<Element> stack_;
285
+ bool seen_rdf_ = false; // only the first rdf:RDF element counts
286
+ bool seen_root_ = false;
287
+ };
288
+
289
+ } // namespace
290
+
291
+ Scan scan(std::string const& xml) { return Scanner(xml).run(); }
292
+
293
+ bool valid_text(char const* text, size_t size) noexcept {
294
+ auto const* bytes = reinterpret_cast<unsigned char const*>(text);
295
+ for (size_t i = 0; i < size;) {
296
+ unsigned char lead = bytes[i];
297
+ unsigned long cp;
298
+ size_t length;
299
+ if (lead < 0x80) {
300
+ cp = lead;
301
+ length = 1;
302
+ } else if ((lead & 0xE0) == 0xC0) {
303
+ cp = lead & 0x1F;
304
+ length = 2;
305
+ } else if ((lead & 0xF0) == 0xE0) {
306
+ cp = lead & 0x0F;
307
+ length = 3;
308
+ } else if ((lead & 0xF8) == 0xF0) {
309
+ cp = lead & 0x07;
310
+ length = 4;
311
+ } else {
312
+ return false;
313
+ }
314
+ if (i + length > size) return false;
315
+ for (size_t k = 1; k < length; ++k) {
316
+ if ((bytes[i + k] & 0xC0) != 0x80) return false;
317
+ cp = (cp << 6) | (bytes[i + k] & 0x3F);
318
+ }
319
+ static constexpr unsigned long kMinimum[] = {0, 0, 0x80, 0x800, 0x10000};
320
+ if (cp < kMinimum[length]) return false; // an overlong form
321
+ bool allowed = cp == 0x9 || cp == 0xA || cp == 0xD || (cp >= 0x20 && cp <= 0xD7FF) ||
322
+ (cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF);
323
+ if (!allowed) return false;
324
+ i += length;
325
+ }
326
+ return true;
327
+ }
328
+
329
+ } // namespace qpdf_ruby::xmp
@@ -0,0 +1,49 @@
1
+ #pragma once
2
+
3
+ #include <set>
4
+ #include <string>
5
+ #include <utility>
6
+ #include <vector>
7
+
8
+ namespace qpdf_ruby::xmp {
9
+
10
+ // A property of the document - a child element or a namespaced attribute of an rdf:Description
11
+ // directly under the packet's first rdf:RDF - with where it sits in the packet.
12
+ struct Property {
13
+ std::string ns;
14
+ std::string local;
15
+ // [begin, end) of the whole element, or of the attribute with the white space before it.
16
+ size_t begin = 0;
17
+ size_t end = 0;
18
+ // The text value, references resolved; empty for an element with child elements, CDATA or a
19
+ // reference the scanner does not know (has_value false).
20
+ std::string value;
21
+ bool has_value = false;
22
+ };
23
+
24
+ // What the metadata steps need to know about an XMP packet, read with namespace scoping:
25
+ // comments, CDATA sections, processing instructions and a DOCTYPE are skipped, so a property that
26
+ // is only mentioned in one of them does not count, and neither does an element of the same name
27
+ // anywhere but on the document's own rdf:Description.
28
+ struct Scan {
29
+ // Every tag closed in order, one root element, every prefix bound, attributes quoted and
30
+ // separated. Not a validating parser: names and text outside the root are not checked.
31
+ bool well_formed = false;
32
+ // (namespace URI, local name) of every property of the document - XMP allows a simple property
33
+ // as an element or as an attribute.
34
+ std::set<std::pair<std::string, std::string>> properties;
35
+ std::vector<Property> located;
36
+ // Offset of the "<" of the end tag of the outermost rdf:RDF element; npos without one.
37
+ size_t rdf_close = std::string::npos;
38
+
39
+ bool has(std::string const& ns, std::string const& local) const { return properties.count({ns, local}) > 0; }
40
+ };
41
+
42
+ Scan scan(std::string const& xml);
43
+
44
+ // True if `size` bytes at `text` are UTF-8 made only of XML 1.0 characters (no NUL, no control
45
+ // character but tab, line feed and carriage return, no surrogate, no U+FFFE/U+FFFF) - what may go
46
+ // into an XMP packet. Allocates nothing and never throws.
47
+ bool valid_text(char const* text, size_t size) noexcept;
48
+
49
+ } // namespace qpdf_ruby::xmp
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QpdfRuby
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/qpdf_ruby.rb CHANGED
@@ -8,6 +8,7 @@ module QpdfRuby
8
8
  ENCRYPTION_REVISION_AES_256 = 5 # Acrobat 9.x, 256-bit AES
9
9
  ENCRYPTION_REVISION_AES_256U = 6 # Acrobat X, 256-bit AES (PDF 2.0+ update)
10
10
 
11
- class Error < StandardError; end
12
- # Your code goes here...
11
+ # Raised for every failure inside the extension (a PDF QPDF cannot read, write errors, ...).
12
+ # A RuntimeError, so code that rescued the extension's RuntimeErrors before keeps working.
13
+ class Error < RuntimeError; end
13
14
  end