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, the #draw method can be used to draw a box onto frame. If drawing is successful, the next box can be drawn. Otherwise, #find_next_region should be called to determine the next region for placing the box. If the call returns true, a region was found and #draw can be tried again. Once #find_next_region returns false the frame has no more space for placing boxes.

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.

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.

If the contour line of the frame is not specified, then the rectangular area is used as contour line.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/hexapdf/layout/frame.rb', line 109

def initialize(left, bottom, width, height, contour_line: nil)
  @left = left
  @bottom = bottom
  @width = width
  @height = height
  @contour_line = contour_line || Geom2D::PolygonSet.new(
    [create_rectangle(left, bottom, left + width, bottom + height)]
  )

  @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
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.



103
104
105
# File 'lib/hexapdf/layout/frame.rb', line 103

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.



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

def available_width
  @available_width
end

#bottomObject (readonly)

The y-coordinate of the bottom-left corner.



70
71
72
# File 'lib/hexapdf/layout/frame.rb', line 70

def bottom
  @bottom
end

#contour_lineObject (readonly)

The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.



82
83
84
# File 'lib/hexapdf/layout/frame.rb', line 82

def contour_line
  @contour_line
end

#heightObject (readonly)

The height of the frame.



76
77
78
# File 'lib/hexapdf/layout/frame.rb', line 76

def height
  @height
end

#leftObject (readonly)

The x-coordinate of the bottom-left corner.



67
68
69
# File 'lib/hexapdf/layout/frame.rb', line 67

def left
  @left
end

#shapeObject (readonly)

The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.



79
80
81
# File 'lib/hexapdf/layout/frame.rb', line 79

def shape
  @shape
end

#widthObject (readonly)

The width of the frame.



73
74
75
# File 'lib/hexapdf/layout/frame.rb', line 73

def width
  @width
end

#xObject (readonly)

The x-coordinate where the next box will be placed.

Note: Since the algorithm for #draw takes the margin of a box into account, the actual x-coordinate (and y-coordinate, available width and available height) might be different.



88
89
90
# File 'lib/hexapdf/layout/frame.rb', line 88

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.



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

def y
  @y
end

Instance Method Details

#draw(canvas, box) ⇒ Object

Draws the given box onto the canvas at the frame’s current position. Returns true if drawing was possible, false otherwise.

When positioning the box, the style properties “position”, “position_hint” and “margin” are taken into account. Note that the margin is ignored if a box’s side coincides with the frame’s original boundary.

After a box is successfully drawn, the frame’s shape and contour line are adjusted to remove the occupied area.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/hexapdf/layout/frame.rb', line 137

def draw(canvas, box)
  aw = available_width
  ah = available_height
  used_margin_left = used_margin_right = used_margin_top = 0

  if box.style.position != :absolute
    if box.style.margin?
      margin = box.style.margin
      ah -= margin.bottom unless float_equal(@y - ah, @bottom)
      ah -= used_margin_top = margin.top unless float_equal(@y, @bottom + @height)
      aw -= used_margin_right = margin.right unless float_equal(@x + aw, @left + @width)
      aw -= used_margin_left = margin.left unless float_equal(@x, @left)
    end

    return false unless box.fit(aw, ah, self)
  end

  width = box.width
  height = box.height

  case box.style.position
  when :absolute
    x, y = box.style.position_hint
    x += left
    y += bottom
    rectangle = if box.style.margin?
                  margin = 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 + used_margin_left
    x += aw - width if box.style.position_hint == :right
    y = @y - height - used_margin_top
    # We can 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 + used_margin_left + aw - width
        when :center
          max_margin = [used_margin_left, used_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 + used_margin_left + (aw - width) / 2.0
          end
        else
          @x + used_margin_left
        end
    y = @y - height - used_margin_top
    rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
  end

  box.draw(canvas, x, y)
  remove_area(rectangle)

  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.



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/hexapdf/layout/frame.rb', line 215

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

  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.



237
238
239
240
241
242
243
# File 'lib/hexapdf/layout/frame.rb', line 237

def remove_area(polygon)
  @shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
  @contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon,
                                                           :difference)
  @region_selection = :max_width
  find_next_region
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.



257
258
259
# File 'lib/hexapdf/layout/frame.rb', line 257

def width_specification(offset = 0)
  WidthFromPolygon.new(@contour_line, offset)
end