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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/LICENSE +3 -0
  5. data/README.md +2 -1
  6. data/Rakefile +3 -1
  7. data/VERSION +1 -1
  8. data/examples/{hello_world.rb → 001-hello_world.rb} +0 -0
  9. data/examples/{graphics.rb → 002-graphics.rb} +1 -1
  10. data/examples/{arc.rb → 003-arcs.rb} +2 -2
  11. data/examples/{optimizing.rb → 004-optimizing.rb} +0 -0
  12. data/examples/{merging.rb → 005-merging.rb} +0 -0
  13. data/examples/{standard_pdf_fonts.rb → 006-standard_pdf_fonts.rb} +0 -0
  14. data/examples/{truetype.rb → 007-truetype.rb} +0 -0
  15. data/examples/{show_char_bboxes.rb → 008-show_char_bboxes.rb} +0 -0
  16. data/examples/{text_layouter_alignment.rb → 009-text_layouter_alignment.rb} +3 -3
  17. data/examples/{text_layouter_inline_boxes.rb → 010-text_layouter_inline_boxes.rb} +7 -9
  18. data/examples/{text_layouter_line_wrapping.rb → 011-text_layouter_line_wrapping.rb} +6 -5
  19. data/examples/{text_layouter_styling.rb → 012-text_layouter_styling.rb} +6 -8
  20. data/examples/013-text_layouter_shapes.rb +176 -0
  21. data/examples/014-text_in_polygon.rb +60 -0
  22. data/examples/{boxes.rb → 015-boxes.rb} +29 -21
  23. data/examples/016-frame_automatic_box_placement.rb +90 -0
  24. data/examples/017-frame_text_flow.rb +60 -0
  25. data/lib/hexapdf/cli/command.rb +4 -3
  26. data/lib/hexapdf/cli/files.rb +1 -1
  27. data/lib/hexapdf/cli/inspect.rb +0 -1
  28. data/lib/hexapdf/cli/merge.rb +1 -1
  29. data/lib/hexapdf/cli/modify.rb +1 -1
  30. data/lib/hexapdf/configuration.rb +2 -0
  31. data/lib/hexapdf/content/canvas.rb +3 -3
  32. data/lib/hexapdf/content/graphic_object.rb +1 -0
  33. data/lib/hexapdf/content/graphic_object/geom2d.rb +132 -0
  34. data/lib/hexapdf/dictionary.rb +7 -1
  35. data/lib/hexapdf/dictionary_fields.rb +35 -83
  36. data/lib/hexapdf/document.rb +9 -5
  37. data/lib/hexapdf/document/fonts.rb +1 -1
  38. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  39. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  40. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  41. data/lib/hexapdf/font/cmap/writer.rb +2 -2
  42. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  43. data/lib/hexapdf/font/true_type/table.rb +1 -1
  44. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  45. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  46. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  47. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  48. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  49. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  50. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  51. data/lib/hexapdf/image_loader/png.rb +2 -2
  52. data/lib/hexapdf/layout.rb +3 -0
  53. data/lib/hexapdf/layout/box.rb +64 -46
  54. data/lib/hexapdf/layout/frame.rb +348 -0
  55. data/lib/hexapdf/layout/inline_box.rb +2 -2
  56. data/lib/hexapdf/layout/line.rb +3 -3
  57. data/lib/hexapdf/layout/style.rb +81 -14
  58. data/lib/hexapdf/layout/text_box.rb +84 -0
  59. data/lib/hexapdf/layout/text_fragment.rb +8 -8
  60. data/lib/hexapdf/layout/text_layouter.rb +278 -169
  61. data/lib/hexapdf/layout/width_from_polygon.rb +246 -0
  62. data/lib/hexapdf/rectangle.rb +9 -9
  63. data/lib/hexapdf/stream.rb +2 -2
  64. data/lib/hexapdf/type.rb +1 -0
  65. data/lib/hexapdf/type/action.rb +1 -1
  66. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  67. data/lib/hexapdf/type/catalog.rb +1 -1
  68. data/lib/hexapdf/type/cid_font.rb +2 -1
  69. data/lib/hexapdf/type/font.rb +0 -1
  70. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  71. data/lib/hexapdf/type/font_simple.rb +3 -3
  72. data/lib/hexapdf/type/font_true_type.rb +8 -0
  73. data/lib/hexapdf/type/font_type0.rb +2 -1
  74. data/lib/hexapdf/type/font_type1.rb +7 -1
  75. data/lib/hexapdf/type/font_type3.rb +61 -0
  76. data/lib/hexapdf/type/graphics_state_parameter.rb +8 -8
  77. data/lib/hexapdf/type/image.rb +10 -0
  78. data/lib/hexapdf/type/page.rb +83 -10
  79. data/lib/hexapdf/version.rb +1 -1
  80. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  81. data/test/hexapdf/content/graphic_object/test_geom2d.rb +79 -0
  82. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  83. data/test/hexapdf/font/test_true_type_wrapper.rb +1 -1
  84. data/test/hexapdf/font/test_type1_wrapper.rb +1 -1
  85. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  86. data/test/hexapdf/font/true_type/table/test_directory.rb +1 -1
  87. data/test/hexapdf/font/true_type/table/test_head.rb +7 -3
  88. data/test/hexapdf/layout/test_box.rb +57 -15
  89. data/test/hexapdf/layout/test_frame.rb +313 -0
  90. data/test/hexapdf/layout/test_inline_box.rb +1 -1
  91. data/test/hexapdf/layout/test_style.rb +74 -0
  92. data/test/hexapdf/layout/test_text_box.rb +77 -0
  93. data/test/hexapdf/layout/test_text_layouter.rb +220 -239
  94. data/test/hexapdf/layout/test_width_from_polygon.rb +108 -0
  95. data/test/hexapdf/test_dictionary_fields.rb +22 -26
  96. data/test/hexapdf/test_document.rb +3 -3
  97. data/test/hexapdf/test_reference.rb +1 -0
  98. data/test/hexapdf/test_writer.rb +2 -2
  99. data/test/hexapdf/type/test_font_true_type.rb +25 -0
  100. data/test/hexapdf/type/test_font_type1.rb +6 -0
  101. data/test/hexapdf/type/test_font_type3.rb +26 -0
  102. data/test/hexapdf/type/test_image.rb +10 -0
  103. data/test/hexapdf/type/test_page.rb +114 -0
  104. data/test/test_helper.rb +1 -1
  105. metadata +65 -17
  106. data/examples/text_layouter_shapes.rb +0 -170
