class HexaPDF::Layout::TextLayouter::SimpleLineWrapping
Implementation of a simple line wrapping algorithm.
The algorithm arranges the given items so that the maximum number is put onto each line, taking the differences of Box
, Glue
and Penalty
items into account. It is not as advanced as say Knuth's line wrapping algorithm in that it doesn't optimize paragraphs.
Public Class Methods
Arranges the items into lines.
The width_block
argument has to be a callable object that returns the width of the line:
-
If the line width doesn't depend on the height or the vertical position of the line (i.e. fixed line width), the
width_block
should have an arity of zero. However, this doesn't mean that the block is called only once; it is actually called before each new line (e.g. for varying line widths that don't depend on the line height; one common case is the indentation of the first line). This is the general case. -
However, if lines should have varying widths (e.g. for flowing text around shapes), the
width_block
argument should be an object responding to call(line_like) whereline_like
is a Line-like object responding to y_min, y_max and height holding the values for the currently layed out line. The caller is responsible for tracking the height of the already layed out lines. This method involves more work and is therefore slower.
Regardless of whether varying line widths are used or not, each time a line is finished, it is yielded to the caller. The second argument item
is the item that caused the line break (e.g. a Box
, Glue
or Penalty
). The return value should be truthy if line wrapping should continue, or falsy if it should stop. If the yielded line is empty and the yielded item is a box item, this single item didn't fit into the available width; the caller has to handle this situation, e.g. by stopping.
In case of varying widths, the width_block
may also return nil
in which case the algorithm should revert back to a stored item index and then start as if beginning a new line. Which index to use is told the algorithm through the special return value :store_start_of_line
of the yielded-to block. When this return value is used, the current start of the line index should be stored for later use.
After the algorithm is finished, it returns the unused items.
# File lib/hexapdf/layout/text_layouter.rb, line 337 def self.call(items, width_block, &block) obj = new(items, width_block) if width_block.arity == 1 obj.variable_width_wrapping(&block) else obj.fixed_width_wrapping(&block) end end
Creates a new line wrapping object that arranges the items
on lines with the given width.
# File lib/hexapdf/layout/text_layouter.rb, line 350 def initialize(items, width_block) @items = items @width_block = width_block @line_items = [] @width = 0 @glue_items = [] @beginning_of_line_index = 0 @last_breakpoint_index = 0 @last_breakpoint_line_items_index = 0 @break_prohibited_state = false @height_calc = Line::HeightCalculator.new @line = DummyLine.new(0, 0) @available_width = @width_block.call(@line) end
Public Instance Methods
Peforms line wrapping with a fixed width per line, with line height playing no role.
# File lib/hexapdf/layout/text_layouter.rb, line 368 def fixed_width_wrapping index = 0 while (item = @items[index]) case item.type when :box unless add_box_item(item.item) if @break_prohibited_state index = reset_line_to_last_breakpoint_state item = @items[index] end break unless yield(create_line, item) reset_after_line_break(index) redo end when :glue unless add_glue_item(item.item, index) break unless yield(create_line, item) reset_after_line_break(index + 1) end when :penalty if item.penalty <= -Penalty::INFINITY add_box_item(item.item) if item.width > 0 break unless yield(create_unjustified_line, item) reset_after_line_break(index + 1) elsif item.penalty >= Penalty::INFINITY @break_prohibited_state = true add_box_item(item.item) if item.width > 0 elsif item.width > 0 if item_fits_on_line?(item) next_index = index + 1 next_item = @items[next_index] next_item = @items[next_index += 1] while next_item&.type == :penalty if next_item && !item_fits_on_line?(next_item) @line_items.concat(@glue_items).push(item.item) @width += item.width end update_last_breakpoint(index) else @break_prohibited_state = true end else update_last_breakpoint(index) end end index += 1 end line = create_unjustified_line last_line_used = true last_line_used = yield(line, nil) if item.nil? && !line.items.empty? item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1] end
Performs the line wrapping with variable widths.
# File lib/hexapdf/layout/text_layouter.rb, line 425 def variable_width_wrapping index = @stored_index = 0 while (item = @items[index]) case item.type when :box y_min, y_max, new_height = @height_calc.simulate_height(item.item) if new_height > @line.height @line.update(y_min, y_max) @available_width = @width_block.call(@line) if !@available_width || @width > @available_width index = (@available_width ? @beginning_of_line_index : @stored_index) item = @items[index] reset_after_line_break_variable_width(index) redo end end if add_box_item(item.item) @height_calc << item.item else if @break_prohibited_state index = reset_line_to_last_breakpoint_state item = @items[index] end break unless (action = yield(create_line, item)) reset_after_line_break_variable_width(index, true, action) redo end when :glue unless add_glue_item(item.item, index) break unless (action = yield(create_line, item)) reset_after_line_break_variable_width(index + 1, true, action) end when :penalty if item.penalty <= -Penalty::INFINITY add_box_item(item.item) if item.width > 0 break unless (action = yield(create_unjustified_line, item)) reset_after_line_break_variable_width(index + 1, true, action) elsif item.penalty >= Penalty::INFINITY @break_prohibited_state = true add_box_item(item.item) if item.width > 0 elsif item.width > 0 if item_fits_on_line?(item) next_index = index + 1 next_item = @items[next_index] next_item = @items[next_index += 1] while next_item&.type == :penalty y_min, y_max, new_height = @height_calc.simulate_height(next_item.item) if next_item && @width + next_item.width > @width_block.call(DummyLine.new(y_min, y_max)) @line_items.concat(@glue_items).push(item.item) @width += item.width # No need to clean up, since in the next iteration a line break occurs end update_last_breakpoint(index) else @break_prohibited_state = true end else update_last_breakpoint(index) end end index += 1 end line = create_unjustified_line last_line_used = true last_line_used = yield(line, nil) if item.nil? && !line.items.empty? item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1] end
Private Instance Methods
Adds the box item to the line items if it fits on the line.
Returns true
if the item could be added and false
otherwise.
# File lib/hexapdf/layout/text_layouter.rb, line 501 def add_box_item(item) return false unless @width + item.width <= @available_width @line_items.concat(@glue_items).push(item) @width += item.width @glue_items.clear true end
Adds the glue item to the line items if it fits on the line.
Returns true
if the item could be added and false
otherwise.
# File lib/hexapdf/layout/text_layouter.rb, line 512 def add_glue_item(item, index) return false unless @width + item.width <= @available_width unless @line_items.empty? # ignore glue at beginning of line @glue_items << item @width += item.width update_last_breakpoint(index) end true end
Creates a Line
object from the current line items.
# File lib/hexapdf/layout/text_layouter.rb, line 543 def create_line Line.new(@line_items) end
Creates a Line
object from the current line items that ignores line justification.
# File lib/hexapdf/layout/text_layouter.rb, line 548 def create_unjustified_line create_line.tap(&:ignore_justification!) end
Returns true
if the item fits on the line.
# File lib/hexapdf/layout/text_layouter.rb, line 538 def item_fits_on_line?(item) @width + item.width <= @available_width end
Resets the line state variables to their initial values. The index
specifies the items index of the first item on the new line. The line_height
specifies the line height to use for getting the available width.
# File lib/hexapdf/layout/text_layouter.rb, line 555 def reset_after_line_break(index) @beginning_of_line_index = index @line_items.clear @width = 0 @glue_items.clear @last_breakpoint_index = index @last_breakpoint_line_items_index = 0 @break_prohibited_state = false @available_width = @width_block.call(@line) end
Specialized reset method for variable width wrapping.
-
The arguments
index
andline_height
are also passed toreset_after_line_break
. -
If the
action
argument is:store_start_of_line
, the stored item index is reset to the index of the first item of the line.
# File lib/hexapdf/layout/text_layouter.rb, line 572 def reset_after_line_break_variable_width(index, reset_line = false, action = :none) @stored_index = @beginning_of_line_index if action == :store_start_of_line @line.update(0, 0) if reset_line @height_calc.reset reset_after_line_break(index) end
Resets the line items array to contain only those items that were in it when the last breakpoint was encountered and returns the items' index of the last breakpoint.
# File lib/hexapdf/layout/text_layouter.rb, line 531 def reset_line_to_last_breakpoint_state @line_items.slice!(@last_breakpoint_line_items_index..-1) @break_prohibited_state = false @last_breakpoint_index end
Updates the information on the last possible breakpoint of the current line.
# File lib/hexapdf/layout/text_layouter.rb, line 523 def update_last_breakpoint(index) @break_prohibited_state = false @last_breakpoint_index = index @last_breakpoint_line_items_index = @line_items.size end