Skip to content
Zever
Home

Documents

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

3 min readEdit on GitHub

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.

  • 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.
Terminal window
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.

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

main.go
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))
}
bytes: 1240

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

  • 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 (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.
Render returns nil bytes and an error

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.

LaTeX renders fail with “command not found”

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.

Remote renders fail validation before any request

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