hexapdf 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/split.rb +74 -14
  5. data/lib/hexapdf/document.rb +10 -4
  6. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  7. data/lib/hexapdf/object.rb +22 -0
  8. data/lib/hexapdf/parser.rb +23 -1
  9. data/lib/hexapdf/pdf_array.rb +2 -2
  10. data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
  11. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  12. data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
  13. data/lib/hexapdf/type/acro_form/form.rb +133 -10
  14. data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
  15. data/lib/hexapdf/type/cid_font.rb +1 -1
  16. data/lib/hexapdf/type/font.rb +1 -1
  17. data/lib/hexapdf/type/font_simple.rb +1 -1
  18. data/lib/hexapdf/type/font_type0.rb +3 -3
  19. data/lib/hexapdf/type/form.rb +4 -1
  20. data/lib/hexapdf/type/page.rb +5 -5
  21. data/lib/hexapdf/utils/object_hash.rb +0 -1
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/test/hexapdf/layout/test_text_layouter.rb +9 -1
  24. data/test/hexapdf/test_document.rb +14 -6
  25. data/test/hexapdf/test_object.rb +27 -0
  26. data/test/hexapdf/test_parser.rb +46 -0
  27. data/test/hexapdf/test_pdf_array.rb +1 -1
  28. data/test/hexapdf/test_writer.rb +2 -2
  29. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
  30. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  31. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  32. data/test/hexapdf/type/acro_form/test_form.rb +83 -11
  33. data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
  34. data/test/hexapdf/type/test_form.rb +7 -0
  35. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  36. data/test/test_helper.rb +2 -0
  37. metadata +4 -3
@@ -58,7 +58,7 @@ module HexaPDF
58
58
 
59
59
  # Returns the CID font of this type 0 font.
60
60
  def descendant_font
61
- document.cache(@data, :descendant_font) do
61
+ cache(:descendant_font) do
62
62
  document.wrap(self[:DescendantFonts][0])
63
63
  end
64
64
  end
@@ -116,7 +116,7 @@ module HexaPDF
116
116
  #
117
117
  # Note that the CMap is cached internally when accessed the first time.
118
118
  def cmap
119
- document.cache(@data, :cmap) do
119
+ cache(:cmap) do
120
120
  val = self[:Encoding]
121
121
  if val.kind_of?(Symbol)
122
122
  HexaPDF::Font::CMap.for_name(val.to_s)
@@ -135,7 +135,7 @@ module HexaPDF
135
135
  #
136
136
  # See: PDF1.7 s9.10.2
137
137
  def ucs2_cmap
138
- document.cache(@data, :ucs2_cmap) do
138
+ cache(:ucs2_cmap) do
139
139
  encoding = self[:Encoding]
140
140
  system_info = descendant_font[:CIDSystemInfo]
141
141
  registry = system_info[:Registry]
@@ -94,9 +94,12 @@ module HexaPDF
94
94
 
95
95
  # Replaces the contents of the form XObject with the given string.
96
96
  #
97
+ # This also clears the cache to avoid returning invalid objects.
98
+ #
97
99
  # Note: This is the same as #stream= but here for interface compatibility with Page.
98
100
  def contents=(data)
99
101
  self.stream = data
102
+ clear_cache
100
103
  end
101
104
 
102
105
  # Returns the resource dictionary which is automatically created if it doesn't exist.
@@ -134,7 +137,7 @@ module HexaPDF
134
137
  #
135
138
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
136
139
  def canvas
137
- document.cache(@data, :canvas) do
140
+ cache(:canvas) do
138
141
  unless stream.empty?
139
142
  raise HexaPDF::Error, "Cannot create a canvas for a form XObjects with contents"
140
143
  end
@@ -383,7 +383,7 @@ module HexaPDF
383
383
  raise ArgumentError, "Invalid value for 'type', expected: :page, :underlay or :overlay"
384
384
  end
385
385
  cache_key = "#{type}_canvas".intern
386
- return document.cache(@data, cache_key) if document.cached?(@data, cache_key)
386
+ return cache(cache_key) if cached?(cache_key)
387
387
 
388
388
  if type == :page && key?(:Contents)
389
389
  raise HexaPDF::Error, "Cannot get the canvas for a page with contents"
@@ -400,14 +400,14 @@ module HexaPDF
400
400
 
