Class: HexaPDF::Layout::TextLayouter::SimpleLineWrapping

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/layout/text_layouter.rb

Overview

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.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(items, width_block, frame) ⇒ SimpleLineWrapping

Creates a new line wrapping object that arranges the items on lines with the given width.



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/hexapdf/layout/text_layouter.rb', line 364

def initialize(items, width_block, frame)
  @items = items
  @width_block = width_block
  @frame = frame
  @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
  @fill_horizontal = false

  @height_calc = Line::HeightCalculator.new
  @line = DummyLine.new(0, 0)

  @available_width = @width_block.call(@line)
end

Class Method Details

.call(items, width_block, frame = nil, &block) ⇒ Object

:call-seq:

SimpleLineWrapping.call(items, width_block, frame = nil) {|line, item| block }   -> rest

Arranges the items into lines.

The optional frame argument needs to be a Frame object that is used when fitting inline boxes. If not provided, a custom Frame object is used. However, if the items contain inline boxes that need to access a frame’s context object, it is mandatory to provide an appropriate Frame object.

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) where line_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.



351
352
353
354
355
356
357
358
# File 'lib/hexapdf/layout/text_layouter.rb', line 351

def self.call(items, width_block, frame = nil, &block)
  obj = new(items, width_block, frame)
  if width_block.arity == 1
    obj.variable_width_wrapping(&block)
  else
    obj.fixed_width_wrapping(&block)
  end
end

Instance Method Details

#fixed_width_wrappingObject

Peforms line wrapping with a fixed width per line, with line height playing no role.



384
385
386
387
388
389
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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/hexapdf/layout/text_layouter.rb', line 384

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 = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
end

#variable_width_wrappingObject

Performs the line wrapping with variable widths.



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/hexapdf/layout/text_layouter.rb', line 439

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 = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
end