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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f6da89731f271f230b6784f82f01bb4f1dfe4060d4caae6d956fa73d8fc13e
4
- data.tar.gz: c3d2e9dc8baf1975acbca704b8b76dab210c0f959ce78b8655d1c3c295585d10
3
+ metadata.gz: 2c89e78922fbb1a3922b13c425086212a70454a28b6f17f9f75b8fe9ab772256
4
+ data.tar.gz: 3144ff7e6bae54b2b71fe80bd1b22652b5466120d03cf2fa7f7dcccab15c9d7a
5
5
  SHA512:
6
- metadata.gz: c850b48c57e89e431b3d7340b1cd4819f28887402cdc03af7e108533249a4e8742351732c22217b0fed011e4799b9b00268219ec2b1ea17d6576ef50125ffbbb
7
- data.tar.gz: 60f89a46753d7caa4d70a3832c2dc68bd01470528c5c6c6939df10920608f3e451ed7c11d42874fd6944df1a06bdd4847020e1289fb1b07c100fc502cc48e5ce
6
+ metadata.gz: 2e6736a64f275a612a85727c94ebe7e5ccde471b5024f974611b6a43ce747ee317f0544769996816f943aff50071e9fcc5eb5e92b08316af921b29cc6dea9c1d
7
+ data.tar.gz: 8941eff1af71bed2d082e4eb66256d9b284800aa9cfd95100fadc3eeb4888663f647d5f939816b82fbdd6e735b479aff0724ee6fc72ba8931456c6a64c67d822
data/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
- ## [Unreleased]
1
+ <!-- generated by git-cliff start -->
2
+ # Changelog
3
+
4
+ All notable changes to this project will be documented in this file.
5
+
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ [unreleased]: https://github.com/dieter-medium/qpdf_ruby/compare/v0.2.0..HEAD
10
+
11
+ <!-- generated by git-cliff end -->
12
+
13
+ ## [0.2.0] - 2026-10-02
14
+
15
+ Makes the tagged PDFs Chromium prints (`Page.printToPDF` with `generateTaggedPDF`) PDF/UA-1
16
+ conformant: `apply_pdfua_fixes` plus `ensure_bbox` take Chromium's output to a file veraPDF accepts
17
+ as PDF/UA-1 and PAC 2024 passes without errors.
18
+
19
+ ### 🚀 Added
20
+
21
+ - `apply_pdfua_fixes(link_texts:, title:)` runs every step below and returns a report.
22
+ - `artifact_tagged_decorations`: CSS backgrounds and borders Chromium tags as content of a
23
+ grouping element (a `Table`'s cell backgrounds, an `<article>`'s card) become artifacts.
24
+ - `mark_untagged_content_as_artifacts` and `untagged_content`: untagged paths, text, XObjects,
25
+ shadings and inline images are wrapped in `/Artifact` (7.1-3), never inside tagged content.
26
+ - `describe_links`: Link annotations get `/Contents` (7.18), from given texts, the structure
27
+ element's `/Alt`, named and string destinations ("Page N"), or the URI.
28
+ - `wrap_list_bodies`: list item content moves into `LBody` (7.2-20), ParentTree updated.
29
+ - `map_nonstandard_roles`: PDF 2.0 structure types get a RoleMap entry (7.1-5).
30
+ - `retag_grouping_figures`: the `Figure` Chromium wraps around an HTML `<figure>` becomes a `Div`
31
+ (7.3-1); `figures_without_alt` counts what only a person can fix.
32
+ - `add_pdfua_identification(title:)`: XMP `pdfuaid:part` and `dc:title`, `DisplayDocTitle`,
33
+ `Marked` (7.1-8).
34
+ - `ensure_bbox` computes Figure bounds from the painted content (images, forms, strokes with line
35
+ width, caps and joins) and sets `Placement /Block` outside a line of text.
36
+ - `parent_tree_mismatches`, `links`, `metadata` and `role_map` for inspection.
37
+
38
+ ### 🐛 Fixed
39
+
40
+ - `ensure_bbox`: an uninitialized crop box crashed it, the CTM was replaced instead of
41
+ concatenated, MCIDs were matched across pages and Form XObjects, and `/A` was overwritten.
42
+ - `to_memory` leaked the QPDF buffer behind every call.
43
+ - Ruby exceptions no longer skip C++ destructors, and a `#to_s` callback that reopens the document
44
+ can no longer leave a freed handle in use.
45
+ - Builds against QPDF 11.9 as well as QPDF 12, and runs on Ruby 4.
46
+
47
+ ### 🛠️ Development
48
+
49
+ - veraPDF runs in CI (`bin/install-verapdf.sh`); `bin/asan-rspec.sh` runs the specs under
50
+ AddressSanitizer and LeakSanitizer.
51
+ - `rake release` checks the push credential first, following RubyGems' own order
52
+ (`GEM_HOST_API_KEY`, `gem.push_key`, push-host key, `rubygems_api_key`).
53
+ - The Docker image is built in two stages and pushed for linux/amd64 and linux/arm64 from `main`
54
+ and `v*` tags.
55
+
56
+
57
+ ## [0.1.1] - 2025-06-30
58
+
59
+ ### 🚀 Features
60
+
61
+ - Add PDF encryption support
62
+
63
+
2
64
 
