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 +4 -4
- data/CHANGELOG.md +69 -1
- data/README.md +94 -54
- data/ext/qpdf_ruby/document_handle.cpp +77 -25
- data/ext/qpdf_ruby/document_handle.hpp +32 -2
- 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 +7 -3
- data/ext/qpdf_ruby/pdfua.cpp +909 -0
- data/ext/qpdf_ruby/pdfua.hpp +102 -0
- data/ext/qpdf_ruby/qpdf_ruby.cpp +384 -138
- data/ext/qpdf_ruby/qpdf_ruby.hpp +1 -3
- 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/struct_node.hpp +2 -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 +7 -2
- data/sig/qpdf_ruby.rbs +78 -1
- metadata +11 -11
- 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/docker/Dockerfile +0 -46
- data/ext/qpdf_ruby/pdf_image_mapper.cpp +0 -294
- data/ext/qpdf_ruby/pdf_image_mapper.hpp +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c89e78922fbb1a3922b13c425086212a70454a28b6f17f9f75b8fe9ab772256
|
|
4
|
+
data.tar.gz: 3144ff7e6bae54b2b71fe80bd1b22652b5466120d03cf2fa7f7dcccab15c9d7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e6736a64f275a612a85727c94ebe7e5ccde471b5024f974611b6a43ce747ee317f0544769996816f943aff50071e9fcc5eb5e92b08316af921b29cc6dea9c1d
|
|
7
|
+
data.tar.gz: 8941eff1af71bed2d082e4eb66256d9b284800aa9cfd95100fadc3eeb4888663f647d5f939816b82fbdd6e735b479aff0724ee6fc72ba8931456c6a64c67d822
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
|
-
|
|
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
|
-
> **
|
|
4
|
-
|
|
5
|
-
QpdfRuby is a
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
43
|
-
* **QPDF** \>=
|
|
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
|
-
#
|
|
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 ≥
|
|
57
|
-
|
|
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("
|
|
74
|
-
|
|
75
|
-
# 1. tag decorative paths
|
|
76
|
-
pdf.mark_paths_as_artifacts
|
|
108
|
+
pdf = QpdfRuby::Document.new("chromium-output.pdf")
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
#
|
|
82
|
-
|
|
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.
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|