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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0b004db58a66ef4268db7138eb13c2813fcceb4e22a492674597f631d6f889e
4
- data.tar.gz: b29ee27e11e4aea41a482e3eb217c7ddf70a5cdf4b21f70b2d16be01747822df
3
+ metadata.gz: 9283c2555bc64be8606a06fb6fc62c9498382499c30c7eea8afd36c4dcfc6a82
4
+ data.tar.gz: e93caaff66db1dcf3944873ad5b41fe9a8876623d33ae0c53918461a6f0a4c49
5
5
  SHA512:
6
- metadata.gz: 25dc0c66f65bf454f2ac91c362031c57720d060edaeb54912aba896115216fbe43ce4fd947e21e94a445e521168e1eb629d51db42dc38c9ca5639ca41e8786e5
7
- data.tar.gz: 3368f119c878bb7bcce11f21a4bd6edb267e4605ae079ca85dafbce6defa1397e9b5f0649bbdfc3a7084d161eb0f6c20da934b5b0f288344538f296703a031c7
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
- ## Work in progress
108
+ ## Status
109
109
 
110
- The gem is at 0.1.0 — foundation only (autoload tree, error hierarchy, constants, configuration). All remaining implementation work is tracked as individual TODO files in `TODO.general-rels/`. The master index is `TODO.general-rels/README.md`. Pick up work in dependency order; each TODO file's front-matter declares `depends_on`.
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 (sketch — APIs land across the TODO sequence)
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.catalog.pages.count
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.open("input.pdf")
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
- # Encryption configuration via document.config
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
@@ -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, width, height)
193
- emit_op(Pdfrb::Content::Operator::Rectangle, x, y, width, height)
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.7.43"
4
+ VERSION = "0.7.45"
5
5
  end
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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pdfrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.43
4
+ version: 0.7.45
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.