hexapdf 1.8.0 → 1.9.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +3 -0
  4. data/Rakefile +1 -1
  5. data/data/hexapdf/fonts/Inter-Bold.ttf +0 -0
  6. data/data/hexapdf/fonts/Inter-BoldItalic.ttf +0 -0
  7. data/data/hexapdf/fonts/Inter-Italic.ttf +0 -0
  8. data/data/hexapdf/fonts/Inter-Regular.ttf +0 -0
  9. data/data/hexapdf/fonts/OFL.txt +92 -0
  10. data/examples/019-acro_form.rb +3 -1
  11. data/examples/030-pdfa.rb +9 -16
  12. data/examples/034-text_shaping.rb +37 -0
  13. data/lib/hexapdf/configuration.rb +13 -1
  14. data/lib/hexapdf/document/annotations.rb +25 -0
  15. data/lib/hexapdf/font/cmap/writer.rb +15 -9
  16. data/lib/hexapdf/font/true_type_wrapper.rb +6 -3
  17. data/lib/hexapdf/font_loader.rb +47 -0
  18. data/lib/hexapdf/layout/container_box.rb +2 -4
  19. data/lib/hexapdf/layout/style.rb +66 -4
  20. data/lib/hexapdf/layout/table_box.rb +96 -13
  21. data/lib/hexapdf/layout/text_fragment.rb +13 -7
  22. data/lib/hexapdf/layout/text_shaper.rb +162 -10
  23. data/lib/hexapdf/type/annotations/appearance_generator.rb +42 -0
  24. data/lib/hexapdf/type/annotations/ink.rb +107 -0
  25. data/lib/hexapdf/type/annotations.rb +1 -0
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/digital_signature/common.rb +5 -5
  28. data/test/hexapdf/digital_signature/test_cms_handler.rb +1 -1
  29. data/test/hexapdf/document/test_annotations.rb +10 -0
  30. data/test/hexapdf/document/test_layout.rb +6 -3
  31. data/test/hexapdf/font/cmap/test_writer.rb +8 -6
  32. data/test/hexapdf/font/test_true_type_wrapper.rb +4 -0
  33. data/test/hexapdf/layout/test_container_box.rb +3 -1
  34. data/test/hexapdf/layout/test_style.rb +4 -0
  35. data/test/hexapdf/layout/test_table_box.rb +117 -1
  36. data/test/hexapdf/layout/test_text_fragment.rb +18 -8
  37. data/test/hexapdf/layout/test_text_shaper.rb +55 -5
  38. data/test/hexapdf/type/annotations/test_appearance_generator.rb +63 -0
  39. data/test/hexapdf/type/annotations/test_ink.rb +31 -0
  40. metadata +25 -3
@@ -26,7 +26,7 @@ module HexaPDF
26
26
  def signer_certificate
27
27
  @signer_certificate ||=
28
28
  begin
