# `PdfElixide.Document.Table`
[🔗](https://github.com/r8/pdf_elixide/blob/v0.16.0/lib/pdf_elixide/document/table.ex#L1)

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

## Detection is a guess

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
it 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 is tunable through `t:PdfElixide.Document.tables_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 extent could not be determined.

## Reading cells

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"

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

A table is also 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"], ...]

## Rendering and the native handle

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
`PdfElixide.Document.to_html/2` produce for that table within its page:

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

All three go through `:ref`, a handle to the detected table held on the Rust
side, so they work only on a table that came from extraction — not on a
hand-built struct — and stop working once `close/1` releases it. A `%Table{}`
therefore holds the same table twice: the decoded `:rows` you read here, and
behind `:ref` the version carrying 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` — close the tables you are done
rendering.

The three renderers take the handle's lock shared and `close/1` takes it
exclusively; see the [Concurrency](guides/concurrency.md) guide.

# `markdown_opts`

```elixir
@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. It is the only option the table renderer
  reads, which is why `to_html/1` takes none.

An unknown key, or a `:bold_markers` value other than those two, raises
`ArgumentError` naming the offending key; see the "Errors versus exceptions"
section of `PdfElixide.Error`.

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`

```elixir
@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()]
}
```

# `cell`

```elixir
@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">

# `cell_text`

```elixir
@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`

```elixir
@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 — it frees the native copy, glyph metrics and all, 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`. It takes the handle's
lock exclusively, so it waits for an in-flight render rather than interrupting
it — freeing the table as soon as the handle is idle, not preemptively.

# `closed?`

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

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

# `row`

```elixir
@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>

# `row_count`

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

The number of rows in the table.

    Table.row_count(table)
    #=> 5

# `to_html`

```elixir
@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 `""`.

Cell text is escaped — see the "Escaping" section of
`PdfElixide.Document.to_html/2`. This renderer has no image path, so nothing in
its output is unescaped.

# `to_html!`

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

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

# `to_markdown`

```elixir
@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..."}

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 padded with empty cells on the right.

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

# `to_markdown!`

```elixir
@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`

```elixir
@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!`

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
