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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 263ee1cee601c0c5b86775ad6bbeb97777f4e7e01c95f63074281d00fbec3d54
4
- data.tar.gz: 5d157430c7657ae19b8954975721863a68db7dbd1a5e3439b8a830da3bc7db4a
3
+ metadata.gz: 04b26536220e55c3be57627ba747561baeee1d58f37e57a2f68f5f121ec1381c
4
+ data.tar.gz: 7a3f7a5a10b43e253e34bb5551042db89e3d94694a568beee186c2995fefe867
5
5
  SHA512:
6
- metadata.gz: 8a6e80f5ad5a3dba5e4dfe087ca33554c0a8fce926e6ad9934f307485bc1728996f336d87ef1b53295dbb79d80d66d41f820c81208a8bcd488b382a6c103c2da
7
- data.tar.gz: '08f52568ba799d4d02c347982407dec464eee0d861d85dafd7c8da8541738e84ccdce83c1ac7866f357f549ccd1f16f2712967f5f22fedaae54977b083ef59af'
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 "encrypt INPUT OUTPUT --password PASSWORD", "Encrypt a PDF (stub — needs Phase 11 integration)"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/pdfrb.rb CHANGED
@@ -93,5 +93,7 @@ module Pdfrb
93
93
 
94
94
  # CLI (only loaded when invoked via the +pdfrb+ executable; avoids
95
95
  # pulling in thor for library-only users).
96
+ autoload :Compare, "pdfrb/compare"
97
+
96
98
  autoload :CLI, "pdfrb/cli"
97
99
  end
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.2.1
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