29
- cert = create_cert(name: '/CN=RSA signer/DC=gettalong', serial: 2,
29
+ cert = create_cert(name: '/CN=RSA signer', serial: 1,
30
30
  public_key: signer_key, issuer: ca_certificate)
31
31
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature')
32
32
  cert
@@ -36,7 +36,7 @@ module HexaPDF
36
36
  def non_repudiation_signer_certificate
37
37
  @non_repudiation_signer_certificate ||=
38
38
  begin
39
- cert = create_cert(name: '/CN=Non repudiation signer/DC=gettalong', serial: 2,
39
+ cert = create_cert(name: '/CN=Non repudiation signer', serial: 2,
40
40
  public_key: signer_key, issuer: ca_certificate)
41
41
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'nonRepudiation')
42
42
  cert
@@ -50,7 +50,7 @@ module HexaPDF
50
50
  def dsa_signer_certificate
51
51
  @dsa_signer_certificate ||=
52
52
  begin
53
- cert = create_cert(name: '/CN=DSA signer/DC=gettalong', serial: 3,
53
+ cert = create_cert(name: '/CN=DSA signer', serial: 3,
54
54
  public_key: dsa_signer_key, issuer: ca_certificate)
55
55
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature')
56
56
  cert
@@ -64,7 +64,7 @@ module HexaPDF
64
64
  def ecdsa_signer_certificate
65
65
  @ecdsa_signer_certificate ||=
66
66
  begin
67
- cert = create_cert(name: '/CN=ECDSA signer/DC=gettalong', serial: 4,
67
+ cert = create_cert(name: '/CN=ECDSA signer', serial: 4,
68
68
  public_key: ecdsa_signer_key, issuer: ca_certificate)
69
69
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature')
70
70
  cert
@@ -74,7 +74,7 @@ module HexaPDF
74
74
  def timestamp_certificate
75
75
  @timestamp_certificate ||=
76
76
  begin
77
- cert = create_cert(name: '/CN=timestamp/DC=gettalong', serial: 3,
77
+ cert = create_cert(name: '/CN=timestamp', serial: 5,
78
78
  public_key: signer_key, issuer: ca_certificate)
79
79
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
80
80
  extended_key_usage: 'timeStamping')
@@ -43,7 +43,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
43
43
  it "allows access to the signer information" do
44
44
  info = @handler.signer_info
45
45
  assert(info)
46
- assert_equal(2, info.serial)
46
+ assert_equal(1, info.serial)
47
47
  assert_equal(CERTIFICATES.signer_certificate.issuer, info.issuer)
48
48
  end
49
49
 
@@ -72,4 +72,14 @@ describe HexaPDF::Document::Annotations do
72
72
  assert_equal(annot, @page[:Annots].first)
73
73
  end
74
74
  end
75
+
76
+ describe "create_scribble" do
77
+ it "creates an appropriate ink annotation object" do
78
+ annot = @annots.create(:scribble, @page, 10, 10, 20, 15)
79
+ assert_equal(:Annot, annot[:Type])
80
+ assert_equal(:Ink, annot[:Subtype])
81
+ assert_equal([[10, 10, 20, 15]], annot.paths)
82
+ assert_equal(annot, @page[:Annots].first)
83
+ end
84
+ end
75
85
  end
@@ -513,19 +513,22 @@ describe HexaPDF::Document::Layout do
513
513
  assert_equal(10, box.width)
514
514
  assert_equal(15, box.height)
515
515
  items = box.instance_variable_get(:@items)
516
- assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM.join(" ").size, items[0].items.length)
516
+ assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM.join(" ").size,
517
+ items.sum {|i| i.items.length })
517
518
  end
518
519
 
519
520
  it "can use just some sentences from the lorem ipsum text" do
520
521
  box = @layout.lorem_ipsum_box(sentences: 1)
521
522
  items = box.instance_variable_get(:@items)
522
- assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM[0].size, items[0].items.length)
523
+ assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM[0].size,
524
+ items.sum {|i| i.items.length })
523
525
  end
524
526
 
525
527
  it "can use multiple of the selected sentences" do
526
528
  box = @layout.lorem_ipsum_box(sentences: 2, count: 2)
527
529
  items = box.instance_variable_get(:@items)
528
- assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM[0, 2].join(" ").size * 2 + 2, items[0].items.length)
530
+ assert_equal(HexaPDF::Document::Layout::LOREM_IPSUM[0, 2].join(" ").size * 2 + 2,
531
+ items.sum {|i| i.items.length })
529
532
  end
530
533
  end
531
534
 
@@ -19,8 +19,9 @@ describe HexaPDF::Font::CMap::Writer do
19
19
  1 begincodespacerange
20
20
  <0000> <FFFF>
21
21
  endcodespacerange
22
- 2 beginbfchar
22
+ 3 beginbfchar
23
23
  <0060><0090>
24
+ <00A0><00410042>
24
25
  <3A51><d840dc3e>
25
26
  endbfchar
26
27
  2 beginbfrange
@@ -79,6 +80,7 @@ describe HexaPDF::Font::CMap::Writer do
79
80
  0x1379.upto(0x137B) do |i|
80
81
  @to_unicode_mapping << [i, 0x90FE + i - 0x1379]
81
82
  end
83
+ @to_unicode_mapping << [0x00A0, "AB"]
82
84
  @to_unicode_mapping << [0x3A51, 0x2003E]
83
85
  end
84
86
 
@@ -89,17 +91,17 @@ describe HexaPDF::Font::CMap::Writer do
89
91
  end
90
92
 
91
93
  it "works if the last item is a range" do
