hexapdf 0.12.3 → 0.14.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -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/dictionary.rb +12 -6
  13. data/lib/hexapdf/dictionary_fields.rb +2 -10
  14. data/lib/hexapdf/document.rb +41 -16
  15. data/lib/hexapdf/document/files.rb +0 -1
  16. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  17. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  19. data/lib/hexapdf/font/cmap.rb +1 -4
  20. data/lib/hexapdf/font/true_type/subsetter.rb +16 -3
  21. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  22. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  23. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  24. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  25. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  26. data/lib/hexapdf/image_loader/png.rb +3 -2
  27. data/lib/hexapdf/importer.rb +3 -2
  28. data/lib/hexapdf/layout/line.rb +1 -1
  29. data/lib/hexapdf/layout/style.rb +23 -23
  30. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  31. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  32. data/lib/hexapdf/object.rb +52 -25
  33. data/lib/hexapdf/parser.rb +107 -7
  34. data/lib/hexapdf/pdf_array.rb +15 -5
  35. data/lib/hexapdf/revisions.rb +29 -21
  36. data/lib/hexapdf/serializer.rb +37 -10
  37. data/lib/hexapdf/task/optimize.rb +6 -4
  38. data/lib/hexapdf/tokenizer.rb +22 -0
  39. data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
  40. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  41. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  42. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  43. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  44. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  45. data/lib/hexapdf/type/actions/uri.rb +3 -2
  46. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  47. data/lib/hexapdf/type/catalog.rb +2 -2
  48. data/lib/hexapdf/type/cid_font.rb +1 -1
  49. data/lib/hexapdf/type/file_specification.rb +1 -1
  50. data/lib/hexapdf/type/font.rb +1 -1
  51. data/lib/hexapdf/type/font_simple.rb +4 -2
  52. data/lib/hexapdf/type/font_true_type.rb +6 -2
  53. data/lib/hexapdf/type/font_type0.rb +4 -4
  54. data/lib/hexapdf/type/form.rb +6 -2
  55. data/lib/hexapdf/type/image.rb +2 -2
  56. data/lib/hexapdf/type/page.rb +21 -12
  57. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  58. data/lib/hexapdf/type/resources.rb +5 -0
  59. data/lib/hexapdf/type/trailer.rb +2 -3
  60. data/lib/hexapdf/utils/object_hash.rb +0 -1
  61. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  62. data/lib/hexapdf/version.rb +1 -1
  63. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  64. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  65. data/test/hexapdf/content/test_canvas.rb +3 -3
  66. data/test/hexapdf/content/test_color_space.rb +1 -1
  67. data/test/hexapdf/encryption/test_aes.rb +4 -4
  68. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  69. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  70. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  71. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  72. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  73. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  74. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  75. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  76. data/test/hexapdf/test_configuration.rb +2 -2
  77. data/test/hexapdf/test_dictionary.rb +8 -1
  78. data/test/hexapdf/test_dictionary_fields.rb +9 -2
  79. data/test/hexapdf/test_document.rb +18 -10
  80. data/test/hexapdf/test_object.rb +71 -26
  81. data/test/hexapdf/test_parser.rb +205 -51
  82. data/test/hexapdf/test_pdf_array.rb +8 -1
  83. data/test/hexapdf/test_revisions.rb +35 -0
  84. data/test/hexapdf/test_serializer.rb +7 -0
  85. data/test/hexapdf/test_tokenizer.rb +28 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  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 +8 -1
  96. data/test/hexapdf/type/test_page.rb +8 -1
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/type/test_resources.rb +6 -0
  99. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  100. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  101. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  102. data/test/test_helper.rb +2 -0
  103. metadata +6 -12
@@ -65,8 +65,11 @@ module HexaPDF
65
65
  # * Returns the native Ruby object for values with class HexaPDF::Object. However, all
66
66
  # subclasses of HexaPDF::Object are returned as is (it makes no sense, for example, to return
67
67
  # the hash that describes the Catalog instead of the Catalog object).
68
+ #
69
+ # Note: Hash or Array values will always be returned as-is, i.e. not wrapped with Dictionary or
70
+ # PDFArray.
68
71
  def [](arg1, arg2 = nil)
