# Documents

Render document sources to PDF, PNG, or JPG with a local, LaTeX, or remote renderer.

`document.Document` turns a source into a PDF or image with one `Render` call, a typed `OutputFormat` (`pdf`, `png`, `jpg`), and a `Quality` bounded to 0 to 100. Callers never learn whether LaTeX or a remote API did the work.

## Quick start

- `local` (default, `adapters/document/local`) renders HTML in headless Chrome.
- `latex` shells out to a fixed, configured compiler; use it for `.tex` sources.
- `remote` sends the job to a rendering API over HTTP. It needs `endpoint` (https, or http with `allow_insecure: true`); a non-empty `api_key` is sent as a bearer token. Outbound requests dial only globally routable addresses and never follow redirects; a local rendering sidecar needs `allow_private: true`, which logs a line at startup.

```sh
DOCUMENT_ADAPTER=local
DOCUMENT_QUALITY=80
DOCUMENT_DPI=150
DOCUMENT_TMP_DIR=/tmp/zever-doc
DOCUMENT_MAX_OUTPUT_BYTES=67108864
DOCUMENT_TIMEOUT=30s
```

`quality` must be 0 to 100 (`Validate` rejects other values; `ClampQuality` clamps user input before you pass it); `dpi`, `max_output_bytes` (default 64 MiB), and `timeout` must be 0 or more. Durations are nanoseconds, env accepts `30s`. `latex_command` and `latex_pdf_to_ppm` (defaults `pdflatex`, `pdftoppm`) are config only, never interpolated from document content. `latex_runs` bounds compiler passes. For `remote`, `endpoint` must be an absolute URL with scheme and host (`allow_insecure` permits http). See the [document options](https://zenta-dev.github.io/zever/reference/config/#document).

## Render a document

Register the adapter first; the container wires nothing. `Document()` resolves lazily, caches, and retries.

```go title="main.go" check
package main

import (
	"context"
	"fmt"
	"os"

	documentlocal "github.com/zenta-dev/zever/adapters/document/local"
	"github.com/zenta-dev/zever/config"
	"github.com/zenta-dev/zever/container"
	"github.com/zenta-dev/zever/core/document"
)

func main() {
	documentlocal.Register()
	ctx := context.Background()
	c := container.New(config.Default())
	defer c.Close(ctx)

	d, err := c.Document()
	if err != nil {
		panic(err)
	}
	out, err := d.Render(ctx, []byte(`hello`), document.FormatPDF)
	if err != nil {
		panic(err) // out is nil on error, never a partial file
	}
	if err := os.WriteFile("/tmp/hello.pdf", out, 0o600); err != nil {
		panic(err)
	}
	fmt.Println("bytes:", len(out))
}
```

```text
bytes: 1240
```

The byte count varies by adapter and source. Check the error before writing to disk.

## Production checklist

- Sources are capped at 10 MiB and output at 64 MiB by default. Raise them deliberately and with a measured reason.
- The `latex` adapter never interpolates a caller-supplied command, so document content cannot reach a shell. Lock down `tmp_dir` too.
- LaTeX rendering is hardened by default: every render runs in a fresh temp dir as `cwd`, with `-no-shell-escape` and a minimal environment (only `PATH`, `HOME`, and `TEXMF*` are passed; all other parent variables, including credentials, are dropped). `openout_any=p` blocks `\openout` writes outside the render tree and `openin_any=p` restricts `\input` on TeX Live releases that still honor it. Since TeX Live 2026 `openin_any` is a documented no-op upstream (kpathsea `kpse_in_name_ok` always returns true), so the engine alone cannot stop `\input{/etc/passwd}`; the controls below cover that gap. On timeout the whole process group is killed, so a hung render cannot leak processes or pipes.
- **Untrusted LaTeX input.** Treat LaTeX as code. If any part of the source can come from users, set `latex_sandbox: required` and install [bubblewrap](https://github.com/containers/bubblewrap) (`bwrap`). TeX then runs with `--unshare-all` (no network, no IPC or PID visibility), `--die-with-parent`, read-only system and TeX trees, and the per-render temp dir as the only writable path; `/etc`, `/home` and your application files do not exist inside it, so `\input{/etc/hostname}` fails. `latex_sandbox` accepts `auto` (default: use bwrap when it works, otherwise log one warning and continue unsandboxed), `required` (fail at open when bwrap is missing or unusable, for example when unprivileged user namespaces are disabled; in containers this usually needs a seccomp/userns allowance), and `off`. A source pre-scan additionally rejects `\input`, `\include`, `\openin`, `\openout`, `\verbatiminput` and similar with absolute, `..`, `~` or piped paths, `\write18`, and shell-escape packages (`shellesc`, `minted`, `pythontex`, ...) with `ErrUnsafeSource`. The pre-scan is best effort (it does not see `\csname` or `\catcode` tricks) and is never a substitute for the OS sandbox. `latex_allow_unsafe_source: true` disables it for fully trusted sources. Also run the service as an unprivileged user, keep `tmp_dir` on a dedicated mount, and set `timeout` and `max_output_bytes`; output is size-checked before it is read into memory. On Windows there is no OS sandbox (use a container or VM) and a timeout kills only the direct TeX process.
- Bound renders with `timeout` and clean up temp artifacts on error paths.
- `api_key` never appears in logs, and `endpoint` is validated before the first request.

## Troubleshooting

<details>
<summary>`Render` returns nil bytes and an error</summary>

Cause: that is the contract, with no partial files. Fix: inspect the error, do not write the output, and check the source against the 10 MiB cap.
</details>

<details>
<summary>LaTeX renders fail with "command not found"</summary>

Cause: `latex_command` or `latex_pdf_to_ppm` is not on `PATH` (or not an absolute path). Fix: install them, or use the `remote` adapter.
</details>

<details>
<summary>Remote renders fail validation before any request</summary>

Cause: `endpoint` is not an absolute URL with scheme and host, or `timeout` is negative. Fix: correct the config. Nothing was sent.
</details>

## Related

- [Media processing](https://zenta-dev.github.io/zever/guides/data/media/): Image and video limits.
- [Storage and media](https://zenta-dev.github.io/zever/guides/data/storage-media/): Store rendered files.
- [Container](https://zenta-dev.github.io/zever/concepts/container/): Lazy resolve and close order.