401
401
  contents = self[:Contents]
402
402
  if contents.nil?
403
- page_canvas = document.cache(@data, :page_canvas, create_canvas.call)
403
+ page_canvas = cache(:page_canvas, create_canvas.call)
404
404
  self[:Contents] = document.add({Filter: :FlateDecode},
405
405
  stream: page_canvas.stream_data)
406
406
  end
407
407
 
408
408
  if type == :overlay || type == :underlay
409
- underlay_canvas = document.cache(@data, :underlay_canvas, create_canvas.call)
410
- overlay_canvas = document.cache(@data, :overlay_canvas, create_canvas.call)
409
+ underlay_canvas = cache(:underlay_canvas, create_canvas.call)
410
+ overlay_canvas = cache(:overlay_canvas, create_canvas.call)
411
411
 
412
412
  stream = HexaPDF::StreamData.new do
413
413
  Fiber.yield(" q ")
@@ -432,7 +432,7 @@ module HexaPDF
432
432
  self[:Contents] = [underlay, *self[:Contents], overlay]
433
433
  end
434
434
 
435
- document.cache(@data, cache_key)
435
+ cache(cache_key)
436
436
  end
437
437
 
438
438
  # Creates a Form XObject from the page's dictionary and contents for the given PDF document.
@@ -125,7 +125,6 @@ module HexaPDF
125
125
  def oids
126
126
  @oids.keys
127
127
  end
128
- private :oids
129
128
 
130
129
  end
131
130
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.13.0'
40
+ VERSION = '0.14.0'
41
41
 
42
42
  end
@@ -591,25 +591,33 @@ describe HexaPDF::Layout::TextLayouter do
591
591
 
592
592
  describe "horizontal alignment" do
593
593
  before do
594
- @items = boxes(*[[20, 20]] * 4)
594
+ @items = boxes(*[[20, 20]] * 4) + [glue(10), penalty(-5000, boxes(0).first.item)]
595
595
  end
596
596
 
597
597
  it "aligns the contents to the left" do
598
598
  @style.align = :left
599
599
  result = @layouter.fit(@items, 100, 100)
600
600
  assert_equal(0, result.lines[0].x_offset)
601
+ assert_equal(80, result.lines[0].width)
602
+ result = @layouter.fit(@items, proc { 100 }, 100)
603
+ assert_equal(0, result.lines[0].x_offset)
604
+ assert_equal(80, result.lines[0].width)
601
605
  end
602
606
 
603
607
  it "aligns the contents to the center" do
604
608
  @style.align = :center
605
609
  result = @layouter.fit(@items, 100, 100)
606
610
  assert_equal(10, result.lines[0].x_offset)
611
+ result = @layouter.fit(@items, proc { 100 }, 100)
612
+ assert_equal(10, result.lines[0].x_offset)
607
613
  end
608
614
 
609
615
  it "aligns the contents to the right" do
610
616
  @style.align = :right
611
617
  result = @layouter.fit(@items, 100, 100)
612
618
  assert_equal(20, result.lines[0].x_offset)
619
+ result = @layouter.fit(@items, proc { 100 }, 100)
620
+ assert_equal(20, result.lines[0].x_offset)
613
621
  end
614
622
  end
615
623
 
@@ -609,16 +609,24 @@ describe HexaPDF::Document do
609
609
 
610
610
  describe "caching interface" do
611
611
  it "allows setting and retrieving values" do
612
- assert_equal(:test, @doc.cache(:a, :b, :test))
613
- assert_equal(:test, @doc.cache(:a, :b, :other))
614
- assert_equal(:other, @doc.cache(:a, :c) { :other })
612
+ assert_equal(:test, @doc.cache(:a, :b, :test) { :notused })
613
+ assert_equal(:test, @doc.cache(:a, :b) { :other })
614
+ assert_equal(:test, @doc.cache(:a, :b))
615
+ assert_nil(@doc.cache(:a, :c, nil))
616
+ assert_nil(@doc.cache(:a, :c) { :other })
617
+ assert_nil(@doc.cache(:a, :c))
615
618
  assert(@doc.cached?(:a, :b))
616
619
  assert(@doc.cached?(:a, :c))
617
620
  end
618
621
 
622
+ it "allows updating a value" do
623
+ @doc.cache(:a, :b) { :test }
624
+ assert_equal(:new, @doc.cache(:a, :b, update: true) { :new })
625
+ end
626
+
619
627
  it "allows clearing cached values" do
