pdfrb 0.7.43 → 0.7.45
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 +68 -0
- data/CLAUDE.md +18 -2
- data/README.adoc +17 -3
- data/docs/USAGE.md +11 -2
- data/lib/pdfrb/content/canvas.rb +13 -3
- data/lib/pdfrb/document.rb +21 -0
- data/lib/pdfrb/task/extract_text.rb +12 -0
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb.rb +14 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9283c2555bc64be8606a06fb6fc62c9498382499c30c7eea8afd36c4dcfc6a82
|
|
4
|
+
data.tar.gz: e93caaff66db1dcf3944873ad5b41fe9a8876623d33ae0c53918461a6f0a4c49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16b7e2394f0275d631d4ba0946e3b0d5f2973240f9fe1d4f5eebfbb4998e666e6e0ff9e62dd50fd43eebb5ecb7b418846d6ded5be052b1c3462f0d3033cbd1a3
|
|
7
|
+
data.tar.gz: 4219346da2b075bb87024963f248c1a8e26852f9e928c24f0155291e242bd8d4ff28175f74ef1719c05812fc34aa0782d4f81d2a226c108e996d5940d48ab6fe
|
data/CHANGELOG.md
CHANGED
|
@@ -95,6 +95,74 @@ All notable changes to the pdfrb gem will be documented in this file.
|
|
|
95
95
|
* Removed all `instance_variable_set/get`, `send` to private
|
|
96
96
|
methods, `respond_to?` for type checks.
|
|
97
97
|
|
|
98
|
+
## [0.7.44] — 2026-08-26
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
* Top-level `Pdfrb.open(path)` / `Pdfrb.parse(bytes_or_io)` module
|
|
103
|
+
functions.
|
|
104
|
+
* `Document#encrypt!` / `#decrypt!` delegates;
|
|
105
|
+
`ExtractText.call_single_page(page)`.
|
|
106
|
+
* `Canvas#rectangle(point:, width:, height:)` and
|
|
107
|
+
`Canvas#line(from:, to:)` keyword forms alongside positional.
|
|
108
|
+
* Executable cookbook spec (`usage_doc_spec.rb`): every docs/USAGE.md
|
|
109
|
+
snippet runs against temp files.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
* **Oid allocation on parsed documents** started at 1 regardless of
|
|
114
|
+
the xref, so mutating a reopened PDF overwrote existing objects —
|
|
115
|
+
`Task::Merge` dropped the target's original pages (1+2 -> 2).
|
|
116
|
+
Allocation now seeds past max(xref oid, trailer /Size).
|
|
117
|
+
|
|
118
|
+
## [0.7.43] — 2026-08-26
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
|
|
122
|
+
* `Content::InlineImage` value object for BI/ID/EI sequences:
|
|
123
|
+
geometry accessors, s8.9.7 abbreviation expansion (color spaces
|
|
124
|
+
and filter names), components/expected_decoded_size, and
|
|
125
|
+
`decoded_data` applying the filter pipeline on demand.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
* operators/inline_image.rb called `register` inside `class << self`
|
|
130
|
+
(receiver = the singleton class) — never legitimately executable;
|
|
131
|
+
moved to class-body level.
|
|
132
|
+
* operators.rb autoloaded `:InlineImage` from a file defining
|
|
133
|
+
`Operator::BeginInlineImage` (namespace mismatch); correct
|
|
134
|
+
BeginInlineImage/EndInlineImage autoloads now live in operator.rb.
|
|
135
|
+
|
|
136
|
+
## [0.7.42] — 2026-08-25
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
* Packing-size spec no longer skips: rebuilt with 60 small
|
|
141
|
+
annotations so object-stream packing wins decisively (~6.5KB ->
|
|
142
|
+
~1.4KB). Suite is now 0 failures, 0 pending.
|
|
143
|
+
|
|
144
|
+
## [0.7.41] — 2026-08-25
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
* Arlington conformance fixture walk is real: all 12 Annex H
|
|
149
|
+
fixtures validate through the typed field validator with zero
|
|
150
|
+
violations (previously a pending stub).
|
|
151
|
+
|
|
152
|
+
### Fixed
|
|
153
|
+
|
|
154
|
+
* `PageTreeNodeRoot` shadows the inherited required `/Parent` (the
|
|
155
|
+
root TSV omits the key; the root must not carry a parent).
|
|
156
|
+
* `Dictionary.each_field` honors field shadowing: a subclass field
|
|
157
|
+
with the same name fully replaces the inherited one.
|
|
158
|
+
|
|
159
|
+
## [0.7.40] — 2026-08-25
|
|
160
|
+
|
|
161
|
+
### Added
|
|
162
|
+
|
|
163
|
+
* CHANGELOG backfill for 0.7.1 through 0.7.39 (29 releases
|
|
164
|
+
reconstructed from tag history).
|
|
165
|
+
|
|
98
166
|
## [0.7.39] — 2026-08-24
|
|
99
167
|
|
|
100
168
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -105,6 +105,22 @@ These repos live outside the project tree but are the authoritative references.
|
|
|
105
105
|
| `~/src/pdfa/technote-pdfua1-001/` | PDF/UA-1 Technical Note 001 — `ActualText` on `Figure` structure elements. |
|
|
106
106
|
| `~/src/pdfa/technote-pdfua1-002/` | PDF/UA-1 Technical Note 002 — `Reference` structure elements containing links. |
|
|
107
107
|
|
|
108
|
-
##
|
|
108
|
+
## Status
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
Released on RubyGems (0.7.x, BSD-2-Clause). The full stack is
|
|
111
|
+
implemented: reader/writer round-trip, content streams + Canvas,
|
|
112
|
+
encryption (RC4/AES-256), signatures, layout/Composer, conformance
|
|
113
|
+
validation (PDF/A, PDF/UA, PDF/X, PDF/VT), and the CLI.
|
|
114
|
+
|
|
115
|
+
**Arlington TSV coverage is complete** — 612 of 614 non-alias TSVs
|
|
116
|
+
map to `Model::Type::*` classes (only the two single-row OPI alias
|
|
117
|
+
wrappers remain, deliberately). Every commit goes through
|
|
118
|
+
PR + CI; the CHANGELOG records each release.
|
|
119
|
+
|
|
120
|
+
Known depth gaps: CFF/OTF font subsetting (only TrueType subsets),
|
|
121
|
+
veraPDF cross-check for PDF/A output. The original plan files live
|
|
122
|
+
in `TODO.general-rels/` (~157 entries, mostly landed).
|
|
123
|
+
|
|
124
|
+
`docs/USAGE.md` is the verified cookbook — `spec/pdfrb/usage_doc_spec.rb`
|
|
125
|
+
executes every snippet, so keep both in sync when changing public
|
|
126
|
+
APIs.
|
data/README.adoc
CHANGED
|
@@ -31,7 +31,7 @@ https://github.com/gettalong/hexapdf[HexaPDF].
|
|
|
31
31
|
gem install pdfrb
|
|
32
32
|
----
|
|
33
33
|
|
|
34
|
-
== Usage
|
|
34
|
+
== Usage
|
|
35
35
|
|
|
36
36
|
[source,ruby]
|
|
37
37
|
----
|
|
@@ -39,15 +39,29 @@ require "pdfrb"
|
|
|
39
39
|
|
|
40
40
|
# Read
|
|
41
41
|
doc = Pdfrb.open("input.pdf")
|
|
42
|
-
puts doc.
|
|
42
|
+
puts doc.pages.count
|
|
43
|
+
puts Pdfrb::Task::ExtractText.call_single_page(doc.pages.first)
|
|
43
44
|
|
|
44
45
|
# Create
|
|
45
46
|
doc = Pdfrb::Document.new
|
|
47
|
+
font = doc.fonts.add("Helvetica")
|
|
46
48
|
page = doc.pages.add
|
|
47
|
-
page.canvas.text("Hello, PDF", at: [72, 720])
|
|
49
|
+
page.canvas.text("Hello, PDF", at: [72, 720], font: font, size: 24)
|
|
50
|
+
page.canvas.rectangle(point: [72, 700], width: 200, height: 50)
|
|
51
|
+
page.canvas.stroke
|
|
48
52
|
doc.write("hello.pdf")
|
|
53
|
+
|
|
54
|
+
# Merge
|
|
55
|
+
target = Pdfrb.open("base.pdf")
|
|
56
|
+
Pdfrb::Task::Merge.call(target, Pdfrb.open("appendix.pdf"))
|
|
57
|
+
target.write("merged.pdf")
|
|
49
58
|
----
|
|
50
59
|
|
|
60
|
+
A full cookbook (tagged PDF, layers, forms, signatures, diff,
|
|
61
|
+
conformance validation, linearization, encryption, CLI) lives in
|
|
62
|
+
link:docs/USAGE.md[docs/USAGE.md] — every snippet there is executed
|
|
63
|
+
by the test suite.
|
|
64
|
+
|
|
51
65
|
== Architecture
|
|
52
66
|
|
|
53
67
|
[cols="1,4", options="header"]
|
data/docs/USAGE.md
CHANGED
|
@@ -137,9 +137,18 @@ File.binwrite("linearized.pdf", io.string)
|
|
|
137
137
|
## Encryption
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
|
-
doc = Pdfrb.
|
|
140
|
+
doc = Pdfrb::Document.new
|
|
141
|
+
doc.pages.add
|
|
142
|
+
doc.encrypt!(user_password: "secret", owner_password: "owner", bits: 128)
|
|
141
143
|
doc.write("encrypted.pdf")
|
|
142
|
-
|
|
144
|
+
|
|
145
|
+
# Opening an encrypted document:
|
|
146
|
+
doc = Pdfrb.open("encrypted.pdf",
|
|
147
|
+
config: { "encryption.password" => "secret" })
|
|
148
|
+
|
|
149
|
+
# Removing encryption from a parsed document:
|
|
150
|
+
doc.decrypt!
|
|
151
|
+
doc.write("decrypted.pdf")
|
|
143
152
|
```
|
|
144
153
|
|
|
145
154
|
## Merging PDFs
|
data/lib/pdfrb/content/canvas.rb
CHANGED
|
@@ -64,7 +64,11 @@ module Pdfrb
|
|
|
64
64
|
self
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def line(x1, y1, x2, y2)
|
|
67
|
+
def line(x1 = nil, y1 = nil, x2 = nil, y2 = nil, from: nil, to: nil)
|
|
68
|
+
if from && to
|
|
69
|
+
x1, y1 = from
|
|
70
|
+
x2, y2 = to
|
|
71
|
+
end
|
|
68
72
|
move_to(x1, y1).line_to(x2, y2)
|
|
69
73
|
end
|
|
70
74
|
|
|
@@ -189,8 +193,14 @@ module Pdfrb
|
|
|
189
193
|
self
|
|
190
194
|
end
|
|
191
195
|
|
|
192
|
-
def rectangle(x, y,
|
|
193
|
-
|
|
196
|
+
def rectangle(x = nil, y = nil, w = nil, h = nil,
|
|
197
|
+
point: nil, width: nil, height: nil)
|
|
198
|
+
x, y = point if point
|
|
199
|
+
w = width if w.nil?
|
|
200
|
+
h = height if h.nil?
|
|
201
|
+
raise ArgumentError, "rectangle needs x, y, width and height" if [x, y, w, h].any?(&:nil?)
|
|
202
|
+
|
|
203
|
+
emit_op(Pdfrb::Content::Operator::Rectangle, x, y, w, h)
|
|
194
204
|
self
|
|
195
205
|
end
|
|
196
206
|
|
data/lib/pdfrb/document.rb
CHANGED
|
@@ -166,6 +166,14 @@ module Pdfrb
|
|
|
166
166
|
|
|
167
167
|
def encryption; @encryption ||= Document::Encryption.new(self); end
|
|
168
168
|
|
|
169
|
+
def encrypt!(**)
|
|
170
|
+
encryption.encrypt!(**)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def decrypt!
|
|
174
|
+
encryption.decrypt!
|
|
175
|
+
end
|
|
176
|
+
|
|
169
177
|
def info; @info ||= Document::Info.new(self); end
|
|
170
178
|
|
|
171
179
|
def display; @display ||= Document::Display.new(self); end
|
|
@@ -306,6 +314,19 @@ module Pdfrb
|
|
|
306
314
|
|
|
307
315
|
@xref, @trailer_dict = load_xref_and_trailer(io, sxref)
|
|
308
316
|
@object_reader = Pdfrb::Source::ObjectReader.new(self, @xref) if @xref
|
|
317
|
+
seed_next_oid
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# New/modified objects must not collide with objects loaded from
|
|
321
|
+
# the xref: continue allocating past the highest known oid (or
|
|
322
|
+
# the trailer /Size, whichever is larger).
|
|
323
|
+
def seed_next_oid
|
|
324
|
+
return if @xref.nil?
|
|
325
|
+
|
|
326
|
+
max_oid = @xref.entries.keys.max || 0
|
|
327
|
+
size = @trailer_dict && @trailer_dict[:Size]
|
|
328
|
+
size = size.to_i if size.respond_to?(:to_i)
|
|
329
|
+
@next_oid = [max_oid + 1, size.to_i, @next_oid].max
|
|
309
330
|
end
|
|
310
331
|
|
|
311
332
|
def load_xref_and_trailer(io, sxref)
|
|
@@ -31,6 +31,18 @@ module Pdfrb
|
|
|
31
31
|
block_given? ? document : results
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Extract the text of a single page (see #call for the
|
|
35
|
+
# resolution strategy).
|
|
36
|
+
def call_single_page(page)
|
|
37
|
+
extractor = TextCollector.new(page.document, page)
|
|
38
|
+
begin
|
|
39
|
+
extractor.process(page.decoded_content)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
# Malformed content stream — best effort, skip.
|
|
42
|
+
end
|
|
43
|
+
extractor.text
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
# Internal: collects bytes from +Tj+ / +TJ+ / +'+ / +"'.
|
|
35
47
|
# Tracks text-matrix y to insert line breaks at vertical moves.
|
|
36
48
|
# Resolves glyph codes to Unicode via the active font's
|
data/lib/pdfrb/version.rb
CHANGED
data/lib/pdfrb.rb
CHANGED
|
@@ -58,6 +58,20 @@ module Pdfrb
|
|
|
58
58
|
autoload :CLI, "pdfrb/cli"
|
|
59
59
|
|
|
60
60
|
require "logger"
|
|
61
|
+
require "stringio"
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
# Open a PDF file (path or block over the file handle).
|
|
65
|
+
def open(path, **, &)
|
|
66
|
+
Document.open(path, **, &)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Parse PDF bytes/IO into a Document.
|
|
70
|
+
def parse(io, **)
|
|
71
|
+
io = StringIO.new(io.b) if io.is_a?(String)
|
|
72
|
+
Document.new(io: io, **)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
61
75
|
|
|
62
76
|
def self.logger
|
|
63
77
|
@logger ||= Logger.new($stderr, level: Logger::WARN)
|