Class: HexaPDF::Layout::Box

Inherits:
Object
  • Object
show all
Includes:
Geom2D::Utils
Defined in:
lib/hexapdf/layout/box.rb

Overview

The base class for all layout boxes.

Box Model

HexaPDF uses the following box model:

  • Each box can specify a width and height. Padding and border are inside, the margin outside of this rectangle.

  • The #content_width and #content_height accessors can be used to get the width and height of the content box without padding and the border.

  • If width or height is set to zero, they are determined automatically during layouting.

Subclasses

Each subclass should only take keyword arguments on initialization so that the boxes can be instantiated from the common convenience method HexaPDF::Document::Layout#box. To use this facility subclasses need to be registered with the configuration option ‘layout.boxes.map’.

The methods #fit, #supports_position_flow?, #split or #split_content, #empty?, and #draw or #draw_content need to be customized according to the subclass’s use case.

#fit

This method should return true if fitting was successful. Additionally, the @fit_successful instance variable needs to be set to the fit result as it is used in #split.

#supports_position_flow?

If the subclass supports the value :flow of the ‘position’ style property, this method needs to be overridden to return true.

#split

This method splits the content so that the current region is used as good as possible. The default implementation should be fine for most use-cases, so only #split_content needs to be implemented. The method #create_split_box should be used for getting a basic cloned box.

#empty?

This method should return true if the subclass won’t draw anything when #draw is called.

#draw

This method draws the content and the default implementation already handles things like drawing the border and background. Therefore it’s best to implement #draw_content which should just draw the content.

Direct Known Subclasses

ColumnBox, ImageBox, ListBox, TableBox, TableBox::Cell, TextBox

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(width: 0, height: 0, style: nil, properties: nil, &block) ⇒ Box

:call-seq:

Box.new(width: 0, height: 0, style: nil, properties: nil) {|canv, box| block} -> box

