Class: HexaPDF::Layout::Frame
- Inherits:
-
Object
- Object
- HexaPDF::Layout::Frame
- Includes:
- 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 fitting boxes in it and drawing them.
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 provided available width and height are 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.
Used Box Properties
The style properties ‘position’, ‘align’, ‘valign’, ‘margin’ and ‘mask_mode’ 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
A frame’s shape is used to determine the available space for laying out boxes.
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.
It is also possible to provide a different initial shape on initialization.
Constant Summary
Constants included from Utils
Instance Attribute Summary collapse
-
#available_height ⇒ Object
readonly
The available height of the current region for placing a box.
-
#available_width ⇒ Object
readonly
The available width of the current region for placing a box.
-
#bottom ⇒ Object
readonly
The y-coordinate of the bottom-left corner.
-
#context ⇒ Object
readonly
The context object (a HexaPDF::Type::Page or HexaPDF::Type::Form) for which this frame should be used.
-
#height ⇒ Object
readonly
The height of the frame.
-
#left ⇒ Object
readonly
The x-coordinate of the bottom-left corner.
-
#parent_boxes ⇒ Object
readonly
An array of box objects representing the parent boxes.
-
#shape ⇒ Object
readonly
The shape of the frame, either a Geom2D::Rectangle in the simple case or a Geom2D::PolygonSet consisting of rectilinear polygons in the more complex case.
-
#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
-
#child_frame(*init_args, shape: nil, box: nil) ⇒ Object
Creates a new Frame object based on this one.
-
#document ⇒ Object
Returns the HexaPDF::Document instance (through #context) that is associated with this Frame object or
nil
if no context object has been set. -
#draw(canvas, fit_result) ⇒ Object
Draws the box of the given Box::FitResult onto the canvas at the fitted 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 and returns the associated Box::FitResult object.
-
#full? ⇒ Boolean
Returns
true
if the frame has no more space left. -
#initialize(left, bottom, width, height, shape: nil, context: nil, parent_boxes: []) ⇒ Frame
constructor
Creates a new Frame object for the given rectangular area.
-
#remove_area(polygon) ⇒ Object
Removes the given rectilinear polygon from the frame’s shape.
-
#split(fit_result) ⇒ Object
Tries to split the box of the given Box::FitResult into two parts and returns both parts.
-
#width_specification(offset = 0) ⇒ Object
Returns a width specification for the frame’s shape that can be used, for example, with TextLayouter.
Constructor Details
#initialize(left, bottom, width, height, shape: nil, context: nil, parent_boxes: []) ⇒ Frame
Creates a new Frame object for the given rectangular area.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/hexapdf/layout/frame.rb', line 135 def initialize(left, bottom, width, height, shape: nil, context: nil, parent_boxes: []) @left = left @bottom = bottom @width = width @height = height @shape = shape || create_rectangle(left, bottom, left + width, bottom + height) @context = context @parent_boxes = parent_boxes.freeze @x = left @y = bottom + height @available_width = width @available_height = height find_max_width_region if shape @region_selection = :max_height end |
Instance Attribute Details
#available_height ⇒ Object (readonly)
The available height of the current region for placing a box.
Also see the note in the #x documentation for further information.
122 123 124 |
# File 'lib/hexapdf/layout/frame.rb', line 122 def available_height @available_height end |
#available_width ⇒ Object (readonly)
The available width of the current region for placing a box.
Also see the note in the #x documentation for further information.
117 118 119 |
# File 'lib/hexapdf/layout/frame.rb', line 117 def available_width @available_width end |
#bottom ⇒ Object (readonly)
The y-coordinate of the bottom-left corner.
91 92 93 |
# File 'lib/hexapdf/layout/frame.rb', line 91 def bottom @bottom end |
#context ⇒ Object (readonly)
The context object (a HexaPDF::Type::Page or HexaPDF::Type::Form) for which this frame should be used.
126 127 128 |
# File 'lib/hexapdf/layout/frame.rb', line 126 def context @context end |
#height ⇒ Object (readonly)
The height of the frame.
97 98 99 |
# File 'lib/hexapdf/layout/frame.rb', line 97 def height @height end |
#left ⇒ Object (readonly)
The x-coordinate of the bottom-left corner.
88 89 90 |
# File 'lib/hexapdf/layout/frame.rb', line 88 def left @left end |
#parent_boxes ⇒ Object (readonly)
An array of box objects representing the parent boxes.
The immediate parent is the last array entry, the top-most parent the first one. All boxes that are fitted into this frame have to be child boxes of the immediate parent box.
132 133 134 |
# File 'lib/hexapdf/layout/frame.rb', line 132 def parent_boxes @parent_boxes end |
#shape ⇒ Object (readonly)
The shape of the frame, either a Geom2D::Rectangle in the simple case or a Geom2D::PolygonSet consisting of rectilinear polygons in the more complex case.
101 102 103 |
# File 'lib/hexapdf/layout/frame.rb', line 101 def shape @shape end |
#width ⇒ Object (readonly)
The width of the frame.
94 95 96 |
# File 'lib/hexapdf/layout/frame.rb', line 94 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.
107 108 109 |
# File 'lib/hexapdf/layout/frame.rb', line 107 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.
112 113 114 |
# File 'lib/hexapdf/layout/frame.rb', line 112 def y @y end |
Instance Method Details
#child_frame(*init_args, shape: nil, box: nil) ⇒ Object
Creates a new Frame object based on this one.
If the init_args
arguments are provided, a new Frame is created using the constructor. The optional shape
argument is then also passed to the constructor.
Otherwise, this frame is duplicated. This kind of invocation is only useful if the box
argument is provided (because otherwise there would be no difference to this frame).
The box
argument can be used to add the appropriate parent box to the list of #parent_boxes for the newly created frame.
163 164 165 166 167 168 169 170 |
# File 'lib/hexapdf/layout/frame.rb', line 163 def child_frame(*init_args, shape: nil, box: nil) parent_boxes = (box ? @parent_boxes.dup << box : @parent_boxes) if init_args.empty? dup.tap {|result| result.instance_variable_set(:@parent_boxes, parent_boxes) } else self.class.new(*init_args, shape: shape, context: @context, parent_boxes: parent_boxes) end end |
#document ⇒ Object
Returns the HexaPDF::Document instance (through #context) that is associated with this Frame object or nil
if no context object has been set.
174 175 176 |
# File 'lib/hexapdf/layout/frame.rb', line 174 def document @context&.document end |
#draw(canvas, fit_result) ⇒ Object
Draws the box of the given Box::FitResult onto the canvas at the fitted position.
After a box is successfully drawn, the frame’s shape is adjusted to remove the occupied area.
314 315 316 317 |
# File 'lib/hexapdf/layout/frame.rb', line 314 def draw(canvas, fit_result) fit_result.draw(canvas) remove_area(fit_result.mask) 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 fitting or drawing a box 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.
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/hexapdf/layout/frame.rb', line 328 def find_next_region case @region_selection when :max_width if @shape.kind_of?(Geom2D::Rectangle) @x = @shape.x @y = @shape.y + @shape.height @available_width = @shape.width @available_height = @shape.height @region_selection = :trim_shape else find_max_width_region @region_selection = :max_height end 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 available_width != 0 end |
#fit(box) ⇒ Object
Fits the given box into the current region of available space and returns the associated Box::FitResult object.
Fitting a box takes the style properties ‘position’, ‘align’, ‘valign’, ‘margin’, and ‘mask_mode’ into account.
Use the Box::FitResult#success? method to determine whether fitting was successful.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 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 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/hexapdf/layout/frame.rb', line 185 def fit(box) return Box::FitResult.new(box, frame: self) if full? margin = box.style.margin if box.style.margin? position = if box.style.position != :flow || box.supports_position_flow? box.style.position else :default end if position.kind_of?(Array) x, y = box.style.position aw = width - x ah = height - y fit_result = box.fit(aw, ah, self) fit_result.success! x += left y += bottom else aw = available_width ah = available_height margin_top = margin_right = margin_left = margin_bottom = 0 if margin aw -= margin_right = margin.right unless float_equal(@x + aw, @left + @width) aw -= margin_left = margin.left unless float_equal(@x, @left) ah -= margin_bottom = margin.bottom unless float_equal(@y - ah, @bottom) ah -= margin_top = margin.top unless float_equal(@y, @bottom + @height) end fit_result = box.fit(aw, ah, self) return fit_result if fit_result.failure? width = box.width height = box.height case position when :default, :float x = case box.style.align when :left @x + margin_left when :right @x + margin_left + aw - width when :center max_margin = [margin_left, 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 + margin_left + (aw - width) / 2.0 end end y = case box.style.valign when :top @y - margin_top - height when :bottom @y - available_height + margin_bottom when :center max_margin = [margin_top, margin_bottom].max # If we have enough space left for equal margins, we center perfectly if available_height - height >= 2 * max_margin @y - height - (available_height - height) / 2.0 else @y - margin_top - height - (ah - height) / 2.0 end end when :flow x = fit_result.x || left y = @y - height else raise HexaPDF::Error, "Invalid value '#{position}' for style property position" end end mask_mode = if box.style.mask_mode == :default case position when :default, :flow then :fill_frame_horizontal else :box end else box.style.mask_mode end rectangle = case mask_mode when :none create_rectangle(x, y, x, y) when :box if margin create_rectangle([left, x - (margin&.left || 0)].max, [bottom, y - (margin&.bottom || 0)].max, [left + self.width, x + box.width + (margin&.right || 0)].min, [bottom + self.height, y + box.height + (margin&.top || 0)].min) else create_rectangle(x, y, x + box.width, y + box.height) end when :fill_horizontal create_rectangle(@x, [bottom, y - (margin&.bottom || 0)].max, @x + available_width, [@y, y + box.height + (margin&.top || 0)].min) when :fill_frame_horizontal create_rectangle(left, [bottom, y - (margin&.bottom || 0)].max, left + self.width, @y) when :fill_vertical create_rectangle([@x, x - (margin&.left || 0)].max, @y - available_height, [@x + available_width, x + box.width + (margin&.right || 0)].min, @y) when :fill create_rectangle(@x, @y - available_height, @x + available_width, @y) end fit_result.x = x fit_result.y = y fit_result.mask = rectangle fit_result end |
#full? ⇒ Boolean
Returns true
if the frame has no more space left.
376 377 378 |
# File 'lib/hexapdf/layout/frame.rb', line 376 def full? available_width == 0 end |
#remove_area(polygon) ⇒ Object
Removes the given rectilinear polygon from the frame’s shape.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/hexapdf/layout/frame.rb', line 357 def remove_area(polygon) return if polygon.kind_of?(Geom2D::Rectangle) && (polygon.width == 0 || polygon.height == 0) @shape = if @shape.kind_of?(Geom2D::Rectangle) && polygon.kind_of?(Geom2D::Rectangle) && float_equal(@shape.x, polygon.x) && float_equal(@shape.width, polygon.width) && float_equal(@shape.y + @shape.height, polygon.y + polygon.height) if float_equal(@shape.height, polygon.height) Geom2D::PolygonSet() else Geom2D::Rectangle(@shape.x, @shape.y, @shape.width, @shape.height - polygon.height) end else Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference) end @region_selection = :max_width find_next_region end |
#split(fit_result) ⇒ Object
Tries to split the box of the given Box::FitResult into two parts and returns both parts.
See Box#split for further details.
306 307 308 |
# File 'lib/hexapdf/layout/frame.rb', line 306 def split(fit_result) fit_result.box.split end |
#width_specification(offset = 0) ⇒ Object
Returns a width specification for the frame’s shape 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 shape 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.
392 393 394 |
# File 'lib/hexapdf/layout/frame.rb', line 392 def width_specification(offset = 0) WidthFromPolygon.new(shape, offset) end |