hexapdf 0.12.1 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +130 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/command.rb +4 -2
  5. data/lib/hexapdf/cli/image2pdf.rb +2 -1
  6. data/lib/hexapdf/cli/info.rb +51 -2
  7. data/lib/hexapdf/cli/inspect.rb +30 -8
  8. data/lib/hexapdf/cli/merge.rb +1 -1
  9. data/lib/hexapdf/cli/split.rb +74 -14
  10. data/lib/hexapdf/configuration.rb +15 -0
  11. data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
  12. data/lib/hexapdf/content/parser.rb +1 -1
  13. data/lib/hexapdf/dictionary.rb +9 -6
  14. data/lib/hexapdf/dictionary_fields.rb +1 -9
  15. data/lib/hexapdf/document.rb +41 -16
  16. data/lib/hexapdf/document/files.rb +0 -1
  17. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  18. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  19. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  20. data/lib/hexapdf/font/cmap.rb +1 -4
  21. data/lib/hexapdf/font/true_type/subsetter.rb +12 -3
  22. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  23. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  24. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  25. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  26. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  27. data/lib/hexapdf/image_loader/png.rb +3 -2
  28. data/lib/hexapdf/importer.rb +3 -2
  29. data/lib/hexapdf/layout/line.rb +1 -1
  30. data/lib/hexapdf/layout/style.rb +23 -23
  31. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  32. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  33. data/lib/hexapdf/object.rb +52 -25
  34. data/lib/hexapdf/parser.rb +96 -4
  35. data/lib/hexapdf/pdf_array.rb +12 -5
  36. data/lib/hexapdf/revisions.rb +29 -21
  37. data/lib/hexapdf/serializer.rb +34 -8
  38. data/lib/hexapdf/task/optimize.rb +6 -4
  39. data/lib/hexapdf/tokenizer.rb +4 -3
  40. data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
  41. data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
  42. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  43. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  44. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  45. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  46. data/lib/hexapdf/type/actions/uri.rb +3 -2
  47. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  48. data/lib/hexapdf/type/catalog.rb +2 -2
  49. data/lib/hexapdf/type/cid_font.rb +1 -1
  50. data/lib/hexapdf/type/file_specification.rb +1 -1
  51. data/lib/hexapdf/type/font.rb +1 -1
  52. data/lib/hexapdf/type/font_simple.rb +4 -2
  53. data/lib/hexapdf/type/font_true_type.rb +6 -2
  54. data/lib/hexapdf/type/font_type0.rb +4 -4
  55. data/lib/hexapdf/type/form.rb +15 -2
  56. data/lib/hexapdf/type/image.rb +2 -2
  57. data/lib/hexapdf/type/page.rb +37 -13
  58. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  59. data/lib/hexapdf/type/resources.rb +1 -0
  60. data/lib/hexapdf/type/trailer.rb +2 -3
  61. data/lib/hexapdf/utils/object_hash.rb +0 -1
  62. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  63. data/lib/hexapdf/version.rb +1 -1
  64. data/test/hexapdf/common_tokenizer_tests.rb +6 -1
  65. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  66. data/test/hexapdf/content/test_canvas.rb +3 -3
  67. data/test/hexapdf/content/test_color_space.rb +1 -1
  68. data/test/hexapdf/encryption/test_aes.rb +4 -4
  69. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  70. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  71. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  72. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  73. data/test/hexapdf/font/true_type/test_subsetter.rb +5 -0
  74. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  75. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  76. data/test/hexapdf/layout/test_style.rb +1 -1
  77. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  78. data/test/hexapdf/test_configuration.rb +2 -2
  79. data/test/hexapdf/test_dictionary.rb +8 -1
  80. data/test/hexapdf/test_dictionary_fields.rb +2 -2
  81. data/test/hexapdf/test_document.rb +18 -10
  82. data/test/hexapdf/test_object.rb +71 -26
  83. data/test/hexapdf/test_parser.rb +171 -53
  84. data/test/hexapdf/test_pdf_array.rb +8 -1
  85. data/test/hexapdf/test_revisions.rb +35 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
  89. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  90. data/test/hexapdf/type/acro_form/test_field.rb +39 -0
  91. data/test/hexapdf/type/acro_form/test_form.rb +87 -15
  92. data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
  93. data/test/hexapdf/type/test_font_simple.rb +2 -1
  94. data/test/hexapdf/type/test_font_true_type.rb +6 -0
  95. data/test/hexapdf/type/test_form.rb +26 -1
  96. data/test/hexapdf/type/test_page.rb +45 -7
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  99. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  100. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  101. data/test/test_helper.rb +2 -0
  102. metadata +6 -11