@@ -39,23 +39,36 @@ module HexaPDF
39
39
  #
40
40
  # HexaPDF uses the following box model:
41
41
  #
42
- # * Each box can specify a content width and content height. Padding, border and margin are
43
- # *outside* of this content rectangle.
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 #width and #height accessors can be used to get the width and height of the box
46
- # including padding and the border.
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
- # The width of the content box, i.e. without padding or borders.
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
- # The value 0 means that the width is dynamically determined.
53
- attr_reader :content_width
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 height of the content box, i.e. without padding or borders.
56
- #
57
- # The value 0 means that the height is dynamically determined.
58
- attr_reader :content_height
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 for its content that uses the
76
- # provided block when it is asked to draw itself on a canvas (see #draw).
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(content_width: 0, content_height: 0, width: 0, height: 0,
86
- style: Style.new, &block)
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
- @content_width = content_width
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
- # Returns the width of the box, including padding and border widths.
96
- def width
97
- @content_width + @style.padding.left + @style.padding.right +
98
- @style.border.width.left + @style.border.width.right
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
- # Returns the height of the box, including padding and border widths.
102
- def height
103
- @content_height + @style.padding.top + @style.padding.bottom +
104
- @style.border.width.top + @style.border.width.bottom
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
- # :call-seq:
108
- # box.draw(canvas, x, y)
113
+ # Fits the box into the Frame and returns +true+ if fitting was successful.
109
114
  #
110
- # Draws the contents of the box onto the canvas at the position (x, y).
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 coordinate system is translated so that the origin is at the lower left corner of the
113
- # contents box during the drawing operations.
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 || style.background_color || !style.underlays.none? ||
140
- !style.border.none? || !style.overlays.none?)
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