What Tincture would need to be a defensible choice for enterprise document generation, and roughly in what order.
Nothing here is assumed. Where something is missing it was checked against the source; where something is claimed done it was checked against a validator. Partial support is said to be partial, because "we have forms" and "we have the forms you need" are different claims.
At a glance
Done and independently verified
| Accessibility — PDF/UA-1 | veraPDF, 106/106 rules (1) |
| Archival — PDF/A-2b, 2u, 2a | veraPDF, up to 153/153 rules (2) |
| Digital signatures | openssl cms -verify (3) |
| Telemetry | (9) |
Next, roughly in order
- Transparency and shading — small, self-contained, immediately visible.
- Benchmarks against alternatives — there is still no answer to "how fast is it" relative to ChromicPDF or elixir-pdf.
- Object and cross-reference streams — file size; content streams are written uncompressed today.
- Forms completed — appearance streams for text and choice fields, which would also let signed and archival documents carry form fields.
- Signature timestamping and incremental updates — proof of when, and more than one signature per document.
- Colour beyond RGB — spot inks and ICC spaces, for print production.
- Complex scripts — the largest effort, and the narrowest audience unless you are targeting those markets.
- Reading PDFs — probably a sibling library.
Where it stands today
Solid: text and vector drawing, TrueType/OpenType embedding with subsetting on by default, the typography engine (TeX hyphenation, Knuth-Plass line breaking, GPOS kerning, GSUB ligatures), page templates with pagination, tables, JPEG and PNG images with alpha, interactive forms with every field type, AES-256 encryption, tagged PDF for accessibility, PDF/A archival output, digital signatures, telemetry. No required runtime dependencies. 1,224 tests, 89% coverage, clean Credo and Dialyzer, CI on four Elixir versions.
That covers invoices, statements, reports, letters and contracts — documents a person reads, keeps, and has to be able to rely on. What follows is what it does not yet cover.
1. Accessibility — tagged PDF (PDF/UA)
Status: verified compliant. examples/output/accessible.pdf passes
veraPDF 1.30.2 against the PDF/UA-1 profile — 106 of 106 rules, 1701 of 1701
checks, isCompliant="true".
Reproduce it with:
verapdf --flavour ua1 examples/output/accessible.pdfCompliance belongs to a document, not a library: Tincture gives you the tools to produce a conforming file, and whether yours conforms depends on how you tag it. What the library guarantees is that correct usage is not blocked by a defect in the output — which is what validation established, by finding three.
Tincture.tag/4 produces the structure tree, marked content around the
operators that draw each element, the parent tree linking them, /MarkInfo,
/Lang, alternative text and /Scope on header cells. That is the layer a
screen reader reads, and it is what Section 508, EN 301 549 and equivalent
rules are asking for.
What remains:
- Tagging the remaining layout helpers.
Layout.Table.render/6now emits its own structure.Layout.TemplateandLayout.Boxdo not. - PAC. Only veraPDF has been used. PAC applies some checks veraPDF does not.
/Tabs /Son pages, so tab order follows structure rather than annotation order.- Automatic alt text prompting: nothing forces a
:figureto carry:alt, and a figure without it is invisible to a reader.
2. Archival — PDF/A
Status: verified compliant. examples/output/archival.pdf passes veraPDF
1.30.2 at PDF/A-2b, PDF/A-2u and PDF/A-2a — and PDF/UA-1 at the same time,
since 2a is the accessible archival level and subsumes the tagging requirement.
verapdf --flavour 2a examples/output/archival.pdf
verapdf --flavour ua1 examples/output/archival.pdfTincture.set_pdf_a/2 adds what archival validity needs: an sRGB output
intent, XMP carrying the conformance claim, and a file identifier. The ICC
profile is built by Tincture.PDF.ICC from published sRGB constants rather
than read from the system or shipped from elsewhere, so a document is
reproducible on any machine.
Output is deterministic: the file identifier is derived from the content and the profile carries no creation date, so an archived file can be checked against a rebuild.
What remains:
Enforcement.Done.export/2refuses to write a conformance claim into a document that breaks one, naming every violation. SeeTincture.PDF.Archival. It remains a partial check by construction — Tincture sees the document it built, not the file a validator sees — so passing it means "nothing Tincture knows of is wrong", not "this is PDF/A".- PDF/A-1. Part 1 is stricter than part 2 and additionally requires a
/CIDSetfor every subset font, which Tincture deliberately does not emit — see the note under item 1. It also forbids transparency. - CMYK output intents. The built-in intent is sRGB. Print production targeting a CMYK condition needs its own profile, which means accepting one from the caller.
3. Digital signatures
Status: done, and verified against OpenSSL. Tincture.sign/3 produces a
detached PKCS#7 signature covering the whole file, in a /ByteRange a verifier
can reproduce. examples/output/signed.pdf verifies with:
openssl cms -verify -binary -inform DER -in <extracted> -content <byte range>OTP ships no CMS encoder, so Tincture.PDF.CMS builds the SignedData
structure in DER directly — still no third-party dependencies, since :crypto
and :public_key are OTP applications.
Signing breaks the pure-transformation model that holds everywhere else:
a signature covers the finished bytes including where objects landed, so
export/2 reserves space, measures the real offsets, signs, and patches the
result back without moving anything. See Tincture.PDF.Sign.
What remains:
- Timestamps (RFC 3161). A signature proves the document has not changed, and who signed it as far as the certificate is trusted. It does not prove when: the time is the signing machine's own claim. Long-term validation (PAdES B-LT, B-LTA) needs a timestamp authority, which means an HTTP client and therefore a dependency decision.
- Incremental updates, without which a document can carry only one signature and cannot be counter-signed or amended after signing.
- Signature appearances. The widget carries no appearance stream, so a signed field is invisible in a static render and a signed document is currently rejected by the PDF/A check for that reason. Signed archival documents (PAdES over PDF/A) need this.
- PAdES subfilters.
/adbe.pkcs7.detachedis the widely supported form;/ETSI.CAdES.detachedis what the European regulation names.
4. Colour beyond RGB
Status: :device_gray, :device_rgb and :device_cmyk only. No
/Separation, no /DeviceN, no ICC-based spaces.
Print production needs spot colours and calibrated colour. A brand colour that must match across a print run cannot be expressed as device CMYK.
Needs: /Separation for spot inks, /DeviceN for multi-ink, ICCBased for
calibrated spaces, and an overprint control (/ExtGState, item 5).
5. Transparency and shading
Status: absent. No /ExtGState, no /Shading, no /Pattern.
PNG alpha is supported via /SMask on images, but there is no way to set
opacity on drawn content, no blend modes, and no gradients. Any design-led
document — a report cover, a marketing page — will want at least gradients and
constant alpha.
Comparatively small and self-contained: /ExtGState for /CA and /ca,
shading types 2 and 3 for axial and radial gradients.
6. Forms, completed
Status: the field set is complete; appearances are half done. Text, checkbox, choice, radio group, push button and signature fields all exist, with reset, URL and submit actions.
Button-like fields (checkbox, radio, push button) carry generated /AP
appearance streams, so they render anywhere — printing, thumbnails,
server-side rasterising — not only in an interactive viewer.
Fields whose appearance is their typed value (text, choice) still rely on
/NeedAppearances true. Every mainstream viewer honours it, but a flattened
form — one converted to static content — needs those generated too, which means
laying out and clipping the value at export time.
Also missing: JavaScript actions for client-side validation, and per-field
appearance customisation (/MK border and background colours).
Field values can only use the standard 14 fonts, because /DA resolves against
the AcroForm resource dictionary. Referencing an embedded font there is
possible in principle and is not implemented.
7. Complex scripts
Status: bidi: :basic and GSUB ligature substitution only.
Arabic and the Indic scripts need contextual shaping — glyph selection that depends on neighbouring characters — plus mark positioning and cursive attachment. That means implementing the relevant parts of GSUB (types 5–8) and GPOS (types 3–6), which is a significant body of work and the reason HarfBuzz exists.
Vertical writing for CJK is a separate gap: no /WMode, no vertical metrics
from vhea/vmtx.
Worth being honest that this is the largest single item on the list, and that "supports Unicode" is not the same as "typesets Arabic correctly".
8. File size and streaming
Status: absent. No object streams, no cross-reference streams, no linearisation.
Every object is written uncompressed into a classic xref table. Object streams and xref streams (PDF 1.5, 2001) would meaningfully shrink documents with many small objects. Linearisation ("fast web view") lets a viewer render page one before the whole file arrives, which matters when documents are served over HTTP.
Separately, export/1 builds the entire document in memory. A streaming
export would bound memory for large documents — a thousand-page statement run
currently holds everything at once.
9. Operability
Status: absent.
Telemetry.Done. Three spans — document, page and font embed — with duration, byte sizes and document shape.:telemetryis optional, so the zero-required-dependency claim still holds. SeeTincture.Telemetry.- Benchmarks against alternatives. The benchmark suite guards against regression but does not compare with ChromicPDF or elixir-pdf, so there is no answer to "how fast is it".
- Memory profile. No measured figure for a large document.
Small, and disproportionately useful for adoption.
10. Reading PDFs
Status: absent — Tincture is write-only.
Merging documents, stamping an existing file, filling a form template someone else produced, extracting text: all common enterprise needs, none possible.
This is arguably a separate library. Listed because evaluators will ask, and "no" is a better answer than silence.
A note on the verified items
Compliance belongs to a document, not a library. Tincture provides the tools to produce a conforming file; whether yours conforms depends on how you use them. What validation established is narrower and more useful: that correct usage is not blocked by a defect in the output.
It was worth doing. Running veraPDF and OpenSSL against the examples found five
real defects that 1,200 tests had not — among them every content stream
declaring a /Length one byte too large, and table header /Scope written
where no reader would look for it. Each is recorded in
CHANGELOG.md.
Two of those were things that looked like constraints and turned out to be bugs:
links appeared to be forbidden in archival documents, and checkboxes likewise,
until it emerged that Tincture was omitting an annotation flag in one case and
over-setting /NeedAppearances in the other. A rule that bans working features
is a bug in the rule.
Contributing
Any of these is a reasonable place to start, and the ones with a clear
specification (shading, /ExtGState, object streams) are the most tractable
without deep PDF background. Open an issue before starting something large so
we can agree the shape.