Class: HexaPDF::Layout::Frame
- Inherits:
-
Object
- Object
- HexaPDF::Layout::Frame
- Includes:
- Geom2D::Utils
- Defined in:
- lib/hexapdf/layout/frame.rb
Overview
A Frame describes the available space for placing boxes and provides additional methods for calculating the needed information for the actual placement.
Usage
After a Frame object is initialized, it is ready for drawing boxes on it.
The explicit way of drawing a box follows these steps:
-
Call #fit with the box to see if the box can fit into the currently selected region of available space. If fitting is successful, the box can be drawn using #draw.
The method #fit is also called for absolutely positioned boxes but since these boxes are not subject to the normal constraints, the available space used is the width and height inside the frame to the right and top of the bottom-left corner of the box.
-
If the box didn’t fit, call #find_next_region to determine the next region for placing the box. If a new region was found, start over with #fit. Otherwise the frame has no more space for placing boxes.
-
Alternatively to calling #find_next_region it is also possible to call #split. This method tries to split the box into two so that the first part fits into the current region. If splitting is successful, the first box can be drawn (Make sure that the second box is handled correctly). Otherwise, start over with #find_next_region.
For applications where splitting is not necessary, an easier way is to just use #draw and #find_next_region together, as #draw calls #fit if the box was not fit into the current region.
Used Box Properties
The style properties “position”, “position_hint” and “margin” are taken into account when fitting, splitting or drawing a box. Note that the margin is ignored if a box’s side coincides with the frame’s original boundary.
Frame Shape and Contour Line
A frame’s shape is used to determine the available space for laying out boxes and its contour line is used whenever text should be flown around objects. They are normally the same but can differ if a box with an arbitrary contour line is drawn onto the frame.
Initially, a frame has a rectangular shape. However, once boxes are added and the frame’s available area gets reduced, a frame may have a polygon set consisting of arbitrary rectilinear polygons as shape.
In contrast to the frame’s shape its contour line may be a completely arbitrary polygon set.
Defined Under Namespace
Classes: FitData
Instance Attribute Summary collapse
-
#available_height ⇒ Object
readonly
The available height for placing a box.
-
#available_width ⇒ Object
readonly
The available width for placing a box.
-
#bottom ⇒ Object
readonly
The y-coordinate of the bottom-left corner.
-
#height ⇒ Object
readonly
The height of the frame.
-
#left ⇒ Object
readonly
The x-coordinate of the bottom-left corner.
-
#shape ⇒ Object
readonly
The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
-
#width ⇒ Object
readonly
The width of the frame.
-
#x ⇒ Object
readonly
The x-coordinate where the next box will be placed.
-
#y ⇒ Object
readonly
The y-coordinate where the next box will be placed.
Instance Method Summary collapse
-
#contour_line ⇒ Object
The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
-
#draw(canvas, box) ⇒ Object
Draws the given (fitted) box onto the canvas at the frame’s current position.
-
#find_next_region ⇒ Object
Finds the next region for placing boxes.
-
#fit(box) ⇒ Object
Fits the given box into the current region of available space.
-
#full? ⇒ Boolean
Returns
true
if the frame has no more space left. -
#initialize(left, bottom, width, height, contour_line: nil) ⇒ Frame
constructor
Creates a new Frame object for the given rectangular area.
-
#remove_area(polygon) ⇒ Object
Removes the given rectilinear polygon from both the frame’s shape and the frame’s contour line.
-
#split(box) ⇒ Object
Tries to split the (fitted) box into two parts, where the first part needs to fit into the available space, and returns both parts.
-
#width_specification(offset = 0) ⇒ Object
Returns a width specification for the frame’s contour line that can be used, for example, with TextLayouter.
Constructor Details
#initialize(left, bottom, width, height, contour_line: nil) ⇒ Frame
Creates a new Frame object for the given rectangular area.
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/hexapdf/layout/frame.rb', line 166 def initialize(left, bottom, width, height, contour_line: nil) @left = left @bottom = bottom @width = width @height = height @contour_line = contour_line @shape = Geom2D::PolygonSet.new( [create_rectangle(left, bottom, left + width, bottom + height)] ) @x = left @y = bottom + height @available_width = width @available_height = height @region_selection = :max_height @fit_data = FitData.new end |
Instance Attribute Details
#available_height ⇒ Object (readonly)
The available height for placing a box.
Also see the note in the #x documentation for further information.
163 164 165 |
# File 'lib/hexapdf/layout/frame.rb', line 163 def available_height @available_height end |
#available_width ⇒ Object (readonly)
The available width for placing a box.
Also see the note in the #x documentation for further information.
158 159 160 |
# File 'lib/hexapdf/layout/frame.rb', line 158 def available_width @available_width end |
#bottom ⇒ Object (readonly)
The y-coordinate of the bottom-left corner.
133 134 135 |
# File 'lib/hexapdf/layout/frame.rb', line 133 def bottom @bottom end |
#height ⇒ Object (readonly)
The height of the frame.
139 140 141 |
# File 'lib/hexapdf/layout/frame.rb', line 139 def height @height end |
#left ⇒ Object (readonly)
The x-coordinate of the bottom-left corner.
130 131 132 |
# File 'lib/hexapdf/layout/frame.rb', line 130 def left @left end |
#shape ⇒ Object (readonly)
The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
142 143 144 |
# File 'lib/hexapdf/layout/frame.rb', line 142 def shape @shape end |
#width ⇒ Object (readonly)
The width of the frame.
136 137 138 |
# File 'lib/hexapdf/layout/frame.rb', line 136 def width @width end |
#x ⇒ Object (readonly)
The x-coordinate where the next box will be placed.
Note: Since the algorithm for drawing takes the margin of a box into account, the actual x-coordinate (and y-coordinate, available width and available height) might be different.
148 149 150 |
# File 'lib/hexapdf/layout/frame.rb', line 148 def x @x end |
#y ⇒ Object (readonly)
The y-coordinate where the next box will be placed.
Also see the note in the #x documentation for further information.
153 154 155 |
# File 'lib/hexapdf/layout/frame.rb', line 153 def y @y end |
Instance Method Details
#contour_line ⇒ Object
The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
341 342 343 |
# File 'lib/hexapdf/layout/frame.rb', line 341 def contour_line @contour_line || @shape end |
#draw(canvas, box) ⇒ Object
Draws the given (fitted) box onto the canvas at the frame’s current position. Returns true
if drawing was possible, false
otherwise.
If the given box is not the last fitted box, #fit is called before drawing the box.
After a box is successfully drawn, the frame’s shape and contour line are adjusted to remove the occupied area.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/hexapdf/layout/frame.rb', line 230 def draw(canvas, box) unless box == @fit_data.box fit(box) || return end width = box.width height = box.height margin = box.style.margin if box.style.margin? if height == 0 @fit_data.reset return true end case box.style.position when :absolute x, y = box.style.position_hint x += left y += bottom rectangle = if box.style.margin? create_rectangle(x - margin.left, y - margin.bottom, x + width + margin.right, y + height + margin.top) else create_rectangle(x, y, x + width, y + height) end when :float x = @x + @fit_data.margin_left x += @fit_data.available_width - width if box.style.position_hint == :right y = @y - height - @fit_data.margin_top # We use the real margins from the box because they either have the desired effect or just # extend the rectangle outside the frame. rectangle = create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0), x + width + (margin&.right || 0), @y) when :flow x = 0 y = @y - height rectangle = create_rectangle(left, y, left + self.width, @y) else x = case box.style.position_hint when :right @x + @fit_data.margin_left + @fit_data.available_width - width when :center max_margin = [@fit_data.margin_left, @fit_data.margin_right].max # If we have enough space left for equal margins, we center perfectly if available_width - width >= 2 * max_margin @x + (available_width - width) / 2.0 else @x + @fit_data.margin_left + (@fit_data.available_width - width) / 2.0 end else @x + @fit_data.margin_left end y = @y - height - @fit_data.margin_top rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y) end box.draw(canvas, x, y) remove_area(rectangle) @fit_data.reset true end |
#find_next_region ⇒ Object
Finds the next region for placing boxes. Returns false
if no useful region was found.
This method should be called after drawing a box using #draw was not successful. It finds a different region on each invocation. So if a box doesn’t fit into the first region, this method should be called again to find another region and to try again.
The first tried region starts at the top-most, left-most vertex of the polygon and uses the maximum width. The next tried region uses the maximum height. If both don’t work, part of the frame’s shape is removed to try again.
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/hexapdf/layout/frame.rb', line 302 def find_next_region case @region_selection when :max_width find_max_width_region @region_selection = :max_height when :max_height x, y, aw, ah = @x, @y, @available_width, @available_height find_max_height_region if @x == x && @y == y && @available_width == aw && @available_height == ah trim_shape else @region_selection = :trim_shape end else trim_shape end @fit_data.reset available_width != 0 end |
#fit(box) ⇒ Object
Fits the given box into the current region of available space.
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/hexapdf/layout/frame.rb', line 184 def fit(box) aw = available_width ah = available_height @fit_data.reset(box, aw, ah) if full? false elsif box.style.position == :absolute x, y = box.style.position_hint box.fit(width - x, height - y, self) true else if box.style.margin? margin = box.style.margin ah -= margin.bottom unless float_equal(@y - ah, @bottom) ah -= @fit_data.margin_top = margin.top unless float_equal(@y, @bottom + @height) aw -= @fit_data.margin_right = margin.right unless float_equal(@x + aw, @left + @width) aw -= @fit_data.margin_left = margin.left unless float_equal(@x, @left) @fit_data.available_width = aw @fit_data.available_height = ah end box.fit(aw, ah, self) end end |
#full? ⇒ Boolean
Returns true
if the frame has no more space left.
336 337 338 |
# File 'lib/hexapdf/layout/frame.rb', line 336 def full? available_width == 0 end |
#remove_area(polygon) ⇒ Object
Removes the given rectilinear polygon from both the frame’s shape and the frame’s contour line.
325 326 327 328 329 330 331 332 333 |
# File 'lib/hexapdf/layout/frame.rb', line 325 def remove_area(polygon) @shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference) if @contour_line @contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon, :difference) end @region_selection = :max_width find_next_region end |
#split(box) ⇒ Object
Tries to split the (fitted) box into two parts, where the first part needs to fit into the available space, and returns both parts.
If the given box is not the last fitted box, #fit is called before splitting the box.
See Box#split for further details.
216 217 218 219 220 221 |
# File 'lib/hexapdf/layout/frame.rb', line 216 def split(box) fit(box) unless box == @fit_data.box boxes = box.split(@fit_data.available_width, @fit_data.available_height, self) @fit_data.reset unless boxes[0] == @fit_data.box boxes end |
#width_specification(offset = 0) ⇒ Object
Returns a width specification for the frame’s contour line that can be used, for example, with TextLayouter.
Since not all text may start at the top of the frame, the offset argument can be used to specify a vertical offset from the top of the frame where layouting should start.
To be compatible with TextLayouter, the top left corner of the bounding box of the frame’s contour line is the origin of the coordinate system for the width specification, with positive x-values to the right and positive y-values downwards.
Depending on the complexity of the frame, the result may be any of the allowed width specifications of TextLayouter#fit.
357 358 359 |
# File 'lib/hexapdf/layout/frame.rb', line 357 def width_specification(offset = 0) WidthFromPolygon.new(contour_line, offset) end |