Class: HexaPDF::Layout::TextBox::SimpleLineWrapping

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/layout/text_box.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.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(items, available_width) ⇒ SimpleLineWrapping

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



294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/hexapdf/layout/text_box.rb', line 294

def initialize(items, available_width)
  @items = items
  @available_width = @width_block = available_width
  @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 = LineFragment::HeightCalculator.new
  @line_height = 0
end

Class Method Details

.call(items, available_width, &block) ⇒ Object

:call-seq:

SimpleLineWrapping.call(items, available_width) {|line, item| block }   -> rest

Arranges the items into lines.

The available_width argument can either be a simple number or a callable object:

  • If all lines should have the same width, the available_width argument should be a number. This is the general case.

  • However, if lines should have varying lengths (e.g. for flowing text around shapes), the available_width argument should be an object responding to #call(line_height) where line_height is the height of the currently layed out line. The caller is responsible for tracking the height of the already layed out lines. The result of the method call should be the available width.

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 TextFragment or InlineBox that doesn’t fit anymore, or nil in case of mandatory line breaks or when the line break occured at a glue item. If the yielded line is empty and the yielded item is not nil, this single item doesn’t fit into the available width; the caller has to handle this situation, e.g. by stopping.

After the algorithm is finished, it returns the unused items.



281
282
283
284
285
286
287
288
# File 'lib/hexapdf/layout/text_box.rb', line 281

def self.call(items, available_width, &block)
  obj = new(items, available_width)
  if available_width.respond_to?(:call)
    obj.variable_width_wrapping(&block)
  else
    obj.fixed_width_wrapping(&block)
  end
end

Instance Method Details

#fixed_width_wrappingObject

Peforms the line wrapping with a fixed width.



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/hexapdf/layout/text_box.rb', line 310

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.item)
        reset_after_line_break(index)
        redo
      end
    when :glue
      unless add_glue_item(item.item, index)
        break unless yield(create_line, nil)
        reset_after_line_break(index + 1)
      end
    when :penalty
      if item.penalty <= -Penalty::INFINITY
        break unless yield(create_unjustified_line, nil)
        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 && 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

#variable_width_wrappingObject

Performs the line wrapping with variable widths.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
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
# File 'lib/hexapdf/layout/text_box.rb', line 366

def variable_width_wrapping
  index = 0
  @available_width = @width_block.call(@line_height)

  while (item = @items[index])
    case item.type
    when :box
      new_height = @height_calc.simulate_height(item.item)
      if new_height > @line_height
        @line_height = new_height
        @available_width = @width_block.call(@line_height)
      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 yield(create_line, item.item)
        reset_after_line_break(index)
        redo
      end
    when :glue
      unless add_glue_item(item.item, index)
        break unless yield(create_line, nil)
        reset_after_line_break(index + 1)
      end
    when :penalty
      if item.penalty <= -Penalty::INFINITY
        break unless yield(create_unjustified_line, nil)
        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[n_index += 1] while next_item && next_item.type == :penalty
          new_height = @height_calc.simulate_height(next_item.item)
          if next_item && @width + next_item.width > @width_block.call(new_height)
            @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