hexapdf 0.12.3 → 0.14.3

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.
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