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
@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,:aggressivewraps them too. This is the only conversion option upstream's table renderer reads, which is whyto_html/1takes 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.
@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
@spec cell(t(), non_neg_integer(), non_neg_integer()) :: PdfElixide.Document.Table.Cell.t() | nil
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">
@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"
@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.
Returns whether the table has been released with close/1.
@spec row(t(), non_neg_integer()) :: PdfElixide.Document.Table.Row.t() | nil
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>
@spec row_count(t()) :: non_neg_integer()
The number of rows in the table.
Table.row_count(table)
#=> 5
@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 "".
Same as to_html/1, but returns the HTML directly and raises
PdfElixide.Error on failure.
@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.
@spec to_markdown!(t(), markdown_opts()) :: String.t()
Same as to_markdown/2, but returns the Markdown directly and raises
PdfElixide.Error on failure.
@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 "".
Same as to_text/1, but returns the text directly and raises
PdfElixide.Error on failure.