69
- data = value[arg1, *arg2]
72
+ data = arg2 ? value[arg1, arg2] : value[arg1]
70
73
  return if data.nil?
71
74
 
72
75
  if arg2 || arg1.kind_of?(Range)
@@ -83,7 +86,7 @@ module HexaPDF
83
86
  # subclasses) and the given data has not (including subclasses), the data is stored inside the
84
87
  # HexaPDF::Object.
85
88
  def []=(index, data)
86
- if value[index].class == HexaPDF::Object && !data.kind_of?(HexaPDF::Object) &&
89
+ if value[index].instance_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Object) &&
87
90
  !data.kind_of?(HexaPDF::Reference)
88
91
  value[index].value = data
89
92
  else
@@ -113,6 +116,13 @@ module HexaPDF
113
116
  value.delete_at(index)
114
117
  end
115
118
 
119
+ # Deletes all values from the PDFArray that are equal to the given object.
120
+ #
121
+ # Returns the last deleted item, or +nil+ if no matching item is found.
122
+ def delete(object)
123
+ value.delete(object)
124
+ end
125
+
116
126
  # :call-seq:
117
127
  # array.slice!(index) -> obj or nil
118
128
  # array.slice!(start, length) -> new_array or nil
@@ -174,9 +184,9 @@ module HexaPDF
174
184
  self
175
185
  end
176
186
 
177
- # Returns a duplicate of the underlying array.
187
+ # Returns an array containing the preprocessed values (like in #[]).
178
188
  def to_ary
179
- value.dup
189
+ each.to_a
180
190
  end
181
191
 
182
192
  private
@@ -196,7 +206,7 @@ module HexaPDF
196
206
  data = document.deref(data)
197
207
  value[index] = data if index
198
208
  end
199
- if data.class == HexaPDF::Object || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
209
+ if data.instance_of?(HexaPDF::Object) || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
200
210
  data = data.value
201
211
  end
202
212
  data
@@ -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
 
@@ -317,6 +343,7 @@ module HexaPDF
317
343
  @io << data.freeze
318
344
  end
319
345
  @io << "\nendstream"
346
+ @in_object = false
320
347
 
321
348
  nil
322
349
  else
@@ -324,12 +351,12 @@ module HexaPDF
324
351
  obj.value[:Length] = data.size
325
352
 
326
353
  str = serialize_hash(obj.value)
354
+ @in_object = false
355
+
327
356
  str << "stream\n"
328
357
  str << data
329
358
  str << "\nendstream"
330
359
  end
331
- ensure
332
- @in_object = false
333
360
  end
334
361
 
335
362
  # Invokes the correct serialization method for the object.
@@ -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)
@@ -188,6 +188,28 @@ module HexaPDF
188
188
  token
189
189
  end
190
190
 
191
+ # Returns a single integer or keyword token read from the current position and advances the scan
192
+ # pointer. If the current position doesn't contain such a token, +nil+ is returned without
193
+ # advancing the scan pointer. The value +NO_MORE_TOKENS+ is returned if there are no more tokens
194
+ # available.
195
+ #
196
+ # Initial runs of whitespace characters are ignored.
197
+ #
198
+ # Note: This is a special method meant for use with reconstructing the cross-reference table!
199
+ def next_integer_or_keyword
200
+ skip_whitespace
201
+ byte = @ss.string.getbyte(@ss.pos) || -1
202
+ if 48 <= byte && byte <= 57
203
+ parse_number
204
+ elsif (97 <= byte && byte <= 122) || (65 <= byte && byte <= 90)
205
+ parse_keyword
206
+ elsif byte == -1 # we reached the end of the file
207
+ NO_MORE_TOKENS
208
+ else
209
+ nil
210
+ end
211
+ end
212
+
191
213
  # Reads the byte (an integer) at the current position and advances the scan pointer.
192
214
  def next_byte
193
215
  prepare_string_scanner(1)
@@ -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
@@ -249,38 +244,38 @@ module HexaPDF
249
244
  rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
250
245
  end
251
246
 
