Typographic-quality PDF generation, in pure Elixir.
A document is an immutable struct threaded through a pipeline. Nothing is
written until export/1, so a document can be built, inspected and tested
before any bytes exist.
Tincture.new()
|> Tincture.page_size(:a4)
|> Tincture.set_font("Helvetica", 12)
|> Tincture.text_at(72, 700, "Hello")
|> Tincture.export()Coordinates
PDF user space has its origin at the bottom-left of the page, with y increasing upwards — the opposite of most screen coordinate systems. A y of 700 on A4 is near the top. All positions and sizes are in points (1/72 inch), so A4 is 595 x 842 and US Letter is 612 x 792.
Text
text_at/4 draws a single string at a point. For anything that needs to
wrap, use text_paragraph/6, which runs the typography engine: TeX
hyphenation and Knuth-Plass line breaking, the same global optimisation TeX
uses, rather than greedy first-fit.
rich = Tincture.Typography.RichText.from_plain(body, font: "Times-Roman", size: 11)
Tincture.text_paragraph(pdf, 72, 700, rich, 450,
align: :justified,
line_break: :optimal
)For multi-page documents with headers, footers and page numbering, see
Tincture.Layout.Template.
Fonts
The 14 standard PDF fonts are always available by name. TrueType and
OpenType fonts are embedded with register_ttf_font/4 and
register_otf_font/4, and are subsetted by default — only the glyphs
the document draws are embedded, which typically cuts an embedded font by
70-90%.
Architecture
Each layer is usable on its own; the one above it is a convenience.
flowchart TD
A["Tincture<br/>drawing, text, links, images"] --> B
B["Tincture.Layout<br/>boxes, tables, page templates"] --> C
C["Tincture.Typography<br/>hyphenation, line breaking, kerning"] --> D
D["Tincture.PDF<br/>document state, fonts, serialisation"] --> E
E["Tincture.Font<br/>TrueType, OpenType, CFF parsing"]Zero runtime dependencies
No Chrome, no wkhtmltopdf, no NIFs, no ports. The whole pipeline — font parsing, subsetting, hyphenation, line breaking, PDF serialisation — is Elixir and Erlang/OTP. It runs anywhere the BEAM runs.
Summary
Functions
Add a document bookmark pointing to a page number.
Add a new page and make it the current page.
Mark a block of drawing as an artifact: decoration, not meaning.
Add a cubic Bezier segment to the current path.
Add a checkbox.
Add a choice field — a dropdown by default, or a list box.
Draw a circle. See rectangle/6 for the paint modes.
Set the clipping path from the current path.
Set the clipping path from the current path using the even-odd rule.
Encrypt the document with AES-256.
Export a PDF struct to a binary.
Fill the current path.
Fill the current path using the even-odd rule.
Draw a JPEG image at X/Y with the given width and height.
Draw a PNG image at X/Y with the given width and height.
Draw a stroked line between two points.
Add a line segment from current path point to X/Y.
Add a clickable link over a rectangular region of the current page.
Move current path point to X/Y.
Create a new PDF state struct.
Set the current page size.
Every PDF/A violation Tincture can detect in this document.
Add a push button.
Add a radio button group: one field, several buttons, at most one selected.
Draw a rectangle.
Register an embedded OpenType font by name from an .otf file path.
Register an embedded OpenType font by name from an .otf file path with embedding options.
Register an embedded TrueType font by name from a .ttf file path.
Register an embedded TrueType font by name from a .ttf file path with embedding options.
Restore graphics state.
Export and write a PDF to disk.
Save graphics state.
Set dash pattern and phase.
Set fill color using normalized RGB values (0..1).
Set the current font name and size.
Set the document's natural language, as a BCP 47 tag such as "en-GB".
Set line cap style: 0 butt, 1 round, 2 projecting square.
Set line join style: 0 miter, 1 round, 2 bevel.
Set stroke line width.
Set document metadata fields (for example: :title, :author, :keywords).
Set miter limit for stroked joins.
Switch the current page by page number (1-based).
Declare a PDF/A conformance level, for a document that has to outlive its software.
Set stroke color using normalized RGB values (0..1).
Sign a signature field, so the document can be shown not to have changed.
Add a signature field: a place for someone to sign.
Stroke the current path.
Tag a block of content with its logical role, making the document accessible.
Whether the document carries logical structure.
Place text at X/Y coordinates on the current page.
Place text at X/Y coordinates rotated by the given angle in degrees.
Place rotated text at X/Y, splitting glyph runs across fallback fonts when needed.
Place text at X/Y coordinates, splitting glyph runs across fallback fonts when needed.
Add a fillable text field.
Draw text at X/Y and make it clickable.
Render a laid-out paragraph of rich text at X/Y origin using typography layout.
Types
@type paint() :: Tincture.PDF.paint()
How a shape is painted. See rectangle/6.
@type paragraph_option() :: {:align, :left | :center | :right | :justified} | {:line_height, number()} | {:line_break, :greedy | :optimal} | {:optimal_cost_model, :quadratic | :box_glue} | {:justify_max_space_multiplier, number() | :infinity} | {:justify_min_space_multiplier, number()} | {:widow_penalty, number()} | {:orphan_penalty, number()} | {:hyphen_penalty, number()} | {:fitness_class_penalty, number()} | {:consecutive_hyphen_penalty, number()} | {:rotate, number()} | {:fallback_fonts, [String.t()]} | {:bidi, :off | :basic} | {:shaping, :off | :latin_ligatures | :gsub_ligatures} | {:kerning, :off | :gpos}
@type rich_text() :: Tincture.Typography.RichText.t()
Functions
@spec add_bookmark(Tincture.PDF.t(), String.t(), pos_integer()) :: Tincture.PDF.t()
Add a document bookmark pointing to a page number.
@spec add_page(Tincture.PDF.t()) :: Tincture.PDF.t()
Add a new page and make it the current page.
@spec artifact(Tincture.PDF.t(), (Tincture.PDF.t() -> Tincture.PDF.t())) :: Tincture.PDF.t()
Mark a block of drawing as an artifact: decoration, not meaning.
Rules, borders, background shading, page furniture — content a sighted reader parses as visual structure and a screen reader should skip entirely. In a tagged document every operator must be either tagged or marked as an artifact; anything that is neither is read out as stray noise and fails conformance.
pdf
|> Tincture.artifact(fn pdf ->
pdf
|> Tincture.set_stroke_color({0.85, 0.86, 0.88})
|> Tincture.line(50, 700, 545, 700)
|> Tincture.stroke()
end)Tincture.Layout.Table.render/6 does this for its own borders when it is
tagging.
@spec bezier( Tincture.PDF.t(), number(), number(), number(), number(), number(), number() ) :: Tincture.PDF.t()
Add a cubic Bezier segment to the current path.
@spec checkbox(Tincture.PDF.t(), number(), number(), number(), String.t(), keyword()) :: Tincture.PDF.t()
Add a checkbox.
Checkboxes are square, so a single size gives both dimensions.
Options
:value—truefor checked. Defaults tofalse.:read_only,:required,:no_export— field flags.:tooltip,:border,:page— as fortext_field/7.
Examples
Tincture.checkbox(pdf, 72, 700, 14, "accept_terms", value: false)
@spec choice_field( Tincture.PDF.t(), number(), number(), number(), number(), String.t(), keyword() ) :: Tincture.PDF.t()
Add a choice field — a dropdown by default, or a list box.
Options
:options— required, the list of choices.:value— the initially selected choice.:dropdown—true(default) for a combo box,falsefor a list box.:editable— allow a value outside the list. Defaults tofalse.:sort— present the options sorted. Defaults tofalse.:read_only,:required,:no_export,:tooltip,:border,:page,:font,:size— as fortext_field/7.
Examples
Tincture.choice_field(pdf, 72, 700, 200, 24, "country",
options: ["Italy", "Norway", "Japan"],
value: "Italy"
)
@spec circle(Tincture.PDF.t(), number(), number(), number(), paint()) :: Tincture.PDF.t()
Draw a circle. See rectangle/6 for the paint modes.
@spec clip(Tincture.PDF.t()) :: Tincture.PDF.t()
Set the clipping path from the current path.
@spec clip_even_odd(Tincture.PDF.t()) :: Tincture.PDF.t()
Set the clipping path from the current path using the even-odd rule.
@spec encrypt( Tincture.PDF.t(), keyword() ) :: Tincture.PDF.t()
Encrypt the document with AES-256.
Uses the standard security handler at revision 6 (/V 5 /R 6), the
encryption defined by PDF 2.0. Readers from Acrobat X (2010) onward support
it, as do macOS Preview, Chrome, Firefox, PDFBox, Ghostscript and qpdf.
What this protects
A user password is real encryption. Without it the document cannot be read at all.
Tincture.encrypt(pdf, user_password: "hunter2")An owner password on its own is not. The document is encrypted under the
empty user password, so any reader can open it; the permissions below are
advisory, and qpdf --decrypt strips them without knowing the password.
This is how the PDF format works, not a limitation of this implementation.
# Anyone can open this; the restriction is a request, not a control.
Tincture.encrypt(pdf, owner_password: "master", permissions: [:print])Use an owner password to express intent, and a user password when the content genuinely must not be read.
Options
:user_password— required to open the document. Defaults to"".:owner_password— grants full rights. Defaults to the user password.:permissions— what a viewer is asked to allow. Defaults to everything. One or more of:print,:modify,:copy,:annotate,:fill_forms,:extract_for_accessibility,:assemble,:print_high_quality.:encrypt_metadata— encrypt document metadata too. Defaults totrue.
Examples
pdf
|> Tincture.text_at(72, 700, "Confidential")
|> Tincture.encrypt(user_password: "hunter2", permissions: [:print])
|> Tincture.export()Calling this twice replaces the earlier settings; the document is encrypted once, at export.
@spec export( Tincture.PDF.t(), keyword() ) :: binary()
Export a PDF struct to a binary.
Options
:enforce— whether to refuse a document that declares a PDF/A level and breaks it. Defaults totrue, and only ever applies to a document that calledset_pdf_a/2; nothing is checked otherwise.
A conformance claim is written into the file's metadata, so exporting a document that declares PDF/A and violates it produces a file that lies about itself — and nothing discovers that until someone tries to rely on it. Rather than emit one, this raises and names what is wrong.
Pass enforce: false to export anyway. The claim stays in the file, so the
result asserts a conformance it does not have; the violations are logged as a
warning. It exists for iterating, not for shipping.
Tincture.export(pdf) # refuses a false claim
Tincture.export(pdf, enforce: false) # exports it, and says soSee pdf_a_violations/1 to inspect without exporting.
@spec fill(Tincture.PDF.t()) :: Tincture.PDF.t()
Fill the current path.
@spec fill_even_odd(Tincture.PDF.t()) :: Tincture.PDF.t()
Fill the current path using the even-odd rule.
@spec image_jpeg(Tincture.PDF.t(), number(), number(), number(), number(), Path.t()) :: Tincture.PDF.t()
Draw a JPEG image at X/Y with the given width and height.
@spec image_png(Tincture.PDF.t(), number(), number(), number(), number(), Path.t()) :: Tincture.PDF.t()
Draw a PNG image at X/Y with the given width and height.
@spec line(Tincture.PDF.t(), number(), number(), number(), number(), paint()) :: Tincture.PDF.t()
Draw a stroked line between two points.
@spec line_to(Tincture.PDF.t(), number(), number()) :: Tincture.PDF.t()
Add a line segment from current path point to X/Y.
@spec link( Tincture.PDF.t(), number(), number(), number(), number(), Tincture.PDF.link_target() | String.t() ) :: Tincture.PDF.t()
Add a clickable link over a rectangular region of the current page.
target is either a URL string (or {:url, url}) for an external link, or
{:page, page_number} for an internal cross-reference.
Coordinates are in PDF user space, with the origin at the bottom-left of the
page — the same space text_at/4 and rectangle/5 use. The rectangle is
given as x, y, width, height; corners are normalised, so a negative
width or height still produces a valid region rather than a dead link.
Linking does not draw anything. Use text_link/5 to draw text and make it
clickable in one call, or pair this with your own rectangle/5 and
set_fill_color/2 calls.
Options
:page— which page to attach the link to. Defaults to the current page.:border—:none(default) or{horizontal, vertical, width}. The default suppresses the black rectangle most viewers would otherwise draw around every link.
Examples
pdf
|> Tincture.link(72, 700, 120, 14, "https://elixir-lang.org")
# Internal cross-reference to page 3.
Tincture.link(pdf, 72, 680, 120, 14, {:page, 3})
# Attach a link to a page other than the current one.
Tincture.link(pdf, 72, 660, 120, 14, "https://hex.pm", page: 1)
@spec link( Tincture.PDF.t(), number(), number(), number(), number(), Tincture.PDF.link_target() | String.t(), keyword() ) :: Tincture.PDF.t()
@spec move_to(Tincture.PDF.t(), number(), number()) :: Tincture.PDF.t()
Move current path point to X/Y.
@spec new() :: Tincture.PDF.t()
Create a new PDF state struct.
@spec page_size(Tincture.PDF.t(), page_size()) :: Tincture.PDF.t()
Set the current page size.
@spec pdf_a_violations(Tincture.PDF.t()) :: [Tincture.PDF.Archival.violation()]
Every PDF/A violation Tincture can detect in this document.
Empty for a document that declares no level. Not a conformance check —
Tincture sees the document it built, not the file a validator sees, and
PDF/A has requirements no library can settle for you. Validate the output
with verapdf --flavour 2b.
@spec push_button( Tincture.PDF.t(), number(), number(), number(), number(), String.t(), keyword() ) :: Tincture.PDF.t()
Add a push button.
A push button holds no value — it exists to do something when clicked — so
:action is required.
Options
:action— required. One of::reset— clear every field in the form.{:url, url}— open a URL.{:submit, url}— submit the form's values to a URL.
:label— the caption drawn on the button face.:read_only,:required,:no_export— field flags.:tooltip,:border,:font,:size,:page— as fortext_field/7.
Examples
pdf
|> Tincture.push_button(72, 100, 90, 24, "submit", label: "Send",
action: {:submit, "https://example.com/apply"})
|> Tincture.push_button(180, 100, 90, 24, "reset", label: "Clear",
action: :reset)
@spec radio_group(Tincture.PDF.t(), String.t(), [keyword()], keyword()) :: Tincture.PDF.t()
Add a radio button group: one field, several buttons, at most one selected.
A radio group is the one field type that is not a single widget. The value lives on the group and each button is its own widget, so buttons are given as a list rather than added one at a time — a lone radio button is not a meaningful thing to place.
Each button needs :value, :x, :y and :size. The :value is what the
field takes when that button is chosen, and is what appears in the filled
document. "Off" is reserved by the specification for "nothing selected" and
is rejected as a button value.
Options
:selected— the:valueof the initially selected button. Defaults to none selected.:allow_deselect— let clicking the selected button turn it off again. Defaults tofalse, which is what a radio group usually means.:read_only,:required,:no_export— field flags.:tooltip,:border,:font,:size— as fortext_field/7.:page— the default page for buttons that do not name their own.
Examples
Tincture.radio_group(pdf, "delivery", [
[value: "standard", x: 72, y: 700, size: 12],
[value: "express", x: 72, y: 680, size: 12],
[value: "collect", x: 72, y: 660, size: 12]
], selected: "standard")
@spec rectangle(Tincture.PDF.t(), number(), number(), number(), number(), paint()) :: Tincture.PDF.t()
Draw a rectangle.
Painting
paint selects how the shape is painted, and defaults to :stroke:
:stroke— outline only:fill— filled with the current fill colour:fill_even_odd— filled using the even-odd rule:fill_and_stroke— both:none— emit the path without painting, to be painted by a laterfill/1orstroke/1
A PDF path-painting operator also ends the path, so a shape can be painted
once. Calling fill/1 after a shape that already stroked itself does
nothing, because there is no longer a path to fill.
Examples
# A filled band across the top of the page.
pdf
|> Tincture.set_fill_color({0.06, 0.35, 0.55})
|> Tincture.rectangle(0, 746, 595, 96, :fill)
@spec register_otf_font(Tincture.PDF.t(), String.t(), Path.t()) :: Tincture.PDF.t()
Register an embedded OpenType font by name from an .otf file path.
@spec register_otf_font(Tincture.PDF.t(), String.t(), Path.t(), keyword()) :: Tincture.PDF.t()
Register an embedded OpenType font by name from an .otf file path with embedding options.
Takes the same options as register_ttf_font/4, including :subset, which
defaults to :used_text.
@spec register_ttf_font(Tincture.PDF.t(), String.t(), Path.t()) :: Tincture.PDF.t()
Register an embedded TrueType font by name from a .ttf file path.
@spec register_ttf_font(Tincture.PDF.t(), String.t(), Path.t(), keyword()) :: Tincture.PDF.t()
Register an embedded TrueType font by name from a .ttf file path with embedding options.
Options
:subset— how much of the font to embed. Defaults to:used_text.:used_text— embed only the glyphs the document actually draws. Typically cuts an embedded font by 70-90%.:ascii_basic— embed the printable ASCII range (32..126). Useful when you intend to edit the text later with an external tool.:none— embed the font verbatim.
Subsetted fonts carry the six-letter
/BaseFonttag the PDF specification requires (for exampleABCDEF+Inter). If a font cannot be subsetted safely, Tincture logs a warning and embeds it in full rather than producing a broken document.:enforce_embedding_permissions— whentrue, raise instead of warn if the font's OS/2fsTyperestricts embedding or subsetting. Defaults tofalse.
Examples
Tincture.new()
|> Tincture.register_ttf_font("Inter", "priv/fonts/Inter.ttf")
|> Tincture.set_font("Inter", 12)
# Embed the whole font, e.g. for a template others will edit.
Tincture.register_ttf_font(pdf, "Inter", path, subset: :none)
@spec restore_state(Tincture.PDF.t()) :: Tincture.PDF.t()
Restore graphics state.
@spec save(Tincture.PDF.t(), Path.t(), keyword()) :: :ok | {:error, term()}
Export and write a PDF to disk.
Takes the same options as export/2.
@spec save_state(Tincture.PDF.t()) :: Tincture.PDF.t()
Save graphics state.
@spec set_dash(Tincture.PDF.t(), [number()], number()) :: Tincture.PDF.t()
Set dash pattern and phase.
@spec set_fill_color(Tincture.PDF.t(), {number(), number(), number()}) :: Tincture.PDF.t()
Set fill color using normalized RGB values (0..1).
@spec set_font(Tincture.PDF.t(), String.t(), number()) :: Tincture.PDF.t()
Set the current font name and size.
@spec set_language(Tincture.PDF.t(), String.t()) :: Tincture.PDF.t()
Set the document's natural language, as a BCP 47 tag such as "en-GB".
Without it a screen reader has to guess which language to pronounce the text as, so this is a requirement for an accessible document rather than a nicety.
@spec set_line_cap(Tincture.PDF.t(), 0 | 1 | 2) :: Tincture.PDF.t()
Set line cap style: 0 butt, 1 round, 2 projecting square.
@spec set_line_join(Tincture.PDF.t(), 0 | 1 | 2) :: Tincture.PDF.t()
Set line join style: 0 miter, 1 round, 2 bevel.
@spec set_line_width(Tincture.PDF.t(), number()) :: Tincture.PDF.t()
Set stroke line width.
@spec set_metadata(Tincture.PDF.t(), map() | keyword()) :: Tincture.PDF.t()
Set document metadata fields (for example: :title, :author, :keywords).
@spec set_miter_limit(Tincture.PDF.t(), number()) :: Tincture.PDF.t()
Set miter limit for stroked joins.
@spec set_page(Tincture.PDF.t(), pos_integer()) :: Tincture.PDF.t()
Switch the current page by page number (1-based).
@spec set_pdf_a(Tincture.PDF.t(), atom()) :: Tincture.PDF.t()
Declare a PDF/A conformance level, for a document that has to outlive its software.
PDF/A (ISO 19005) is what records-retention policies specify. It is mostly a set of constraints rather than new capability, and this adds the three things a document cannot be valid without: an sRGB output intent, so device colour has a defined meaning; XMP metadata carrying the conformance claim; and a file identifier.
pdf
|> Tincture.set_pdf_a(:a2b)
|> Tincture.set_metadata(title: "Annual report 2026")Levels
:a2b— basic. Visual reproduction is preserved.:a2u— as:a2b, and all text has a Unicode mapping, so it can be extracted and searched.:a2a— as:a2u, and the document is tagged. Combine withtag/4andset_language/2.
:a3b, :a3u and :a3a select part 3, which differs only in permitting
arbitrary embedded files.
What this does not do
Declaring a level does not enforce it. PDF/A also requires every font to be embedded — so the standard 14 are not usable, since they are referenced by name rather than embedded — and forbids encryption. Tincture does not refuse to build a document that breaks those rules, because the check belongs to a validator that can see the whole file.
Validate the result. veraPDF is free:
verapdf --flavour 2b out.pdfColour
The output intent is sRGB, built into the library rather than read from the system, so a document is reproducible anywhere. Any RGB colour you set is therefore interpreted as sRGB. CMYK is not covered by this output intent and would need its own.
@spec set_stroke_color(Tincture.PDF.t(), {number(), number(), number()}) :: Tincture.PDF.t()
Set stroke color using normalized RGB values (0..1).
@spec sign(Tincture.PDF.t(), String.t(), keyword()) :: Tincture.PDF.t()
Sign a signature field, so the document can be shown not to have changed.
Place the field first with signature_field/7, then sign it:
pdf
|> Tincture.signature_field(50, 120, 220, 48, "signature")
|> Tincture.sign("signature",
private_key: key,
certificate: certificate,
reason: "I approve this document",
location: "Sheffield"
)Unlike everything else in Tincture, this cannot take effect until the file
exists: a signature covers the finished bytes, including where every object
landed. So it is applied during export/2 — the document merely records the
intent. See Tincture.PDF.Sign for how that works.
Options
:private_key— required. A decoded key, as:public_key.pem_entry_decode/1returns.:certificate— required. The signer's certificate, in DER.:chain— intermediate certificates in DER, for a verifier that does not already hold them. Defaults to[].:digest—:sha256(default),:sha384or:sha512.:signing_time— aDateTime. Defaults to now. Pass one explicitly to make output reproducible.:name,:reason,:location,:contact— shown by the reader.:reserved_bytes— space set aside for the signature. Defaults to 16 kB, enough for RSA-4096 with a chain. Export raises rather than truncates if the signature does not fit.
Reading a key and certificate
[key_entry] = :public_key.pem_decode(File.read!("key.pem"))
key = :public_key.pem_entry_decode(key_entry)
[{:Certificate, certificate, _}] = :public_key.pem_decode(File.read!("cert.pem"))What a signature here does and does not prove
It proves the document has not changed since it was signed, and who signed it — as far as the certificate can be trusted. It does not prove when: there is no timestamp authority, so the time is the signing machine's own claim. Long-term validation needs an RFC 3161 timestamp, which Tincture does not yet produce.
Signing one document twice, or signing after changing it, needs incremental updates, which Tincture also does not yet produce — so one signature per document.
@spec signature_field( Tincture.PDF.t(), number(), number(), number(), number(), String.t(), keyword() ) :: Tincture.PDF.t()
Add a signature field: a place for someone to sign.
This reserves the field. Tincture does not yet apply a digital signature —
that needs a /ByteRange computed over the finished file — so the field is
left unsigned for a viewer to fill.
Options
:read_only,:required,:no_export— field flags.:tooltip,:border,:page— as fortext_field/7.
Examples
Tincture.signature_field(pdf, 72, 120, 220, 48, "signature",
tooltip: "Sign here")
@spec stroke(Tincture.PDF.t()) :: Tincture.PDF.t()
Stroke the current path.
@spec tag(Tincture.PDF.t(), atom(), keyword(), (Tincture.PDF.t() -> Tincture.PDF.t())) :: Tincture.PDF.t()
Tag a block of content with its logical role, making the document accessible.
An untagged PDF is a picture of a document: the bytes record where each glyph sits and nothing records what it means, so a screen reader has no headings to navigate by, no table structure, and no reliable reading order. Tagging adds that layer — a structure tree in the catalog, and marked content in the page linking the drawn operators to it.
Everything drawn inside fun belongs to the element:
pdf
|> Tincture.set_language("en-GB")
|> Tincture.tag(:document, fn pdf ->
pdf
|> Tincture.tag(:h1, fn pdf ->
Tincture.text_at(pdf, 50, 760, "Quarterly report")
end)
|> Tincture.tag(:p, fn pdf ->
Tincture.text_at(pdf, 50, 730, "Revenue rose by nine per cent.")
end)
end)Nesting is how structure is expressed, so a table is a :table containing
:tr containing :th and :td:
Tincture.tag(pdf, :table, fn pdf ->
Tincture.tag(pdf, :tr, fn pdf ->
pdf
|> Tincture.tag(:th, [scope: :column], &Tincture.text_at(&1, 50, 700, "Region"))
|> Tincture.tag(:th, [scope: :column], &Tincture.text_at(&1, 200, 700, "Revenue"))
end)
end)Tags
Container tags group other elements and wrap no content of their own:
:document, :part, :article, :section, :div, :table, :thead,
:tbody, :tfoot, :tr, :list, :list_item, :toc, :index.
Content tags bracket what is drawn: :p, :h1 to :h6, :heading, :th,
:td, :label, :list_body, :caption, :figure, :formula, :quote,
:block_quote, :code, :note, :reference, :span, :link,
:toc_item.
Options
:alt— alternative text. Required in practice for:figure: an image with no alt text is invisible to a screen reader.:actual_text— the text this content really represents, for when the glyphs are not the words (a ligature, a decorative capital).:lang— a BCP 47 tag, when this element differs from the document.:title— a human-readable title for the element.:scope—:row,:columnor:both. Only valid on:th, and what tells a reader which cells a header governs.
What this does and does not give you
It produces the structure: /StructTreeRoot, marked content, the parent
tree, /MarkInfo, /Lang, alt text and table scope. That is what assistive
technology reads.
It does not certify PDF/UA conformance. That is a validation exercise against a checker such as veraPDF or PAC, and it imposes further rules — on fonts, colour contrast, and metadata — that a library cannot enforce on your behalf. Tag your documents and then verify them.
@spec tagged?(Tincture.PDF.t()) :: boolean()
Whether the document carries logical structure.
@spec text_at(Tincture.PDF.t(), number(), number(), String.t()) :: Tincture.PDF.t()
Place text at X/Y coordinates on the current page.
@spec text_at_rotated(Tincture.PDF.t(), number(), number(), number(), String.t()) :: Tincture.PDF.t()
Place text at X/Y coordinates rotated by the given angle in degrees.
@spec text_at_rotated_with_fallback( Tincture.PDF.t(), number(), number(), number(), String.t(), [String.t()] ) :: Tincture.PDF.t()
Place rotated text at X/Y, splitting glyph runs across fallback fonts when needed.
@spec text_at_rotated_with_fallback( Tincture.PDF.t(), number(), number(), number(), String.t(), [String.t()], keyword() ) :: Tincture.PDF.t()
@spec text_at_with_fallback(Tincture.PDF.t(), number(), number(), String.t(), [ String.t() ]) :: Tincture.PDF.t()
Place text at X/Y coordinates, splitting glyph runs across fallback fonts when needed.
@spec text_at_with_fallback( Tincture.PDF.t(), number(), number(), String.t(), [String.t()], keyword() ) :: Tincture.PDF.t()
@spec text_field( Tincture.PDF.t(), number(), number(), number(), number(), String.t(), keyword() ) :: Tincture.PDF.t()
Add a fillable text field.
The field is a widget on the current page and an entry in the document's
interactive form. name addresses the field's value in the filled document,
so it must be unique.
Options
:value— the initial value. Defaults to empty.:size— font size. Defaults to0, meaning auto-size: the viewer fits the text to the box, which is usually what you want for a field whose height you chose.:font— defaults to"Helvetica".:multiline— allow line breaks. Defaults tofalse.:password— mask the value as it is typed. Defaults tofalse.:max_length— maximum characters accepted.:read_only,:required,:no_export— field flags, allfalseby default.:tooltip— hover text.:border—:none(default) or{horizontal, vertical, width}.:page— which page to place the widget on. Defaults to the current page.
Examples
pdf
|> Tincture.text_field(72, 700, 300, 24, "full_name", tooltip: "Your full name")
|> Tincture.text_field(72, 640, 300, 80, "notes", multiline: true)
@spec text_link( Tincture.PDF.t(), number(), number(), String.t(), Tincture.PDF.link_target() | String.t() ) :: Tincture.PDF.t()
Draw text at X/Y and make it clickable.
The link rectangle is measured from the text using the current font, so it
covers exactly the drawn glyphs. Takes the same options as link/7, plus:
:color— an{r, g, b}fill colour to draw the text in. The colour change is wrapped in a save/restore of the graphics state, so it does not leak into later drawing. Defaults to leaving the colour alone, so links are not silently recoloured.
Examples
pdf
|> Tincture.set_font("Helvetica", 12)
|> Tincture.text_link(72, 700, "Elixir", "https://elixir-lang.org")
# Conventional blue link.
Tincture.text_link(pdf, 72, 680, "Hex", "https://hex.pm", color: {0.0, 0.3, 0.8})
@spec text_link( Tincture.PDF.t(), number(), number(), String.t(), Tincture.PDF.link_target() | String.t(), keyword() ) :: Tincture.PDF.t()
@spec text_paragraph(Tincture.PDF.t(), number(), number(), rich_text(), number(), [ paragraph_option() ]) :: Tincture.PDF.t()
Render a laid-out paragraph of rich text at X/Y origin using typography layout.