PdfElixide.Document.Table (pdf_elixide v0.9.0)

Copy Markdown View Source

A table detected on a PDF page, with its zero-based page index, bounding box, and rows.

Tables are detected by a spatial algorithm rather than read from explicit markup, so a detection is a best guess. The :real_grid? flag reports whether the detection looks like a genuine data grid (at least two rows and columns, consistently populated) as opposed to a form layout or a label-colon-value list; filter on it when false positives matter:

Enum.filter(tables, & &1.real_grid?)

The detector itself is tunable — the strategies, tolerances and size floors it uses are PdfElixide.Document.table_detection_opts/0, passed to PdfElixide.Document.tables/3. Reach for those when a page yields no table, or too many.

:bbox is nil when the detector could not determine the table's extent.

Read a value out with cell/3 or cell_text/3, both zero-based and both nil when the index falls outside the table:

Table.cell_text(table, 0, 0)
#=> "Age"

Table.cell(table, 0, 0)
#=> #PdfElixide.Document.Table.Cell<"Age">

Both indices are positions — the row within :rows, the column within that row's :cells — so they reach exactly what Enum.at/2 would. Note that the detector drops the cells a merge covers without leaving a placeholder, so a row containing a cell whose :colspan or :rowspan is greater than one stores fewer cells than :col_count, and the positions after the merge no longer line up with the visual column.

A table is enumerable over its rows, and each row over its cells, so the whole grid of text is one nested Enum.map/2:

Enum.map(table, fn row -> Enum.map(row, & &1.text) end)
#=> [["Age", "0.042", "0.011", "0.001"], ...]

A single table renders on its own with to_markdown/2, to_html/1, or to_text/1 — the same output PdfElixide.Document.to_markdown/2 and to_html/2 produce for that table within its page:

Table.to_markdown(table)
#=> {:ok, "| Age | 0.042 | 0.011 | 0.001 |\n|---|---|---|---|\n..."}

Rendering goes through :ref, a handle to the detected table held on the Rust side, so it works only on a table that came from extraction — not on a hand-built struct — and stops working once close/1 releases it.

That handle means a %Table{} holds the same table twice: the decoded :rows you read here, and behind :ref the detected table as pdf_oxide built it, kept because only it carries the glyph metrics the renderers need. Both live until close/1 or garbage collection, so on a table-dense page — and more so with PdfElixide.Document.tables/1, which returns every page's tables at once — close the tables you are done rendering.

Summary

Functions

The cell at the zero-based row row_index and column col, or nil when either index falls outside the table.

The text of the cell at the zero-based row row_index and column col, or nil when either index falls outside the table.

Releases the detected table held behind :ref.

Returns whether the table has been released with close/1.

The row at the zero-based index, or nil when the table has no such row.

The number of rows in the table.

Renders the table as an HTML <table> fragment.

Same as to_html/1, but returns the HTML directly and raises PdfElixide.Error on failure.

Renders the table as a Markdown table.

Same as to_markdown/2, but returns the Markdown directly and raises PdfElixide.Error on failure.

Renders the table as plain text, padding each column to a fixed width so the grid lines up in a monospaced font.

Same as to_text/1, but returns the text directly and raises PdfElixide.Error on failure.

Types

markdown_opts()

@type markdown_opts() :: [{:bold_markers, :conservative | :aggressive}]

Options for to_markdown/2.

  • :bold_markers — how **bold** markers are placed around spans whose font is bold: :conservative (the default) skips whitespace-only spans, :aggressive wraps them too. This is the only conversion option upstream's table renderer reads, which is why to_html/1 takes none; unknown keys are ignored, and a wrongly typed value returns {:error, %PdfElixide.Error{reason: :other}} rather than raising.

Bold markers are suppressed in the header row of a multi-row table either way, since a Markdown header is already rendered bold by readers.

t()

