qpdf_ruby 0.1.0 → 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.
@@ -1,204 +1,450 @@
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 <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>
12
+ #include <map>
13
+ #include <optional>
14
+ #include <string>
15
+ #include <vector>
17
16
 
18
17
  VALUE rb_mQpdfRuby;
19
18
  VALUE rb_cDocument;
19
+ VALUE rb_eQpdfRubyError;
20
20
 
21
21
  using namespace qpdf_ruby;
22
22
 
23
- VALUE rb_qpdf_get_structure_string(VALUE self) {
24
- DocumentHandle* h;
25
- Data_Get_Struct(self, DocumentHandle, h);
26
-
27
- try {
28
- QPDF& pdf = h->qpdf();
29
-
30
- QPDFObjectHandle catalog = pdf.getRoot();
31
- QPDFObjectHandle struct_root = catalog.getKey("/StructTreeRoot");
32
- if (!struct_root.isDictionary()) {
33
- rb_raise(rb_eRuntimeError, "No StructTreeRoot found");
34
- }
35
- QPDFObjectHandle topKids = struct_root.getKey("/K");
23
+ // ---- the wrapped DocumentHandle ------------------------------------------------------------------
36
24
 
37
- PDFStructWalker walker(std::cout); // For now, std::cout, unless you pass another stream
38
- walker.buildPageObjectMap(pdf);
25
+ static void doc_free(void* ptr) { qpdf_ruby_close(static_cast<DocumentHandle*>(ptr)); }
39
26
 
40
- std::string result;
41
- if (topKids.isArray()) {
42
- for (int i = 0; i < topKids.getArrayNItems(); ++i) {
43
- result += walker.get_structure_as_string(topKids.getArrayItem(i));
44
- }
45
- } else {
46
- result = walker.get_structure_as_string(topKids);
47
- }
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
+ };
48
34
 
49
- return rb_str_new(result.c_str(), result.length());
50
- } catch (const std::exception& e) {
51
- rb_raise(rb_eRuntimeError, "Error: %s", e.what());
52
- }
53
- 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;
54
39
  }
55
40
 
56
- VALUE rb_qpdf_mark_paths_as_artifacts(VALUE self) {
57
- DocumentHandle* h;
58
- Data_Get_Struct(self, DocumentHandle, h);
59
- try {
60
- QPDF& pdf = h->qpdf();
41
+ static VALUE doc_alloc(VALUE klass) { return TypedData_Wrap_Struct(klass, &document_type, nullptr); }
61
42
 
62
- std::vector<QPDFObjectHandle> pages = pdf.getAllPages();
63
- std::regex path_regex(R"((?:[-+]?\d*\.?\d+(?:e[-+]?\d+)?\s+){4}re\s+(?:S|s|f\*?|F|B\*?|b\*?|n))",
64
- std::regex::ECMAScript | std::regex::optimize);
43
+ // ---- arguments and results (see the rules in ruby_guard.hpp) ------------------------------------
65
44
 
66
- for (auto& page_obj : pages) {
67
- QPDFPageObjectHelper poh(page_obj);
68
- std::vector<QPDFObjectHandle> contents = poh.getPageContents();
69
- std::vector<QPDFObjectHandle> new_contents_array;
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
+ }
70
50
 
71
- for (auto& content_stream : contents) {
72
- if (content_stream.isStream()) {
73
- // Use std::shared_ptr instead of PointerHolder
74
- std::shared_ptr<Buffer> stream_buffer = content_stream.getStreamData();
75
- 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); }
76
52
 
77
- std::string new_stream_data_str = std::regex_replace(stream_data_str, path_regex, "/Artifact BMC\n$&\nEMC");
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
+ }
78
57
 
79
- // Create a new Buffer with std::shared_ptr
80
- std::shared_ptr<Buffer> new_buffer = std::make_shared<Buffer>(new_stream_data_str.length());
81
- memcpy(new_buffer->getBuffer(), new_stream_data_str.data(), new_stream_data_str.length());
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
+ }
82
66
 
83
- QPDFObjectHandle new_stream = QPDFObjectHandle::newStream(&pdf, new_buffer);
84
- new_contents_array.push_back(new_stream);
85
- } else {
86
- new_contents_array.push_back(content_stream);
87
- }
88
- }
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)); }
89
69
 