@@ -67,30 +67,38 @@ module HexaPDF
67
67
  object_loader = lambda {|xref_entry| parser.load_object(xref_entry) }
68
68
 
69
69
  revisions = []
70
- xref_section, trailer = parser.load_revision(parser.startxref_offset)
71
- revisions << Revision.new(document.wrap(trailer, type: :XXTrailer),
72
- xref_section: xref_section, loader: object_loader)
73
- seen_xref_offsets = {parser.startxref_offset => true}
74
-
75
- while (prev = revisions[0].trailer.value[:Prev]) &&
76
- !seen_xref_offsets.key?(prev)
77
- # PDF1.7 s7.5.5 states that :Prev needs to be indirect, Adobe's reference 3.4.4 says it
78
- # should be direct. Adobe's POV is followed here. Same with :XRefStm.
79
- xref_section, trailer = parser.load_revision(prev)
80
- seen_xref_offsets[prev] = true
81
-
82
- stm = revisions[0].trailer.value[:XRefStm]
83
- if stm && !seen_xref_offsets.key?(stm)
84
- stm_xref_section, = parser.load_revision(stm)
85
- xref_section.merge!(stm_xref_section)
86
- seen_xref_offsets[stm] = true
70
+ begin
71
+ xref_section, trailer = parser.load_revision(parser.startxref_offset)
72
+ revisions << Revision.new(document.wrap(trailer, type: :XXTrailer),
73
+ xref_section: xref_section, loader: object_loader)
74
+ seen_xref_offsets = {parser.startxref_offset => true}
75
+
76
+ while (prev = revisions[0].trailer.value[:Prev]) &&
77
+ !seen_xref_offsets.key?(prev)
78
+ # PDF1.7 s7.5.5 states that :Prev needs to be indirect, Adobe's reference 3.4.4 says it
79
+ # should be direct. Adobe's POV is followed here. Same with :XRefStm.
80
+ xref_section, trailer = parser.load_revision(prev)
81
+ seen_xref_offsets[prev] = true
82
+
83
+ stm = revisions[0].trailer.value[:XRefStm]
84
+ if stm && !seen_xref_offsets.key?(stm)
85
+ stm_xref_section, = parser.load_revision(stm)
86
+ xref_section.merge!(stm_xref_section)
87
+ seen_xref_offsets[stm] = true
88
+ end
89
+
90
+ revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
91
+ xref_section: xref_section, loader: object_loader))
87
92
  end
88
-
89
- revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
90
- xref_section: xref_section, loader: object_loader))
93
+ rescue HexaPDF::MalformedPDFError
94
+ reconstructed_revision = parser.reconstructed_revision
95
+ unless revisions.empty?
96
+ reconstructed_revision.trailer.data.value = revisions.last.trailer.data.value
97
+ end
98
+ revisions << reconstructed_revision
91
99
  end
92
100
 
93
- document.version = parser.file_header_version
101
+ document.version = parser.file_header_version rescue '1.0'
94
102
  new(document, initial_revisions: revisions, parser: parser)
95
103
  end
96
104
 
@@ -88,13 +88,39 @@ module HexaPDF
88
88
 
