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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -5
- data/README.md +94 -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 +909 -0
- data/ext/qpdf_ruby/pdfua.hpp +102 -0
- data/ext/qpdf_ruby/qpdf_ruby.cpp +369 -214
- 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 +257 -0
- data/ext/qpdf_ruby/xmp_scan.hpp +27 -0
- data/lib/qpdf_ruby/version.rb +1 -1
- data/lib/qpdf_ruby.rb +3 -2
- data/sig/qpdf_ruby.rbs +78 -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
data/ext/qpdf_ruby/qpdf_ruby.hpp
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
#include "ruby.h"
|
|
5
5
|
|
|
6
|
-
|
|
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
|
data/lib/qpdf_ruby/version.rb
CHANGED
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
|
-
|
|
12
|
-
#
|
|
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
|
-
|
|
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
|