252
- form = (@widget[:AP] ||= {})[:N] = @document.add({Type: :XObject, Subtype: :Form,
253
- BBox: [0, 0, rect.width, rect.height]})
247
+ form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
248
+ # Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
249
+ # key; we can do this since we know this has to be a Form object
250
+ form = @document.wrap(form, type: :XObject, subtype: :Form) unless form[:Subtype] == :Form
251
+ form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
252
+ form.contents = ''
254
253
  form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
255
254
 
256
255
  canvas = form.canvas
257
256
  apply_background_and_border(border_style, canvas)
258
257
  style.font_size = calculate_font_size(font, font_size, rect, border_style)
258
+ style.clear_cache
259
259
 
260
260
  canvas.marked_content_sequence(:Tx) do
261
- if (value = @field.field_value)
261
+ if @field.field_value || @field.concrete_field_type == :list_box
262
262
  canvas.save_graphics_state do
263
263
  canvas.rectangle(padding, padding, rect.width - 2 * padding,
264
264
  rect.height - 2 * padding).clip_path.end_path
265
- fragment = HexaPDF::Layout::TextFragment.create(value, style)
266
- # Adobe seems to be left/right-aligning based on twice the border width and
267
- # vertically centering based on the cap height, if enough space is available
268
- x = case @field.text_alignment
269
- when :left then 2 * padding
270
- when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
271
- when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
272
- end
273
- cap_height = font.wrapped_font.cap_height * font.scaling_factor / 1000.0 *
274
- style.font_size
275
- y = padding + (rect.height - 2 * padding - cap_height) / 2.0
276
- y = padding - style.scaled_font_descender if y < 0
277
- fragment.draw(canvas, x, y)
265
+ if @field.concrete_field_type == :multiline_text_field
266
+ draw_multiline_text(canvas, rect, style, padding)
267
+ elsif @field.concrete_field_type == :list_box
268
+ draw_list_box(canvas, rect, style, padding)
269
+ else
270
+ draw_single_line_text(canvas, rect, style, padding)
271
+ end
278
272
  end
279
273
  end
280
274
  end
281
275
  end
282
276
 
283
277
  alias create_combo_box_appearances create_text_appearances
278
+ alias create_list_box_appearances create_text_appearances
284
279
 
285
280
  private
286
281
 
@@ -341,6 +336,13 @@ module HexaPDF
341
336
  canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
342
337
  else
343
338
  canvas.rectangle(offset, offset, width, height)
339
+ if @field.concrete_field_type == :comb_text_field
340
+ cell_width = rect.width.to_f / @field[:MaxLen]
341
+ 1.upto(@field[:MaxLen] - 1) do |i|
342
+ canvas.line(i * cell_width, border_style.width,
343
+ i * cell_width, border_style.width + height)
344
+ end
345
+ end
344
346
  end
345
347
  end
346
348
  canvas.stroke
@@ -385,14 +387,115 @@ module HexaPDF
385
387
  end
386
388
  end
387
389
 
