Class: HexaPDF::Layout::TableBox::Cells

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

Overview

Represents the cells of a TableBox.

This class is a wrapper around an array of arrays and provides some utility methods for managing and styling the cells.

Table data transformation into correct form

One of the main purposes of this class is to transform the cell data provided on initialization into the representation a TableBox instance can work with.

The data argument for ::new is an array of arrays representing the rows of the table. Each row array may contain one of the following items:

  • A single Box instance defining the content of the cell.

  • An array of Box instances defining the content of the cell.

  • A hash which defines the content of the cell as well as, optionally, additional information through the following keys:

    :content

    The content for the cell. This may be a single Box or an array of Box instances.

    :row_span

    An integer specifying the number of rows this cell should span.

    :col_span

    An integer specifying the number of columsn this cell should span.

    :properties

    A hash of properties (see Box#properties) to be set on the cell itself.

    All other key-value pairs are taken to be cell styling information (like :background_color) and assigned to the cell style.

Additionally, the first item in the data argument is treated specially if it is not an array:

  • If it is a hash, it is assumed to be style properties to be set on all created cell instances.

  • If it is a callable object, it needs to accept a cell as argument and is called for all created cell instances.

Any properties or styling information retrieved from the respective item in data takes precedence over the above globally specified information.

Here is an example input data array:

data = [[box1, {col_span: 2, content: box2}, box3],
        [box4, box5, {col_span: 2, row_span: 2, content: [box6.1, box6.2]}],
        [box7, box8]]

And this is what the table will look like:

| box1 | box2         | box 3 |
| box4 | box5 | box6.1 box6.2 |
| box7 | box8 |               |

Instance Method Summary collapse

Constructor Details

#initialize(data, cell_style: nil) ⇒ Cells

Creates a new Cells instance with the given data which cannot be changed afterwards.

The optional cell_style argument can either be a hash of style properties to be assigned to every cell or a block accepting a cell for more control over e.g. style assignment. If the data has such a cell style as its first item, the cell_style argument is not used.

See the class documentation for details on the data argument.



331
332
333
334
335
# File 'lib/hexapdf/layout/table_box.rb', line 331

def initialize(data, cell_style: nil)
  @cells = []
  @number_of_columns = 0
  assign_data(data, cell_style)
end

Instance Method Details

#[](row, column) ⇒ Object

Returns the cell (a Cell instance) in the given row and column.

Note that the same cell instance may be returned for different (row, column) arguments if the cell spans more than one row and/or column.



341
342
343
# File 'lib/hexapdf/layout/table_box.rb', line 341

def [](row, column)
  @cells[row]&.[](column)
end

#draw_rows(start_row, end_row, canvas, x, y) ⇒ Object

Draws the rows from start_row to end_row on the given canvas, with the top-left corner of the resulting table being at (x, y).



472
473
474
475
476
477
478
479
# File 'lib/hexapdf/layout/table_box.rb', line 472

def draw_rows(start_row, end_row, canvas, x, y)
  @cells[start_row..end_row].each.with_index(start_row) do |columns, row_index|
    columns.each_with_index do |cell, col_index|
      next if cell.row != row_index || cell.column != col_index
      cell.draw(canvas, x + cell.left, y - cell.top - cell.height)
    end
  end
end

#each_row(&block) ⇒ Object

Iterates over each row.



356
357
358
# File 'lib/hexapdf/layout/table_box.rb', line 356

def each_row(&block)
  @cells.each(&block)
end

#fit_rows(start_row, available_height, column_info, frame) ⇒ Object

Fits all rows starting from start_row into an area with the given available_height, using the column information in column_info. Returns the used height as well as the row index of the last row that fit (which may be -1 if no row fits).

The column_info argument needs to be an array of arrays of the form [x_pos, width] containing the horizontal positions and widths of each column.

The frame argument is further handed down to the Cell instances for fitting.

The fitting of a cell is done through the Cell#fit method which stores the result in the cell itself. Furthermore, Cell#left and Cell#top are also assigned correctly.



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
430
431
432
433
434
435
436
437
438
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
# File 'lib/hexapdf/layout/table_box.rb', line 382

def fit_rows(start_row, available_height, column_info, frame)
  height = available_height
  last_fitted_row_index = -1
  row_heights = {}
  zero_height_rows = {}
  row_spans = []

  @cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
    # 1. Fit all columns of the row and record the max height of all non-row-span cells. If
    #    a row has zero height (usually because it only has row-span cells), record that
    #    information. Additionally store all cells with row-spans.
    row_fit = true
    row_height = 0
    columns.each_with_index do |cell, col_index|
      next if cell.row != row_index || cell.column != col_index
      available_cell_width = if cell.col_span > 1
                               column_info[cell.column, cell.col_span].map(&:last).sum
                             else
                               column_info[cell.column].last
                             end
      unless cell.fit(available_cell_width, available_height, frame).success?
        row_fit = false
        break
      end
      if row_height < cell.preferred_height && cell.row_span == 1
        row_height = cell.preferred_height
      end
      row_spans << cell if cell.row_span > 1
    end

    zero_height_rows[row_index] = true if row_height == 0

    if row_fit
      # 2. If all cells of the row fit, we subtract the recorded row height of the
      #    non-row-span cells from the available height for the next pass.
      last_fitted_row_index = row_index
      row_heights[row_index] = row_height
      available_height -= row_height

      # 3. We look at all row-span cells that end at the current row index. If the row-span
      #    cell is larger than the sum of the row heights, we proportionally enlarge the
      #    stored height of each spanned row and subtract the difference from the available
      #    height for the next pass. If the row span contains initially zero-height rows,
      #    only those rows are enlarged. Row-span cells themselves are not updated at this
      #    point!
      row_spans.each do |cell|
        upper_row_index = cell.row + cell.row_span - 1
        next unless upper_row_index == row_index

        rows = cell.row.upto(upper_row_index)
        row_span_height = rows.sum {|ri| row_heights[ri] }
        if row_span_height < cell.preferred_height
          zero_height_rows_in_span = rows.select {|ri| zero_height_rows[ri] }
          rows = zero_height_rows_in_span if zero_height_rows_in_span.size > 0
          adjustment = (cell.preferred_height - row_span_height) / rows.size.to_f
          rows.each {|ri| row_heights[ri] += adjustment }
          available_height -= cell.preferred_height - row_span_height
        end
      end
    else
      last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
      break
    end
  end

  if last_fitted_row_index >= 0
    # 4. Once all possible rows have been fitted and the heights of the rows are fixed, the
    #    final height and top-left corner of each cell needs to be set.
    running_height = 0
    @cells[start_row..last_fitted_row_index].each.with_index(start_row) do |columns, row_index|
      columns.each_with_index do |cell, col_index|
        next if cell.row != row_index || cell.column != col_index
        cell.left = column_info[cell.column].first
        cell.top = running_height
        if cell.row_span == 1
          cell.update_height(row_heights[row_index])
        else
          new_height = cell.row.upto(cell.row + cell.row_span - 1).sum {|ri| row_heights[ri] }
          cell.update_height(new_height)
        end
      end
      running_height += row_heights[row_index]
    end
  end

  [height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index]
end

#number_of_columnsObject

Returns the number of columns.



351
352
353
# File 'lib/hexapdf/layout/table_box.rb', line 351

def number_of_columns
  @number_of_columns
end

#number_of_rowsObject

Returns the number of rows.



346
347
348
# File 'lib/hexapdf/layout/table_box.rb', line 346

def number_of_rows
  @cells.size
end

#style(**properties, &block) ⇒ Object

Applies the given style properties to all cells and optionally yields all cells for more complex customization.



362
363
364
365
366
367
368
369
# File 'lib/hexapdf/layout/table_box.rb', line 362

def style(**properties, &block)
  @cells.each do |columns|
    columns.each do |cell|
      cell.style.update(**properties)
      block&.call(cell)
    end
  end
end