hexapdf 1.11.0 → 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 +30 -0
- data/examples/027-composer_optional_content.rb +11 -15
- data/lib/hexapdf/image_loader/png.rb +31 -15
- 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/acro_form/form.rb +2 -1
- data/lib/hexapdf/type/catalog.rb +4 -4
- data/lib/hexapdf/type/optional_content_group.rb +20 -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/acro_form/test_form.rb +1 -0
- data/test/hexapdf/type/test_catalog.rb +18 -5
- data/test/hexapdf/type/test_optional_content_group.rb +8 -4
- 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,33 @@
|
|
|
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
|
+
|
|
18
|
+
## 1.11.1 - 2026-10-06
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
* [HexaPDF::Type::OptionalContentGroup] methods `#apply_intent`, `#on!`,
|
|
23
|
+
`#off!`, `#add_to_ui` to return self
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
* [HexaPDF::Type::AcroForm::Form] validation to also remove objects with an
|
|
28
|
+
invalid /Type field
|
|
29
|
+
|
|
30
|
+
|
|
1
31
|
## 1.11.0 - 2026-09-22
|
|
2
32
|
|
|
3
33
|
### Added
|
|
@@ -21,13 +21,14 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
|
21
21
|
answer: {font: 'ZapfDingbats', fill_color: "green"},
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
a2 =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
oc = composer.document.optional_content
|
|
25
|
+
all = oc.ocg('All answers').off!.add_to_ui
|
|
26
|
+
a1 = oc.ocg('Answer 1').off!.add_to_ui
|
|
27
|
+
a2 = oc.ocg('Answer 2').off!.add_to_ui
|
|
28
|
+
a3 = oc.ocg('Answer 3').off!.add_to_ui
|
|
29
|
+
a1m = oc.create_ocmd([a1, all], policy: :any_on)
|
|
30
|
+
a2m = oc.create_ocmd([a2, all], policy: :any_on)
|
|
31
|
+
a3m = oc.create_ocmd([a3, all], policy: :any_on)
|
|
31
32
|
|
|
32
33
|
composer.text('The Great Ruby Quiz', text_align: :center, margin: [0, 0, 24],
|
|
33
34
|
font: 'Helvetica bold', font_size: 24)
|
|
@@ -41,7 +42,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
|
41
42
|
answers.multiple do |answer|
|
|
42
43
|
answer.text('Yukihiro “Matz” Matsumoto', position: :float)
|
|
43
44
|
answer.text("\u{a0}\u{a0}✔", style: :answer,
|
|
44
|
-
properties: {'optional_content' =>
|
|
45
|
+
properties: {'optional_content' => a1m})
|
|
45
46
|
end
|
|
46
47
|
answers.text('Rob Pike')
|
|
47
48
|
end
|
|
@@ -57,7 +58,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
|
57
58
|
answers.multiple do |answer|
|
|
58
59
|
answer.text('1993', position: :float)
|
|
59
60
|
answer.text("\u{a0}\u{a0}✔", style: :answer,
|
|
60
|
-
properties: {'optional_content' =>
|
|
61
|
+
properties: {'optional_content' => a2m})
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
end
|
|
@@ -68,7 +69,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
|
68
69
|
answer = composer.document.layout.text('There are several PDF libraries for ' \
|
|
69
70
|
'Ruby but the best is HexaPDF! :)',
|
|
70
71
|
width: 400,
|
|
71
|
-
properties: {'optional_content' =>
|
|
72
|
+
properties: {'optional_content' => a3m})
|
|
72
73
|
item.formatted_text([{box: answer}], border: {width: [0, 0, 1]})
|
|
73
74
|
end
|
|
74
75
|
end
|
|
@@ -77,9 +78,4 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
|
77
78
|
action.add_state_change(:toggle, [a1, a2, a3])
|
|
78
79
|
composer.text("Click to toggle answers", border: {width: 1, color: "red"},
|
|
79
80
|
align: :right, padding: 2, overlays: [[:link, action: action]])
|
|
80
|
-
|
|
81
|
-
composer.document.optional_content.default_configuration(
|
|
82
|
-
BaseState: :OFF,
|
|
83
|
-
Order: [all, a1, a2, a3],
|
|
84
|
-
)
|
|
85
81
|
end
|
|
@@ -346,33 +346,49 @@ module HexaPDF
|
|
|
346
346
|
def separate_alpha_channel(offset, decode_parms)
|
|
347
347
|
bytes_per_colors = (decode_parms[:BitsPerComponent] * decode_parms[:Colors] + 7) / 8
|
|
348
348
|
bytes_per_alpha = (decode_parms[:BitsPerComponent] + 7) / 8
|
|
349
|
+
bytes_per_pixel = bytes_per_colors + bytes_per_alpha
|
|
349
350
|
bytes_per_row = (decode_parms[:Columns] * decode_parms[:BitsPerComponent] *
|
|
350
351
|
(decode_parms[:Colors] + 1) + 7) / 8 + 1
|
|
351
|
-
image_data = ''.b
|
|
352
|
-
mask_data = ''.b
|
|
353
352
|
|
|
354
353
|
flate_decode = @document.config.constantize('filter.map', :FlateDecode)
|
|
355
354
|
source = flate_decode.decoder(Fiber.new(&image_data_proc(offset)))
|
|
356
355
|
|
|
356
|
+
image_deflate = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate_compression'],
|
|
357
|
+
Zlib::MAX_WBITS,
|
|
358
|
+
HexaPDF::GlobalConfiguration['filter.flate_memory'])
|
|
359
|
+
mask_deflate = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate_compression'],
|
|
360
|
+
Zlib::MAX_WBITS,
|
|
361
|
+
HexaPDF::GlobalConfiguration['filter.flate_memory'])
|
|
362
|
+
|
|
363
|
+
image_data = ''.b
|
|
364
|
+
mask_data = ''.b
|
|
365
|
+
image_temp = ''.b
|
|
366
|
+
mask_temp = ''.b
|
|
357
367
|
data = ''.b
|
|
358
368
|
while source.alive? && (new_data = source.resume)
|
|
359
369
|
data << new_data
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
i = 0
|
|
371
|
+
image_temp.clear
|
|
372
|
+
mask_temp.clear
|
|
373
|
+
while i + bytes_per_row <= data.length
|
|
374
|
+
image_temp << data.getbyte(i)
|
|
375
|
+
mask_temp << data.getbyte(i)
|
|
376
|
+
i += 1
|
|
377
|
+
row_loop_end_index = i + bytes_per_row - 1
|
|
378
|
+
while i < row_loop_end_index
|
|
379
|
+
image_temp << data.byteslice(i, bytes_per_colors)
|
|
380
|
+
mask_temp << data.byteslice(i + bytes_per_colors, bytes_per_alpha)
|
|
381
|
+
i += bytes_per_pixel
|
|
369
382
|
end
|
|
370
|
-
data = data[bytes_per_row..-1]
|
|
371
383
|
end
|
|
384
|
+
image_data << image_deflate.deflate(image_temp)
|
|
385
|
+
mask_data << mask_deflate.deflate(mask_temp)
|
|
386
|
+
data = data[i..-1]
|
|
372
387
|
end
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
image_data << image_deflate.finish
|
|
389
|
+
mask_data << mask_deflate.finish
|
|
390
|
+
image_deflate.close
|
|
391
|
+
mask_deflate.close
|
|
376
392
|
|
|
377
393
|
[image_data, mask_data]
|
|
378
394
|
end
|
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
|
|
@@ -573,7 +573,8 @@ module HexaPDF
|
|
|
573
573
|
|
|
574
574
|
validate_array = lambda do |parent, container|
|
|
575
575
|
container.map! do |field|
|
|
576
|
-
if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) ||
|
|
576
|
+
if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) ||
|
|
577
|
+
field.null? || (field.key?(:Type) && field[:Type] != :Annot)
|
|
577
578
|
yield("Invalid object in AcroForm field hierarchy", true)
|
|
578
579
|
next nil
|
|
579
580
|
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)
|
|
@@ -187,10 +187,14 @@ module HexaPDF
|
|
|
187
187
|
end
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
#
|
|
190
|
+
# :call-seq:
|
|
191
|
+
# ocg.apply_intent(intent) -> ocg
|
|
192
|
+
#
|
|
193
|
+
# Applies the given intent (:View, :Design or a custom intent) to the OCG and returns +self+.
|
|
191
194
|
def apply_intent(intent)
|
|
192
195
|
self[:Intent] = key?(:Intent) ? Array(self[:Intent]) : []
|
|
193
196
|
self[:Intent] << intent
|
|
197
|
+
self
|
|
194
198
|
end
|
|
195
199
|
|
|
196
200
|
# Returns +true+ if this OCG has an intent of :View.
|
|
@@ -209,23 +213,35 @@ module HexaPDF
|
|
|
209
213
|
document.optional_content.default_configuration.ocg_on?(self)
|
|
210
214
|
end
|
|
211
215
|
|
|
216
|
+
# :call-seq:
|
|
217
|
+
# ocg.on! -> ocg
|
|
218
|
+
#
|
|
212
219
|
# Sets the state of the OCG to on in the default configuration (see
|
|
213
|
-
# OptionalContentProperties#default_configuration)
|
|
220
|
+
# OptionalContentProperties#default_configuration) and returns +self+.
|
|
214
221
|
def on!
|
|
215
222
|
document.optional_content.default_configuration.ocg_state(self, :on)
|
|
223
|
+
self
|
|
216
224
|
end
|
|
217
225
|
|
|
226
|
+
# :call-seq:
|
|
227
|
+
# ocg.off! -> ocg
|
|
228
|
+
#
|
|
218
229
|
# Sets the state of the OCG to off in the default configuration (see
|
|
219
|
-
# OptionalContentProperties#default_configuration)
|
|
230
|
+
# OptionalContentProperties#default_configuration) and returns +self+.
|
|
220
231
|
def off!
|
|
221
232
|
document.optional_content.default_configuration.ocg_state(self, :off)
|
|
233
|
+
self
|
|
222
234
|
end
|
|
223
235
|
|
|
236
|
+
# :call-seq:
|
|
237
|
+
# ocg.add_to_ui(path: nil) -> ocg
|
|
238
|
+
#
|
|
224
239
|
# Adds the OCG to the PDF processor's user interface in the default configuration (see
|
|
225
240
|
# OptionalContentProperties#default_configuration), either at the top-level or under the given
|
|
226
|
-
# hierarchical +path+ but always as the last item
|
|
241
|
+
# hierarchical +path+ but always as the last item, and returns +self+.
|
|
227
242
|
def add_to_ui(path: nil)
|
|
228
243
|
document.optional_content.default_configuration.add_ocg_to_ui(self, path: path)
|
|
244
|
+
self
|
|
229
245
|
end
|
|
230
246
|
|
|
231
247
|
# :call-seq:
|
|
@@ -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
|
|
@@ -537,6 +537,7 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
|
537
537
|
@doc.add({T: :Tx3, FT: :Tx, Kids: [@doc.add({T: :Tx4}),
|
|
538
538
|
[:nothing],
|
|
539
539
|
@doc.add({T: :Tx5, Kids: [@doc.add({T: :Tx6})]})]}),
|
|
540
|
+
@doc.wrap({Type: :Page}),
|
|
540
541
|
]
|
|
541
542
|
@acro_form[:Fields][6][:Kids][0][:Parent] = @acro_form[:Fields][6]
|
|
542
543
|
@acro_form[:Fields][6][:Kids][2][:Parent] = @acro_form[:Fields][6]
|
|
@@ -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
|
|
|
@@ -69,7 +69,8 @@ describe HexaPDF::Type::OptionalContentGroup do
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
it "can apply one or more intents" do
|
|
72
|
-
@ocg.apply_intent(:View)
|
|
72
|
+
ocg = @ocg.apply_intent(:View)
|
|
73
|
+
assert_same(@ocg, ocg)
|
|
73
74
|
@ocg.apply_intent(:Design)
|
|
74
75
|
assert(@ocg.intent_view?)
|
|
75
76
|
assert(@ocg.intent_design?)
|
|
@@ -85,17 +86,20 @@ describe HexaPDF::Type::OptionalContentGroup do
|
|
|
85
86
|
|
|
86
87
|
it "can set its default state to on" do
|
|
87
88
|
@doc.optional_content.default_configuration[:OFF] = [@ocg]
|
|
88
|
-
@ocg.on!
|
|
89
|
+
ocg = @ocg.on!
|
|
90
|
+
assert_same(@ocg, ocg)
|
|
89
91
|
assert(@ocg.on?)
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
it "can set its default state to off" do
|
|
93
|
-
@ocg.off!
|
|
95
|
+
ocg = @ocg.off!
|
|
96
|
+
assert_same(@ocg, ocg)
|
|
94
97
|
refute(@ocg.on?)
|
|
95
98
|
end
|
|
96
99
|
|
|
97
100
|
it "can add itself to the UI" do
|
|
98
|
-
@ocg.add_to_ui(path: 'Test')
|
|
101
|
+
ocg = @ocg.add_to_ui(path: 'Test')
|
|
102
|
+
assert_same(@ocg, ocg)
|
|
99
103
|
assert_equal([['Test', @ocg]], @doc.optional_content.default_configuration[:Order].value)
|
|
100
104
|
end
|
|
101
105
|
end
|
|
@@ -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
|