620
- @doc.cache(:a, :b, :c)
621
- @doc.cache(:b, :c, :d)
628
+ @doc.cache(:a, :b) { :c }
629
+ @doc.cache(:b, :c) { :d }
622
630
  @doc.clear_cache(:a)
623
631
  refute(@doc.cached?(:a, :b))
624
632
  assert(@doc.cached?(:b, :c))
@@ -626,7 +634,7 @@ describe HexaPDF::Document do
626
634
  refute(@doc.cached?(:a, :c))
627
635
  end
628
636
 
629
- it "fails if no cached value exists and neither a value nor a block is given" do
637
+ it "fails if no cached value exists and no block is given" do
630
638
  assert_raises(LocalJumpError) { @doc.cache(:a, :b) }
631
639
  end
632
640
  end
@@ -3,6 +3,7 @@
3
3
  require 'test_helper'
4
4
  require 'hexapdf/object'
5
5
  require 'hexapdf/reference'
6
+ require 'hexapdf/document'
6
7
 
7
8
  describe HexaPDF::Object do
8
9
  describe "class.deep_copy" do
@@ -199,6 +200,32 @@ describe HexaPDF::Object do
199
200
  end
200
201
  end
201
202
 
203
+ describe "caching" do
204
+ before do
205
+ @obj = HexaPDF::Object.new({}, document: HexaPDF::Document.new)
206
+ end
207
+
208
+ it "can set and return a cached value" do
209
+ assert_equal(:value, @obj.cache(:data, :value))
210
+ assert_equal(:value, @obj.cache(:data, :other))
211
+ assert_equal(:value, @obj.cache(:block) { :value })
212
+ assert_equal(:other, @obj.cache(:data, :other, update: true))
213
+ end
214
+
215
+ it "can check for the existence of a cached value" do
216
+ refute(@obj.cached?(:data))
217
+ @obj.cache(:data, :value)
218
+ assert(@obj.cached?(:data))
219
+ end
220
+
221
+ it "can clear all cached values" do
222
+ @obj.cache(:data, :value)
223
+ assert(@obj.cached?(:data))
224
+ @obj.clear_cache
225
+ refute(@obj.cached?(:data))
226
+ end
227
+ end
228
+
202
229
  describe "validation" do
203
230
  before do
204
231
  @doc = Object.new
@@ -386,6 +386,41 @@ describe HexaPDF::Parser do
386
386
  assert_match(/dictionary/, exp.message)
387
387
  end
388
388
 
389
+ describe "invalid numbering of main xref section" do
390
+ it "handles the xref if the numbering is off by N" do
391
+ create_parser(" 1 0 obj 1 endobj\n" \
392
+ "xref\n1 2\n0000000000 65535 f \n0000000001 00000 n \ntrailer\n<<>>\n")
393
+ section, _trailer = @parser.parse_xref_section_and_trailer(17)
394
+ assert_equal(HexaPDF::XRefSection.in_use_entry(1, 0, 1), section[1])
395
+ end
396
+
397
+ it "fails if the first entry is not the one for oid=0" do
398
+ create_parser(" 1 0 obj 1 endobj\n" \
399
+ "xref\n1 2\n0000000000 00005 f \n0000000001 00000 n \ntrailer\n<<>>\n")
400
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
401
+ assert_match(/Main.*invalid numbering/i, exp.message)
402
+
403
+ create_parser(" 1 0 obj 1 endobj\n" \
404
+ "xref\n1 2\n0000000001 00000 n \n0000000001 00000 n \ntrailer\n<<>>\n")
405
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
406
+ assert_match(/Main.*invalid numbering/i, exp.message)
407
+ end
408
+
409
+ it "fails if the tested entry position is invalid" do
410
+ create_parser(" 1 0 obj 1 endobj\n" \
411
+ "xref\n1 2\n0000000000 65535 f \n0000000005 00000 n \ntrailer\n<<>>\n")
412
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
413
+ assert_match(/Main.*invalid numbering/i, exp.message)
414
+ end
415
+
416
+ it "fails if the tested entry position's oid doesn't match the corrected entry oid" do
417
+ create_parser(" 2 0 obj 1 endobj\n" \
418
+ "xref\n1 2\n0000000000 65535 f \n0000000001 00000 n \ntrailer\n<<>>\n")
419
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(17) }
420
+ assert_match(/Main.*invalid numbering/i, exp.message)
421
+ end
422
+ end
423
+
389
424
  describe "with strict parsing" do