@type t() :: %PdfElixide.Document.Table{
  bbox: PdfElixide.Geometry.Rect.t() | nil,
  col_count: non_neg_integer(),
  has_header?: boolean(),
  page: non_neg_integer(),
  real_grid?: boolean(),
  ref: reference(),
  rows: [PdfElixide.Document.Table.Row.t()]
}

Functions

cell(table, row_index, col)

The cell at the zero-based row row_index and column col, or nil when either index falls outside the table.

Table.cell(table, 0, 0)
#=> #PdfElixide.Document.Table.Cell<"Age">

cell_text(table, row_index, col)

@spec cell_text(t(), non_neg_integer(), non_neg_integer()) :: String.t() | nil

The text of the cell at the zero-based row row_index and column col, or nil when either index falls outside the table.

Table.cell_text(table, 0, 0)
#=> "Age"

close(table)

@spec close(t()) :: :ok

Releases the detected table held behind :ref.

The table is normally freed when the BEAM garbage-collects the handle; close/1 frees it now, which is worth doing when walking many tables and keeping only their text — what it frees is upstream's own copy of the table, glyph metrics and all, which is the larger of the two representations a %Table{} holds. The struct's own fields — rows, cells, spans — are plain data and stay readable afterwards; only to_markdown/2, to_html/1, and to_text/1 stop working, returning {:error, %PdfElixide.Error{reason: :closed}} (bang variants raise it).

Infallible and idempotent, so there is no close!/1.

closed?(table)

@spec closed?(t()) :: boolean()

Returns whether the table has been released with close/1.

row(table, index)

The row at the zero-based index, or nil when the table has no such row.

Table.row(table, 0)
#=> #PdfElixide.Document.Table.Row<4 cells>

row_count(table)

@spec row_count(t()) :: non_neg_integer()

The number of rows in the table.

Table.row_count(table)
#=> 5

to_html(table)

@spec to_html(t()) :: {:ok, String.t()} | {:error, PdfElixide.Error.t()}

Renders the table as an HTML <table> fragment.

Table.to_html(table)
#=> {:ok, "<table>\n<thead>\n<tr><th>Age</th>...</table>\n"}

The fragment carries no styling and is not wrapped in a document: header rows become <thead>/<th>, the rest <tbody>/<td>, and merged cells keep their colspan/rowspan as attributes. An empty table renders as "".

to_html!(table)

@spec to_html!(t()) :: String.t()

Same as to_html/1, but returns the HTML directly and raises PdfElixide.Error on failure.

to_markdown(table, opts \\ [])

@spec to_markdown(t(), markdown_opts()) ::
  {:ok, String.t()} | {:error, PdfElixide.Error.t()}

Renders the table as a Markdown table.

Table.to_markdown(table)
#=> {:ok, "| Age | 0.042 | 0.011 | 0.001 |\n|---|---|---|---|\n..."}

Two upstream quirks are worth knowing. Markdown requires a header row, so the first row is rendered as one even when :has_header? is false. And while :colspan widens a cell into extra pipe-delimited columns, :rowspan is ignored entirely — a row whose cells were absorbed by a merge above it is simply padded with empty cells on the right.

An empty table renders as "". See markdown_opts/0 for the options.

to_markdown!(table, opts \\ [])

@spec to_markdown!(t(), markdown_opts()) :: String.t()

Same as to_markdown/2, but returns the Markdown directly and raises PdfElixide.Error on failure.

to_text(table)

@spec to_text(t()) :: {:ok, String.t()} | {:error, PdfElixide.Error.t()}

Renders the table as plain text, padding each column to a fixed width so the grid lines up in a monospaced font.

Table.to_text(table)
#=> {:ok, "Age       0.042  0.011  0.001\n..."}

Cells that span columns are excluded from the width calculation, and an empty table renders as "".

to_text!(table)

@spec to_text!(t()) :: String.t()

Same as to_text/1, but returns the text directly and raises PdfElixide.Error on failure.