hexapdf 0.5.0 → 0.6.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 +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
|