390
425
  before do
391
426
  @document.config['parser.on_correctable_error'] = proc { true }
@@ -408,6 +443,12 @@ describe HexaPDF::Parser do
408
443
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
409
444
  assert_match(/invalid.*cross-reference entry/i, exp.message)
410
445
  end
446
+
447
+ it "fails if the main cross-reference section has invalid numbering" do
448
+ create_parser("xref\n1 1\n0000000001 00000 n \ntrailer\n<<>>\n")
449
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_xref_section_and_trailer(0) }
450
+ assert_match(/Main.*invalid numbering/i, exp.message)
451
+ end
411
452
  end
412
453
  end
413
454
 
@@ -454,6 +495,11 @@ describe HexaPDF::Parser do
454
495
  assert_equal(5, @parser.load_object(@xref).value)
455
496
  end
456
497
 
498
+ it "handles cases where the line contains an invalid string that exceeds the read buffer" do
499
+ create_parser("(1" << "(abc" * 32188 << "\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
500
+ assert_equal(6, @parser.load_object(@xref).value)
501
+ end
502
+
457
503
  it "ignores invalid objects" do
458
504
  create_parser("1 x obj\n5\nendobj\n1 0 xobj\n6\nendobj\n1 0 obj 4\nendobj\ntrailer\n<</Size 1>>")
459
505
  assert_equal(4, @parser.load_object(@xref).value)
@@ -164,6 +164,6 @@ describe HexaPDF::PDFArray do
164
164
  end
165
165
 
166
166
  it "can be converted to a simple array" do
167
- assert_equal(@array.value, @array.to_ary)
167
+ assert_equal([1, :data, "deref", @array[3]], @array.to_ary)
168
168
  end
169
169
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.13.0)>>
43
+ <</Producer(HexaPDF version 0.14.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.13.0)>>
75
+ <</Producer(HexaPDF version 0.14.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -25,14 +25,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
25
25
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
26
26
  end
27
27
 
28
- it "fails for unsupported choice fields" do
29
- @field = @doc.wrap(@field, type: :XXAcroFormField, subtype: :Ch)
30
- @field[:FT] = :Ch
31
- @field.initialize_as_list_box
32
- @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
33
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
34
- end
35
-
36
28
  it "fails for unsupported field types" do
37
29
  @field[:FT] = :Unknown
38
30
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
@@ -121,6 +113,24 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
121
113
  [:curve_to, [8.236068, 7.249543, 9.0, 8.572712, 9.0, 10.0]],
122
114
  [:stroke_path], [:restore_graphics_state]])
123
115
  end
116
+
117
+ it "handles the special case of a comb field" do
118
+ @field = @doc.add({FT: :Tx, MaxLen: 4}, type: :XXAcroFormField, subtype: :Tx)
119
+ @field.initialize_as_comb_text_field
120
+ @widget = @field.create_widget(@page, Rect: [0, 0, 10, 20])
121
+ @xform = @doc.add({Type: :XObject, Subtype: :Form, BBox: @widget[:Rect]})
122
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
123
+ @widget.border_style(width: 2)
124
+ execute
125
+ assert_operators(@xform.stream,
126
+ [[:save_graphics_state],
127
+ [:set_line_width, [2]],
128
+ [:append_rectangle, [1, 1, 8, 18]],
129
+ [:move_to, [2.5, 2]], [:line_to, [2.5, 20.0]],
130
+ [:move_to, [5.0, 2]], [:line_to, [5.0, 20.0]],
131
+ [:move_to, [7.5, 2]], [:line_to, [7.5, 20.0]],
132
+ [:stroke_path], [:restore_graphics_state]])
133
+ end
124
134
  end
125
135
 
126
136
  describe "draw_marker" do
@@ -342,7 +352,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
342
352
  end
343
353
  end
344
354
 
345
- describe "text field" do
355
+ describe "text fields" do
346
356
  before do
347
357
  @form.set_default_appearance_string
348
358
  @field = @doc.add({FT: :Tx}, type: :XXAcroFormField, subtype: :Tx)
