Search by

badpixxel / md2pdf

BadPixxel

Build & Render Pdf from Markdown or Twig Templates

Package info

gitlab.com/badpixxel-public/md2pdf

Issues

Type:package

pkg:composer/badpixxel/md2pdf

Statistics

Installs: 5 458

Dependents: 0

Suggesters: 0

Stars: 0

3.0.0 2026-09-23 14:49 UTC

This package is auto-updated.

Last update: 2026-09-23 12:50:31 UTC


README

Build & render Pdf documents from Markdown or Twig templates, with DomPdf.

Features

  • Markdown or Twig contents, assembled as cover, header, footer & pages
  • Blocks: covers, columns, headings, landscape pages, page breaks, table of contents
  • Styles: five of them, each drawing its own covers & page bands from a primary color
  • Styled charts: bar, line & pie, drawn by the package as Svg with the colors of the style, written as ```chart fences in Markdown, or as blocks
  • Mermaid diagrams, drawn by a Kroki server with colors of their own, then kept on disk
  • GitHub alerts: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]
  • Color emojis, sequences included: skin tones, families, professions, flags & keycaps
  • Tables, code highlighting, table of contents, heading permalinks & Fontawesome icons
  • Extensible Markdown: add your own CommonMark extensions to any document

Documentation

PageContents
Getting startedInstall, a first document, how to render it
BlocksEvery block & its options, charts, landscape pages
StylesThe five styles, colors, section pages, drawings
MarkdownEverything the converter renders, attributes, Mermaid
TemplatesNamespaces, Twig extensions, the limits of DomPdf

llms.txt holds the same map, written for the coding agents. Both are shipped with the package, so they are read from vendor/badpixxel/md2pdf/ as well. The package also carries a Claude skill, linked into an application with:

ln -s ../../vendor/badpixxel/md2pdf/.claude/skills/md2pdf .claude/skills/md2pdf

Requirements

Php 8.1+, Symfony 6.4 LTS, 7.4 LTS or 8.0.

Demo

The repository holds a demo application, one document per feature, in its own bundle under demo/. It is not shipped with the package:

make up                          # start containers, demo is served by the dev container
make demo                        # render the demo docs folder in every theme, into samples/
PathContents
demo/src/SamplesSample documents, one class per feature or document
demo/templates/SamplesTheir Markdown & Twig contents
demo/templatesThe demo pages themselves
docs/demoThe demo docs folder, rendered by the console

Docs folders & the console

A directory of Markdown pages, an index.md carrying the options, an assets/ directory: the md2pdf console renders it as one Pdf per language, with the theme of your choice.

vendor/bin/md2pdf themes
vendor/bin/md2pdf build docs/ --lang all --theme ribbons --out build/
vendor/bin/md2pdf build docs/ --watch
vendor/bin/md2pdf demo --theme all --lang all --out build/demo/

The format of the folder, its options and how to add a theme are in docs/docs-folder.md.

Samples

The demo folder, rendered in every theme, is kept in samples/ — one Pdf per theme and language, none of them shipped with the package:

make demo                                          # render them all again
bin/md2pdf demo --theme curves --lang fr --out samples/   # one of them

Tests

make test                   # all suites, locally
make test SUITE=Operational # one suite: Core, Integration or Operational
make verify                 # update vendors, then quality checks & tests in every
                            # container, one per supported Symfony version

Operational renders every sample document to a complete Pdf, without any Symfony kernel.

Blocks

A document is composed of blocks: each one knows its template, its options, and what it requires. Options are resolved when the document is rendered, against its own contents.

use BadPixxel\Md2Pdf\Blocks\{Cover, Heading, TableOfContents, TwoColumns};

$document
    ->setCover(new Cover(introduction: $intro, version: "V1.2"))
    ->addContent(new TableOfContents("Contents"))
    ->addContent(new Heading(2, "Parties"))
    ->addContent(new TwoColumns(
        leftTemplate: "Invoices/seller.md.twig",
        rightTemplate: "Invoices/buyer.md.twig",
    ))
    ->addContent("Invoices/lines.html.twig", array("lines" => $lines))
;
  • Options are given by named arguments, typed by the constructor, or as an options array. Both are validated by the block resolver: a required option that is missing, an unknown one or an invalid value stops the rendering.
  • What the document already carries fills the blocks: the title of a cover, the logo of a header, the licence of a footer.
  • A block asked to render only its own options ignores the document, ie: a table of contents.
  • Every block takes two common options, given as arguments or set fluently: only() renders it without the contents of the document, shift() moves its headings, ie: TemplateBlock::from("Pdf/syntax.md")->shift(1) renders a document written on its own as a chapter of another one. Their names are in the BlockOptions dictionary.
  • A template named by a string is a block too, with free options: an application template knows its own variables, and nothing here could validate them.

Writing a block of your own is a class, a template and its options:

class ReleaseCover extends AbstractBlock
{
    protected const TEMPLATE = "@my-theme/Blocks/release-cover.md.twig";

    public function __construct(?string $version = null, ?string $released = null)
    {
        parent::__construct(self::filter(array("version" => $version, "released" => $released)));
    }

    protected function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setRequired(array("version", "released"))
            ->setAllowedTypes("version", "string")
            ->setAllowedTypes("released", "string")
        ;
    }
}

Styles

A style gives a document its colors, its page bands & its covers. Five are shipped: Standard, Ribbons, Curves, Circles and BadPixxel.

use BadPixxel\Md2Pdf\Blocks\Ribbons\{BackCover, Cover, Section};
use BadPixxel\Md2Pdf\Models\Styles\Ribbons;

Ribbons::setupStyles($document, "Northwind Industries");   // or a primary color of your own

$document
    ->setCover(new Cover(company: "Northwind Industries", kicker: "Annual", title: "Report"))
    ->setBackCover(new BackCover(kicker: "Thank you", contact: $contact))
    ->addContent(new Section(kicker: "Appendix", title: "Annexes"))
;

Drawings are Svg templates, inlined by the svg() Twig function: DomPdf ignores inline <svg> elements, but draws a Svg data Uri as vectors. Colors of the document are available to them, so the same style follows any primary color.

A full page drawing hides the page bands, on the cover & on the back cover: it must be a positioned block written at the root of the page, never nested in a block of its own.

Templates

Three ways to name a template, and one rule: an application template always wins.

NameWhere it livesResolution
@Md2Pdf/Layouts/2-columns.html.twigThis packageIts own namespace, never shadowed
Invoices/lines.html.twigYour applicationMain namespace, searched before the package
@my-theme/Styles/default.html.twigA theme, a bundle…Its own namespace, asked for explicitly
$renderer
    ->addTemplatePath(__DIR__.'/../templates/pdf')     // application contents
    ->addTemplateNamespace('my-theme', $themeDir)      // a provider of its own
;

In a Symfony application, twig.paths entries are already in the main namespace: they win over the package templates too, whether the document is rendered by a controller or by a command.

The package directory stays registered in the main namespace as a fallback, so Layouts/2-columns.html.twig keeps working — but an application file of that name replaces it.

Extending the Markdown converter

$document
    ->addMarkdownExtension(new MyExtension())
    ->setMdAlertsOptions(array('labels' => array('note' => 'Note', 'tip' => 'Astuce')))
;

Emojis

Emojis are rendered as images, from a pack of Twemoji SVG files compiled into a single binary file: DomPdf has no emoji glyph, and no font could render sequences. Nothing is downloaded at render time.

Rebuild the pack when Twemoji publishes a new version:

php bin/build-emoji-pack.php

The pack is in resources/emoji/twemoji.pack.

Licences

This package is released under the MIT licence.

Emoji graphics come from jdecked/twemoji: graphics © Twitter, Inc and other contributors, licensed under CC-BY 4.0, see resources/emoji/LICENSE-GRAPHICS.