hexapdf 0.4.0 → 0.5.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 +46 -0
- data/CONTRIBUTERS +1 -1
- data/README.md +5 -5
- data/VERSION +1 -1
- data/examples/emoji-smile.png +0 -0
- data/examples/emoji-wink.png +0 -0
- data/examples/graphics.rb +9 -8
- data/examples/standard_pdf_fonts.rb +2 -1
- data/examples/text_box_alignment.rb +47 -0
- data/examples/text_box_inline_boxes.rb +56 -0
- data/examples/text_box_line_wrapping.rb +57 -0
- data/examples/text_box_shapes.rb +166 -0
- data/examples/text_box_styling.rb +72 -0
- data/examples/truetype.rb +3 -4
- data/lib/hexapdf/cli/optimize.rb +2 -2
- data/lib/hexapdf/configuration.rb +8 -6
- data/lib/hexapdf/content/canvas.rb +8 -5
- data/lib/hexapdf/content/parser.rb +3 -2
- data/lib/hexapdf/content/processor.rb +14 -3
- data/lib/hexapdf/document.rb +1 -0
- data/lib/hexapdf/document/fonts.rb +2 -1
- data/lib/hexapdf/document/pages.rb +23 -0
- data/lib/hexapdf/font/invalid_glyph.rb +78 -0
- data/lib/hexapdf/font/true_type/font.rb +14 -3
- data/lib/hexapdf/font/true_type/table.rb +1 -0
- data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
- data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -0
- data/lib/hexapdf/font/true_type/table/glyf.rb +4 -0
- data/lib/hexapdf/font/true_type/table/kern.rb +170 -0
- data/lib/hexapdf/font/true_type/table/post.rb +5 -1
- data/lib/hexapdf/font/true_type_wrapper.rb +71 -24
- data/lib/hexapdf/font/type1/afm_parser.rb +3 -2
- data/lib/hexapdf/font/type1/character_metrics.rb +0 -9
- data/lib/hexapdf/font/type1/font.rb +11 -0
- data/lib/hexapdf/font/type1/font_metrics.rb +6 -1
- data/lib/hexapdf/font/type1_wrapper.rb +51 -7
- data/lib/hexapdf/font_loader/standard14.rb +1 -1
- data/lib/hexapdf/layout.rb +51 -0
- data/lib/hexapdf/layout/inline_box.rb +95 -0
- data/lib/hexapdf/layout/line_fragment.rb +333 -0
- data/lib/hexapdf/layout/numeric_refinements.rb +56 -0
- data/lib/hexapdf/layout/style.rb +365 -0
- data/lib/hexapdf/layout/text_box.rb +727 -0
- data/lib/hexapdf/layout/text_fragment.rb +206 -0
- data/lib/hexapdf/layout/text_shaper.rb +155 -0
- data/lib/hexapdf/task.rb +0 -1
- data/lib/hexapdf/task/dereference.rb +1 -1
- data/lib/hexapdf/tokenizer.rb +3 -2
- data/lib/hexapdf/type/font_descriptor.rb +2 -1
- data/lib/hexapdf/type/font_type0.rb +3 -1
- data/lib/hexapdf/type/form.rb +12 -4
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +7 -0
- data/test/hexapdf/content/common.rb +8 -0
- data/test/hexapdf/content/test_canvas.rb +10 -22
- data/test/hexapdf/content/test_processor.rb +4 -1
- data/test/hexapdf/document/test_pages.rb +16 -0
- data/test/hexapdf/font/test_invalid_glyph.rb +34 -0
- data/test/hexapdf/font/test_true_type_wrapper.rb +25 -11
- data/test/hexapdf/font/test_type1_wrapper.rb +26 -10
- data/test/hexapdf/font/true_type/table/common.rb +27 -0
- data/test/hexapdf/font/true_type/table/test_cmap.rb +14 -20
- data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +7 -0
- data/test/hexapdf/font/true_type/table/test_glyf.rb +8 -6
- data/test/hexapdf/font/true_type/table/test_head.rb +9 -13
- data/test/hexapdf/font/true_type/table/test_hhea.rb +16 -23
- data/test/hexapdf/font/true_type/table/test_hmtx.rb +4 -7
- data/test/hexapdf/font/true_type/table/test_kern.rb +61 -0
- data/test/hexapdf/font/true_type/table/test_loca.rb +7 -13
- data/test/hexapdf/font/true_type/table/test_maxp.rb +4 -9
- data/test/hexapdf/font/true_type/table/test_name.rb +14 -17
- data/test/hexapdf/font/true_type/table/test_os2.rb +3 -5
- data/test/hexapdf/font/true_type/table/test_post.rb +21 -19
- data/test/hexapdf/font/true_type/test_font.rb +4 -0
- data/test/hexapdf/font/type1/common.rb +6 -0
- data/test/hexapdf/font/type1/test_afm_parser.rb +9 -0
- data/test/hexapdf/font/type1/test_font.rb +6 -0
- data/test/hexapdf/layout/test_inline_box.rb +40 -0
- data/test/hexapdf/layout/test_line_fragment.rb +206 -0
- data/test/hexapdf/layout/test_style.rb +143 -0
- data/test/hexapdf/layout/test_text_box.rb +640 -0
- data/test/hexapdf/layout/test_text_fragment.rb +208 -0
- data/test/hexapdf/layout/test_text_shaper.rb +64 -0
- data/test/hexapdf/task/test_dereference.rb +1 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/test_font_descriptor.rb +4 -2
- data/test/hexapdf/type/test_font_type0.rb +7 -0
- data/test/hexapdf/type/test_form.rb +12 -0
- metadata +29 -2
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/layout'
|
|
5
|
+
require 'hexapdf/document'
|
|
6
|
+
require_relative "../content/common"
|
|
7
|
+
|
|
8
|
+
module TestTextBoxHelpers
|
|
9
|
+
def boxes(*dims)
|
|
10
|
+
dims.map do |width, height|
|
|
11
|
+
box = HexaPDF::Layout::InlineBox.new(width, height || 0) {}
|
|
12
|
+
HexaPDF::Layout::TextBox::Box.new(box)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def glue(width)
|
|
17
|
+
HexaPDF::Layout::TextBox::Glue.new(HexaPDF::Layout::InlineBox.new(width, 0) {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def penalty(penalty, item = nil)
|
|
21
|
+
if item
|
|
22
|
+
HexaPDF::Layout::TextBox::Penalty.new(penalty, item.width, item: item)
|
|
23
|
+
else
|
|
24
|
+
HexaPDF::Layout::TextBox::Penalty.new(penalty)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def assert_box(obj, item)
|
|
29
|
+
assert_kind_of(HexaPDF::Layout::TextBox::Box, obj)
|
|
30
|
+
if obj.item.kind_of?(HexaPDF::Layout::InlineBox)
|
|
31
|
+
assert_same(item, obj.item)
|
|
32
|
+
else
|
|
33
|
+
assert_same(item.style, obj.item.style)
|
|
34
|
+
assert_equal(item.items, obj.item.items)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def assert_glue(obj, fragment)
|
|
39
|
+
assert_kind_of(HexaPDF::Layout::TextBox::Glue, obj)
|
|
40
|
+
assert_same(fragment.style, obj.item.style)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def assert_penalty(obj, penalty, item = nil)
|
|
44
|
+
assert_kind_of(HexaPDF::Layout::TextBox::Penalty, obj)
|
|
45
|
+
assert_equal(penalty, obj.penalty)
|
|
46
|
+
if item
|
|
47
|
+
assert_same(item.style, obj.item.style)
|
|
48
|
+
assert_equal(item.items, obj.item.items)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
|
|
54
|
+
include TestTextBoxHelpers
|
|
55
|
+
|
|
56
|
+
before do
|
|
57
|
+
@doc = HexaPDF::Document.new
|
|
58
|
+
@font = @doc.fonts.load("Times")
|
|
59
|
+
@obj = HexaPDF::Layout::TextBox::SimpleTextSegmentation
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def setup_fragment(text, style = nil)
|
|
63
|
+
if style
|
|
64
|
+
HexaPDF::Layout::TextFragment.new(items: style.font.decode_utf8(text), style: style)
|
|
65
|
+
else
|
|
66
|
+
HexaPDF::Layout::TextFragment.create(text, font: @font)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "handles InlineBox objects" do
|
|
71
|
+
input = HexaPDF::Layout::InlineBox.new(10, 10) { }
|
|
72
|
+
result = @obj.call([input, input])
|
|
73
|
+
assert_equal(2, result.size)
|
|
74
|
+
assert_box(result[0], input)
|
|
75
|
+
assert_box(result[1], input)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "handles plain text" do
|
|
79
|
+
frag = setup_fragment("Testtext")
|
|
80
|
+
result = @obj.call([frag])
|
|
81
|
+
assert_equal(1, result.size)
|
|
82
|
+
assert_box(result[0], frag)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "inserts a glue in places where spaces are" do
|
|
86
|
+
frag = setup_fragment("This is a test")
|
|
87
|
+
space = setup_fragment(" ", frag.style)
|
|
88
|
+
|
|
89
|
+
result = @obj.call([frag])
|
|
90
|
+
assert_equal(7, result.size)
|
|
91
|
+
assert_glue(result[1], space)
|
|
92
|
+
assert_glue(result[3], space)
|
|
93
|
+
assert_glue(result[5], space)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "inserts a glue representing 8 spaces when a tab is encountered" do
|
|
97
|
+
frag = setup_fragment("This\ttest")
|
|
98
|
+
tab = setup_fragment(" " * 8, frag.style)
|
|
99
|
+
|
|
100
|
+
result = @obj.call([frag])
|
|
101
|
+
assert_equal(3, result.size)
|
|
102
|
+
assert_glue(result[1], tab)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "insert a mandatory break when an Unicode line boundary characters is encountered" do
|
|
106
|
+
frag = setup_fragment("A\rB\r\nC\nD\vE\fF\u{85}G\u{2028}H\u{2029}I")
|
|
107
|
+
|
|
108
|
+
result = @obj.call([frag])
|
|
109
|
+
assert_equal(17, result.size)
|
|
110
|
+
[1, 3, 5, 7, 9, 11, 13, 15].each do |index|
|
|
111
|
+
assert_penalty(result[index], HexaPDF::Layout::TextBox::Penalty::MandatoryBreak.penalty)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "insert a standard penalty after a hyphen" do
|
|
116
|
+
frag = setup_fragment("hy-phen-a-tion - cool!")
|
|
117
|
+
|
|
118
|
+
result = @obj.call([frag])
|
|
119
|
+
assert_equal(12, result.size)
|
|
120
|
+
[1, 3, 5, 9].each do |index|
|
|
121
|
+
assert_penalty(result[index], HexaPDF::Layout::TextBox::Penalty::Standard.penalty)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "insert a neutral penalty in places where zero-width-spaces are" do
|
|
126
|
+
frag = setup_fragment("zero\u{200B}width\u{200B}space")
|
|
127
|
+
|
|
128
|
+
result = @obj.call([frag])
|
|
129
|
+
assert_equal(5, result.size)
|
|
130
|
+
assert_penalty(result[1], 0)
|
|
131
|
+
assert_penalty(result[3], 0)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "insert a special penalty for soft-hyphens" do
|
|
135
|
+
frag = setup_fragment("soft\u{00AD}hyphened")
|
|
136
|
+
hyphen = setup_fragment("-", frag.style)
|
|
137
|
+
|
|
138
|
+
result = @obj.call([frag])
|
|
139
|
+
assert_equal(3, result.size)
|
|
140
|
+
assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::Standard.penalty, hyphen)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "insert a prohibited break penalty for non-breaking spaces" do
|
|
144
|
+
frag = setup_fragment("soft\u{00A0}hyphened")
|
|
145
|
+
space = setup_fragment(" ", frag.style)
|
|
146
|
+
|
|
147
|
+
result = @obj.call([frag])
|
|
148
|
+
assert_equal(3, result.size)
|
|
149
|
+
assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::ProhibitedBreak.penalty, space)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Common tests for fixed and variable width line wrapping. The including class needs to define a
|
|
154
|
+
# #call(items, width = 100) method with a default with of 100.
|
|
155
|
+
module CommonLineWrappingTests
|
|
156
|
+
extend Minitest::Spec::DSL
|
|
157
|
+
|
|
158
|
+
include TestTextBoxHelpers
|
|
159
|
+
|
|
160
|
+
it "breaks before a box if it doesn't fit onto the line anymore" do
|
|
161
|
+
rest, lines = call(boxes(25, 50, 25, 10))
|
|
162
|
+
assert(rest.empty?)
|
|
163
|
+
assert_equal(2, lines.count)
|
|
164
|
+
lines.each {|line| line.items.each {|item| assert_kind_of(HexaPDF::Layout::InlineBox, item)}}
|
|
165
|
+
assert_equal(100, lines[0].width)
|
|
166
|
+
assert_equal(10, lines[1].width)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "breaks at a glue and ignores it if it doesn't fit onto the line anymore" do
|
|
170
|
+
rest, lines = call(boxes(90, 20).insert(-2, glue(20)))
|
|
171
|
+
assert(rest.empty?)
|
|
172
|
+
assert_equal(2, lines.count)
|
|
173
|
+
assert_equal(90, lines[0].width)
|
|
174
|
+
assert_equal(20, lines[1].width)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "handles spaces at the start of a line" do
|
|
178
|
+
rest, lines = call(boxes(25, 50).unshift(glue(15)))
|
|
179
|
+
assert(rest.empty?)
|
|
180
|
+
assert_equal(1, lines.count)
|
|
181
|
+
assert_equal(75, lines[0].width)
|
|
182
|
+
assert_equal(25, lines[0].items[0].width)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "handles spaces at the end of a line" do
|
|
186
|
+
rest, lines = call(boxes(20, 50, 20).insert(-2, glue(10)).insert(-2, glue(10)))
|
|
187
|
+
assert(rest.empty?)
|
|
188
|
+
assert_equal(2, lines.count)
|
|
189
|
+
assert_equal(70, lines[0].width)
|
|
190
|
+
assert_equal(20, lines[1].width)
|
|
191
|
+
assert_equal(50, lines[0].items[-1].width)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "handles spaces at the end of a line before a mandatory break" do
|
|
195
|
+
rest, lines = call(boxes(20, 50, 20).insert(-2, glue(10)).insert(-2, penalty(-5000)))
|
|
196
|
+
assert(rest.empty?)
|
|
197
|
+
assert_equal(2, lines.count)
|
|
198
|
+
assert_equal(70, lines[0].width)
|
|
199
|
+
assert_equal(20, lines[1].width)
|
|
200
|
+
assert_equal(50, lines[0].items[-1].width)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "handles multiple glue items after another" do
|
|
204
|
+
rest, lines = call(boxes(20, 20, 50, 20).insert(1, glue(20)).insert(1, glue(20)))
|
|
205
|
+
assert(rest.empty?)
|
|
206
|
+
assert_equal(2, lines.count)
|
|
207
|
+
assert_equal(80, lines[0].width)
|
|
208
|
+
assert_equal(70, lines[1].width)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "handles mandatory line breaks" do
|
|
212
|
+
rest, lines = call(boxes(20, 20).insert(-2, penalty(-5000)))
|
|
213
|
+
assert(rest.empty?)
|
|
214
|
+
assert_equal(2, lines.count)
|
|
215
|
+
assert_equal(20, lines[0].width)
|
|
216
|
+
assert_equal(20, lines[1].width)
|
|
217
|
+
assert(lines[0].ignore_justification?)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "handles breaking at penalties with zero width" do
|
|
221
|
+
rest, lines = call(boxes(80, 10, 20).insert(1, penalty(0)).insert(-2, penalty(0)))
|
|
222
|
+
assert(rest.empty?)
|
|
223
|
+
assert_equal(2, lines.count)
|
|
224
|
+
assert_equal(90, lines[0].width)
|
|
225
|
+
assert_equal(20, lines[1].width)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "handles breaking at penalties with non-zero width if they fit on the line" do
|
|
229
|
+
item = HexaPDF::Layout::InlineBox.new(20, 0) {}
|
|
230
|
+
rest, lines = call(boxes(20, 60, 30).insert(1, penalty(0, item)).insert(-2, penalty(0, item)))
|
|
231
|
+
assert(rest.empty?)
|
|
232
|
+
assert_equal(2, lines.count)
|
|
233
|
+
assert_equal(100, lines[0].width)
|
|
234
|
+
assert_equal(30, lines[1].width)
|
|
235
|
+
assert_same(item, lines[0].items[-1])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "handles penalties with non-zero width if they don't fit on the line" do
|
|
239
|
+
item = HexaPDF::Layout::InlineBox.new(20, 0) {}
|
|
240
|
+
rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(0, item)] + boxes(30))
|
|
241
|
+
assert(rest.empty?)
|
|
242
|
+
assert_equal(2, lines.count)
|
|
243
|
+
assert_equal(70, lines[0].width)
|
|
244
|
+
assert_equal(40, lines[1].width)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it "handles breaking at prohibited breakpoints by back-tracking to the last valid breakpoint " do
|
|
248
|
+
item = HexaPDF::Layout::InlineBox.new(20, 0) {}
|
|
249
|
+
rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
|
|
250
|
+
assert(rest.empty?)
|
|
251
|
+
assert_equal(2, lines.count)
|
|
252
|
+
assert_equal(70, lines[0].width)
|
|
253
|
+
assert_equal(60, lines[1].width)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "stops when nil is returned by the block: last item is a box" do
|
|
257
|
+
lines = []
|
|
258
|
+
rest = @obj.call(boxes(20, 20, 20), 20) {|line| lines << line; lines.count > 1 ? nil : true}
|
|
259
|
+
assert_equal(2, rest.count)
|
|
260
|
+
assert_equal(2, lines.count)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "stops when nil is returned by the block: last item is a glue" do
|
|
264
|
+
done = false
|
|
265
|
+
items = boxes(20, 15, 20).insert(-2, glue(10))
|
|
266
|
+
rest = @obj.call(items, 20) { done ? nil : (done = true; 20) }
|
|
267
|
+
assert_equal(3, rest.count)
|
|
268
|
+
assert_equal(15, rest[0].width)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "stops when nil is returned by the block: last item is a mandatory break penalty" do
|
|
272
|
+
items = boxes(20, 20).insert(-2, penalty(-5000))
|
|
273
|
+
rest = @obj.call(items, 20) { nil }
|
|
274
|
+
assert_equal(3, rest.count)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it "stops when nil is returned by the block: works for the last line" do
|
|
278
|
+
lines = []
|
|
279
|
+
rest = @obj.call(boxes(20, 20), 20) {|line| lines << line; lines.count > 1 ? nil : true}
|
|
280
|
+
assert_equal(1, rest.count)
|
|
281
|
+
assert_equal(2, lines.count)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
|
|
287
|
+
before do
|
|
288
|
+
@obj = HexaPDF::Layout::TextBox::SimpleLineWrapping
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
describe "fixed width wrapping" do
|
|
292
|
+
include CommonLineWrappingTests
|
|
293
|
+
|
|
294
|
+
def call(items, width = 100)
|
|
295
|
+
lines = []
|
|
296
|
+
rest = @obj.call(items, width) {|line, _| lines << line; true }
|
|
297
|
+
[rest, lines]
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe "variable width wrapping" do
|
|
302
|
+
include CommonLineWrappingTests
|
|
303
|
+
|
|
304
|
+
def call(items, width = proc { 100 })
|
|
305
|
+
lines = []
|
|
306
|
+
rest = @obj.call(items, width) {|line, _| lines << line; true }
|
|
307
|
+
[rest, lines]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "handles changing widths" do
|
|
311
|
+
height = 0
|
|
312
|
+
width_block = lambda do |line_height|
|
|
313
|
+
case height + line_height
|
|
314
|
+
when 0..10 then 60
|
|
315
|
+
when 11..20 then 40
|
|
316
|
+
when 21..30 then 20
|
|
317
|
+
else 60
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
lines = []
|
|
321
|
+
rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block) do |line|
|
|
322
|
+
height += line.height
|
|
323
|
+
lines << line
|
|
324
|
+
true
|
|
325
|
+
end
|
|
326
|
+
assert(rest.empty?)
|
|
327
|
+
assert_equal(3, lines.size)
|
|
328
|
+
assert_equal(30, lines[0].width)
|
|
329
|
+
assert_equal(20, lines[1].width)
|
|
330
|
+
assert_equal(40, lines[2].width)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "handles changing widths when breaking on a penalty" do
|
|
334
|
+
height = 0
|
|
335
|
+
width_block = lambda do |line_height|
|
|
336
|
+
case height + line_height
|
|
337
|
+
when 0..10 then 80
|
|
338
|
+
else 50
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
lines = []
|
|
342
|
+
item = HexaPDF::Layout::InlineBox.new(20, 10) {}
|
|
343
|
+
items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
|
|
344
|
+
rest = @obj.call(items, width_block) do |line|
|
|
345
|
+
height += line.height
|
|
346
|
+
lines << line
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
assert(rest.empty?)
|
|
350
|
+
assert_equal(2, lines.size)
|
|
351
|
+
assert_equal(40, lines[0].width)
|
|
352
|
+
assert_equal(40, lines[1].width)
|
|
353
|
+
assert_equal(25, height)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
describe HexaPDF::Layout::TextBox do
|
|
359
|
+
include TestTextBoxHelpers
|
|
360
|
+
|
|
361
|
+
before do
|
|
362
|
+
@doc = HexaPDF::Document.new
|
|
363
|
+
@font = @doc.fonts.load("Times")
|
|
364
|
+
@style = HexaPDF::Layout::Style.new(font: @font)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it "creates an instance from text and options" do
|
|
368
|
+
box = HexaPDF::Layout::TextBox.create("T", font: @font, width: 100, height: 100)
|
|
369
|
+
assert_equal(1, box.items.length)
|
|
370
|
+
assert_equal(@font.decode_utf8("T"), box.items[0].item.items)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it "doesn't run the text segmentation algorithm on already segmented items" do
|
|
374
|
+
item = HexaPDF::Layout::InlineBox.new(20, 0) {}
|
|
375
|
+
box = HexaPDF::Layout::TextBox.new(items: [item], width: 100, height: 100)
|
|
376
|
+
items = box.items
|
|
377
|
+
assert_equal(1, items.length)
|
|
378
|
+
assert_box(items[0], item)
|
|
379
|
+
|
|
380
|
+
box.items = items
|
|
381
|
+
assert_same(items, box.items)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe "fit" do
|
|
385
|
+
it "handles text indentation" do
|
|
386
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [20, 20], [20, 20]), width: 60,
|
|
387
|
+
style: @style)
|
|
388
|
+
box.style.text_indent = 20
|
|
389
|
+
rest, height = box.fit
|
|
390
|
+
assert_equal(60, box.lines[0].width)
|
|
391
|
+
assert_equal(20, box.lines[1].width)
|
|
392
|
+
assert(rest.empty?)
|
|
393
|
+
assert_equal(40, height)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "fits using unlimited height" do
|
|
397
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 100)), width: 20,
|
|
398
|
+
style: @style)
|
|
399
|
+
rest, height = box.fit
|
|
400
|
+
assert(rest.empty?)
|
|
401
|
+
assert_equal(20 * 100, height)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it "fits using a limited height" do
|
|
405
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 100)), width: 20, height: 100,
|
|
406
|
+
style: @style)
|
|
407
|
+
rest, height = box.fit
|
|
408
|
+
assert_equal(95, rest.count)
|
|
409
|
+
assert_equal(100, height)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it "takes line spacing into account when calculating the height" do
|
|
413
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 5)), width: 20, style: @style)
|
|
414
|
+
box.style.line_spacing = :double
|
|
415
|
+
rest, height = box.fit
|
|
416
|
+
assert(rest.empty?)
|
|
417
|
+
assert_equal(20 * (5 + 4), height)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
it "handles empty lines" do
|
|
421
|
+
items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
|
|
422
|
+
boxes([20, 20]) + [penalty(-5000)] * 2
|
|
423
|
+
box = HexaPDF::Layout::TextBox.new(items: items, width: 30, style: @style)
|
|
424
|
+
rest, height = box.fit
|
|
425
|
+
assert(rest.empty?)
|
|
426
|
+
assert_equal(5, box.lines.count)
|
|
427
|
+
assert_equal(20 + 20 + 9 + 20 + 9, height)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
describe "fixed width" do
|
|
431
|
+
it "stops if an item is wider than the available width, with unlimited height" do
|
|
432
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [50, 20]), width: 30,
|
|
433
|
+
style: @style)
|
|
434
|
+
rest, height = box.fit
|
|
435
|
+
assert_equal(1, rest.count)
|
|
436
|
+
assert_equal(20, height)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it "stops if an item is wider than the available width, with limited height" do
|
|
440
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [50, 20]), width: 30, height: 100,
|
|
441
|
+
style: @style)
|
|
442
|
+
rest, height = box.fit
|
|
443
|
+
assert_equal(1, rest.count)
|
|
444
|
+
assert_equal(20, height)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
describe "variable width with limited height" do
|
|
449
|
+
it "searches for a vertical offset if the first item is wider than the available width" do
|
|
450
|
+
width_block = lambda do |height, _|
|
|
451
|
+
case height
|
|
452
|
+
when 0..20 then 10
|
|
453
|
+
else 40
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes([20, 18]), width: width_block,
|
|
457
|
+
height: 100, style: @style)
|
|
458
|
+
rest, height = box.fit
|
|
459
|
+
assert(rest.empty?)
|
|
460
|
+
assert_equal(1, box.lines.count)
|
|
461
|
+
assert_equal(24, box.lines[0].y_offset)
|
|
462
|
+
assert_equal(42, height)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
it "searches for a vertical offset if an item is wider than the available width" do
|
|
466
|
+
width_block = lambda do |height, line_height|
|
|
467
|
+
if (40..60).cover?(height) || (40..60).cover?(height + line_height)
|
|
468
|
+
10
|
|
469
|
+
else
|
|
470
|
+
40
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 18]] * 7)), width: width_block,
|
|
474
|
+
height: 100, style: @style)
|
|
475
|
+
rest, height = box.fit
|
|
476
|
+
assert_equal(1, rest.count)
|
|
477
|
+
assert_equal(3, box.lines.count)
|
|
478
|
+
assert_equal(0, box.lines[0].y_offset)
|
|
479
|
+
assert_equal(18, box.lines[1].y_offset)
|
|
480
|
+
assert_equal(48, box.lines[2].y_offset)
|
|
481
|
+
assert_equal(84, height)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "post-processes lines for justification if needed" do
|
|
486
|
+
frag10 = HexaPDF::Layout::TextFragment.create(" ", font: @font)
|
|
487
|
+
frag10.items.freeze
|
|
488
|
+
frag10b = HexaPDF::Layout::TextBox::Box.new(frag10)
|
|
489
|
+
frag20 = HexaPDF::Layout::TextFragment.create(" ", font: @font, font_size: 20)
|
|
490
|
+
frag20b = HexaPDF::Layout::TextBox::Box.new(frag20)
|
|
491
|
+
items = boxes(20, 20, 20, 20, 30).insert(1, frag10b).insert(3, frag20b).insert(5, frag10b)
|
|
492
|
+
# Width of spaces: 2.5 * 2 + 5 = 10 (from AFM file, adjusted for font size)
|
|
493
|
+
# Line width: 20 * 4 + width_of_spaces = 90
|
|
494
|
+
# Missing width: 100 - 90 = 10
|
|
495
|
+
# -> Each space must be doubled!
|
|
496
|
+
|
|
497
|
+
box = HexaPDF::Layout::TextBox.new(items: items, width: 100)
|
|
498
|
+
box.style.align = :justify
|
|
499
|
+
rest, _height = box.fit
|
|
500
|
+
assert(rest.empty?)
|
|
501
|
+
assert_equal(9, box.lines[0].items.count)
|
|
502
|
+
assert_in_delta(100, box.lines[0].width)
|
|
503
|
+
assert_equal(-250, box.lines[0].items[1].items[0])
|
|
504
|
+
assert_equal(-250, box.lines[0].items[4].items[0])
|
|
505
|
+
assert_equal(-250, box.lines[0].items[6].items[0])
|
|
506
|
+
assert_equal(30, box.lines[1].width)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
it "applies the optional horizontal offsets if set" do
|
|
510
|
+
x_offsets = lambda {|height, line_height| height + line_height}
|
|
511
|
+
box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 10]] * 7)), width: 60,
|
|
512
|
+
x_offsets: x_offsets, height: 100, style: @style)
|
|
513
|
+
rest, height = box.fit
|
|
514
|
+
assert(rest.empty?)
|
|
515
|
+
assert_equal(30, height)
|
|
516
|
+
assert_equal(10, box.lines[0].x_offset)
|
|
517
|
+
assert_equal(20, box.lines[1].x_offset)
|
|
518
|
+
assert_equal(30, box.lines[2].x_offset)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
describe "draw" do
|
|
523
|
+
def assert_positions(content, positions)
|
|
524
|
+
processor = TestHelper::OperatorRecorder.new
|
|
525
|
+
HexaPDF::Content::Parser.new.parse(content, processor)
|
|
526
|
+
result = processor.recorded_ops
|
|
527
|
+
result.select! {|name, _| name == :set_text_matrix}.map! {|_, ops| ops[-2, 2]}
|
|
528
|
+
positions.each_with_index do |(x, y), index|
|
|
529
|
+
assert_in_delta(x, result[index][0], 0.00001)
|
|
530
|
+
assert_in_delta(y, result[index][1], 0.00001)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
before do
|
|
535
|
+
@frag = HexaPDF::Layout::TextFragment.create("This is some more text.\n" \
|
|
536
|
+
"This is some more text.", font: @font)
|
|
537
|
+
@width = HexaPDF::Layout::TextFragment.create("This is some ", font: @font).width
|
|
538
|
+
@box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width)
|
|
539
|
+
@canvas = @doc.pages.add.canvas
|
|
540
|
+
|
|
541
|
+
@line1w = HexaPDF::Layout::TextFragment.create("This is some", font: @font).width
|
|
542
|
+
@line2w = HexaPDF::Layout::TextFragment.create("more text.", font: @font).width
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
it "can horizontally align the contents to the left" do
|
|
546
|
+
top = 100
|
|
547
|
+
@box.style.align = :left
|
|
548
|
+
@box.draw(@canvas, 5, top)
|
|
549
|
+
assert_positions(@canvas.contents,
|
|
550
|
+
[[5, top - @frag.y_max],
|
|
551
|
+
[5, top - @frag.y_max - @frag.height],
|
|
552
|
+
[5, top - @frag.y_max - @frag.height * 2],
|
|
553
|
+
[5, top - @frag.y_max - @frag.height * 3]])
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
it "can horizontally align the contents to the center" do
|
|
557
|
+
top = 100
|
|
558
|
+
@box.style.align = :center
|
|
559
|
+
@box.draw(@canvas, 5, top)
|
|
560
|
+
assert_positions(@canvas.contents,
|
|
561
|
+
[[5 + (@width - @line1w) / 2, top - @frag.y_max],
|
|
562
|
+
[5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height],
|
|
563
|
+
[5 + (@width - @line1w) / 2, top - @frag.y_max - @frag.height * 2],
|
|
564
|
+
[5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height * 3]])
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
it "can horizontally align the contents to the right" do
|
|
568
|
+
top = 100
|
|
569
|
+
@box.style.align = :right
|
|
570
|
+
@box.draw(@canvas, 5, top)
|
|
571
|
+
assert_positions(@canvas.contents,
|
|
572
|
+
[[5 + @width - @line1w, top - @frag.y_max],
|
|
573
|
+
[5 + @width - @line2w, top - @frag.y_max - @frag.height],
|
|
574
|
+
[5 + @width - @line1w, top - @frag.y_max - @frag.height * 2],
|
|
575
|
+
[5 + @width - @line2w, top - @frag.y_max - @frag.height * 3]])
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
it "can justify the contents" do
|
|
579
|
+
top = 100
|
|
580
|
+
@box.style.align = :justify
|
|
581
|
+
@box.draw(@canvas, 5, top)
|
|
582
|
+
assert_positions(@canvas.contents,
|
|
583
|
+
[[5, top - @frag.y_max],
|
|
584
|
+
[5, top - @frag.y_max - @frag.height],
|
|
585
|
+
[5, top - @frag.y_max - @frag.height * 2],
|
|
586
|
+
[5, top - @frag.y_max - @frag.height * 3]])
|
|
587
|
+
assert_in_delta(@width, @box.lines[0].width, 0.0001)
|
|
588
|
+
assert_in_delta(@width, @box.lines[2].width, 0.0001)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
it "doesn't justify lines ending in a mandatory break or the last line" do
|
|
592
|
+
@box.style.align = :justify
|
|
593
|
+
@box.draw(@canvas, 5, 100)
|
|
594
|
+
assert_equal(@line2w, @box.lines[1].width, 0.0001)
|
|
595
|
+
assert_equal(@line2w, @box.lines[3].width, 0.0001)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
it "can vertically align the contents in the center" do
|
|
599
|
+
top = 100
|
|
600
|
+
@box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
|
|
601
|
+
@box.style.valign = :center
|
|
602
|
+
|
|
603
|
+
_, height = @box.fit
|
|
604
|
+
initial_baseline = top - ((top - height) / 2) - @frag.y_max
|
|
605
|
+
@box.draw(@canvas, 5, top)
|
|
606
|
+
assert_positions(@canvas.contents,
|
|
607
|
+
[[5, initial_baseline],
|
|
608
|
+
[5, initial_baseline - @frag.height],
|
|
609
|
+
[5, initial_baseline - @frag.height * 2],
|
|
610
|
+
[5, initial_baseline - @frag.height * 3]])
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it "can vertically align the contents to the bottom" do
|
|
614
|
+
top = 100
|
|
615
|
+
@box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
|
|
616
|
+
@box.style.valign = :bottom
|
|
617
|
+
|
|
618
|
+
_, height = @box.fit
|
|
619
|
+
initial_baseline = height - @frag.y_max
|
|
620
|
+
@box.draw(@canvas, 5, top)
|
|
621
|
+
assert_positions(@canvas.contents,
|
|
622
|
+
[[5, initial_baseline],
|
|
623
|
+
[5, initial_baseline - @frag.height],
|
|
624
|
+
[5, initial_baseline - @frag.height * 2],
|
|
625
|
+
[5, initial_baseline - @frag.height * 3]])
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
it "raises an error if vertical alignment is :center/:bottom and an unlimited height is used" do
|
|
629
|
+
@box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width)
|
|
630
|
+
assert_raises(HexaPDF::Error) do
|
|
631
|
+
@box.style.valign = :center
|
|
632
|
+
@box.draw(@canvas, 0, 0)
|
|
633
|
+
end
|
|
634
|
+
assert_raises(HexaPDF::Error) do
|
|
635
|
+
@box.style.valign = :bottom
|
|
636
|
+
@box.draw(@canvas, 0, 0)
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
end
|