@@ -392,35 +402,58 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
392
402
  assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
393
403
  end
394
404
 
395
- describe "single line text fields" do
396
- describe "font size calculation" do
397
- before do
398
- @widget[:Rect].height = 20
399
- @widget[:Rect].width = 100
400
- @field.field_value = ''
401
- end
405
+ it "re-uses the existing form XObject" do
406
+ @field[:V] = 'test'
407
+ @generator.create_appearances
408
+ form = @widget[:AP][:N]
409
+ form[:key] = :value
402
410
 
403
- it "uses the non-zero font size" do
404
- @field.set_default_appearance_string(font_size: 10)
405
- @generator.create_appearances
406
- assert_operators(@widget[:AP][:N].stream,
407
- [:set_font_and_size, [:F1, 10]],
408
- range: 5)
409
- end
411
+ @field[:V] = 'test1'
412
+ @generator.create_appearances
413
+ assert_same(form, @widget[:AP][:N])
414
+ refute(form.key?(:key))
415
+ assert_match(/test1/, form.contents)
416
+ end
410
417
 
411
- it "calculates the font size based on the rectangle height and border width" do
412
- @generator.create_appearances
413
- assert_operators(@widget[:AP][:N].stream,
414
- [:set_font_and_size, [:F1, 12.923875]],
415
- range: 5)
416
- @widget.border_style(width: 2, color: :transparent)
417
- @generator.create_appearances
418
- assert_operators(@widget[:AP][:N].stream,
419
- [:set_font_and_size, [:F1, 11.487889]],
420
- range: 5)
421
- end
418
+ describe "font size calculation" do
419
+ before do
420
+ @widget[:Rect].height = 20
421
+ @widget[:Rect].width = 100
422
+ @field.field_value = ''
423
+ end
424
+
425
+ it "uses the non-zero font size" do
426
+ @field.set_default_appearance_string(font_size: 10)
427
+ @generator.create_appearances
428
+ assert_operators(@widget[:AP][:N].stream,
429
+ [:set_font_and_size, [:F1, 10]],
430
+ range: 5)
431
+ end
432
+
433
+ it "calculates the font size based on the rectangle height and border width" do
434
+ @generator.create_appearances
435
+ assert_operators(@widget[:AP][:N].stream,
436
+ [:set_font_and_size, [:F1, 12.923875]],
437
+ range: 5)
438
+ @widget.border_style(width: 2, color: :transparent)
439
+ @generator.create_appearances
440
+ assert_operators(@widget[:AP][:N].stream,
441
+ [:set_font_and_size, [:F1, 11.487889]],
442
+ range: 5)
443
+ end
444
+
445
+ it " in case of mulitline auto-sizing" do
446
+ @field.initialize_as_multiline_text_field
447
+ @field[:V] = 'a'
448
+ @field.set_default_appearance_string(font_size: 0)
449
+ @generator.create_appearances
450
+ assert_operators(@widget[:AP][:N].stream,
451
+ [:set_font_and_size, [:F1, 12]],
452
+ range: 6)
422
453
  end
454
+ end
423
455
 
456
+ describe "single line text fields" do
424
457
  describe "quadding e.g. text alignment" do
425
458
  before do
426
459
  @field.field_value = 'Test'
@@ -480,16 +513,235 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
480
513
  end
481
514
  end
482
515
 