90
- if (new_contents_array.size() == 1) {
91
- page_obj.replaceKey("/Contents", new_contents_array[0]);
92
- } else {
93
- page_obj.replaceKey("/Contents", QPDFObjectHandle::newArray(new_contents_array));
94
- }
95
- }
96
- } catch (const QPDFExc& e) { // Catching specific QPDF exceptions is good
97
- rb_raise(rb_eRuntimeError, "QPDF Error: %s (filename: %s)", e.what(), e.getFilename().c_str());
98
- } catch (const std::exception& e) { // Fallback for other standard exceptions
99
- rb_raise(rb_eRuntimeError, "Error: %s", e.what());
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));
100
81
  }
82
+ return texts;
83
+ }
101
84
 
102
- return Qnil;
85
+ static VALUE ruby_string(std::string const& value) {
86
+ return rb_utf8_str_new(value.data(), static_cast<long>(value.size()));
103
87
  }
104
88
 
105
- VALUE rb_qpdf_ensure_bboxs(VALUE self) {
106
- DocumentHandle* h;
107
- Data_Get_Struct(self, DocumentHandle, h);
89
+ // ---- opening and writing ---------------------------------------------------------------------
90
+
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
+ }
108
107
 
109
- try {
110
- QPDF& pdf = h->qpdf();
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
+ }
111
125
 
112
- QPDFObjectHandle catalog = pdf.getRoot();
113
- QPDFObjectHandle struct_root = catalog.getKey("/StructTreeRoot");
114
- if (!struct_root.isDictionary()) {
115
- rb_raise(rb_eRuntimeError, "No StructTreeRoot found");
116
- }
117
- QPDFObjectHandle topKids = struct_root.getKey("/K");
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
+ }
118
133
 
119
- PDFImageMapper finder(0);
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
+ }
120
140
 
121
- finder.find(pdf);
141
+ // ---- structure tree --------------------------------------------------------------------------
122
142
 
123
- std::unordered_map<int, std::array<double, 4>> mcid2bbox;
124
- for (const auto& kv : finder.getImageMap()) {
125
- if (kv.second.mcid >= 0) mcid2bbox[kv.second.mcid] = kv.second.bbox;
126
- }
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
+ }
127
148
 
128
- PDFStructWalker walker(std::cout, mcid2bbox); // For now, std::cout, unless you pass another stream
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
+ }
129
167
 
130
- if (topKids.isArray()) {
131
- for (int i = 0; i < topKids.getArrayNItems(); ++i) {
132
- walker.ensureLayoutBBox(topKids.getArrayItem(i));
133
- }
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);
134
176
  } else {
135
- walker.ensureLayoutBBox(topKids);
177
+ walker.ensureLayoutBBox(kids);
136
178
  }
137
- } catch (const std::exception& e) {
138
- rb_raise(rb_eRuntimeError, "Error: %s", e.what());
139
- }
179
+ });
140
180
  return Qnil;
141
181
  }
142
182
 
143
- static void doc_free(void* ptr) { qpdf_ruby::qpdf_ruby_close(static_cast<qpdf_ruby::DocumentHandle*>(ptr)); }
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
+ }
144
195
 
145
- static VALUE doc_alloc(VALUE klass) { return Data_Wrap_Struct(klass, /* mark */ 0, doc_free, nullptr); }
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
+ }
146
201
 
147
- static VALUE doc_initialize(VALUE self, VALUE filename) {
148
- Check_Type(filename, T_STRING);
149
- DocumentHandle* h = qpdf_ruby::qpdf_ruby_open(StringValueCStr(filename));
150
- if (!h) rb_sys_fail("qpdf_ruby_open");
151
- DATA_PTR(self) = h;
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
+ }
152
207
 
153
- return self;
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);
154
212
  }
155
213
 
156
- static VALUE doc_write(VALUE self, VALUE out_filename) {
157
- DocumentHandle* h;
158
- Data_Get_Struct(self, DocumentHandle, h);
159
- if (qpdf_ruby::qpdf_ruby_write(h, StringValueCStr(out_filename)) == -1) rb_sys_fail("qpdf_ruby_write");
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
+ }
160
223
 
161
- return Qnil;
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()); }));
162
227
  }
163
228
 
164
- VALUE qpdf_ruby_write_memory(DocumentHandle* h) {
165
- if (!h) rb_sys_fail("Bad handle");
166
- std::string bytes = h->write_to_memory();
167
- return rb_str_new(bytes.data(), bytes.size());
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()); }));
168
232
  }
