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