hexapdf 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -2
- data/CONTRIBUTERS +1 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/boxes.rb +68 -0
- data/examples/graphics.rb +12 -12
- data/examples/{text_box_alignment.rb → text_layouter_alignment.rb} +14 -14
- data/examples/text_layouter_inline_boxes.rb +66 -0
- data/examples/{text_box_line_wrapping.rb → text_layouter_line_wrapping.rb} +9 -10
- data/examples/{text_box_shapes.rb → text_layouter_shapes.rb} +58 -54
- data/examples/text_layouter_styling.rb +125 -0
- data/examples/truetype.rb +5 -7
- data/lib/hexapdf/cli/command.rb +1 -0
- data/lib/hexapdf/configuration.rb +170 -106
- data/lib/hexapdf/content/canvas.rb +41 -36
- data/lib/hexapdf/content/graphics_state.rb +15 -0
- data/lib/hexapdf/content/operator.rb +1 -1
- data/lib/hexapdf/dictionary.rb +20 -8
- data/lib/hexapdf/dictionary_fields.rb +8 -6
- data/lib/hexapdf/document.rb +25 -26
- data/lib/hexapdf/document/fonts.rb +4 -4
- data/lib/hexapdf/document/images.rb +2 -2
- data/lib/hexapdf/document/pages.rb +16 -16
- data/lib/hexapdf/encryption/security_handler.rb +41 -9
- data/lib/hexapdf/filter/flate_decode.rb +1 -1
- data/lib/hexapdf/filter/lzw_decode.rb +1 -1
- data/lib/hexapdf/filter/predictor.rb +7 -1
- data/lib/hexapdf/font/true_type/font.rb +20 -0
- data/lib/hexapdf/font/type1/font.rb +23 -0
- data/lib/hexapdf/font_loader.rb +1 -0
- data/lib/hexapdf/font_loader/from_configuration.rb +2 -3
- data/lib/hexapdf/font_loader/from_file.rb +65 -0
- data/lib/hexapdf/image_loader/png.rb +2 -2
- data/lib/hexapdf/layout.rb +3 -2
- data/lib/hexapdf/layout/box.rb +146 -0
- data/lib/hexapdf/layout/inline_box.rb +40 -31
- data/lib/hexapdf/layout/{line_fragment.rb → line.rb} +12 -13
- data/lib/hexapdf/layout/style.rb +630 -41
- data/lib/hexapdf/layout/text_fragment.rb +80 -12
- data/lib/hexapdf/layout/{text_box.rb → text_layouter.rb} +164 -109
- data/lib/hexapdf/number_tree_node.rb +1 -1
- data/lib/hexapdf/parser.rb +4 -1
- data/lib/hexapdf/revisions.rb +11 -4
- data/lib/hexapdf/stream.rb +8 -9
- data/lib/hexapdf/tokenizer.rb +5 -3
- data/lib/hexapdf/type.rb +3 -0
- data/lib/hexapdf/type/action.rb +56 -0
- data/lib/hexapdf/type/actions.rb +52 -0
- data/lib/hexapdf/type/actions/go_to.rb +52 -0
- data/lib/hexapdf/type/actions/go_to_r.rb +54 -0
- data/lib/hexapdf/type/actions/launch.rb +73 -0
- data/lib/hexapdf/type/actions/uri.rb +65 -0
- data/lib/hexapdf/type/annotation.rb +85 -0
- data/lib/hexapdf/type/annotations.rb +51 -0
- data/lib/hexapdf/type/annotations/link.rb +70 -0
- data/lib/hexapdf/type/annotations/markup_annotation.rb +70 -0
- data/lib/hexapdf/type/annotations/text.rb +81 -0
- data/lib/hexapdf/type/catalog.rb +3 -1
- data/lib/hexapdf/type/embedded_file.rb +6 -11
- data/lib/hexapdf/type/file_specification.rb +4 -6
- data/lib/hexapdf/type/font.rb +3 -1
- data/lib/hexapdf/type/font_descriptor.rb +18 -16
- data/lib/hexapdf/type/form.rb +3 -1
- data/lib/hexapdf/type/graphics_state_parameter.rb +3 -1
- data/lib/hexapdf/type/image.rb +4 -2
- data/lib/hexapdf/type/info.rb +2 -5
- data/lib/hexapdf/type/names.rb +2 -5
- data/lib/hexapdf/type/object_stream.rb +2 -1
- data/lib/hexapdf/type/page.rb +14 -1
- data/lib/hexapdf/type/page_tree_node.rb +9 -6
- data/lib/hexapdf/type/resources.rb +2 -5
- data/lib/hexapdf/type/trailer.rb +2 -5
- data/lib/hexapdf/type/viewer_preferences.rb +2 -5
- data/lib/hexapdf/type/xref_stream.rb +3 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +3 -1
- data/test/hexapdf/content/test_canvas.rb +29 -3
- data/test/hexapdf/content/test_graphics_state.rb +11 -0
- data/test/hexapdf/content/test_operator.rb +3 -2
- data/test/hexapdf/document/test_fonts.rb +8 -8
- data/test/hexapdf/document/test_images.rb +4 -12
- data/test/hexapdf/document/test_pages.rb +7 -7
- data/test/hexapdf/encryption/test_security_handler.rb +1 -5
- data/test/hexapdf/filter/test_predictor.rb +40 -12
- data/test/hexapdf/font/true_type/test_font.rb +16 -0
- data/test/hexapdf/font/type1/test_font.rb +30 -0
- data/test/hexapdf/font_loader/test_from_file.rb +29 -0
- data/test/hexapdf/font_loader/test_standard14.rb +4 -3
- data/test/hexapdf/layout/test_box.rb +104 -0
- data/test/hexapdf/layout/test_inline_box.rb +24 -10
- data/test/hexapdf/layout/{test_line_fragment.rb → test_line.rb} +9 -9
- data/test/hexapdf/layout/test_style.rb +519 -31
- data/test/hexapdf/layout/test_text_fragment.rb +136 -15
- data/test/hexapdf/layout/{test_text_box.rb → test_text_layouter.rb} +224 -144
- data/test/hexapdf/layout/test_text_shaper.rb +1 -1
- data/test/hexapdf/test_configuration.rb +12 -6
- data/test/hexapdf/test_dictionary.rb +27 -2
- data/test/hexapdf/test_dictionary_fields.rb +10 -1
- data/test/hexapdf/test_document.rb +14 -13
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/test_revisions.rb +34 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_type.rb +18 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/actions/test_launch.rb +24 -0
- data/test/hexapdf/type/actions/test_uri.rb +23 -0
- data/test/hexapdf/type/annotations/test_link.rb +19 -0
- data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
- data/test/hexapdf/type/annotations/test_text.rb +38 -0
- data/test/hexapdf/type/test_annotation.rb +38 -0
- data/test/hexapdf/type/test_file_specification.rb +0 -7
- data/test/hexapdf/type/test_info.rb +0 -5
- data/test/hexapdf/type/test_page.rb +14 -0
- data/test/hexapdf/type/test_page_tree_node.rb +4 -1
- data/test/hexapdf/type/test_trailer.rb +0 -4
- data/test/test_helper.rb +6 -3
- metadata +36 -15
- data/examples/text_box_inline_boxes.rb +0 -56
- data/examples/text_box_styling.rb +0 -72
- data/test/hexapdf/type/test_embedded_file.rb +0 -16
- data/test/hexapdf/type/test_names.rb +0 -9
@@ -70,14 +70,38 @@ module HexaPDF
|
|
70
70
|
|
71
71
|
# The style to be applied.
|
72
72
|
#
|
73
|
-
# Only the following properties are used:
|
74
|
-
#
|
73
|
+
# Only the following properties are used:
|
74
|
+
#
|
75
|
+
# * Style#font
|
76
|
+
# * Style#font_size
|
77
|
+
# * Style#horizontal_scaling
|
78
|
+
# * Style#character_spacing
|
79
|
+
# * Style#word_spacing
|
80
|
+
# * Style#text_rise
|
81
|
+
# * Style#text_rendering_mode
|
82
|
+
# * Style#subscript
|
83
|
+
# * Style#superscript
|
84
|
+
# * Style#underline
|
85
|
+
# * Style#strikeout
|
86
|
+
# * Style#fill_color
|
87
|
+
# * Style#fill_alpha
|
88
|
+
# * Style#stroke_color
|
89
|
+
# * Style#stroke_alpha
|
90
|
+
# * Style#stroke_width
|
91
|
+
# * Style#stroke_cap_style
|
92
|
+
# * Style#stroke_join_style
|
93
|
+
# * Style#stroke_miter_limit
|
94
|
+
# * Style#stroke_dash_pattern
|
95
|
+
# * Style#underlay_callback
|
96
|
+
# * Style#overlay_callback
|
75
97
|
attr_reader :style
|
76
98
|
|
77
99
|
# Creates a new TextFragment object with the given items and style.
|
100
|
+
#
|
101
|
+
# The argument +style+ can either be a Style object or a hash of style options.
|
78
102
|
def initialize(items:, style: Style.new)
|
79
103
|
@items = items || []
|
80
|
-
@style = style
|
104
|
+
@style = (style.kind_of?(Style) ? style : Style.new(style))
|
81
105
|
end
|
82
106
|
|
83
107
|
# Draws the text onto the canvas at the given position.
|
@@ -85,13 +109,57 @@ module HexaPDF
|
|
85
109
|
# Before the text is drawn using HexaPDF::Content;:Canvas#show_glyphs, the text properties
|
86
110
|
# mentioned in the description of #style are set.
|
87
111
|
def draw(canvas, x, y)
|
112
|
+
return if items.empty?
|
113
|
+
|
114
|
+
style.underlays.draw(canvas, x, y + y_min, self)
|
115
|
+
|
116
|
+
# Set general font related graphics state
|
88
117
|
canvas.move_text_cursor(offset: [x, y])
|
89
|
-
canvas.font(style.font, size: style.
|
118
|
+
canvas.font(style.font, size: style.calculated_font_size).
|
90
119
|
horizontal_scaling(style.horizontal_scaling).
|
91
120
|
character_spacing(style.character_spacing).
|
92
121
|
word_spacing(style.word_spacing).
|
93
|
-
text_rise(style.
|
122
|
+
text_rise(style.calculated_text_rise).
|
123
|
+
text_rendering_mode(style.text_rendering_mode)
|
124
|
+
|
125
|
+
# Set fill and/or stroke related graphics state
|
126
|
+
canvas.opacity(fill_alpha: style.fill_alpha, stroke_alpha: style.stroke_alpha)
|
127
|
+
trm = canvas.text_rendering_mode
|
128
|
+
if trm.value.even? # text is filled
|
129
|
+
canvas.fill_color(style.fill_color)
|
130
|
+
end
|
131
|
+
if trm == :stroke || trm == :fill_stroke || trm == :stroke_clip || trm == :fill_stroke_clip
|
132
|
+
canvas.stroke_color(style.stroke_color).
|
133
|
+
line_width(style.stroke_width).
|
134
|
+
line_cap_style(style.stroke_cap_style).
|
135
|
+
line_join_style(style.stroke_join_style).
|
136
|
+
miter_limit(style.stroke_miter_limit).
|
137
|
+
line_dash_pattern(style.stroke_dash_pattern)
|
138
|
+
end
|
139
|
+
|
94
140
|
canvas.show_glyphs_only(items)
|
141
|
+
|
142
|
+
if style.underline
|
143
|
+
y_offset = style.calculated_underline_position
|
144
|
+
canvas.stroke_color(style.fill_color).
|
145
|
+
line_width(style.calculated_underline_thickness).
|
146
|
+
line_cap_style(:butt).
|
147
|
+
line_dash_pattern(0).
|
148
|
+
line(x, y + y_offset, x + width, y + y_offset).
|
149
|
+
stroke
|
150
|
+
end
|
151
|
+
|
152
|
+
if style.strikeout
|
153
|
+
y_offset = style.calculated_strikeout_position
|
154
|
+
canvas.stroke_color(style.fill_color).
|
155
|
+
line_width(style.calculated_strikeout_thickness).
|
156
|
+
line_cap_style(:butt).
|
157
|
+
line_dash_pattern(0).
|
158
|
+
line(x, y + y_offset, x + width, y + y_offset).
|
159
|
+
stroke
|
160
|
+
end
|
161
|
+
|
162
|
+
style.overlays.draw(canvas, x, y + y_min, self)
|
95
163
|
end
|
96
164
|
|
97
165
|
# The minimum x-coordinate of the first glyph.
|
@@ -106,24 +174,24 @@ module HexaPDF
|
|
106
174
|
|
107
175
|
# The minimum y-coordinate, calculated using the scaled descender of the font.
|
108
176
|
def y_min
|
109
|
-
@y_min ||= style.scaled_font_descender + style.
|
177
|
+
@y_min ||= style.scaled_font_descender + style.calculated_text_rise
|
110
178
|
end
|
111
179
|
|
112
180
|
# The maximum y-coordinate, calculated using the scaled ascender of the font.
|
113
181
|
def y_max
|
114
|
-
@y_max ||= style.scaled_font_ascender + style.
|
182
|
+
@y_max ||= style.scaled_font_ascender + style.calculated_text_rise
|
115
183
|
end
|
116
184
|
|
117
185
|
# The minimum y-coordinate of any item.
|
118
186
|
def exact_y_min
|
119
|
-
@exact_y_min ||= (@items.min_by(&:y_min)&.y_min || 0) *
|
120
|
-
style.
|
187
|
+
@exact_y_min ||= (@items.min_by(&:y_min)&.y_min || 0) *
|
188
|
+
style.calculated_font_size / 1000.0 + style.calculated_text_rise
|
121
189
|
end
|
122
190
|
|
123
191
|
# The maximum y-coordinate of any item.
|
124
192
|
def exact_y_max
|
125
|
-
@exact_y_max ||= (@items.max_by(&:y_max)&.y_max || 0) *
|
126
|
-
style.
|
193
|
+
@exact_y_max ||= (@items.max_by(&:y_max)&.y_max || 0) *
|
194
|
+
style.calculated_font_size / 1000.0 + style.calculated_text_rise
|
127
195
|
end
|
128
196
|
|
129
197
|
# The width of the text fragment.
|
@@ -148,7 +216,7 @@ module HexaPDF
|
|
148
216
|
|
149
217
|
# Returns the vertical alignment inside a line which is always :text for text fragments.
|
150
218
|
#
|
151
|
-
# See
|
219
|
+
# See Line for details.
|
152
220
|
def valign
|
153
221
|
:text
|
154
222
|
end
|
@@ -34,7 +34,7 @@
|
|
34
34
|
require 'hexapdf/error'
|
35
35
|
require 'hexapdf/layout/text_fragment'
|
36
36
|
require 'hexapdf/layout/inline_box'
|
37
|
-
require 'hexapdf/layout/
|
37
|
+
require 'hexapdf/layout/line'
|
38
38
|
require 'hexapdf/layout/numeric_refinements'
|
39
39
|
|
40
40
|
module HexaPDF
|
@@ -54,14 +54,14 @@ module HexaPDF
|
|
54
54
|
#
|
55
55
|
# Laying out text consists of two phases:
|
56
56
|
#
|
57
|
-
# 1. The items
|
58
|
-
#
|
59
|
-
#
|
57
|
+
# 1. The items are broken into pieces which are wrapped into Box, Glue or Penalty objects.
|
58
|
+
# Additional Penalty objects marking line breaking opportunities are inserted where needed.
|
59
|
+
# This step is done by the SimpleTextSegmentation module.
|
60
60
|
#
|
61
61
|
# 2. The pieces are arranged into lines using a very simple algorithm that just puts the maximum
|
62
62
|
# number of consecutive pieces into each line. This step is done by the SimpleLineWrapping
|
63
63
|
# module.
|
64
|
-
class
|
64
|
+
class TextLayouter
|
65
65
|
|
66
66
|
using NumericRefinements
|
67
67
|
|
@@ -81,6 +81,11 @@ module HexaPDF
|
|
81
81
|
@item.width
|
82
82
|
end
|
83
83
|
|
84
|
+
# The height of the item.
|
85
|
+
def height
|
86
|
+
@item.height
|
87
|
+
end
|
88
|
+
|
84
89
|
# Returns :box.
|
85
90
|
def type
|
86
91
|
:box
|
@@ -127,6 +132,12 @@ module HexaPDF
|
|
127
132
|
# All numbers greater than this one are deemed infinite.
|
128
133
|
INFINITY = 1000
|
129
134
|
|
135
|
+
# The penalty value for a mandatory paragraph break.
|
136
|
+
PARAGRAPH_BREAK = -INFINITY - 1_000_000
|
137
|
+
|
138
|
+
# The penalty value for a mandatory line break.
|
139
|
+
LINE_BREAK = -INFINITY - 1_000_001
|
140
|
+
|
130
141
|
# The penalty for breaking at this point.
|
131
142
|
attr_reader :penalty
|
132
143
|
|
@@ -148,8 +159,11 @@ module HexaPDF
|
|
148
159
|
:penalty
|
149
160
|
end
|
150
161
|
|
151
|
-
# Singleton object describing a Penalty for a mandatory break.
|
152
|
-
|
162
|
+
# Singleton object describing a Penalty for a mandatory paragraph break.
|
163
|
+
MandatoryParagraphBreak = new(PARAGRAPH_BREAK)
|
164
|
+
|
165
|
+
# Singleton object describing a Penalty for a mandatory line break.
|
166
|
+
MandatoryLineBreak = new(LINE_BREAK)
|
153
167
|
|
154
168
|
# Singleton object describing a Penalty for a prohibited break.
|
155
169
|
ProhibitedBreak = new(Penalty::INFINITY)
|
@@ -217,11 +231,13 @@ module HexaPDF
|
|
217
231
|
glues[item.style] ||=
|
218
232
|
Glue.new(TextFragment.new(items: [glyph].freeze, style: item.style))
|
219
233
|
result << glues[item.style]
|
220
|
-
when "\n", "\v", "\f", "\u{85}", "\u{
|
221
|
-
result << Penalty::
|
234
|
+
when "\n", "\v", "\f", "\u{85}", "\u{2029}"
|
235
|
+
result << Penalty::MandatoryParagraphBreak
|
236
|
+
when "\u{2028}"
|
237
|
+
result << Penalty::MandatoryLineBreak
|
222
238
|
when "\r"
|
223
239
|
if item.items[i + 1]&.kind_of?(Numeric) || item.items[i + 1].str != "\n"
|
224
|
-
result << Penalty::
|
240
|
+
result << Penalty::MandatoryParagraphBreak
|
225
241
|
end
|
226
242
|
when '-'
|
227
243
|
result << Penalty::Standard
|
@@ -255,32 +271,35 @@ module HexaPDF
|
|
255
271
|
class SimpleLineWrapping
|
256
272
|
|
257
273
|
# :call-seq:
|
258
|
-
# SimpleLineWrapping.call(items,
|
274
|
+
# SimpleLineWrapping.call(items, width_block) {|line, item| block } -> rest
|
259
275
|
#
|
260
276
|
# Arranges the items into lines.
|
261
277
|
#
|
262
|
-
# The +
|
278
|
+
# The +width_block+ argument has to be a callable object that returns the width of the line:
|
263
279
|
#
|
264
|
-
# * If
|
265
|
-
#
|
280
|
+
# * If the line width doesn't depend on the height or the vertical position of the line
|
281
|
+
# (i.e. fixed line width), the +width_block+ should have an arity of zero. However, this
|
282
|
+
# doesn't mean that the block is called only once; it is actually called before each new
|
283
|
+
# line (e.g. for varying line widths that don't depend on the line height; one common case
|
284
|
+
# is the indentation of the first line). This is the general case.
|
266
285
|
#
|
267
|
-
# * However, if lines should have varying
|
268
|
-
# +
|
286
|
+
# * However, if lines should have varying widths (e.g. for flowing text around shapes), the
|
287
|
+
# +width_block+ argument should be an object responding to #call(line_height) where
|
269
288
|
# +line_height+ is the height of the currently layed out line. The caller is responsible
|
270
|
-
# for tracking the height of the already layed out lines.
|
271
|
-
#
|
289
|
+
# for tracking the height of the already layed out lines. This method involves more work
|
290
|
+
# and is therefore slower.
|
272
291
|
#
|
273
292
|
# Regardless of whether varying line widths are used or not, each time a line is finished,
|
274
|
-
# it is yielded to the caller. The second argument +item+ is the
|
275
|
-
#
|
276
|
-
#
|
277
|
-
# this single item
|
278
|
-
# situation, e.g. by stopping.
|
293
|
+
# it is yielded to the caller. The second argument +item+ is the item that caused the line
|
294
|
+
# break (e.g. a Box, Glue or Penalty). The return value should be truthy if line wrapping
|
295
|
+
# should continue, or falsy if it should stop. If the yielded line is empty and the yielded
|
296
|
+
# item is a box item, this single item didn't fit into the available width; the caller has
|
297
|
+
# to handle this situation, e.g. by stopping.
|
279
298
|
#
|
280
299
|
# After the algorithm is finished, it returns the unused items.
|
281
|
-
def self.call(items,
|
282
|
-
obj = new(items,
|
283
|
-
if
|
300
|
+
def self.call(items, width_block, &block)
|
301
|
+
obj = new(items, width_block)
|
302
|
+
if width_block.arity == 1
|
284
303
|
obj.variable_width_wrapping(&block)
|
285
304
|
else
|
286
305
|
obj.fixed_width_wrapping(&block)
|
@@ -291,9 +310,10 @@ module HexaPDF
|
|
291
310
|
|
292
311
|
# Creates a new line wrapping object that arranges the +items+ on lines with the given
|
293
312
|
# width.
|
294
|
-
def initialize(items,
|
313
|
+
def initialize(items, width_block)
|
295
314
|
@items = items
|
296
|
-
@
|
315
|
+
@width_block = width_block
|
316
|
+
@available_width = @width_block.call(0)
|
297
317
|
@line_items = []
|
298
318
|
@width = 0
|
299
319
|
@glue_items = []
|
@@ -302,11 +322,11 @@ module HexaPDF
|
|
302
322
|
@last_breakpoint_line_items_index = 0
|
303
323
|
@break_prohibited_state = false
|
304
324
|
|
305
|
-
@height_calc =
|
325
|
+
@height_calc = Line::HeightCalculator.new
|
306
326
|
@line_height = 0
|
307
327
|
end
|
308
328
|
|
309
|
-
# Peforms
|
329
|
+
# Peforms line wrapping with a fixed width per line, with line height playing no role.
|
310
330
|
def fixed_width_wrapping
|
311
331
|
index = 0
|
312
332
|
|
@@ -318,18 +338,18 @@ module HexaPDF
|
|
318
338
|
index = reset_line_to_last_breakpoint_state
|
319
339
|
item = @items[index]
|
320
340
|
end
|
321
|
-
break unless yield(create_line, item
|
341
|
+
break unless yield(create_line, item)
|
322
342
|
reset_after_line_break(index)
|
323
343
|
redo
|
324
344
|
end
|
325
345
|
when :glue
|
326
346
|
unless add_glue_item(item.item, index)
|
327
|
-
break unless yield(create_line,
|
347
|
+
break unless yield(create_line, item)
|
328
348
|
reset_after_line_break(index + 1)
|
329
349
|
end
|
330
350
|
when :penalty
|
331
351
|
if item.penalty <= -Penalty::INFINITY
|
332
|
-
break unless yield(create_unjustified_line,
|
352
|
+
break unless yield(create_unjustified_line, item)
|
333
353
|
reset_after_line_break(index + 1)
|
334
354
|
elsif item.penalty >= Penalty::INFINITY
|
335
355
|
@break_prohibited_state = true
|
@@ -365,7 +385,6 @@ module HexaPDF
|
|
365
385
|
# Performs the line wrapping with variable widths.
|
366
386
|
def variable_width_wrapping
|
367
387
|
index = 0
|
368
|
-
@available_width = @width_block.call(@line_height)
|
369
388
|
|
370
389
|
while (item = @items[index])
|
371
390
|
case item.type
|
@@ -382,18 +401,18 @@ module HexaPDF
|
|
382
401
|
index = reset_line_to_last_breakpoint_state
|
383
402
|
item = @items[index]
|
384
403
|
end
|
385
|
-
break unless yield(create_line, item
|
404
|
+
break unless yield(create_line, item)
|
386
405
|
reset_after_line_break(index)
|
387
406
|
redo
|
388
407
|
end
|
389
408
|
when :glue
|
390
409
|
unless add_glue_item(item.item, index)
|
391
|
-
break unless yield(create_line,
|
410
|
+
break unless yield(create_line, item)
|
392
411
|
reset_after_line_break(index + 1)
|
393
412
|
end
|
394
413
|
when :penalty
|
395
414
|
if item.penalty <= -Penalty::INFINITY
|
396
|
-
break unless yield(create_unjustified_line,
|
415
|
+
break unless yield(create_unjustified_line, item)
|
397
416
|
reset_after_line_break(index + 1)
|
398
417
|
elsif item.penalty >= Penalty::INFINITY
|
399
418
|
@break_prohibited_state = true
|
@@ -474,12 +493,12 @@ module HexaPDF
|
|
474
493
|
@width + item.width <= @available_width
|
475
494
|
end
|
476
495
|
|
477
|
-
# Creates a
|
496
|
+
# Creates a Line object from the current line items.
|
478
497
|
def create_line
|
479
|
-
|
498
|
+
Line.new(@line_items)
|
480
499
|
end
|
481
500
|
|
482
|
-
# Creates a
|
501
|
+
# Creates a Line object from the current line items that ignores line justification.
|
483
502
|
def create_unjustified_line
|
484
503
|
create_line.tap(&:ignore_justification!)
|
485
504
|
end
|
@@ -494,6 +513,7 @@ module HexaPDF
|
|
494
513
|
@last_breakpoint_index = index
|
495
514
|
@last_breakpoint_line_items_index = 0
|
496
515
|
@break_prohibited_state = false
|
516
|
+
@available_width = @width_block.call(0)
|
497
517
|
|
498
518
|
@line_height = 0
|
499
519
|
@height_calc.reset
|
@@ -502,12 +522,12 @@ module HexaPDF
|
|
502
522
|
end
|
503
523
|
|
504
524
|
|
505
|
-
# Creates a new
|
525
|
+
# Creates a new TextLayouter object for the given text and returns it.
|
506
526
|
#
|
507
527
|
# See ::new for information on +height+.
|
508
528
|
#
|
509
|
-
# The style
|
510
|
-
# mandatory.
|
529
|
+
# The style that gets applied to the text and the layout itself can be specified using
|
530
|
+
# additional options, of which font is mandatory.
|
511
531
|
def self.create(text, width:, height: nil, x_offsets: nil, **options)
|
512
532
|
frag = TextFragment.create(text, **options)
|
513
533
|
new(items: [frag], width: width, height: height, x_offsets: x_offsets, style: frag.style)
|
@@ -519,19 +539,19 @@ module HexaPDF
|
|
519
539
|
# Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
|
520
540
|
attr_reader :style
|
521
541
|
|
522
|
-
# The items (TextFragment and InlineBox objects)
|
542
|
+
# The items (TextFragment and InlineBox objects) that should be layed out.
|
523
543
|
attr_reader :items
|
524
544
|
|
525
|
-
# Array of
|
545
|
+
# Array of Line objects describing the layed out lines.
|
526
546
|
#
|
527
547
|
# The array is only valid after #fit was called.
|
528
548
|
attr_reader :lines
|
529
549
|
|
530
|
-
# The actual height of the text
|
531
|
-
# i.e. if #fit has not been called.
|
550
|
+
# The actual height of the layed out text. Can be +nil+ if the items have not been layed out
|
551
|
+
# yet, i.e. if #fit has not been called.
|
532
552
|
attr_reader :actual_height
|
533
553
|
|
534
|
-
# Creates a new
|
554
|
+
# Creates a new TextLayouter object with the given width containing the given items.
|
535
555
|
#
|
536
556
|
# The width can either be a simple number specifying a fixed width, or an object that responds
|
537
557
|
# to #call(height, line_height) where +height+ is the bottom of last line and +line_height+ is
|
@@ -539,12 +559,15 @@ module HexaPDF
|
|
539
559
|
# these height restrictions.
|
540
560
|
#
|
541
561
|
# The optional +x_offsets+ argument works like +width+ but can be used to specify (varying)
|
542
|
-
# offsets from the left
|
543
|
-
#
|
562
|
+
# offsets from the left side (e.g. when the left side of the text should follow a certain
|
563
|
+
# shape).
|
564
|
+
#
|
565
|
+
# The height is optional and if not specified means that the text layout has infinite height.
|
544
566
|
#
|
545
|
-
# The
|
567
|
+
# The +style+ argument can either be a Style object or a hash of style options. See #style for
|
568
|
+
# the properties that are used by the layouter.
|
546
569
|
def initialize(items: [], width:, height: nil, x_offsets: nil, style: Style.new)
|
547
|
-
@style = style
|
570
|
+
@style = (style.kind_of?(Style) ? style : Style.new(style))
|
548
571
|
@lines = []
|
549
572
|
self.items = items
|
550
573
|
@width = width
|
@@ -552,7 +575,7 @@ module HexaPDF
|
|
552
575
|
@x_offsets = x_offsets && (x_offsets.respond_to?(:call) ? x_offsets : proc { x_offsets })
|
553
576
|
end
|
554
577
|
|
555
|
-
# Sets the items to be arranged by the text
|
578
|
+
# Sets the items to be arranged by the text layouter, clearing the internal state.
|
556
579
|
#
|
557
580
|
# If the items array contains items before text segmentation, the text segmentation algorithm
|
558
581
|
# is automatically applied.
|
@@ -566,80 +589,105 @@ module HexaPDF
|
|
566
589
|
end
|
567
590
|
|
568
591
|
# :call-seq:
|
569
|
-
#
|
592
|
+
# text_layouter.fit -> [remaining_items, reason]
|
570
593
|
#
|
571
|
-
# Fits the items into the
|
572
|
-
#
|
594
|
+
# Fits the items into the set area and returns the remaining items as well as the reason why
|
595
|
+
# there are remaining items.
|
573
596
|
#
|
574
|
-
#
|
575
|
-
#
|
597
|
+
# The reason can be +:success+ if there are no remaining items, +:box+ if a single text or
|
598
|
+
# inline box item is too wide to fit alone on a line, or +:height+ if there was not enough
|
599
|
+
# height for all items.
|
600
|
+
#
|
601
|
+
# The fitted lines can be retrieved via #lines and the total height via #actual_height.
|
602
|
+
#
|
603
|
+
# Note: If no height has been set and variable line widths are used, no search for a possible
|
604
|
+
# vertical offset is done in case a single item doesn't fit.
|
576
605
|
#
|
577
606
|
# This method is automatically called as part of the drawing routine but it can also be used
|
578
|
-
# by itself to determine the actual height of the text
|
607
|
+
# by itself to determine the actual height of the layed out text.
|
579
608
|
def fit
|
580
609
|
@lines.clear
|
581
610
|
@actual_height = 0
|
582
|
-
y_offset = 0
|
583
|
-
|
584
|
-
items = @items
|
585
|
-
if style.text_indent != 0
|
586
|
-
items = [Box.new(InlineBox.new(style.text_indent, 0) { })].concat(items)
|
587
|
-
end
|
588
|
-
|
589
|
-
if @width.respond_to?(:call)
|
590
|
-
width_arg = proc {|h| @width.call(@actual_height, h)}
|
591
|
-
width_block = @width
|
592
|
-
else
|
593
|
-
width_arg = @width
|
594
|
-
width_block = proc { @width }
|
595
|
-
end
|
596
611
|
|
597
|
-
rest =
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
612
|
+
rest = @items
|
613
|
+
y_offset = 0
|
614
|
+
indent = (style.text_indent != 0 ? style.text_indent : 0)
|
615
|
+
width_block = if @width.respond_to?(:call)
|
616
|
+
proc {|h| @width.call(@actual_height, h) - indent }
|
617
|
+
else
|
618
|
+
proc { @width - indent }
|
619
|
+
end
|
620
|
+
|
621
|
+
while true
|
622
|
+
too_wide_box = nil
|
623
|
+
|
624
|
+
rest = style.text_line_wrapping_algorithm.call(rest, width_block) do |line, item|
|
625
|
+
line << TextFragment.new(items: [], style: style) if item&.type != :box && line.items.empty?
|
626
|
+
new_height = @actual_height + line.height +
|
627
|
+
(@lines.empty? ? 0 : style.line_spacing.gap(@lines.last, line))
|
628
|
+
|
629
|
+
if new_height > @height
|
630
|
+
nil
|
631
|
+
elsif !line.items.empty?
|
632
|
+
# valid line found, use it
|
633
|
+
cur_width = width_block.call(line.height)
|
634
|
+
line.x_offset = indent + horizontal_alignment_offset(line, cur_width)
|
635
|
+
line.x_offset += @x_offsets.call(@actual_height, line.height) if @x_offsets
|
636
|
+
line.y_offset = if y_offset
|
637
|
+
y_offset + (@lines.last ? -@lines.last.y_min + line.y_max : 0)
|
638
|
+
else
|
639
|
+
style.line_spacing.baseline_distance(@lines.last, line)
|
640
|
+
end
|
624
641
|
@actual_height = new_height
|
642
|
+
@lines << line
|
643
|
+
y_offset = nil
|
644
|
+
indent = if item&.type == :penalty && item.penalty == Penalty::PARAGRAPH_BREAK
|
645
|
+
style.text_indent
|
646
|
+
else
|
647
|
+
0
|
648
|
+
end
|
625
649
|
true
|
650
|
+
elsif @height != Float::INFINITY
|
651
|
+
# some height left but item didn't fit on the line, search downwards for usable space
|
652
|
+
old_height = @actual_height
|
653
|
+
while item.width > width_block.call(item.height) && @actual_height <= @height
|
654
|
+
@actual_height += item.height / 3
|
655
|
+
end
|
656
|
+
if @actual_height + item.height <= @height
|
657
|
+
y_offset = @actual_height - old_height
|
658
|
+
true
|
659
|
+
else
|
660
|
+
@actual_height = old_height
|
661
|
+
too_wide_box = item
|
662
|
+
nil
|
663
|
+
end
|
626
664
|
else
|
665
|
+
too_wide_box = item
|
627
666
|
nil
|
628
667
|
end
|
668
|
+
end
|
669
|
+
|
670
|
+
if too_wide_box && too_wide_box.item.kind_of?(TextFragment) &&
|
671
|
+
too_wide_box.item.items.size > 1
|
672
|
+
rest[0..rest.index(too_wide_box)] = too_wide_box.item.items.map do |item|
|
673
|
+
Box.new(TextFragment.new(items: [item], style: too_wide_box.item.style))
|
674
|
+
end
|
675
|
+
too_wide_box = nil
|
629
676
|
else
|
630
|
-
|
677
|
+
reason = (too_wide_box ? :box : (rest.empty? ? :success : :height))
|
678
|
+
break
|
631
679
|
end
|
632
680
|
end
|
633
681
|
|
634
|
-
[rest,
|
682
|
+
[rest, reason]
|
635
683
|
end
|
636
684
|
|
637
|
-
# Draws the text
|
685
|
+
# Draws the layed out text onto the canvas with the top-left corner being at [x, y].
|
638
686
|
#
|
639
687
|
# Depending on the value of +fit+ the text may also be fitted:
|
640
688
|
#
|
641
689
|
# * If +true+, then #fit is always called.
|
642
|
-
# * If +:if_needed+, then #fit is only called if it has been called before.
|
690
|
+
# * If +:if_needed+, then #fit is only called if it has not been called before.
|
643
691
|
# * If +false+, then #fit is never called.
|
644
692
|
def draw(canvas, x, y, fit: :if_needed)
|
645
693
|
self.fit if fit == true || (!@actual_height && fit == :if_needed)
|
@@ -649,7 +697,15 @@ module HexaPDF
|
|
649
697
|
y -= initial_baseline_offset + @lines.first.y_offset
|
650
698
|
@lines.each_with_index do |line, index|
|
651
699
|
line_x = x + line.x_offset
|
652
|
-
line.each
|
700
|
+
line.each do |item, item_x, item_y|
|
701
|
+
if item.kind_of?(TextFragment)
|
702
|
+
item.draw(canvas, line_x + item_x, y + item_y)
|
703
|
+
elsif !item.empty?
|
704
|
+
canvas.restore_graphics_state
|
705
|
+
item.draw(canvas, line_x + item_x, y + item_y)
|
706
|
+
canvas.save_graphics_state
|
707
|
+
end
|
708
|
+
end
|
653
709
|
y -= @lines[index + 1].y_offset if @lines[index + 1]
|
654
710
|
end
|
655
711
|
end
|
@@ -657,20 +713,19 @@ module HexaPDF
|
|
657
713
|
|
658
714
|
private
|
659
715
|
|
660
|
-
# Returns the initial baseline offset from the top
|
661
|
-
# option.
|
716
|
+
# Returns the initial baseline offset from the top, based on the valign style option.
|
662
717
|
def initial_baseline_offset
|
663
718
|
case style.valign
|
664
719
|
when :top
|
665
720
|
@lines.first.y_max
|
666
721
|
when :center
|
667
722
|
if @height == Float::INFINITY
|
668
|
-
raise HexaPDF::Error, "Can't vertically align
|
723
|
+
raise HexaPDF::Error, "Can't vertically align when using unlimited height"
|
669
724
|
end
|
670
725
|
(@height - @actual_height) / 2.0 + @lines.first.y_max
|
671
726
|
when :bottom
|
672
727
|
if @height == Float::INFINITY
|
673
|
-
raise HexaPDF::Error, "Can't vertically align
|
728
|
+
raise HexaPDF::Error, "Can't vertically align when using unlimited height"
|
674
729
|
end
|
675
730
|
(@height - @actual_height) + @lines.first.y_max
|
676
731
|
end
|