# clj-pdf: generating PDF documents from plain Clojure vectors

> clj-pdf turns a vector of keywords and strings into a PDF, with a document element vocabulary for lists, tables, charts, fonts and sections, and a templating macro that fills anchors from a sequence of maps.

**clj-pdf/clj-pdf** — PDF generation library for Clojure

- Repository: https://github.com/clj-pdf/clj-pdf
- Stars: 699 · Forks: 98
- Language: Clojure
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/clj-pdf-clj-pdf

## A document is a vector, so the whole library is data

clj-pdf generates PDFs by calling the pdf function in the clj-pdf.core namespace with an input and an output. The input can be a vector containing the document or an input stream, in which case the forms are read sequentially from it. The output can be a string, treated as a file name, or an output stream. Inside the input sits a map of metadata followed by one or more elements, and each element must be a sequence starting with a keyword naming the element, or a string, which is treated as a paragraph. That is the entire core idea. There is no builder object, no fluent API and no separate template language, so a document is a value you can build with plain Clojure functions and pass straight into pdf.

## The smallest useful document

Here is the complete example from the project overview, which produces a PDF with a list, some phrases and a paragraph.

```clojure
(ns example
  (:use clj-pdf.core))

(pdf
  [{}
   [:list {:roman true}
          [:chunk {:style :bold} "a bold item"]
          "another item"
          "yet another item"]
   [:phrase "some text"]
   [:phrase "some more text"]
   [:paragraph "yet more text"]]
  "doc.pdf")
```

Three things are visible in those few lines. The metadata map is empty, so the document has no title or author. The list is a keyword followed by a map of options and then its items, where a chunk carries its own style and a bare string is a paragraph-level item. And a phrase groups text runs, which is what you use when you want part of a paragraph to be bold while the rest is not. The overview links a rendered example PDF and an image of the output, which is a fast way to see the shape of the result before writing any code.

## Sequences expand, so a document can be generated by a loop

Because the elements are ordinary sequences, a document can be produced by a comprehension rather than by hand. The overview shows this form.

```clojure
(pdf
 [{}
   (for [i (range 3)]
    [:paragraph (str "item: " i)])]
 "doc.pdf")
```

The project states that this is equivalent to spelling out the three paragraph elements one by one. That equivalence is the reason the library is pleasant to use in a Clojure codebase, since a document can be assembled from a query result without an intermediate representation. The same point carries into the styling layer, where the options are described as a stylesheet applied to element and chunk attributes, with named styles for fonts, colors, alignment and spacing, and the same style map can be attached at document, section or chunk level.

## Writing your own element helpers

The project says plainly that since clj-pdf uses regular Clojure vectors you can easily add your own helper functions. The example given is a pdf-table helper, which takes column widths and rows and produces the format the library expects.

```clojure
(defn pdf-table [column-widths & rows]
  (into
    [:pdf-table column-widths]
    (map (partial map (fn [element] [:pdf-cell element])) rows)))

(pdf-table
  [10 20 15]
  ["foo" [:chunk {:style :bold} "bar"] [:phrase "baz"]]
  ["foo" "foo" "foo"]
  ["foo" "foo" "foo"])
```

The raw form it generates puts a width vector and then one sequence per row, where a cell is either a plain value or a vector of elements. The helper wraps every cell in a pdf-cell vector, which is the kind of mechanical detail that is tedious to write by hand in every document. There is also a separate PDF table element, listed alongside the regular table and the table cell elements in the document element reference, so the library distinguishes between a table that flows with the page content and one laid out as a PDF table.

## Templating with anchors filled from maps

For the common case of a report generated from rows in a database, the library offers a template macro. It produces a function that accepts a sequence of maps and applies the template to each item, and the overview says it is primarily meant to complement working with clojure.java.jdbc, which returns sequences of maps representing table rows. The template uses a dollar sign to mark the anchors, and those get swapped with the values from the map with the corresponding keys. So a template is a document vector with $name and $occupation placeholders, and a sequence of maps is the data that fills them in. The overview calls these options rudimentary, which is honest, but for the report-from-a-query case they remove exactly the boilerplate that a hand-rolled template engine would exist to remove.

## The element vocabulary is long, and charting is included

