Class: HexaPDF::Layout::Frame

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

Utils::EPSILON

Instance Attribute Summary collapse

Instance Method Summary collapse

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

#bottomObject (readonly)

The y-coordinate of the bottom-left corner.



91
92
93
# File 'lib/hexapdf/layout/frame.rb', line 91

def bottom
  @bottom
end

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

#heightObject (readonly)

The height of the frame.



97
98
99
# File 'lib/hexapdf/layout/frame.rb', line 97

def height
  @height
end

#leftObject (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_boxesObject (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

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

#widthObject (readonly)

The width of the frame.



94
95
96
# File 'lib/hexapdf/layout/frame.rb', line 94

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.



107
108
109
# File 'lib/hexapdf/layout/frame.rb', line 107

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.



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

#documentObject

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_regionObject

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.

Returns:

  • (Boolean)


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