Class: HexaPDF::Layout::Frame

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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_heightObject (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_widthObject (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

#bottomObject (readonly)

The y-coordinate of the bottom-left corner.



133
134
135
# File 'lib/hexapdf/layout/frame.rb', line 133

def bottom
  @bottom
end

#heightObject (readonly)

The height of the frame.



139
140
141
# File 'lib/hexapdf/layout/frame.rb', line 139

def height
  @height
end

#leftObject (readonly)

The x-coordinate of the bottom-left corner.



130
131
132
# File 'lib/hexapdf/layout/frame.rb', line 130

def left
  @left
end

#shapeObject (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

#widthObject (readonly)

The width of the frame.



136
137
138
# File 'lib/hexapdf/layout/frame.rb', line 136

def width
  @width
end

#xObject (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

#yObject (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_lineObject

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_regionObject

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.

Returns:

  • (Boolean)


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