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.cpp
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#include "qpdf_ruby.hpp"
|
|
2
|
+
#include "document_handle.hpp"
|
|
3
|
+
#include "mcid_bounds.hpp"
|
|
2
4
|
#include "pdf_struct_walker.hpp"
|
|
5
|
+
#include "pdfua.hpp"
|
|
6
|
+
#include "ruby_guard.hpp"
|
|
3
7
|
#include "struct_node.hpp"
|
|
4
|
-
#include "pdf_image_mapper.hpp"
|
|
5
|
-
#include "document_handle.hpp"
|
|
6
8
|
|
|
7
9
|
#include <qpdf/QPDF.hh>
|
|
8
|
-
#include <qpdf/QPDFWriter.hh>
|
|
9
|
-
#include <qpdf/QPDFPageObjectHelper.hh>
|
|
10
10
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
11
11
|
|
|
12
|
-
#include <
|
|
13
|
-
#include <
|
|
14
|
-
#include <
|
|
15
|
-
#include <
|
|
16
|
-
#include <regex>
|
|
12
|
+
#include <map>
|
|
13
|
+
#include <optional>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <vector>
|
|
17
16
|
|
|
18
17
|
VALUE rb_mQpdfRuby;
|
|
19
18
|
VALUE rb_cDocument;
|
|
@@ -21,275 +20,431 @@ VALUE rb_eQpdfRubyError;
|
|
|
21
20
|
|
|
22
21
|
using namespace qpdf_ruby;
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
DocumentHandle* h;
|
|
26
|
-
Data_Get_Struct(self, DocumentHandle, h);
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
QPDF& pdf = h->qpdf();
|
|
23
|
+
// ---- the wrapped DocumentHandle ------------------------------------------------------------------
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
QPDFObjectHandle struct_root = catalog.getKey("/StructTreeRoot");
|
|
33
|
-
if (!struct_root.isDictionary()) {
|
|
34
|
-
rb_raise(rb_eRuntimeError, "No StructTreeRoot found");
|
|
35
|
-
}
|
|
36
|
-
QPDFObjectHandle topKids = struct_root.getKey("/K");
|
|
25
|
+
static void doc_free(void* ptr) { qpdf_ruby_close(static_cast<DocumentHandle*>(ptr)); }
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
static const rb_data_type_t document_type = {
|
|
28
|
+
"QpdfRuby::Document",
|
|
29
|
+
{nullptr, doc_free, nullptr},
|
|
30
|
+
nullptr,
|
|
31
|
+
nullptr,
|
|
32
|
+
RUBY_TYPED_FREE_IMMEDIATELY,
|
|
33
|
+
};
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
result = walker.get_structure_as_string(topKids);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return rb_str_new(result.c_str(), result.length());
|
|
51
|
-
} catch (const std::exception& e) {
|
|
52
|
-
rb_raise(rb_eRuntimeError, "Error: %s", e.what());
|
|
53
|
-
}
|
|
54
|
-
return Qnil;
|
|
35
|
+
static DocumentHandle* handle_of(VALUE self) {
|
|
36
|
+
auto* h = static_cast<DocumentHandle*>(rb_check_typeddata(self, &document_type));
|
|
37
|
+
if (!h) rb_raise(rb_eQpdfRubyError, "document is not open");
|
|
38
|
+
return h;
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
VALUE
|
|
58
|
-
DocumentHandle* h;
|
|
59
|
-
Data_Get_Struct(self, DocumentHandle, h);
|
|
60
|
-
try {
|
|
61
|
-
QPDF& pdf = h->qpdf();
|
|
41
|
+
static VALUE doc_alloc(VALUE klass) { return TypedData_Wrap_Struct(klass, &document_type, nullptr); }
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
std::regex path_regex(R"((?:[-+]?\d*\.?\d+(?:e[-+]?\d+)?\s+){4}re\s+(?:S|s|f\*?|F|B\*?|b\*?|n))",
|
|
65
|
-
std::regex::ECMAScript | std::regex::optimize);
|
|
43
|
+
// ---- arguments and results (see the rules in ruby_guard.hpp) ------------------------------------
|
|
66
44
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
45
|
+
// Ruby side, before any C++ object exists: these may raise.
|
|
46
|
+
static VALUE checked_string(VALUE value) {
|
|
47
|
+
Check_Type(value, T_STRING);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
if (content_stream.isStream()) {
|
|
74
|
-
// Use std::shared_ptr instead of PointerHolder
|
|
75
|
-
std::shared_ptr<Buffer> stream_buffer = content_stream.getStreamData();
|
|
76
|
-
std::string stream_data_str(reinterpret_cast<char*>(stream_buffer->getBuffer()), stream_buffer->getSize());
|
|
51
|
+
static VALUE checked_optional_string(VALUE value) { return NIL_P(value) ? Qnil : checked_string(value); }
|
|
77
52
|
|
|
78
|
-
|
|
53
|
+
static int collect_link_text(VALUE key, VALUE value, VALUE pairs) {
|
|
54
|
+
rb_ary_push(pairs, rb_assoc_new(rb_obj_as_string(key), rb_obj_as_string(value)));
|
|
55
|
+
return ST_CONTINUE;
|
|
56
|
+
}
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
// The link texts hash as an array of [String, String] pairs (keys and values through #to_s), or nil.
|
|
59
|
+
static VALUE checked_link_texts(VALUE hash) {
|
|
60
|
+
if (NIL_P(hash)) return Qnil;
|
|
61
|
+
Check_Type(hash, T_HASH);
|
|
62
|
+
VALUE pairs = rb_ary_new();
|
|
63
|
+
rb_hash_foreach(hash, collect_link_text, pairs);
|
|
64
|
+
return pairs;
|
|
65
|
+
}
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
new_contents_array.push_back(content_stream);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
67
|
+
// C++ side, inside guarded only: reading checked values never raises.
|
|
68
|
+
static std::string cpp_string(VALUE str) { return std::string(RSTRING_PTR(str), RSTRING_LEN(str)); }
|
|
90
69
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
70
|
+
static std::optional<std::string> cpp_optional_string(VALUE str) {
|
|
71
|
+
if (NIL_P(str)) return std::nullopt;
|
|
72
|
+
return cpp_string(str);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static std::map<std::string, std::string> cpp_link_texts(VALUE pairs) {
|
|
76
|
+
std::map<std::string, std::string> texts;
|
|
77
|
+
if (NIL_P(pairs)) return texts;
|
|
78
|
+
for (long i = 0; i < RARRAY_LEN(pairs); ++i) {
|
|
79
|
+
VALUE pair = rb_ary_entry(pairs, i);
|
|
80
|
+
texts[cpp_string(rb_ary_entry(pair, 0))] = cpp_string(rb_ary_entry(pair, 1));
|
|
101
81
|
}
|
|
82
|
+
return texts;
|
|
83
|
+
}
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
static VALUE ruby_string(std::string const& value) {
|
|
86
|
+
return rb_utf8_str_new(value.data(), static_cast<long>(value.size()));
|
|
104
87
|
}
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
DocumentHandle* h;
|
|
108
|
-
Data_Get_Struct(self, DocumentHandle, h);
|
|
89
|
+
// ---- opening and writing ---------------------------------------------------------------------
|
|
109
90
|
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
// Calling initialize again (send(:initialize, ...)) closes the document opened before.
|
|
92
|
+
static VALUE doc_initialize(int argc, VALUE* argv, VALUE self) {
|
|
93
|
+
VALUE filename = Qnil, password = Qnil;
|
|
94
|
+
rb_scan_args(argc, argv, "11", &filename, &password);
|
|
95
|
+
checked_string(filename);
|
|
96
|
+
password = checked_optional_string(password);
|
|
97
|
+
|
|
98
|
+
qpdf_ruby_close(static_cast<DocumentHandle*>(rb_check_typeddata(self, &document_type)));
|
|
99
|
+
DATA_PTR(self) = nullptr;
|
|
100
|
+
DATA_PTR(self) = guarded([&] {
|
|
101
|
+
return DocumentHandle::open(cpp_string(filename), cpp_optional_string(password).value_or("")).release();
|
|
102
|
+
});
|
|
103
|
+
RB_GC_GUARD(filename);
|
|
104
|
+
RB_GC_GUARD(password);
|
|
105
|
+
return self;
|
|
106
|
+
}
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
static VALUE doc_from_memory(int argc, VALUE* argv, VALUE klass) {
|
|
109
|
+
VALUE data = Qnil, password = Qnil;
|
|
110
|
+
rb_scan_args(argc, argv, "11", &data, &password);
|
|
111
|
+
checked_string(data);
|
|
112
|
+
password = checked_optional_string(password);
|
|
113
|
+
|
|
114
|
+
VALUE doc = doc_alloc(klass);
|
|
115
|
+
DATA_PTR(doc) = guarded([&] {
|
|
116
|
+
auto const* bytes = reinterpret_cast<unsigned char const*>(RSTRING_PTR(data));
|
|
117
|
+
std::vector<unsigned char> copy(bytes, bytes + RSTRING_LEN(data));
|
|
118
|
+
return DocumentHandle::open_memory("ruby-memory", std::move(copy), cpp_optional_string(password).value_or(""))
|
|
119
|
+
.release();
|
|
120
|
+
});
|
|
121
|
+
RB_GC_GUARD(data);
|
|
122
|
+
RB_GC_GUARD(password);
|
|
123
|
+
return doc;
|
|
124
|
+
}
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
static VALUE doc_write(VALUE self, VALUE out_filename) {
|
|
127
|
+
checked_string(out_filename);
|
|
128
|
+
DocumentHandle* h = handle_of(self);
|
|
129
|
+
guarded([&] { h->write(cpp_string(out_filename)); });
|
|
130
|
+
RB_GC_GUARD(out_filename);
|
|
131
|
+
return Qnil;
|
|
132
|
+
}
|
|
121
133
|
|
|
122
|
-
|
|
134
|
+
static VALUE doc_to_memory(VALUE self) {
|
|
135
|
+
DocumentHandle* h = handle_of(self);
|
|
136
|
+
return guarded_to_ruby([&] { return h->write_to_memory(); }, [](std::string const& bytes) {
|
|
137
|
+
return rb_str_new(bytes.data(), static_cast<long>(bytes.size()));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
// ---- structure tree --------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
static QPDFObjectHandle struct_kids(QPDF& pdf) {
|
|
144
|
+
QPDFObjectHandle struct_root = pdf.getRoot().getKey("/StructTreeRoot");
|
|
145
|
+
if (!struct_root.isDictionary()) throw std::runtime_error("No StructTreeRoot found");
|
|
146
|
+
return struct_root.getKey("/K");
|
|
147
|
+
}
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
static VALUE doc_show_structure(VALUE self) {
|
|
150
|
+
DocumentHandle* h = handle_of(self);
|
|
151
|
+
return guarded_to_ruby(
|
|
152
|
+
[&] {
|
|
153
|
+
QPDF& pdf = h->qpdf();
|
|
154
|
+
QPDFObjectHandle kids = struct_kids(pdf);
|
|
155
|
+
PDFStructWalker walker;
|
|
156
|
+
walker.buildPageObjectMap(pdf);
|
|
157
|
+
std::string out;
|
|
158
|
+
if (kids.isArray()) {
|
|
159
|
+
for (auto kid : kids.aitems()) out += walker.get_structure_as_string(kid);
|
|
160
|
+
} else {
|
|
161
|
+
out = walker.get_structure_as_string(kids);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
},
|
|
165
|
+
ruby_string);
|
|
166
|
+
}
|
|
130
167
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
static VALUE doc_ensure_bbox(VALUE self) {
|
|
169
|
+
DocumentHandle* h = handle_of(self);
|
|
170
|
+
guarded([&] {
|
|
171
|
+
QPDF& pdf = h->qpdf();
|
|
172
|
+
QPDFObjectHandle kids = struct_kids(pdf);
|
|
173
|
+
PDFStructWalker walker(std::cout, find_mcid_bounds(pdf));
|
|
174
|
+
if (kids.isArray()) {
|
|
175
|
+
for (auto kid : kids.aitems()) walker.ensureLayoutBBox(kid);
|
|
135
176
|
} else {
|
|
136
|
-
walker.ensureLayoutBBox(
|
|
177
|
+
walker.ensureLayoutBBox(kids);
|
|
137
178
|
}
|
|
138
|
-
}
|
|
139
|
-
rb_raise(rb_eRuntimeError, "Error: %s", e.what());
|
|
140
|
-
}
|
|
179
|
+
});
|
|
141
180
|
return Qnil;
|
|
142
181
|
}
|
|
143
182
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
static VALUE
|
|
183
|
+
// ---- PDF/UA ----------------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
static VALUE counts_hash(pdfua::UntaggedCounts const& c) {
|
|
186
|
+
VALUE hash = rb_hash_new();
|
|
187
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("paths")), LONG2NUM(c.paths));
|
|
188
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("texts")), LONG2NUM(c.texts));
|
|
189
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("xobjects")), LONG2NUM(c.xobjects));
|
|
190
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("shadings")), LONG2NUM(c.shadings));
|
|
191
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("inline_images")), LONG2NUM(c.inline_images));
|
|
192
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("total")), LONG2NUM(c.total()));
|
|
193
|
+
return hash;
|
|
194
|
+
}
|
|
147
195
|
|
|
148
|
-
static VALUE
|
|
149
|
-
|
|
196
|
+
static VALUE doc_mark_untagged_content_as_artifacts(VALUE self) {
|
|
197
|
+
DocumentHandle* h = handle_of(self);
|
|
198
|
+
auto counts = guarded([&] { return pdfua::mark_untagged_content_as_artifacts(h->qpdf()); });
|
|
199
|
+
return counts_hash(counts);
|
|
200
|
+
}
|
|
150
201
|
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
static VALUE doc_mark_paths_as_artifacts(VALUE self) {
|
|
203
|
+
rb_category_warn(RB_WARN_CATEGORY_DEPRECATED,
|
|
204
|
+
"QpdfRuby::Document#mark_paths_as_artifacts is deprecated; use #mark_untagged_content_as_artifacts");
|
|
205
|
+
return doc_mark_untagged_content_as_artifacts(self);
|
|
206
|
+
}
|
|
153
207
|
|
|
154
|
-
|
|
208
|
+
static VALUE doc_untagged_content(VALUE self) {
|
|
209
|
+
DocumentHandle* h = handle_of(self);
|
|
210
|
+
auto counts = guarded([&] { return pdfua::count_untagged_content(h->qpdf()); });
|
|
211
|
+
return counts_hash(counts);
|
|
212
|
+
}
|
|
155
213
|
|
|
156
|
-
|
|
214
|
+
static VALUE doc_describe_links(int argc, VALUE* argv, VALUE self) {
|
|
215
|
+
VALUE texts = Qnil;
|
|
216
|
+
rb_scan_args(argc, argv, "01", &texts);
|
|
217
|
+
VALUE pairs = checked_link_texts(texts);
|
|
218
|
+
DocumentHandle* h = handle_of(self);
|
|
219
|
+
long described = guarded([&] { return pdfua::describe_links(h->qpdf(), cpp_link_texts(pairs)); });
|
|
220
|
+
RB_GC_GUARD(pairs);
|
|
221
|
+
return LONG2NUM(described);
|
|
222
|
+
}
|
|
157
223
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
224
|
+
static VALUE doc_artifact_tagged_decorations(VALUE self) {
|
|
225
|
+
DocumentHandle* h = handle_of(self);
|
|
226
|
+
return LONG2NUM(guarded([&] { return pdfua::artifact_tagged_decorations(h->qpdf()); }));
|
|
227
|
+
}
|
|
163
228
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} catch (const std::exception& e) {
|
|
169
|
-
rb_raise(rb_eQpdfRubyError, "%s", e.what());
|
|
170
|
-
}
|
|
229
|
+
static VALUE doc_wrap_list_bodies(VALUE self) {
|
|
230
|
+
DocumentHandle* h = handle_of(self);
|
|
231
|
+
return LONG2NUM(guarded([&] { return pdfua::wrap_list_bodies(h->qpdf()); }));
|
|
232
|
+
}
|
|
171
233
|
|
|
172
|
-
|
|
234
|
+
static VALUE doc_parent_tree_mismatches(VALUE self) {
|
|
235
|
+
DocumentHandle* h = handle_of(self);
|
|
236
|
+
return LONG2NUM(guarded([&] { return pdfua::count_parent_tree_mismatches(h->qpdf()); }));
|
|
173
237
|
}
|
|
174
238
|
|
|
175
|
-
static VALUE
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (qpdf_ruby::qpdf_ruby_write(h, StringValueCStr(out_filename)) == -1) rb_sys_fail("qpdf_ruby_write");
|
|
239
|
+
static VALUE doc_map_nonstandard_roles(VALUE self) {
|
|
240
|
+
DocumentHandle* h = handle_of(self);
|
|
241
|
+
return LONG2NUM(guarded([&] { return pdfua::map_nonstandard_roles(h->qpdf()); }));
|
|
242
|
+
}
|
|
180
243
|
|
|
181
|
-
|
|
244
|
+
static VALUE doc_retag_grouping_figures(VALUE self) {
|
|
245
|
+
DocumentHandle* h = handle_of(self);
|
|
246
|
+
return LONG2NUM(guarded([&] { return pdfua::retag_grouping_figures(h->qpdf()); }));
|
|
182
247
|
}
|
|
183
248
|
|
|
184
|
-
VALUE
|
|
185
|
-
|
|
186
|
-
|
|
249
|
+
static VALUE doc_figures_without_alt(VALUE self) {
|
|
250
|
+
DocumentHandle* h = handle_of(self);
|
|
251
|
+
return LONG2NUM(guarded([&] { return pdfua::count_figures_without_alt(h->qpdf()); }));
|
|
252
|
+
}
|
|
187
253
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
254
|
+
// Every Link annotation: page (1-based), target URI (nil for internal links) and description.
|
|
255
|
+
static VALUE doc_links(VALUE self) {
|
|
256
|
+
DocumentHandle* h = handle_of(self);
|
|
257
|
+
struct Link {
|
|
258
|
+
int page;
|
|
259
|
+
std::optional<std::string> uri;
|
|
260
|
+
std::optional<std::string> contents;
|
|
261
|
+
};
|
|
262
|
+
auto collect = [&] {
|
|
263
|
+
std::vector<Link> out;
|
|
264
|
+
auto pages = h->qpdf().getAllPages();
|
|
265
|
+
for (size_t i = 0; i < pages.size(); ++i) {
|
|
266
|
+
QPDFObjectHandle annots = pages[i].getKey("/Annots");
|
|
267
|
+
if (!annots.isArray()) continue;
|
|
268
|
+
for (auto annot : annots.aitems()) {
|
|
269
|
+
if (!annot.isDictionary() || !annot.getKey("/Subtype").isNameAndEquals("/Link")) continue;
|
|
270
|
+
Link link{static_cast<int>(i) + 1, std::nullopt, std::nullopt};
|
|
271
|
+
QPDFObjectHandle action = annot.getKey("/A");
|
|
272
|
+
if (action.isDictionary() && action.getKey("/URI").isString()) link.uri = action.getKey("/URI").getUTF8Value();
|
|
273
|
+
if (annot.getKey("/Contents").isString()) link.contents = annot.getKey("/Contents").getUTF8Value();
|
|
274
|
+
out.push_back(link);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
};
|
|
279
|
+
return guarded_to_ruby(collect, [](std::vector<Link> const& links) {
|
|
280
|
+
VALUE array = rb_ary_new();
|
|
281
|
+
for (auto const& link : links) {
|
|
282
|
+
VALUE hash = rb_hash_new();
|
|
283
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("page")), INT2NUM(link.page));
|
|
284
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("uri")), link.uri ? ruby_string(*link.uri) : Qnil);
|
|
285
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("contents")), link.contents ? ruby_string(*link.contents) : Qnil);
|
|
286
|
+
rb_ary_push(array, hash);
|
|
287
|
+
}
|
|
288
|
+
return array;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
206
291
|
|
|
207
|
-
|
|
292
|
+
// The catalog's XMP metadata as a string, or nil.
|
|
293
|
+
static VALUE doc_metadata(VALUE self) {
|
|
294
|
+
DocumentHandle* h = handle_of(self);
|
|
295
|
+
return guarded_to_ruby(
|
|
296
|
+
[&]() -> std::optional<std::string> {
|
|
297
|
+
QPDFObjectHandle metadata = h->qpdf().getRoot().getKey("/Metadata");
|
|
298
|
+
if (!metadata.isStream()) return std::nullopt;
|
|
299
|
+
auto data = metadata.getStreamData(qpdf_dl_generalized);
|
|
300
|
+
return std::string(reinterpret_cast<char const*>(data->getBuffer()), data->getSize());
|
|
301
|
+
},
|
|
302
|
+
[](std::optional<std::string> const& xmp) { return xmp ? ruby_string(*xmp) : Qnil; });
|
|
303
|
+
}
|
|
208
304
|
|
|
209
|
-
|
|
210
|
-
|
|
305
|
+
// The structure tree's RoleMap as {"Aside" => "Sect", ...}.
|
|
306
|
+
static VALUE doc_role_map(VALUE self) {
|
|
307
|
+
DocumentHandle* h = handle_of(self);
|
|
308
|
+
return guarded_to_ruby(
|
|
309
|
+
[&] {
|
|
310
|
+
std::map<std::string, std::string> out;
|
|
311
|
+
QPDFObjectHandle tree = h->qpdf().getRoot().getKey("/StructTreeRoot");
|
|
312
|
+
QPDFObjectHandle map = tree.isDictionary() ? tree.getKey("/RoleMap") : QPDFObjectHandle::newNull();
|
|
313
|
+
if (map.isDictionary()) {
|
|
314
|
+
for (auto [key, value] : map.ditems()) {
|
|
315
|
+
if (value.isName()) out[key.substr(1)] = value.getName().substr(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
},
|
|
320
|
+
[](std::map<std::string, std::string> const& roles) {
|
|
321
|
+
VALUE hash = rb_hash_new();
|
|
322
|
+
for (auto const& [key, value] : roles) rb_hash_aset(hash, ruby_string(key), ruby_string(value));
|
|
323
|
+
return hash;
|
|
324
|
+
});
|
|
211
325
|
}
|
|
212
326
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Qtrue, // accessibility
|
|
230
|
-
Qfalse, // assemble
|
|
231
|
-
Qfalse, // annotate_and_form
|
|
232
|
-
Qfalse, // form_filling
|
|
233
|
-
Qtrue, // encrypt_metadata
|
|
234
|
-
Qtrue // use_aes
|
|
235
|
-
};
|
|
327
|
+
// rb_get_kwargs raises ArgumentError for an unknown keyword (a typo like tittle:) and leaves Qundef
|
|
328
|
+
// for one not given; this turns that into nil.
|
|
329
|
+
static VALUE given(VALUE value) { return value == Qundef ? Qnil : value; }
|
|
330
|
+
|
|
331
|
+
static VALUE doc_add_pdfua_identification(int argc, VALUE* argv, VALUE self) {
|
|
332
|
+
VALUE kwargs = Qnil;
|
|
333
|
+
rb_scan_args(argc, argv, ":", &kwargs);
|
|
334
|
+
ID keys[1] = {rb_intern("title")};
|
|
335
|
+
VALUE values[1];
|
|
336
|
+
rb_get_kwargs(kwargs, keys, 0, 1, values);
|
|
337
|
+
VALUE title = checked_optional_string(given(values[0]));
|
|
338
|
+
DocumentHandle* h = handle_of(self);
|
|
339
|
+
bool changed = guarded([&] { return pdfua::add_pdfua_identification(h->qpdf(), cpp_optional_string(title)); });
|
|
340
|
+
RB_GC_GUARD(title);
|
|
341
|
+
return changed ? Qtrue : Qfalse;
|
|
342
|
+
}
|
|
236
343
|
|
|
237
|
-
|
|
238
|
-
keys[1] = rb_intern("owner_pw");
|
|
239
|
-
keys[2] = rb_intern("encryption_revision");
|
|
240
|
-
keys[3] = rb_intern("allow_print");
|
|
241
|
-
keys[4] = rb_intern("allow_modify");
|
|
242
|
-
keys[5] = rb_intern("allow_extract");
|
|
243
|
-
keys[6] = rb_intern("accessibility");
|
|
244
|
-
keys[7] = rb_intern("assemble");
|
|
245
|
-
keys[8] = rb_intern("annotate_and_form");
|
|
246
|
-
keys[9] = rb_intern("form_filling");
|
|
247
|
-
keys[10] = rb_intern("encrypt_metadata");
|
|
248
|
-
keys[11] = rb_intern("use_aes");
|
|
344
|
+
static VALUE report_hash(pdfua::Report const& report);
|
|
249
345
|
|
|
346
|
+
static VALUE doc_apply_pdfua_fixes(int argc, VALUE* argv, VALUE self) {
|
|
347
|
+
VALUE kwargs = Qnil;
|
|
250
348
|
rb_scan_args(argc, argv, ":", &kwargs);
|
|
349
|
+
ID keys[2] = {rb_intern("link_texts"), rb_intern("title")};
|
|
350
|
+
VALUE values[2];
|
|
351
|
+
rb_get_kwargs(kwargs, keys, 0, 2, values);
|
|
352
|
+
VALUE pairs = checked_link_texts(given(values[0]));
|
|
353
|
+
VALUE title = checked_optional_string(given(values[1]));
|
|
354
|
+
DocumentHandle* h = handle_of(self);
|
|
355
|
+
|
|
356
|
+
VALUE out = guarded_to_ruby(
|
|
357
|
+
[&] { return pdfua::apply(h->qpdf(), cpp_link_texts(pairs), cpp_optional_string(title)); }, report_hash);
|
|
358
|
+
RB_GC_GUARD(pairs);
|
|
359
|
+
RB_GC_GUARD(title);
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
static VALUE report_hash(pdfua::Report const& report) {
|
|
364
|
+
VALUE hash = rb_hash_new();
|
|
365
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("decorations")), LONG2NUM(report.decorations));
|
|
366
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("artifacts")), counts_hash(report.artifacts));
|
|
367
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("links")), LONG2NUM(report.links));
|
|
368
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("list_bodies")), LONG2NUM(report.list_bodies));
|
|
369
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("roles")), LONG2NUM(report.roles));
|
|
370
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("figure_groups")), LONG2NUM(report.figure_groups));
|
|
371
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("figures_without_alt")), LONG2NUM(report.figures_without_alt));
|
|
372
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("identified")), report.identified ? Qtrue : Qfalse);
|
|
373
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("unidentified_reason")),
|
|
374
|
+
report.unidentified_reason ? ruby_string(*report.unidentified_reason) : Qnil);
|
|
375
|
+
return hash;
|
|
376
|
+
}
|
|
251
377
|
|
|
252
|
-
|
|
378
|
+
// ---- encryption ------------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
static VALUE doc_encrypt(int argc, VALUE* argv, VALUE self) {
|
|
381
|
+
VALUE kwargs = Qnil;
|
|
382
|
+
ID keys[12] = {rb_intern("user_pw"), rb_intern("owner_pw"), rb_intern("encryption_revision"),
|
|
383
|
+
rb_intern("allow_print"), rb_intern("allow_modify"), rb_intern("allow_extract"),
|
|
384
|
+
rb_intern("accessibility"), rb_intern("assemble"), rb_intern("annotate_and_form"),
|
|
385
|
+
rb_intern("form_filling"), rb_intern("encrypt_metadata"), rb_intern("use_aes")};
|
|
386
|
+
VALUE values[12];
|
|
387
|
+
rb_scan_args(argc, argv, ":", &kwargs);
|
|
253
388
|
rb_get_kwargs(kwargs, keys, 0, 12, values);
|
|
254
389
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
390
|
+
// rb_get_kwargs leaves Qundef for keywords not given.
|
|
391
|
+
auto or_default = [&](int i, VALUE fallback) { return values[i] == Qundef ? fallback : values[i]; };
|
|
392
|
+
VALUE user_pw = checked_string(or_default(0, rb_str_new_cstr("")));
|
|
393
|
+
VALUE owner_pw = checked_string(or_default(1, rb_str_new_cstr("")));
|
|
394
|
+
int revision = NUM2INT(or_default(2, INT2NUM(4)));
|
|
395
|
+
auto print = static_cast<qpdf_r3_print_e>(NUM2INT(or_default(3, INT2NUM(qpdf_r3p_low))));
|
|
396
|
+
|
|
397
|
+
DocumentHandle* h = handle_of(self);
|
|
398
|
+
guarded([&] {
|
|
399
|
+
h->set_encryption(cpp_string(user_pw), cpp_string(owner_pw), revision, print, RTEST(or_default(4, Qfalse)),
|
|
400
|
+
RTEST(or_default(5, Qfalse)), RTEST(or_default(6, Qtrue)), RTEST(or_default(7, Qfalse)),
|
|
401
|
+
RTEST(or_default(8, Qfalse)), RTEST(or_default(9, Qfalse)), RTEST(or_default(10, Qtrue)),
|
|
402
|
+
RTEST(or_default(11, Qtrue)));
|
|
403
|
+
});
|
|
404
|
+
RB_GC_GUARD(user_pw);
|
|
405
|
+
RB_GC_GUARD(owner_pw);
|
|
271
406
|
return Qnil;
|
|
272
407
|
}
|
|
273
408
|
|
|
274
|
-
|
|
409
|
+
// ---- definitions -----------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
extern "C" {
|
|
412
|
+
RUBY_FUNC_EXPORTED void Init_qpdf_ruby(void) {
|
|
275
413
|
rb_mQpdfRuby = rb_define_module("QpdfRuby");
|
|
414
|
+
rb_eQpdfRubyError = rb_define_class_under(rb_mQpdfRuby, "Error", rb_eRuntimeError);
|
|
276
415
|
rb_cDocument = rb_define_class_under(rb_mQpdfRuby, "Document", rb_cObject);
|
|
277
|
-
rb_eQpdfRubyError = rb_define_class_under(rb_mQpdfRuby, "Error", rb_eStandardError);
|
|
278
|
-
|
|
279
416
|
rb_define_alloc_func(rb_cDocument, doc_alloc);
|
|
280
417
|
|
|
281
418
|
rb_define_method(rb_cDocument, "initialize", RUBY_METHOD_FUNC(doc_initialize), -1);
|
|
282
|
-
rb_define_singleton_method(rb_cDocument, "from_memory", RUBY_METHOD_FUNC(doc_from_memory),
|
|
283
|
-
|
|
419
|
+
rb_define_singleton_method(rb_cDocument, "from_memory", RUBY_METHOD_FUNC(doc_from_memory), -1);
|
|
284
420
|
rb_define_method(rb_cDocument, "write", RUBY_METHOD_FUNC(doc_write), 1);
|
|
285
421
|
rb_define_method(rb_cDocument, "to_memory", RUBY_METHOD_FUNC(doc_to_memory), 0);
|
|
286
422
|
|
|
287
|
-
rb_define_method(rb_cDocument, "
|
|
288
|
-
rb_define_method(rb_cDocument, "ensure_bbox", RUBY_METHOD_FUNC(
|
|
289
|
-
|
|
290
|
-
rb_define_method(rb_cDocument, "
|
|
423
|
+
rb_define_method(rb_cDocument, "show_structure", RUBY_METHOD_FUNC(doc_show_structure), 0);
|
|
424
|
+
rb_define_method(rb_cDocument, "ensure_bbox", RUBY_METHOD_FUNC(doc_ensure_bbox), 0);
|
|
425
|
+
|
|
426
|
+
rb_define_method(rb_cDocument, "mark_untagged_content_as_artifacts",
|
|
427
|
+
RUBY_METHOD_FUNC(doc_mark_untagged_content_as_artifacts), 0);
|
|
428
|
+
rb_define_method(rb_cDocument, "mark_paths_as_artifacts", RUBY_METHOD_FUNC(doc_mark_paths_as_artifacts), 0);
|
|
429
|
+
rb_define_method(rb_cDocument, "untagged_content", RUBY_METHOD_FUNC(doc_untagged_content), 0);
|
|
430
|
+
rb_define_method(rb_cDocument, "describe_links", RUBY_METHOD_FUNC(doc_describe_links), -1);
|
|
431
|
+
rb_define_method(rb_cDocument, "artifact_tagged_decorations", RUBY_METHOD_FUNC(doc_artifact_tagged_decorations), 0);
|
|
432
|
+
rb_define_method(rb_cDocument, "wrap_list_bodies", RUBY_METHOD_FUNC(doc_wrap_list_bodies), 0);
|
|
433
|
+
rb_define_method(rb_cDocument, "parent_tree_mismatches", RUBY_METHOD_FUNC(doc_parent_tree_mismatches), 0);
|
|
434
|
+
rb_define_method(rb_cDocument, "map_nonstandard_roles", RUBY_METHOD_FUNC(doc_map_nonstandard_roles), 0);
|
|
435
|
+
rb_define_method(rb_cDocument, "retag_grouping_figures", RUBY_METHOD_FUNC(doc_retag_grouping_figures), 0);
|
|
436
|
+
rb_define_method(rb_cDocument, "figures_without_alt", RUBY_METHOD_FUNC(doc_figures_without_alt), 0);
|
|
437
|
+
rb_define_method(rb_cDocument, "add_pdfua_identification", RUBY_METHOD_FUNC(doc_add_pdfua_identification), -1);
|
|
438
|
+
rb_define_method(rb_cDocument, "apply_pdfua_fixes", RUBY_METHOD_FUNC(doc_apply_pdfua_fixes), -1);
|
|
439
|
+
|
|
440
|
+
rb_define_method(rb_cDocument, "links", RUBY_METHOD_FUNC(doc_links), 0);
|
|
441
|
+
rb_define_method(rb_cDocument, "metadata", RUBY_METHOD_FUNC(doc_metadata), 0);
|
|
442
|
+
rb_define_method(rb_cDocument, "role_map", RUBY_METHOD_FUNC(doc_role_map), 0);
|
|
443
|
+
|
|
444
|
+
rb_define_method(rb_cDocument, "encrypt", RUBY_METHOD_FUNC(doc_encrypt), -1);
|
|
291
445
|
|
|
292
446
|
rb_define_const(rb_mQpdfRuby, "PRINT_FULL", INT2NUM(qpdf_r3p_full));
|
|
293
447
|
rb_define_const(rb_mQpdfRuby, "PRINT_LOW", INT2NUM(qpdf_r3p_low));
|
|
294
448
|
rb_define_const(rb_mQpdfRuby, "PRINT_NONE", INT2NUM(qpdf_r3p_none));
|
|
295
449
|
}
|
|
450
|
+
} // extern "C"
|