89
89
  # Creates a new Serializer object.
90
90
  def initialize
91
- @dispatcher = Hash.new do |h, klass|
92
- method = nil
93
- klass.ancestors.each do |ancestor_klass|
94
- method = "serialize_#{ancestor_klass.name.to_s.downcase.gsub(/::/, '_')}"
95
- (h[klass] = method; break) if respond_to?(method, true)
96
- end
97
- method
91
+ @dispatcher = {
92
+ Hash => 'serialize_hash',
93
+ Array => 'serialize_array',
94
+ Symbol => 'serialize_symbol',
95
+ String => 'serialize_string',
96
+ Integer => 'serialize_integer',
97
+ Float => 'serialize_float',
98
+ Time => 'serialize_time',
99
+ TrueClass => 'serialize_trueclass',
100
+ FalseClass => 'serialize_falseclass',
101
+ NilClass => 'serialize_nilclass',
102
+ HexaPDF::Reference => 'serialize_hexapdf_reference',
103
+ HexaPDF::Object => 'serialize_hexapdf_object',
104
+ HexaPDF::Stream => 'serialize_hexapdf_stream',
105
+ HexaPDF::Dictionary => 'serialize_hexapdf_object',
106
+ HexaPDF::PDFArray => 'serialize_hexapdf_object',
107
+ HexaPDF::Rectangle => 'serialize_hexapdf_object',
108
+ }
109
+ @dispatcher.default_proc = lambda do |h, klass|
110
+ h[klass] = if klass <= HexaPDF::Stream
111
+ "serialize_hexapdf_stream"
112
+ elsif klass <= HexaPDF::Object
113
+ "serialize_hexapdf_object"
114
+ else
115
+ method = nil
116
+ klass.ancestors.each do |ancestor_klass|
117
+ name = ancestor_klass.name.to_s.downcase
118
+ name.gsub!(/::/, '_')
119
+ method = "serialize_#{name}"
120
+ break if respond_to?(method, true)
121
+ end
122
+ method
123
+ end
98
124
  end
99
125
  @encrypter = false
100
126
  @io = nil
@@ -243,7 +269,7 @@ module HexaPDF
243
269
  else
244
270
  obj.dup
245
271
  end
246
- obj.gsub!(/[\(\)\\\r]/n, STRING_ESCAPE_MAP)
272
+ obj.gsub!(/[()\\\r]/n, STRING_ESCAPE_MAP)
247
273
  "(#{obj})"
248
274
  end
249
275
 
@@ -129,9 +129,10 @@ module HexaPDF
129
129
  xref_stream = false
130
130
  objects_to_delete = []
131
131
  rev.each do |obj|
132
- if obj.type == :ObjStm
132
+ case obj.type
133
+ when :ObjStm
133
134
  objects_to_delete << obj
134
- elsif obj.type == :XRef
135
+ when :XRef
135
136
  xref_stream = true
136
137
  objects_to_delete << obj if xref_streams == :delete
137
138
  else
@@ -150,9 +151,10 @@ module HexaPDF
150
151
  objstms = [doc.wrap({Type: :ObjStm})]
151
152
  old_objstms = []
152
153
  rev.each do |obj|
153
- if obj.type == :XRef
154
+ case obj.type
155
+ when :XRef
154
156
  xref_stream = true
155
- elsif obj.type == :ObjStm
157
+ when :ObjStm
156
158
  old_objstms << obj
157
159
  end
158
160
  delete_fields_with_defaults(obj)
@@ -249,17 +249,18 @@ module HexaPDF
249
249
  #
250
250
  # See: PDF1.7 s7.3.3
251
251
  def parse_number
252
- if (val = @ss.scan(/[+-]?\d++(?!\.)/))
252
+ val = scan_until(WHITESPACE_OR_DELIMITER_RE) || @ss.scan(/.*/)
253
+ if val.match?(/\A[+-]?\d++(?!\.)\z/)
253
254
  tmp = val.to_i