92
- @to_unicode_mapping.pop
93
- @to_unicode_cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
94
- @to_unicode_cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
94
+ @to_unicode_mapping[-2, 2] = []
95
+ @to_unicode_cmap_data.sub!(/3 beginbfchar/, '1 beginbfchar')
96
+ @to_unicode_cmap_data.sub!(/<00A0>.*<d840dc3e>\n/m, '')
95
97
  assert_equal(@to_unicode_cmap_data,
96
98
  HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
97
99
  end
98
100
 
99
101
  it "works with only ranges" do
100
- @to_unicode_mapping.delete_at(-1)
102
+ @to_unicode_mapping[-2, 2] = []
101
103
  @to_unicode_mapping.delete_at(0x5f)
102
- @to_unicode_cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
104
+ @to_unicode_cmap_data.sub!(/\n3 beginbfchar.*endbfchar/m, '')
103
105
  assert_equal(@to_unicode_cmap_data,
104
106
  HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
105
107
  end
@@ -37,6 +37,10 @@ describe HexaPDF::Font::TrueTypeWrapper do
37
37
  refute(HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false).subset?)
38
38
  end
39
39
 
40
+ it "can be asked for the filename from which the wrapped font was created" do
41
+ assert_equal(@font_file, @font_wrapper.filename)
42
+ end
43
+
40
44
  describe "decode_*" do
41
45
  it "decode_utf8 returns an array of glyph objects" do
42
46
  assert_equal("Test",
@@ -71,9 +71,11 @@ describe HexaPDF::Layout::ContainerBox do
71
71
  end
72
72
 
73
73
  it "splits the box if splitting is allowed and the content is too big" do
74
- box = create_box([child_box(height: 80), child_box(height: 30)], splitable: true)
74
+ box = create_box([child_box(width: 30, height: 80), child_box(height: 30)], splitable: true)
75
75
  box.fit(@frame.available_width, @frame.available_height, @frame)
76
76
  assert(box.fit_result.overflow?)
77
+ assert_equal(100, box.width)
78
+ assert_equal(80, box.height)
77
79
  end
78
80
  end
79
81
 
@@ -785,6 +785,10 @@ describe HexaPDF::Layout::Style do
785
785
  assert_equal(100, @style.horizontal_scaling)
786
786
  assert_equal(0, @style.text_rise)
787
787
  assert_equal({}, @style.font_features)
788
+ assert_nil(@style.font_script)
789
+ assert_equal(:ltr, @style.direction)
790
+ assert_nil(@style.language)
791
+ assert_equal(:internal, @style.shaping_engine)
788
792
  assert_equal(HexaPDF::Content::TextRenderingMode::FILL, @style.text_rendering_mode)
789
793
  assert_equal([0], @style.fill_color.components)
790
794
  assert_equal(1, @style.fill_alpha)
@@ -128,6 +128,20 @@ describe HexaPDF::Layout::TableBox::Cell do
128
128
  assert_equal(32, cell.height)
129
129
  end
130
130
 
131
+ it "can split a cell if necessary" do
132
+ children = [HexaPDF::Layout::Box.create(width: 70, height: 60),
133
+ HexaPDF::Layout::Box.create(width: 70, height: 60)]
134
+ container = HexaPDF::Layout::ContainerBox.new(children: children, splitable: true)
135
+ [container, container.children].each do |used_children|
136
+ cell = create_cell(children: used_children)
137
+ assert(cell.fit(100, 100, @frame).overflow?)
138
+ assert_equal(100, cell.width)
139
+ assert_equal(72, cell.height)
140
+ assert_equal(used_children == container ? 100 : 82, cell.preferred_width)
141
+ assert_equal(72, cell.preferred_height)
142
+ end
143
+ end
144
+
131
145
  it "doesn't fit children that are too big" do
132
146
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 300, height: 20))
133
147
  assert(cell.fit(100, 100, @frame).failure?)
@@ -142,6 +156,18 @@ describe HexaPDF::Layout::TableBox::Cell do
142
156
  end
143
157
  end
144
158
 
159
+ describe "split" do
160
+ it "assigns the overflown boxes to the split box" do
161
+ children = [HexaPDF::Layout::Box.create(width: 70, height: 60),
162
+ HexaPDF::Layout::Box.create(width: 70, height: 60)]
163
+ cell = create_cell(children: children)
164
+ assert(cell.fit(100, 100, @frame).overflow?)
165
+ box, overflow_box = cell.split
166
+ assert_same(cell, box)
167
+ assert_equal([children[1]], overflow_box.children)
168
+ end
169
+ end
170
+
145
171
  describe "draw" do
146
172
  before do
