hexapdf 0.14.4 → 0.15.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/lib/hexapdf/cli/form.rb +30 -8
  4. data/lib/hexapdf/configuration.rb +18 -3
  5. data/lib/hexapdf/content/canvas.rb +1 -0
  6. data/lib/hexapdf/encryption/standard_security_handler.rb +16 -0
  7. data/lib/hexapdf/error.rb +4 -3
  8. data/lib/hexapdf/parser.rb +18 -6
  9. data/lib/hexapdf/revision.rb +16 -0
  10. data/lib/hexapdf/type/acro_form.rb +1 -0
  11. data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
  12. data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
  13. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/form.rb +37 -0
  15. data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
  16. data/lib/hexapdf/type/annotation.rb +18 -9
  17. data/lib/hexapdf/type/annotations/widget.rb +3 -1
  18. data/lib/hexapdf/type/font_descriptor.rb +9 -2
  19. data/lib/hexapdf/type/page.rb +81 -0
  20. data/lib/hexapdf/utils/graphics_helpers.rb +4 -4
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/test/hexapdf/content/test_canvas.rb +21 -0
  23. data/test/hexapdf/encryption/test_standard_security_handler.rb +27 -0
  24. data/test/hexapdf/test_parser.rb +23 -3
  25. data/test/hexapdf/test_revision.rb +21 -0
  26. data/test/hexapdf/test_writer.rb +2 -2
  27. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
  28. data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
  29. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  30. data/test/hexapdf/type/acro_form/test_form.rb +46 -2
  31. data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
  32. data/test/hexapdf/type/annotations/test_widget.rb +2 -0
  33. data/test/hexapdf/type/test_annotation.rb +24 -10
  34. data/test/hexapdf/type/test_font_descriptor.rb +7 -0
  35. data/test/hexapdf/type/test_page.rb +187 -49
  36. data/test/hexapdf/utils/test_graphics_helpers.rb +8 -0
  37. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 958692ab2c53f74fe599c0ba8c9c046aa41b38d2bf840a47dec8f0e258fd86e0
4
- data.tar.gz: b41d46ccb39d36d351cc143ba0afc145e785307b2a7ae90bc0f32d1ab76949af
3
+ metadata.gz: 6901d1281fa7f0585e2fe02b27985c9b7ff26770015902566c72ea62f5398e10
4
+ data.tar.gz: 4b1a82d17d4d1144b47dbb713552bb5e977896b94210a54c7b83871399d393ac
5
5
  SHA512:
6
- metadata.gz: c16e231aeeb12b55daf75a28a8e3918f6807127257655f42d1f3433c76cb25f0ea42daafc8dd20bd37099f9ef5f6f2a6a3e3b2468acf000c041452a32908e1ee
7
- data.tar.gz: a9a1fff7c7ff699c2b48333d89fae1aa67cd7ce61003845f76e4855cfc2f2ad5969ae668359091cd3fa225ea9a8b99fb9883e28b07fe6fcd985c7f83b842969d
6
+ metadata.gz: 1d4acb6e9f867195e998b3fd900102013a8d4b74c576f3c3243225a515bbcf2be8b62852db95cbcd9213ec06334b174fba4db406dfb8e228083d9a527eb8a5a1
7
+ data.tar.gz: 3306a678655f59c35b3349cd247606ca7e45a34e033dd9b9b3bad33c20a3fda95ba07bff88acdb7c5666c23b1ab63d293963e68db7e12371c56106645df7d9bb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,71 @@
1
+ ## 0.15.4 - 2021-05-27
2
+
3
+ ### Fixed
4
+
5
+ * [HexaPDF::Type::Annotation#appearance] to handle cases where there is
6
+ no valid appearance stream
7
+
8
+
9
+ ## 0.15.3 - 2021-05-01
10
+
11
+ ### Fixed
12
+
13
+ * Handling of general (not document-level), unencrypted metadata streams
14
+
15
+
16
+ ## 0.15.2 - 2021-05-01
17
+
18
+ ### Fixed
19
+
20
+ * Handling of unencrypted metadata streams
21
+
22
+
23
+ ## 0.15.1 - 2021-04-15
24
+
25
+ ### Fixed
26
+
27
+ * Potential division by zero when calculating the scaling for XObjects
28
+ * Handling of XObjects with a width or height of zero when drawing on canvas
29
+
30
+
31
+ ## 0.15.0 - 2021-04-12
32
+
33
+ ### Added
34
+
35
+ * [HexaPDF::Type::Page#flatten_annotations] for flattening the annotations of a
36
+ page
37
+ * [HexaPDF::Type::AcroForm::Form#flatten] for flattening interactive forms
38
+ * [HexaPDF::Revision#update] for updating the stored wrapper class of a PDF
39
+ object
40
+ * [HexaPDF::Type::AcroForm::SignatureField] for working with AcroForm signature
41
+ fields
42
+ * Support for form field flattening to the `hexapdf form` CLI command
43
+
44
+ ### Changed
45
+
46
+ * **Breaking change**: Overhauled the interface for accessing appearances of
47
+ annotations to make it more convenient
48
+ * Validation of [HexaPDF::Type::FontDescriptor] to delete invalid `/FontWeight`
49
+ value
50
+ * [HexaPDF::MalformedPDFError#pos] an accessor instead of a reader and update
51
+ the exception message
52
+ * Configuration option 'acro_form.fallback_font' to allow a callable object for
53
+ more advanced fallback font handling
54
+
55
+ ### Fixed
56
+
57
+ * [HexaPDF::Type::Annotations::Widget#background_color] to correctly handle
58
+ empty background color arrays
59
+ * [HexaPDF::Type::AcroForm::Field#delete_widget] to update the wrapper object
60
+ stored in the document in case the widget is embedded
61
+ * Processing of invalid PDF files containing a space,CR,LF combination after
62
+ the 'stream' keyword
63
+ * Cross-reference stream reconstruction with respect to detection of linearized
64
+ files
65
+ * Detection of existing appearances for AcroForm push button fields when
66
+ creating appearances
67
+
68
+
1
69
  ## 0.14.4 - 2021-02-27
2
70
 
3
71
  ### Added
@@ -52,18 +52,26 @@ module HexaPDF
52
52
  If the the output file name is not given, all form fields are listed in page order. Use
53
53
  the global --verbose option to show additional information like field type and location.
54
54
 
55
- If the output file name is given, the fields can be interactively filled out. By
56
- additionally using the --template option, the data for the fields is read from the given
57
- template file instead of the standard input.
55
+ If the output file name is given, the fields can be filled out interactively, via a
56
+ template or just flattened by using the respective options. Form field flattening can also
57
+ be activated in addition to filling out the form. If neither --fill, --template nor
58
+ --flatten is specified, --fill is implied.
58
59
  EOF
59
60
 
60
61
  options.on("--password PASSWORD", "-p", String,
61
62
  "The password for decryption. Use - for reading from standard input.") do |pwd|
62
63
  @password = (pwd == '-' ? read_password : pwd)
63
64
  end
65
+ options.on("--fill", "Fill out the form") do
66
+ @fill = true
67
+ end
64
68
  options.on("--template TEMPLATE_FILE", "-t TEMPLATE_FILE",
65
- "Use the template file for the field values") do |template|
69
+ "Use the template file for the field values (implies --fill)") do |template|
66
70
  @template = template
71
+ @fill = true
72
+ end
73
+ options.on('--flatten', 'Flatten the form fields') do
74
+ @flatten = true
67
75
  end
68
76
  options.on("--[no-]viewer-override", "Let the PDF viewer override the visual " \
69
77
  "appearance. Default: use setting from input PDF") do |need_appearances|
@@ -75,6 +83,8 @@ module HexaPDF
75
83
  end
76
84
 
77
85
  @password = nil
86
+ @fill = false
87
+ @flatten = false
78
88
  @template = nil
79
89
  @need_appearances = nil
80
90
  @incremental = true
@@ -82,16 +92,28 @@ module HexaPDF
82
92
 
83
93
  def execute(in_file, out_file = nil) #:nodoc:
84
94
  maybe_raise_on_existing_file(out_file) if out_file
95
+ if (@fill || @flatten) && !out_file
96
+ raise "Output file missing"
97
+ end
85
98
  with_document(in_file, password: @password, out_file: out_file,
86
99
  incremental: @incremental) do |doc|
87
100
  if !doc.acro_form
88
101
  raise "This PDF doesn't contain an interactive form"
89
102
  elsif out_file
90
103
  doc.acro_form[:NeedAppearances] = @need_appearances unless @need_appearances.nil?
91
- if @template
92
- fill_form_with_template(doc)
93
- else
94
- fill_form(doc)
104
+ if @fill || !@flatten
105
+ if @template
106
+ fill_form_with_template(doc)
107
+ else
108
+ fill_form(doc)
109
+ end
110
+ end
111
+ if @flatten
112
+ unless doc.acro_form.flatten.empty?
113
+ $stderr.puts "Warning: Not all form fields could be flattened"
114
+ doc.catalog.delete(:AcroForm)
115
+ doc.delete(doc.acro_form)
116
+ end
95
117
  end
96
118
  else
97
119
  list_form_fields(doc)
@@ -164,9 +164,20 @@ module HexaPDF
164
164
  # acro_form.fallback_font::
165
165
  # The font that should be used when a variable text field references a font that cannot be used.
166
166
  #
167
- # Can either be the name of a font, like 'Helvetica', or an array consisting of the font name
168
- # and a hash of font options, like ['Helvetica', variant: :italic]. If set to +nil+, the use of
169
- # the fallback font is disabled.
167
+ # Can be one of the following:
168
+ #
169
+ # * The name of a font, like 'Helvetica'.
170
+ #
171
+ # * An array consisting of the font name and a hash of font options, like ['Helvetica',
172
+ # variant: :italic].
173
+ #
174
+ # * A callable object receiving the field and the font object (or +nil+ if no valid font object
175
+ # was found) and which has to return either a font name or an array consisting of the font
176
+ # name and a hash of font options. This way the response can be different depending on the
177
+ # original font and it would also allow e.g. modifying the configured fonts to add custom
178
+ # ones.
179
+ #
180
+ # If set to +nil+, the use of the fallback font is disabled.
170
181
  #
171
182
  # Default is 'Helvetica'.
172
183
  #
@@ -516,6 +527,9 @@ module HexaPDF
516
527
  XXAcroFormField: 'HexaPDF::Type::AcroForm::Field',
517
528
  XXAppearanceDictionary: 'HexaPDF::Type::Annotation::AppearanceDictionary',
518
529
  Border: 'HexaPDF::Type::Annotation::Border',
530
+ SigFieldLock: 'HexaPDF::Type::AcroForm::SignatureField::LockDictionary',
531
+ SV: 'HexaPDF::Type::AcroForm::SignatureField::SeedValueDictionary',
532
+ SVCert: 'HexaPDF::Type::AcroForm::SignatureField::CertificateSeedValueDictionary',
519
533
  },
520
534
  'object.subtype_map' => {
521
535
  nil => {
@@ -561,6 +575,7 @@ module HexaPDF
561
575
  Tx: 'HexaPDF::Type::AcroForm::TextField',
562
576
  Btn: 'HexaPDF::Type::AcroForm::ButtonField',
563
577
  Ch: 'HexaPDF::Type::AcroForm::ChoiceField',
578
+ Sig: 'HexaPDF::Type::AcroForm::SignatureField',
564
579
  },
565
580
  })
566
581
 
@@ -1260,6 +1260,7 @@ module HexaPDF
1260
1260
  unless obj.kind_of?(HexaPDF::Stream)
1261
1261
  obj = context.document.images.add(obj)
1262
1262
  end
1263
+ return obj if obj.width == 0 || obj.height == 0
1263
1264
 
1264
1265
  width, height = calculate_dimensions(obj.width, obj.height,
1265
1266
  rwidth: width, rheight: height)
@@ -240,6 +240,22 @@ module HexaPDF
240
240
  end
241
241
  end
242
242
 
243
+ def decrypt(obj) #:nodoc:
244
+ if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata]
245
+ obj
246
+ else
247
+ super
248
+ end
249
+ end
250
+
251
+ def encrypt_stream(obj) #:nodoc
252
+ if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata]
253
+ obj.stream_encoder
254
+ else
255
+ super
256
+ end
257
+ end
258
+
243
259
  private
244
260
 
245
261
  # Prepares the security handler for use in encrypting the document.
data/lib/hexapdf/error.rb CHANGED
@@ -43,18 +43,19 @@ module HexaPDF
43
43
  class MalformedPDFError < Error
44
44
 
45
45
  # The byte position in the PDF file where the error occured.
46
- attr_reader :pos
46
+ attr_accessor :pos
47
47
 
48
48
  # Creates a new malformed PDF error object for the given exception message.
49
49
  #
50
- # The byte position where the error occured can be given via the +pos+ argument.
50
+ # The byte position where the error occured can either be given via the +pos+ argument or later
51
+ # via the #pos accessor but must be set before the exception message is retrieved.
51
52
  def initialize(message, pos: nil)
52
53
  super(message)
53
54
  @pos = pos
54
55
  end
55
56
 
56
57
  def message # :nodoc:
57
- "PDF malformed#{pos ? "around position #{pos}" : ''}: #{super}"
58
+ "PDF malformed around position #{pos}: #{super}"
58
59
  end
59
60
 
60
61
  end
@@ -140,11 +140,13 @@ module HexaPDF
140
140
  raise_malformed("A stream needs a dictionary, not a(n) #{object.class}", pos: offset)
141
141
  end
142
142
  tok1 = @tokenizer.next_byte
143
- tok2 = @tokenizer.next_byte if tok1 == 13 # 13=CR, 10=LF
143
+ if tok1 == 32 # space
144
+ maybe_raise("Keyword stream followed by space instead of LF or CR/LF", pos: @tokenizer.pos)
145
+ tok1 = @tokenizer.next_byte
146
+ end
147
+ tok2 = @tokenizer.next_byte if tok1 == 13 # CR
144
148
  if tok1 != 10 && tok1 != 13
145
- tok2 = @tokenizer.next_byte
146
- maybe_raise("Keyword stream must be followed by LF or CR/LF", pos: @tokenizer.pos,
147
- force: tok1 != 32 || (tok2 != 10 && tok2 != 13)) # 32=space
149
+ raise_malformed("Keyword stream must be followed by LF or CR/LF", pos: @tokenizer.pos)
148
150
  elsif tok1 == 13 && tok2 != 10
149
151
  maybe_raise("Keyword stream must be followed by LF or CR/LF, not CR alone",
150
152
  pos: @tokenizer.pos)
@@ -214,7 +216,12 @@ module HexaPDF
214
216
  unless obj.respond_to?(:xref_section)
215
217
  raise_malformed("Object is not a cross-reference stream", pos: pos)
216
218
  end
217
- xref_section = obj.xref_section
219
+ begin
220
+ xref_section = obj.xref_section
221
+ rescue MalformedPDFError => e
222
+ e.pos = pos
223
+ raise
224
+ end
218
225
  trailer = obj.trailer
219
226
  unless xref_section.entry?(obj.oid, obj.gen)
220
227
  maybe_raise("Cross-reference stream doesn't contain entry for itself", pos: pos)
@@ -401,6 +408,7 @@ module HexaPDF
401
408
 
402
409
  xref = XRefSection.new
403
410
  @tokenizer.pos = 0
411
+ linearized = nil
404
412
  while true
405
413
  @tokenizer.skip_whitespace
406
414
  pos = @tokenizer.pos
@@ -416,13 +424,17 @@ module HexaPDF
416
424
  @tokenizer.pos = next_new_line_pos
417
425
  elsif gen.kind_of?(Integer) && tok.kind_of?(Tokenizer::Token) && tok == 'obj'
418
426
  xref.add_in_use_entry(token, gen, pos)
427
+ if linearized.nil?
428
+ obj = @tokenizer.next_object rescue nil
429
+ linearized = obj.kind_of?(Hash) && obj.key?(:Linearized)
430
+ end
419
431
  @tokenizer.scan_until(/(?:\n|\r\n?)endobj\b/)
420
432
  end
421
433
  elsif token.kind_of?(Tokenizer::Token) && token == 'trailer'
422
434
  obj = @tokenizer.next_object rescue nil
423
435
  # Use last trailer found in case of multiple revisions but use first trailer in case of
424
436
  # linearized file.
425
- trailer = obj if obj.kind_of?(Hash) && (obj.key?(:Prev) || trailer.nil?)
437
+ trailer = obj if obj.kind_of?(Hash) && (!linearized || trailer.nil?)
426
438
  elsif token == Tokenizer::NO_MORE_TOKENS
427
439
  break
428
440
  else
@@ -158,6 +158,22 @@ module HexaPDF
158
158
  add_without_check(obj)
159
159
  end
160
160
 
161
+ # :call-seq:
162
+ # revision.update(obj) -> obj or nil
163
+ #
164
+ # Updates the stored object to point to the given HexaPDF::Object wrapper, returning the object
165
+ # if successful or +nil+ otherwise.
166
+ #
167
+ # If +obj+ isn't stored in this revision or the stored object doesn't contain the same
168
+ # HexaPDF::PDFData object as the given object, nothing is done.
169
+ #
170
+ # This method should only be used if the wrong wrapper class is stored (e.g. because
171
+ # auto-detection didn't or couldn't work correctly) and thus needs correction.
172
+ def update(obj)
173
+ return nil if object(obj)&.data != obj.data
174
+ add_without_check(obj)
175
+ end
176
+
161
177
  # :call-seq:
162
178
  # revision.delete(ref, mark_as_free: true)
163
179
  # revision.delete(oid, mark_as_free: true)
@@ -48,6 +48,7 @@ module HexaPDF
48
48
  autoload(:TextField, 'hexapdf/type/acro_form/text_field')
49
49
  autoload(:ButtonField, 'hexapdf/type/acro_form/button_field')
50
50
  autoload(:ChoiceField, 'hexapdf/type/acro_form/choice_field')
51
+ autoload(:SignatureField, 'hexapdf/type/acro_form/signature_field')
51
52
 
52
53
  autoload(:AppearanceGenerator, 'hexapdf/type/acro_form/appearance_generator')
53
54
 
@@ -120,7 +120,7 @@ module HexaPDF
120
120
  # widget.marker_style(style: :cross)
121
121
  # # => no visible rectangle, gray background, cross mark when checked
122
122
  def create_check_box_appearances
123
- unless @widget.appearance&.normal_appearance&.value&.size == 2
123
+ unless @widget.appearance_dict&.normal_appearance&.value&.size == 2
124
124
  raise HexaPDF::Error, "Widget of check box doesn't define name for on state"
125
125
  end
126
126
  border_style = @widget.border_style
@@ -128,11 +128,11 @@ module HexaPDF
128
128
 
129
129
  rect = update_widget(@field[:V], border_width)
130
130
 
131
- off_form = @widget.appearance.normal_appearance[:Off] =
131
+ off_form = @widget.appearance_dict.normal_appearance[:Off] =
132
132
  @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
133
133
  apply_background_and_border(border_style, off_form.canvas)
134
134
 
135
- on_form = @widget.appearance.normal_appearance[@field.check_box_on_name] =
135
+ on_form = @widget.appearance_dict.normal_appearance[@field.check_box_on_name] =
136
136
  @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
137
137
  canvas = on_form.canvas
138
138
  apply_background_and_border(border_style, canvas)
@@ -169,22 +169,22 @@ module HexaPDF
169
169
  # widget.marker_style(style: :circle, size: 0, color: 0)
170
170
  # # => default appearance
171
171
  def create_radio_button_appearances
172
- unless @widget.appearance&.normal_appearance&.value&.size == 2
172
+ unless @widget.appearance_dict&.normal_appearance&.value&.size == 2
173
173
  raise HexaPDF::Error, "Widget of radio button doesn't define unique name for on state"
174
174
  end
175
175
 
176
- on_name = (@widget.appearance.normal_appearance.value.keys - [:Off]).first
176
+ on_name = (@widget.appearance_dict.normal_appearance.value.keys - [:Off]).first
177
177
  border_style = @widget.border_style
178
178
  marker_style = @widget.marker_style
179
179
 
180
180
  rect = update_widget(@field[:V] == on_name ? on_name : :Off, border_style.width)
181
181
 
182
- off_form = @widget.appearance.normal_appearance[:Off] =
182
+ off_form = @widget.appearance_dict.normal_appearance[:Off] =
183
183
  @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
184
184
  apply_background_and_border(border_style, off_form.canvas,
185
185
  circular: marker_style.style == :circle)
186
186
 
187
- on_form = @widget.appearance.normal_appearance[on_name] =
187
+ on_form = @widget.appearance_dict.normal_appearance[on_name] =
188
188
  @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
189
189
  canvas = on_form.canvas
190
190
  apply_background_and_border(border_style, canvas,
@@ -219,17 +219,8 @@ module HexaPDF
219
219
  #
220
220
  # Note: Multiline, comb and rich text fields are currently not supported!
221
221
  def create_text_appearances
222
- font_name, font_size = @field.parse_default_appearance_string
223
222
  default_resources = @document.acro_form.default_resources
224
- font = default_resources.font(font_name).font_wrapper rescue nil
225
- unless font
226
- fallback_font_name, fallback_font_options = @document.config['acro_form.fallback_font']
227
- if fallback_font_name
228
- font = @document.fonts.add(fallback_font_name, **(fallback_font_options || {}))
229
- else
230
- raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
231
- end
232
- end
223
+ font, font_size = retrieve_font_information(default_resources)
233
224
  style = HexaPDF::Layout::Style.new(font: font)
234
225
  border_style = @widget.border_style
235
226
  padding = [1, border_style.width].max
@@ -482,6 +473,27 @@ module HexaPDF
482
473
  end
483
474
  end
484
475
 
476
+ # Returns the font wrapper and font size to be used for a variable text field.
477
+ def retrieve_font_information(resources)
478
+ font_name, font_size = @field.parse_default_appearance_string
479
+ font_object = resources.font(font_name) rescue nil
480
+ font = font_object&.font_wrapper
481
+ unless font
482
+ fallback_font = @document.config['acro_form.fallback_font']
483
+ fallback_font_name, fallback_font_options = if fallback_font.respond_to?(:call)
484
+ fallback_font.call(@field, font_object)
485
+ else
486
+ fallback_font
487
+ end
488
+ if fallback_font_name
489
+ font = @document.fonts.add(fallback_font_name, **(fallback_font_options || {}))
490
+ else
491
+ raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
492
+ end
493
+ end
494
+ [font, font_size]
495
+ end
496
+
485
497
  # Calculates the font size for text fields based on the font and font size of the default
486
498
  # appearance string, the annotation rectangle and the border style.
487
499
  def calculate_font_size(font, font_size, rect, border_style)
@@ -184,7 +184,7 @@ module HexaPDF
184
184
  #
185
185
  # Defaults to :Yes if no other name could be determined.
186
186
  def check_box_on_name
187
- each_widget.to_a.first&.appearance&.normal_appearance&.value&.each_key&.
187
+ each_widget.to_a.first&.appearance_dict&.normal_appearance&.value&.each_key&.
188
188
  find {|key| key != :Off } || :Yes
189
189
  end
190
190
 
@@ -192,7 +192,7 @@ module HexaPDF
192
192
  # button.
193
193
  def radio_button_values
194
194
  each_widget.map do |widget|
195
- widget.appearance&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
195
+ widget.appearance_dict&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
196
196
  end.compact
197
197
  end
198
198
 
@@ -233,7 +233,11 @@ module HexaPDF
233
233
  def create_appearances(force: false)
234
234
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
235
235
  each_widget do |widget|
236
- next if !force && widget.appearance?
236
+ normal_appearance = widget.appearance_dict&.normal_appearance
237
+ next if !force && normal_appearance &&
238
+ ((!push_button? && normal_appearance.value.length == 2 &&
239
+ normal_appearance.value.each_value.all?(HexaPDF::Stream)) ||
240
+ (push_button? && normal_appearance.kind_of?(HexaPDF::Stream)))
237
241
  if check_box?
238
242
  appearance_generator_class.new(widget).create_check_box_appearances
239
243
  elsif radio_button?
@@ -250,7 +254,7 @@ module HexaPDF
250
254
  create_appearances
251
255
  value = self[:V]
252
256
  each_widget do |widget|
253
- widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
257
+ widget[:AS] = (widget.appearance_dict&.normal_appearance&.key?(value) ? value : :Off)
254
258
  end
255
259
  end
256
260