254
255
  # Handle object references, see PDF1.7 s7.3.10
255
256
  prepare_string_scanner(10)
256
257
  tmp = Reference.new(tmp, @ss[1].to_i) if @ss.scan(REFERENCE_RE)
257
258
  tmp
258
- elsif (val = @ss.scan(/[+-]?(?:\d+\.\d*|\.\d+)/))
259
+ elsif val.match?(/\A[+-]?(?:\d+\.\d*|\.\d+)\z/)
259
260
  val << '0' if val.getbyte(-1) == 46 # dot '.'
260
261
  Float(val)
261
262
  else
262
- parse_keyword
263
+ TOKEN_CACHE[val] # val is keyword
263
264
  end
264
265
  end
265
266
 
@@ -37,6 +37,7 @@
37
37
  require 'hexapdf/error'
38
38
  require 'hexapdf/layout/style'
39
39
  require 'hexapdf/layout/text_fragment'
40
+ require 'hexapdf/layout/text_layouter'
40
41
 
41
42
  module HexaPDF
42
43
  module Type
@@ -80,14 +81,8 @@ module HexaPDF
80
81
  else
81
82
  raise HexaPDF::Error, "Unsupported button field type"
82
83
  end
83
- when :Tx
84
+ when :Tx, :Ch
84
85
  create_text_appearances
85
- when :Ch
86
- if @field.combo_box?
87
- create_text_appearances
88
- else
89
- raise HexaPDF::Error, "List box not supported yet"
90
- end
91
86
  else
92
87
  raise HexaPDF::Error, "Unsupported field type #{@field.field_type}"
93
88
  end
@@ -206,6 +201,10 @@ module HexaPDF
206
201
  # * The font, font size and font color are taken from the associated field's default
207
202
  # appearance string. See VariableTextField.
208
203
  #
204
+ # If the font is not usable by HexaPDF (which may be due to a variety of reasons, e.g. no
205
+ # associated information in the form's default resources), the font specified by the
206
+ # configuration option +acro_form.fallback_font+ will be used.
207
+ #
209
208
  # * The widget's rectangle /Rect must be defined. If the height is zero, it is auto-sized
210
209
  # based on the font size. If additionally the font size is zero, a font size of
211
210
  # +acro_form.default_font_size+ is used. If the width is zero, the
@@ -222,7 +221,7 @@ module HexaPDF
222
221
  def create_text_appearances
223
222
  font_name, font_size = @field.parse_default_appearance_string
224
223
  default_resources = @document.acro_form.default_resources
225
- font = default_resources.font(font_name).font_wrapper
224
+ font = default_resources.font(font_name).font_wrapper rescue nil
226
225
  unless font
227
226
  fallback_font_name, fallback_font_options = @document.config['acro_form.fallback_font']
228
227
  if fallback_font_name
@@ -245,38 +244,35 @@ module HexaPDF
245
244
  rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
246
245
  end
247
246
 
248
- form = (@widget[:AP] ||= {})[:N] = @document.add({Type: :XObject, Subtype: :Form,
249
- BBox: [0, 0, rect.width, rect.height]})
247
+ form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
248
+ form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
249
+ form.contents = ''
250
250
  form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
251
251
 
252
252
  canvas = form.canvas
253
253
  apply_background_and_border(border_style, canvas)
254
254
  style.font_size = calculate_font_size(font, font_size, rect, border_style)
255
+ style.clear_cache
255
256
 
256
257
  canvas.marked_content_sequence(:Tx) do
257
- if (value = @field.field_value)
258
+ if @field.field_value || @field.concrete_field_type == :list_box
258
259
  canvas.save_graphics_state do
259
260
  canvas.rectangle(padding, padding, rect.width - 2 * padding,
260
261
  rect.height - 2 * padding).clip_path.end_path
