CLI tool
plutoprint/plutoprint avatar
plutoprint/plutoprint

PlutoPrint renders HTML to PDF and PNG through a C engine

A Python Library for Generating PDFs and Images from HTML, powered by PlutoBook

1,161 stars45 forksPythonMIT

At a glance

What is it?
PlutoPrint is a Python library for turning HTML or XML into paged PDF documents and image files, built on the PlutoBook rendering engine and distributed as a wheel that bundles a compiled binary for three platforms. The API is a Book object you load content into, and the interesting extensions are a CSS function for QR codes and a hook for fetching custom resources.
Who is it for?
PlutoPrint suits a Python service that has to produce a PDF or an image from HTML, such as an invoice, a ticket or a report, and who wants CSS layout rather than a template engine's own idea of a page. It is the wrong choice if you need a pure Python install on an unusual platform, because the prebuilt binaries cover only Windows, Linux on x86-64 and Apple silicon, and everything else needs a build toolchain.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it is for, and what it is built on

PlutoPrint is described as a lightweight and easy-to-use Python library for generating high-quality PDFs and images directly from HTML or XML content, and it is based on PlutoBook's rendering engine, with a simple API to convert HTML into PDF documents or image files. The stated use cases are reports, invoices and visual snapshots, and the samples in the readme are invoices and tickets.

That framing matters because it is a paged document renderer, not an HTML screenshot tool. A screenshot of a web page is a single tall image; a PDF is a sequence of pages with a page size and margins, where content that overflows flows onto the next page and a report is a document rather than a picture of one. The library exposes page sizes and page margins as objects, and the examples use a landscape letter size and a page with no margins, which are the kinds of choices a document generator needs and a screenshot tool does not.

The implementation detail that shapes installation is that this is a Python library with a compiled core. The build backend is meson-python, the project has a meson.build and a subprojects/ directory, the meson arguments build a static default library, and the classifier list includes both the C and the Python programming languages. The Rust-like split of the repository is visible in the tree too, with a plutoprint/ directory for the Python side and a fontconfig/ directory, which is about font discovery on the system.

The licence is MIT, the documentation is on readthedocs, and the current release is 0.22.0, published on 2026-08-04, with 0.21.0 and 0.20.0 before it. The last push was on 2026-10-15 and the repository is not archived.

Installing it, and what the prebuilt binaries cover

Installation is a pip install, and the detail that follows it decides whether your install is quick or not.

bash
pip install plutoprint

PlutoPrint requires PlutoBook, and on Windows with the win_amd64 platform, Linux with manylinux_2_28_x86_64, and macOS with macosx_14_0_arm64, prebuilt binaries are bundled with the package and no further steps are necessary. On other architectures, or when building from source, the getting started guide in the documentation covers the dependency setup and the build instructions.

So the practical situation is that three platform combinations install as a wheel with a binary inside, and everything else needs a C toolchain and the Meson build. That is a normal situation for a library with a compiled core, and it is worth checking against your deployment target before you plan a rollout. A Linux distribution on an older glibc than the manylinux_2_28 baseline, a 32 bit build, or a musl based container image are the cases that need attention.

There is a Homebrew route for macOS and Linux, which is a useful option for a workstation rather than a service:

bash
brew update
brew install plutoprint

The command line entry point is a script named plutoprint, and the simplest use of it converts a file:

bash
plutoprint input.html output.pdf --size=A4

That is a complete tool for the common case, and it is the one to try first before writing any Python, because it tells you immediately whether the rendering engine handles the CSS you care about.

The Book object, and the page range arguments

The Python API is a Book object that you load content into and then export.

python
import plutoprint

book = plutoprint.Book(plutoprint.PAGE_SIZE_A4)
book.load_url("hello.html")

# Export the entire document to PDF
book.write_to_pdf("hello.pdf")

The Book constructor takes a page size, and there is a set of named sizes including A4 and letter, and the page sizes can be oriented, since the QR example uses a landscape letter. Content is loaded either from a URL with load_url or from a string with load_html, and load_html takes a second argument for user style, which is how you inject CSS without touching the document.

The export call is where the library earns its keep, because write_to_pdf takes a range and a step:

python
    # Export pages 2 to 15 (inclusive) in order
    book.write_to_pdf("hello-range.pdf", 2, 15, 1)

    # Export pages 15 to 2 (inclusive) in reverse order
    book.write_to_pdf("hello-reverse.pdf", 15, 2, -1)

That is three arguments, a first page, a last page and a step, and the step can be negative to walk the document backwards. For a report generator that needs to emit a section, or a split and merge workflow, or a preview of a range of pages, that is the feature that makes the library worth using rather than merely adequate.

The image path is parallel. A Book created with media set to the screen media type renders at the document's natural size, and write_to_png takes an optional width, an optional height, or both:

python
    book = plutoprint.Book(media=plutoprint.MEDIA_TYPE_SCREEN)
    book.load_html("<b>Hello World</b>", user_style="body { text-align: center }")

    # Outputs an image at the document's natural size
    book.write_to_png("hello.png")

And there is a query for the natural size, the document width and height, which is what you need if you want to scale the output by a factor rather than to a fixed pixel size, for example a five times scaled image computed from the natural dimensions.

One caveat is stated in the comments: asking for a fixed width and height may stretch or squish the content, which is the expected behaviour of a fixed canvas rather than a bug.

Rendering page by page onto a canvas

For anything the high level export methods do not cover, there is a lower level path with a canvas, and the two variants match the two output formats.

The PDF canvas is a context manager that takes a file name and a page size, and the drawing sequence is explicit:

python
    with plutoprint.PDFCanvas("hello-canvas.pdf", book.get_page_size()) as canvas:
       canvas.scale(plutoprint.UNITS_PX, plutoprint.UNITS_PX)
       for page_index in range(book.get_page_count() - 1, -1, -1):
          canvas.set_size(book.get_page_size_at(page_index))
          book.render_page(canvas, page_index)
          canvas.show_page()

Three things to read there. The canvas is scaled, so the units you draw in are pixels rather than PDF points, which is what lets the same layout code work for both outputs. The page size is set per page rather than once, because get_page_size_at takes an index, so pages of different sizes in one document are handled. And the loop walks the pages in reverse, because the PDF canvas accumulates pages and they are emitted in the order they are shown.

The image canvas is the simpler counterpart, and the difference is that you clear the surface first:

python
    with plutoprint.ImageCanvas(width, height) as canvas:
       canvas.clear_surface(1, 1, 1)
       book.render_document(canvas)
       canvas.write_to_png("hello-canvas.png")

The three arguments to clear_surface are the colour components, and passing one for each gives a white background. Note the difference in what is rendered: the PDF path renders a page at a time with render_page, while the image path renders the whole document with render_document, which is why the image canvas takes a width and a height for the whole document rather than per page sizes.

The pattern to take away is that the high level methods cover most cases, and the canvas API is there for the cases where you need to place content yourself, which is a normal design for a rendering library.

A CSS function for QR codes

The first of the two extensions is a CSS function, and it is the one that makes a business document work without an image pipeline.

The function is written as -pluto-qrcode, takes a string and optionally a colour, and is used in the content property of a pseudo element:

css
    .email::before   { content: -pluto-qrcode('mailto:[email protected]', #16a34a); }
    .tel::before     { content: -pluto-qrcode('tel:+1234567890', #2563eb); }
    .website::before { content: -pluto-qrcode('https://example.com', #ef4444); }
    .github::before  { content: -pluto-qrcode('https://github.com/plutoprint', #f59e0b); }

The example in the readme is a contact card: a table with rows for email, telephone, website and GitHub, a style block that gives the table rounded corners and a border and centres it on the page, and a rule that gives the table headers a fixed size and a space above the QR code. The result is written out as both a PNG and a PDF from the same Book, and the expected output image is linked from the companion samples repository.

What makes this more than a curiosity is that the QR code is generated by the renderer as part of the layout. There is no separate step that produces an image file, and no URL to fetch, and the code scales with the layout like any other content, and the colour is part of the same stylesheet. For a document that has to carry a scannable contact card, that removes an entire class of pipeline work.

The extension is written in the vendor prefixed form the CSS specification reserves for experimental features, which is a reasonable convention for a renderer feature, and it means a browser will ignore the declaration rather than error on it.

Custom resource fetchers, and putting a chart in the document

The second extension is the escape hatch for anything the renderer cannot load, and the example that motivates it is charts.

The idea is that a resource fetcher decides what happens when the document asks for a URL. Subclass it, intercept the scheme you care about, and return a resource you generated yourself. The example intercepts a chart scheme, decodes the values from the URL, draws a bar chart with Matplotlib into an in-memory buffer as an SVG, and hands it back:

python
    class CustomResourceFetcher(plutoprint.ResourceFetcher):
       def fetch_url(self, url):
          if not url.startswith('chart:'):
             return super().fetch_url(url)
          values = [float(v) for v in urllib.parse.unquote(url[6:]).split(',')]
          labels = [chr(65 + i) for i in range(len(values))]

          plt.bar(labels, values)
          plt.title('Bar Chart')
          plt.xlabel('Labels')
          plt.ylabel('Values')

          buffer = io.BytesIO()
          plt.savefig(buffer, format='svg', transparent=True)

          return plutoprint.ResourceData(buffer.getvalue(), "image/svg+xml", "utf-8")

The fetcher is then attached to the Book, and the document refers to the generated resources with the custom scheme:

python
    book = plutoprint.Book(plutoprint.PAGE_SIZE_A4.landscape(), plutoprint.PAGE_MARGINS_NONE)

    book.custom_resource_fetcher = CustomResourceFetcher()

with the HTML containing images whose source is a chart URL holding the values. The ResourceData return carries the bytes, the media type and the encoding, which is why an SVG generated in memory can be embedded as an image and rendered as vector content in the PDF rather than as a raster.

Two details make this practical rather than theoretical. The subclass calls the superclass implementation for anything that is not a chart URL, so the normal fetch path still works for ordinary assets, and the fallback is a single line. And the scheme is fictional, so it cannot collide with a real network request, which means a document that uses it is not accidentally fetching anything.

The result is written to both a PNG and a PDF from the same book, and the expected output is again linked from the samples repository, which is the fastest way to judge whether the rendering matches what you need.

Editorial conclusion

PlutoPrint suits a Python service that has to produce a PDF or an image from HTML, such as an invoice, a ticket or a report, and who wants CSS layout rather than a template engine's own idea of a page. It is the wrong choice if you need a pure Python install on an unusual platform, because the prebuilt binaries cover only Windows, Linux on x86-64 and Apple silicon, and everything else needs a build toolchain. Before adopting, decide whether you want the whole document or a page range, because the range and order arguments are the part of the API you will use most, and check the sample outputs in the companion samples repository rather than assuming a browser's rendering will match.

Frequently asked questions

How do I install PlutoPrint and what are the requirements?

Run pip install plutoprint. It requires PlutoBook, and prebuilt binaries are bundled for Windows on win_amd64, Linux on manylinux_2_28_x86_64 and macOS on macosx_14_0_arm64, so no further steps are needed there. Other architectures need a build, and the getting started guide covers the dependency setup. Python 3.10 or newer is required.

How do I export a range of pages from PlutoPrint?

The write_to_pdf method takes a first page, a last page and a step after the file name, so pages 2 to 15 in order is write_to_pdf with 2, 15 and 1, and the same range in reverse is 15, 2 and -1. There is also a lower level PDFCanvas API for rendering pages individually.

Does PlutoPrint support generating QR codes?

Yes, through a CSS function written as -pluto-qrcode that takes a string and an optional colour, used in the content property of a pseudo element. The code is generated by the renderer as part of the layout, so it needs no separate image step.

Can I embed a chart generated by Matplotlib in a PlutoPrint document?

Yes, by subclassing ResourceFetcher, intercepting a custom URL scheme, generating the SVG in memory and returning it as ResourceData, then assigning the fetcher to the book's custom_resource_fetcher attribute. The example does exactly that with a chart: URL holding the values.

Official sources

  1. License: MIT
  2. plutoprint/plutoprint on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/plutoprint-plutoprint.svg)](https://hysenlabs.com/projects/plutoprint-plutoprint)

Was this useful?