147
173
  @canvas = HexaPDF::Document.new.pages.add.canvas
@@ -349,6 +375,13 @@ describe HexaPDF::Layout::TableBox::Cells do
349
375
  cells = create_cells([[:a, :b], [:c], [:d, :e]])
350
376
  assert_equal([[:a, :b], [:c], [:d, :e]], cells.each_row.map {|cols| cols.map(&:children) })
351
377
  end
378
+
379
+ it "allows iterating over rows containing an overridden one" do
380
+ cells = create_cells([[:a, :b], [:c], [:d, :e]])
381
+ cells.instance_variable_set(:@overridden_row_index, 1)
382
+ cells.instance_variable_set(:@overridden_row_cells, [cells[2, 1], cells[2, 0]])
383
+ assert_equal([[:a, :b], [:e, :d], [:d, :e]], cells.each_row.map {|cols| cols.map(&:children) })
384
+ end
352
385
  end
353
386
 
354
387
  describe "style" do
@@ -612,6 +645,29 @@ describe HexaPDF::Layout::TableBox do
612
645
  check_box(box, :overflow, 160, 10,
613
646
  [[0, 0, 80, 10], [80, 0, 80, 10], [nil, nil, 80, 0], [nil, nil, 0, 0]])
614
647
  end
648
+
649
+ it "fails if not even enough height for the first row is available" do
650
+ check_box(create_box(height: 10), :failure, 160, 10)
651
+ end
652
+
653
+ describe "last row splitting" do
654
+ before do
655
+ @boxes = [[80, 50], [80, 40], [80, 70]].map do |w, h|
656
+ HexaPDF::Layout::Box.new(width: w, height: h, &@draw_block)
657
+ end
658
+ end
659
+
660
+ it "splits the last row if necessary" do
661
+ box = create_box(cells: [[@boxes[0], @boxes[1, 2]]], cell_style: {padding: 0, border: {width: 0}})
662
+ check_box(box, :overflow, 160, 50, [[0, 0, 80, 50], [80, 0, 80, 50]])
663
+ end
664
+
665
+ it "doesn't split the last row if it is part of a row span" do
666
+ cells = [[@boxes[0], {content: @boxes[1, 2], row_span: 2}]]
667
+ box = create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}})
668
+ check_box(box, :failure, 160, 0)
669
+ end
670
+ end
615
671
  end
616
672
 
617
673
  describe "split" do
@@ -672,6 +728,23 @@ describe HexaPDF::Layout::TableBox do
672
728
  end
673
729
  end
674
730
  end
731
+
732
+ it "splits the last row of a table" do
733
+ box = create_box(cells: [[@fixed_size_boxes[0], @fixed_size_boxes[1, 3]]],
734
+ cell_style: {padding: 0, border: {width: 0}})
735
+ assert(box.fit(100, 15, @frame).overflow?)
736
+ box_a, box_b = box.split
737
+ assert_same(box_a, box)
738
+
739
+ assert_equal(0, box_a.start_row_index)
740
+ assert_equal(0, box_a.last_fitted_row_index)
741
+ assert_equal(0, box_b.start_row_index)
742
+ assert_equal(-1, box_b.last_fitted_row_index)
743
+
744
+ assert_nil(box_b.cells[0, 0].children)
745
+ assert_same(box_a.cells[0, 0].style, box_b.cells[0, 0].style)
746
+ assert_equal(@fixed_size_boxes[2, 2], box_b.cells[0, 1].children)
747
+ end
675
748
  end
676
749
 
677
750
  describe "draw_content" do
@@ -722,7 +795,7 @@ describe HexaPDF::Layout::TableBox do
722
795
  assert_operators(@canvas.contents, operators)
723
796
  end
724
797
 
725
- it "correctly works for split boxes" do
798
+ it "correctly works for split tables" do
726
799
  box = create_box(cell_style: {padding: 0, border: {width: 0}})
727
800
  assert(box.fit(100, 10, @frame).overflow?)
728
801
  _, split_box = box.split
@@ -753,6 +826,49 @@ describe HexaPDF::Layout::TableBox do
753
826
  assert_operators(@canvas.contents, operators)
754
827
  end
755
828
 
