Class: HexaPDF::Layout::TextLayouter
- Inherits:
-
Object
- Object
- HexaPDF::Layout::TextLayouter
- Defined in:
- lib/hexapdf/layout/text_layouter.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.
-
Text can be fitted into arbitrarily shaped areas, even containing holes.
Layouting Algorithm
Laying out text consists of three phases:
-
The items 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.
-
The lines of step two may actually not be whole lines but line fragments if the area has holes or other discontinuities. The #fit method deals with those so that the line wrapping algorithm can be separate.
Defined Under Namespace
Modules: SimpleTextSegmentation Classes: Box, DummyLine, Glue, Penalty, Result, SimpleLineWrapping
Instance Attribute Summary collapse
-
#style ⇒ Object
readonly
The style to be applied.
Instance Method Summary collapse
-
#fit(items, width, height) ⇒ Object
:call-seq: text_layouter.fit(items, width, height) -> result.
-
#initialize(style = Style.new) ⇒ TextLayouter
constructor
Creates a new TextLayouter object with the given style.
Constructor Details
#initialize(style = Style.new) ⇒ TextLayouter
Creates a new TextLayouter object with the given style.
The style
argument can either be a Style object or a hash of style options. See #style for the properties that are used by the layouter.
630 631 632 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 630 def initialize(style = Style.new) @style = (style.kind_of?(Style) ? style : Style.new(style)) end |
Instance Attribute Details
#style ⇒ Object (readonly)
The style to be applied.
Only the following properties are used: Style#text_indent, Style#align, Style#valign, Style#line_spacing, Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
624 625 626 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 624 def style @style end |
Instance Method Details
#fit(items, width, height) ⇒ Object
:call-seq:
text_layouter.fit(items, width, height) -> result
Fits the items into the given area and returns a Result object with all the information.
The height
argument is just a number specifying the maximum height that can be used.
The width
argument can be one of the following:
- **a number**
-
In this case the layed out lines have this number as maximum width. This is the standard case and means that the area in which the text is layed out is a rectangle.
- **an array with an even number of numbers**
-
The array has to be of the form [offset, width, offset, width, …], so the even indices specify offsets (relative to the current position, not absolute offsets from the left), the odd indices widths. This allows laying out lines containing holes in them.
A simple example: [15, 100, 30, 40]. This means that a space of 15 on the left is never used, then comes text with a maximum width of 100, starting at the absolute offset 15, followed by a hole with a width of 30 and then text again with a width of 40, starting at the absolute offset 145 (=15 + 100 + 30).
- **an object responding to #call(height, line_height)**
-
The provided argument
height
is the bottom of last line (or 0 in case of the first line) andline_height
is the height of the line to be layed out. The return value has to be of one of the forms above (i.e. a single number or an array of numbers) and should describe the area given these height restrictions.This allows laying out text inside complex, arbitrarily formed shapes and can be used, for example, for flowing text around objects.
The text segmentation algorithm specified via #style is applied to the items in case they are not already in segmented form. This also means that Result#remaining_items always contains segmented items.
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 670 def fit(items, width, height) unless items.empty? || items[0].respond_to?(:type) items = style.text_segmentation_algorithm.call(items) end # result variables lines = [] actual_height = 0 rest = items # processing state variables indent = style.text_indent line_fragments = [] line_height = 0 last_line = nil y_offset = 0 width_spec = nil width_spec_index = 0 width_block = if width.respond_to?(:call) last_actual_height = nil last_line_height = nil proc do |h| line_height = [line_height, h || 0].max if last_actual_height != actual_height || last_line_height != line_height spec = width.call(actual_height, line_height) spec = [0, spec] unless spec.kind_of?(Array) last_actual_height = actual_height last_line_height = line_height else spec = width_spec end if spec == width_spec # no changes, just need to return the width of the current part width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0) elsif line_fragments.each_with_index.all? {|l, i| l.width <= spec[i * 2 + 1] } # width_spec changed, parts can only get smaller but processed parts still fit width_spec = spec width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0) else # width_spec changed and some processed part doesn't fit anymore, retry from start line_fragments.clear width_spec = spec width_spec_index = 0 nil end end elsif width.kind_of?(Array) width_spec = width proc { width_spec[width_spec_index * 2 + 1] - (width_spec_index == 0 ? indent : 0) } else width_spec = [0, width] proc { width - indent } end while true too_wide_box = nil rest = style.text_line_wrapping_algorithm.call(rest, width_block) do |line, item| # make sure empty lines broken by mandatory paragraph breaks are not empty line << TextFragment.new([], style) if item&.type != :box && line.items.empty? # item didn't fit into first part, find next available part if line.items.empty? && line_fragments.empty? old_height = actual_height while item.width > width_block.call(item.height) && actual_height <= height width_spec_index += 1 if width_spec_index >= width_spec.size / 2 actual_height += item.height / 3 width_spec_index = 0 end end if actual_height + item.height <= height width_spec_index.times { line_fragments << Line.new } y_offset = actual_height - old_height next true else actual_height = old_height too_wide_box = item next nil end end # continue with line fragments of current line if there are still parts and items # available; also handles the case if at least the first fragment is not empty and a # single item didn't fit into at least one of the other parts line_fragments << line unless line_fragments.size == width_spec.size / 2 || !item || item.type == :penalty width_spec_index += 1 next (width_spec_index == 1 ? :store_start_of_line : true) end combined_line = create_combined_line(line_fragments) new_height = actual_height + combined_line.height + (last_line ? style.line_spacing.gap(last_line, combined_line) : 0) if new_height <= height # valid line found, use it apply_offsets(line_fragments, width_spec, indent, last_line, combined_line, y_offset) lines.concat(line_fragments) line_fragments.clear width_spec_index = 0 indent = if item&.type == :penalty && item.penalty == Penalty::PARAGRAPH_BREAK style.text_indent else 0 end last_line = combined_line actual_height = new_height line_height = 0 y_offset = nil true else nil end end if too_wide_box && (too_wide_box.item.kind_of?(TextFragment) && too_wide_box.item.items.size > 1) rest[0..rest.index(too_wide_box)] = too_wide_box.item.items.map do |item| Box.new(TextFragment.new([item], too_wide_box.item.style)) end too_wide_box = nil else status = (too_wide_box ? :box_too_wide : (rest.empty? ? :success : :height)) break end end unless lines.empty? lines.first.y_offset += initial_baseline_offset(lines, height, actual_height) end Result.new(status, lines, rest) end |