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
data/lib/hexapdf/layout/box.rb
CHANGED
|
@@ -39,23 +39,36 @@ module HexaPDF
|
|
|
39
39
|
#
|
|
40
40
|
# HexaPDF uses the following box model:
|
|
41
41
|
#
|
|
42
|
-
# * Each box can specify a
|
|
43
|
-
#
|
|
42
|
+
# * Each box can specify a width and height. Padding and border are inside, the margin outside
|
|
43
|
+
# of this rectangle.
|
|
44
44
|
#
|
|
45
|
-
# * The #
|
|
46
|
-
#
|
|
45
|
+
# * The #content_width and #content_height accessors can be used to get the width and height of
|
|
46
|
+
# the content box without padding and the border.
|
|
47
47
|
#
|
|
48
|
+
# * If width or height is set to zero, they are determined automatically during layouting.
|
|
48
49
|
class Box
|
|
49
50
|
|
|
50
|
-
#
|
|
51
|
+
# Creates a new Box object, using the provided block as drawing block (see ::new). Any
|
|
52
|
+
# additional keyword arguments are used for creating the box's Style object.
|
|
51
53
|
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
+
# If +content_box+ is +true+, the width and height are taken to mean the content width and
|
|
55
|
+
# height and the style's padding and border are removed from them appropriately.
|
|
56
|
+
def self.create(width: 0, height: 0, content_box: false, **style, &block)
|
|
57
|
+
style = Style.new(style)
|
|
58
|
+
if content_box
|
|
59
|
+
width += style.padding.left + style.padding.right +
|
|
60
|
+
style.border.width.left + style.border.width.right
|
|
61
|
+
height += style.padding.top + style.padding.bottom +
|
|
62
|
+
style.border.width.top + style.border.width.bottom
|
|
63
|
+
end
|
|
64
|
+
new(width: width, height: height, style: style, &block)
|
|
65
|
+
end
|
|
54
66
|
|
|
55
|
-
# The
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
# The width of the box, including padding and/or borders.
|
|
68
|
+
attr_reader :width
|
|
69
|
+
|
|
70
|
+
# The height of the box, including padding and/or borders.
|
|
71
|
+
attr_reader :height
|
|
59
72
|
|
|
60
73
|
# The style to be applied.
|
|
61
74
|
#
|
|
@@ -69,60 +82,62 @@ module HexaPDF
|
|
|
69
82
|
attr_reader :style
|
|
70
83
|
|
|
71
84
|
# :call-seq:
|
|
72
|
-
# Box.new(content_width: 0, content_height: 0, style: Style.new) {|canv, box| block} -> box
|
|
73
85
|
# Box.new(width: 0, height: 0, style: Style.new) {|canv, box| block} -> box
|
|
74
86
|
#
|
|
75
|
-
# Creates a new Box object with the given width and height
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# Alternative to specifying the content width/height, it is also possible to specify the box
|
|
79
|
-
# width/height. The content width is then immediately calculated using the border and padding
|
|
80
|
-
# information from the style and stored.
|
|
87
|
+
# Creates a new Box object with the given width and height that uses the provided block when
|
|
88
|
+
# it is asked to draw itself on a canvas (see #draw).
|
|
81
89
|
#
|
|
82
90
|
# Since the final location of the box is not known beforehand, the drawing operations inside
|
|
83
91
|
# the block should draw inside the rectangle (0, 0, content_width, content_height) - note that
|
|
84
92
|
# the width and height of the box may not be known beforehand.
|
|
85
|
-
def initialize(
|
|
86
|
-
|
|
93
|
+
def initialize(width: 0, height: 0, style: Style.new, &block)
|
|
94
|
+
@width = @initial_width = width
|
|
95
|
+
@height = @initial_height = height
|
|
87
96
|
@style = (style.kind_of?(Style) ? style : Style.new(style))
|
|
88
97
|
@draw_block = block
|
|
89
|
-
@
|
|
90
|
-
@content_width = [width - self.width, 0].max if width != 0 && @content_width == 0
|
|
91
|
-
@content_height = content_height
|
|
92
|
-
@content_height = [height - self.height, 0].max if height != 0 && @content_height == 0
|
|
98
|
+
@outline = nil
|
|
93
99
|
end
|
|
94
100
|
|
|
95
|
-
#
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
# The width of the content box, i.e. without padding and/or borders.
|
|
102
|
+
def content_width
|
|
103
|
+
[0, width - (@style.padding.left + @style.padding.right +
|
|
104
|
+
@style.border.width.left + @style.border.width.right)].max
|
|
99
105
|
end
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
def
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
# The height of the content box, i.e. without padding and/or borders.
|
|
108
|
+
def content_height
|
|
109
|
+
[0, height - (@style.padding.top + @style.padding.bottom +
|
|
110
|
+
@style.border.width.top + @style.border.width.bottom)].max
|
|
105
111
|
end
|
|
106
112
|
|
|
107
|
-
#
|
|
108
|
-
# box.draw(canvas, x, y)
|
|
113
|
+
# Fits the box into the Frame and returns +true+ if fitting was successful.
|
|
109
114
|
#
|
|
110
|
-
#
|
|
115
|
+
# The default implementation uses the whole available space for width and height if they were
|
|
116
|
+
# initially set to 0. Otherwise the specified dimensions are used.
|
|
117
|
+
def fit(available_width, available_height, frame)
|
|
118
|
+
@width = (@initial_width > 0 ? @initial_width : available_width)
|
|
119
|
+
@height = (@initial_height > 0 ? @initial_height : available_height)
|
|
120
|
+
@width <= available_width && @height <= available_height
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Draws the content of the box onto the canvas at the position (x, y).
|
|
124
|
+
#
|
|
125
|
+
# The coordinate system is translated so that the origin is at the bottom left corner of the
|
|
126
|
+
# **content box** during the drawing operations.
|
|
111
127
|
#
|
|
112
|
-
# The
|
|
113
|
-
#
|
|
128
|
+
# The block specified when creating the box is invoked with the canvas and the box as
|
|
129
|
+
# arguments. Subclasses can specify an on-demand drawing method by setting the +@draw_block+
|
|
130
|
+
# instance variable to +nil+ or a valid block. This is useful to avoid unnecessary set-up
|
|
131
|
+
# operations when the block does nothing.
|
|
114
132
|
def draw(canvas, x, y)
|
|
115
|
-
if style.background_color
|
|
133
|
+
if style.background_color? && style.background_color
|
|
116
134
|
canvas.save_graphics_state do
|
|
117
135
|
canvas.fill_color(style.background_color).rectangle(x, y, width, height).fill
|
|
118
136
|
end
|
|
119
137
|
end
|
|
120
138
|
|
|
121
|
-
style.underlays.draw(canvas, x, y, self)
|
|
122
|
-
|
|
123
|
-
unless style.border.none?
|
|
124
|
-
style.border.draw(canvas, x, y, width, height)
|
|
125
|
-
end
|
|
139
|
+
style.underlays.draw(canvas, x, y, self) if style.underlays?
|
|
140
|
+
style.border.draw(canvas, x, y, width, height) if style.border?
|
|
126
141
|
|
|
127
142
|
if @draw_block
|
|
128
143
|
canvas.translate(x + style.padding.left + style.border.width.left,
|
|
@@ -131,13 +146,16 @@ module HexaPDF
|
|
|
131
146
|
end
|
|
132
147
|
end
|
|
133
148
|
|
|
134
|
-
style.overlays.draw(canvas, x, y, self)
|
|
149
|
+
style.overlays.draw(canvas, x, y, self) if style.overlays?
|
|
135
150
|
end
|
|
136
151
|
|
|
137
152
|
# Returns +true+ if no drawing operations are performed.
|
|
138
153
|
def empty?
|
|
139
|
-
!(@draw_block ||
|
|
140
|
-
|
|
154
|
+
!(@draw_block ||
|
|
155
|
+
(style.background_color? && style.background_color) ||
|
|
156
|
+
(style.underlays? && !style.underlays.none?) ||
|
|
157
|
+
(style.border? && !style.border.none?) ||
|
|
158
|
+
(style.overlays? && !style.overlays.none?))
|
|
141
159
|
end
|
|
142
160
|
|
|
143
161
|
end
|
|
@@ -0,0 +1,348 @@
|
|
|
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-2017 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
|
+
|
|
34
|
+
require 'hexapdf/layout/width_from_polygon'
|
|
35
|
+
require 'geom2d/polygon'
|
|
36
|
+
|
|
37
|
+
module HexaPDF
|
|
38
|
+
module Layout
|
|
39
|
+
|
|
40
|
+
# A Frame describes the available space for placing boxes and provides additional methods for
|
|
41
|
+
# calculating the needed information for the actual placement.
|
|
42
|
+
#
|
|
43
|
+
# == Usage
|
|
44
|
+
#
|
|
45
|
+
# After a Frame object is initialized, the #draw method can be used to draw a box onto frame. If
|
|
46
|
+
# drawing is successful, the next box can be drawn. Otherwise, #find_next_region should be
|
|
47
|
+
# called to determine the next region for placing the box. If the call returns +true+, a region
|
|
48
|
+
# was found and #draw can be tried again. Once #find_next_region returns +false+ the frame has
|
|
49
|
+
# no more space for placing boxes.
|
|
50
|
+
#
|
|
51
|
+
# == Frame Shape and Contour Line
|
|
52
|
+
#
|
|
53
|
+
# A frame's shape is used to determine the available space for laying out boxes and its contour
|
|
54
|
+
# line is used whenever text should be flown around objects. They are normally the same but can
|
|
55
|
+
# differ if a box with an arbitrary contour line is drawn onto the frame.
|
|
56
|
+
#
|
|
57
|
+
# Initially, a frame has a rectangular shape. However, once boxes are added and the frame's
|
|
58
|
+
# available area gets reduced, a frame may have a polygon set consisting of arbitrary
|
|
59
|
+
# rectilinear polygons as shape.
|
|
60
|
+
#
|
|
61
|
+
# In contrast to the frame's shape its contour line may be a completely arbitrary polygon set.
|
|
62
|
+
class Frame
|
|
63
|
+
|
|
64
|
+
include Geom2D::Utils
|
|
65
|
+
|
|
66
|
+
# The x-coordinate of the bottom-left corner.
|
|
67
|
+
attr_reader :left
|
|
68
|
+
|
|
69
|
+
# The y-coordinate of the bottom-left corner.
|
|
70
|
+
attr_reader :bottom
|
|
71
|
+
|
|
72
|
+
# The width of the frame.
|
|
73
|
+
attr_reader :width
|
|
74
|
+
|
|
75
|
+
# The height of the frame.
|
|
76
|
+
attr_reader :height
|
|
77
|
+
|
|
78
|
+
# The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
|
|
79
|
+
attr_reader :shape
|
|
80
|
+
|
|
81
|
+
# The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
|
|
82
|
+
attr_reader :contour_line
|
|
83
|
+
|
|
84
|
+
# The x-coordinate where the next box will be placed.
|
|
85
|
+
#
|
|
86
|
+
# Note: Since the algorithm for #draw takes the margin of a box into account, the actual
|
|
87
|
+
# x-coordinate (and y-coordinate, available width and available height) might be different.
|
|
88
|
+
attr_reader :x
|
|
89
|
+
|
|
90
|
+
# The y-coordinate where the next box will be placed.
|
|
91
|
+
#
|
|
92
|
+
# Also see the note in the #x documentation for further information.
|
|
93
|
+
attr_reader :y
|
|
94
|
+
|
|
95
|
+
# The available width for placing a box.
|
|
96
|
+
#
|
|
97
|
+
# Also see the note in the #x documentation for further information.
|
|
98
|
+
attr_reader :available_width
|
|
99
|
+
|
|
100
|
+
# The available height for placing a box.
|
|
101
|
+
#
|
|
102
|
+
# Also see the note in the #x documentation for further information.
|
|
103
|
+
attr_reader :available_height
|
|
104
|
+
|
|
105
|
+
# Creates a new Frame object for the given rectangular area.
|
|
106
|
+
#
|
|
107
|
+
# If the contour line of the frame is not specified, then the rectangular area is used as
|
|
108
|
+
# contour line.
|
|
109
|
+
def initialize(left, bottom, width, height, contour_line: nil)
|
|
110
|
+
@left = left
|
|
111
|
+
@bottom = bottom
|
|
112
|
+
@width = width
|
|
113
|
+
@height = height
|
|
114
|
+
@contour_line = contour_line || Geom2D::PolygonSet.new(
|
|
115
|
+
[create_rectangle(left, bottom, left + width, bottom + height)]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@shape = Geom2D::PolygonSet.new(
|
|
119
|
+
[create_rectangle(left, bottom, left + width, bottom + height)]
|
|
120
|
+
)
|
|
121
|
+
@x = left
|
|
122
|
+
@y = bottom + height
|
|
123
|
+
@available_width = width
|
|
124
|
+
@available_height = height
|
|
125
|
+
@region_selection = :max_height
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Draws the given box onto the canvas at the frame's current position. Returns +true+ if
|
|
129
|
+
# drawing was possible, +false+ otherwise.
|
|
130
|
+
#
|
|
131
|
+
# When positioning the box, the style properties "position", "position_hint" and "margin" are
|
|
132
|
+
# taken into account. Note that the margin is ignored if a box's side coincides with the
|
|
133
|
+
# frame's original boundary.
|
|
134
|
+
#
|
|
135
|
+
# After a box is successfully drawn, the frame's shape and contour line are adjusted to remove
|
|
136
|
+
# the occupied area.
|
|
137
|
+
def draw(canvas, box)
|
|
138
|
+
aw = available_width
|
|
139
|
+
ah = available_height
|
|
140
|
+
used_margin_left = used_margin_right = used_margin_top = 0
|
|
141
|
+
|
|
142
|
+
if box.style.position != :absolute
|
|
143
|
+
if box.style.margin?
|
|
144
|
+
margin = box.style.margin
|
|
145
|
+
ah -= margin.bottom unless float_equal(@y - ah, @bottom)
|
|
146
|
+
ah -= used_margin_top = margin.top unless float_equal(@y, @bottom + @height)
|
|
147
|
+
aw -= used_margin_right = margin.right unless float_equal(@x + aw, @left + @width)
|
|
148
|
+
aw -= used_margin_left = margin.left unless float_equal(@x, @left)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
return false unless box.fit(aw, ah, self)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
width = box.width
|
|
155
|
+
height = box.height
|
|
156
|
+
|
|
157
|
+
case box.style.position
|
|
158
|
+
when :absolute
|
|
159
|
+
x, y = box.style.position_hint
|
|
160
|
+
x += left
|
|
161
|
+
y += bottom
|
|
162
|
+
rectangle = if box.style.margin?
|
|
163
|
+
margin = box.style.margin
|
|
164
|
+
create_rectangle(x - margin.left, y - margin.bottom,
|
|
165
|
+
x + width + margin.right, y + height + margin.top)
|
|
166
|
+
else
|
|
167
|
+
create_rectangle(x, y, x + width, y + height)
|
|
168
|
+
end
|
|
169
|
+
when :float
|
|
170
|
+
x = @x + used_margin_left
|
|
171
|
+
x += aw - width if box.style.position_hint == :right
|
|
172
|
+
y = @y - height - used_margin_top
|
|
173
|
+
# We can use the real margins from the box because they either have the desired effect or
|
|
174
|
+
# just extend the rectangle outside the frame.
|
|
175
|
+
rectangle = create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0),
|
|
176
|
+
x + width + (margin&.right || 0), @y)
|
|
177
|
+
when :flow
|
|
178
|
+
x = 0
|
|
179
|
+
y = @y - height
|
|
180
|
+
rectangle = create_rectangle(left, y, left + self.width, @y)
|
|
181
|
+
else
|
|
182
|
+
x = case box.style.position_hint
|
|
183
|
+
when :right
|
|
184
|
+
@x + used_margin_left + aw - width
|
|
185
|
+
when :center
|
|
186
|
+
max_margin = [used_margin_left, used_margin_right].max
|
|
187
|
+
# If we have enough space left for equal margins, we center perfectly
|
|
188
|
+
if available_width - width >= 2 * max_margin
|
|
189
|
+
@x + (available_width - width) / 2.0
|
|
190
|
+
else
|
|
191
|
+
@x + used_margin_left + (aw - width) / 2.0
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
@x + used_margin_left
|
|
195
|
+
end
|
|
196
|
+
y = @y - height - used_margin_top
|
|
197
|
+
rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
box.draw(canvas, x, y)
|
|
201
|
+
remove_area(rectangle)
|
|
202
|
+
|
|
203
|
+
true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Finds the next region for placing boxes. Returns +false+ if no useful region was found.
|
|
207
|
+
#
|
|
208
|
+
# This method should be called after drawing a box using #draw was not successful. It finds a
|
|
209
|
+
# different region on each invocation. So if a box doesn't fit into the first region, this
|
|
210
|
+
# method should be called again to find another region and to try again.
|
|
211
|
+
#
|
|
212
|
+
# The first tried region starts at the top-most, left-most vertex of the polygon and uses the
|
|
213
|
+
# maximum width. The next tried region uses the maximum height. If both don't work, part of
|
|
214
|
+
# the frame's shape is removed to try again.
|
|
215
|
+
def find_next_region
|
|
216
|
+
case @region_selection
|
|
217
|
+
when :max_width
|
|
218
|
+
find_max_width_region
|
|
219
|
+
@region_selection = :max_height
|
|
220
|
+
when :max_height
|
|
221
|
+
x, y, aw, ah = @x, @y, @available_width, @available_height
|
|
222
|
+
find_max_height_region
|
|
223
|
+
if @x == x && @y == y && @available_width == aw && @available_height == ah
|
|
224
|
+
trim_shape
|
|
225
|
+
else
|
|
226
|
+
@region_selection = :trim_shape
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
trim_shape
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
available_width != 0
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Removes the given *rectilinear* polygon from both the frame's shape and the frame's contour
|
|
236
|
+
# line.
|
|
237
|
+
def remove_area(polygon)
|
|
238
|
+
@shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
|
|
239
|
+
@contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon,
|
|
240
|
+
:difference)
|
|
241
|
+
@region_selection = :max_width
|
|
242
|
+
find_next_region
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns a width specification for the frame's contour line that can be used, for example,
|
|
246
|
+
# with TextLayouter.
|
|
247
|
+
#
|
|
248
|
+
# Since not all text may start at the top of the frame, the offset argument can be used to
|
|
249
|
+
# specify a vertical offset from the top of the frame where layouting should start.
|
|
250
|
+
#
|
|
251
|
+
# To be compatible with TextLayouter, the top left corner of the bounding box of the frame's
|
|
252
|
+
# contour line is the origin of the coordinate system for the width specification, with
|
|
253
|
+
# positive x-values to the right and positive y-values downwards.
|
|
254
|
+
#
|
|
255
|
+
# Depending on the complexity of the frame, the result may be any of the allowed width
|
|
256
|
+
# specifications of TextLayouter#fit.
|
|
257
|
+
def width_specification(offset = 0)
|
|
258
|
+
WidthFromPolygon.new(@contour_line, offset)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
# Creates a Geom2D::Polygon object representing the rectangle with the bottom left corner
|
|
264
|
+
# (blx, bly) and the top right corner (trx, try).
|
|
265
|
+
def create_rectangle(blx, bly, trx, try)
|
|
266
|
+
Geom2D::Polygon(Geom2D::Point(blx, bly), Geom2D::Point(trx, bly),
|
|
267
|
+
Geom2D::Point(trx, try), Geom2D::Point(blx, try))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Finds the region with the maximum width.
|
|
271
|
+
def find_max_width_region
|
|
272
|
+
return unless (segments = find_starting_point)
|
|
273
|
+
|
|
274
|
+
x_right = @x + @available_width
|
|
275
|
+
|
|
276
|
+
# Available height can be determined by finding the segment with the highest y-coordinate
|
|
277
|
+
# which lies (maybe only partly) between the vertical lines @x and x_right.
|
|
278
|
+
segments.select! {|s| s.max.x > @x && s.min.x < x_right }
|
|
279
|
+
@available_height = @y - segments.last.start_point.y
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Finds the region with the maximum height.
|
|
283
|
+
def find_max_height_region
|
|
284
|
+
return unless (segments = find_starting_point)
|
|
285
|
+
|
|
286
|
+
# Find segment with maximum y-coordinate directly below (@x,@y), this determines the
|
|
287
|
+
# available height
|
|
288
|
+
index = segments.rindex {|s| s.min.x <= @x && @x < s.max.x }
|
|
289
|
+
y1 = segments[index].start_point.y
|
|
290
|
+
@available_height = @y - y1
|
|
291
|
+
|
|
292
|
+
# Find segment with minium min.x coordinate whose y-coordinate is between y1 and @y and
|
|
293
|
+
# min.x > @x, for getting the available width
|
|
294
|
+
segments.select! {|s| s.min.x > @x && y1 <= s.start_point.y && s.start_point.y <= @y }
|
|
295
|
+
segment = segments.min_by {|s| s.min.x }
|
|
296
|
+
@available_width = segment.min.x - @x if segment
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Trims the frame's shape so that the next starting point is different.
|
|
300
|
+
def trim_shape
|
|
301
|
+
return unless (segments = find_starting_point)
|
|
302
|
+
|
|
303
|
+
# Just use the second top-most segment
|
|
304
|
+
# TODO: not the optimal solution!
|
|
305
|
+
index = segments.rindex {|s| s.start_point.y < @y }
|
|
306
|
+
y = segments[index].start_point.y
|
|
307
|
+
remove_area(Geom2D::Polygon([left, y], [left + width, y],
|
|
308
|
+
[left + width, @y], [left, @y]))
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Finds and sets the top-left point for the next region. This is always the top-most,
|
|
312
|
+
# left-most vertex of the frame's shape.
|
|
313
|
+
#
|
|
314
|
+
# If successful, additionally sets the available width to the length of the segment containing
|
|
315
|
+
# the point and returns the sorted horizontal segments except the top-most one.
|
|
316
|
+
#
|
|
317
|
+
# Otherwise, sets all region specific values to zero and returns +nil+.
|
|
318
|
+
def find_starting_point
|
|
319
|
+
segments = sorted_horizontal_segments
|
|
320
|
+
if segments.empty?
|
|
321
|
+
@x = @y = @available_width = @available_height = 0
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
top_segment = segments.pop
|
|
326
|
+
@x = top_segment.min.x
|
|
327
|
+
@y = top_segment.start_point.y
|
|
328
|
+
@available_width = top_segment.length
|
|
329
|
+
|
|
330
|
+
segments
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Returns the horizontal segments of the frame's shape, sorted by maximum y-, then minimum
|
|
334
|
+
# x-coordinate.
|
|
335
|
+
def sorted_horizontal_segments
|
|
336
|
+
@shape.each_segment.select(&:horizontal?).sort! do |a, b|
|
|
337
|
+
if a.start_point.y == b.start_point.y
|
|
338
|
+
b.start_point.x <=> a.start_point.x
|
|
339
|
+
else
|
|
340
|
+
a.start_point.y <=> b.start_point.y
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
end
|
|
348
|
+
end
|