The document element reference in the overview runs to more than twenty entries, and that list is the best catalogue of what the library can produce. It includes the document structure elements, such as section, anchor, chapter, chunk, spacer, pagebreak, multi-column, and the clear double page behaviour for facing pages. It includes the text elements, paragraph, phrase, string, heading, subscript and superscript, plus list and table with their cell variants. It includes graphics, line, image and SVG. And it includes metadata, where the font entry can be a built-in face or a custom TTF, with a separate section on using custom TTF fonts. Charting is documented as its own group, with bar chart, line chart and pie chart. The scale of the vocabulary is the trade-off here: it covers far more than a report needs, and the overview is where you find out which of those elements exist at all.

## Merging documents with collate

When you need several documents in one file, the clj-pdf.core/collate function takes an output stream followed by two or more documents, and the documents can be an InputStream, a file name, a URL or a byte array. The overview shows the byte array form, with three documents built into ByteArrayOutputStream instances and then written out through a FileOutputStream, and a second form that passes an options map with keys such as title, author, creator, keywords, orientation, size and subject, with the note that all keys in the options map are optional. The collate call is what you want for producing one file from separate sections that were generated independently, for example a report body plus an appendix produced by different code paths.

## Where clj-pdf stands right now

The release history here needs reading carefully, because it is unusual. The two listed releases are 1.11.24, marked as the stable release, dated 2015-01-17, and 1.11.26, marked as the latest release before major breaking changes for background color, dated 2015-01-19. The last push to the repository was on 2026-09-30, so the source tree has moved on from those tags by a very long way without producing new tagged releases in the listing. The project is available as a Maven artifact from Clojars, and the repository carries a project.clj, a CHANGELOG.md, a LICENSE file, a circle.yml for continuous integration, a coveralls coverage badge, a dev/ directory, docs/ and a test/ directory. The licence is worth checking rather than assuming, since GitHub could not classify it for this repository and the LICENSE file is the authority.

## Conclusion

clj-pdf fits a Clojure developer who already has their data as maps and wants a PDF out of it without a template language, since a document is an ordinary vector and a row is a chunk, so the whole thing composes with the rest of a Clojure program. It does not fit a project that needs the release tags to mean anything recent, because the two listed tags are 1.11.24 and 1.11.26 from January 2015, with 1.11.26 marked as the latest release before major breaking changes for background colour, while the last push to the repository was on 2026-09-30, so the source tree has moved on without new tagged releases in the listing. Before you depend on it, check the CHANGELOG.md and the version in project.clj against what you need, especially the background colour behaviour, and confirm which licence the LICENSE file grants, since GitHub could not classify it for this repository.

## FAQ

### How do I generate a PDF with clj-pdf?

Call the pdf function from clj-pdf.core with an input and an output. The input is a vector holding a metadata map followed by document elements, and the output is a file name string or an output stream. Each element is a sequence starting with a keyword naming the element, or a string treated as a paragraph.

### How does clj-pdf templating work?

The template macro generates a function that accepts a sequence of maps and applies the template to each one. A dollar sign marks the anchors, which are replaced with the values from the map with the matching key, and the overview says this is mainly meant to complement clojure.java.jdbc query results.

### Can I combine several PDFs into one with clj-pdf?

Yes, the clj-pdf.core/collate function takes an output stream followed by two or more documents, each of which can be an InputStream, a file name, a URL or a byte array. An optional options map can set the title, author, creator, keywords, orientation, size and subject.

### Which document elements does clj-pdf support?

The documented elements include section, anchor, chapter, chunk, spacer, pagebreak, multi-column, paragraph, phrase, string, heading, subscript, superscript, list, table, table cell, pdf-table, pdf-cell, line, image, SVG, plus metadata with fonts including custom TTF fonts, and charting with bar, line and pie charts.

## Sources

- [clj-pdf/clj-pdf on GitHub](https://github.com/clj-pdf/clj-pdf)
- [Issues](https://github.com/clj-pdf/clj-pdf/issues)
- [README](https://github.com/clj-pdf/clj-pdf/blob/master/README.md)
- [Releases](https://github.com/clj-pdf/clj-pdf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/clj-pdf-clj-pdf
