Skip to content

Papercut 0.3.0

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Oct 21:35
· 150 commits to main since this release

The first release on Packagist. 0.1 and 0.2 were never published, so there are no deprecation shims; the table
below maps v0.1 names for anyone who used the repository directly.

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().
  • Requires dskripchenko/php-pdf 1.26, which parses fonts and their kerning in about a millisecond. The kerning cache (KerningCache) is replaced by a font cache (FontStateCache) that keeps each parsed font, kerning included, in a file of a few hundred KB rather than 5-6 MB. PhpPdfBackend and DefaultBackend::make() take cacheDir instead of kerningCacheDir; PdfOptions(cacheDir:) and the default directory are unchanged, and old kern-*.ser files are left for you to delete.
  • In Laravel, papercut.cache_dir = null now means storage/framework/cache/papercut instead of the system temp directory, which PHP-FPM and the CLI can see differently.
  • Embedded font subsets are named with a tag (/BaseFont /ABCDEF+Inter-Regular) and carry a /CIDSet, as the PDF and PDF/A specs require (php-pdf 1.26), so PDF bytes differ from earlier versions; the drawing is unchanged.

Added

  • Word export (experimental): the new papercut/docx package. $document->export(new DocxExporter()) writes a .docx that Word, LibreOffice and Google Docs open and paginate themselves: columns flow as paragraphs, rows become borderless tables at the PDF's widths, and boxes with padding, a border or a background become one-cell tables. Lists, tables (repeating headers, rows that don't split, colspan), links, headings, images, page numbers, headers and footers, metadata, lang and pagination rules carry over. Each run of pages with the same size, margins, header, footer and layers becomes a Word section, so later pages keep their own setup and headers. Pictures Word can't take as they are are drawn at the PDF's size: WebP and AVIF are converted, image cover is cropped and opacity faded with ext-gd; QR codes are drawn at 300 dpi (styled ones with ext-imagick and Bacon, else plain); SVG is drawn at 300 dpi with ext-imagick, with nothing outside the file loaded. Opacity is drawn by blending each faded colour onto what lies beneath it, and text backgrounds are shaded in their own colour. An image, QR code or SVG in a background (before anything else in it) or alone in an overlay place becomes a floating picture at the PDF's position, repeated on every page through the header. Corner radius on boxes, other layer content, a border or background on a layer picture, and PDF pages and templates are reported as dropped, and the rest of what Word can't show exactly as approximated. The same input gives the same bytes. Built on dskripchenko/php-docx 1.18.
  • Laravel: ->docx() on a PendingDocument (needs papercut/docx, suggested; throws MissingExporter without it), and preview routes accept ?format=docx.
  • PDF templates: Page::template(PdfPage::file($path)) makes a page the size of a page of an existing PDF and draws that page behind everything else on it, for certificates and letterheads. PdfPage::file() and PdfPage::bytes() take a page number and a password, and also draw a PDF page anywhere in the content, sized like an image. The page is drawn as it is, with its text and vector artwork intact, stored once however many pages use it, and encrypted with the document. A missing file, bytes that aren't a PDF, a wrong password or a missing page throw PdfUnreadable. Backends opt in through a new PdfPageLoader interface. In Blade, <x-doc::page template="…"> (or template-data, with template-page and template-password) and <x-doc::pdf-page src|data>. HTML exports leave PDF pages out and report them as dropped.
  • Placed content: Page::overlay(Place::at($x, $y, ...)) puts content at a point on the page, over everything else, on every page. x and y are lengths from the top-left corner or percentages of the page, anchor() picks the point of the content placed there, and width() takes a length or a percentage of the page width. In Blade, <x-doc::place x y anchor width> in the page's overlay slot. HTML exports position places with CSS.
  • SVG: Svg::file($path), Svg::fromString($markup) and <x-doc::svg src|data> (or markup inside the tag) draw an SVG as vector paths through php-pdf's SVG renderer, sized like an image from its width, height or view box, placed by preserveAspectRatio and clipped to its box. fill-rule="evenodd" keeps the holes in rings, letters and stars. SVG <text> is set in the document's embedded fonts, never a base-14 one. Untrusted SVG is checked first: no external files, DTDs or entity declarations, scripts and event handlers removed, and sources over 10 MB or over a million units of drawing work (about a second and a half) refused. Elements not drawn yet, like <clipPath> and <filter>, are named in a warning. HTML exports show an <img> with a data: URI and the SVG's <title> as alt text. Uses the even-odd fill and the SVG group and gradient fixes in php-pdf 1.26.
  • QR codes: QrCode::make($value) and <x-doc::qr-code value="…"> draw a QR code as vector squares in the text color, square, 25mm by default, with error-correction level and quiet zone. Encoded with php-pdf's encoder (up to 213 bytes at Medium) behind a QrEncoder interface, or from your own grid with QrCode::fromModules(). HTML exports draw an inline SVG.
  • Styled QR codes: shape(), eyes(), eyeColor() and gradient() on QrCode, and shape, eyes, eye-color, gradient and gradient-type on <x-doc::qr-code>, give a code dots or rounded modules, dot or pointy eyes, a separate eye color and a linear or radial gradient. Dots and rounded modules raise the error correction to Quartile, unless it is High, so they scan reliably. Papercut encodes the modules and the optional bacon/bacon-qr-code package draws them, through the SVG support; styling without it throws MissingPackage with the command to install it. BaconQrEncoder uses Bacon's encoder for text up to 2,953 bytes. HTML exports show a styled code as an SVG image.
  • Encryption: PdfOptions(encryption: Encryption::password('secret')) and Laravel's ->encrypt() write AES-256 PDFs that open only with the password. Encryption sets print, copy, modify, annotate and assemble permissions; a missing owner password is generated at random so the permissions hold, and screen-reader access is always allowed.
  • Bookmarks: headings h1–h4 fill the PDF's bookmarks panel, nested by level. bookmark($title, $level) (Blade bookmark, bookmark-level) adds or retitles one on any component, bookmark(false) removes a heading's, and PdfOptions(bookmarks: false) / papercut.bookmarks turns them off. Headers, footers and backgrounds never add bookmarks.
  • php artisan papercut:fonts:install <family> downloads a family from Google Fonts into the app for use in PDFs, and prints its fonts config entry.
  • PendingOutput::store($path, $disk = null, $options = []) writes a PDF or HTML export to a Laravel filesystem disk.
  • 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.
  • Core: lists. ListNode::ordered() / unordered() with ListItem, start(), marker() and ListItem::withValue(); ListMarker (decimal, lower/upper alpha and roman, disc, circle, square, none). Markers follow nesting depth unless set, sit in a right-aligned gutter on the first line's baseline, and items split across pages. PDF and HTML (<ol>/<ul>/<li>).
  • Core: Markup::html(), Markup::text() and Markup::flatten() with MarkupOptions (styles: StyleMode::Theme or Original, images resolver closure, blankParagraphs). Converts an HTML fragment or plain text to nodes. Unsupported content is reported through the fidelity report at approximated or dropped, so strict mode fails only on the dropped cases. Nesting deeper than 256 levels is flattened to text with one report. Bytes the image resolver returns that are not a recognised image format count as a missing image. img width/height attributes set a width only (the image keeps the aspect of its bytes and still shrinks to fit a page). Block-level tags without a mapping (blockquote, pre, section, tables, dl, hr, …) keep their content as separate paragraphs (pre keeps its line breaks); unknown tags are reported once per tag name. List start/value attributes are clamped to the 32-bit range, and roman markers fall back to decimal outside 1–3999.
  • Laravel: <x-doc::ol>, <x-doc::ul> and <x-doc::li>.
  • Laravel: <x-doc::markup :html|:text> (inline inside a text tag; numbers are taken as strings) and config keys papercut.markup.images (a class implementing Papercut\Laravel\Markup\ImageResolver), markup.styles and markup.blank_paragraphs.
  • Laravel: <x-doc::node :node="$node" /> embeds a PHP-built Node or Component, as a block or inline inside a text tag. An inline tag cannot span an embedded node.
  • Preview routes accept ?format=pdf|html (and docx, see Word export); 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.
  • Images: an image taller than an empty page shrinks to fit it, keeping its aspect ratio, as it already did for width (previously LayoutOverflow). An explicit height is still respected.
  • Images: WebP, GIF, BMP and AVIF are accepted when the GD extension is loaded (ext-gd, now in suggest). They are converted to PNG for PDF, which is lossless and keeps transparency; HTML export embeds the original bytes. PNG and JPEG never need GD. Without GD the error names the extension to install, and the unsupported-format message lists every accepted format.
  • Paper sizes: Page::named() adds a0, a1, a2, b4, b5, tabloid and executive (Page::sizeNames() lists them all), and an unknown name lists the valid ones. In Laravel, <x-doc::page size> and papercut.paper also accept a width and height, such as "8.5in 11in".
  • Core: decorator components in Papercut\Decorator: Card, Callout, Blockquote, Section, Field, ChoiceList, KeyValueList and Badge. They render from existing nodes, so PDF and HTML export both work, and their look comes from built-in named styles (card, callout.warning, choice.marker, badge.tip, …) that any Theme resolves and withStyle() can override.
  • Core: Theme accepts space-separated style names (apply('callout callout.warning')), merged left to right; an unknown name in the list is reported with a suggestion.
  • Core: View::htmlTag() sets the element HTML export writes (div, section, aside, blockquote, header, footer, figure); PDF ignores it.
  • Core: highlighted text runs. Spans honour background, and <mark> in Text::markup() paints yellow-200 behind the run (one rectangle per line fragment) in PDF and HTML.
  • Core: Markup::html() converts <blockquote> to Blockquote (a trailing <cite> or <footer> becomes the attribution) and <mark> to a highlight; <span style="background-color"> keeps its colour in StyleMode::Original.
  • Laravel: <x-doc::card>, <x-doc::callout>, <x-doc::blockquote>, <x-doc::section>, <x-doc::field>, <x-doc::choices> / <x-doc::choice>, <x-doc::key-value> / <x-doc::kv> and <x-doc::badge>, plus a responses example and a "Decorators" section in the README.
  • Upgrade note: decorator style names (card, card.title, callout.*, section.*, field.*, choice.*, key-value.*, badge.*, blockquote.*) are used by the decorators, and a theme or config style with the same name replaces the built-in one. If you already define a style called card, rename it or expect it to restyle <x-doc::card>.
  • Core: Theme::withStyle() rejects a name containing whitespace, since it could never be resolved; Callout::tone() rejects internal whitespace; blank titles, numbers, labels and cites count as unset; a fully transparent span background paints nothing.
  • Core: em and lh lengths in styles (padding="0.5em", min-height="4lh", Length::em(), Length::lh()): em is the element's font size and lh its line height, resolved to points when styles are computed. A relative font-size is a share of the parent's. Page margins, table tracks and editor-HTML CSS stay absolute.
  • Decorators follow the theme's size: Field::lines(n) reserves n lines of the field's own text inside its padding, choice markers are 0.75em and centred on the first line of their label, and the field label, badge and blockquote cite use 0.9em instead of 9pt.
  • Core: a responses example (examples/core/documents/responses.php), the PHP twin of the Blade one, built from all eight decorators.
  • A documentation site built with VuePress in site/, deployed to GitHub Pages by .github/workflows/docs.yml.
  • PhpPdfBackend(fontCacheFormat: FontStateCache::PHP) writes the font cache as PHP files that opcache keeps in shared memory, for services that render PDFs all day.