3
65
  ## [0.1.0] - 2025-06-11
4
66
 
5
67
  - Initial release
68
+
69
+ ### 🔄 Released
70
+
71
+ - [unreleased](https://github.com/dieter-medium/qpdf_ruby/compare/v0.2.0..HEAD)
72
+ - [0.2.0](https://github.com/dieter-medium/qpdf_ruby/compare/v0.1.1..v0.2.0)
73
+ - [0.1.1](https://github.com/dieter-medium/qpdf_ruby/compare/v0.1.0..v0.1.1)
data/README.md CHANGED
@@ -1,37 +1,73 @@
1
1
  # QpdfRuby
2
2
 
3
- > **Patch & polish PDFs so that PAC 2024 finally turns green.**
4
-
5
- QpdfRuby is a very small Ruby wrapper around the battle‑tested
6
- [QPDF \>= 12](https://qpdf.sourceforge.net/) C++ library. Right now the
7
- library focuses on only **three specialised tasks** that are needed when
8
- PDFs are printed from Chromium‑based browsers and subsequently audited
9
- with the PAC 2024 accessibility checker:
10
-
11
- 1. **Export the structure tree as XML** – handy for debugging.
12
- 2. **Mark vector path objects as `/Artifact`** so that decorative lines,
13
- boxes, &c. are ignored by assistive technologies.
14
- 3. **Add missing `/BBox` entries to every `/Figure` element** (derived
15
- from the page’s graphic operators) so that screen readers know the
16
- physical extent of each image.
17
-
18
- Together these tweaks eliminate the most common complaints PAC 2024 has
19
- about browser‑generated PDFs.
20
-
21
- ---
22
-
23
- ## Features in Detail
24
-
25
- | Feature | Ruby API |
26
- | ---------------------------------------------- | ---------------------------------------- |
27
- | Dump structure tree as XML | `doc.show_structure` |
28
- | Mark path objects ( `re … S/s/f/F/B/b` ) | `doc.mark_paths_as_artifacts` |
29
- | Ensure `/Figure` elements have a layout BBox¹ | `doc.ensure_bbox` |
30
-
31
- _¹Internally the gem parses each page’s content stream, maps image
32
- `/MCID`s to their transformation matrix, computes the bounding box
33
- (courtesy of a little linear algebra) and finally writes the result into
34
- the structure tree._
3
+ > **Make Chromium's tagged PDFs PDF/UA-1 conformant.**
4
+
5
+ QpdfRuby is a small Ruby wrapper around the [QPDF](https://qpdf.sourceforge.net/) C++ library,
6
+ made for PDFs printed by Chromium (`Page.printToPDF` with `generateTaggedPDF`) and checked with
7
+ veraPDF or PAC 2024. Chromium tags the text of a page but leaves gaps that PDF/UA-1 rejects; the
8
+ gem closes them without touching the content that is tagged:
9
+
10
+ | PDF/UA-1 rule (veraPDF) | What Chromium does | What QpdfRuby does | Ruby API |
11
+ | --- | --- | --- | --- |
12
+ | 7.1-3 untagged content | backgrounds, borders, bars, `aria-hidden` and SVG decorations end up outside any marked content; it never writes `/Artifact` | wraps every painting operation outside marked content in `/Artifact BMC … EMC` (not a Form XObject that carries tagged content) | `doc.mark_untagged_content_as_artifacts` |
13
+ | PAC: content in an inadmissible location, invalid TR | CSS backgrounds and borders are tagged as content of the grouping element they belong to (a `Table`'s cell backgrounds, an `<article>`'s card) | turns such shape-only content into artifacts and takes it out of the structure tree; content with text stays tagged | `doc.artifact_tagged_decorations` |
14
+ | 7.18.1-2, 7.18.5-2 link descriptions | no `/Contents` on Link annotations | your text per URI, else the structure element's `/Alt`/`/ActualText`, else the URI (internal links: your text for `"#<destination>"`, else "Page N" - named destinations from `/Dests`, string destinations from the `/Names` tree) | `doc.describe_links(texts = {})` |
15
+ | 7.2-20 list items | `LI` holds `Lbl` and the content directly, never `LBody` | moves the content into an `LBody`, ParentTree entries updated (a Form XObject's MCIDs in its own entry) | `doc.wrap_list_bodies` |
16
+ | 7.1-5 non-standard types | PDF 2.0 types (`Strong`, `Em`, `Aside`, …) without a RoleMap | maps them to PDF 1.7 types in the RoleMap (table below) | `doc.map_nonstandard_roles` |
17
+ | 7.3-1 figure alternative | an HTML `<figure>` becomes a `Figure` without alt text around the image's own `Figure` | retags that grouping `Figure` as `Div` | `doc.retag_grouping_figures` |
18
+ | 7.1-8 metadata | no XMP `pdfuaid` identification | adds `pdfuaid:part 1` and `dc:title`, each only if missing (read namespace-aware - a commented-out one does not count, an empty one does; extends existing XMP, never replaces a title), sets `DisplayDocTitle` and `Marked` - the identification only for a file with a structure tree | `doc.add_pdfua_identification(title: nil)` |
19
+
20
+ `doc.apply_pdfua_fixes(link_texts: {}, title: nil)` runs all of them and returns a report.
21
+ Each step only adds what is missing, so running it twice changes nothing (the report's
22
+ `identified` is true only when the identification step changed something). When the file cannot
23
+ be identified - it has no structure tree, or its existing XMP has no RDF element the gem can find -
24
+ the other steps still run and `unidentified_reason` says why; it is `nil` otherwise. Figures that
25
+ have no alternative text at all cannot be fixed by a tool - `doc.figures_without_alt` counts them.
26
+
27
+ The RoleMap entries `map_nonstandard_roles` adds, each only for a type the file uses and does not
28
+ map yet. Some are lossy - an assistive technology reads `Title` as a paragraph, `Aside` as a
29
+ section:
30
+
31
+ | PDF 2.0 type | mapped to |
32
+ | --- | --- |
33
+ | `Aside` | `Sect` |
34
+ | `DocumentFragment` | `Part` |
35
+ | `Em`, `Strong`, `Sub` | `Span` |
36
+ | `FENote` | `Note` |
37
+ | `Title` | `P` |
38
+
39
+ Untagged content is looked for in page content and the Form XObjects it draws - not in annotation
40
+ appearance streams, tiling patterns or Type 3 glyph procedures, which Chromium does not emit.
41
+
42
+ Also:
43
+
44
+ | Feature | Ruby API |
45
+ | --- | --- |
46
+ | Count untagged content (dry run, per kind) | `doc.untagged_content` |
47
+ | Add a layout `/BBox` to every `/Figure`¹, and `/Placement /Block` to one outside a line of text (PAC 2024 warns of a "possibly inappropriate use" of a Figure without it) | `doc.ensure_bbox` |
48
+ | Dump the structure tree as XML | `doc.show_structure` |
49
+ | Inspect links, XMP, RoleMap | `doc.links`, `doc.metadata`, `doc.role_map` |
50
+ | Count ParentTree entries that do not name their content's structure element (0 = consistent) | `doc.parent_tree_mismatches` |
51
+ | Encrypt | `doc.encrypt(user_pw:, owner_pw:, …)` |
52
+ | Read/write files or memory | `Document.new(path, password = nil)`, `Document.from_memory(bytes, password = nil)`, `#write(path)`, `#to_memory` |
53
+
54
+ _¹The gem parses each page's content stream, and the content of the Form XObjects it draws, under
55
+ the full transformation matrix and unites what each piece of marked content paints - images, Form
56
+ XObjects, paths - per page, content stream and MCID (a marked-content reference's `/Stm` names the
57
+ form whose MCIDs it means). A stroked path counts with its line width, caps and joins (from `w`, `J`,
58
+ `j`, `M` or an ExtGState); a curve's stroke is bounded a little generously. Text adds nothing (its extent needs font metrics), and content outside
59
+ the crop box is ignored; a Figure with no other content gets the crop box._
60
+
61
+ `#mark_paths_as_artifacts` is deprecated: it now does what `#mark_untagged_content_as_artifacts`
62
+ does. Its old behaviour - wrapping rectangles anywhere, also inside tagged content - fixed few
63
+ Chromium decorations and broke rules 7.1-1/7.1-2.
64
+
65
+ Every error raises `QpdfRuby::Error` (a `RuntimeError`); an argument of the wrong type raises
66
+ `TypeError`, an unknown keyword (`tittle:`) `ArgumentError`.
67
+
68
+ Checked against veraPDF 1.30.2 on Chromium 154 output (`spec/fixtures/chromium/`): both
69
+ fixtures are PDF/UA-1 compliant after `apply_pdfua_fixes`. A machine check covers the machine
70
+ checkable part only - alternative texts, headings and reading order still need a person.
35
71
 
36
72
  ---
37
73
 
@@ -39,8 +75,8 @@ the structure tree._
39
75
 
40
76
  ### Requirements
41
77
 
42
- * **Ruby** \>= 3.1
43
- * **QPDF** \>= 12.0.0 (headers & libs)
78
+ * **Ruby** \>= 3.3 (tested with 3.3, 3.4 and 4.0)
79
+ * **QPDF** \>= 11.9 (headers & libs; tested with 11.9.1 and 12.2)
44
80
 
45
81
  ### macOS
46
82
  ```bash
@@ -50,12 +86,11 @@ bundle config set --local build.qpdf_ruby "--with-qpdf-dir=$(brew --prefix qpdf)
50
86
 
51
87
  ### Debian/Ubuntu
52
88
  ```bash
53
- # on Debian 11/Ubuntu 20.04 you may need newer packages from testing
89
+ # Debian 13 (trixie) and newer ship QPDF 12
54
90
  sudo apt-get update && sudo apt-get install -y libqpdf-dev qpdf
55
91
  ```
56
- If `apt` cannot provide QPDF ≥ 12 you can compile it yourself or pull the
57
- package from *testing/unstable* – see the [Dockerfile](./docker/Dockerfile) for a working
58
- `apt preferences` snippet.
92
+ If `apt` cannot provide QPDF ≥ 11.9, compile it yourself or use Debian trixie (see the
93
+ [Dockerfile](./docker/Dockerfile)).
59
94
 
60
95
  ### Add the gem
61
96
  ```bash
@@ -70,24 +105,19 @@ bundle add qpdf_ruby
70
105
  ```ruby
71
106
  require "qpdf_ruby"
72
107
 
73
- pdf = QpdfRuby::Document.new("input.pdf")
74
-
75
- # 1. tag decorative paths
76
- pdf.mark_paths_as_artifacts
108
+ pdf = QpdfRuby::Document.new("chromium-output.pdf")
77
109
 
78
- # 2. add BBox to every <Figure>
79
- pdf.ensure_bbox
110
+ report = pdf.apply_pdfua_fixes(
111
+ link_texts: { "mailto:jana@example.com" => "E-mail Jana" }, # optional, per URI or "#destination"
112
+ title: "Curriculum vitae - Jana Example" # optional, else the PDF's /Title
113
+ )
114
+ # => { decorations: 3, artifacts: { paths: 75, texts: 101, …, total: 176 }, links: 2, list_bodies: 34,
115
+ # roles: 0, figure_groups: 0, figures_without_alt: 0, identified: true, unidentified_reason: nil }
80
116
 
81
- # 3. introspect structure tree (optional)
82
- File.write("structure.xml", pdf.show_structure)
83
-
84
- # 4. save 🎉
85
- pdf.write("fixed.pdf")
117
+ pdf.ensure_bbox # layout BBoxes for figures (PAC 2024 asks for them)
118
+ pdf.write("accessible.pdf") # or pdf.to_memory
86
119
  ```
87
120
 
88
- Run PAC 2024 on `fixed.pdf` – it should report far fewer (or zero!)
89
- errors compared to the original browser output.
90
-
91
121
  ---
92
122
 
93
123
  ## Development
@@ -97,7 +127,17 @@ cd qpdf_ruby
97
127
  bin/setup # install gem + test deps
98
128
  autotest # guard & RSpec
99
129
  ```
100
- * Bump **version.rb** → `bundle exec rake release` to push a new gem.
130
+ * Bump **version.rb** → `bundle exec rake release` to push a new gem. It first runs
131
+ `rake release_credentials_check`, which shows where the push credential comes from: a set
132
+ `GEM_HOST_API_KEY` wins (as in RubyGems), else `gem.push_key` in the credentials file RubyGems uses,
133
+ else that file's key for the push host, else its `rubygems_api_key`.
134
+ * `bundle exec rspec` runs the specs; the veraPDF examples run only where `verapdf` is on the
135
+ `PATH` (`bin/install-verapdf.sh [DIR]` installs the pinned CLI; needs Java). They are skipped
136
+ otherwise - `REQUIRE_VERAPDF=1`, set in CI, makes them fail instead.
137
+ * `bin/asan-rspec.sh [RSPEC_ARGS]` builds the extension with AddressSanitizer in a scratch copy and
138
+ runs the specs under it, with LeakSanitizer (`bin/lsan.supp` filters Ruby's own heap) - the way to
139
+ see a use-after-free a plain run survives by luck. A destructor skipped by a Ruby raise is the one
140
+ leak it cannot see; `spec/qpdf_ruby/document_error_path_memory_spec.rb` watches RSS for that.
101
141
 
102
142
  ### Testing with local QPDF builds
103
143
  If you tinker with QPDF itself, point Bundler to your custom prefix:
@@ -119,7 +159,7 @@ Bug reports & pull requests are welcome at
119
159
 
120
160
  ### Code Style
121
161
  * C++ 17, clang‑format enforced
122
- * Ruby 3.2, rubocop default rules
162
+ * Ruby 3.3, rubocop default rules
123
163
 
124
164
  ---
125
165
 
@@ -1,17 +1,22 @@
1
+ #define POINTERHOLDER_TRANSITION 1
2
+
1
3
  #include "document_handle.hpp"
2
4
 
3
5
  #include <qpdf/QPDFWriter.hh>
6
+ #include <qpdf/QPDF.hh>
4
7
  #include <stdexcept>
5
8
  #include <system_error>
6
9
  #include <cerrno>
7
10
 
8
11
  using namespace qpdf_ruby;
9
12
 
10
- std::unique_ptr<DocumentHandle> DocumentHandle::open(const std::string& filename) {
13
+ std::unique_ptr<DocumentHandle> DocumentHandle::open(const std::string& filename, std::string const& pwd) {
11
14
  auto qpdf = std::make_shared<QPDF>();
12
15
  try {
13
- // Use empty password → owner & user pwd same.
14
- qpdf->processFile(filename.c_str());
16
+ qpdf->processFile(filename.c_str(), pwd.empty() ? nullptr : pwd.c_str());
17
+ } catch (const QPDFExc& qex) {
18
+ throw std::runtime_error(std::string("qpdf_ruby: failed to open \"") + filename + "\": " + qex.what() +
19
+ " (code: " + std::to_string(qex.getErrorCode()) + ")");
15
20
  } catch (const std::exception& ex) {
16
21
  throw std::runtime_error(std::string("qpdf_ruby: failed to open “") + filename + "”: " + ex.what());
17
22
  }
@@ -40,6 +45,7 @@ void DocumentHandle::write(const std::string& out_filename) {
40
45
  // honour original file’s extension-level features (linearized? encrypted? …)
41
46
  QPDFWriter w(*m_qpdf, out_filename.c_str());
42
47
  w.setStaticID(true); // deterministic IDs – helps tests
48
+ setup_encryption(w);
43
49
  w.write();
44
50
  } catch (const std::exception& ex) {
45
51
  throw std::runtime_error(std::string("qpdf_ruby: failed to write “") + out_filename + "”: " + ex.what());
@@ -50,36 +56,75 @@ std::string DocumentHandle::write_to_memory() {
50
56
  try {
51
57
  QPDFWriter w(*m_qpdf, nullptr);
52
58
  w.setStaticID(true);
59
+
60
+ setup_encryption(w);
61
+
53
62
  w.setOutputMemory();
54
63
  w.write();
55
64
 
56
- auto b = w.getBuffer();
65
+ auto b = w.getBufferSharedPointer(); // getBuffer() hands over a Buffer the caller must delete
57
66
  return std::string(reinterpret_cast<char const*>(b->getBuffer()), b->getSize());
58
67
  } catch (std::exception const& ex) {
59
68
  throw std::runtime_error("qpdf_ruby: write_to_memory failed: " + std::string(ex.what()));
60
69
  }
61
70
  }
62
71
 
72
+ void DocumentHandle::setup_encryption(QPDFWriter& w) const {
73
+ if (!m_encryption_requested) return;
74
+
75
+ switch (m_encrypt_R) {
76
+ case 4:
77
+ w.setR4EncryptionParametersInsecure(m_user_pw.c_str(), m_owner_pw.c_str(), m_encrypt_accessibility,
78
+ m_encrypt_allow_extract, m_encrypt_assemble, m_encrypt_annotate_and_form,
79
+ m_encrypt_form_filling, m_encrypt_allow_modify, m_encrypt_allow_print,
80
+ m_encrypt_encrypt_metadata, m_encrypt_use_aes);
81
+ break;
82
+ case 5:
83
+ w.setR5EncryptionParameters(m_user_pw.c_str(), m_owner_pw.c_str(), m_encrypt_accessibility,
84
+ m_encrypt_allow_extract, m_encrypt_assemble, m_encrypt_annotate_and_form,
85
+ m_encrypt_form_filling, m_encrypt_allow_modify, m_encrypt_allow_print,
86
+ m_encrypt_encrypt_metadata);
87
+ break;
88
+ case 6:
89
+ w.setR6EncryptionParameters(m_user_pw.c_str(), m_owner_pw.c_str(), m_encrypt_accessibility,
90
+ m_encrypt_allow_extract, m_encrypt_assemble, m_encrypt_annotate_and_form,
91
+ m_encrypt_form_filling, m_encrypt_allow_modify, m_encrypt_allow_print,
92
+ m_encrypt_encrypt_metadata);
93
+ break;
94
+ default:
95
+ throw std::runtime_error("Unsupported encryption revision (R): Only 4, 5, 6 are supported.");
96
+ }
97
+ }
98
+
99
+ void DocumentHandle::set_encryption(const std::string& user_pw, const std::string& owner_pw, int R,
100
+ qpdf_r3_print_e allow_print, bool allow_modify, bool allow_extract,
101
+ bool accessibility, bool assemble, bool annotate_and_form, bool form_filling,
102
+ bool encrypt_metadata, bool use_aes) {
103
+ m_user_pw = user_pw;
104
+ m_owner_pw = owner_pw;
105
+ m_encrypt_R = R;
106
+ m_encrypt_allow_print = allow_print;
107
+ m_encrypt_allow_modify = allow_modify;
108
+ m_encrypt_allow_extract = allow_extract;
109
+ m_encrypt_accessibility = accessibility;
110
+ m_encrypt_assemble = assemble;
111
+ m_encrypt_annotate_and_form = annotate_and_form;
112
+ m_encrypt_form_filling = form_filling;
113
+ m_encrypt_encrypt_metadata = encrypt_metadata;
114
+ m_encrypt_use_aes = use_aes;
115
+ m_encryption_requested = true;
116
+ }
117
+
63
118
  // ------------------------- C bridge impl --------------------------------
64
119
 
65
120
  extern "C" {
66
- DocumentHandle* qpdf_ruby_open(const char* filename) {
67
- try {
68
- return DocumentHandle::open(filename).release();
69
- } catch (const std::exception&) {
70
- errno = EIO;
71
- return nullptr;
72
- }
121
+ DocumentHandle* qpdf_ruby_open(const char* filename, char const* pwd) {
122
+ return DocumentHandle::open(filename, pwd ? pwd : "").release();
73
123
  }
74
124
 
75
125
  DocumentHandle* qpdf_ruby_open_memory(char const* desc, unsigned char const* buf, size_t len, char const* pwd) {
76
- try {
77
- std::vector<unsigned char> copy(buf, buf + len); // simple ownership
78
- return DocumentHandle::open_memory(desc, std::move(copy), pwd ? pwd : "").release();
79
- } catch (...) {
80
- errno = EIO;
81
- return nullptr;
82
- }
126
+ std::vector<unsigned char> copy(buf, buf + len); // simple ownership
127
+ return DocumentHandle::open_memory(desc, std::move(copy), pwd ? pwd : "").release();
83
128
  }
84
129
 
85
130
  int qpdf_ruby_write(DocumentHandle* handle, const char* out_filename) {
@@ -87,13 +132,20 @@ int qpdf_ruby_write(DocumentHandle* handle, const char* out_filename) {
87
132
  errno = EBADF;
88
133
  return -1;
89
134
  }
90
- try {
91
- handle->write(out_filename);
92
- return 0;
93
- } catch (const std::exception&) {
94
- errno = EIO;
95
- return -1;
96
- }
135
+
136
+ handle->write(out_filename);
137
+ return 0;
138
+ }
139
+
140
+ void qpdf_ruby_set_encryption(DocumentHandle* handle, const char* user_pw, const char* owner_pw, int R,
141
+ qpdf_r3_print_e allow_print, int allow_modify, int allow_extract, int accessibility,
142
+ int assemble, int annotate_and_form, int form_filling, int encrypt_metadata,
143
+ int use_aes) {
144
+ if (!handle) return;
145
+
146
+ handle->set_encryption(std::string(user_pw ? user_pw : ""), std::string(owner_pw ? owner_pw : ""), R, allow_print,
147
+ allow_modify != 0, allow_extract != 0, accessibility != 0, assemble != 0,
148
+ annotate_and_form != 0, form_filling != 0, encrypt_metadata != 0, use_aes != 0);
97
149
  }
98
150
 
99
151
  void qpdf_ruby_close(DocumentHandle* handle) {
@@ -1,8 +1,11 @@
1
1
  #pragma once
2
2
 
3
+ #define POINTERHOLDER_TRANSITION 1
4
+
3
5
  #include <memory>
4
6
  #include <string>
5
7
  #include <qpdf/QPDF.hh>
8
+ #include <qpdf/QPDFWriter.hh>
6
9
 
7
10
  namespace qpdf_ruby {
8
11
 
@@ -16,7 +19,7 @@ namespace qpdf_ruby {
16
19
  class DocumentHandle final {
17
20
  public:
18
21
  // ---- factory ----------------------------------------------------------
19
- static std::unique_ptr<DocumentHandle> open(const std::string& filename);
22
+ static std::unique_ptr<DocumentHandle> open(const std::string& filename, std::string const& pwd);
20
23
  static std::unique_ptr<DocumentHandle> open_memory(std::string const& description, std::vector<unsigned char> data,
21
24
  std::string const& password = "");
22
25
 
@@ -26,6 +29,12 @@ class DocumentHandle final {
26
29
 
27
30
  std::string write_to_memory();
28
31
 
32
+ void set_encryption(const std::string& user_pw, const std::string& owner_pw, int R, qpdf_r3_print_e allow_print,
33
+ bool allow_modify, bool allow_extract, bool accessibility = true, bool assemble = true,
34
+ bool annotate_and_form = true, bool form_filling = true, bool encrypt_metadata = true,
35
+ bool use_aes = true);
36
+ void setup_encryption(QPDFWriter& w) const;
37
+
29
38
  /** Direct access for C++ helpers that need the raw QPDF. */
30
39
  QPDF& qpdf() { return *m_qpdf; }
31
40
  const QPDF& qpdf() const { return *m_qpdf; }
@@ -42,11 +51,26 @@ class DocumentHandle final {
42
51
 
43
52
  std::shared_ptr<QPDF> m_qpdf;
44
53
  std::vector<unsigned char> m_owned_buf;
54
+
55
+ // --- New encryption settings ---
56
+ bool m_encryption_requested = false;
57
+ std::string m_user_pw;
58
+ std::string m_owner_pw;
59
+ int m_encrypt_R = 4;
60
+ qpdf_r3_print_e m_encrypt_allow_print{qpdf_r3p_full};
61
+ bool m_encrypt_allow_modify = true;
62
+ bool m_encrypt_allow_extract = true;
63
+ bool m_encrypt_accessibility = true;
64
+ bool m_encrypt_assemble = true;
65
+ bool m_encrypt_annotate_and_form = true;
66
+ bool m_encrypt_form_filling = true;
67
+ bool m_encrypt_encrypt_metadata = true;
68
+ bool m_encrypt_use_aes = true;
45
69
  };
46
70
 
47
71
  extern "C" {
48
72
  /** Returns a freshly allocated handle or nullptr on error (see errno). */
49
- DocumentHandle* qpdf_ruby_open(const char* filename);
73
+ DocumentHandle* qpdf_ruby_open(const char* filename, char const* pwd);
50
74
 
51
75
  DocumentHandle* qpdf_ruby_open_memory(char const* desc, unsigned char const* buf, size_t len, char const* pwd);
52
76
 
@@ -57,4 +81,10 @@ int qpdf_ruby_write(DocumentHandle* handle, const char* out_filename);
57
81
  void qpdf_ruby_close(DocumentHandle* handle);
58
82
  }
59
83
 
84
+ void qpdf_ruby_set_encryption(struct DocumentHandle* handle, const char* user_pw, const char* owner_pw, int R,
85
+ qpdf_r3_print_e allow_print,
86
+ int allow_modify, // bool as int (0/1)
87
+ int allow_extract, int accessibility, int assemble, int annotate_and_form,
88
+ int form_filling, int encrypt_metadata, int use_aes);
89
+
60
90
  } // namespace qpdf_ruby