hexapdf 1.4.0 → 1.5.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: 82d0430964f9f4c6925af5bb076a2e641908c3a4cb6796acc64dbe1303bc3407
4
- data.tar.gz: 689a86b637a86331ca203d7d7ac90a9fb3c50c275f07b21d8109013faa509f07
3
+ metadata.gz: de7c1790b3c958a91f071b5c20063eafea93fed12a034b89890242fec25c3026
4
+ data.tar.gz: 0e201dc452930a2a81461be5bf9cd27d2749b92815498c06b37a1e2635a20d7d
5
5
  SHA512:
6
- metadata.gz: 4cfe8038379e5dc7f3bebeb38b44525676b934cb2b2355ac7575fa7a4466a6c4ec9ab9e0a80a184aebab32b2aa87e06dff735c10915abebe11541ada95d8129c
7
- data.tar.gz: 62562b4bae557ad3dac03cb51913a8d1d6796d6cad9ce0626bc901cc15afeb301e42d061c345a1ebcb30e09a018dbe4b7c26fc4c567423c262f67607d04b95c9
6
+ metadata.gz: 1d1b13a5c28c83ca8ec4730cfc0af3016ceb14831c16587b000d8b69d0c7482d166bed21542f4929a8e4614fc732208c5670451a34339776b78a66dea8374949
7
+ data.tar.gz: 309e3aa2a80ec92b4fd35e72e9ab0c114fe4022467be9fb6fb5805085d6616ea8301ab30345a7322b009aeeffa5d6b052abfb8f0bafbaa9ca8b2223af3a6b223
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## 1.5.0 - 2025-12-08
2
+
3
+ ### Added
4
+
5
+ * Support for basic authentication to
6
+ [HexaPDF::DigitalSignature::Signing::TimestampHandler]
7
+
8
+ ### Changed
9
+
10
+ * Dictionary validation to delete field entries that have an invalid type
11
+ * CLI command `hexapdf images` to create directories specified in the `--prefix`
12
+ * CLI command `hexapdf images` to omit the dash in the file names if `--prefix`
13
+ points to a directory
14
+
15
+ ## Fixed
16
+
17
+ * [HexaPDF::Type::Annotation#appearance] to work in case /AP contains a value of
18
+ an invalid type
19
+ * [HexaPDF::DigitalSignature::CMSHandler] to throw an appropriate error when
20
+ encountering invalid signature contents
21
+
22
+
23
+ ## 1.4.1 - 2025-09-23
24
+
25
+ ### Added
26
+
27
+ * [HexaPDF::Font::Encoding::Base#to_compact_array] for creating a compact array
28
+ representation of the encoding
29
+
30
+ ### Changed
31
+
32
+ - CLI to handle missing file errors better
33
+
34
+ ### Fixed
35
+
36
+ * Serialization of strings that need to be UTF-16 encoded when using encryption
37
+ * [HexaPDF::Document#write_to_string] to pass on arguments to `#write`
38
+ * [HexaPDF::Type::FontType1] validation to handle PDFs with an invalid value of
39
+ /SymbolEncoding for the /Encoding key
40
+ * [HexaPDF::Type::FontType1] validation to handle PDFs with an invalid value of
41
+ /StandardEncoding for the /Encoding key
42
+ * CLI command `hexapdf form` to ignore widgets that don't belong to any field
43
+ * Validation of invalid sorted tree root nodes with odd number of direct entries
44
+
45
+
1
46
  ## 1.4.0 - 2025-08-03
2
47
 
3
48
  ### Added
@@ -24,8 +69,8 @@
24
69
  of a table cell
25
70
  * [HexaPDF::Layout::Style::Quad#set] to allow setting a subset of values using a
26
71
  hash
27
- * CLI command `hp form` to show the names of radio button widgets
28
- * CLI command `hp form` to show position and size of widgets in easier to
72
+ * CLI command `hexapdf form` to show the names of radio button widgets
73
+ * CLI command `hexapdf form` to show position and size of widgets in easier to
29
74
  understand form
30
75
  * Default signing handler to not set /DigestMethod entry on signature reference
31
76
  dictionary anymore
@@ -290,7 +290,7 @@ module HexaPDF
290
290
  page.each_annotation do |annotation|
291
291
  next unless annotation[:Subtype] == :Widget
292
292
  field = annotation.form_field
293
- next if field.concrete_field_type == :push_button
293
+ next if !field.concrete_field_type || field.concrete_field_type == :push_button
294
294
  if with_seen || !seen[field.full_field_name]
295
295
  yield(page, page_index, field, annotation)
296
296
  seen[field.full_field_name] = true
@@ -35,6 +35,7 @@
35
35
  #++
36
36
 
37
37
  require 'set'
38
+ require 'fileutils'
38
39
  require 'hexapdf/cli/command'
39
40
 
40
41
  module HexaPDF
@@ -132,7 +133,7 @@ module HexaPDF
132
133
  printf("%5s %5s %9s %6s %6s %5s %4s %3s %5s %5s %6s %5s %8s\n",
133
134
  "index", "page", "oid", "width", "height", "color", "comp", "bpc",
134
135
  "x-ppi", "y-ppi", "size", "type", "writable")
135
- puts("-" * 77)
136
+ puts("-" * 84)
136
137
  each_image(doc) do |image, index, pindex, (x_ppi, y_ppi)|
137
138
  info = image.info
138
139
  size = human_readable_file_size(image[:Length] + image[:SMask]&.[](:Length).to_i)
@@ -145,20 +146,34 @@ module HexaPDF
145
146
 
146
147
  # Extracts the images with the given indices.
147
148
  def extract_images(doc)
149
+ FileUtils.mkdir_p(File.dirname("#{@prefix}filename"))
150
+ prefix = File.directory?(@prefix) ? @prefix : "@{prefix}-"
151
+
148
152
  done = Set.new
153
+ count = total = 0
149
154
  each_image(doc) do |image, index, _|
150
155
  next unless (@indices.include?(index) || @indices.include?(0)) && !done.include?(index)
156
+ total += 1
151
157
  info = image.info
152
158
  if info.writable
153
- path = "#{@prefix}-#{index}.#{image.info.extension}"
159
+ count += 1
160
+ path = "#{@prefix}#{index}.#{image.info.extension}"
154
161
  maybe_raise_on_existing_file(path)
155
- puts "Extracting #{path}..." if command_parser.verbosity_info?
162
+ if command_parser.verbosity_info?
163
+ puts "Extracting image #{index} (#{image.width}x#{image.height}, " \
164
+ "#{info.color_space}, #{info.type}) to #{path}..."
165
+ end
156
166
  image.write(path)
157
167
  done << index
168
+ if info.color_space == :cmyk && info.type == :jpeg
169
+ $stderr.puts "Note (image #{path}): JPEG uses CMYK colorspace and may " \
170
+ "need color post-processing"
171
+ end
158
172
  elsif command_parser.verbosity_warning?
159
173
  $stderr.puts "Warning (image #{index}): PDF image format not supported for writing"
160
174
  end
161
175
  end
176
+ puts "Created #{count} image files (out of #{total} selected)" if command_parser.verbosity_info?
162
177
  end
163
178
 
164
179
  # Iterates over all images.
data/lib/hexapdf/cli.rb CHANGED
@@ -61,6 +61,9 @@ module HexaPDF
61
61
  # Runs the CLI application.
62
62
  def self.run(args = ARGV)
63
63
  Application.new.parse(args)
64
+ rescue Errno::ENOENT => e
65
+ path = e.message.scan(/(?<= - ).*?$/).first
66
+ $stderr.puts "Problem encountered: No such file - #{path}"
64
67
  rescue StandardError => e
65
68
  $stderr.puts "Problem encountered: #{e.message}"
66
69
  unless e.kind_of?(HexaPDF::Error)
@@ -301,7 +301,13 @@ module HexaPDF
301
301
  yield(msg, true)
302
302
  self[name] = obj.intern
303
303
  else
304
- yield(msg, false)
304
+ yield(msg, !field.required? || field.default?)
305
+ if field.required? && field.default?
306
+ self[name] = obj = field.default
307
+ else
308
+ delete(name)
309
+ next
310
+ end
305
311
  end
306
312
  end
307
313
 
@@ -49,7 +49,11 @@ module HexaPDF
49
49
  # Creates a new signature handler for the given signature dictionary.
50
50
  def initialize(signature_dict)
51
51
  super
52
- @pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
52
+ begin
53
+ @pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
54
+ rescue
55
+ raise HexaPDF::Error, "Signature contents is invalid"
56
+ end
53
57
  end
54
58
 
55
59
  # Returns the common name of the signer.
@@ -53,8 +53,8 @@ module HexaPDF
53
53
  # == Usage
54
54
  #
55
55
  # It is necessary to provide at least the URL of the timestamp authority server (TSA) via
56
- # #tsa_url, everything else is optional and uses default values. The TSA server must not use
57
- # authentication to be usable.
56
+ # #tsa_url, everything else is optional and uses default values. The TSA server can optionally
57
+ # use HTTP basic authentication.
58
58
  #
59
59
  # Example:
60
60
  #
@@ -66,6 +66,18 @@ module HexaPDF
66
66
  # This value is required.
67
67
  attr_accessor :tsa_url
68
68
 
69
+ # The username for basic authentication to the TSA server.
70
+ #
71
+ # If the username is not set, no basic authentication is done.
72
+ #
73
+ # See: #tsa_password
74
+ attr_accessor :tsa_username
75
+
76
+ # The password for basic authentication to the TSA server.
77
+ #
78
+ # See: #tsa_username
79
+ attr_accessor :tsa_password
80
+
69
81
  # The hash algorithm to use for timestamping. Defaults to SHA512.
70
82
  attr_accessor :tsa_hash_algorithm
71
83
 
@@ -127,8 +139,14 @@ module HexaPDF
127
139
  req.message_imprint = digest.digest
128
140
  req.policy_id = tsa_policy_id if tsa_policy_id
129
141
 
130
- http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
131
- 'content-type' => 'application/timestamp-query')
142
+ url = URI(tsa_url)
143
+ http_request = Net::HTTP::Post.new(url, 'Content-Type' => 'application/timestamp-query')
144
+ http_request.body = req.to_der
145
+ http_request.basic_auth(tsa_username, tsa_password) if tsa_username
146
+ http_response = Net::HTTP.start(url.hostname, url.port, use_ssl: (url.scheme == 'https')) do |http|
147
+ http.request(http_request)
148
+ end
149
+
132
150
  if http_response.kind_of?(Net::HTTPOK)
133
151
  response = OpenSSL::Timestamp::Response.new(http_response.body)
134
152
  if response.status == 0
@@ -136,6 +154,8 @@ module HexaPDF
136
154
  else
137
155
  raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
138
156
  end
157
+ elsif http_response.kind_of?(Net::HTTPUnauthorized)
158
+ raise HexaPDF::Error, "Basic authentication to the server failed: #{http_response.body}"
139
159
  else
140
160
  raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
141
161
  end
@@ -823,7 +823,7 @@ module HexaPDF
823
823
  # See #write for further information and details on the available arguments.
824
824
  def write_to_string(**args)
825
825
  io = StringIO.new(''.b)
826
- write(io)
826
+ write(io, **args)
827
827
  io.string
828
828
  end
829
829
 
@@ -81,6 +81,33 @@ module HexaPDF
81
81
  @code_to_name.key(name)
82
82
  end
83
83
 
84
+ # Returns the encoding in a compact array form.
85
+ #
86
+ # If the optional +base_encoding+ argument is specified, all codes that have the same value
87
+ # in the base encoding are ignored.
88
+ #
89
+ # The returned array is of the form:
90
+ #
91
+ # code1 name1 name2 ... code2 name3 name4 ...
92
+ #
93
+ # This means that name1 is associated with code1, name2 with code1 + 1 and so on.
94
+ #
95
+ # See: PDF 2.0 s9.6.5.1
96
+ def to_compact_array(base_encoding: nil)
97
+ result = []
98
+ last_code = -3
99
+ @code_to_name.sort.each do |code, name|
100
+ next if base_encoding&.name(code) == name
101
+ if last_code + 1 == code
102
+ result << name
103
+ else
104
+ result << code << name
105
+ end
106
+ last_code = code
107
+ end
108
+ result
109
+ end
110
+
84
111
  end
85
112
 
86
113
  end
@@ -279,9 +279,7 @@ module HexaPDF
279
279
  if VALID_ENCODING_NAMES.include?(@encoding.encoding_name)
280
280
  dict[:Encoding] = @encoding.encoding_name
281
281
  elsif @encoding != @wrapped_font.encoding
282
- differences = [min]
283
- (min..max).each {|code| differences << @encoding.name(code) }
284
- dict[:Encoding] = {Differences: differences}
282
+ dict[:Encoding] = {Differences: @encoding.to_compact_array}
285
283
  end
286
284
  end
287
285
 
@@ -131,8 +131,8 @@ module HexaPDF
131
131
  # fixed height (only if the actual content is smaller or equal than it):
132
132
  #
133
133
  # #>pdf-composer
134
- # cells = [[{content: layout.text('A'), height: 5}, layout.text('B')],
135
- # [{content: layout.text('C'), height: 40}, layout.text('D')]]
134
+ # cells = [[{content: layout.text('A'), min_height: 5}, layout.text('B')],
135
+ # [{content: layout.text('C'), min_height: 40}, layout.text('D')]]
136
136
  # composer.table(cells)
137
137
  #
138
138
  # The cells can be styled using a callable object for more complex styling:
@@ -276,16 +276,16 @@ module HexaPDF
276
276
  #
277
277
  # See: PDF2.0 s7.3.4
278
278
  def serialize_string(obj)
279
+ if obj.encoding != Encoding::BINARY && obj.match?(/[^ -~\t\r\n]/)
280
+ utf16_encoded = true
281
+ obj = "\xFE\xFF".b << obj.encode(Encoding::UTF_16BE).force_encoding(Encoding::BINARY)
282
+ end
279
283
  obj = if @encrypter && @object.kind_of?(HexaPDF::Object) && @object.indirect?
280
284
  encrypter.encrypt_string(obj, @object)
281
- elsif obj.encoding != Encoding::BINARY
282
- if obj.match?(/[^ -~\t\r\n]/)
283
- "\xFE\xFF".b << obj.encode(Encoding::UTF_16BE).force_encoding(Encoding::BINARY)
284
- else
285
- obj.b
286
- end
285
+ elsif utf16_encoded
286
+ obj
287
287
  else
288
- obj.dup
288
+ obj.b
289
289
  end
290
290
  obj.gsub!(/[()\\\r]/n, STRING_ESCAPE_MAP)
291
291
  "(#{obj})"
@@ -243,7 +243,7 @@ module HexaPDF
243
243
  # The appearance state in /AS or the one provided via +state_name+ is taken into account if
244
244
  # necessary.
245
245
  def appearance(type: :normal, state_name: self[:AS])
246
- entry = appearance_dict&.send("#{type}_appearance")
246
+ entry = appearance_dict&.send("#{type}_appearance") rescue nil
247
247
  if entry.kind_of?(HexaPDF::Dictionary) && !entry.kind_of?(HexaPDF::Stream)
248
248
  entry = entry[state_name]
249
249
  end
@@ -183,7 +183,18 @@ module HexaPDF
183
183
 
184
184
  encoding = self[:Encoding]
185
185
  if encoding.kind_of?(Symbol) && !PREDEFINED_ENCODING.include?(encoding)
186
- yield("The /Encoding value '#{encoding}' is invalid", false)
186
+ correctable = (self[:BaseFont] == :Symbol && encoding == :SymbolEncoding) ||
187
+ (!symbolic? && encoding == :StandardEncoding)
188
+ yield("The /Encoding value '#{encoding}' is invalid", correctable)
189
+ if correctable
190
+ if encoding == :SymbolEncoding
191
+ delete(:Encoding)
192
+ else
193
+ diffs = HexaPDF::Font::Encoding.for_name(:StandardEncoding).
194
+ to_compact_array(base_encoding: HexaPDF::Font::Encoding.for_name(:WinAnsiEncoding))
195
+ self[:Encoding] = {BaseEncoding: :WinAnsiEncoding, Differences: diffs}
196
+ end
197
+ end
187
198
  end
188
199
  end
189
200
 
@@ -322,7 +322,10 @@ module HexaPDF
322
322
  if key?(container_name)
323
323
  container = self[container_name]
324
324
  if container.length.odd?
325
- yield("Sorted tree leaf node contains odd number of entries", false)
325
+ root_node = !key?(:Limits)
326
+ yield("Sorted tree #{root_node ? 'root' : 'leaf'} node contains odd number of entries",
327
+ root_node)
328
+ container.value.clear if root_node
326
329
  return
327
330
  end
328
331
  index = 0
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.4.0'
40
+ VERSION = '1.5.0'
41
41
 
42
42
  end
@@ -112,7 +112,12 @@ module HexaPDF
112
112
  @tsa_server.mount_proc('/') do |request, response|
113
113
  @tsr = OpenSSL::Timestamp::Request.new(request.body)
114
114
  case @tsr.policy_id || '1.2.3.4.0'
115
- when '1.2.3.4.0', '1.2.3.4.2'
115
+ when '1.2.3.4.0', '1.2.3.4.2', '1.2.3.4.3'
116
+ if @tsr.policy_id == '1.2.3.4.3'
117
+ WEBrick::HTTPAuth.basic_auth(request, response, 'HexaPDF Auth') do |username, password|
118
+ username == 'hexatest' && password == 'hexapwd'
119
+ end
120
+ end
116
121
  fac = OpenSSL::Timestamp::Factory.new
117
122
  fac.gen_time = Time.now
118
123
  fac.serial_number = 1
@@ -67,6 +67,18 @@ describe HexaPDF::DigitalSignature::Signing::TimestampHandler do
67
67
  assert_equal("1.2.3.4.2", policy_id)
68
68
  end
69
69
 
70
+ it "allows using basic authentication on the server" do
71
+ @handler.tsa_policy_id = '1.2.3.4.3'
72
+ @handler.tsa_username = 'hexatest'
73
+ @handler.tsa_password = 'invalid'
74
+ msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
75
+ assert_match(/Basic authentication/, msg.message)
76
+
77
+ @handler.tsa_password = 'hexapwd'
78
+ token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
79
+ assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
80
+ end
81
+
70
82
  it "returns the serialized timestamp token" do
71
83
  token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
72
84
  assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
@@ -17,6 +17,12 @@ describe HexaPDF::DigitalSignature::CMSHandler do
17
17
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
18
18
  end
19
19
 
20
+ it "fails with an appropriate error if the the signature contents is invalid" do
21
+ @dict.contents = :Unknown
22
+ msg = assert_raises(HexaPDF::Error) { HexaPDF::DigitalSignature::CMSHandler.new(@dict) }
23
+ assert_match(/contents is invalid/, msg.message)
24
+ end
25
+
20
26
  it "returns the signer name" do
21
27
  assert_equal("RSA signer", @handler.signer_name)
22
28
  end
@@ -1,6 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
+ require 'hexapdf/font/encoding'
4
5
  require 'hexapdf/font/encoding/base'
5
6
 
6
7
  describe HexaPDF::Font::Encoding::Base do
@@ -42,4 +43,23 @@ describe HexaPDF::Font::Encoding::Base do
42
43
  assert_nil(@base.code(:Unknown))
43
44
  end
44
45
  end
46
+
47
+ describe "to_compact_array" do
48
+ before do
49
+ @base.code_to_name[66] = :B
50
+ @base.code_to_name[67] = :C
51
+ @base.code_to_name[20] = :space
52
+ @base.code_to_name[28] = :D
53
+ @base.code_to_name[29] = :E
54
+ end
55
+
56
+ it "returns the difference array" do
57
+ assert_equal([20, :space, 28, :D, :E, 65, :A, :B, :C], @base.to_compact_array)
58
+ end
59
+
60
+ it "ignores the codes that are the same in the base encoding" do
61
+ std_encoding = HexaPDF::Font::Encoding.for_name(:StandardEncoding)
62
+ assert_equal([20, :space, 28, :D, :E, ], @base.to_compact_array(base_encoding: std_encoding))
63
+ end
64
+ end
45
65
  end
@@ -251,8 +251,23 @@ describe HexaPDF::Dictionary do
251
251
  refute(@obj.validate(auto_correct: false))
252
252
  assert(@obj.validate(auto_correct: true))
253
253
  @obj.value[:NameField] = "string"
254
+ refute(@obj.validate(auto_correct: false))
254
255
  assert(@obj.validate(auto_correct: true))
256
+
257
+ @test_class.define_field(:RequiredDefault, type: String, required: true, default: 'str')
258
+ @obj.value[:RequiredDefault] = 20
259
+ refute(@obj.validate(auto_correct: false))
260
+ assert_equal(20, @obj.value[:RequiredDefault])
255
261
  assert(@obj.validate(auto_correct: true))
262
+ assert_equal("str", @obj.value[:RequiredDefault])
263
+
264
+ @obj.value[:AllowedValues] = '20'
265
+ assert(@obj.validate(auto_correct: true))
266
+ refute(@obj.key?(:AllowedValues))
267
+
268
+ @obj.value[:Inherited] = 20
269
+ refute(@obj.validate(auto_correct: true))
270
+ refute(@obj.key?(:Inherited))
256
271
  end
257
272
 
258
273
  it "checks whether the value is an allowed one" do
@@ -347,7 +347,7 @@ describe HexaPDF::Document do
347
347
 
348
348
  it "validates the trailer object" do
349
349
  @doc.trailer[:ID] = :Symbol
350
- refute(@doc.validate {|_, _, obj| assert_same(@doc.trailer, obj) })
350
+ assert(@doc.validate {|_a, _b, obj| assert_same(@doc.trailer, obj) })
351
351
  end
352
352
 
353
353
  it "validates only loaded objects" do
@@ -391,7 +391,7 @@ describe HexaPDF::Document do
391
391
  end
392
392
 
393
393
  it "fails if the document is not valid" do
394
- @doc.trailer[:Size] = :Symbol
394
+ @doc.catalog[:PageLayout] = :invalid_value
395
395
  assert_raises(HexaPDF::Error) { @doc.write(StringIO.new(''.b)) }
396
396
  end
397
397
 
@@ -611,5 +611,6 @@ describe HexaPDF::Document do
611
611
  assert_equal(Encoding::ASCII_8BIT, str.encoding)
612
612
  doc = HexaPDF::Document.new(io: StringIO.new(str))
613
613
  assert_equal(:test, doc.trailer.info[:test])
614
+ assert_nil(doc.trailer.info[:ModDate])
614
615
  end
615
616
  end
@@ -181,7 +181,8 @@ describe HexaPDF::Serializer do
181
181
 
182
182
  it "encrypts strings in indirect PDF objects" do
183
183
  assert_serialized("(enc:1:test)", HexaPDF::Object.new("test", oid: 1))
184
- assert_serialized("<</x[(enc:1:test)]>>", HexaPDF::Object.new({x: ["test"]}, oid: 1))
184
+ assert_serialized("<</x[(enc:1:\xFE\xFF\x00t\x00e\x00s\x00t\x00\xF6)]>>".b,
185
+ HexaPDF::Object.new({x: ["testö"]}, oid: 1))
185
186
  end
186
187
 
187
188
  it "doesn't encrypt strings in direct PDF objects" do
@@ -52,6 +52,14 @@ describe HexaPDF::Type::Annotations::Widget do
52
52
  assert_kind_of(HexaPDF::Type::AcroForm::TextField, result)
53
53
  refute_same(@widget.data, result.data)
54
54
  end
55
+
56
+ it "works when the type of the field is defined higher up in the field hierarchy" do
57
+ @widget[:Parent] = {T: 'parent', Kids: [@widget]}
58
+ @widget[:Parent][:Parent] = {FT: :Tx, Kids: [@widget[:Parent]]}
59
+ result = @widget.form_field
60
+ assert_kind_of(HexaPDF::Type::AcroForm::TextField, result)
61
+ refute_same(@widget.data, result.data)
62
+ end
55
63
  end
56
64
 
57
65
  describe "background_color" do
@@ -67,6 +67,9 @@ describe HexaPDF::Type::Annotation do
67
67
  it "returns the appearance stream of the given type" do
68
68
  assert_nil(@annot.appearance)
69
69
 
70
+ @annot[:AP] = 'some invalid type'
71
+ assert_nil(@annot.appearance)
72
+
70
73
  @annot[:AP] = {N: {}}
71
74
  assert_nil(@annot.appearance)
72
75
 
@@ -143,5 +143,19 @@ describe HexaPDF::Type::FontType1 do
143
143
  @font[:Encoding] = :Other
144
144
  refute(@font.validate)
145
145
  end
146
+
147
+ it "works around certain invalid PDFs with a /SymbolEncoding value for /Encoding" do
148
+ @font[:Encoding] = :SymbolEncoding
149
+ @font[:BaseFont] = :Symbol
150
+ assert(@font.validate)
151
+ refute(@font.key?(:Encoding))
152
+ end
153
+
154
+ it "works around certain invalid PDFs with a /StandardEncoding value for /Encoding" do
155
+ @font[:Encoding] = :StandardEncoding
156
+ assert(@font.validate)
157
+ assert(:WinAnsiEncoding, @font[:Encoding][:BaseEncoding])
158
+ assert_equal([39, :quoteright, 96, :quoteleft], @font[:Encoding][:Differences][0, 4])
159
+ end
146
160
  end
147
161
  end
@@ -219,11 +219,21 @@ describe HexaPDF::Utils::SortedTreeNode do
219
219
  it "checks that leaf node containers have an even number of entries" do
220
220
  @kid11[:Names].delete_at(0)
221
221
  refute(@kid11.validate do |message, c|
222
- assert_match(/odd number/, message)
222
+ assert_match(/leaf.*odd number/, message)
223
223
  refute(c)
224
224
  end)
225
225
  end
226
226
 
227
+ it "corrects a root node container with an odd number of entries" do
228
+ @root.value.clear
229
+ @root[:Names] = ['Test']
230
+ assert(@root.validate do |message, c|
231
+ assert_match(/root.*odd number/, message)
232
+ assert(c)
233
+ end)
234
+ assert(@root[:Names].empty?)
235
+ end
236
+
227
237
  it "checks that the keys are of the correct type" do
228
238
  @kid11[:Names][2] = 5
229
239
  refute(@kid11.validate do |message, c|
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-03 00:00:00.000000000 Z
10
+ date: 2025-12-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: cmdparse