pdfrb 0.7.42 → 0.7.44
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/docs/USAGE.md +11 -2
- data/lib/pdfrb/content/canvas.rb +13 -3
- data/lib/pdfrb/content/inline_image.rb +103 -0
- data/lib/pdfrb/content/operator.rb +6 -0
- data/lib/pdfrb/content/operators/inline_image.rb +2 -2
- data/lib/pdfrb/content/operators.rb +0 -1
- data/lib/pdfrb/content/parser.rb +1 -1
- data/lib/pdfrb/content/processor.rb +3 -3
- data/lib/pdfrb/content.rb +1 -0
- 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 +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 836c9a2bfe781c6816a25f2db87e99c3b30265fcfb80f116f1b54ccfe9ce149d
|
|
4
|
+
data.tar.gz: 2a7b2a9f1d607b1dd29fd6aca4a2b9a977b315e98834026974ae73d384a62718
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc2be0c8bcf1f7a6a77f3db649dba782cc789e532a17afb2fd6152b5fa8109e2c0d6410a01f8df0c820e53fa3439035ff3db24fde8e889a32b6bc880a93cda25
|
|
7
|
+
data.tar.gz: ad3a5ae6eb6dc87577477bba0a7b4ccb6e75e5c914dee698ee4bc661f2df96b363172ef6aded34815b5965ec3f01edf0e6c710b70ba4a4ab95510acc6a0a2b2d
|
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
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
module Content
|
|
5
|
+
# A BI ... ID ... EI inline image (s8.9.7). Carries the expanded
|
|
6
|
+
# header keys (abbreviations resolved by the parser), the raw
|
|
7
|
+
# encoded byte payload, and decodes on demand via the filter
|
|
8
|
+
# pipeline.
|
|
9
|
+
class InlineImage
|
|
10
|
+
# Filter-name abbreviations permitted in inline images
|
|
11
|
+
# (s8.9.7 Table 89).
|
|
12
|
+
FILTER_ABBREVIATIONS = {
|
|
13
|
+
AHx: :ASCIIHexDecode,
|
|
14
|
+
A85: :ASCII85Decode,
|
|
15
|
+
Fl: :FlateDecode,
|
|
16
|
+
LZW: :LZWDecode,
|
|
17
|
+
CCF: :CCITTFaxDecode,
|
|
18
|
+
DCT: :DCTDecode,
|
|
19
|
+
RL: :RunLengthDecode,
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Color-space-name abbreviations (s8.9.7 Table 89).
|
|
23
|
+
COLOR_SPACE_ABBREVIATIONS = {
|
|
24
|
+
G: :DeviceGray,
|
|
25
|
+
RGB: :DeviceRGB,
|
|
26
|
+
CMYK: :DeviceCMYK,
|
|
27
|
+
I: :Indexed,
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :header, :data
|
|
31
|
+
|
|
32
|
+
def initialize(header: {}, data: "")
|
|
33
|
+
@header = header.dup.freeze
|
|
34
|
+
@data = (data || "".b).dup.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def width; header[:Width]; end
|
|
38
|
+
def height; header[:Height]; end
|
|
39
|
+
def bits_per_component; header[:BitsPerComponent]; end
|
|
40
|
+
|
|
41
|
+
def image_mask?
|
|
42
|
+
[true, 1].include?(header[:ImageMask])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def decode; header[:Decode]; end
|
|
46
|
+
def interpolate?; header[:Interpolate] == true; end
|
|
47
|
+
|
|
48
|
+
# /ColorSpace with the inline abbreviation expanded
|
|
49
|
+
# (/G -> /DeviceGray etc.).
|
|
50
|
+
def color_space
|
|
51
|
+
cs = header[:ColorSpace]
|
|
52
|
+
return nil unless cs.is_a?(::Symbol)
|
|
53
|
+
|
|
54
|
+
COLOR_SPACE_ABBREVIATIONS.fetch(cs, cs)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Decoded filter chain as an array of Symbols; nil when the
|
|
58
|
+
# image data is stored raw.
|
|
59
|
+
def filters
|
|
60
|
+
f = header[:Filter]
|
|
61
|
+
return [] if f.nil?
|
|
62
|
+
|
|
63
|
+
arr = f.is_a?(::Array) ? f : [f]
|
|
64
|
+
arr.map { |name| FILTER_ABBREVIATIONS.fetch(name.to_sym, name.to_sym) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Apply the filter chain to the raw payload. Returns the raw
|
|
68
|
+
# data when no filter is declared.
|
|
69
|
+
def decoded_data
|
|
70
|
+
list = filters
|
|
71
|
+
return @data if list.empty?
|
|
72
|
+
|
|
73
|
+
Pdfrb::Filter.apply(
|
|
74
|
+
@data, filters: list, parms: Array(header[:DecodeParms]),
|
|
75
|
+
direction: :decode
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Number of colour components implied by /ColorSpace.
|
|
80
|
+
def components
|
|
81
|
+
case color_space
|
|
82
|
+
when :DeviceGray, :Indexed then 1
|
|
83
|
+
when :DeviceRGB then 3
|
|
84
|
+
when :DeviceCMYK then 4
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Expected decoded byte count: rows * width * components *
|
|
89
|
+
# bits, packed per row.
|
|
90
|
+
def expected_decoded_size
|
|
91
|
+
return nil if width.nil? || height.nil? || bits_per_component.nil?
|
|
92
|
+
return nil if image_mask?
|
|
93
|
+
|
|
94
|
+
comps = components
|
|
95
|
+
return nil if comps.nil?
|
|
96
|
+
|
|
97
|
+
bits = width * comps * bits_per_component
|
|
98
|
+
bytes_per_row = (bits + 7) / 8
|
|
99
|
+
bytes_per_row * height
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -12,6 +12,12 @@ module Pdfrb
|
|
|
12
12
|
module Operator
|
|
13
13
|
@registry = {}
|
|
14
14
|
|
|
15
|
+
# Referenced directly by the Parser for BI ... ID ... EI
|
|
16
|
+
# sequences; the remaining operator classes load via
|
|
17
|
+
# eager_load! / Operator[].
|
|
18
|
+
autoload :BeginInlineImage, "pdfrb/content/operators/inline_image"
|
|
19
|
+
autoload :EndInlineImage, "pdfrb/content/operators/inline_image"
|
|
20
|
+
|
|
15
21
|
class << self
|
|
16
22
|
attr_reader :registry
|
|
17
23
|
|
|
@@ -29,8 +29,8 @@ module Pdfrb
|
|
|
29
29
|
def invoke(processor, image = nil)
|
|
30
30
|
processor.inline_image(image) if image
|
|
31
31
|
end
|
|
32
|
-
register
|
|
33
32
|
end
|
|
33
|
+
register
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
class EndInlineImage < Base
|
|
@@ -38,8 +38,8 @@ module Pdfrb
|
|
|
38
38
|
def name; "EI"; end
|
|
39
39
|
|
|
40
40
|
def invoke(_processor, *_operands); end
|
|
41
|
-
register
|
|
42
41
|
end
|
|
42
|
+
register
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -16,7 +16,6 @@ module Pdfrb
|
|
|
16
16
|
autoload :GraphicsStateParams, "pdfrb/content/operators/graphics_state_params"
|
|
17
17
|
autoload :MarkedContent, "pdfrb/content/operators/marked_content"
|
|
18
18
|
autoload :Clipping, "pdfrb/content/operators/clipping"
|
|
19
|
-
autoload :InlineImage, "pdfrb/content/operators/inline_image"
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
end
|
data/lib/pdfrb/content/parser.rb
CHANGED
|
@@ -82,7 +82,7 @@ module Pdfrb
|
|
|
82
82
|
# keyword from the data. Read raw bytes until "\nEI" or
|
|
83
83
|
# " EI" terminator.
|
|
84
84
|
data = read_inline_image_data
|
|
85
|
-
|
|
85
|
+
Pdfrb::Content::InlineImage.new(header: header, data: data)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
# Map inline-image abbreviation keys to full PDF names per
|
|
@@ -78,9 +78,9 @@ module Pdfrb
|
|
|
78
78
|
def marked_content_point(_tag, _properties = nil); end
|
|
79
79
|
|
|
80
80
|
# Called when a BI/ID/EI inline image is encountered in the
|
|
81
|
-
# content stream. +image+ is a
|
|
82
|
-
#
|
|
83
|
-
#
|
|
81
|
+
# content stream. +image+ is a Content::InlineImage with the
|
|
82
|
+
# expanded header keys, the raw payload, and decoded_data /
|
|
83
|
+
# geometry accessors. Subclasses override to render or extract.
|
|
84
84
|
def inline_image(_image); end
|
|
85
85
|
end
|
|
86
86
|
end
|
data/lib/pdfrb/content.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Pdfrb
|
|
|
5
5
|
autoload :GraphicsState, "pdfrb/content/graphics_state"
|
|
6
6
|
autoload :Operator, "pdfrb/content/operator"
|
|
7
7
|
autoload :Parser, "pdfrb/content/parser"
|
|
8
|
+
autoload :InlineImage, "pdfrb/content/inline_image"
|
|
8
9
|
autoload :Processor, "pdfrb/content/processor"
|
|
9
10
|
autoload :ColorSpace, "pdfrb/content/color_space"
|
|
10
11
|
autoload :TransformationMatrix, "pdfrb/content/transformation_matrix"
|
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)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.44
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -782,6 +782,7 @@ files:
|
|
|
782
782
|
- lib/pdfrb/content/graphic_object/rectangle.rb
|
|
783
783
|
- lib/pdfrb/content/graphics_state.rb
|
|
784
784
|
- lib/pdfrb/content/hidden_text_detector.rb
|
|
785
|
+
- lib/pdfrb/content/inline_image.rb
|
|
785
786
|
- lib/pdfrb/content/operator.rb
|
|
786
787
|
- lib/pdfrb/content/operators.rb
|
|
787
788
|
- lib/pdfrb/content/operators/clipping.rb
|