# mmo.pdf — overview for AI code generation
`mmo.pdf` (PHP package `mmo.pdf`, PHP namespace `MMO\pdf`) is Marc Mouret's PDF library. It does two things, which
share no code:
1. **`MMO\pdf\SimplePdf`** — **writes a new PDF from code**: an invoice, a receipt, a report. Headings, text,
labelled fields, a table whose columns fit their content, totals; UTF-8 texts (accents, `€`); the bytes, a file,
or the response a route or a page returns. Built on FPDF 1.86, bundled in `src/vendor/fpdf` with its core fonts and
loaded by the package: nothing to install, nothing to include.
2. **`MMO\pdf\PDFTK`** — **fills an existing PDF's form**: a PHP wrapper around the `pdftk` command-line tool. Reads
an existing PDF's page/field metadata and fills its AcroForm fields with values, using MMOFramework's own
`{path:type}` template-string syntax to bind form field names to arbitrary data.
`MMO\pdf\documents\PDFDocument` / `MMO\pdf\PDFPrinter` / `MMO\pdf\helpers\Page` / `MMO\pdf\helpers\Cell` are
unfinished scaffolding for a `Document`/`Printer` pair — see "What's unfinished" below. Don't reach for them.
Read this whole page before generating code against this package: which of the two real subsystems to use
depends entirely on whether you're **filling an existing PDF** or **generating a brand new one**, and they
share no code path.
## Decide: write a new PDF, or fill an existing one?
- **Writing a new PDF** (an invoice, a quote, a receipt, a report you lay out yourself) → `MMO\pdf\SimplePdf`
(see "Writing a new PDF" below). For what it does not do (an image, a line, a logo), `->fpdf()` gives the
`FPDF` object underneath, at the current position.
- **Filling an existing PDF** (an official form, a contract template, an existing layout you don't control) →
`MMO\pdf\PDFTK`. It reads the PDF's own AcroForm field definitions and only needs values, not layout.
## Writing a new PDF — an invoice
```php
use MMO\pdf\SimplePdf;
$pdf = (new SimplePdf('Invoice ' . $invoice->number)) // the title in the file's properties; A4 portrait
->heading('Invoice ' . $invoice->number) // bold, 16pt
->fields(['Customer' => $invoice->customer, 'Date' => $invoice->date->format('d/m/Y')])
->table(
['Item', 'Qty', 'Unit price', 'Total'], // the columns' titles
array_map(fn($line) => [$line->label, $line->quantity, money($line->price), money($line->total())], $invoice->lines),
['L', 'R', 'R', 'R'] // each column's alignment
)
->totals(['Subtotal' => money($subtotal), 'VAT 20%' => money($vat), 'Total' => money($total)]) // the last one bold
->text('Payment within 30 days.');
return $pdf->response("invoice-{$invoice->number}.pdf"); // from a route: shown in the browser
// $pdf->response($name, false) the browser saves it $pdf->output() the bytes (`%PDF-...`) $pdf->save($file)
```
- **From a page** (`src/pages/invoices/pdf.phtml`, served at `invoices/pdf?id=42`): a page answers with a response
of its own by stopping its route — `$route->stop($pdf->response('invoice-42.pdf')); return;` (mmo.client's
`docs/Pages.md`). From a controller, an `#[APIRoute('GET|invoices/{id}.pdf', ...)]` method returns it, typed
`\MMO\core\interfaces\iResponse`.
- **Texts are UTF-8**: written in the core fonts' encoding (cp1252), so accents, `€`, `œ` print; a character outside
it (Greek, Cyrillic, an emoji) prints as `?`.
- **The table** measures its columns on their content and scales them to the page's width; a text too long for its
column is cut with `...`; a long table goes on to new pages, its header repeated. Pass `$widths` (mm) to fix them.
- **The rest**: `space($mm)`, `text($text, 'B'|'I'|'', 'L'|'C'|'R'|'J')`, `heading($text, $size)`; the public
`$font` (`Helvetica`, `Times`, `Courier`), `$size` (points) and `$lineHeight` (mm), set before writing.
- **In a test**: `$this->assertStringStartsWith('%PDF-', $pdf->output())`; call `$pdf->fpdf()->SetCompression(false)`
first and the texts can be read in the bytes (`"(900,00 \x80)"`: `€` is `\x80` in cp1252). Through the site:
`$this->http('GET', 'invoices/pdf?id=42')` (mmo.core's `docs/Testing.md`).
## The things that matter most
1. **`PDFTK` is a singleton module — never `new` it.** It extends `MMO\core\classes\aModule` (which uses
`tSingleton`), so access it via `PDFTK::Shared()`, exactly like any other MMOFramework module.
2. **`PDFTK::fill()`'s template strings are MMOFramework template syntax, not raw values.** Its signature is
`fill(string $filename, array|\ArrayAccess $data, array $templates, ?callable $coercer = null, array $options = [], bool $flatten = false): string`.
`$templates` maps a PDF field name (as `pdftk` reports it) to a template string in the `{path:type}` syntax
documented on `\MMO\core\strings\loopTemplate()` (`?` prefix for optional, `:type` suffix passed to
`$coercer`). Paths are resolved against `$data` with `\MMO\core\arrays\get()`. A field with no matching
template falls back to the PDF's own default value, then to `''`/an exception depending on whether it's
required (`$this->throwOnRequired`, `throwOnMaxLength`, `throwOnMinLength`, `throwOnEnum` — all `true` by
default except `throwOnMaxLength`; call `PDFTK::Shared()->force()` to relax all four at once).
3. **Call `getInfo()` first to know what fields actually exist.** `PDFTK::getInfo(string $filename): array`
shells out to `pdftk ... dump_data_utf8` + `dump_data_fields_utf8` and returns
`['pages' => [...], 'fields' => [name => ['type' => 'Text'|'Button'|'Choice'|..., 'required' => bool,
'maxLength' => int, 'default' => ..., 'enum' => [...]]]]` plus top-level PDF metadata (author, page count,
etc.). Write your `$templates` array against the real field names this returns — they're whatever the PDF's
original author named them, not something `mmo.pdf` controls.
4. **`fill()` returns the filled PDF as a raw string, not a filename.** It shells out to
`pdftk fill_form - output -` and returns `$exec->getOutput()` directly — write it to disk yourself
(`file_put_contents()`) or stream it in a response (see below).
5. **FPDF is bundled, and loaded with the package.** `src/vendor/fpdf/fpdf.php` (FPDF 1.86, [fpdf.org](http://www.fpdf.org),
its licence beside it) and its core fonts (`src/vendor/fpdf/font/`) are part of the package's sources: `\MMO\inc('mmo.pdf')`
or a project depending on `mmo.pdf` has the global `FPDF` class. `SimplePdf::fpdf()` gives it for anything
`SimplePdf` does not do; its texts are cp1252 (`SimplePdf::encode($utf8)`). The old `fpdf186/` folder at the
package root is a local copy, not tracked beyond `fpdf.php`: don't include it.
## Loading the package
```php
require __DIR__ . '/../core/src/bootstrap.php'; // mmo.core must be bootstrapped first
\MMO\inc('mmo.pdf'); // loads mmo.pdf's own bootstrap.php (PDFTK, FPDF, SimplePdf)
```
## Working example — filling an existing PDF form
```php
use MMO\pdf\PDFTK;
$pdf = PDFTK::Shared();
// 1. Inspect the template to see its real field names/types/constraints.
$info = $pdf->getInfo(__DIR__ . '/templates/contract.pdf');
// $info['fields'] => ['CustomerName' => ['type' => 'Text', 'required' => true, 'maxLength' => 60], ...]
// 2. Map each field name to a template string over your own data shape.
$data = ['customer' => ['name' => 'Marc Mouret'], 'total' => 25792];
$templates = [
'CustomerName' => '{customer/name}',
'Total' => '{total} EUR',
];
// 3. Fill it -- returns the filled PDF's bytes.
$bytes = $pdf->fill(__DIR__ . '/templates/contract.pdf', $data, $templates);
file_put_contents(__DIR__ . '/out/contract-filled.pdf', $bytes);
```
## Serving a generated/filled PDF over HTTP
`MMO\core\server\Response::FromMime()` does **not** special-case PDF — `application/pdf` resolves to the
`'pdf'` extension, which isn't one of `FromMime()`'s matched cases, so it silently falls through to
`Response::Text()`, which is wrong for binary PDF content. Don't use it here.
The accurate, working pattern is `MMO\core\server\StreamResponse::File()`, which streams a file already on
disk and infers `Content-Type: application/pdf` from the `.pdf` extension automatically:
```php
use MMO\core\server\StreamResponse;
// PDFTK output is bytes, not a file -- write it out first.
$bytes = PDFTK::Shared()->fill($templatePath, $data, $templates);
$tmp = \MMO\core\files\temporaryFilename('pdf') . '.pdf';
file_put_contents($tmp, $bytes);
return StreamResponse::File($tmp, [], 'contract.pdf'); // sets Content-Disposition: attachment; filename="contract.pdf"
// SimplePdf answers on its own: return $pdf->response('invoice.pdf');
```
`StreamResponse::File()` defaults to `Content-Disposition: attachment` (forces a download); override the
`Content-Disposition` header yourself if you want the browser to render the PDF inline instead.
## What's unfinished (don't build on these without checking first)
- **`MMO\pdf\documents\PDFDocument`** extends `MMO\core\documents\Document` and sets itself up with
`MMO\pdf\PDFPrinter` as its printer, plus PDF metadata setters (`setTitle()`, `setAuthor()`, `setSubject()`,
`setKeywords()`, `setCreator()`). But **`PDFPrinter` (extends `MMO\core\classes\aExporter`) has no FPDF
rendering logic at all** — it only defines value-export helpers (`DateTimeInterface` → JSON string,
`Exception` → `['type', 'message']`) and a no-op `hash()`. Nothing turns a `PDFDocument`'s body into PDF
bytes yet.
- **`MMO\pdf\helpers\Page`** (orientation/unit/page size/margins/zoom/layout, an `iArray` of content) and
**`MMO\pdf\helpers\Cell`** (`x`, `y`, `text`, `border`) are plain data holders with no `render()`/output
method — nothing consumes them either.
- **None of the four classes above are `include()`d by `src/bootstrap.php`** (only `exceptions/*.php`,
`helpers/TTFFile.php` and `PDFTK.php` are). They exist in `src/` and are tracked in git, but the framework's
autoload fallback cannot reach them either: `src/bootstrap.php` registers no `paths` entry for `MMO\pdf`, so
`class_exists()` on any of them is `false` until you `include` its file yourself.
- **`MMO\pdf\helpers\TTFFile`** is a working, from-scratch TrueType font file parser (reads `head`/`hhea`/
`maxp`/`hmtx`/`loca`/`glyf`/`cmap`/`name`/`OS/2`/`post` tables, and can `extractSubset()` a glyph subset for a
given character list or encoding via `src/fonts/*.map`). It parses correctly on its own, but nothing else in
`src/` currently calls it — it reads as groundwork for future custom-font embedding, not a finished feature.
- **Stamping and merging PDFs** (mentioned as a goal in old comments in `PDFTK.php`, using `Imagick` to draw
checkmarks/images onto form pages and `pdftk multistamp`) is entirely commented out — there is no working
`stamp()`/`merge()` method today. The README's Prerequisites section still asks to install `php-imagick`,
which is leftover from this: no currently-active code in `src/` calls `Imagick` anywhere.
## Prerequisites
- The `pdftk` binary must be installed and on `PATH` (`PDFTK::$bin`, default `'pdftk'`) for anything that uses
`PDFTK`.
- `mb_convert_encoding` (the `mbstring` extension) is required — `src/bootstrap.php` throws immediately if it's
missing.
- `SimplePdf` needs nothing more: FPDF and its core fonts are part of the package (`src/vendor/fpdf`).