hexapdf 0.7.0 → 0.8.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 +39 -1
- data/CONTRIBUTERS +1 -1
- data/LICENSE +3 -0
- data/README.md +2 -1
- data/Rakefile +3 -1
- data/VERSION +1 -1
- data/examples/{hello_world.rb → 001-hello_world.rb} +0 -0
- data/examples/{graphics.rb → 002-graphics.rb} +1 -1
- data/examples/{arc.rb → 003-arcs.rb} +2 -2
- data/examples/{optimizing.rb → 004-optimizing.rb} +0 -0
- data/examples/{merging.rb → 005-merging.rb} +0 -0
- data/examples/{standard_pdf_fonts.rb → 006-standard_pdf_fonts.rb} +0 -0
- data/examples/{truetype.rb → 007-truetype.rb} +0 -0
- data/examples/{show_char_bboxes.rb → 008-show_char_bboxes.rb} +0 -0
- data/examples/{text_layouter_alignment.rb → 009-text_layouter_alignment.rb} +3 -3
- data/examples/{text_layouter_inline_boxes.rb → 010-text_layouter_inline_boxes.rb} +7 -9
- data/examples/{text_layouter_line_wrapping.rb → 011-text_layouter_line_wrapping.rb} +6 -5
- data/examples/{text_layouter_styling.rb → 012-text_layouter_styling.rb} +6 -8
- data/examples/013-text_layouter_shapes.rb +176 -0
- data/examples/014-text_in_polygon.rb +60 -0
- data/examples/{boxes.rb → 015-boxes.rb} +29 -21
- data/examples/016-frame_automatic_box_placement.rb +90 -0
- data/examples/017-frame_text_flow.rb +60 -0
- data/lib/hexapdf/cli/command.rb +4 -3
- data/lib/hexapdf/cli/files.rb +1 -1
- data/lib/hexapdf/cli/inspect.rb +0 -1
- data/lib/hexapdf/cli/merge.rb +1 -1
- data/lib/hexapdf/cli/modify.rb +1 -1
- data/lib/hexapdf/configuration.rb +2 -0
- data/lib/hexapdf/content/canvas.rb +3 -3
- data/lib/hexapdf/content/graphic_object.rb +1 -0
- data/lib/hexapdf/content/graphic_object/geom2d.rb +132 -0
- data/lib/hexapdf/dictionary.rb +7 -1
- data/lib/hexapdf/dictionary_fields.rb +35 -83
- data/lib/hexapdf/document.rb +9 -5
- data/lib/hexapdf/document/fonts.rb +1 -1
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
- data/lib/hexapdf/font/cmap/writer.rb +2 -2
- data/lib/hexapdf/font/true_type/builder.rb +1 -1
- data/lib/hexapdf/font/true_type/table.rb +1 -1
- data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
- data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
- data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
- data/lib/hexapdf/font/true_type/table/post.rb +1 -1
- data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
- data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
- data/lib/hexapdf/image_loader/jpeg.rb +1 -1
- data/lib/hexapdf/image_loader/png.rb +2 -2
- data/lib/hexapdf/layout.rb +3 -0
- data/lib/hexapdf/layout/box.rb +64 -46
- data/lib/hexapdf/layout/frame.rb +348 -0
- data/lib/hexapdf/layout/inline_box.rb +2 -2
- data/lib/hexapdf/layout/line.rb +3 -3
- data/lib/hexapdf/layout/style.rb +81 -14
- data/lib/hexapdf/layout/text_box.rb +84 -0
- data/lib/hexapdf/layout/text_fragment.rb +8 -8
- data/lib/hexapdf/layout/text_layouter.rb +278 -169
- data/lib/hexapdf/layout/width_from_polygon.rb +246 -0
- data/lib/hexapdf/rectangle.rb +9 -9
- data/lib/hexapdf/stream.rb +2 -2
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/type/action.rb +1 -1
- data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/cid_font.rb +2 -1
- data/lib/hexapdf/type/font.rb +0 -1
- data/lib/hexapdf/type/font_descriptor.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +3 -3
- data/lib/hexapdf/type/font_true_type.rb +8 -0
- data/lib/hexapdf/type/font_type0.rb +2 -1
- data/lib/hexapdf/type/font_type1.rb +7 -1
- data/lib/hexapdf/type/font_type3.rb +61 -0
- data/lib/hexapdf/type/graphics_state_parameter.rb +8 -8
- data/lib/hexapdf/type/image.rb +10 -0
- data/lib/hexapdf/type/page.rb +83 -10
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +2 -2
- data/test/hexapdf/content/graphic_object/test_geom2d.rb +79 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
- data/test/hexapdf/font/test_true_type_wrapper.rb +1 -1
- data/test/hexapdf/font/test_type1_wrapper.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_directory.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_head.rb +7 -3
- data/test/hexapdf/layout/test_box.rb +57 -15
- data/test/hexapdf/layout/test_frame.rb +313 -0
- data/test/hexapdf/layout/test_inline_box.rb +1 -1
- data/test/hexapdf/layout/test_style.rb +74 -0
- data/test/hexapdf/layout/test_text_box.rb +77 -0
- data/test/hexapdf/layout/test_text_layouter.rb +220 -239
- data/test/hexapdf/layout/test_width_from_polygon.rb +108 -0
- data/test/hexapdf/test_dictionary_fields.rb +22 -26
- data/test/hexapdf/test_document.rb +3 -3
- data/test/hexapdf/test_reference.rb +1 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/test_font_true_type.rb +25 -0
- data/test/hexapdf/type/test_font_type1.rb +6 -0
- data/test/hexapdf/type/test_font_type3.rb +26 -0
- data/test/hexapdf/type/test_image.rb +10 -0
- data/test/hexapdf/type/test_page.rb +114 -0
- data/test/test_helper.rb +1 -1
- metadata +65 -17
- data/examples/text_layouter_shapes.rb +0 -170
|
@@ -43,11 +43,11 @@ module HexaPDF
|
|
|
43
43
|
class InlineBox
|
|
44
44
|
|
|
45
45
|
# Creates an InlineBox that wraps a basic Box. All arguments (except +valign+) and the block
|
|
46
|
-
# are passed to Box::
|
|
46
|
+
# are passed to Box::create.
|
|
47
47
|
#
|
|
48
48
|
# See ::new for the +valign+ argument.
|
|
49
49
|
def self.create(valign: :baseline, **args, &block)
|
|
50
|
-
new(Box.
|
|
50
|
+
new(Box.create(**args, &block), valign: valign)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# The vertical alignment of the box.
|
data/lib/hexapdf/layout/line.rb
CHANGED
|
@@ -175,8 +175,8 @@ module HexaPDF
|
|
|
175
175
|
# An optional vertical offset that should be taken into account when positioning the line.
|
|
176
176
|
#
|
|
177
177
|
# For the first line in a paragraph this describes the offset from the top of the box to the
|
|
178
|
-
#
|
|
179
|
-
# the baseline of this line.
|
|
178
|
+
# baseline of the line. For all other lines it describes the offset from the previous baseline
|
|
179
|
+
# to the baseline of this line.
|
|
180
180
|
attr_accessor :y_offset
|
|
181
181
|
|
|
182
182
|
# Creates a new Line object, adding all given items to it.
|
|
@@ -290,7 +290,7 @@ module HexaPDF
|
|
|
290
290
|
#
|
|
291
291
|
# Clears all cached values.
|
|
292
292
|
#
|
|
293
|
-
# This method needs to be called if the
|
|
293
|
+
# This method needs to be called if the line's items are changed!
|
|
294
294
|
def clear_cache
|
|
295
295
|
@x_max = @y_min = @y_max = @text_y_min = @text_y_max = @width = nil
|
|
296
296
|
self
|
data/lib/hexapdf/layout/style.rb
CHANGED
|
@@ -138,12 +138,17 @@ module HexaPDF
|
|
|
138
138
|
# The value for right.
|
|
139
139
|
attr_accessor :right
|
|
140
140
|
|
|
141
|
+
# Creates a new Quad object. See #set for more information.
|
|
142
|
+
def initialize(obj)
|
|
143
|
+
set(obj)
|
|
144
|
+
end
|
|
145
|
+
|
|
141
146
|
# :call-seq:
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
147
|
+
# quad.set(value)
|
|
148
|
+
# quad.set(array)
|
|
149
|
+
# quad.set(quad)
|
|
145
150
|
#
|
|
146
|
-
#
|
|
151
|
+
# Sets all values of the quad.
|
|
147
152
|
#
|
|
148
153
|
# * If a single value is provided that is neither a Quad nor an array, it is handled as if
|
|
149
154
|
# an array with one value was given.
|
|
@@ -159,7 +164,7 @@ module HexaPDF
|
|
|
159
164
|
# third value.
|
|
160
165
|
# * Four or more values: Top is set to the first, right to the second, bottom to the third
|
|
161
166
|
# and left to the fourth value.
|
|
162
|
-
def
|
|
167
|
+
def set(obj)
|
|
163
168
|
case obj
|
|
164
169
|
when Quad
|
|
165
170
|
@top = obj.top
|
|
@@ -209,6 +214,8 @@ module HexaPDF
|
|
|
209
214
|
|
|
210
215
|
# Draws the border onto the canvas, inside the rectangle (x, y, w, h).
|
|
211
216
|
def draw(canvas, x, y, w, h)
|
|
217
|
+
return if none?
|
|
218
|
+
|
|
212
219
|
canvas.save_graphics_state do
|
|
213
220
|
if width.simple? && color.simple? && style.simple?
|
|
214
221
|
draw_simple_border(canvas, x, y, w, h)
|
|
@@ -229,12 +236,11 @@ module HexaPDF
|
|
|
229
236
|
miter_limit(10).
|
|
230
237
|
line_cap_style(line_cap_style(:top))
|
|
231
238
|
|
|
239
|
+
canvas.rectangle(x, y, w, h).clip_path.end_path
|
|
232
240
|
if style.top == :solid
|
|
233
241
|
canvas.line_dash_pattern(0).
|
|
234
242
|
rectangle(x + offset, y + offset, w - 2 * offset, h - 2 * offset).stroke
|
|
235
243
|
else
|
|
236
|
-
canvas.rectangle(x, y, w, h).clip_path.end_path
|
|
237
|
-
|
|
238
244
|
canvas.line_dash_pattern(line_dash_pattern(:top, w)).
|
|
239
245
|
line(x, y + h - offset, x + w, y + h - offset).
|
|
240
246
|
line(x + w, y + offset, x, y + offset).stroke
|
|
@@ -336,7 +342,7 @@ module HexaPDF
|
|
|
336
342
|
when :dotted
|
|
337
343
|
# Adjust the gap so that full dots appear in the corners.
|
|
338
344
|
w = width.send(edge)
|
|
339
|
-
gap = (length - w).to_f / (length.to_f / (w * 2)).ceil
|
|
345
|
+
gap = [(length - w).to_f / (length.to_f / (w * 2)).ceil, 1].max
|
|
340
346
|
HexaPDF::Content::LineDashPattern.new([0, gap], [gap - w * 0.5, 0].max)
|
|
341
347
|
end
|
|
342
348
|
end
|
|
@@ -358,7 +364,7 @@ module HexaPDF
|
|
|
358
364
|
# The object resolved in this way needs to respond to #call(canvas, box) where +canvas+ is the
|
|
359
365
|
# HexaPDF::Content::Canvas object on which it should be drawn and +box+ is a box-like object
|
|
360
366
|
# (e.g. Box or TextFragment). The coordinate system is translated so that the origin is at the
|
|
361
|
-
#
|
|
367
|
+
# bottom left corner of the box during the drawing operations.
|
|
362
368
|
class Layers
|
|
363
369
|
|
|
364
370
|
# Creates a new Layers object popuplated with the given +layers+.
|
|
@@ -762,6 +768,54 @@ module HexaPDF
|
|
|
762
768
|
# A Layers object containing all the layers that should be drawn under the box; defaults to no
|
|
763
769
|
# layers being drawn.
|
|
764
770
|
|
|
771
|
+
##
|
|
772
|
+
# :method: position
|
|
773
|
+
# :call-seq:
|
|
774
|
+
# position(value = nil)
|
|
775
|
+
#
|
|
776
|
+
# Specifies how a box should be positioned in a frame. The property #position_hint provides
|
|
777
|
+
# additional, position specific data. Defaults to :default.
|
|
778
|
+
#
|
|
779
|
+
# Possible values:
|
|
780
|
+
#
|
|
781
|
+
# :default:: Position the box at the current position. The exact horizontal position is given
|
|
782
|
+
# via the position hint. Space to the left/right of the box can't be used for other
|
|
783
|
+
# boxes.
|
|
784
|
+
#
|
|
785
|
+
# :float:: Position the box at the current position but let it "float" so that the space to
|
|
786
|
+
# the left/right can still be used. The position hint specifies where the box should
|
|
787
|
+
# float.
|
|
788
|
+
#
|
|
789
|
+
# :absolute:: Position the box at an absolute position relative to the frame. The coordinates
|
|
790
|
+
# are given via the position hint.
|
|
791
|
+
|
|
792
|
+
##
|
|
793
|
+
# :method: position_hint
|
|
794
|
+
# :call-seq:
|
|
795
|
+
# position_hint(value = nil)
|
|
796
|
+
#
|
|
797
|
+
# Specifies additional information on how a box should be positioned in a frame. The exact
|
|
798
|
+
# meaning depends on the value of the #position property.
|
|
799
|
+
#
|
|
800
|
+
# Possible values depending on the #position property:
|
|
801
|
+
#
|
|
802
|
+
# :default::
|
|
803
|
+
#
|
|
804
|
+
# :left:: (default) Align the box to the left side of the available region.
|
|
805
|
+
# :right:: Align the box to the right side of the available region.
|
|
806
|
+
# :center:: Horizontally center the box in the available region.
|
|
807
|
+
#
|
|
808
|
+
# :float::
|
|
809
|
+
#
|
|
810
|
+
# :left:: (default) Float the box to the left side of the available region.
|
|
811
|
+
# :right:: Float the box to the right side of the available region.
|
|
812
|
+
#
|
|
813
|
+
# :absolute::
|
|
814
|
+
#
|
|
815
|
+
# An array with the x- and y-coordinates of the bottom left corner of the absolutely
|
|
816
|
+
# positioned box. The coordinates are taken as being relative to the bottom left corner of
|
|
817
|
+
# the frame into which the box is drawn.
|
|
818
|
+
|
|
765
819
|
[
|
|
766
820
|
[:font, "raise HexaPDF::Error, 'No font set'"],
|
|
767
821
|
[:font_size, 10],
|
|
@@ -796,9 +850,11 @@ module HexaPDF
|
|
|
796
850
|
[:border, "Border.new", "Border.new(value)"],
|
|
797
851
|
[:overlays, "Layers.new", "Layers.new(value)"],
|
|
798
852
|
[:underlays, "Layers.new", "Layers.new(value)"],
|
|
853
|
+
[:position, :default],
|
|
854
|
+
[:position_hint, nil],
|
|
799
855
|
].each do |name, default, setter = "value", extra_args = ""|
|
|
800
856
|
default = default.inspect unless default.kind_of?(String)
|
|
801
|
-
module_eval(<<-EOF, __FILE__, __LINE__)
|
|
857
|
+
module_eval(<<-EOF, __FILE__, __LINE__ + 1)
|
|
802
858
|
def #{name}(value = UNSET#{extra_args})
|
|
803
859
|
value == UNSET ? (@#{name} ||= #{default}) : (@#{name} = #{setter}; self)
|
|
804
860
|
end
|
|
@@ -836,7 +892,7 @@ module HexaPDF
|
|
|
836
892
|
[:text_line_wrapping_algorithm, 'TextLayouter::SimpleLineWrapping'],
|
|
837
893
|
].each do |name, default|
|
|
838
894
|
default = default.inspect unless default.kind_of?(String)
|
|
839
|
-
module_eval(<<-EOF, __FILE__, __LINE__)
|
|
895
|
+
module_eval(<<-EOF, __FILE__, __LINE__ + 1)
|
|
840
896
|
def #{name}(value = UNSET, &block)
|
|
841
897
|
if value == UNSET && !block
|
|
842
898
|
@#{name} ||= #{default}
|
|
@@ -914,12 +970,22 @@ module HexaPDF
|
|
|
914
970
|
|
|
915
971
|
# The ascender of the font scaled appropriately.
|
|
916
972
|
def scaled_font_ascender
|
|
917
|
-
@
|
|
973
|
+
@scaled_font_ascender ||= font.wrapped_font.ascender * font.scaling_factor * font_size / 1000
|
|
918
974
|
end
|
|
919
975
|
|
|
920
976
|
# The descender of the font scaled appropriately.
|
|
921
977
|
def scaled_font_descender
|
|
922
|
-
@
|
|
978
|
+
@scaled_font_descender ||= font.wrapped_font.descender * font.scaling_factor * font_size / 1000
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# The minimum y-coordinate, calculated using the scaled descender of the font.
|
|
982
|
+
def scaled_y_min
|
|
983
|
+
@scaled_y_min ||= scaled_font_descender + calculated_text_rise
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# The maximum y-coordinate, calculated using the scaled descender of the font.
|
|
987
|
+
def scaled_y_max
|
|
988
|
+
@scaled_y_max ||= scaled_font_ascender + calculated_text_rise
|
|
923
989
|
end
|
|
924
990
|
|
|
925
991
|
# Returns the width of the item scaled appropriately (by taking font size, characters spacing,
|
|
@@ -946,7 +1012,8 @@ module HexaPDF
|
|
|
946
1012
|
# ascender, descender.
|
|
947
1013
|
def clear_cache
|
|
948
1014
|
@scaled_font_size = @scaled_character_spacing = @scaled_word_spacing = nil
|
|
949
|
-
@scaled_horizontal_scaling = @
|
|
1015
|
+
@scaled_horizontal_scaling = @scaled_font_ascender = @scaled_font_descender = nil
|
|
1016
|
+
@scaled_y_min = @scaled_y_max = nil
|
|
950
1017
|
@scaled_item_widths.clear
|
|
951
1018
|
end
|
|
952
1019
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2018 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#++
|
|
33
|
+
require 'hexapdf/layout/box'
|
|
34
|
+
require 'hexapdf/layout/text_layouter'
|
|
35
|
+
|
|
36
|
+
module HexaPDF
|
|
37
|
+
module Layout
|
|
38
|
+
|
|
39
|
+
# A TextBox is used for drawing text, either inside a rectangular box or by flowing it around
|
|
40
|
+
# objects of a Frame.
|
|
41
|
+
#
|
|
42
|
+
# This class uses TextLayouter behind the scenes to do the hard work.
|
|
43
|
+
class TextBox < Box
|
|
44
|
+
|
|
45
|
+
# Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
|
|
46
|
+
# objects).
|
|
47
|
+
def initialize(items, **kwargs)
|
|
48
|
+
super(kwargs)
|
|
49
|
+
@tl = TextLayouter.new(style)
|
|
50
|
+
@items = items
|
|
51
|
+
@result = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Fits the text box into the Frame.
|
|
55
|
+
#
|
|
56
|
+
# Depending on the 'position' style property, the text is either fit into the rectangular area
|
|
57
|
+
# given by +available_width+ and +available_height+, or fit to the outline of the frame
|
|
58
|
+
# starting from the top.
|
|
59
|
+
def fit(available_width, available_height, frame)
|
|
60
|
+
@result = if style.position == :flow
|
|
61
|
+
@tl.fit(@items, frame.width_specification, frame.contour_line.bbox.height)
|
|
62
|
+
else
|
|
63
|
+
@tl.fit(@items, available_width, available_height)
|
|
64
|
+
end
|
|
65
|
+
@height = @result.height
|
|
66
|
+
@width = @result.lines.max_by(&:width)&.width || 0
|
|
67
|
+
|
|
68
|
+
success = (@result.status == :success)
|
|
69
|
+
@draw_block = success ? method(:draw_text) : nil
|
|
70
|
+
success
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Draws the text into the box.
|
|
76
|
+
def draw_text(canvas, _self)
|
|
77
|
+
return unless @result && !@result.lines.empty?
|
|
78
|
+
@result.draw(canvas, 0, @result.height)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -49,7 +49,7 @@ module HexaPDF
|
|
|
49
49
|
# The items of a text fragment may be frozen to indicate that the fragment is potentially used
|
|
50
50
|
# multiple times.
|
|
51
51
|
#
|
|
52
|
-
# The rectangle with the
|
|
52
|
+
# The rectangle with the bottom left corner (#x_min, #y_min) and the top right corner (#x_max,
|
|
53
53
|
# #y_max) describes the minimum bounding box of the whole text fragment and is usually *not*
|
|
54
54
|
# equal to the box (0, 0)-(#width, #height).
|
|
55
55
|
class TextFragment
|
|
@@ -129,7 +129,7 @@ module HexaPDF
|
|
|
129
129
|
# * It is assumed that the text matrix is not rotated, skewed, etc. so that setting the text
|
|
130
130
|
# position can be done using the optimal method.
|
|
131
131
|
def draw(canvas, x, y, ignore_text_properties: false)
|
|
132
|
-
style.underlays.draw(canvas, x, y + y_min, self)
|
|
132
|
+
style.underlays.draw(canvas, x, y + y_min, self) if style.underlays?
|
|
133
133
|
|
|
134
134
|
# Set general font related graphics state if necessary
|
|
135
135
|
unless ignore_text_properties
|
|
@@ -173,7 +173,7 @@ module HexaPDF
|
|
|
173
173
|
end
|
|
174
174
|
canvas.show_glyphs_only(items)
|
|
175
175
|
|
|
176
|
-
if style.underline
|
|
176
|
+
if style.underline? && style.underline
|
|
177
177
|
y_offset = style.calculated_underline_position
|
|
178
178
|
canvas.save_graphics_state do
|
|
179
179
|
canvas.stroke_color(style.fill_color).
|
|
@@ -185,7 +185,7 @@ module HexaPDF
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
if style.strikeout
|
|
188
|
+
if style.strikeout? && style.strikeout
|
|
189
189
|
y_offset = style.calculated_strikeout_position
|
|
190
190
|
canvas.save_graphics_state do
|
|
191
191
|
canvas.stroke_color(style.fill_color).
|
|
@@ -197,7 +197,7 @@ module HexaPDF
|
|
|
197
197
|
end
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
-
style.overlays.draw(canvas, x, y + y_min, self)
|
|
200
|
+
style.overlays.draw(canvas, x, y + y_min, self) if style.overlays?
|
|
201
201
|
end
|
|
202
202
|
|
|
203
203
|
# The minimum x-coordinate of the first glyph.
|
|
@@ -212,12 +212,12 @@ module HexaPDF
|
|
|
212
212
|
|
|
213
213
|
# The minimum y-coordinate, calculated using the scaled descender of the font.
|
|
214
214
|
def y_min
|
|
215
|
-
|
|
215
|
+
style.scaled_y_min
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
# The maximum y-coordinate, calculated using the scaled ascender of the font.
|
|
219
219
|
def y_max
|
|
220
|
-
|
|
220
|
+
style.scaled_y_max
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
# The minimum y-coordinate of any item.
|
|
@@ -263,7 +263,7 @@ module HexaPDF
|
|
|
263
263
|
#
|
|
264
264
|
# This method needs to be called if the fragment's items or attributes are changed!
|
|
265
265
|
def clear_cache
|
|
266
|
-
@x_min = @x_max = @
|
|
266
|
+
@x_min = @x_max = @exact_y_min = @exact_y_max = @width = @height = nil
|
|
267
267
|
self
|
|
268
268
|
end
|
|
269
269
|
|
|
@@ -50,9 +50,11 @@ module HexaPDF
|
|
|
50
50
|
#
|
|
51
51
|
# * The first line may be indented by setting Style#text_indent which may also be negative.
|
|
52
52
|
#
|
|
53
|
+
# * Text can be fitted into arbitrarily shaped areas, even containing holes.
|
|
54
|
+
#
|
|
53
55
|
# == Layouting Algorithm
|
|
54
56
|
#
|
|
55
|
-
# Laying out text consists of
|
|
57
|
+
# Laying out text consists of three phases:
|
|
56
58
|
#
|
|
57
59
|
# 1. The items are broken into pieces which are wrapped into Box, Glue or Penalty objects.
|
|
58
60
|
# Additional Penalty objects marking line breaking opportunities are inserted where needed.
|
|
@@ -61,6 +63,10 @@ module HexaPDF
|
|
|
61
63
|
# 2. The pieces are arranged into lines using a very simple algorithm that just puts the maximum
|
|
62
64
|
# number of consecutive pieces into each line. This step is done by the SimpleLineWrapping
|
|
63
65
|
# module.
|
|
66
|
+
#
|
|
67
|
+
# 3. The lines of step two may actually not be whole lines but line fragments if the area has
|
|
68
|
+
# holes or other discontinuities. The #fit method deals with those so that the line wrapping
|
|
69
|
+
# algorithm can be separate.
|
|
64
70
|
class TextLayouter
|
|
65
71
|
|
|
66
72
|
using NumericRefinements
|
|
@@ -273,7 +279,8 @@ module HexaPDF
|
|
|
273
279
|
# Implementation of a simple line wrapping algorithm.
|
|
274
280
|
#
|
|
275
281
|
# The algorithm arranges the given items so that the maximum number is put onto each line,
|
|
276
|
-
# taking the differences of Box, Glue and Penalty items into account.
|
|
282
|
+
# taking the differences of Box, Glue and Penalty items into account. It is not as advanced as
|
|
283
|
+
# say Knuth's line wrapping algorithm in that it doesn't optimize paragraphs.
|
|
277
284
|
class SimpleLineWrapping
|
|
278
285
|
|
|
279
286
|
# :call-seq:
|
|
@@ -302,6 +309,12 @@ module HexaPDF
|
|
|
302
309
|
# item is a box item, this single item didn't fit into the available width; the caller has
|
|
303
310
|
# to handle this situation, e.g. by stopping.
|
|
304
311
|
#
|
|
312
|
+
# In case of varying widths, the +width_block+ may also return +nil+ in which case the
|
|
313
|
+
# algorithm should revert back to a stored item index and then start as if beginning a new
|
|
314
|
+
# line. Which index to use is told the algorithm through the special return value
|
|
315
|
+
# +:store_start_of_line+ of the yielded-to block. When this return value is used, the
|
|
316
|
+
# current start of the line index should be stored for later use.
|
|
317
|
+
#
|
|
305
318
|
# After the algorithm is finished, it returns the unused items.
|
|
306
319
|
def self.call(items, width_block, &block)
|
|
307
320
|
obj = new(items, width_block)
|
|
@@ -390,7 +403,7 @@ module HexaPDF
|
|
|
390
403
|
|
|
391
404
|
# Performs the line wrapping with variable widths.
|
|
392
405
|
def variable_width_wrapping
|
|
393
|
-
index = 0
|
|
406
|
+
index = @stored_index = 0
|
|
394
407
|
|
|
395
408
|
while (item = @items[index])
|
|
396
409
|
case item.type
|
|
@@ -399,6 +412,12 @@ module HexaPDF
|
|
|
399
412
|
if new_height > @line_height
|
|
400
413
|
@line_height = new_height
|
|
401
414
|
@available_width = @width_block.call(@line_height)
|
|
415
|
+
if !@available_width || @width > @available_width
|
|
416
|
+
index = (@available_width ? @beginning_of_line_index : @stored_index)
|
|
417
|
+
item = @items[index]
|
|
418
|
+
reset_after_line_break_variable_width(index, @line_height)
|
|
419
|
+
redo
|
|
420
|
+
end
|
|
402
421
|
end
|
|
403
422
|
if add_box_item(item.item)
|
|
404
423
|
@height_calc << item.item
|
|
@@ -407,19 +426,19 @@ module HexaPDF
|
|
|
407
426
|
index = reset_line_to_last_breakpoint_state
|
|
408
427
|
item = @items[index]
|
|
409
428
|
end
|
|
410
|
-
break unless yield(create_line, item)
|
|
411
|
-
|
|
429
|
+
break unless (action = yield(create_line, item))
|
|
430
|
+
reset_after_line_break_variable_width(index, 0, action)
|
|
412
431
|
redo
|
|
413
432
|
end
|
|
414
433
|
when :glue
|
|
415
434
|
unless add_glue_item(item.item, index)
|
|
416
|
-
break unless yield(create_line, item)
|
|
417
|
-
|
|
435
|
+
break unless (action = yield(create_line, item))
|
|
436
|
+
reset_after_line_break_variable_width(index + 1, 0, action)
|
|
418
437
|
end
|
|
419
438
|
when :penalty
|
|
420
439
|
if item.penalty <= -Penalty::INFINITY
|
|
421
|
-
break unless yield(create_unjustified_line, item)
|
|
422
|
-
|
|
440
|
+
break unless (action = yield(create_unjustified_line, item))
|
|
441
|
+
reset_after_line_break_variable_width(index + 1, 0, action)
|
|
423
442
|
elsif item.penalty >= Penalty::INFINITY
|
|
424
443
|
@break_prohibited_state = true
|
|
425
444
|
add_box_item(item.item) if item.width > 0
|
|
@@ -510,8 +529,9 @@ module HexaPDF
|
|
|
510
529
|
end
|
|
511
530
|
|
|
512
531
|
# Resets the line state variables to their initial values. The +index+ specifies the items
|
|
513
|
-
# index of the first item on the new line.
|
|
514
|
-
|
|
532
|
+
# index of the first item on the new line. The +line_height+ specifies the line height to
|
|
533
|
+
# use for getting the available width.
|
|
534
|
+
def reset_after_line_break(index, line_height = 0)
|
|
515
535
|
@beginning_of_line_index = index
|
|
516
536
|
@line_items.clear
|
|
517
537
|
@width = 0
|
|
@@ -519,155 +539,247 @@ module HexaPDF
|
|
|
519
539
|
@last_breakpoint_index = index
|
|
520
540
|
@last_breakpoint_line_items_index = 0
|
|
521
541
|
@break_prohibited_state = false
|
|
522
|
-
@available_width = @width_block.call(
|
|
542
|
+
@available_width = @width_block.call(line_height)
|
|
543
|
+
end
|
|
523
544
|
|
|
524
|
-
|
|
545
|
+
# Specialized reset method for variable width wrapping.
|
|
546
|
+
#
|
|
547
|
+
# * The arguments +index+ and +line_height+ are also passed to #reset_after_line_break.
|
|
548
|
+
#
|
|
549
|
+
# * If the +action+ argument is +:store_start_of_line+, the stored item index is reset to
|
|
550
|
+
# the index of the first item of the line.
|
|
551
|
+
def reset_after_line_break_variable_width(index, line_height, action = :none)
|
|
552
|
+
@stored_index = @beginning_of_line_index if action == :store_start_of_line
|
|
553
|
+
@line_height = line_height
|
|
525
554
|
@height_calc.reset
|
|
555
|
+
reset_after_line_break(index, line_height)
|
|
526
556
|
end
|
|
527
557
|
|
|
528
558
|
end
|
|
529
559
|
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
560
|
+
# Encapsulates the result of layouting items using a TextLayouter and provides a method for
|
|
561
|
+
# drawing the result (i.e. the layed out lines) on a canvas.
|
|
562
|
+
class Result
|
|
563
|
+
|
|
564
|
+
# The status after layouting the items:
|
|
565
|
+
#
|
|
566
|
+
# +:success+:: There are no remaining items.
|
|
567
|
+
# +:box_too_wide+:: A single text or inline box was too wide to fit alone on a line.
|
|
568
|
+
# +:height+:: There was not enough height for all items to layout.
|
|
569
|
+
#
|
|
570
|
+
# Even if the result is not +:success+, the layouting may still be successful depending on
|
|
571
|
+
# the usage. For example, if we expect that there may be too many items to fit, +:height+ is
|
|
572
|
+
# still a success.
|
|
573
|
+
attr_reader :status
|
|
574
|
+
|
|
575
|
+
# Array of layed out lines.
|
|
576
|
+
attr_reader :lines
|
|
577
|
+
|
|
578
|
+
# The actual height of all layed out lines (this includes a possible offset for the first
|
|
579
|
+
# line).
|
|
580
|
+
attr_reader :height
|
|
581
|
+
|
|
582
|
+
# The remaining items that couldn't be layed out.
|
|
583
|
+
attr_reader :remaining_items
|
|
584
|
+
|
|
585
|
+
# Creates a new Result structure.
|
|
586
|
+
def initialize(status, lines, remaining_items)
|
|
587
|
+
@status = status
|
|
588
|
+
@lines = lines
|
|
589
|
+
@height = @lines.sum(&:y_offset) - (@lines.last&.y_min || 0)
|
|
590
|
+
@remaining_items = remaining_items
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Draws the layed out lines onto the canvas with the top-left corner being at [x, y].
|
|
594
|
+
def draw(canvas, x, y)
|
|
595
|
+
last_item = nil
|
|
596
|
+
canvas.save_graphics_state do
|
|
597
|
+
# Best effort for leading in case we have an evenly spaced paragraph
|
|
598
|
+
canvas.leading(@lines[1].y_offset) if @lines.size > 1
|
|
599
|
+
@lines.each_with_index do |line, index|
|
|
600
|
+
y -= @lines[index].y_offset
|
|
601
|
+
line_x = x + line.x_offset
|
|
602
|
+
line.each do |item, item_x, item_y|
|
|
603
|
+
if item.kind_of?(TextFragment)
|
|
604
|
+
item.draw(canvas, line_x + item_x, y + item_y,
|
|
605
|
+
ignore_text_properties: last_item&.style == item.style)
|
|
606
|
+
last_item = item
|
|
607
|
+
elsif !item.empty?
|
|
608
|
+
canvas.restore_graphics_state
|
|
609
|
+
item.draw(canvas, line_x + item_x, y + item_y)
|
|
610
|
+
canvas.save_graphics_state
|
|
611
|
+
last_item = nil
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
539
618
|
end
|
|
540
619
|
|
|
541
620
|
# The style to be applied.
|
|
542
621
|
#
|
|
543
622
|
# Only the following properties are used: Style#text_indent, Style#align, Style#valign,
|
|
544
|
-
# Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
|
|
623
|
+
# Style#line_spacing, Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
|
|
545
624
|
attr_reader :style
|
|
546
625
|
|
|
547
|
-
#
|
|
548
|
-
attr_reader :items
|
|
549
|
-
|
|
550
|
-
# Array of Line objects describing the layed out lines.
|
|
551
|
-
#
|
|
552
|
-
# The array is only valid after #fit was called.
|
|
553
|
-
attr_reader :lines
|
|
554
|
-
|
|
555
|
-
# The actual height of the layed out text. Can be +nil+ if the items have not been layed out
|
|
556
|
-
# yet, i.e. if #fit has not been called.
|
|
557
|
-
attr_reader :actual_height
|
|
558
|
-
|
|
559
|
-
# Creates a new TextLayouter object with the given width containing the given items.
|
|
560
|
-
#
|
|
561
|
-
# The width can either be a simple number specifying a fixed width, or an object that responds
|
|
562
|
-
# to #call(height, line_height) where +height+ is the bottom of last line and +line_height+ is
|
|
563
|
-
# the height of the line to be layed out. The return value should be the available width given
|
|
564
|
-
# these height restrictions.
|
|
565
|
-
#
|
|
566
|
-
# The optional +x_offsets+ argument works like +width+ but can be used to specify (varying)
|
|
567
|
-
# offsets from the left side (e.g. when the left side of the text should follow a certain
|
|
568
|
-
# shape).
|
|
569
|
-
#
|
|
570
|
-
# The height is optional and if not specified means that the text layout has infinite height.
|
|
626
|
+
# Creates a new TextLayouter object with the given style.
|
|
571
627
|
#
|
|
572
628
|
# The +style+ argument can either be a Style object or a hash of style options. See #style for
|
|
573
629
|
# the properties that are used by the layouter.
|
|
574
|
-
def initialize(
|
|
630
|
+
def initialize(style = Style.new)
|
|
575
631
|
@style = (style.kind_of?(Style) ? style : Style.new(style))
|
|
576
|
-
@lines = []
|
|
577
|
-
self.items = items
|
|
578
|
-
@width = width
|
|
579
|
-
@height = height || Float::INFINITY
|
|
580
|
-
@x_offsets = x_offsets && (x_offsets.respond_to?(:call) ? x_offsets : proc { x_offsets })
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
# Sets the items to be arranged by the text layouter, clearing the internal state.
|
|
584
|
-
#
|
|
585
|
-
# If the items array contains items before text segmentation, the text segmentation algorithm
|
|
586
|
-
# is automatically applied.
|
|
587
|
-
def items=(items)
|
|
588
|
-
unless items.empty? || items[0].respond_to?(:type)
|
|
589
|
-
items = style.text_segmentation_algorithm.call(items)
|
|
590
|
-
end
|
|
591
|
-
@items = items.freeze
|
|
592
|
-
@lines.clear
|
|
593
|
-
@actual_height = nil
|
|
594
632
|
end
|
|
595
633
|
|
|
596
634
|
# :call-seq:
|
|
597
|
-
# text_layouter.fit
|
|
635
|
+
# text_layouter.fit(items, width, height) -> result
|
|
636
|
+
#
|
|
637
|
+
# Fits the items into the given area and returns a Result object with all the information.
|
|
598
638
|
#
|
|
599
|
-
#
|
|
600
|
-
# there are remaining items.
|
|
639
|
+
# The +height+ argument is just a number specifying the maximum height that can be used.
|
|
601
640
|
#
|
|
602
|
-
# The
|
|
603
|
-
# inline box item is too wide to fit alone on a line, or +:height+ if there was not enough
|
|
604
|
-
# height for all items.
|
|
641
|
+
# The +width+ argument can be one of the following:
|
|
605
642
|
#
|
|
606
|
-
#
|
|
643
|
+
# **a number**::
|
|
644
|
+
# In this case the layed out lines have this number as maximum width. This is the standard
|
|
645
|
+
# case and means that the area in which the text is layed out is a rectangle.
|
|
607
646
|
#
|
|
608
|
-
#
|
|
609
|
-
#
|
|
647
|
+
# **an array with an even number of numbers**::
|
|
648
|
+
# The array has to be of the form [offset, width, offset, width, ...], so the even indices
|
|
649
|
+
# specify offsets (relative to the current position, not absolute offsets from the left),
|
|
650
|
+
# the odd indices widths. This allows laying out lines containing holes in them.
|
|
610
651
|
#
|
|
611
|
-
#
|
|
612
|
-
#
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
652
|
+
# A simple example: [15, 100, 30, 40]. This means that a space of 15 on the left is never
|
|
653
|
+
# used, then comes text with a maximum width of 100, starting at the absolute offset 15,
|
|
654
|
+
# followed by a hole with a width of 30 and then text again with a width of 40, starting
|
|
655
|
+
# at the absolute offset 145 (=15 + 100 + 30).
|
|
656
|
+
#
|
|
657
|
+
# **an object responding to #call(height, line_height)**::
|
|
658
|
+
#
|
|
659
|
+
# The provided argument +height+ is the bottom of last line (or 0 in case of the first
|
|
660
|
+
# line) and +line_height+ is the height of the line to be layed out. The return value has
|
|
661
|
+
# to be of one of the forms above (i.e. a single number or an array of numbers) and should
|
|
662
|
+
# describe the area given these height restrictions.
|
|
663
|
+
#
|
|
664
|
+
# This allows laying out text inside complex, arbitrarily formed shapes and can be used,
|
|
665
|
+
# for example, for flowing text around objects.
|
|
666
|
+
#
|
|
667
|
+
# The text segmentation algorithm specified via #style is applied to the items in case they
|
|
668
|
+
# are not already in segmented form. This also means that Result#remaining_items always
|
|
669
|
+
# contains segmented items.
|
|
670
|
+
def fit(items, width, height)
|
|
671
|
+
unless items.empty? || items[0].respond_to?(:type)
|
|
672
|
+
items = style.text_segmentation_algorithm.call(items)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# result variables
|
|
676
|
+
lines = []
|
|
677
|
+
actual_height = 0
|
|
678
|
+
rest = items
|
|
616
679
|
|
|
617
|
-
|
|
680
|
+
# processing state variables
|
|
681
|
+
indent = style.text_indent
|
|
682
|
+
line_fragments = []
|
|
683
|
+
line_height = 0
|
|
684
|
+
last_line = nil
|
|
618
685
|
y_offset = 0
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
686
|
+
width_spec = nil
|
|
687
|
+
width_spec_index = 0
|
|
688
|
+
width_block =
|
|
689
|
+
if width.respond_to?(:call)
|
|
690
|
+
last_actual_height = nil
|
|
691
|
+
last_line_height = nil
|
|
692
|
+
proc do |h|
|
|
693
|
+
line_height = [line_height, h || 0].max
|
|
694
|
+
if last_actual_height != actual_height || last_line_height != line_height
|
|
695
|
+
spec = width.call(actual_height, line_height)
|
|
696
|
+
spec = [0, spec] unless spec.kind_of?(Array)
|
|
697
|
+
last_actual_height = actual_height
|
|
698
|
+
last_line_height = line_height
|
|
699
|
+
else
|
|
700
|
+
spec = width_spec
|
|
701
|
+
end
|
|
702
|
+
if spec == width_spec
|
|
703
|
+
# no changes, just need to return the width of the current part
|
|
704
|
+
width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0)
|
|
705
|
+
elsif line_fragments.each_with_index.all? {|l, i| l.width <= spec[i * 2 + 1] }
|
|
706
|
+
# width_spec changed, parts can only get smaller but processed parts still fit
|
|
707
|
+
width_spec = spec
|
|
708
|
+
width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0)
|
|
709
|
+
else
|
|
710
|
+
# width_spec changed and some processed part doesn't fit anymore, retry from start
|
|
711
|
+
line_fragments.clear
|
|
712
|
+
width_spec = spec
|
|
713
|
+
width_spec_index = 0
|
|
714
|
+
nil
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
elsif width.kind_of?(Array)
|
|
718
|
+
width_spec = width
|
|
719
|
+
proc { width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0) }
|
|
720
|
+
else
|
|
721
|
+
width_spec = [0, width]
|
|
722
|
+
proc { width - indent }
|
|
723
|
+
end
|
|
625
724
|
|
|
626
725
|
while true
|
|
627
726
|
too_wide_box = nil
|
|
628
727
|
|
|
629
728
|
rest = style.text_line_wrapping_algorithm.call(rest, width_block) do |line, item|
|
|
729
|
+
# make sure empty lines broken by mandatory paragraph breaks are not empty
|
|
630
730
|
line << TextFragment.new([], style) if item&.type != :box && line.items.empty?
|
|
631
|
-
new_height = @actual_height + line.height +
|
|
632
|
-
(@lines.empty? ? 0 : style.line_spacing.gap(@lines.last, line))
|
|
633
731
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
732
|
+
# item didn't fit into first part, find next available part
|
|
733
|
+
if line.items.empty? && line_fragments.empty?
|
|
734
|
+
old_height = actual_height
|
|
735
|
+
while item.width > width_block.call(item.height) && actual_height <= height
|
|
736
|
+
width_spec_index += 1
|
|
737
|
+
if width_spec_index >= width_spec.size / 2
|
|
738
|
+
actual_height += item.height / 3
|
|
739
|
+
width_spec_index = 0
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
if actual_height + item.height <= height
|
|
743
|
+
width_spec_index.times { line_fragments << Line.new }
|
|
744
|
+
y_offset = actual_height - old_height
|
|
745
|
+
next true
|
|
746
|
+
else
|
|
747
|
+
actual_height = old_height
|
|
748
|
+
too_wide_box = item
|
|
749
|
+
next nil
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# continue with line fragments of current line if there are still parts and items
|
|
754
|
+
# available; also handles the case if at least the first fragment is not empty and a
|
|
755
|
+
# single item didn't fit into at least one of the other parts
|
|
756
|
+
line_fragments << line
|
|
757
|
+
unless line_fragments.size == width_spec.size / 2 || !item || item.type == :penalty
|
|
758
|
+
width_spec_index += 1
|
|
759
|
+
next (width_spec_index == 1 ? :store_start_of_line : true)
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
combined_line = create_combined_line(line_fragments)
|
|
763
|
+
new_height = actual_height + combined_line.height +
|
|
764
|
+
(last_line ? style.line_spacing.gap(last_line, combined_line) : 0)
|
|
765
|
+
|
|
766
|
+
if new_height <= height
|
|
637
767
|
# valid line found, use it
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
y_offset + (@lines.last ? -@lines.last.y_min + line.y_max : 0)
|
|
643
|
-
else
|
|
644
|
-
style.line_spacing.baseline_distance(@lines.last, line)
|
|
645
|
-
end
|
|
646
|
-
@actual_height = new_height
|
|
647
|
-
@lines << line
|
|
648
|
-
y_offset = nil
|
|
768
|
+
apply_offsets(line_fragments, width_spec, indent, last_line, combined_line, y_offset)
|
|
769
|
+
lines.concat(line_fragments)
|
|
770
|
+
line_fragments.clear
|
|
771
|
+
width_spec_index = 0
|
|
649
772
|
indent = if item&.type == :penalty && item.penalty == Penalty::PARAGRAPH_BREAK
|
|
650
773
|
style.text_indent
|
|
651
774
|
else
|
|
652
775
|
0
|
|
653
776
|
end
|
|
777
|
+
last_line = combined_line
|
|
778
|
+
actual_height = new_height
|
|
779
|
+
line_height = 0
|
|
780
|
+
y_offset = nil
|
|
654
781
|
true
|
|
655
|
-
elsif @height != Float::INFINITY
|
|
656
|
-
# some height left but item didn't fit on the line, search downwards for usable space
|
|
657
|
-
old_height = @actual_height
|
|
658
|
-
while item.width > width_block.call(item.height) && @actual_height <= @height
|
|
659
|
-
@actual_height += item.height / 3
|
|
660
|
-
end
|
|
661
|
-
if @actual_height + item.height <= @height
|
|
662
|
-
y_offset = @actual_height - old_height
|
|
663
|
-
true
|
|
664
|
-
else
|
|
665
|
-
@actual_height = old_height
|
|
666
|
-
too_wide_box = item
|
|
667
|
-
nil
|
|
668
|
-
end
|
|
669
782
|
else
|
|
670
|
-
too_wide_box = item
|
|
671
783
|
nil
|
|
672
784
|
end
|
|
673
785
|
end
|
|
@@ -679,74 +791,71 @@ module HexaPDF
|
|
|
679
791
|
end
|
|
680
792
|
too_wide_box = nil
|
|
681
793
|
else
|
|
682
|
-
|
|
794
|
+
status = (too_wide_box ? :box_too_wide : (rest.empty? ? :success : :height))
|
|
683
795
|
break
|
|
684
796
|
end
|
|
685
797
|
end
|
|
686
798
|
|
|
687
|
-
|
|
799
|
+
unless lines.empty?
|
|
800
|
+
lines.first.y_offset += initial_baseline_offset(lines, height, actual_height)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
Result.new(status, lines, rest)
|
|
688
804
|
end
|
|
689
805
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
#
|
|
693
|
-
# Draws the layed out text onto the canvas with the top-left corner being at [x, y].
|
|
694
|
-
#
|
|
695
|
-
# Depending on the value of +fit+ the text may also be fitted:
|
|
696
|
-
#
|
|
697
|
-
# * If +true+, then #fit is always called.
|
|
698
|
-
# * If +:if_needed+, then #fit is only called if it has not been called before.
|
|
699
|
-
# * If +false+, then #fit is never called.
|
|
806
|
+
private
|
|
807
|
+
|
|
808
|
+
# :nodoc:
|
|
700
809
|
#
|
|
701
|
-
#
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
last_item = nil
|
|
707
|
-
canvas.save_graphics_state do
|
|
708
|
-
if @lines.size > 1
|
|
709
|
-
canvas.leading(style.line_spacing.baseline_distance(@lines[0], @lines[1]))
|
|
710
|
-
end
|
|
711
|
-
y -= initial_baseline_offset + @lines.first.y_offset
|
|
712
|
-
@lines.each_with_index do |line, index|
|
|
713
|
-
line_x = x + line.x_offset
|
|
714
|
-
line.each do |item, item_x, item_y|
|
|
715
|
-
if item.kind_of?(TextFragment)
|
|
716
|
-
item.draw(canvas, line_x + item_x, y + item_y,
|
|
717
|
-
ignore_text_properties: last_item&.style == item.style)
|
|
718
|
-
last_item = item
|
|
719
|
-
elsif !item.empty?
|
|
720
|
-
canvas.restore_graphics_state
|
|
721
|
-
item.draw(canvas, line_x + item_x, y + item_y)
|
|
722
|
-
canvas.save_graphics_state
|
|
723
|
-
last_item = nil
|
|
724
|
-
end
|
|
725
|
-
end
|
|
726
|
-
y -= @lines[index + 1].y_offset if @lines[index + 1]
|
|
727
|
-
end
|
|
810
|
+
# A dummy line class for use with Style#line_spacing methods in case a line actually consists
|
|
811
|
+
# of multiple line fragments.
|
|
812
|
+
DummyLine = Struct.new(:y_min, :y_max) do
|
|
813
|
+
def height
|
|
814
|
+
y_max - y_min
|
|
728
815
|
end
|
|
816
|
+
end
|
|
729
817
|
|
|
730
|
-
|
|
818
|
+
# Creates a line combining all items from the given line fragments for height calculations.
|
|
819
|
+
def create_combined_line(line_frags)
|
|
820
|
+
if line_frags.size == 1
|
|
821
|
+
line_frags[0]
|
|
822
|
+
else
|
|
823
|
+
calc = Line::HeightCalculator.new
|
|
824
|
+
line_frags.each {|l| l.items.each {|i| calc << i } }
|
|
825
|
+
y_min, y_max, = calc.result
|
|
826
|
+
DummyLine.new(y_min, y_max)
|
|
827
|
+
end
|
|
731
828
|
end
|
|
732
829
|
|
|
733
|
-
|
|
830
|
+
# Applies the necessary x- and y-offsets to the line fragments.
|
|
831
|
+
def apply_offsets(line_frags, width_spec, indent, last_line, combined_line, y_offset)
|
|
832
|
+
cumulated_width = 0
|
|
833
|
+
line_frags.each_with_index do |line, index|
|
|
834
|
+
line.x_offset = cumulated_width + indent
|
|
835
|
+
line.x_offset += width_spec[index * 2]
|
|
836
|
+
line.x_offset += horizontal_alignment_offset(line, width_spec[index * 2 + 1] - indent)
|
|
837
|
+
cumulated_width += width_spec[index * 2] + width_spec[index * 2 + 1]
|
|
838
|
+
if index == 0
|
|
839
|
+
line.y_offset = if y_offset
|
|
840
|
+
y_offset + combined_line.y_max -
|
|
841
|
+
(last_line ? last_line.y_min : line.y_max)
|
|
842
|
+
else
|
|
843
|
+
style.line_spacing.baseline_distance(last_line, combined_line)
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
indent = 0
|
|
847
|
+
end
|
|
848
|
+
end
|
|
734
849
|
|
|
735
850
|
# Returns the initial baseline offset from the top, based on the valign style option.
|
|
736
|
-
def initial_baseline_offset
|
|
851
|
+
def initial_baseline_offset(lines, height, actual_height)
|
|
737
852
|
case style.valign
|
|
738
853
|
when :top
|
|
739
|
-
|
|
854
|
+
lines.first.y_max
|
|
740
855
|
when :center
|
|
741
|
-
|
|
742
|
-
raise HexaPDF::Error, "Can't vertically align when using unlimited height"
|
|
743
|
-
end
|
|
744
|
-
(@height - @actual_height) / 2.0 + @lines.first.y_max
|
|
856
|
+
(height - actual_height) / 2.0 + lines.first.y_max
|
|
745
857
|
when :bottom
|
|
746
|
-
|
|
747
|
-
raise HexaPDF::Error, "Can't vertically align when using unlimited height"
|
|
748
|
-
end
|
|
749
|
-
(@height - @actual_height) + @lines.first.y_max
|
|
858
|
+
(height - actual_height) + lines.first.y_max
|
|
750
859
|
end
|
|
751
860
|
end
|
|
752
861
|
|