169
233
 
170
- static VALUE doc_from_memory(VALUE klass, VALUE str, VALUE password) {
171
- Check_Type(str, T_STRING);
172
- Check_Type(password, T_STRING);
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()); }));
237
+ }
173
238
 
174
- DocumentHandle* h = qpdf_ruby_open_memory("ruby-memory", reinterpret_cast<unsigned char const*>(RSTRING_PTR(str)),
175
- RSTRING_LEN(str), StringValueCStr(password));
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
+ }
176
243
 
177
- if (!h) rb_sys_fail("qpdf_ruby_open_memory");
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()); }));
247
+ }
178
248
 
179
- VALUE obj = Data_Wrap_Struct(klass, 0, doc_free, h);
180
- return obj;
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()); }));
181
252
  }
182
253
 
183
- static VALUE doc_to_memory(VALUE self) {
184
- DocumentHandle* h;
185
- Data_Get_Struct(self, DocumentHandle, h);
186
- return qpdf_ruby_write_memory(h); // returns a Ruby ::String
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
+ }
291
+
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
+ }
304
+
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
+ });
325
+ }
326
+
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
+ }
343
+
344
+ static VALUE report_hash(pdfua::Report const& report);
345
+
346
+ static VALUE doc_apply_pdfua_fixes(int argc, VALUE* argv, VALUE self) {
347
+ VALUE kwargs = Qnil;
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;
187
376
  }
188
377
 
189
- RUBY_FUNC_EXPORTED "C" void Init_qpdf_ruby(void) {
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);
388
+ rb_get_kwargs(kwargs, keys, 0, 12, values);
389
+
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);
406
+ return Qnil;
407
+ }
408
+
409
+ // ---- definitions -----------------------------------------------------------------------------
410
+
411
+ extern "C" {
412
+ RUBY_FUNC_EXPORTED void Init_qpdf_ruby(void) {
190
413
  rb_mQpdfRuby = rb_define_module("QpdfRuby");
414
+ rb_eQpdfRubyError = rb_define_class_under(rb_mQpdfRuby, "Error", rb_eRuntimeError);
191
415
  rb_cDocument = rb_define_class_under(rb_mQpdfRuby, "Document", rb_cObject);
192
-
193
416
  rb_define_alloc_func(rb_cDocument, doc_alloc);
194
417
 
195
- rb_define_method(rb_cDocument, "initialize", RUBY_METHOD_FUNC(doc_initialize), 1);
196
- rb_define_singleton_method(rb_cDocument, "from_memory", RUBY_METHOD_FUNC(doc_from_memory), 2);
197
-
418
+ rb_define_method(rb_cDocument, "initialize", RUBY_METHOD_FUNC(doc_initialize), -1);
419
+ rb_define_singleton_method(rb_cDocument, "from_memory", RUBY_METHOD_FUNC(doc_from_memory), -1);
198
420
  rb_define_method(rb_cDocument, "write", RUBY_METHOD_FUNC(doc_write), 1);
199
421
  rb_define_method(rb_cDocument, "to_memory", RUBY_METHOD_FUNC(doc_to_memory), 0);
200
422
 
201
- rb_define_method(rb_cDocument, "mark_paths_as_artifacts", RUBY_METHOD_FUNC(rb_qpdf_mark_paths_as_artifacts), 0);
202
- rb_define_method(rb_cDocument, "ensure_bbox", RUBY_METHOD_FUNC(rb_qpdf_ensure_bboxs), 0);
203
- rb_define_method(rb_cDocument, "show_structure", RUBY_METHOD_FUNC(rb_qpdf_get_structure_string), 0);
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);
445
+
446
+ rb_define_const(rb_mQpdfRuby, "PRINT_FULL", INT2NUM(qpdf_r3p_full));
447
+ rb_define_const(rb_mQpdfRuby, "PRINT_LOW", INT2NUM(qpdf_r3p_low));
448
+ rb_define_const(rb_mQpdfRuby, "PRINT_NONE", INT2NUM(qpdf_r3p_none));
204
449
  }
450
+ } // extern "C"
@@ -3,8 +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);
6
+ extern "C" void Init_qpdf_ruby(void);
9
7
 
10
8
  #endif /* QPDF_RUBY_H */