390
+ # Draws a single line of text inside the widget's rectangle.
391
+ def draw_single_line_text(canvas, rect, style, padding)
392
+ value = @field.field_value
393
+ fragment = HexaPDF::Layout::TextFragment.create(value, style)
394
+
395
+ if @field.concrete_field_type == :comb_text_field
396
+ unless @field.key?(:MaxLen)
397
+ raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
398
+ end
399
+ new_items = []
400
+ cell_width = rect.width.to_f / @field[:MaxLen]
401
+ scaled_cell_width = cell_width / style.scaled_font_size.to_f
402
+ fragment.items.each_cons(2) do |a, b|
403
+ new_items << a << -(scaled_cell_width - a.width / 2.0 - b.width / 2.0)
404
+ end
405
+ new_items << fragment.items.last
406
+ fragment.items.replace(new_items)
407
+ fragment.clear_cache
408
+ # Adobe always seems to add 1 to the first offset...
409
+ x_offset = 1 + (cell_width - style.scaled_item_width(fragment.items[0])) / 2.0
410
+ x = case @field.text_alignment
411
+ when :left then x_offset
412
+ when :right then x_offset + cell_width * (@field[:MaxLen] - value.length)
413
+ when :center then x_offset + cell_width * ((@field[:MaxLen] - value.length) / 2)
414
+ end
415
+ else
416
+ # Adobe seems to be left/right-aligning based on twice the border width
417
+ x = case @field.text_alignment
418
+ when :left then 2 * padding
419
+ when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
420
+ when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
421
+ end
422
+ end
423
+
424
+ # Adobe seems to be vertically centering based on the cap height, if enough space is
425
+ # available
426
+ cap_height = style.font.wrapped_font.cap_height * style.font.scaling_factor / 1000.0 *
427
+ style.font_size
428
+ y = padding + (rect.height - 2 * padding - cap_height) / 2.0
429
+ y = padding - style.scaled_font_descender if y < 0
430
+ fragment.draw(canvas, x, y)
431
+ end
432
+
433
+ # Draws multiple lines of text inside the widget's rectangle.
434
+ def draw_multiline_text(canvas, rect, style, padding)
435
+ items = [Layout::TextFragment.create(@field.field_value, style)]
436
+ layouter = Layout::TextLayouter.new(style)
437
+ layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
438
+
439
+ result = nil
440
+ if style.font_size == 0 # need to auto-size text
441
+ style.font_size = 12 # Adobe seems to use this as starting point
442
+ style.clear_cache
443
+ loop do
444
+ result = layouter.fit(items, rect.width - 4 * padding, rect.height - 4 * padding)
445
+ break if result.status == :success || style.font_size <= 4 # don't make text too small
446
+ style.font_size -= 1
447
+ style.clear_cache
448
+ end
449
+ else
450
+ result = layouter.fit(items, rect.width - 4 * padding, 2**20)
451
+ end
452
+
453
+ unless result.lines.empty?
454
+ result.draw(canvas, 2 * padding, rect.height - 2 * padding - result.lines[0].height / 2.0)
455
+ end
456
+ end
457
+
458
+ # Draws the visible option items of the list box in the widget's rectangle.
459
+ def draw_list_box(canvas, rect, style, padding)
460
+ option_items = @field.option_items
461
+ top_index = @field.list_box_top_index
462
+ items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
463
+
464
+ indices = @field[:I] || []
465
+ value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
466
+ indices = value_indices if indices != value_indices
467
+
468
+ layouter = Layout::TextLayouter.new(style)
469
+ layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
470
+ result = layouter.fit(items, rect.width - 4 * padding, rect.height)
471
+
472
+ unless result.lines.empty?
473
+ top_gap = style.line_spacing.gap(result.lines[0], result.lines[0])
474
+ line_height = style.line_spacing.baseline_distance(result.lines[0], result.lines[0])
475
+ canvas.fill_color(153, 193, 218) # Adobe's color for selection highlighting
476
+ indices.map! {|i| rect.height - padding - (i - top_index + 1) * line_height }.each do |y|
477
+ next if y + line_height > rect.height || y + line_height < padding
478
+ canvas.rectangle(padding, y, rect.width - 2 * padding, line_height)
479
+ end
480
+ canvas.fill if canvas.graphics_object == :path
481
+ result.draw(canvas, 2 * padding, rect.height - padding - top_gap)
482
+ end
483
+ end
484
+
388
485
  # Calculates the font size for text fields based on the font and font size of the default
389
486
  # appearance string, the annotation rectangle and the border style.
390
487
  def calculate_font_size(font, font_size, rect, border_style)
391
488
  if font_size == 0
392
- unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
393
- font.scaling_factor / 1000.0
394
- # The constant factor was found empirically by checking what Adobe Reader etc. do
395
- (rect.height - 2 * border_style.width) / unit_font_size * 0.83
489
+ if @field.concrete_field_type == :multiline_text_field
490
+ 0 # Handled by multiline drawing code
491
+ elsif @field.concrete_field_type == :list_box
492
+ 12 # Seems to be Adobe's default
493
+ else
494
+ unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
495
+ font.scaling_factor / 1000.0
496
+ # The constant factor was found empirically by checking what Adobe Reader etc. do
497
+ (rect.height - 2 * border_style.width) / unit_font_size * 0.83
498
+ end
396
499
  else
397
500
  font_size
398
501
  end