829
+ it "correctly works for split cells" do
830
+ box = create_box(cells: [[@fixed_size_boxes[0], @fixed_size_boxes[1, 3]]],
831
+ cell_style: {padding: 0, border: {width: 0}})
832
+ box.cells[0, 0].style.background_color = 'red'
833
+ assert(box.fit(100, 10, @frame).overflow?)
834
+ _, split_box = box.split
835
+ assert(split_box.fit(100, 100, @frame).success?)
836
+
837
+ box.draw(@canvas, 20, 10)
838
+ split_box.draw(@canvas, 0, 50)
839
+ operators = [[:save_graphics_state],
840
+ [:set_device_rgb_non_stroking_color, [1, 0, 0]],
841
+ [:append_rectangle, [20, 10, 50, 10]],
842
+ [:fill_path_non_zero],
843
+ [:restore_graphics_state],
844
+ [:save_graphics_state],
845
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 10]],
846
+ [:move_to, [0, 0]],
847
+ [:end_path],
848
+ [:restore_graphics_state],
849
+ [:save_graphics_state],
850
+ [:concatenate_matrix, [1, 0, 0, 1, 70, 10]],
851
+ [:move_to, [0, 0]],
852
+ [:end_path],
853
+ [:restore_graphics_state],
854
+ [:save_graphics_state],
855
+ [:set_device_rgb_non_stroking_color, [1, 0, 0]],
856
+ [:append_rectangle, [0, 50, 50, 20]],
857
+ [:fill_path_non_zero],
858
+ [:restore_graphics_state],
859
+ [:save_graphics_state],
860
+ [:concatenate_matrix, [1, 0, 0, 1, 50, 60]],
861
+ [:move_to, [0, 0]],
862
+ [:end_path],
863
+ [:restore_graphics_state],
864
+ [:save_graphics_state],
865
+ [:concatenate_matrix, [1, 0, 0, 1, 50, 50]],
866
+ [:move_to, [0, 0]],
867
+ [:end_path],
868
+ [:restore_graphics_state]]
869
+ assert_operators(@canvas.contents, operators)
870
+ end
871
+
756
872
  it "correctly works for tables with headers and footers" do
757
873
  box = create_box(header: lambda {|_| [@fixed_size_boxes[10, 1]] },
758
874
  footer: lambda {|_| [@fixed_size_boxes[12, 1]] },
@@ -50,24 +50,29 @@ describe HexaPDF::Layout::TextFragment do
50
50
  it "replaces invalid glyphs with the result of the block" do
51
51
  zapf_dingbats = @doc.fonts.add('ZapfDingbats')
52
52
  i = 0
53
- fallback = lambda do |codepoint, _invalid_glyph|
54
- case (i += 1) % 3
53
+ fallback = lambda do |codepoint, invalid_glyph|
54
+ case (i += 1) % 4
55
55
  when 0 then []
56
56
  when 1 then [zapf_dingbats.decode_codepoint(codepoint)]
57
57
  when 2 then @font.decode_utf8("Tom")
58
+ when 3 then [invalid_glyph]
58
59
  end
59
60
  end
60
61
  style = HexaPDF::Layout::Style.new(font: @font, font_size: 20, font_features: {kern: true})
61
62
 
62
- frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("✂Tom✂✂Tom✂", style, &fallback)
63
- assert_equal(5, frags.size)
63
+ frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("✂TomTom✂Tom✂Tom\u{ad}",
64
+ style, &fallback)
65
+ assert_equal(8, frags.size)
64
66
  assert_equal(zapf_dingbats, frags[0].style.font)
65
67
  assert_equal(:a2, frags[0].items[0].name)
68
+ assert_equal("Tom", frags[1].text)
69
+ assert_equal("Tom", frags[2].text)
66
70
  assert_equal(@font, frags[2].style.font)
67
-
68
- frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("Tom✂Tom", style, &fallback)
69
- assert_equal(3, frags.size)
70
- assert_equal(frags[0].width, frags[1].width)
71
+ assert_equal("Tom", frags[3].text)
72
+ assert_equal(:'.notdef', frags[4].items[0].name)
73
+ assert_equal("Tom", frags[5].text)
74
+ assert_equal("Tom", frags[6].text)
75
+ assert_equal("\u{ad}", frags[7].text)
71
76
  end
72
77
  end
73
78
 
@@ -173,6 +178,11 @@ describe HexaPDF::Layout::TextFragment do
173
178
  [:move_text_next_line]])
174
179
  end
175
180
 
