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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da17a2a9dd8459e6f4624f59621c84f1b0be73f4d23347bffcdd6c534b58680e
4
- data.tar.gz: ecdcfedc4f89722f799d04fda467acbed24bf256a1fa626623fce1d6399d6d98
3
+ metadata.gz: 3167ea077cd1778bb88e07ea026c1dfc58585624b79130f38ae995ca7829077e
4
+ data.tar.gz: 534b414e43ce0a459a75d1b89b05793480cfc4ceb1537855a28b0bac32a46d51
5
5
  SHA512:
6
- metadata.gz: 5a39a7635005c7ef8be1dc0a1efc21b1aca00fba3bedb80886e5d909d9fc0ebfed0f967fff6f10f96ed6b05a883fee0337fea3802bfcde3a93a644e512d0ee3b
7
- data.tar.gz: 4e9ccdbe854d0e5fb00e7042d61c758da93c5b354982b48a20f4a6cec3a07d209dac5711d1eb9b3b6274d8662acac8851124a838bed5795cc6e40c924f4bba26
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
- all = composer.document.optional_content.ocg('All answers')
25
- a1 = composer.document.optional_content.ocg('Answer 1')
26
- a1m = composer.document.optional_content.create_ocmd([a1, all], policy: :any_on)
27
- a2 = composer.document.optional_content.ocg('Answer 2')
28
- a2m = composer.document.optional_content.create_ocmd([a2, all], policy: :any_on)
29
- a3 = composer.document.optional_content.ocg('Answer 3')
30
- a3m = composer.document.optional_content.create_ocmd([a3, all], policy: :any_on)
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' => a1})
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' => a2})
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' => a3})
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
- while data.length >= bytes_per_row
361
- i = 1
362
- image_data << data.getbyte(0)
363
- mask_data << data.getbyte(0)
364
- while i < bytes_per_row
365
- bytes_per_colors.times {|j| image_data << data.getbyte(i + j) }
366
- i += bytes_per_colors
367
- bytes_per_alpha.times {|j| mask_data << data.getbyte(i + j) }
368
- i += bytes_per_alpha
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
- image_data = Filter.string_from_source(flate_decode.encoder(Fiber.new { image_data }))
375
- mask_data = Filter.string_from_source(flate_decode.encoder(Fiber.new { mask_data }))
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
@@ -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
@@ -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) || field.null?
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
@@ -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)
@@ -187,10 +187,14 @@ module HexaPDF
187
187
  end
188
188
  end
189
189
 
190
- # Applies the given intent (:View, :Design or a custom intent) to the OCG.
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
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.11.0'
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
@@ -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] = :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
 
@@ -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.0
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