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 +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/hexapdf/layout/frame.rb +6 -1
- data/lib/hexapdf/task/import_pages.rb +4 -8
- data/lib/hexapdf/task/merge_acro_form.rb +1 -1
- data/lib/hexapdf/type/catalog.rb +4 -4
- data/lib/hexapdf/type/optional_content_membership.rb +13 -0
- data/lib/hexapdf/type/struct_tree_root.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/layout/test_frame.rb +16 -0
- data/test/hexapdf/task/test_import_pages.rb +1 -0
- data/test/hexapdf/task/test_merge_acro_form.rb +6 -0
- data/test/hexapdf/test_document.rb +7 -1
- data/test/hexapdf/type/test_catalog.rb +18 -5
- data/test/hexapdf/type/test_optional_content_membership.rb +40 -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: 3167ea077cd1778bb88e07ea026c1dfc58585624b79130f38ae995ca7829077e
|
|
4
|
+
data.tar.gz: 534b414e43ce0a459a75d1b89b05793480cfc4ceb1537855a28b0bac32a46d51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/hexapdf/layout/frame.rb
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
|
|
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
|
|
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
|
-
|
|
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]
|
|
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
|
data/lib/hexapdf/type/catalog.rb
CHANGED
|
@@ -133,16 +133,16 @@ module HexaPDF
|
|
|
133
133
|
|
|
134
134
|
# Returns the main AcroForm object.
|
|
135
135
|
#
|
|
136
|
-
# * If
|
|
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
|
|
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
|
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -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
|
-
|
|
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] =
|
|
66
|
-
|
|
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
|
-
|
|
83
|
+
assert_nil(@catalog.acro_form)
|
|
72
84
|
end
|
|
73
85
|
|
|
74
|
-
it "
|
|
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
|
-
|
|
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.
|
|
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
|