181
+ it "without any horizontal or vertical movement" do
182
+ @fragment.draw(@canvas, 0, 0, ignore_text_properties: true)
183
+ assert_operators(@canvas.contents, [[:begin_text]])
184
+ end
185
+
176
186
  it "only horizontal movement" do
177
187
  @fragment.draw(@canvas, 20, 0, ignore_text_properties: true)
178
188
  assert_operators(@canvas.contents, [[:begin_text],
@@ -15,7 +15,7 @@ describe HexaPDF::Layout::TextShaper do
15
15
  end
16
16
 
17
17
  def setup_fragment(items, **options)
18
- style = HexaPDF::Layout::Style.new(font: @font, font_size: 20, font_features: options)
18
+ style = HexaPDF::Layout::Style.new(font: @font, font_size: 20, **options)
19
19
  HexaPDF::Layout::TextFragment.new(items, style)
20
20
  end
21
21
 
@@ -26,14 +26,15 @@ describe HexaPDF::Layout::TextShaper do
26
26
 
27
27
  it "handles ligatures" do
28
28
  fragment = setup_fragment(@font.decode_utf8('fish fish fi').insert(1, 100).
29
- insert(0, 100), liga: true)
29
+ insert(0, 100), font_features: {liga: true})
30
30
  @shaper.shape_text(fragment)
31
31
  assert_equal([100, :fi, :s, :h, :space, :fi, :s, :h, :space, :fi],
32
32
  fragment.items.map {|item| item.kind_of?(Numeric) ? item : item.id })
33
33
  end
34
34
 
35
35
  it "handles kerning" do
36
- fragment = setup_fragment(@font.decode_utf8('fish fish wow').insert(1, 100), kern: true)
36
+ fragment = setup_fragment(@font.decode_utf8('fish fish wow').insert(1, 100),
37
+ font_features: {kern: true})
37
38
  @shaper.shape_text(fragment)
38
39
  assert_equal([:f, 100, :i, :s, :h, :space, :f, 20, :i, :s, :h, :space, :w, 10, :o, 25, :w],
39
40
  fragment.items.map {|item| item.kind_of?(Numeric) ? item : item.id })
@@ -47,16 +48,65 @@ describe HexaPDF::Layout::TextShaper do
47
48
  @font = HexaPDF::Font::TrueTypeWrapper.new(@doc, @wrapped_font)
48
49
  end
49
50
 
50
- it "handles kerning" do
51
+ it "handles kerning via the kern table" do
51
52
  data = [0, 1].pack('n2') <<
52
53
  [0, 6 + 8 + 12, 0x1].pack('n3') <<
53
54
  [2, 0, 0, 0, 53, 80, -20, 80, 81, -10].pack('n4n2s>n2s>')
54
55
  table = create_table(:Kern, data, standalone: true)
55
56
  @wrapped_font.instance_eval { @tables[:kern] = table }
56
- fragment = setup_fragment(@font.decode_utf8('Top Top').insert(1, 100), kern: true)
57
+ fragment = setup_fragment(@font.decode_utf8('Top Top').insert(1, 100),
58
+ shaping_engine: :internal, font_features: {kern: true})
57
59
  @shaper.shape_text(fragment)
58
60
  assert_equal([53, [100], 80, [10], 81, 3, 53, [20], 80, [10], 81],
59
61
  fragment.items.map {|item| item.kind_of?(Numeric) ? [item] : item.id })
60
62
  end
63
+
64
+ describe "HarfBuzz OpenType shaper" do
65
+ before do
66
+ @font = @doc.fonts.add('Inter')
67
+ skip if Gem.win_platform?
68
+ end
69
+
70
+ it "performs the shaping" do
71
+ # Test composition of o+diaresis, invalid char \n, kerning WA, x/y offsets with marks
72
+ fragment = setup_fragment(@font.decode_utf8("ö\nWAď̄"), shaping_engine: :harfbuzz,
73
+ font_features: {kern: true})
74
+ assert_equal(8, fragment.items.size)
75
+ result = @shaper.shape_text(fragment)
76
+ assert_equal(2, result.size)
77
+ [[791, 0, 459, [56.640625], 2, 603],
78
+ [[664.55078125], 1773, [-664.55078125]]].each_with_index do |expected, index|
79
+ assert_equal(expected,
80
+ result[index].items.map {|item| item.kind_of?(Numeric) ? [item] : item.id })
81
+ end
82
+ assert_equal("\n", result[0].items[1].str)
83
+ end
84
+
85
+ it "handles glyphs with the same cluster number" do
86
+ # Force Harfbuzz into cluster level 0 to force the same cluster numbers
87
+ cluster_level_method = HarfBuzz::Buffer.instance_method(:cluster_level=)
88
+ HarfBuzz::Buffer.remove_method(:cluster_level=)
89
+ HarfBuzz::Buffer.define_method(:cluster_level=) {|val| }
90
+
91
+ fragment = setup_fragment(@font.decode_utf8("ď̄aď̄"), shaping_engine: :harfbuzz)
92
+ result = @shaper.shape_text(fragment)
93
+ assert_equal("ď̄", result[0].items[0].str)
94
+ assert_equal("", result[1].items[1].str)
95
+ ensure
96
+ HarfBuzz::Buffer.remove_method(:cluster_level=)
97
+ HarfBuzz::Buffer.define_method(:cluster_level=, cluster_level_method)
98
+ end
99
+
100
+ it "raises an error if the harfbuzz-ruby gem is not available" do
101
+ Object.send(:remove_const, :HARFBUZZ_AVAILABLE)
102
+ Object.const_set(:HARFBUZZ_AVAILABLE, false)
103
+ fragment = setup_fragment(@font.decode_utf8('test'), shaping_engine: :harfbuzz)
104
+ error = assert_raises(HexaPDF::Error) { @shaper.shape_text(fragment) }
105
+ assert_match(/harfbuzz-ruby/, error.message)
106
+ ensure
107
+ Object.send(:remove_const, :HARFBUZZ_AVAILABLE)
108
+ Object.const_set(:HARFBUZZ_AVAILABLE, true)
109
+ end
110
+ end
61
111
  end
62
112
  end
@@ -605,4 +605,67 @@ describe HexaPDF::Type::Annotations::AppearanceGenerator do
605
605
  [:fill_and_stroke_path_non_zero]], range: 6..-1)
