pdfrb 0.2.1 → 0.3.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 +24 -0
- data/lib/pdfrb/cli.rb +22 -1
- data/lib/pdfrb/document/form_xobject.rb +68 -0
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04b26536220e55c3be57627ba747561baeee1d58f37e57a2f68f5f121ec1381c
|
|
4
|
+
data.tar.gz: 7a3f7a5a10b43e253e34bb5551042db89e3d94694a568beee186c2995fefe867
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70ec6abd4b94413e5cfcf1c18b4437332fc84c38262bc58347c3b55b354416cbf4f1ecfe24995dfeea12776306e8c07b1acdddd0d96b64e572bc82d60ed629df
|
|
7
|
+
data.tar.gz: 35daa8aa6439430f7d6dbc043777bc969f0844a14dd503650076fe5ffc9edd03859f22f64a3b3d290cc84eb10774589dca52c7138fc4b102bfab660692fb4461
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the pdfrb gem will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] — 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Added — Semantic PDF diff
|
|
8
|
+
|
|
9
|
+
* **Pdfrb::Compare** — semantic PDF comparison engine. Compares two
|
|
10
|
+
PDFs at the structural level: page count, per-page text similarity
|
|
11
|
+
(Levenshtein), font inventory diffs, image count delta, outline
|
|
12
|
+
(bookmark) diffs, metadata (/Info) diffs, and structural (Catalog +
|
|
13
|
+
Trailer key) diffs.
|
|
14
|
+
* **Pdfrb::Compare::Report** — immutable report with `equivalent?`,
|
|
15
|
+
`similarity` (0..1), `summary` string, and `to_h` serialisation.
|
|
16
|
+
* **CLI: `pdfrb diff LEFT RIGHT`** — compare two PDFs from the
|
|
17
|
+
command line. Exits 0 if equivalent, 1 if different.
|
|
18
|
+
* **Programmatic API**: `Pdfrb::Compare.compare(left, right)` and
|
|
19
|
+
`Pdfrb::Compare.equivalent?(left, right)`. Accepts bytes, IO, or
|
|
20
|
+
Document objects.
|
|
21
|
+
|
|
22
|
+
### Metrics
|
|
23
|
+
|
|
24
|
+
* 550 specs (was 537), 0 failures, 4 pending.
|
|
25
|
+
* 0 rubocop offenses.
|
|
26
|
+
* 182 → 185 lib files; 49 → 50 spec files.
|
|
27
|
+
|
|
5
28
|
## [0.2.1] — 2026-08-02
|
|
6
29
|
|
|
30
|
+
|
|
7
31
|
### Added
|
|
8
32
|
|
|
9
33
|
* **Form XObjects** (`Document::FormXObject`) — create reusable form
|
data/lib/pdfrb/cli.rb
CHANGED
|
@@ -67,7 +67,28 @@ module Pdfrb
|
|
|
67
67
|
puts "Wrote #{output}"
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
desc "
|
|
70
|
+
desc "diff LEFT RIGHT", "Compare two PDFs semantically"
|
|
71
|
+
def diff(left, right)
|
|
72
|
+
report = Pdfrb::Compare.compare(
|
|
73
|
+
File.binread(left),
|
|
74
|
+
File.binread(right)
|
|
75
|
+
)
|
|
76
|
+
puts report.summary
|
|
77
|
+
unless report.equivalent?
|
|
78
|
+
puts
|
|
79
|
+
report.per_page_text_diffs.first(5).each do |d|
|
|
80
|
+
puts " Page #{d[:page] + 1}: #{(d[:similarity] * 100).round(1)}% similar"
|
|
81
|
+
end
|
|
82
|
+
unless report.font_diff[:added].empty? && report.font_diff[:removed].empty?
|
|
83
|
+
puts " Fonts added: #{report.font_diff[:added].join(', ')}" unless report.font_diff[:added].empty?
|
|
84
|
+
puts " Fonts removed: #{report.font_diff[:removed].join(', ')}" unless report.font_diff[:removed].empty?
|
|
85
|
+
end
|
|
86
|
+
puts " Page count delta: #{report.page_count_delta}" unless report.page_count_delta.zero?
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc "encrypt INPUT OUTPUT --password PASSWORD", "Encrypt a PDF"
|
|
71
92
|
method_option :password, type: :string, required: true
|
|
72
93
|
def encrypt(input, output)
|
|
73
94
|
warn "encrypt: not yet fully implemented (Phase 11 integration pending)"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pdfrb
|
|
4
|
+
class Document
|
|
5
|
+
# Form XObject facade. Creates reusable form templates that can
|
|
6
|
+
# be drawn on multiple pages via the /Do operator. A Form XObject
|
|
7
|
+
# is a stream with /Type (omitted), /Subtype /Form, /BBox, and
|
|
8
|
+
# optional /Matrix + /Resources.
|
|
9
|
+
class FormXObject
|
|
10
|
+
attr_reader :document, :stream, :name
|
|
11
|
+
|
|
12
|
+
def initialize(document, name: nil, bbox: nil, matrix: nil)
|
|
13
|
+
@document = document
|
|
14
|
+
@bbox = bbox || [0, 0, 612, 792]
|
|
15
|
+
@matrix = matrix
|
|
16
|
+
|
|
17
|
+
dict = { Subtype: :Form, BBox: @bbox }
|
|
18
|
+
dict[:Matrix] = @matrix if @matrix
|
|
19
|
+
@stream = document.add(dict, type: Pdfrb::Model::Cos::Stream)
|
|
20
|
+
@name = (name || "Form#{@stream.oid}").to_sym
|
|
21
|
+
@canvas = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns a Canvas for drawing into this Form XObject.
|
|
25
|
+
# The Canvas writes to the Form XObject's content stream.
|
|
26
|
+
def canvas
|
|
27
|
+
return @canvas if @canvas
|
|
28
|
+
|
|
29
|
+
contents_stream = @document.add({}, type: Pdfrb::Model::Cos::Stream)
|
|
30
|
+
@stream.value[:Resources] = {}
|
|
31
|
+
@canvas = Pdfrb::Content::Canvas.new(contents_stream, document: @document)
|
|
32
|
+
@stream.stream = +"".b # placeholder; filled by ensure_stream_payload
|
|
33
|
+
@contents_stream = contents_stream
|
|
34
|
+
@canvas
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Finalize: copy the canvas content into the Form XObject stream
|
|
38
|
+
# and register it in the document's resource names. Called
|
|
39
|
+
# automatically before write.
|
|
40
|
+
def finalize!
|
|
41
|
+
return unless @contents_stream
|
|
42
|
+
|
|
43
|
+
@stream.stream = @contents_stream.stream
|
|
44
|
+
@stream.value[:Resources] = @contents_stream.value[:Resources] || {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Register this Form XObject in a page's /Resources /XObject dict
|
|
48
|
+
# so it can be referenced via the /Do operator.
|
|
49
|
+
def register_on_page(page)
|
|
50
|
+
resources = page.value[:Resources]
|
|
51
|
+
resources = {} unless resources.is_a?(::Hash)
|
|
52
|
+
xobjects = resources[:XObject] || {}
|
|
53
|
+
xobjects[@name] = Pdfrb::Model::Reference.new(@stream.oid, @stream.gen)
|
|
54
|
+
resources[:XObject] = xobjects
|
|
55
|
+
page.value[:Resources] = resources
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Draw this Form XObject on a canvas at the given position.
|
|
59
|
+
def draw_on(target_canvas, at:, scale: 1.0)
|
|
60
|
+
target_canvas.emit_op(Pdfrb::Content::Operator::SaveGraphicsState)
|
|
61
|
+
target_canvas.translate(at[0], at[1])
|
|
62
|
+
target_canvas.scale(scale) if (scale - 1.0).abs > 1e-9
|
|
63
|
+
target_canvas.append(" /#{@name} Do\n")
|
|
64
|
+
target_canvas.emit_op(Pdfrb::Content::Operator::RestoreGraphicsState)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/pdfrb/version.rb
CHANGED
data/lib/pdfrb.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -737,6 +737,7 @@ files:
|
|
|
737
737
|
- lib/pdfrb/document/destinations.rb
|
|
738
738
|
- lib/pdfrb/document/files.rb
|
|
739
739
|
- lib/pdfrb/document/fonts.rb
|
|
740
|
+
- lib/pdfrb/document/form_xobject.rb
|
|
740
741
|
- lib/pdfrb/document/images.rb
|
|
741
742
|
- lib/pdfrb/document/metadata.rb
|
|
742
743
|
- lib/pdfrb/document/outline.rb
|