Class: HexaPDF::Layout::TextLayouter::SimpleLineWrapping
- Inherits:
-
Object
- Object
- HexaPDF::Layout::TextLayouter::SimpleLineWrapping
- 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
-
.call(items, width_block, frame = nil, &block) ⇒ Object
:call-seq: SimpleLineWrapping.call(items, width_block, frame = nil) {|line, item| block } -> rest.
Instance Method Summary collapse
-
#fixed_width_wrapping ⇒ Object
Peforms line wrapping with a fixed width per line, with line height playing no role.
-
#initialize(items, width_block, frame) ⇒ SimpleLineWrapping
constructor
Creates a new line wrapping object that arranges the
items
on lines with the given width. -
#variable_width_wrapping ⇒ Object
Performs the line wrapping with variable widths.
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) 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.
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_wrapping ⇒ Object
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_wrapping ⇒ Object
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 |