qpdf_ruby 0.1.1 → 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.
@@ -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,257 @@
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
+ struct Element {
77
+ std::string qname;
78
+ Bindings bindings;
79
+ bool rdf_root;
80
+ };
81
+
82
+ // The namespace of a qualified name under `bindings`; false for an unbound prefix.
83
+ bool resolve(std::string const& qname, Bindings const& bindings, bool is_attribute, std::string& ns,
84
+ std::string& local) {
85
+ size_t colon = qname.find(':');
86
+ if (colon == std::string::npos) {
87
+ local = qname;
88
+ ns.clear();
89
+ if (!is_attribute) { // unprefixed attributes have no namespace; elements take the default
90
+ auto it = bindings.find("");
91
+ if (it != bindings.end()) ns = it->second;
92
+ }
93
+ return !local.empty();
94
+ }
95
+ std::string prefix = qname.substr(0, colon);
96
+ local = qname.substr(colon + 1);
97
+ if (prefix.empty() || local.empty() || local.find(':') != std::string::npos) return false;
98
+ if (prefix == "xml") {
99
+ ns = kXmlNs;
100
+ return true;
101
+ }
102
+ auto it = bindings.find(prefix);
103
+ if (it == bindings.end() || it->second.empty()) return false;
104
+ ns = it->second;
105
+ return true;
106
+ }
107
+
108
+ class Scanner {
109
+ public:
110
+ explicit Scanner(std::string const& xml) : xml_(xml) {}
111
+
112
+ Scan run() {
113
+ Scan out;
114
+ if (!walk(out)) return Scan{};
115
+ out.well_formed = stack_.empty() && seen_root_;
116
+ if (!out.well_formed) return Scan{};
117
+ return out;
118
+ }
119
+
120
+ private:
121
+ bool skip_past(std::string const& end) {
122
+ size_t at = xml_.find(end, pos_);
123
+ if (at == std::string::npos) return false;
124
+ pos_ = at + end.size();
125
+ return true;
126
+ }
127
+
128
+ bool starts(std::string const& s) const { return xml_.compare(pos_, s.size(), s) == 0; }
129
+
130
+ std::string name() {
131
+ size_t start = pos_;
132
+ while (pos_ < xml_.size() && !is_name_end(xml_[pos_])) ++pos_;
133
+ return xml_.substr(start, pos_ - start);
134
+ }
135
+
136
+ void spaces() {
137
+ while (pos_ < xml_.size() && is_space(xml_[pos_])) ++pos_;
138
+ }
139
+
140
+ bool walk(Scan& out) {
141
+ while (pos_ < xml_.size()) {
142
+ size_t lt = xml_.find('<', pos_);
143
+ if (lt == std::string::npos) return true;
144
+ pos_ = lt;
145
+ bool ok = starts("<!--") ? skip_past("-->")
146
+ : starts("<![CDATA[") ? !stack_.empty() && skip_past("]]>")
147
+ : starts("<?") ? skip_past("?>")
148
+ : starts("<!DOCTYPE") ? doctype()
149
+ : starts("</") ? end_tag(out)
150
+ : starts("<!") ? false
151
+ : start_tag(out);
152
+ if (!ok) return false;
153
+ }
154
+ return true;
155
+ }
156
+
157
+ bool doctype() {
158
+ if (seen_root_ || !stack_.empty()) return false;
159
+ for (int brackets = 0; pos_ < xml_.size(); ++pos_) {
160
+ char c = xml_[pos_];
161
+ if (c == '[') ++brackets;
162
+ if (c == ']') --brackets;
163
+ if (c == '>' && brackets == 0) {
164
+ ++pos_;
165
+ return true;
166
+ }
167
+ }
168
+ return false;
169
+ }
170
+
171
+ bool end_tag(Scan& out) {
172
+ size_t lt = pos_;
173
+ pos_ += 2;
174
+ std::string qname = name();
175
+ spaces();
176
+ if (pos_ >= xml_.size() || xml_[pos_] != '>' || stack_.empty() || stack_.back().qname != qname) return false;
177
+ ++pos_;
178
+ if (stack_.back().rdf_root) out.rdf_close = lt;
179
+ stack_.pop_back();
180
+ return true;
181
+ }
182
+
183
+ bool start_tag(Scan& out) {
184
+ ++pos_;
185
+ std::string qname = name();
186
+ if (qname.empty()) return false;
187
+ if (stack_.empty() && seen_root_) return false; // a second root element
188
+
189
+ std::vector<std::pair<std::string, std::string>> attributes;
190
+ Bindings bindings = stack_.empty() ? Bindings{} : stack_.back().bindings;
191
+ bool self_closing = false;
192
+ while (true) {
193
+ size_t before = pos_;
194
+ spaces();
195
+ if (pos_ >= xml_.size()) return false;
196
+ if (xml_[pos_] == '>') {
197
+ ++pos_;
198
+ break;
199
+ }
200
+ if (starts("/>")) {
201
+ pos_ += 2;
202
+ self_closing = true;
203
+ break;
204
+ }
205
+ if (pos_ == before) return false; // attributes must be separated by white space
206
+ std::string attribute = name();
207
+ spaces();
208
+ if (attribute.empty() || pos_ >= xml_.size() || xml_[pos_] != '=') return false;
209
+ ++pos_;
210
+ spaces();
211
+ if (pos_ >= xml_.size() || (xml_[pos_] != '"' && xml_[pos_] != '\'')) return false;
212
+ char quote = xml_[pos_++];
213
+ size_t close = xml_.find(quote, pos_);
214
+ if (close == std::string::npos) return false;
215
+ std::string raw = xml_.substr(pos_, close - pos_), value;
216
+ pos_ = close + 1;
217
+ if (raw.find('<') != std::string::npos || !decode(raw, value)) return false;
218
+ if (attribute == "xmlns") {
219
+ bindings[""] = value;
220
+ } else if (attribute.rfind("xmlns:", 0) == 0) {
221
+ // An empty prefix is no name; undeclaring a prefix is XML 1.1 only.
222
+ if (attribute.size() == 6 || value.empty()) return false;
223
+ bindings[attribute.substr(6)] = value;
224
+ } else {
225
+ attributes.emplace_back(attribute, value);
226
+ }
227
+ }
228
+
229
+ std::string ns, local;
230
+ if (!resolve(qname, bindings, false, ns, local)) return false;
231
+ out.properties.insert({ns, local});
232
+ for (auto const& [attribute, value] : attributes) {
233
+ std::string attribute_ns, attribute_local;
234
+ if (!resolve(attribute, bindings, true, attribute_ns, attribute_local)) return false;
235
+ if (!attribute_ns.empty()) out.properties.insert({attribute_ns, attribute_local});
236
+ }
237
+
238
+ bool rdf_root = ns == kRdfNs && local == "RDF" && !seen_rdf_;
239
+ if (rdf_root) seen_rdf_ = true;
240
+ if (stack_.empty()) seen_root_ = true;
241
+ if (self_closing) return true;
242
+ stack_.push_back({qname, std::move(bindings), rdf_root});
243
+ return true;
244
+ }
245
+
246
+ std::string const& xml_;
247
+ size_t pos_ = 0;
248
+ std::vector<Element> stack_;
249
+ bool seen_rdf_ = false; // only the first rdf:RDF element counts
250
+ bool seen_root_ = false;
251
+ };
252
+
253
+ } // namespace
254
+
255
+ Scan scan(std::string const& xml) { return Scanner(xml).run(); }
256
+
257
+ } // namespace qpdf_ruby::xmp
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #include <set>
4
+ #include <string>
5
+ #include <utility>
6
+
7
+ namespace qpdf_ruby::xmp {
8
+
9
+ // What the identification step needs to know about an XMP packet, read with namespace scoping:
10
+ // comments, CDATA sections, processing instructions and a DOCTYPE are skipped, so a property that
11
+ // is only mentioned in one of them does not count.
12
+ struct Scan {
13
+ // Every tag closed in order, one root element, every prefix bound, attributes quoted and
14
+ // separated. Not a validating parser: names and text outside the root are not checked.
15
+ bool well_formed = false;
16
+ // (namespace URI, local name) of every element and every namespaced attribute - XMP allows a
17
+ // simple property in either form.
18
+ std::set<std::pair<std::string, std::string>> properties;
19
+ // Offset of the "<" of the end tag of the outermost rdf:RDF element; npos without one.
20
+ size_t rdf_close = std::string::npos;
21
+
22
+ bool has(std::string const& ns, std::string const& local) const { return properties.count({ns, local}) > 0; }
23
+ };
24
+
25
+ Scan scan(std::string const& xml);
26
+
27
+ } // 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.2.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
data/sig/qpdf_ruby.rbs CHANGED
@@ -1,4 +1,81 @@
1
1
  module QpdfRuby
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ ENCRYPTION_REVISION_AES_128: Integer
5
+ ENCRYPTION_REVISION_AES_256: Integer
6
+ ENCRYPTION_REVISION_AES_256U: Integer
7
+ PRINT_FULL: Integer
8
+ PRINT_LOW: Integer
9
+ PRINT_NONE: Integer
10
+
11
+ class Error < RuntimeError
12
+ end
13
+
14
+ class Document
15
+ type untagged_counts = {
16
+ paths: Integer,
17
+ texts: Integer,
18
+ xobjects: Integer,
19
+ shadings: Integer,
20
+ inline_images: Integer,
21
+ total: Integer
22
+ }
23
+
24
+ type pdfua_report = {
25
+ decorations: Integer,
26
+ artifacts: untagged_counts,
27
+ links: Integer,
28
+ list_bodies: Integer,
29
+ roles: Integer,
30
+ figure_groups: Integer,
31
+ figures_without_alt: Integer,
32
+ identified: bool,
33
+ unidentified_reason: String?
34
+ }
35
+
36
+ type link = { page: Integer, uri: String?, contents: String? }
37
+
38
+ def self.from_memory: (String data, ?String? password) -> Document
39
+
40
+ def initialize: (String filename, ?String? password) -> void
41
+
42
+ def write: (String filename) -> nil
43
+ def to_memory: () -> String
44
+
45
+ def show_structure: () -> String
46
+ def ensure_bbox: () -> nil
47
+
48
+ def apply_pdfua_fixes: (?link_texts: Hash[untyped, untyped]?, ?title: String?) -> pdfua_report
49
+ def mark_untagged_content_as_artifacts: () -> untagged_counts
50
+ # deprecated: use #mark_untagged_content_as_artifacts
51
+ def mark_paths_as_artifacts: () -> untagged_counts
52
+ def untagged_content: () -> untagged_counts
53
+ def describe_links: (?Hash[untyped, untyped]? texts) -> Integer
54
+ def artifact_tagged_decorations: () -> Integer
55
+ def wrap_list_bodies: () -> Integer
56
+ def parent_tree_mismatches: () -> Integer
57
+ def map_nonstandard_roles: () -> Integer
58
+ def retag_grouping_figures: () -> Integer
59
+ def figures_without_alt: () -> Integer
60
+ def add_pdfua_identification: (?title: String?) -> bool
61
+
62
+ def links: () -> Array[link]
63
+ def metadata: () -> String?
64
+ def role_map: () -> Hash[String, String]
65
+
66
+ def encrypt: (
67
+ ?user_pw: String,
68
+ ?owner_pw: String,
69
+ ?encryption_revision: Integer,
70
+ ?allow_print: Integer,
71
+ ?allow_modify: boolish,
72
+ ?allow_extract: boolish,
73
+ ?accessibility: boolish,
74
+ ?assemble: boolish,
75
+ ?annotate_and_form: boolish,
76
+ ?form_filling: boolish,
77
+ ?encrypt_metadata: boolish,
78
+ ?use_aes: boolish
79
+ ) -> nil
80
+ end
4
81
  end