hexapdf 0.7.0 → 0.8.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 +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
|
|