class HexaPDF::Layout::Frame
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 usingdraw
.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 withfit
. Otherwise the frame has no more space for placing boxes. -
Alternatively to calling
find_next_region
it is also possible to callsplit
. 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 withfind_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.
Attributes
The available height for placing a box.
Also see the note in the x
documentation for further information.
The available width for placing a box.
Also see the note in the x
documentation for further information.
The y-coordinate of the bottom-left corner.
The height of the frame.
The x-coordinate of the bottom-left corner.
The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
The width of the frame.
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.
The y-coordinate where the next box will be placed.
Also see the note in the x
documentation for further information.
Public Class Methods
Creates a new Frame
object for the given rectangular area.
# File lib/hexapdf/layout/frame.rb, line 165 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
Public Instance Methods
The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
# File lib/hexapdf/layout/frame.rb, line 340 def contour_line @contour_line || @shape end
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.
# File lib/hexapdf/layout/frame.rb, line 229 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
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.
# File lib/hexapdf/layout/frame.rb, line 301 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
Fits the given box into the current region of available space.
# File lib/hexapdf/layout/frame.rb, line 183 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
Returns true
if the frame has no more space left.
# File lib/hexapdf/layout/frame.rb, line 335 def full? available_width == 0 end
Removes the given rectilinear polygon from both the frame's shape and the frame's contour line.
# File lib/hexapdf/layout/frame.rb, line 324 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
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.
# File lib/hexapdf/layout/frame.rb, line 215 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
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
.
# File lib/hexapdf/layout/frame.rb, line 356 def width_specification(offset = 0) WidthFromPolygon.new(contour_line, offset) end
Private Instance Methods
Creates a Geom2D::Polygon object representing the rectangle with the bottom left corner (blx, bly) and the top right corner (trx, try).
# File lib/hexapdf/layout/frame.rb, line 364 def create_rectangle(blx, bly, trx, try) Geom2D::Polygon(Geom2D::Point(blx, bly), Geom2D::Point(trx, bly), Geom2D::Point(trx, try), Geom2D::Point(blx, try)) end
Finds the region with the maximum height.
# File lib/hexapdf/layout/frame.rb, line 382 def find_max_height_region return unless (segments = find_starting_point) # Find segment with maximum y-coordinate directly below (@x,@y), this determines the # available height index = segments.rindex {|s| s.min.x <= @x && @x < s.max.x } y1 = segments[index].start_point.y @available_height = @y - y1 # Find segment with minium min.x coordinate whose y-coordinate is between y1 and @y and # min.x > @x, for getting the available width segments.select! {|s| s.min.x > @x && y1 <= s.start_point.y && s.start_point.y <= @y } segment = segments.min_by {|s| s.min.x } @available_width = segment.min.x - @x if segment end
Finds the region with the maximum width.
# File lib/hexapdf/layout/frame.rb, line 370 def find_max_width_region return unless (segments = find_starting_point) x_right = @x + @available_width # Available height can be determined by finding the segment with the highest y-coordinate # which lies (maybe only partly) between the vertical lines @x and x_right. segments.select! {|s| s.max.x > @x && s.min.x < x_right } @available_height = @y - segments.last.start_point.y end
Finds and sets the top-left point for the next region. This is always the top-most, left-most vertex of the frame's shape.
If successful, additionally sets the available width to the length of the segment containing the point and returns the sorted horizontal segments except the top-most one.
Otherwise, sets all region specific values to zero and returns nil
.
# File lib/hexapdf/layout/frame.rb, line 417 def find_starting_point segments = sorted_horizontal_segments if segments.empty? @x = @y = @available_width = @available_height = 0 return end top_segment = segments.pop @x = top_segment.min.x @y = top_segment.start_point.y @available_width = top_segment.length segments end
Returns the horizontal segments of the frame's shape, sorted by maximum y-, then minimum x-coordinate.
# File lib/hexapdf/layout/frame.rb, line 434 def sorted_horizontal_segments @shape.each_segment.select(&:horizontal?).sort! do |a, b| if a.start_point.y == b.start_point.y b.start_point.x <=> a.start_point.x else a.start_point.y <=> b.start_point.y end end end
Trims the frame's shape so that the next starting point is different.
# File lib/hexapdf/layout/frame.rb, line 399 def trim_shape return unless (segments = find_starting_point) # Just use the second top-most segment # TODO: not the optimal solution! index = segments.rindex {|s| s.start_point.y < @y } y = segments[index].start_point.y remove_area(Geom2D::Polygon([left, y], [left + width, y], [left + width, @y], [left, @y])) end