# `PhoenixKitAgent.Attachments.Extract`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/attachments/extract.ex#L1)

Turns the bytes of an attachment into what the model reads (spec §9): the
text of a text file, a CSV file or a PDF; CSV text of a workbook; JPEG or
PNG pictures of an image or of the first pages of a scan.

Every function takes the path of a file already read from Storage and
answers `{:ok, result}` or `{:error, code}`; the codes are the ones the
attachment row keeps in `error` (`unreadable`, `too_large`,
`pdf_tools_missing`, `image_tools_missing`) and say nothing of the content.
A file that makes a parser raise is `unreadable`; what the parser said is
dropped, as it can quote the file.

## What is not shown

A text that holds less than the file is `truncated`, and says so where it
ends, in a line the model reads with it (`continues/1`): `[... the file
continues: 12 more rows not shown in full]`, `[... the sheet continues: 1240
of 1300 rows not shown in full]`, `[... the workbook continues: 2 more sheets
not shown: Q3, Q4]`, `[... the document continues: 38 of 50 pages not shown
in full]` — so that a total or a count is not taken from part of the data
without a word. These lines are not counted in `text_chars`.

A workbook's sheets share `text_chars`: a sheet that needs less than an
equal share leaves the rest to the others, and each is cut between rows, so
one large sheet does not push the others out.

# `pdf_text`

```elixir
@type pdf_text() :: %{text: String.t(), pages: pos_integer(), truncated: boolean()}
```

The text of a PDF, with how many pages it has.

# `text`

```elixir
@type text() :: %{text: String.t(), truncated: boolean()}
```

Text for the model, cut to the limit; `truncated` when the file had more.

# `csv`

```elixir
@spec csv(Path.t()) :: {:ok, text()} | {:error, :unreadable}
```

The first rows of a CSV file, as written in it (any delimiter).

# `pdf`

```elixir
@spec pdf(Path.t()) ::
  {:ok, pdf_text()}
  | {:ok, {:scan, pos_integer()}}
  | {:error, :unreadable | :pdf_tools_missing}
```

The text of a PDF (`pdftotext`) with its page count. `{:ok, {:scan, pages}}`
when it holds (almost) no text: a scan, to be read as pictures.

# `pdf_pages`

```elixir
@spec pdf_pages(Path.t()) ::
  {:ok, [binary()], pos_integer()} | {:error, :unreadable | :pdf_tools_missing}
```

The first pages of a PDF as PNG binaries (at most `scan_pages`, 1600 px on the long side) and the page count.

# `picture`

```elixir
@spec picture(Path.t()) ::
  {:ok, binary()} | {:error, :unreadable | :image_tools_missing}
```

A picture as a JPEG whose long side is at most 1600 px (never enlarged),
upright, on white where it was transparent, without its camera metadata
(EXIF with the place and time of a photo, XMP, IPTC, comments:
`PhoenixKitAgent.Attachments.Jpeg.strip_metadata/1`) — what goes to a model
provider is the picture, not where it was taken.

# `text`

```elixir
@spec text(Path.t()) :: {:ok, text()} | {:error, :unreadable}
```

The start of a text file as text (a byte order mark and NUL bytes dropped).
A byte that is not UTF-8 shows as U+FFFD, it is not silently left out.

# `xlsx`

```elixir
@spec xlsx(Path.t()) :: {:ok, text()} | {:error, :unreadable | :too_large}
```

A workbook as CSV: for each sheet (hidden ones left out) a `## Name` line and
its first rows, blank rows skipped, each sheet within its share of
`text_chars` (see the moduledoc). The parts of the workbook are inflated
by `PhoenixKitAgent.Attachments.Unzip`, which counts the bytes it produces
and does not believe the sizes the file declares: one that unpacks to more
than the limit is `:too_large`, however it was forged, and is not opened.

---

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