Class: HexaPDF::Layout::TextBox
- Inherits:
-
Object
- Object
- HexaPDF::Layout::TextBox
- Defined in:
- lib/hexapdf/layout/text_box.rb
Overview
Arranges text and inline objects into lines according to a specified width and height as well as other options.
Features
-
Existing line breaking characters inside of TextFragment objects are respected when fitting text. If this is not wanted, they have to be removed beforehand.
-
The first line may be indented by setting Style#text_indent which may also be negative.
Layouting Algorithm
Laying out text consists of two phases:
-
The items of the text box are broken into pieces which are wrapped into Box, Glue or Penalty objects. Additional Penalty objects marking line breaking opportunities are inserted where needed. This step is done by the SimpleTextSegmentation module.
-
The pieces are arranged into lines using a very simple algorithm that just puts the maximum number of consecutive pieces into each line. This step is done by the SimpleLineWrapping module.
Defined Under Namespace
Modules: SimpleTextSegmentation Classes: Box, Glue, Penalty, SimpleLineWrapping
Instance Attribute Summary collapse
-
#actual_height ⇒ Object
readonly
The actual height of the text box.
-
#items ⇒ Object
The items (TextFragment and InlineBox objects) of the text box that should be layed out.
-
#lines ⇒ Object
readonly
Array of LineFragment objects describing the lines of the text box.
-
#style ⇒ Object
readonly
The style to be applied.
Class Method Summary collapse
-
.create(text, width:, height: nil, x_offsets: nil, **options) ⇒ Object
Creates a new TextBox object for the given text and returns it.
Instance Method Summary collapse
-
#draw(canvas, x, y, fit: :if_needed) ⇒ Object
Draws the text box onto the canvas with the top-left corner being at [x, y].
-
#fit ⇒ Object
:call-seq: text_box.fit -> [remaining_items, actual_height].
-
#initialize(items: [], width:, height: nil, x_offsets: nil, style: Style.new) ⇒ TextBox
constructor
Creates a new TextBox object with the given width containing the given items.
Constructor Details
#initialize(items: [], width:, height: nil, x_offsets: nil, style: Style.new) ⇒ TextBox
Creates a new TextBox object with the given width containing the given items.
The width can either be a simple number specifying a fixed width, or an object that responds to #call(height, line_height) where height
is the bottom of last line and line_height
is the height of the line to be layed out. The return value should be the available width given these height restrictions.
The optional x_offsets
argument works like width
but can be used to specify (varying) offsets from the left of the box (e.g. when the left side of the text should follow a certain shape).
The height is optional and if not specified means that the text box has infinite height.
546 547 548 549 550 551 552 553 |
# File 'lib/hexapdf/layout/text_box.rb', line 546 def initialize(items: [], width:, height: nil, x_offsets: nil, style: Style.new) @style = style @lines = [] self.items = items @width = width @height = height || Float::INFINITY @x_offsets = x_offsets && (x_offsets.respond_to?(:call) ? x_offsets : proc { x_offsets }) end |
Instance Attribute Details
#actual_height ⇒ Object (readonly)
The actual height of the text box. Can be nil
if the items have not been layed out yet, i.e. if #fit has not been called.
532 533 534 |
# File 'lib/hexapdf/layout/text_box.rb', line 532 def actual_height @actual_height end |
#items ⇒ Object
The items (TextFragment and InlineBox objects) of the text box that should be layed out.
523 524 525 |
# File 'lib/hexapdf/layout/text_box.rb', line 523 def items @items end |
#lines ⇒ Object (readonly)
Array of LineFragment objects describing the lines of the text box.
The array is only valid after #fit was called.
528 529 530 |
# File 'lib/hexapdf/layout/text_box.rb', line 528 def lines @lines end |
#style ⇒ Object (readonly)
The style to be applied.
Only the following properties are used: Style#text_indent, Style#align, Style#valign, Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
520 521 522 |
# File 'lib/hexapdf/layout/text_box.rb', line 520 def style @style end |
Class Method Details
.create(text, width:, height: nil, x_offsets: nil, **options) ⇒ Object
Creates a new TextBox object for the given text and returns it.
See ::new for information on height
.
The style of the text box can be specified using additional options, of which font is mandatory.
511 512 513 514 |
# File 'lib/hexapdf/layout/text_box.rb', line 511 def self.create(text, width:, height: nil, x_offsets: nil, **) frag = TextFragment.create(text, **) new(items: [frag], width: width, height: height, x_offsets: x_offsets, style: frag.style) end |
Instance Method Details
#draw(canvas, x, y, fit: :if_needed) ⇒ Object
Draws the text box onto the canvas with the top-left corner being at [x, y].
Depending on the value of fit
the text may also be fitted:
-
If
true
, then #fit is always called. -
If
:if_needed
, then #fit is only called if it has been called before. -
If
false
, then #fit is never called.
644 645 646 647 648 649 650 651 652 653 654 655 656 |
# File 'lib/hexapdf/layout/text_box.rb', line 644 def draw(canvas, x, y, fit: :if_needed) self.fit if fit == true || (!@actual_height && fit == :if_needed) return if @lines.empty? canvas.save_graphics_state do y -= initial_baseline_offset + @lines.first.y_offset @lines.each_with_index do |line, index| line_x = x + line.x_offset line.each {|item, item_x, item_y| item.draw(canvas, line_x + item_x, y + item_y) } y -= @lines[index + 1].y_offset if @lines[index + 1] end end end |
#fit ⇒ Object
:call-seq:
text_box.fit -> [remaining_items, actual_height]
Fits the items into the text box and returns the remaining items as well as the actual height needed.
Note: If the text box height has not been set and variable line widths are used, no search for a possible vertical offset is done in case a single item doesn’t fit.
This method is automatically called as part of the drawing routine but it can also be used by itself to determine the actual height of the text box.
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 |
# File 'lib/hexapdf/layout/text_box.rb', line 579 def fit @lines.clear @actual_height = 0 y_offset = 0 items = @items if style.text_indent != 0 items = [Box.new(InlineBox.new(style.text_indent, 0) { })].concat(items) end if @width.respond_to?(:call) width_arg = proc {|h| @width.call(@actual_height, h)} width_block = @width else width_arg = @width width_block = proc { @width } end rest = style.text_line_wrapping_algorithm.call(items, width_arg) do |line, item| line << TextFragment.new(items: [], style: style) if item.nil? && line.items.empty? new_height = @actual_height + line.height + (@lines.empty? ? 0 : style.line_spacing.gap(@lines.last, line)) if new_height <= @height && !line.items.empty? # valid line found, use it cur_width = width_block.call(@actual_height, line.height) line.x_offset = horizontal_alignment_offset(line, cur_width) line.x_offset += @x_offsets.call(@actual_height, line.height) if @x_offsets line.y_offset = if y_offset y_offset + (@lines.last ? -@lines.last.y_min + line.y_max : 0) else style.line_spacing.baseline_distance(@lines.last, line) end @actual_height = new_height @lines << line y_offset = nil true elsif new_height <= @height && @height != Float::INFINITY # some height left but item didn't fit on the line, search downwards for usable space new_height = @actual_height while item.width > width_block.call(new_height, item.height) && new_height <= @height new_height += item.height / 3 end if new_height + item.height <= @height y_offset = new_height - @actual_height @actual_height = new_height true else nil end else nil end end [rest, @actual_height] end |