Creates a new Box object with the given width and height that uses the provided block when it is asked to draw itself on a canvas (see #draw).

Since the final location of the box is not known beforehand, the drawing operations inside the block should draw inside the rectangle (0, 0, content_width, content_height) - note that the width and height of the box may not be known beforehand.



155
156
157
158
159
160
161
162
163
# File 'lib/hexapdf/layout/box.rb', line 155

def initialize(width: 0, height: 0, style: nil, properties: nil, &block)
  @width = @initial_width = width
  @height = @initial_height = height
  @style = Style.create(style)
  @properties = properties || {}
  @draw_block = block
  @fit_successful = false
  @split_box = false
end

Instance Attribute Details

#heightObject (readonly)

The height of the box, including padding and/or borders.



112
113
114
# File 'lib/hexapdf/layout/box.rb', line 112

def height
  @height
end

#propertiesObject (readonly)

Hash with custom properties. The keys should be strings and can be arbitrary.

This can be used to store arbitrary information on boxes for later use. For example, a generic style layer could use one or more custom properties for its work.

The Box class itself uses the following properties:

optional_content

If this property is set, it needs to be an optional content group dictionary, a String defining an (optionally existing) optional content group dictionary, or an optional content membership dictionary.

The whole content of the box, i.e. including padding, border, background…, is wrapped with the appropriate commands so that the optional content group or membership dictionary specifies whether the content is shown or not.

See: HexaPDF::Type::OptionalContentProperties



144
145
146
# File 'lib/hexapdf/layout/box.rb', line 144

def properties
  @properties
end

#styleObject (readonly)

The style to be applied.

Only the following properties are used:

  • Style#background_color

  • Style#background_alpha

  • Style#padding

  • Style#border

  • Style#overlays

  • Style#underlays



124
125
126
# File 'lib/hexapdf/layout/box.rb', line 124

def style
  @style
end

#widthObject (readonly)

The width of the box, including padding and/or borders.



109
110
111
# File 'lib/hexapdf/layout/box.rb', line 109

def width
  @width
end

Class Method Details

.create(width: 0, height: 0, content_box: false, style: nil, **style_properties, &block) ⇒ Object

Creates a new Box object, using the provided block as drawing block (see ::new).

If content_box is true, the width and height are taken to mean the content width and height and the style’s padding and border are added to them appropriately.

The style argument defines the Style object (see Style::create for details) for the box. Any additional keyword arguments have to be style properties and are applied to the style object.



97
98
99
100
101
102
103
104
105
106
# File 'lib/hexapdf/layout/box.rb', line 97

def self.create(width: 0, height: 0, content_box: false, style: nil, **style_properties, &block)
  style = Style.create(style).update(**style_properties)
  if content_box
    width += style.padding.left + style.padding.right +
      style.border.width.left + style.border.width.right
    height += style.padding.top + style.padding.bottom +
      style.border.width.top + style.border.width.bottom
  end
  new(width: width, height: height, style: style, &block)
end

Instance Method Details

#content_heightObject

The height of the content box, i.e. without padding and/or borders.



182
183
184
185
# File 'lib/hexapdf/layout/box.rb', line 182

def content_height
  height = @height - reserved_height
  height < 0 ? 0 : height
end

#content_widthObject

The width of the content box, i.e. without padding and/or borders.



176
177
178
179
# File 'lib/hexapdf/layout/box.rb', line 176

def content_width
  width = @width - reserved_width
  width < 0 ? 0 : width
end

#draw(canvas, x, y) ⇒ Object

Draws the content of the box onto the canvas at the position (x, y).

The coordinate system is translated so that the origin is at the bottom left corner of the **content box** during the drawing operations when @draw_block is used.

The block specified when creating the box is invoked with the canvas and the box as arguments. Subclasses can specify an on-demand drawing method by setting the @draw_block instance variable to nil or a valid block. This is useful to avoid unnecessary set-up operations when the block does nothing.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/hexapdf/layout/box.rb', line 239

def draw(canvas, x, y)
  if (oc = properties['optional_content'])
    canvas.optional_content(oc)
  end

  if style.background_color? && style.background_color
    canvas.save_graphics_state do
      canvas.opacity(fill_alpha: style.background_alpha).
        fill_color(style.background_color).rectangle(x, y, width, height).fill
    end
  end

  style.underlays.draw(canvas, x, y, self) if style.underlays?
  style.border.draw(canvas, x, y, width, height) if style.border?

  draw_content(canvas, x + reserved_width_left, y + reserved_height_bottom)

  style.overlays.draw(canvas, x, y, self) if style.overlays?

  canvas.end_optional_content if oc
end

#empty?Boolean

Returns true if no drawing operations are performed.

Returns:

  • (Boolean)


262
263
264
265
266
267
268
# File 'lib/hexapdf/layout/box.rb', line 262

def empty?
  !(@draw_block ||
    (style.background_color? && style.background_color) ||
    (style.underlays? && !style.underlays.none?) ||
    (style.border? && !style.border.none?) ||
    (style.overlays? && !style.overlays.none?))
end

#fit(available_width, available_height, _frame) ⇒ Object

Fits the box into the Frame and returns true if fitting was successful.

The arguments available_width and available_height are the width and height of the current region of the frame. The frame itself is provided as third argument.

The default implementation uses the available width and height for the box width and height if they were initially set to 0. Otherwise the specified dimensions are used.



194
195
196
197
198
# File 'lib/hexapdf/layout/box.rb', line 194

def fit(available_width, available_height, _frame)
  @width = (@initial_width > 0 ? @initial_width : available_width)
  @height = (@initial_height > 0 ? @initial_height : available_height)
  @fit_successful = (@width <= available_width && @height <= available_height)
end

#split(available_width, available_height, frame) ⇒ Object

Tries to split the box into two, the first of which needs to fit into the current region of the frame, and returns the parts as array.

If the first item in the result array is not nil, it needs to be this box and it means that even when #fit fails, a part of the box may still fit. Note that #fit should not be called before #draw on the first box since it is already fitted. If not even a part of this box fits into the current region, nil should be returned as the first array element.

Possible return values:

[self]

The box fully fits into the current region.

[nil, self]

The box can’t be split or no part of the box fits into the current region.

[self, new_box]

A part of the box fits and a new box is returned for the rest.

This default implementation provides the basic functionality based on the #fit result that should be sufficient for most subclasses; only #split_content needs to be implemented if necessary.



217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/hexapdf/layout/box.rb', line 217

def split(available_width, available_height, frame)
  if @fit_successful
    [self, nil]
  elsif (style.position != :flow &&
         (float_compare(@width, available_width) > 0 ||
          float_compare(@height, available_height) > 0)) ||
      content_height == 0 || content_width == 0
    [nil, self]
  else
    split_content(available_width, available_height, frame)
  end
end

#split_box?Boolean

Returns true if this is a split box, i.e. the rest of another box after it was split.

Returns:

  • (Boolean)


166
167
168
# File 'lib/hexapdf/layout/box.rb', line 166

def split_box?
  @split_box
end

#supports_position_flow?Boolean

Returns false since a basic box doesn’t support the ‘position’ style property value :flow.

Returns:

  • (Boolean)


171
172
173
# File 'lib/hexapdf/layout/box.rb', line 171

def supports_position_flow?
  false
end