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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +3 -0
- data/Rakefile +1 -1
- data/data/hexapdf/fonts/Inter-Bold.ttf +0 -0
- data/data/hexapdf/fonts/Inter-BoldItalic.ttf +0 -0
- data/data/hexapdf/fonts/Inter-Italic.ttf +0 -0
- data/data/hexapdf/fonts/Inter-Regular.ttf +0 -0
- data/data/hexapdf/fonts/OFL.txt +92 -0
- data/examples/019-acro_form.rb +3 -1
- data/examples/030-pdfa.rb +9 -16
- data/examples/034-text_shaping.rb +37 -0
- data/lib/hexapdf/configuration.rb +13 -1
- data/lib/hexapdf/document/annotations.rb +25 -0
- data/lib/hexapdf/font/cmap/writer.rb +15 -9
- data/lib/hexapdf/font/true_type_wrapper.rb +6 -3
- data/lib/hexapdf/font_loader.rb +47 -0
- data/lib/hexapdf/layout/container_box.rb +2 -4
- data/lib/hexapdf/layout/style.rb +66 -4
- data/lib/hexapdf/layout/table_box.rb +96 -13
- data/lib/hexapdf/layout/text_fragment.rb +13 -7
- data/lib/hexapdf/layout/text_shaper.rb +162 -10
- data/lib/hexapdf/type/annotations/appearance_generator.rb +42 -0
- data/lib/hexapdf/type/annotations/ink.rb +107 -0
- data/lib/hexapdf/type/annotations.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/digital_signature/common.rb +5 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +1 -1
- data/test/hexapdf/document/test_annotations.rb +10 -0
- data/test/hexapdf/document/test_layout.rb +6 -3
- data/test/hexapdf/font/cmap/test_writer.rb +8 -6
- data/test/hexapdf/font/test_true_type_wrapper.rb +4 -0
- data/test/hexapdf/layout/test_container_box.rb +3 -1
- data/test/hexapdf/layout/test_style.rb +4 -0
- data/test/hexapdf/layout/test_table_box.rb +117 -1
- data/test/hexapdf/layout/test_text_fragment.rb +18 -8
- data/test/hexapdf/layout/test_text_shaper.rb +55 -5
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +63 -0
- data/test/hexapdf/type/annotations/test_ink.rb +31 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
93
|
-
@to_unicode_cmap_data.sub!(/
|
|
94
|
-
@to_unicode_cmap_data.sub!(/<
|
|
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
|
|
102
|
+
@to_unicode_mapping[-2, 2] = []
|
|
101
103
|
@to_unicode_mapping.delete_at(0x5f)
|
|
102
|
-
@to_unicode_cmap_data.sub!(/\
|
|
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
|
|
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,
|
|
54
|
-
case (i += 1) %
|
|
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
|
|
63
|
-
|
|
63
|
+
frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("✂Tom✂Tom✂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
|
-
|
|
69
|
-
assert_equal(
|
|
70
|
-
assert_equal(
|
|
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,
|
|
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),
|
|
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),
|
|
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.
|
|
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:
|
|
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:
|
|
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: []
|