261
- fragment = HexaPDF::Layout::TextFragment.create(value, style)
262
- # Adobe seems to be left/right-aligning based on twice the border width and
263
- # vertically centering based on the cap height, if enough space is available
264
- x = case @field.text_alignment
265
- when :left then 2 * padding
266
- when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
267
- when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
268
- end
269
- cap_height = font.wrapped_font.cap_height * font.scaling_factor / 1000.0 *
270
- style.font_size
271
- y = padding + (rect.height - 2 * padding - cap_height) / 2.0
272
- y = padding - style.scaled_font_descender if y < 0
273
- fragment.draw(canvas, x, y)
262
+ if @field.concrete_field_type == :multiline_text_field
263
+ draw_multiline_text(canvas, rect, style, padding)
264
+ elsif @field.concrete_field_type == :list_box
265
+ draw_list_box(canvas, rect, style, padding)
266
+ else
267
+ draw_single_line_text(canvas, rect, style, padding)
268
+ end
274
269
  end
275
270
  end
276
271
  end
277
272
  end
278
273
 
279
274
  alias create_combo_box_appearances create_text_appearances
275
+ alias create_list_box_appearances create_text_appearances
280
276
 
281
277
  private
282
278
 
@@ -337,6 +333,13 @@ module HexaPDF
337
333
  canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
338
334
  else
339
335
  canvas.rectangle(offset, offset, width, height)
336
+ if @field.concrete_field_type == :comb_text_field
337
+ cell_width = rect.width.to_f / @field[:MaxLen]
338
+ 1.upto(@field[:MaxLen] - 1) do |i|
339
+ canvas.line(i * cell_width, border_style.width,
340
+ i * cell_width, border_style.width + height)
341
+ end
342
+ end
340
343
  end
341
344
  end
342
345
  canvas.stroke
@@ -381,14 +384,115 @@ module HexaPDF
381
384
  end
382
385
  end
383
386
 