516
+ describe "multiline text fields" do
517
+ before do
518
+ @field.set_default_appearance_string(font_size: 10)
519
+ @field.initialize_as_multiline_text_field
520
+ @widget[:Rect].height = 30
521
+ @widget[:Rect].width = 100
522
+ end
523
+
524
+ describe "quadding e.g. text alignment" do
525
+ before do
526
+ @field[:V] = "Test\nValue"
527
+ end
528
+
529
+ it "works for left aligned text" do
530
+ @field.text_alignment(:left)
531
+ @generator.create_appearances
532
+ assert_operators(@widget[:AP][:N].stream,
533
+ [:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
534
+ range: 9)
535
+ end
536
+
537
+ it "works for right aligned text" do
538
+ @field.text_alignment(:right)
539
+ @generator.create_appearances
540
+ assert_operators(@widget[:AP][:N].stream,
541
+ [:set_text_matrix, [1, 0, 0, 1, 78.55, 16.195]],
542
+ range: 9)
543
+ end
544
+
545
+ it "works for center aligned text" do
546
+ @field.text_alignment(:center)
547
+ @generator.create_appearances
548
+ assert_operators(@widget[:AP][:N].stream,
549
+ [:set_text_matrix, [1, 0, 0, 1, 40.275, 16.195]],
550
+ range: 9)
551
+ end
552
+ end
553
+
554
+ it "creates the /N appearance stream according to the set string" do
555
+ @field.field_value = "Test\nValue"
556
+ @generator.create_appearances
557
+ assert_operators(@widget[:AP][:N].stream,
558
+ [[:begin_marked_content, [:Tx]],
559
+ [:save_graphics_state],
560
+ [:append_rectangle, [1, 1, 98, 28]],
561
+ [:clip_path_non_zero],
562
+ [:end_path],
563
+ [:save_graphics_state],
564
+ [:set_leading, [11.5625]],
565
+ [:set_font_and_size, [:F1, 10]],
566
+ [:begin_text],
567
+ [:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
568
+ [:show_text, ['Test']],
569
+ [:move_text_next_line],
570
+ [:show_text, ['Value']],
571
+ [:end_text],
572
+ [:restore_graphics_state],
573
+ [:restore_graphics_state],
574
+ [:end_marked_content]])
575
+
576
+ @field.field_value = "Test\nTest\nTest"
577
+ @field.set_default_appearance_string(font_size: 0)
578
+ @generator.create_appearances
579
+ assert_operators(@widget[:AP][:N].stream,
580
+ [[:begin_marked_content, [:Tx]],
581
+ [:save_graphics_state],
582
+ [:append_rectangle, [1, 1, 98, 28]],
583
+ [:clip_path_non_zero],
584
+ [:end_path],
585
+ [:save_graphics_state],
586
+ [:set_leading, [9.25]],
587
+ [:set_font_and_size, [:F1, 8]],
588
+ [:begin_text],
589
+ [:set_text_matrix, [1, 0, 0, 1, 2, 18.556]],
590
+ [:show_text, ['Test']],
591
+ [:move_text_next_line],
592
+ [:show_text, ['Test']],
593
+ [:move_text_next_line],
594
+ [:show_text, ['Test']],
595
+ [:end_text],
596
+ [:restore_graphics_state],
597
+ [:restore_graphics_state],
598
+ [:end_marked_content]],
599
+ )
600
+ end
601
+ end
602
+
603
+ describe "comb text fields" do
604
+ before do
605
+ @field.set_default_appearance_string(font_size: 10)
606
+ @field.initialize_as_comb_text_field
607
+ @field[:MaxLen] = 10
608
+ @widget[:Rect].height = 20
609
+ @widget[:Rect].width = 100
610
+ end
611
+
612
+ describe "quadding e.g. text alignment" do
613
+ before do
614
+ @field[:V] = 'Test'
615
+ end
616
+
617
+ it "works for left aligned text" do
618
+ @field.text_alignment(:left)
619
+ @generator.create_appearances
620
+ assert_operators(@widget[:AP][:N].stream,
621
+ [:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
622
+ range: 7)
623
+ end
624
+
625
+ it "works for right aligned text" do
626
+ @field.text_alignment(:right)
627
+ @generator.create_appearances
628
+ assert_operators(@widget[:AP][:N].stream,
629
+ [:set_text_matrix, [1, 0, 0, 1, 62.945, 6.41]],
630
+ range: 7)
631
+ end
632
+
633
+ it "works for center aligned text" do
634
+ @field.text_alignment(:center)
635
+ @generator.create_appearances
636
+ assert_operators(@widget[:AP][:N].stream,
637
+ [:set_text_matrix, [1, 0, 0, 1, 32.945, 6.41]],
638
+ range: 7)
639
+ end
640
+
641
+ it "handles centering like Adobe, e.g. shift left, when text cannot be completely centered" do
642
+ @field.field_value = 'Hello'
643
+ @field.text_alignment(:center)
644
+ @generator.create_appearances
645
+ assert_operators(@widget[:AP][:N].stream,
646
+ [:set_text_matrix, [1, 0, 0, 1, 22.39, 6.41]],
647
+ range: 7)
648
+ end
649
+ end
650
+
651
+ it "creates the /N appearance stream according to the set string" do
652
+ @field.field_value = 'Text'
653
+ @generator.create_appearances
654
+ assert_operators(@widget[:AP][:N].stream,
655
+ [[:begin_marked_content, [:Tx]],
656
+ [:save_graphics_state],
657
+ [:append_rectangle, [1, 1, 98, 18]],
658
+ [:clip_path_non_zero],
659
+ [:end_path],
660
+ [:set_font_and_size, [:F1, 10]],
661
+ [:begin_text],
662
+ [:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
663
+ [:show_text_with_positioning, [['T', -416.5, 'e', -472, 'x', -611, 't']]],
664
+ [:end_text],
665
+ [:restore_graphics_state],
666
+ [:end_marked_content]])
667
+ end
668
+
669
+ it "fails if the /MaxLen key is not set" do
670
+ @field.delete(:MaxLen)
671
+ @field[:V] = 't'
672
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
673
+ end
674
+ end
675
+
483
676
  describe "choice fields" do
484
677
  it "works for combo boxes by using the text appearance method" do
485
678
  @form.set_default_appearance_string
486
679
  field = @doc.add({FT: :Ch}, type: :XXAcroFormField, subtype: :Ch)
487
680
  field.initialize_as_combo_box
681
+ field.flag(:edit)
682
+ field.field_value = 'Test'
488
683
  widget = field.create_widget(@page, Rect: [0, 0, 0, 0])
489
684
  generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(widget)
490
685
  generator.create_appearances
491
686
  assert_kind_of(HexaPDF::Type::Form, widget[:AP][:N])
492
687
  end
688
+
689
+ describe "list boxes" do
690
+ before do
691
+ @field = @doc.add({FT: :Ch}, type: :XXAcroFormField, subtype: :Ch)
692
+ @field.initialize_as_list_box
693
+ @field.flag(:multi_select)
694
+ @field.option_items = ['a', 'b', 'c']
695
+ @widget = @field.create_widget(@page, Rect: [0, 0, 90, 36])
696
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
697
+ end
698
+
699
+ it "uses a fixed font size for list box items if auto-sizing is used" do
700
+ @field.set_default_appearance_string(font_size: 0)
701
+ @generator.create_appearances
702
+ assert_operators(@widget[:AP][:N].stream,
703
+ [:set_font_and_size, [:F1, 12]],
704
+ range: 8)
705
+ end
706
+
707
+ it "uses the set values instead of the ones from /I if in conflict" do
708
+ @field[:I] = [0, 1]
709
+ @field[:V] = ['b']
710
+ @generator.create_appearances
711
+ assert_operators(@widget[:AP][:N].stream,
712
+ [[:set_device_rgb_non_stroking_color, [0.6, 0.756863, 0.854902]],
713
+ [:append_rectangle, [1, 7.25, 88, 13.875]],
714
+ [:fill_path_non_zero]],
715
+ range: 5..7)
716
+ end
717
+
718
+ it "creates the /N appearance stream" do
719
+ @field[:I] = [1, 2]
720
+ @field[:V] = ['b', 'c']
721
+ @generator.create_appearances
722
+ assert_operators(@widget[:AP][:N].stream,
723
+ [[:begin_marked_content, [:Tx]],
724
+ [:save_graphics_state],
725
+ [:append_rectangle, [1, 1, 88, 34]],
726
+ [:clip_path_non_zero], [:end_path],
727
+ [:set_device_rgb_non_stroking_color, [0.6, 0.756863, 0.854902]],
728
+ [:append_rectangle, [1, 7.25, 88, 13.875]],
729
+ [:append_rectangle, [1, -6.625, 88, 13.875]],
730
+ [:fill_path_non_zero],
731
+ [:save_graphics_state],
732
+ [:set_leading, [13.875]],
733
+ [:set_font_and_size, [:F1, 12]],
734
+ [:set_device_gray_non_stroking_color, [0.0]],
735
+ [:begin_text],
736
+ [:set_text_matrix, [1, 0, 0, 1, 2, 23.609]],
737
+ [:show_text, ["a"]],
738
+ [:move_text_next_line],
739
+ [:show_text, ["b"]],
740
+ [:end_text],
741
+ [:restore_graphics_state], [:restore_graphics_state],
742
+ [:end_marked_content]])
743
+ end
744
+ end
493
745
  end
494
746
 
495
747
  describe "font resolution in case the referenced font is not usable" do