Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ jobs:
coverage: none
tools: composer:v2
- run: composer install --no-interaction --no-progress --no-dev --classmap-authoritative
- name: Benchmark (10-page table document, median of 5 runs after 1 warm-up)
- name: Benchmark (10-page table document, PDF and HTML, median of 5 runs after 1 warm-up)
run: php -d opcache.enable_cli=1 bench/run.php | tee bench-result.json
- name: Compare with the committed baseline (fails if > 20% slower)
- name: Compare with the committed baseline (PDF and HTML each fail if > 20% slower)
run: php bench/check.php bench-result.json
- uses: actions/upload-artifact@v7
if: always()
Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,53 @@
All notable changes are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project follows [Semantic Versioning](https://semver.org/).

## [Unreleased] - 0.2.0

v0.1 was never published to Packagist, so there are no deprecation shims.

### Changed (breaking)

Migration from v0.1:

| v0.1 | 0.2 |
|---|---|
| `Pdf::view(...)` | `Papercut::view(...)->pdf()` |
| `<x-pdf::…>` | `<x-doc::…>` |
| `PendingPdf` | `PendingOutput` |
| `PdfManager` | `PapercutManager` |
| `Document::render()` / `toString()` | `toPdf()->bytes()` |
| `Document::save($p)` / `stream()` | `toPdf()->save($p)` / `toPdf()->stream()` |
| `RenderOptions` | `PdfOptions` |
| `Image::handle()` | removed; use `Image::file()` / `Image::bytes()` |

- The `Pdf` facade alias is now `Papercut`; `Papercut::pdfOptions()` replaces `Pdf::options()`.
- The Blade tag prefix is configurable through `papercut.prefix` (default `doc`).
- `PdfOptions` is not a drop-in rename of `RenderOptions`; the constructor changed shape:
- the positional order is now theme, fonts, onWarning, strict, then backend, overflow, cacheDir, so use named arguments;
- `fonts` takes a `FontSet`, not a `FontRegistry`;
- fallback families moved to `FontSet::withFallbacks()`;
- the bundled Inter is always registered.
- Save failures now read `Directory does not exist: …` or `Could not write output to "…": …` (previously `Could not write PDF to …`).
- In Laravel, all config errors (theme, styles, overflow, strict, fonts, including a missing or unreadable font file) now surface when the output is rendered, not at `view()`, `make()` or `Papercut::pdfOptions()`.
- `->html()` returns a `PendingHtml` that no longer extends `PendingOutput` and has no `toString()`, `save()`, `download()`, `inline()` or `output()`. Use `->html()->page()` (full page) or `->html()->fragment()` (embeddable fragment), each a `PendingOutput` named `document.html`. `PendingOutput` is now `final`. Returning `->html()` from a route still sends the full page with no `Content-Disposition`.
- `Output` and the export options expose methods, not properties: `$output->bytes()`, `mime()`, `extension()`; `$options->theme()`, `fonts()`, `onWarning()`, `strict()`, `overflow()`, `cacheDir()`.

### Added

- `Papercut::make($document)` and `PendingDocument` (`document()`, `pdf()`); `PendingOutput` takes its `Content-Type` and default filename from the `Output`.
- Core: `Exporter`/`Output` API, `PdfOptions`, `FontSet`, fidelity reporting (`Fidelity`, `FidelityReport`), strict mode (`UnsupportedFeature`) and `MissingExporter`.
- Config key `papercut.strict` (default `false`).
- HTML export: `Document::toHtml()` in core and `Papercut::view(...)->html()` in Laravel (`PendingHtml`: `(string)` and `{{ }}` give the fragment for embedding, `page()` and `fragment()` each return a `PendingOutput`, and a route response is the full page).
- Config key `papercut.html.fonts` (`none` default, `embed` or `link`), and `papercut.html.image_url` / `papercut.html.font_url`: invokable class names implementing `Papercut\Laravel\Html\ImageUrlResolver` / `FontUrlResolver`, built by the container (class names survive `config:cache`; closures are rejected). `link` needs `font_url`. Invalid `html.*` config is reported at the first `->html()` export.
- Laravel: `papercut.html.fonts = sources`, with `html.font_sources` (default `[GoogleFonts::class]`), `html.google_fonts` (`host`, `display`, `families`) and `html.local_fonts` (`mode`, `families`). The service provider binds `GoogleFonts` and `LocalFonts` from config. Checked lazily at the first `->html()` export, and only when used.
- Preview routes accept `?format=pdf|html`; any other value is a 404.
- `Papercut::exportOptions()` (shared theme, fonts, warnings, strict) and `Papercut::htmlOptions()`; `pdfOptions()` builds on the shared options.
- `HtmlOptions` (`fullPage`, `fontMode`, `imageUrl`, `fontUrl`), `FontMode` (`None`, `Embed`, `Link`) and the `imageUrl` / `fontUrl` callbacks: images embed as data URIs unless `imageUrl` returns a safe URL; fonts embed or link per `FontMode`.
- `onWarning` callbacks are arity-aware: a one-parameter callback gets the message only, a callback with a second parameter also gets the `FidelityReport`.
- A table cell outside a table (not in a row) is laid out as a column and warns `A table cell outside a table is laid out as a column`, in both PDF and HTML.
- HTML font sources: `FontMode::Sources` with `HtmlOptions::fontSources`, an ordered stack of `FontSource` objects or closures (`GoogleFonts` for Google Fonts and Bunny Fonts, `LocalFonts::embed()` / `link()` for registered files). Full pages get `<link>` tags in `<head>`; fragments get `@import` rules first in `<style>`. Unsafe answers fall through to the next source with a warning, and a family nobody answers for is reported as `font not linked`.
- `Papercut\Tree\Visit`: `TreeVisitor` callbacks now receive a `Visit` (path, index, parent, header and region context) instead of loose arguments.

## [0.1.0] - unreleased

### Added
Expand Down
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Blade components that compile to a real layout engine, not HTML.

Papercut is a PDF library for modern PHP. You write `<x-pdf::*>` tags in Blade; they become a tree of
Papercut is a PDF library for modern PHP. You write `<x-doc::*>` tags in Blade; they become a tree of
typed, immutable components with flexbox layout and page-aware pagination, and a pure-PHP engine turns
that tree into a PDF. There is no browser, no HTML parser and no CSS cascade. The tag vocabulary is small,
strict and ours, and a typo in an attribute is an error that names the tag, not a silently wrong page.
Expand All @@ -15,24 +15,34 @@ strict and ours, and a typo in an attribute is an error that names the tag, not
## A Blade document

```blade
<x-pdf::document title="Transcript">
<x-pdf::page size="a4" margin="20mm">
<x-pdf::column gap="8pt">
<x-pdf::h1>{{ $student }}</x-pdf::h1>
<x-pdf::table columns="3fr 25mm 20mm">
<x-pdf::thead><x-pdf::th>Course</x-pdf::th><x-pdf::th>Credits</x-pdf::th><x-pdf::th>Grade</x-pdf::th></x-pdf::thead>
<x-doc::document title="Transcript">
<x-doc::page size="a4" margin="20mm">
<x-doc::column gap="8pt">
<x-doc::h1>{{ $student }}</x-doc::h1>
<x-doc::table columns="3fr 25mm 20mm">
<x-doc::thead><x-doc::th>Course</x-doc::th><x-doc::th>Credits</x-doc::th><x-doc::th>Grade</x-doc::th></x-doc::thead>
@foreach ($courses as $c)
<x-pdf::tr><x-pdf::td>{{ $c->title }}</x-pdf::td><x-pdf::td>{{ $c->credits }}</x-pdf::td><x-pdf::td align="right">{{ $c->grade }}</x-pdf::td></x-pdf::tr>
<x-doc::tr><x-doc::td>{{ $c->title }}</x-doc::td><x-doc::td>{{ $c->credits }}</x-doc::td><x-doc::td align="right">{{ $c->grade }}</x-doc::td></x-doc::tr>
@endforeach
</x-pdf::table>
</x-pdf::column>
<x-slot:footer><x-pdf::page-number format="Page {page} of {total}" align="center" /></x-slot:footer>
</x-pdf::page>
</x-pdf::document>
</x-doc::table>
</x-doc::column>
<x-slot:footer><x-doc::page-number format="Page {page} of {total}" align="center" /></x-slot:footer>
</x-doc::page>
</x-doc::document>
```

```php
return Pdf::view('pdf.transcript', compact('student', 'courses'))->download('transcript.pdf');
return Papercut::view('pdf.transcript', compact('student', 'courses'))->pdf()->download('transcript.pdf');
```

## One template, PDF and HTML

```php
$invoice = Papercut::view('pdf.invoice', compact('invoice'));

return $invoice->pdf()->download('invoice.pdf'); // PDF
return $invoice->html(); // full HTML page
// {!! $invoice->html() !!} // fragment inside another Blade view
```

## Features
Expand All @@ -43,12 +53,12 @@ return Pdf::view('pdf.transcript', compact('student', 'courses'))->download('tra
- Keep-together and keep-with-next, so a heading is not stranded at the bottom of a page.
- Page numbers with a page count: `Page {page} of {total}`.
- Real fonts: every document embeds fonts (Inter is bundled; register your own families), with subsetting and fallback families. The 14 standard PDF fonts are not used.
- Composition: your own Blade components can contain `<x-pdf::*>` tags (see `examples/laravel/views/components`).
- Composition: your own Blade components can contain `<x-doc::*>` tags (see `examples/laravel/views/components`).
- Strict, readable errors for unknown attributes, bad values and invalid structure.

## How it works

1. Each `<x-pdf::*>` component builds a node of the core engine (`Papercut\Row`, `Papercut\Table`, ...) and,
1. Each `<x-doc::*>` component builds a node of the core engine (`Papercut\Row`, `Papercut\Table`, ...) and,
instead of emitting HTML, returns a short opaque marker string that stands in for that node in a
per-render registry.
2. Blade composes the markers into the template output as it normally would, so loops, conditionals and
Expand Down
Loading
Loading