hexapdf 1.11.1 → 1.11.2

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: 8cf7d72b14ba19f840f63adbcfa3eb5b1fd7b876731c52c615788f395d23d10d
4
- data.tar.gz: 48d506505dcc50d5cc5c596f7e11b8d0961dbea82cea7223c1ae8d705bb9b57e
3
+ metadata.gz: 3167ea077cd1778bb88e07ea026c1dfc58585624b79130f38ae995ca7829077e
4
+ data.tar.gz: 534b414e43ce0a459a75d1b89b05793480cfc4ceb1537855a28b0bac32a46d51
5
5
  SHA512:
6
- metadata.gz: 47be93a799dd59bcab3fcb92c150a0e1b43b8b115dc4d1b38fc029907222a856dde0e717309c85c312fbd3351f0483ccd50f72194f5fce1352eeb1330d92ef39
7
- data.tar.gz: 154edc9c8bf7f2e4f3e354c38c5b52a6f1099760ace8d86c4feb38e0092d0dd0ee3630f615196890e98db339204e636b43fdaac9dd2884b21b36780b59da7bde
6
+ metadata.gz: 5d70d58918d199a7a48090e1d57ec9a8ce88ad69f53ab5935c157bb526c034f597716e566bcf2d1b969f0edaaf0c54a4baa2883e4e9953ca79dcfa4ac68c7b59
7
+ data.tar.gz: 425b4a95c457e909e6bb31eaee868f23fc329472e1e068222f9dc5c73c9745a023b46c741644b93fd4433bdd740c9259f5614dd481298a8aa4f18e244799020f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 1.11.2 - 2026-10-20
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::OptionalContentMembership#ocgs] for retrieving the list of
6
+ optional content groups referenced by the membership dictionary
7
+
8
+ ### Fixed
9
+
10
+ * Regression in [HexaPDF::Task::ImportPages] concerning files with shared
11
+ resources
12
+ * [HexaPDF::Type::Catalog#acro_form] to treat a malformed /AcroForm value
13
+ like a missing AcroForm
14
+ * [HexaPDF::Task::MergeAcroForm] for PDFs with no default resource dictionary
15
+ * [HexaPDF::Layout::Frame] to work when degenerate polygons are in play
16
+
17
+
1
18
  ## 1.11.1 - 2026-10-06
2
19
 
3
20
  ### Changed
@@ -438,6 +438,11 @@ module HexaPDF
438
438
  # Just use the second top-most segment
439
439
  # TODO: not the optimal solution!
440
440
  index = segments.rindex {|s| s.start_point.y < @y }
441
+ unless index # degenerate case where all segments are on the same horizontal line
442
+ @x = @y = @available_width = @available_height = 0
443
+ return
444
+ end
445
+
441
446
  segment = segments[index]
442
447
  y = segment.start_point.y
443
448
  polygon = if segment.min.x == @x
@@ -477,7 +482,7 @@ module HexaPDF
477
482
  # Returns the horizontal segments of the frame's shape, sorted by maximum y-, then minimum
478
483
  # x-coordinate.
479
484
  def sorted_horizontal_segments
480
- @shape.each_segment.select(&:horizontal?).sort! do |a, b|
485
+ @shape.each_segment.select {|s| s.horizontal? && !s.degenerate? }.sort! do |a, b|
481
486
  if a.start_point.y == b.start_point.y
482
487
  b.start_point.x <=> a.start_point.x
483
488
  else
@@ -138,14 +138,16 @@ module HexaPDF
138
138
  def self.preserve_ocgs(doc, source, pages)
139
139
  # Find all OCGs used on all pages
140
140
  ocgs = Set.new
141
+ seen_resources = Set.new
141
142
  process_ocg_or_ocmd = lambda do |obj|
142
143
  if obj.type == :OCG
143
144
  ocgs << obj
144
145
  elsif obj.type == :OCMD
145
- ocgs.merge(obj[:OCGs].to_ary)
146
+ ocgs.merge(obj.ocgs)
146
147
  end
147
148
  end
148
149
  process_page_or_form = lambda do |page_or_form|
150
+ next unless seen_resources.add?(page_or_form.resources.data) # handle shared resources dict
149
151
  page_or_form.resources[:Properties]&.each do |name, obj|
150
152
  process_ocg_or_ocmd.call(obj) if obj
151
153
  end
@@ -154,17 +156,11 @@ module HexaPDF
154
156
  process_page_or_form.call(obj) if obj[:Subtype] == :Form
155
157
  end
156
158
  end
157
- seen_resources = {}
158
159
  pages.each do |page|
159
- unless seen_resources[page.resources] # handle case when pages share the resources dict
160
- process_page_or_form.call(page)
161
- end
162
-
160
+ process_page_or_form.call(page)
163
161
  page.each_annotation do |annot|
164
162
  process_ocg_or_ocmd.call(annot[:OC]) if annot.key?(:OC)
165
163
  end
166
-
167
- seen_resources[page.resources] = true
168
164
  end
169
165
 
170
166
  return if ocgs.empty?
@@ -128,7 +128,7 @@ module HexaPDF
128
128
  font_name_mapping = {}
129
129
  serializer = HexaPDF::Serializer.new
130
130
 
131
- source_form.default_resources[:Font].each do |font_name, value|
131
+ source_form.default_resources[:Font]&.each do |font_name, value|
132
132
  new_name = target_resources.add_font(target_form.document.import(value))
133
133
  font_name_mapping[serializer.serialize(font_name)] = serializer.serialize(new_name)
134
134
  end
@@ -133,16 +133,16 @@ module HexaPDF
133
133
 
134
134
  # Returns the main AcroForm object.
135
135
  #
136
- # * If an AcroForm object exists, the +create+ argument is not used.
136
+ # * If a valid AcroForm object exists, it is returned regardless of the +create+ argument.
137
137
  #
138
- # * If no AcroForm object exists and +create+ is +true+, a new AcroForm object with default
139
- # settings will be created and returned.
138
+ # * If no valid AcroForm object exists and +create+ is +true+, a new AcroForm object with
139
+ # default settings will be created and returned.
140
140
  #
141
141
  # * If no AcroForm object exists and +create+ is +false+, +nil+ is returned.
142
142
  #
143
143
  # See: AcroForm::Form
144
144
  def acro_form(create: false)
145
- if (form = self[:AcroForm])
145
+ if (form = self[:AcroForm]).kind_of?(Dictionary)
146
146
  form
147
147
  elsif create
148
148
  form = self[:AcroForm] = document.add({}, type: :XXAcroForm)
@@ -57,6 +57,19 @@ module HexaPDF
57
57
  allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
58
58
  define_field :VE, type: PDFArray
59
59
 
60
+ # Returns an array of OCGs whose states determine the visibility of content controlled by this
61
+ # membership dictionary.
62
+ def ocgs
63
+ if (ve = self[:VE])
64
+ ocgs = ve.to_ary.flatten.map! {|entry| document.deref(entry) }
65
+ ocgs.reject! {|entry| entry.kind_of?(Symbol) || entry.nil? || entry.null? }
66
+ ocgs.uniq!
67
+ ocgs
68
+ else
69
+ Array(self[:OCGs]).compact.uniq
70
+ end
71
+ end
72
+
60
73
  end
61
74
 
62
75
  end
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  define_field :ParentTreeNextKey, type: Integer
55
55
  define_field :RoleMap, type: Dictionary
56
56
  define_field :ClassMap, type: Dictionary
57
- define_field :Namespaces, type: PDFArray
57
+ define_field :Namespaces, type: PDFArray, version: '2.0'
58
58
  define_field :PronounciationLexicon, type: PDFArray, version: '2.0'
59
59
  define_field :AF, type: PDFArray, version: '2.0'
60
60
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.11.1'
40
+ VERSION = '1.11.2'
41
41
 
42
42
  end
@@ -502,6 +502,22 @@ describe HexaPDF::Layout::Frame do
502
502
  check_regions(frame, [[0, 100, 30, 100], [0, 60, 100, 60]])
503
503
  end
504
504
 
505
+ # o--------+
506
+ #
507
+ # Such a degenerate case can occur under special circumstances with e.g. a ListBox:
508
+ #
509
+ # composer.list(height: 10, style: {padding: 5}) do |list|
510
+ # list.text('test')
511
+ # end
512
+ #
513
+ # Padding exactly fills the box, leaving zero height, and the algorithm in ListBox creates a
514
+ # degenerate Polygon.
515
+ it "works for a flat region with no height" do
516
+ shape = Geom2D::Polygon([0, 0], [100, 0], [100, 0], [0, 0])
517
+ frame = HexaPDF::Layout::Frame.new(0, 0, 100, 0, shape: shape)
518
+ check_regions(frame, [[0, 0, 100, 0]])
519
+ end
520
+
505
521
  # o---+ +--+
506
522
  # | | +--+ |
507
523
  # | +--+ |
@@ -100,6 +100,7 @@ describe HexaPDF::Task::ImportPages do
100
100
  form = canvas.form
101
101
  form[:OC] = @ocg1
102
102
  form.canvas.optional_content(@ocmd)
103
+ form.resources.add_xobject(form)
103
104
  canvas.xobject(form, at: [0, 0])
104
105
  @target.task(:import_pages, source: @doc)
105
106
  assert_equal(['OCG', 'OCMD'], @target.optional_content.ocgs.map(&:name))
@@ -47,6 +47,12 @@ describe HexaPDF::Task::MergeAcroForm do
47
47
  assert(@doc.acro_form.default_resources[:Font].key?(:F2))
48
48
  end
49
49
 
50
+ it "handles AcroForm dictionaries without /DR entry" do
51
+ @doc1.acro_form.delete(:DR)
52
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
53
+ refute(@doc.acro_form.default_resources[:Font].key?(:F2))
54
+ end
55
+
50
56
  it "updates the /SigFlags if necessary" do
51
57
  @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
52
58
  refute(@doc.acro_form.signature_flag?(:signatures_exist))
@@ -498,8 +498,14 @@ describe HexaPDF::Document do
498
498
  describe "acro_form" do
499
499
  it "returns the main AcroForm object" do
500
500
  assert_nil(@doc.acro_form)
501
+
502
+ @doc.acro_form(create: true)
503
+ refute_nil(@doc.acro_form)
504
+ end
505
+
506
+ it "treats a malformed /AcroForm value like a missing AcroForm" do
501
507
  @doc.catalog[:AcroForm] = 5
502
- assert_equal(5, @doc.acro_form)
508
+ assert_nil(@doc.acro_form)
503
509
  end
504
510
 
505
511
  it "create the AcroForm object if instructed" do
@@ -62,19 +62,32 @@ describe HexaPDF::Type::Catalog do
62
62
 
63
63
  describe "acro_form" do
64
64
  it "returns an existing form object" do
65
- @catalog[:AcroForm] = :test
66
- assert_equal(:test, @catalog.acro_form)
65
+ @catalog[:AcroForm] = {}
66
+ form = @catalog[:AcroForm]
67
+ assert_same(form, @catalog.acro_form)
68
+ end
69
+
70
+ it "creates a new AcroForm object with defaults if create: true" do
71
+ form = @catalog.acro_form(create: true)
72
+ assert_kind_of(HexaPDF::Type::AcroForm::Form, form)
73
+ assert(form[:DA])
67
74
  end
68
75
 
69
76
  it "returns an existing form object even if create: true" do
77
+ form = @catalog.acro_form(create: true)
78
+ assert_same(form, @catalog.acro_form(create: true))
79
+ end
80
+
81
+ it "treats an invalid /AcroForm value like a missing AcroForm" do
70
82
  @catalog[:AcroForm] = :test
71
- assert_equal(:test, @catalog.acro_form(create: true))
83
+ assert_nil(@catalog.acro_form)
72
84
  end
73
85
 
74
- it "creates a new AcroForm object with defaults if create: true" do
86
+ it "replaces an invalid /AcroForm value if create: true" do
87
+ @catalog[:AcroForm] = :test
75
88
  form = @catalog.acro_form(create: true)
76
89
  assert_kind_of(HexaPDF::Type::AcroForm::Form, form)
77
- assert(form[:DA])
90
+ assert_same(form, @catalog.acro_form)
78
91
  end
79
92
  end
80
93
 
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/optional_content_membership'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Type::OptionalContentMembership do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @ocmd = @doc.add({Type: :OCMD})
11
+ end
12
+
13
+ describe "ocgs" do
14
+ before do
15
+ @ocg = @doc.add({Type: :OCG, Name: "test"})
16
+ end
17
+
18
+ it "uses the /OCGs entry if /VE is not available" do
19
+ assert_equal([], @ocmd.ocgs)
20
+
21
+ @ocmd[:OCGs] = []
22
+ assert_equal([], @ocmd.ocgs)
23
+
24
+ @ocmd[:OCGs] = @ocg
25
+ assert_equal([@ocg], @ocmd.ocgs)
26
+
27
+ null = @doc.add({})
28
+ null.data.value = nil
29
+ @ocmd[:OCGS] = [@ocg, nil, null, @ocg]
30
+ assert_equal([@ocg], @ocmd.ocgs)
31
+ end
32
+
33
+ it "uses the /VE entry if available" do
34
+ null = @doc.add({})
35
+ null.data.value = nil
36
+ @ocmd[:VE] = [:Or, @ocg, nil, [:Not, @ocg], [:And, @ocg, [:Or, null, @ocg]]]
37
+ assert_equal([@ocg], @ocmd.ocgs)
38
+ end
39
+ end
40
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.1
4
+ version: 1.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
@@ -922,6 +922,7 @@ files:
922
922
  - test/hexapdf/type/test_object_stream.rb
923
923
  - test/hexapdf/type/test_optional_content_configuration.rb
924
924
  - test/hexapdf/type/test_optional_content_group.rb
925
+ - test/hexapdf/type/test_optional_content_membership.rb
925
926
  - test/hexapdf/type/test_optional_content_properties.rb
926
927
  - test/hexapdf/type/test_outline.rb
927
928
  - test/hexapdf/type/test_outline_item.rb