Class: HexaPDF::Layout::Box
- Inherits:
-
Object
- Object
- HexaPDF::Layout::Box
- Includes:
- 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 #supports_position_flow?, #empty?, #fit_content, #split_content, and #draw_content need to be customized according to the subclass’s use case (also see the documentation of the methods besides the information below):
- #supports_position_flow?
-
If the subclass supports the value :flow of the ‘position’ style property, this method needs to be overridden to return
true
.Additionally, if a box object uses flow positioning, #fit_result.x should be set to the correct value since Frame#fit can’t determine this and uses Frame#left in the absence of a set value.
- #empty?
-
This method should return
true
if the subclass won’t draw anything when #draw is called. - #fit_content
-
This method determines whether the box fits into the available region and should set the status of #fit_result appropriately.
It is called from the #fit method which should not be overridden in most cases. The default implementations of both methods provide code common to all use-cases and delegates the specifics to the subclass-specific #fit_content method.
- #split_content
-
This method is called from #split which handles the common cases based on the status of the #fit_result. It needs to handle the case when only some part of the box fits. The method #create_split_box should be used for getting a basic cloned box.
- #draw_content
-
This method draws the box specific content and is called from #draw which already handles things like drawing the border and background. So #draw should usually not be overridden.
This base class also provides various protected helper methods for use in the above methods:
-
#reserved_width, #reserved_height
-
#reserved_width_left, #reserved_width_right, #reserved_height_top, #reserved_height_bottom
-
#update_content_width, #update_content_height
-
#create_split_box
Direct Known Subclasses
ColumnBox, ContainerBox, ImageBox, ListBox, TableBox, TableBox::Cell, TextBox
Defined Under Namespace
Classes: FitResult
Constant Summary
Constants included from Utils
Instance Attribute Summary collapse
-
#fit_result ⇒ Object
readonly
The FitResult instance holding the result after a call to #fit.
-
#height ⇒ Object
readonly
The height of the box, including padding and/or borders.
-
#properties ⇒ Object
readonly
Hash with custom properties.
-
#style ⇒ Object
readonly
The style to be applied.
-
#width ⇒ Object
readonly
The width of the box, including padding and/or borders.
Class Method Summary collapse
-
.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).
Instance Method Summary collapse
-
#content_height ⇒ Object
The height of the content box, i.e.
-
#content_width ⇒ Object
The width of the content box, i.e.
-
#draw(canvas, x, y) ⇒ Object
Draws the content of the box onto the canvas at the position (x, y).
-
#empty? ⇒ Boolean
Returns
true
if no drawing operations are performed. -
#fit(available_width, available_height, frame) ⇒ Object
Fits the box into the frame and returns the #fit_result.
-
#initialize(width: 0, height: 0, style: nil, properties: nil, &block) ⇒ Box
constructor
:call-seq: Box.new(width: 0, height: 0, style: nil, properties: nil) {|canv, box| block} -> box.
-
#split ⇒ 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.
-
#split_box? ⇒ Boolean
Returns the set truthy value if this is a split box, i.e.
-
#supports_position_flow? ⇒ Boolean
Returns
false
since a basic box doesn’t support the ‘position’ style property value :flow.
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.
276 277 278 279 280 281 282 283 284 |
# File 'lib/hexapdf/layout/box.rb', line 276 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_result = FitResult.new(self) @split_box = false end |
Instance Attribute Details
#fit_result ⇒ Object (readonly)
The FitResult instance holding the result after a call to #fit.
231 232 233 |
# File 'lib/hexapdf/layout/box.rb', line 231 def fit_result @fit_result end |
#height ⇒ Object (readonly)
The height of the box, including padding and/or borders.
228 229 230 |
# File 'lib/hexapdf/layout/box.rb', line 228 def height @height end |
#properties ⇒ Object (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
265 266 267 |
# File 'lib/hexapdf/layout/box.rb', line 265 def properties @properties end |
#style ⇒ Object (readonly)
The style to be applied.
Only the following properties are used:
-
Style#position
-
Style#overflow
-
Style#background_color
-
Style#background_alpha
-
Style#padding
-
Style#border
-
Style#overlays
-
Style#underlays
245 246 247 |
# File 'lib/hexapdf/layout/box.rb', line 245 def style @style end |
#width ⇒ Object (readonly)
The width of the box, including padding and/or borders.
225 226 227 |
# File 'lib/hexapdf/layout/box.rb', line 225 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.
213 214 215 216 217 218 219 220 221 222 |
# File 'lib/hexapdf/layout/box.rb', line 213 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_height ⇒ Object
The height of the content box, i.e. without padding and/or borders.
304 305 306 307 |
# File 'lib/hexapdf/layout/box.rb', line 304 def content_height height = @height - reserved_height height < 0 ? 0 : height end |
#content_width ⇒ Object
The width of the content box, i.e. without padding and/or borders.
298 299 300 301 |
# File 'lib/hexapdf/layout/box.rb', line 298 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).
When @draw_block is used (the block specified when creating the box), the coordinate system is translated so that the origin is at the bottom left corner of the **content box**.
Subclasses should not rely on the @draw_block but implement the #draw_content method. The coordinates passed to it are also modified to represent the bottom-left corner of the content box but the coordinate system is not translated.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/hexapdf/layout/box.rb', line 390 def draw(canvas, x, y) if @fit_result.overflow? && @initial_height > 0 && style.overflow == :error raise HexaPDF::Error, "Box with limited height doesn't completely fit and " \ "style property overflow is set to :error" end 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..draw(canvas, x, y, self) if style. canvas.end_optional_content if oc end |
#empty? ⇒ Boolean
Returns true
if no drawing operations are performed.
418 419 420 421 422 423 424 |
# File 'lib/hexapdf/layout/box.rb', line 418 def empty? !(@draw_block || (style.background_color? && style.background_color) || (style.underlays? && !style.underlays.none?) || (style.border? && !style.border.none?) || (style. && !style..none?)) end |
#fit(available_width, available_height, frame) ⇒ Object
Fits the box into the frame and returns the #fit_result.
The arguments available_width
and available_height
are the width and height of the current region of the frame, adjusted for this box. The frame itself is provided as third argument.
If the box uses flow positioning, the width is set to the frame’s width and the height to the remaining height in the frame. Otherwise the given available width and height are used for the width and height if they were initially set to 0. Otherwise the intially specified dimensions are used. The method returns early if the thus configured box already doesn’t fit. Otherwise, the #fit_content method is called which allows sub-classes to fit their content.
The following variables are set that may later be used during splitting or drawing:
-
(@fit_x, @fit_y): The lower-left corner of the content box where fitting was done. Can be used to adjust the drawing position in #draw_content if necessary.
326 327 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/box.rb', line 326 def fit(available_width, available_height, frame) @fit_result.reset(frame) position_flow = supports_position_flow? && style.position == :flow @width = if @initial_width > 0 @initial_width elsif position_flow frame.width else available_width end @height = if @initial_height > 0 @initial_height elsif position_flow frame.y - frame.bottom else available_height end return @fit_result if !position_flow && (float_compare(@width, available_width) > 0 || float_compare(@height, available_height) > 0 || @width - reserved_width < 0 || @height - reserved_height < 0) fit_content(available_width, available_height, frame) @fit_x = frame.x + reserved_width_left @fit_y = frame.y - @height + reserved_height_bottom @fit_result end |
#split ⇒ 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. The method #fit needs to be called before this method to correctly set-up the #fit_result.
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 again 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, nil]
-
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 status of the #fit_result that should be sufficient for most subclasses; only #split_content needs to be implemented if necessary.
374 375 376 377 378 379 380 |
# File 'lib/hexapdf/layout/box.rb', line 374 def split case @fit_result.status when :overflow then (@initial_height > 0 ? [self, nil] : split_content) when :failure then [nil, self] when :success then [self, nil] end end |
#split_box? ⇒ Boolean
Returns the set truthy value if this is a split box, i.e. the rest of another box after it was split.
288 289 290 |
# File 'lib/hexapdf/layout/box.rb', line 288 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.
293 294 295 |
# File 'lib/hexapdf/layout/box.rb', line 293 def supports_position_flow? false end |