606
606
  end
607
607
  end
608
+
609
+ describe "ink" do
610
+ before do
611
+ @ink = @doc.add({Type: :Annot, Subtype: :Ink, C: [0],
612
+ InkList: [[100, 100, 200, 150], [210, 80, 110, 160]]})
613
+ @generator = HexaPDF::Type::Annotations::AppearanceGenerator.new(@ink)
614
+ end
615
+
616
+ it "sets the print flag and unsets the hidden flag" do
617
+ @ink.flag(:hidden)
618
+ @generator.create_appearance
619
+ assert(@ink.flagged?(:print))
620
+ refute(@ink.flagged?(:hidden))
621
+ end
622
+
623
+ it "creates the appearance" do
624
+ @generator.create_appearance
625
+ assert_equal([96, 76, 214, 164], @ink[:Rect])
626
+ assert_equal([96, 76, 214, 164], @ink.appearance[:BBox])
627
+ assert_operators(@ink.appearance.stream,
628
+ [[:move_to, [100, 100]],
629
+ [:line_to, [200, 150]],
630
+ [:move_to, [210, 80]],
631
+ [:line_to, [110, 160]],
632
+ [:stroke_path]])
633
+ end
634
+
635
+ describe "stroke color" do
636
+ it "uses the specified border color for stroking operations" do
637
+ @ink.border_style(color: "red")
638
+ @generator.create_appearance
639
+ assert_operators(@ink.appearance.stream,
640
+ [:set_device_rgb_stroking_color, [1, 0, 0]], range: 0)
641
+ end
642
+
643
+ it "works with a transparent border" do
644
+ @ink.border_style(color: :transparent)
645
+ @generator.create_appearance
646
+ assert_operators(@ink.appearance.stream, [:end_path], range: 4)
647
+ end
648
+ end
649
+
650
+ it "sets the specified border line width" do
651
+ @ink.border_style(width: 4)
652
+ @generator.create_appearance
653
+ assert_operators(@ink.appearance.stream,
654
+ [:set_line_width, [4]], range: 0)
655
+ end
656
+
657
+ it "sets the specified line dash pattern if it is an array" do
658
+ @ink.border_style(style: [5, 2])
659
+ @generator.create_appearance
660
+ assert_operators(@ink.appearance.stream,
661
+ [:set_line_dash_pattern, [[5, 2], 0]], range: 0)
662
+ end
663
+
664
+ it "sets the specified opacity" do
665
+ @ink.opacity(fill_alpha: 0.5, stroke_alpha: 0.5)
666
+ @generator.create_appearance
667
+ assert_operators(@ink.appearance.stream,
668
+ [:set_graphics_state_parameters, [:GS1]], range: 0)
669
+ end
670
+ end
608
671
  end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/annotations/ink'