387
+ # Draws a single line of text inside the widget's rectangle.
388
+ def draw_single_line_text(canvas, rect, style, padding)
389
+ value = @field.field_value
390
+ fragment = HexaPDF::Layout::TextFragment.create(value, style)
391
+
392
+ if @field.concrete_field_type == :comb_text_field
393
+ unless @field.key?(:MaxLen)
394
+ raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
395
+ end
396
+ new_items = []
397
+ cell_width = rect.width.to_f / @field[:MaxLen]
398
+ scaled_cell_width = cell_width / style.scaled_font_size.to_f
399
+ fragment.items.each_cons(2) do |a, b|
400
+ new_items << a << -(scaled_cell_width - a.width / 2.0 - b.width / 2.0)
401
+ end
402
+ new_items << fragment.items.last
403
+ fragment.items.replace(new_items)
404
+ fragment.clear_cache
405
+ # Adobe always seems to add 1 to the first offset...
406
+ x_offset = 1 + (cell_width - style.scaled_item_width(fragment.items[0])) / 2.0
407
+ x = case @field.text_alignment
408
+ when :left then x_offset
409
+ when :right then x_offset + cell_width * (@field[:MaxLen] - value.length)
410
+ when :center then x_offset + cell_width * ((@field[:MaxLen] - value.length) / 2)
411
+ end
412
+ else
413
+ # Adobe seems to be left/right-aligning based on twice the border width
414
+ x = case @field.text_alignment
415
+ when :left then 2 * padding
416
+ when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
417
+ when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
418
+ end
419
+ end
420
+
421
+ # Adobe seems to be vertically centering based on the cap height, if enough space is
422
+ # available
423
+ cap_height = style.font.wrapped_font.cap_height * style.font.scaling_factor / 1000.0 *
424
+ style.font_size
425
+ y = padding + (rect.height - 2 * padding - cap_height) / 2.0
426
+ y = padding - style.scaled_font_descender if y < 0
427
+ fragment.draw(canvas, x, y)
428
+ end
429
+
430
+ # Draws multiple lines of text inside the widget's rectangle.
431
+ def draw_multiline_text(canvas, rect, style, padding)
432
+ items = [Layout::TextFragment.create(@field.field_value, style)]
433
+ layouter = Layout::TextLayouter.new(style)
434
+ layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
435
+
436
+ result = nil
437
+ if style.font_size == 0 # need to auto-size text
438
+ style.font_size = 12 # Adobe seems to use this as starting point
439
+ style.clear_cache
440
+ loop do
441
+ result = layouter.fit(items, rect.width - 4 * padding, rect.height - 4 * padding)
442
+ break if result.status == :success || style.font_size <= 4 # don't make text too small
443
+ style.font_size -= 1
444
+ style.clear_cache
445
+ end
446
+ else
447
+ result = layouter.fit(items, rect.width - 4 * padding, 2**20)
448
+ end
449
+
450
+ unless result.lines.empty?
451
+ result.draw(canvas, 2 * padding, rect.height - 2 * padding - result.lines[0].height / 2.0)
452
+ end
453
+ end
454
+
455
+ # Draws the visible option items of the list box in the widget's rectangle.
456
+ def draw_list_box(canvas, rect, style, padding)
457
+ option_items = @field.option_items
458
+ top_index = @field.list_box_top_index
459
+ items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
460
+
461
+ indices = @field[:I] || []
462
+ value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
463
+ indices = value_indices if indices != value_indices
464
+
465
+ layouter = Layout::TextLayouter.new(style)
466
+ layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
467
+ result = layouter.fit(items, rect.width - 4 * padding, rect.height)
468
+
469
+ unless result.lines.empty?
470
+ top_gap = style.line_spacing.gap(result.lines[0], result.lines[0])
471
+ line_height = style.line_spacing.baseline_distance(result.lines[0], result.lines[0])
472
+ canvas.fill_color(153, 193, 218) # Adobe's color for selection highlighting
473
+ indices.map! {|i| rect.height - padding - (i - top_index + 1) * line_height }.each do |y|
474
+ next if y + line_height > rect.height || y + line_height < padding
475
+ canvas.rectangle(padding, y, rect.width - 2 * padding, line_height)
476
+ end
477
+ canvas.fill if canvas.graphics_object == :path
478
+ result.draw(canvas, 2 * padding, rect.height - padding - top_gap)
479
+ end
480
+ end
481
+
384
482
  # Calculates the font size for text fields based on the font and font size of the default
385
483
  # appearance string, the annotation rectangle and the border style.
386
484
  def calculate_font_size(font, font_size, rect, border_style)
387
485
  if font_size == 0
388
- unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
389
- font.scaling_factor / 1000.0
390
- # The constant factor was found empirically by checking what Adobe Reader etc. do
391
- (rect.height - 2 * border_style.width) / unit_font_size * 0.83
486
+ if @field.concrete_field_type == :multiline_text_field
487
+ 0 # Handled by multiline drawing code
488
+ elsif @field.concrete_field_type == :list_box
489
+ 12 # Seems to be Adobe's default
490
+ else
491
+ unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
492
+ font.scaling_factor / 1000.0
493
+ # The constant factor was found empirically by checking what Adobe Reader etc. do
494
+ (rect.height - 2 * border_style.width) / unit_font_size * 0.83
495
+ end
392
496
  else
393
497
  font_size
394
498
  end
@@ -134,8 +134,8 @@ module HexaPDF
134
134
  # Check boxes:: For check boxes that are in the on state the value +true+ is returned.
135
135
  # Otherwise +false+ is returned.
136
136
  #
137
- # Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the name of
138
- # the specific radio button that is selected is returned.
137
+ # Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the value (a
138
+ # Symbol) of the specific radio button that is selected is returned.
139
139
  def field_value
140
140
  normalized_field_value(:V)
141
141
  end
@@ -149,7 +149,8 @@ module HexaPDF
149
149
  # +false+ for unchecking it.
