krill 0.1.0 → 0.2.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/.rspec +1 -0
- data/README.md +1 -2
- data/Rakefile +7 -7
- data/krill.gemspec +3 -1
- data/lib/krill.rb +21 -1
- data/lib/krill/afm.rb +174 -0
- data/lib/krill/arranger.rb +182 -0
- data/lib/krill/formatter.rb +108 -0
- data/lib/krill/fragment.rb +146 -0
- data/lib/krill/line_wrap.rb +287 -0
- data/lib/krill/text_box.rb +244 -0
- data/lib/krill/ttf.rb +78 -0
- data/lib/krill/version.rb +1 -1
- data/lib/krill/wrapped_text.rb +17 -0
- metadata +42 -6
- data/LICENSE.txt +0 -21
@@ -0,0 +1,146 @@
|
|
1
|
+
module Krill
|
2
|
+
class Fragment
|
3
|
+
attr_reader :format_state, :text
|
4
|
+
attr_writer :width
|
5
|
+
attr_accessor :line_height, :descender, :ascender
|
6
|
+
attr_accessor :word_spacing, :left, :baseline
|
7
|
+
|
8
|
+
attr_reader :font
|
9
|
+
alias formatter font
|
10
|
+
|
11
|
+
def initialize(text, format_state)
|
12
|
+
@format_state = format_state
|
13
|
+
@font = format_state.fetch(:font)
|
14
|
+
@word_spacing = 0
|
15
|
+
|
16
|
+
# keep the original value of "text", so we can reinitialize @text if formatting parameters
|
17
|
+
# like text direction are changed
|
18
|
+
@original_text = text
|
19
|
+
@text = process_text(@original_text)
|
20
|
+
end
|
21
|
+
|
22
|
+
def width
|
23
|
+
return @width if @word_spacing.zero?
|
24
|
+
@width + @word_spacing * space_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def height
|
28
|
+
top - bottom
|
29
|
+
end
|
30
|
+
|
31
|
+
def superscript?
|
32
|
+
formatter.superscript?
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscript?
|
36
|
+
formatter.subscript?
|
37
|
+
end
|
38
|
+
|
39
|
+
def character_spacing
|
40
|
+
formatter.character_spacing
|
41
|
+
end
|
42
|
+
|
43
|
+
def y_offset
|
44
|
+
if subscript? then -descender
|
45
|
+
elsif superscript? then 0.85 * ascender
|
46
|
+
else 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def underline_points
|
51
|
+
y = baseline - 1.25
|
52
|
+
[[left, y], [right, y]]
|
53
|
+
end
|
54
|
+
|
55
|
+
def strikethrough_points
|
56
|
+
y = baseline + ascender * 0.3
|
57
|
+
[[left, y], [right, y]]
|
58
|
+
end
|
59
|
+
|
60
|
+
def direction
|
61
|
+
@format_state[:direction]
|
62
|
+
end
|
63
|
+
|
64
|
+
def default_direction=(direction)
|
65
|
+
unless @format_state[:direction]
|
66
|
+
@format_state[:direction] = direction
|
67
|
+
@text = process_text(@original_text)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def include_trailing_white_space!
|
72
|
+
@format_state.delete(:exclude_trailing_white_space)
|
73
|
+
@text = process_text(@original_text)
|
74
|
+
end
|
75
|
+
|
76
|
+
def space_count
|
77
|
+
@text.count(" ")
|
78
|
+
end
|
79
|
+
|
80
|
+
def right
|
81
|
+
left + width
|
82
|
+
end
|
83
|
+
|
84
|
+
def top
|
85
|
+
baseline + ascender
|
86
|
+
end
|
87
|
+
|
88
|
+
def bottom
|
89
|
+
baseline - descender
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def process_text(text)
|
95
|
+
string = strip_zero_width_spaces(text)
|
96
|
+
|
97
|
+
if exclude_trailing_white_space?
|
98
|
+
string = string.rstrip
|
99
|
+
|
100
|
+
if soft_hyphens_need_processing?(string)
|
101
|
+
string = process_soft_hyphens(string[0..-2]) + string[-1..-1]
|
102
|
+
end
|
103
|
+
else
|
104
|
+
if soft_hyphens_need_processing?(string)
|
105
|
+
string = process_soft_hyphens(string)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
case direction
|
110
|
+
when :rtl
|
111
|
+
string.reverse
|
112
|
+
else
|
113
|
+
string
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def exclude_trailing_white_space?
|
118
|
+
@format_state[:exclude_trailing_white_space]
|
119
|
+
end
|
120
|
+
|
121
|
+
def soft_hyphens_need_processing?(string)
|
122
|
+
string.length > 0 && normalized_soft_hyphen
|
123
|
+
end
|
124
|
+
|
125
|
+
def normalized_soft_hyphen
|
126
|
+
@format_state[:normalized_soft_hyphen]
|
127
|
+
end
|
128
|
+
|
129
|
+
def process_soft_hyphens(string)
|
130
|
+
if string.encoding != normalized_soft_hyphen.encoding
|
131
|
+
string.force_encoding(normalized_soft_hyphen.encoding)
|
132
|
+
end
|
133
|
+
|
134
|
+
string.gsub(normalized_soft_hyphen, "")
|
135
|
+
end
|
136
|
+
|
137
|
+
def strip_zero_width_spaces(string)
|
138
|
+
if string.encoding == ::Encoding::UTF_8
|
139
|
+
string.gsub(ZWSP, "")
|
140
|
+
else
|
141
|
+
string
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
module Krill
|
2
|
+
class LineWrap
|
3
|
+
|
4
|
+
# The width of the last wrapped line
|
5
|
+
#
|
6
|
+
def width
|
7
|
+
@accumulated_width || 0
|
8
|
+
end
|
9
|
+
|
10
|
+
# The number of spaces in the last wrapped line
|
11
|
+
attr_reader :space_count
|
12
|
+
attr_reader :soft_hyphen
|
13
|
+
attr_reader :zero_width_space
|
14
|
+
|
15
|
+
# Whether this line is the last line in the paragraph
|
16
|
+
def paragraph_finished?
|
17
|
+
@newline_encountered || is_next_string_newline? || @arranger.finished?
|
18
|
+
end
|
19
|
+
|
20
|
+
def tokenize(fragment)
|
21
|
+
fragment.scan(scan_pattern)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Work in conjunction with the PDF::Formatted::Arranger
|
25
|
+
# defined in the :arranger option to determine what formatted text
|
26
|
+
# will fit within the width defined by the :width option
|
27
|
+
#
|
28
|
+
def wrap_line(width:, arranger:, kerning: nil, disable_wrap_by_char: nil)
|
29
|
+
initialize_line(
|
30
|
+
kerning: kerning,
|
31
|
+
width: width,
|
32
|
+
arranger: arranger,
|
33
|
+
disable_wrap_by_char: disable_wrap_by_char)
|
34
|
+
|
35
|
+
while fragment = @arranger.next_string
|
36
|
+
@fragment_output = ""
|
37
|
+
|
38
|
+
fragment.lstrip! if first_fragment_on_this_line?(fragment)
|
39
|
+
next if empty_line?(fragment)
|
40
|
+
|
41
|
+
break unless apply_font_settings_and_add_fragment_to_line(fragment)
|
42
|
+
end
|
43
|
+
|
44
|
+
@arranger.finalize_line
|
45
|
+
@accumulated_width = @arranger.line_width
|
46
|
+
@space_count = @arranger.space_count
|
47
|
+
@arranger.line
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def first_fragment_on_this_line?(fragment)
|
53
|
+
line_empty? && fragment != "\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
def empty_line?(fragment)
|
57
|
+
empty = line_empty? && fragment.empty? && is_next_string_newline?
|
58
|
+
@arranger.update_last_string("", "", soft_hyphen) if empty
|
59
|
+
empty
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_next_string_newline?
|
63
|
+
@arranger.preview_next_string == "\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
def apply_font_settings_and_add_fragment_to_line(fragment)
|
67
|
+
# if font has changed from Unicode to non-Unicode, or vice versa,
|
68
|
+
# the characters used for soft hyphens and zero-width spaces will
|
69
|
+
# be different.
|
70
|
+
set_soft_hyphen_and_zero_width_space(@arranger.current_formatter)
|
71
|
+
add_fragment_to_line(fragment, @arranger.current_formatter)
|
72
|
+
end
|
73
|
+
|
74
|
+
# returns true if all text was printed without running into the end of
|
75
|
+
# the line
|
76
|
+
#
|
77
|
+
def add_fragment_to_line(fragment, formatter)
|
78
|
+
if fragment == ""
|
79
|
+
true
|
80
|
+
elsif fragment == "\n"
|
81
|
+
@newline_encountered = true
|
82
|
+
false
|
83
|
+
else
|
84
|
+
tokenize(fragment).each do |segment|
|
85
|
+
if segment == zero_width_space
|
86
|
+
segment_width = 0
|
87
|
+
else
|
88
|
+
segment_width = formatter.width_of(segment, kerning: @kerning)
|
89
|
+
end
|
90
|
+
|
91
|
+
if @accumulated_width + segment_width <= @width
|
92
|
+
@accumulated_width += segment_width
|
93
|
+
if segment[-1] == soft_hyphen
|
94
|
+
sh_width = formatter.width_of("#{soft_hyphen}", kerning: @kerning)
|
95
|
+
@accumulated_width -= sh_width
|
96
|
+
end
|
97
|
+
@fragment_output += segment
|
98
|
+
else
|
99
|
+
end_of_the_line_reached(formatter, segment)
|
100
|
+
fragment_finished(fragment)
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
fragment_finished(fragment)
|
106
|
+
true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
# The pattern used to determine chunks of text to place on a given line
|
113
|
+
#
|
114
|
+
def scan_pattern(encoding = ::Encoding::UTF_8)
|
115
|
+
ebc = break_chars(encoding)
|
116
|
+
eshy = soft_hyphen(encoding)
|
117
|
+
ehy = hyphen(encoding)
|
118
|
+
ews = whitespace(encoding)
|
119
|
+
|
120
|
+
patterns = [
|
121
|
+
"[^#{ebc}]+#{eshy}",
|
122
|
+
"[^#{ebc}]+#{ehy}+",
|
123
|
+
"[^#{ebc}]+",
|
124
|
+
"[#{ews}]+",
|
125
|
+
"#{ehy}+[^#{ebc}]*",
|
126
|
+
eshy.to_s
|
127
|
+
]
|
128
|
+
|
129
|
+
pattern = patterns
|
130
|
+
.map { |p| p.encode(encoding) }
|
131
|
+
.join('|')
|
132
|
+
|
133
|
+
Regexp.new(pattern)
|
134
|
+
end
|
135
|
+
|
136
|
+
# The pattern used to determine whether any word breaks exist on a
|
137
|
+
# current line, which in turn determines whether character level
|
138
|
+
# word breaking is needed
|
139
|
+
#
|
140
|
+
def word_division_scan_pattern(encoding = ::Encoding::UTF_8)
|
141
|
+
common_whitespaces = ["\t", "\n", "\v", "\r", ' '].map do |c|
|
142
|
+
c.encode(encoding)
|
143
|
+
end
|
144
|
+
|
145
|
+
Regexp.union(
|
146
|
+
common_whitespaces +
|
147
|
+
[
|
148
|
+
zero_width_space(encoding),
|
149
|
+
soft_hyphen(encoding),
|
150
|
+
hyphen(encoding)
|
151
|
+
].compact
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
def soft_hyphen(encoding = ::Encoding::UTF_8)
|
156
|
+
Krill::SHY.encode(encoding)
|
157
|
+
end
|
158
|
+
|
159
|
+
def break_chars(encoding = ::Encoding::UTF_8)
|
160
|
+
[
|
161
|
+
whitespace(encoding),
|
162
|
+
soft_hyphen(encoding),
|
163
|
+
hyphen(encoding)
|
164
|
+
].join('')
|
165
|
+
end
|
166
|
+
|
167
|
+
def zero_width_space(encoding = ::Encoding::UTF_8)
|
168
|
+
Krill::ZWSP.encode(encoding)
|
169
|
+
end
|
170
|
+
|
171
|
+
def whitespace(encoding = ::Encoding::UTF_8)
|
172
|
+
"\s\t#{zero_width_space(encoding)}".encode(encoding)
|
173
|
+
end
|
174
|
+
|
175
|
+
def hyphen(_encoding = ::Encoding::UTF_8)
|
176
|
+
'-'
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
|
181
|
+
def line_empty?
|
182
|
+
@line_empty && @accumulated_width == 0
|
183
|
+
end
|
184
|
+
|
185
|
+
def initialize_line(kerning:, width:, arranger:, disable_wrap_by_char:)
|
186
|
+
@kerning = kerning
|
187
|
+
@width = width
|
188
|
+
|
189
|
+
@disable_wrap_by_char = disable_wrap_by_char
|
190
|
+
|
191
|
+
@accumulated_width = 0
|
192
|
+
@line_empty = true
|
193
|
+
@line_contains_more_than_one_word = false
|
194
|
+
|
195
|
+
@arranger = arranger
|
196
|
+
@arranger.initialize_line
|
197
|
+
|
198
|
+
@newline_encountered = false
|
199
|
+
@line_full = false
|
200
|
+
end
|
201
|
+
|
202
|
+
def set_soft_hyphen_and_zero_width_space(formatter)
|
203
|
+
# this is done once per fragment, after the font settings for the fragment are applied --
|
204
|
+
# it could actually be skipped if the font hasn't changed
|
205
|
+
@soft_hyphen = formatter.normalize_encoding(SHY)
|
206
|
+
@zero_width_space = formatter.unicode? ? ZWSP : ""
|
207
|
+
end
|
208
|
+
|
209
|
+
def fragment_finished(fragment)
|
210
|
+
if fragment == "\n"
|
211
|
+
@newline_encountered = true
|
212
|
+
@line_empty = false
|
213
|
+
else
|
214
|
+
update_output_based_on_last_fragment(fragment, soft_hyphen)
|
215
|
+
update_line_status_based_on_last_output
|
216
|
+
determine_whether_to_pull_preceding_fragment_to_join_this_one(fragment)
|
217
|
+
end
|
218
|
+
remember_this_fragment_for_backward_looking_ops
|
219
|
+
end
|
220
|
+
|
221
|
+
def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
|
222
|
+
remaining_text = fragment.slice(@fragment_output.length..fragment.length)
|
223
|
+
fail CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
|
224
|
+
@arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
|
225
|
+
end
|
226
|
+
|
227
|
+
def determine_whether_to_pull_preceding_fragment_to_join_this_one(current_fragment)
|
228
|
+
if @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
|
229
|
+
unless previous_fragment_ended_with_breakable? || fragment_begins_with_breakable?(current_fragment)
|
230
|
+
@fragment_output = @previous_fragment_output_without_last_word
|
231
|
+
update_output_based_on_last_fragment(@previous_fragment)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def remember_this_fragment_for_backward_looking_ops
|
237
|
+
@previous_fragment = @fragment_output.dup
|
238
|
+
pf = @previous_fragment
|
239
|
+
@previous_fragment_ended_with_breakable = pf =~ /[#{break_chars}]$/
|
240
|
+
last_word = pf.slice(/[^#{break_chars}]*$/)
|
241
|
+
last_word_length = last_word.nil? ? 0 : last_word.length
|
242
|
+
@previous_fragment_output_without_last_word = pf.slice(0, pf.length - last_word_length)
|
243
|
+
end
|
244
|
+
|
245
|
+
def previous_fragment_ended_with_breakable?
|
246
|
+
@previous_fragment_ended_with_breakable
|
247
|
+
end
|
248
|
+
|
249
|
+
def fragment_begins_with_breakable?(fragment)
|
250
|
+
fragment =~ /^[#{break_chars}]/
|
251
|
+
end
|
252
|
+
|
253
|
+
def line_finished?
|
254
|
+
@line_full || paragraph_finished?
|
255
|
+
end
|
256
|
+
|
257
|
+
def update_line_status_based_on_last_output
|
258
|
+
@line_contains_more_than_one_word = true if @fragment_output =~ word_division_scan_pattern
|
259
|
+
end
|
260
|
+
|
261
|
+
def end_of_the_line_reached(formatter, segment)
|
262
|
+
update_line_status_based_on_last_output
|
263
|
+
wrap_by_char(formatter, segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
|
264
|
+
@line_full = true
|
265
|
+
end
|
266
|
+
|
267
|
+
def wrap_by_char(formatter, segment)
|
268
|
+
segment.each_char do |char|
|
269
|
+
break unless append_char(char, formatter)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def append_char(char, formatter)
|
274
|
+
# kerning doesn't make sense in the context of a single character
|
275
|
+
char_width = formatter.compute_width_of(char, kerning: false)
|
276
|
+
|
277
|
+
if @accumulated_width + char_width <= @width
|
278
|
+
@accumulated_width += char_width
|
279
|
+
@fragment_output << char
|
280
|
+
true
|
281
|
+
else
|
282
|
+
false
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require "krill/line_wrap"
|
2
|
+
require "krill/arranger"
|
3
|
+
|
4
|
+
module Krill
|
5
|
+
class TextBox
|
6
|
+
|
7
|
+
def initialize(formatted_text, options={})
|
8
|
+
@original_array = formatted_text
|
9
|
+
@text = nil
|
10
|
+
@at = [0, 720.0] # was [ @document.bounds.left, @document.bounds.top ]
|
11
|
+
@width = options.fetch(:width)
|
12
|
+
@height = options.fetch(:height, @at[1])
|
13
|
+
@direction = options.fetch(:direction, :ltr)
|
14
|
+
@align = options.fetch(:align, @direction == :rtl ? :right : :left)
|
15
|
+
@leading = options.fetch(:leading, 0)
|
16
|
+
@kerning = options.fetch(:kerning, true)
|
17
|
+
@disable_wrap_by_char = options[:disable_wrap_by_char]
|
18
|
+
@line_wrap = Krill::LineWrap.new
|
19
|
+
@arranger = Krill::Arranger.new(kerning: @kerning)
|
20
|
+
end
|
21
|
+
|
22
|
+
def render
|
23
|
+
wrap(normalize_encoding(original_text))
|
24
|
+
end
|
25
|
+
|
26
|
+
# The text that was successfully printed (or, if <tt>dry_run</tt> was
|
27
|
+
# used, the text that would have been successfully printed)
|
28
|
+
attr_reader :text
|
29
|
+
|
30
|
+
# True if nothing printed (or, if <tt>dry_run</tt> was
|
31
|
+
# used, nothing would have been successfully printed)
|
32
|
+
def nothing_printed?
|
33
|
+
@nothing_printed
|
34
|
+
end
|
35
|
+
|
36
|
+
# True if everything printed (or, if <tt>dry_run</tt> was
|
37
|
+
# used, everything would have been successfully printed)
|
38
|
+
def everything_printed?
|
39
|
+
@everything_printed
|
40
|
+
end
|
41
|
+
|
42
|
+
# The upper left corner of the text box
|
43
|
+
attr_reader :at
|
44
|
+
|
45
|
+
# The line height of the last line printed
|
46
|
+
attr_reader :line_height
|
47
|
+
|
48
|
+
# The height of the ascender of the last line printed
|
49
|
+
attr_reader :ascender
|
50
|
+
|
51
|
+
# The height of the descender of the last line printed
|
52
|
+
attr_reader :descender
|
53
|
+
|
54
|
+
# The leading used during printing
|
55
|
+
attr_reader :leading
|
56
|
+
|
57
|
+
def line_gap
|
58
|
+
line_height - (ascender + descender)
|
59
|
+
end
|
60
|
+
|
61
|
+
# The height actually used during the previous <tt>render</tt>
|
62
|
+
def height
|
63
|
+
return 0 if @baseline_y.nil? || @descender.nil?
|
64
|
+
(@baseline_y - @descender).abs
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# The width available at this point in the box
|
70
|
+
def available_width
|
71
|
+
@width
|
72
|
+
end
|
73
|
+
|
74
|
+
# <tt>fragment</tt> is a Krill::Fragment object
|
75
|
+
def draw_fragment(fragment, accumulated_width=0, line_width=0, word_spacing=0)
|
76
|
+
case @align
|
77
|
+
when :left
|
78
|
+
x = @at[0]
|
79
|
+
when :center
|
80
|
+
x = @at[0] + @width * 0.5 - line_width * 0.5
|
81
|
+
when :right
|
82
|
+
x = @at[0] + @width - line_width
|
83
|
+
when :justify
|
84
|
+
x = if @direction == :ltr
|
85
|
+
@at[0]
|
86
|
+
else
|
87
|
+
@at[0] + @width - line_width
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
x += accumulated_width
|
92
|
+
|
93
|
+
y = @at[1] + @baseline_y
|
94
|
+
|
95
|
+
y += fragment.y_offset
|
96
|
+
|
97
|
+
fragment.left = x
|
98
|
+
fragment.baseline = y
|
99
|
+
end
|
100
|
+
|
101
|
+
def original_text
|
102
|
+
@original_array.collect(&:dup)
|
103
|
+
end
|
104
|
+
|
105
|
+
def normalize_encoding(text)
|
106
|
+
text.each do |hash|
|
107
|
+
hash[:text] = hash.fetch(:font).normalize_encoding(hash.fetch(:text))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def move_baseline_down
|
112
|
+
if @baseline_y.zero?
|
113
|
+
@baseline_y = -@ascender
|
114
|
+
else
|
115
|
+
@baseline_y -= (@line_height + @leading)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# See the developer documentation for PDF::Core::Text#wrap
|
120
|
+
#
|
121
|
+
# Formatted#wrap should set the following variables:
|
122
|
+
# <tt>@line_height</tt>::
|
123
|
+
# the height of the tallest fragment in the last printed line
|
124
|
+
# <tt>@descender</tt>::
|
125
|
+
# the descender height of the tallest fragment in the last
|
126
|
+
# printed line
|
127
|
+
# <tt>@ascender</tt>::
|
128
|
+
# the ascender heigth of the tallest fragment in the last
|
129
|
+
# printed line
|
130
|
+
# <tt>@baseline_y</tt>::
|
131
|
+
# the baseline of the current line
|
132
|
+
# <tt>@nothing_printed</tt>::
|
133
|
+
# set to true until something is printed, then false
|
134
|
+
# <tt>@everything_printed</tt>::
|
135
|
+
# set to false until everything printed, then true
|
136
|
+
#
|
137
|
+
# Returns any formatted text that was not printed
|
138
|
+
#
|
139
|
+
def wrap(array)
|
140
|
+
initialize_wrap(array)
|
141
|
+
|
142
|
+
stop = false
|
143
|
+
until stop
|
144
|
+
# wrap before testing if enough height for this line because the
|
145
|
+
# height of the highest fragment on this line will be used to
|
146
|
+
# determine the line height
|
147
|
+
@line_wrap.wrap_line(
|
148
|
+
kerning: @kerning,
|
149
|
+
width: available_width,
|
150
|
+
arranger: @arranger,
|
151
|
+
disable_wrap_by_char: @disable_wrap_by_char)
|
152
|
+
|
153
|
+
if enough_height_for_this_line?
|
154
|
+
move_baseline_down
|
155
|
+
print_line
|
156
|
+
else
|
157
|
+
stop = true
|
158
|
+
end
|
159
|
+
|
160
|
+
stop ||= @arranger.finished?
|
161
|
+
end
|
162
|
+
@text = @printed_lines.join("\n")
|
163
|
+
@everything_printed = @arranger.finished?
|
164
|
+
@arranger.unconsumed
|
165
|
+
end
|
166
|
+
|
167
|
+
def print_line
|
168
|
+
@nothing_printed = false
|
169
|
+
printed_fragments = []
|
170
|
+
fragments_this_line = []
|
171
|
+
|
172
|
+
word_spacing = word_spacing_for_this_line
|
173
|
+
@arranger.fragments.each do |fragment|
|
174
|
+
fragment.word_spacing = word_spacing
|
175
|
+
if fragment.text == "\n"
|
176
|
+
printed_fragments << "\n" if @printed_lines.last == ""
|
177
|
+
break
|
178
|
+
end
|
179
|
+
printed_fragments << fragment.text
|
180
|
+
fragments_this_line << fragment
|
181
|
+
end
|
182
|
+
@arranger.fragments.replace []
|
183
|
+
|
184
|
+
accumulated_width = 0
|
185
|
+
fragments_this_line.reverse! if @direction == :rtl
|
186
|
+
fragments_this_line.each do |fragment_this_line|
|
187
|
+
fragment_this_line.default_direction = @direction
|
188
|
+
format_and_draw_fragment(fragment_this_line, accumulated_width, @line_wrap.width, word_spacing)
|
189
|
+
accumulated_width += fragment_this_line.width
|
190
|
+
end
|
191
|
+
|
192
|
+
@printed_lines << printed_fragments.map do |s|
|
193
|
+
s.force_encoding(::Encoding::UTF_8)
|
194
|
+
end.join
|
195
|
+
end
|
196
|
+
|
197
|
+
def word_spacing_for_this_line
|
198
|
+
if @align == :justify && @line_wrap.space_count > 0 && !@line_wrap.paragraph_finished?
|
199
|
+
(available_width - @line_wrap.width) / @line_wrap.space_count
|
200
|
+
else
|
201
|
+
0
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def enough_height_for_this_line?
|
206
|
+
@line_height = @arranger.max_line_height
|
207
|
+
@descender = @arranger.max_descender
|
208
|
+
@ascender = @arranger.max_ascender
|
209
|
+
diff = if @baseline_y.zero?
|
210
|
+
@ascender + @descender
|
211
|
+
else
|
212
|
+
@descender + @line_height + @leading
|
213
|
+
end
|
214
|
+
require_relatived_total_height = @baseline_y.abs + diff
|
215
|
+
if require_relatived_total_height > @height + 0.0001
|
216
|
+
# no room for the full height of this line
|
217
|
+
@arranger.repack_unretrieved
|
218
|
+
false
|
219
|
+
else
|
220
|
+
true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def initialize_wrap(array)
|
225
|
+
@text = nil
|
226
|
+
@arranger.format_array = array
|
227
|
+
|
228
|
+
# these values will depend on the maximum value within a given line
|
229
|
+
@line_height = 0
|
230
|
+
@descender = 0
|
231
|
+
@ascender = 0
|
232
|
+
@baseline_y = 0
|
233
|
+
|
234
|
+
@printed_lines = []
|
235
|
+
@nothing_printed = true
|
236
|
+
@everything_printed = false
|
237
|
+
end
|
238
|
+
|
239
|
+
def format_and_draw_fragment(fragment, accumulated_width, line_width, word_spacing)
|
240
|
+
draw_fragment(fragment, accumulated_width, line_width, word_spacing)
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
end
|