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 of each paragraph 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, apply_first_text_indent: true, frame: nil) ⇒ Object
:call-seq: text_layouter.fit(items, width, height, apply_first_text_indent: true) -> 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.
678 679 680 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 678 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#text_align, Style#text_valign, Style#line_spacing, Style#fill_horizontal, Style#text_segmentation_algorithm, Style#text_line_wrapping_algorithm
672 673 674 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 672 def style @style end |
Instance Method Details
#fit(items, width, height, apply_first_text_indent: true, frame: nil) ⇒ Object
:call-seq:
text_layouter.fit(items, width, height, apply_first_text_indent: true) -> 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.
Optional arguments:
apply_first_text_indent
-
Specifies whether style.text_indent should be applied to the first line. This should be set to
false
if the items start with a continuation of a paragraph instead of starting a new paragraph (e.g. after a page break). frame
-
If used with the document layout functionality, this should be the frame into which the text is laid out.
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 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 |
# File 'lib/hexapdf/layout/text_layouter.rb', line 729 def fit(items, width, height, apply_first_text_indent: true, frame: nil) 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 = apply_first_text_indent ? style.text_indent : 0 line_fragments = [] line_height = 0 previous_line = nil y_offset = 0 width_spec = nil width_spec_index = 0 width_block = if width.respond_to?(:call) last_actual_height = nil previous_line_height = nil proc do |cur_line| line_height = [line_height, cur_line.height || 0].max if last_actual_height != actual_height || previous_line_height != line_height gap = if previous_line style.line_spacing.gap(previous_line, cur_line) else 0 end spec = width.call(actual_height + gap, cur_line.height) spec = [0, spec] unless spec.kind_of?(Array) last_actual_height = actual_height previous_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 line_height = 0 rest = style.text_line_wrapping_algorithm.call(rest, width_block, frame) 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? # item didn't fit because no more height is available next nil if actual_height + item.height > height # item fits but is followed by penalty item that didn't fit if item.width < width_block.call(item.item) too_wide_box = item next nil end old_height = actual_height while item.width > width_block.call(item.item) && 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 + (previous_line ? style.line_spacing.gap(previous_line, combined_line) : 0) if new_height <= height # valid line found, use it apply_offsets(line_fragments, width_spec, indent, previous_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 previous_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(too_wide_box.item.dup_attributes([item].freeze)) end too_wide_box = nil else status = (too_wide_box ? :box_too_wide : (rest.empty? ? :success : :height)) break end end unless lines.empty? # Apply baseline offset only for non-variable width text lines.first.y_offset += if width_block.arity == 1 lines.first.y_max else initial_baseline_offset(lines, height, actual_height) end end Result.new(status, lines, rest) end |