150
150
  #
151
151
  # Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
152
- # the name of a radio button that should be turned on.
152
+ # the value (a Symbol or an object responding to +#to_sym+) of a radio
153
+ # button that should be turned on.
153
154
  def field_value=(value)
154
155
  normalized_field_value_set(:V, value)
155
156
  end
@@ -179,7 +180,7 @@ module HexaPDF
179
180
  end
180
181
  end
181
182
 
182
- # Returns the name used for setting the check box to the on state.
183
+ # Returns the name (a Symbol) used for setting the check box to the on state.
183
184
  #
184
185
  # Defaults to :Yes if no other name could be determined.
185
186
  def check_box_on_name
@@ -187,7 +188,8 @@ module HexaPDF
187
188
  find {|key| key != :Off } || :Yes
188
189
  end
189
190
 
190
- # Returns the array of values that can be used for the field value of the radio button.
191
+ # Returns the array of Symbol values that can be used for the field value of the radio
192
+ # button.
191
193
  def radio_button_values
192
194
  each_widget.map do |widget|
193
195
  widget.appearance&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
@@ -200,8 +202,8 @@ module HexaPDF
200
202
  # default appearance.
201
203
  #
202
204
  # If the widget is created for a radio button field, the +value+ argument needs to set to
203
- # the value (a symbol) this widget represents. It can be used with #field_value= to set this
204
- # specific widget of the radio button set to on.
205
+ # the value (a Symbol or an object responding to +#to_sym+) this widget represents. It can
206
+ # be used with #field_value= to set this specific widget of the radio button set to on.
205
207
  #
206
208
  # See: Field#create_widget, AppearanceGenerator button field methods
207
209
  def create_widget(page, defaults: true, value: nil, **values)
@@ -209,8 +211,11 @@ module HexaPDF
209
211
  if check_box?
210
212
  widget[:AP] = {N: {Yes: nil, Off: nil}}
211
213
  elsif radio_button?
212
- raise ArgumentError, "Argument value has to be provided for radio buttons" unless value
213
- widget[:AP] = {N: {value => nil, Off: nil}}
214
+ unless value.respond_to?(:to_sym)
215
+ raise ArgumentError, "Argument 'value' has to be provided for radio buttons " \
216
+ "and needs to respond to #to_sym"
217
+ end
218
+ widget[:AP] = {N: {value.to_sym => nil, Off: nil}}
214
219
  end
215
220
  next unless defaults
216
221
  widget.border_style(color: 0, width: 1, style: (push_button? ? :beveled : :solid))
@@ -223,10 +228,12 @@ module HexaPDF
223
228
  #
224
229
  # The created appearance streams depend on the actual type of the button field. See
225
230
  # AppearanceGenerator for the details.
226
- def create_appearances
231
+ #
232
+ # By setting +force+ to +true+ the creation of the appearances can be forced.
233
+ def create_appearances(force: false)
227
234
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
228
235
  each_widget do |widget|
229
- next if widget.appearance?
236
+ next if !force && widget.appearance?
230
237
  if check_box?
231
238
  appearance_generator_class.new(widget).create_check_box_appearances
232
239
  elsif radio_button?
@@ -240,6 +247,7 @@ module HexaPDF
240
247
  # Updates the widgets so that they reflect the current field value.
241
248
  def update_widgets
242
249
  return if push_button?
250
+ create_appearances
243
251
  value = self[:V]
244
252
  each_widget do |widget|
245
253
  widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
@@ -271,8 +279,8 @@ module HexaPDF
271
279
  value == true ? check_box_on_name : :Off
272
280
  elsif value.nil?
273
281
  :Off
274
- elsif radio_button_values.include?(value)
275
- value
282
+ elsif radio_button_values.include?(value.to_sym)
283
+ value.to_sym
276
284
  else
277
285
  @document.config['acro_form.on_invalid_value'].call(self, value)
278
286
  end