Fixed

  • Laravel: <x-doc::image :data="$bytes"> now receives binary image data unchanged. Blade's escaping of bound attributes had replaced bytes that aren't valid UTF-8. Run php artisan view:clear after upgrading, since views compiled before the fix still escape it.
  • Laravel: bound and echoed attribute values reach tags as given. Blade escapes them on the way into a component's attributes, so <x-doc::qr-code :value="$url"> encoded & as &amp; and a bound link href lost its query string.
  • min-height and max-height now apply to boxes and text that size to their content in the PDF (the HTML export already had them). A min-height that does not fit in the space left on a page moves the box to the next page (on an empty page it is cut to the page). A box with max-height (and text with a height) is kept whole and placed or moved, like a fixed-height box; only a cap taller than an empty page lets it split, so no content is cut off. Text also honours an explicit height.
  • Page margins that meet or cross now throw InvalidValue (kind page margins) naming the page and the margins, in both PDF and HTML. Left and right margins wider than the page used to render one letter per line, and the HTML export rendered silently; top and bottom margins taller than the page blamed the header and footer. Checked at export, after the size and orientation are final.
  • Visual change: Field::lines(n) now counts lines inside the padding, so a field with lines is taller by its padding and border (lines="2" goes from 28pt to 40pt at the default size).