6
+
7
+ describe HexaPDF::Type::Annotations::Ink do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @annot = @doc.add({Type: :Annot, Subtype: :Ink, Rect: [0, 0, 0, 0]})
11
+ end
12
+
13
+ it "returns the paths" do
14
+ assert_equal([], @annot.paths)
15
+ @annot[:InkList] = [[10, 20, 30, 40], [50, 60, 70, 80]]
16
+ assert_equal([[10, 20, 30, 40], [50, 60, 70, 80]], @annot.paths)
17
+ end
18
+
19
+ describe "add_paths" do
20
+ it "adds the given path" do
21
+ assert_same(@annot, @annot.add_path(1, 2, 3, 4, 5, 6))
22
+ assert_equal([[1, 2, 3, 4, 5, 6]], @annot[:InkList])
23
+ @annot.add_path(7, 8, 9, 10)
24
+ assert_equal([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10]], @annot[:InkList])
25
+ end
26
+
27
+ it "raises an ArgumentError if an uneven number of arguments is provided" do
28
+ assert_raises(ArgumentError) { @annot.add_path(1, 2, 3) }
29
+ end
30
+ end
31
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-06-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: cmdparse
@@ -77,6 +77,20 @@ dependencies:
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
79
  version: 3.1.2
80
+ - !ruby/object:Gem::Dependency
81
+ name: harfbuzz-ruby
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '1.0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
80
94
  - !ruby/object:Gem::Dependency
81
95
  name: brotli
82
96
  requirement: !ruby/object:Gem::Requirement
@@ -315,6 +329,11 @@ files:
315
329
  - data/hexapdf/cmap/V
316
330
  - data/hexapdf/encoding/glyphlist.txt
317
331
  - data/hexapdf/encoding/zapfdingbats.txt
332
+ - data/hexapdf/fonts/Inter-Bold.ttf
333
+ - data/hexapdf/fonts/Inter-BoldItalic.ttf
334
+ - data/hexapdf/fonts/Inter-Italic.ttf
335
+ - data/hexapdf/fonts/Inter-Regular.ttf
336
+ - data/hexapdf/fonts/OFL.txt
318
337
  - data/hexapdf/sRGB2014.icc
319
338
  - data/hexapdf/sRGB2014.icc.LICENSE
320
339
  - examples/001-hello_world.rb
@@ -350,6 +369,7 @@ files:
350
369
  - examples/031-acro_form_java_script.rb
351
370
  - examples/032-acro_form_list_and_fill.rb
352
371
  - examples/033-text_extraction.rb
372
+ - examples/034-text_shaping.rb
353
373
  - examples/emoji-smile.png
354
374
  - examples/emoji-wink.png
355
375
  - examples/machupicchu.jpg
@@ -546,6 +566,7 @@ files:
546
566
  - lib/hexapdf/type/annotations/border_effect.rb
547
567
  - lib/hexapdf/type/annotations/border_styling.rb
548
568
  - lib/hexapdf/type/annotations/circle.rb
569
+ - lib/hexapdf/type/annotations/ink.rb
549
570
  - lib/hexapdf/type/annotations/interior_color.rb
550
571
  - lib/hexapdf/type/annotations/line.rb
551
572
  - lib/hexapdf/type/annotations/line_ending_styling.rb
@@ -842,6 +863,7 @@ files:
842
863
  - test/hexapdf/type/annotations/test_appearance_generator.rb
843
864
  - test/hexapdf/type/annotations/test_border_effect.rb
844
865
  - test/hexapdf/type/annotations/test_border_styling.rb
866
+ - test/hexapdf/type/annotations/test_ink.rb
845
867
  - test/hexapdf/type/annotations/test_interior_color.rb
846
868
  - test/hexapdf/type/annotations/test_line.rb
847
869
  - test/hexapdf/type/annotations/test_line_ending_styling.rb
@@ -903,7 +925,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
903
925
  - !ruby/object:Gem::Version
904
926
  version: '0'
905
927
  requirements: []
906
- rubygems_version: 4.0.3
928
+ rubygems_version: 3.6.2
907
929
  specification_version: 4
908
930
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
909
931
  test_files: []