Dagger
Search

pdf

rendering its pages, and reporting what it is made of, as a `dagger call`
instead of the usual hand-rolled Dockerfile with poppler-utils in it plus a
shell script to rescue the output.

The text path and the OCR path are different tools for different documents,
and this module is the one to try first. pdftotext reads the text layer a
PDF already carries — exact, instant, no recognition involved. A scan
carries no text layer at all, and that is the case the tesseract module
exists for; this module's job for a scan is to produce the page images OCR
runs on. Convert.Text returning nothing is exactly the signal to hand
Convert.Png's output to tesseract.

There is no official poppler container image, so the module assembles its
own the way tesseract and qemu do: a module-pinned Alpine plus
`apk add poppler-utils ttf-liberation`. The font is not decoration. Poppler
renders a PDF that names a base-14 font without embedding one — the usual
shape for anything not born as a scan — by asking fontconfig for a
substitute, and with no font installed there is nothing to substitute: the
page renders blank, recognition finds nothing, and the command exits 0.
Liberation is the family to install for it, being metric-compatible with
Helvetica, Times and Courier, so substituted text keeps the positions the
document was written with.

Assembling the image means two fetches, not one, and a network that cannot
reach the public internet has to be told about both. New's registry argument
moves the *image*; WithApkRepository, WithApkKey and WithApkAuth move the
*packages*, and are what an air-gapped run needs — a mirrored Alpine image
still runs `apk add` against dl-cdn.alpinelinux.org otherwise.

No cache directive appears anywhere in this module. Every function is a pure
transform of its inputs, so Dagger's 7-day default is correct — the same
posture as tesseract and kicad. (The directive is deliberately not spelled
out here: a `+key="value"` in doc-comment prose is stripped as a directive
and mangles the generated description.)

Three renderers sit behind the outputs, chosen per format rather than
uniformly. The raster family stays on pdftoppm because it alone offers -mono
and -gray, which is what the OCR path wants. The vector family — SVG, EPS and
PostScript — is pdftocairo's, that being the only one of the two that emits
them. HTML is pdftohtml's, which is not a renderer at all so much as a
re-layout, and shares no options with either.

Two of the outputs report where the text *is* rather than what it says, and
are pdftotext's as well: Bbox emits a box per word as XHTML, Tsv emits a row
per layout element as a table. They are the text-layer analogues of the
tesseract module's Hocr and Tsv — the same shape of answer, sourced from the
document's own coordinates rather than from recognition, so the boxes are exact
rather than estimated. That is the whole argument for reaching for this module
before that one even when geometry is what is wanted.

Four reports describe the document instead of converting it, one per tool:
Info is pdfinfo, Fonts is pdffonts, Metadata is `pdfinfo -meta` and Signatures
is pdfsig. Each returns its tool's own report as text. Parsing those into
structured values is deliberately out of scope — the formats are stable but
wide, and a caller who wants one field can read it out of these lines more
cheaply than this module can model all of them.

Two functions change the document's page structure rather than reading it or
drawing it: Split takes it apart a page per file, Merge concatenates several
into one. They are lossless where every conversion is not — the pages carry
their own objects across — and they are the pair with a limitation nothing
else here has: pdfseparate and pdfunite accept no password, so an encrypted
document has to be decrypted before either will touch it.

Two more pull out what the document *contains* rather than a conversion of it,
and both are routinely confused with a render. EmbeddedImages is pdfimages: the
image objects on the pages, at the resolution they are stored at, which for a
scan is better OCR input than any resolution Convert.Png can be asked for
because nothing was resampled to produce it. Attachments is pdfdetach: whole
files hung off the document's catalog, which is how an invoice carries its
machine-readable XML, and which no amount of rendering reaches.

File map (all `package main`, surfaced as one Dagger module):

- enums.go — ColorMode, LayoutMode and ImageFormat plus the tables
mapping them onto poppler's flags, and the internal raster- and
per-page-format tables.
- document.go — *Document, one bound PDF: its passwords, its page range,
and the four reports about it — what pdfinfo says, which fonts it needs,
its XMP metadata, and its signatures.
- convert.go — *Convert, the render options and the eleven outputs that
read them.
- pages.go — Split and Merge, the two page-structure operations. They
sit together rather than with their receivers because they are one
story: the page-naming contract one writes is the order the other
reads.
- extract.go — EmbeddedImages and Attachments, the two extractions. They
sit together because they are one story too: both return what the file
already carries, and both are reached for by mistake instead of a render
or instead of each other.

Installation

dagger install github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d

Entrypoint

Return Type
Pdf !
Arguments
NameTypeDefault ValueDescription
registryString !"docker.io"Container registry hosting the alpine image. This moves the image only: see WithApkRepository for where the packages installed onto it are fetched from.
alpineTagString !"3.24"Tag of the alpine image the toolchain is assembled on.
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string
func (m *MyModule) Example(registry string, alpineTag string) *dagger.Pdf  {
	return dag.
			Pdf(registry, alpineTag)
}
@function
def example(registry: str, alpinetag: str) -> dagger.Pdf:
	return (
		dag.pdf(registry, alpinetag)
	)
@func()
example(registry: string, alpineTag: string): Pdf {
	return dag
		.pdf(registry, alpineTag)
}

Types

Pdf 🔗

Pdf is the root namespace for every exported function in this module. It carries the image coordinates and the fonts the image is built with; Document hangs off it so the generated SDK surfaces conversion under dag.Pdf().Document(...).

container() 🔗

Container returns the assembled toolchain image. This is the escape hatch for everything this module does not wrap: poppler-utils ships thirteen binaries and eleven of them are wrapped here, so pdfattach and pdftops stay reachable via container with-exec — as do the flags of a wrapped tool that this module does not surface, pdffonts -subst and pdfdetach -savefile among them.

Return Type
Container !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string container
func (m *MyModule) Example(registry string, alpineTag string) *dagger.Container  {
	return dag.
			Pdf(registry, alpineTag).
			Container()
}
@function
def example(registry: str, alpinetag: str) -> dagger.Container:
	return (
		dag.pdf(registry, alpinetag)
		.container()
	)
@func()
example(registry: string, alpineTag: string): Container {
	return dag
		.pdf(registry, alpineTag)
		.container()
}

document() 🔗

Document binds one PDF to the toolchain.

The boundary input is a *dagger.File rather than a *dagger.Directory: a PDF resolves nothing relative to its own location, so one file is the whole unit of work however many pages it carries.

Return Type
Document !
Arguments
NameTypeDefault ValueDescription
sourceFile !-

The PDF to read or render.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string document --source file:path
func (m *MyModule) Example(registry string, alpineTag string, source *dagger.File) *dagger.PdfDocument  {
	return dag.
			Pdf(registry, alpineTag).
			Document(source)
}
@function
def example(registry: str, alpinetag: str, source: dagger.File) -> dagger.PdfDocument:
	return (
		dag.pdf(registry, alpinetag)
		.document(source)
	)
@func()
example(registry: string, alpineTag: string, source: File): PdfDocument {
	return dag
		.pdf(registry, alpineTag)
		.document(source)
}

merge() 🔗

Merge concatenates several PDFs into one, in the order they are given, and returns the result.

It hangs off the root rather than off Document because it takes several sources and no single one of them is “the” document: a bound document carries passwords and a page range, and neither has any meaning for a merge.

The sources are an ordered slice rather than a directory and a glob because merge order is meaning. A directory has no order of its own — a caller would be relying on the file names it happens to carry, which is a contract nobody wrote down — so the slice is where the caller states what the result should read like.

One source is legal and produces that source’s document, so a caller merging a computed list does not have to special-case its length. An empty slice is not: there is no document to return and no sensible empty PDF to invent, and pdfunite handed no sources answers with its own usage text.

Nothing else about the sources is narrowed or transformed. Whole documents go in whole — Split is what produces single pages to merge — and the pages keep their own size, so merging US Letter with A4 yields a document whose pages differ in size, which is what a concatenation should do.

Encrypted sources are the one shape this cannot take: pdfunite has no password option, so there is no argument that would let it read one. See the message a run against one carries.

Return Type
File !
Arguments
NameTypeDefault ValueDescription
sources[File ! ] !-

The PDFs to concatenate, in the order their pages should appear in the result.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string merge
func (m *MyModule) Example(registry string, alpineTag string, sources []*dagger.File) *dagger.File  {
	return dag.
			Pdf(registry, alpineTag).
			Merge(sources)
}
@function
def example(registry: str, alpinetag: str, sources: List[dagger.File]) -> dagger.File:
	return (
		dag.pdf(registry, alpinetag)
		.merge(sources)
	)
@func()
example(registry: string, alpineTag: string, sources: File[]): File {
	return dag
		.pdf(registry, alpineTag)
		.merge(sources)
}

version() 🔗

Version returns the poppler release the assembled image ships, as the bare version number reported by pdftotext -v.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string version
func (m *MyModule) Example(ctx context.Context, registry string, alpineTag string) string  {
	return dag.
			Pdf(registry, alpineTag).
			Version(ctx)
}
@function
async def example(registry: str, alpinetag: str) -> str:
	return await (
		dag.pdf(registry, alpinetag)
		.version()
	)
@func()
async example(registry: string, alpineTag: string): Promise<string> {
	return dag
		.pdf(registry, alpineTag)
		.version()
}

withApkAuth() 🔗

WithApkAuth supplies credentials for a repository that requires them.

The secret’s contents are a netrc file — machine mirror.example.com login USER password PASS, one stanza per host — which is what apk-tools 3’s built-in libfetch reads when a repository answers 401. Alpine 3.24 ships apk-tools 3.0; see NETRC in apk(8). Hosts are matched by name only, so a stanza covers a mirror on any port.

It is a *dagger.Secret rather than a string, and is mounted rather than written, so the credentials stay out of the cache key, out of argv, out of the image’s environment and out of any layer a caller exports. That is also why the credentials are not simply userinfo in the WithApkRepository URL, which would put them in /etc/apk/repositories and in every apk error message that quotes it.

Return Type
Pdf !
Arguments
NameTypeDefault ValueDescription
credentialsSecret !-

netrc-formatted credentials for the configured repositories.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string with-apk-auth --credentials env:MYSECRET
func (m *MyModule) Example(registry string, alpineTag string, credentials *dagger.Secret) *dagger.Pdf  {
	return dag.
			Pdf(registry, alpineTag).
			Withapkauth(credentials)
}
@function
def example(registry: str, alpinetag: str, credentials: dagger.Secret) -> dagger.Pdf:
	return (
		dag.pdf(registry, alpinetag)
		.withapkauth(credentials)
	)
@func()
example(registry: string, alpineTag: string, credentials: Secret): Pdf {
	return dag
		.pdf(registry, alpineTag)
		.withApkAuth(credentials)
}

withApkKey() 🔗

WithApkKey trusts a repository’s signing key by dropping it into /etc/apk/keys, which is the other half of WithApkRepository: a private mirror’s index is signed by a key the stock Alpine image has never heard of, and apk refuses an index it cannot verify rather than installing from it.

The file keeps its own name, because the name is load-bearing — an index signature names the key file it was made with, and apk looks that exact file up in the keys directory. A key exported from abuild-keygen is already named correctly; renaming it renames the key.

Repeatable, for a mirror set signed by more than one key. It is a *File rather than a *Secret because a public key is not a credential: it is meant to be in the image, and WithApkAuth is the option for the part that is not.

Return Type
Pdf !
Arguments
NameTypeDefault ValueDescription
keyFile !-

Public key file trusting a repository’s index signature.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string with-apk-key --key file:path
func (m *MyModule) Example(registry string, alpineTag string, key *dagger.File) *dagger.Pdf  {
	return dag.
			Pdf(registry, alpineTag).
			Withapkkey(key)
}
@function
def example(registry: str, alpinetag: str, key: dagger.File) -> dagger.Pdf:
	return (
		dag.pdf(registry, alpinetag)
		.withapkkey(key)
	)
@func()
example(registry: string, alpineTag: string, key: File): Pdf {
	return dag
		.pdf(registry, alpineTag)
		.withApkKey(key)
}

withApkRepository() 🔗

WithApkRepository points package installation at an Alpine repository other than the one the base image ships, which is what makes this module work on a network that cannot reach dl-cdn.alpinelinux.org.

New’s registry argument is not enough on its own and never was: it moves the image, and the packages are still fetched by apk add from whatever /etc/apk/repositories carries — so mirroring Alpine into a private registry buys a container that then fails on its first apk add, or, where the CDN is blackholed rather than refused, hangs until it times out.

Repeatable, in preference order. The first call replaces the image’s list rather than appending to it, because the air-gapped case needs the unreachable defaults gone rather than merely deprioritized: a repository apk still consults is a repository apk still waits for.

The URL is the repository base, spelled the way it would be spelled in /etc/apk/repositories — https://mirror.example.com/alpine/v3.24/main, one call per component. A repository’s index is signed, so pair this with WithApkKey unless the mirror is signed by a key the base image already trusts.

Return Type
Pdf !
Arguments
NameTypeDefault ValueDescription
urlString !-

Base URL of an Alpine repository to resolve packages from.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string with-apk-repository --url string
func (m *MyModule) Example(registry string, alpineTag string, url string) *dagger.Pdf  {
	return dag.
			Pdf(registry, alpineTag).
			Withapkrepository(url)
}
@function
def example(registry: str, alpinetag: str, url: str) -> dagger.Pdf:
	return (
		dag.pdf(registry, alpinetag)
		.withapkrepository(url)
	)
@func()
example(registry: string, alpineTag: string, url: string): Pdf {
	return dag
		.pdf(registry, alpineTag)
		.withApkRepository(url)
}

withFonts() 🔗

WithFonts adds a directory of font files to the image, which is the only way to reach a face Alpine has no package for. It is this module’s WithTessdata.

It matters for exactly the reason the packaged font does. Poppler draws a PDF that names a font without embedding it by asking fontconfig for a substitute, and a substitute it cannot find is not an error: the glyphs are simply absent from the rendered page, and the render succeeds. A document that names a face outside the base-14 set — a corporate font, a CJK family — therefore renders wrong rather than failing, and supplying the face here is the fix.

The directory is mounted under /usr/share/fonts, the one tree /etc/fonts/fonts.conf tells fontconfig to scan, so the faces in it are indistinguishable from packaged ones as far as poppler is concerned. It is mounted rather than copied so a large family does not become an image layer.

Return Type
Pdf !
Arguments
NameTypeDefault ValueDescription
dirDirectory !-

Directory of font files to make available for substitution.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 --registry string --alpine-tag string with-fonts --dir DIR_PATH
func (m *MyModule) Example(registry string, alpineTag string, dir *dagger.Directory) *dagger.Pdf  {
	return dag.
			Pdf(registry, alpineTag).
			Withfonts(dir)
}
@function
def example(registry: str, alpinetag: str, dir: dagger.Directory) -> dagger.Pdf:
	return (
		dag.pdf(registry, alpinetag)
		.withfonts(dir)
	)
@func()
example(registry: string, alpineTag: string, dir: Directory): Pdf {
	return dag
		.pdf(registry, alpineTag)
		.withFonts(dir)
}

Document 🔗

Document is one PDF bound to the toolchain, together with what is needed to open it and how much of it to look at.

It is immutable: every With* returns a copy, so one bound document branches into several outputs without the branches interfering — a text extraction and a render off the same Document, or two page ranges of it, are independent.

attachments() 🔗

Attachments extracts the files embedded in the document and returns the directory holding them, each under the name the document gives it.

This is the other half of what a PDF can carry that is not a page. An embedded file is a whole file attached to the document — the machine-readable XML a ZUGFeRD or Factur-X invoice carries alongside its human-readable page, a spreadsheet behind a report, a CAD file behind a drawing — and it is not drawn, not rendered and not part of any page’s content. Neither Convert nor EmbeddedImages will ever produce it: this is the only function that reaches it.

The names are the document’s own, not this module’s, and that is the one place this function departs from every other directory-returning function here. An attachment’s name is data — it is what the producer called the file and what the consumer expects to find — so normalizing it to a contract the way the page families do would destroy the thing that makes the result usable. A caller wanting the invoice XML asks for invoice.xml.

The flip side is that poppler polices those names, and polices them coarsely: an attachment whose name carries a path — ../elsewhere.xml — makes pdfdetach refuse the whole extraction with Preventing directory traversal rather than skip that one file, so one such name means no attachments at all. That is reported as what it is, carrying the list of names the document holds, because those names are what pdfdetach -savefile needs to fetch them one at a time through Container.

A document carrying no embedded files is refused rather than returned as an empty directory, for the reason EmbeddedImages gives: pdfdetach exits 0 for it, so the directory would be indistinguishable from an extraction that wrote nothing. pdfdetach -list is the report that answers “does this carry any” without failing, and the message names it.

WithPageRange does not narrow it. An embedded file hangs off the document’s catalog rather than off any page — pdfdetach has no page bounds at all — which is the same reason it does not narrow Metadata or Signatures. The passwords do reach it, unlike Split and Merge: pdfdetach takes both.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 attachments
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Attachments()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.attachments()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.attachments()
}

convert() 🔗

Convert opens the conversion namespace: the render options, and the eleven outputs that read them.

It is a separate object rather than more methods on Document because the render options belong to a conversion and not to the document — WithDpi says nothing about the PDF — and because it is what lets one bound document, with its passwords and its page range settled, fan out into several differently configured conversions.

Return Type
Convert !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert
func (m *MyModule) Example(source *dagger.File) *dagger.PdfConvert  {
	return dag.
			Pdf().
			Document(source).
			Convert()
}
@function
def example(source: dagger.File) -> dagger.PdfConvert:
	return (
		dag.pdf()
		.document(source)
		.convert()
	)
@func()
example(source: File): PdfConvert {
	return dag
		.pdf()
		.document(source)
		.convert()
}

embeddedImages() 🔗

EmbeddedImages extracts the image objects the document contains and returns the directory holding them, named image-0000-page-0001.png and so on.

This is not Convert().Png(), and the difference is the reason the function is named this way. Convert().Png() rasterizes a page: it draws everything on the page — text, vectors, annotations, images — into a new bitmap at whatever resolution WithDpi names, and the pixels it produces have never existed before. This returns the image objects themselves, decoded no further than it has to be: for a scanned document that is the scan, at the resolution it was scanned at, with no resampling in between. Which is why it is the better OCR input of the two — every render is a resampling of an image that was already pixels, and recognition accuracy is exactly what that costs.

It follows that WithDpi, WithColorMode, WithScaleTo and WithoutAnnotations have no meaning here and are not reachable from this function at all: they live on Convert, which is where rendering decisions belong. There is nothing to choose about the resolution of an image that already has one. By the same token this returns nothing for a page whose content is text and vectors — there is no image object on it to extract, and Convert().Png() is what draws such a page.

The format is the one real choice, and it is required rather than defaulted because every member either keeps the document’s encoding or replaces it, and no one of those is the obviously right thing to do quietly. See ImageFormat. ORIGINAL is worth reading twice: it keeps JPEG, JPEG 2000, JBIG2 and CCITT G4 streams byte for byte, and for an image encoded in none of those — a raw or Flate-compressed bitmap, which is most PDFs that were not born as scans — it falls back to netpbm, so a .ppm or .pgm in the result is not a failure but an image that had no encoding to keep. ALL is the member whose fallback is PNG instead, and is what to reach for when every image has to be readable by something ordinary.

The names carry both numbers poppler reports, padded and reordered but not renumbered. The one after image- is the image’s index in this extraction, 0-based, which for an unnarrowed run is the num column of pdfimages -list; the one after -page- is the 1-based source page it was drawn on. The index comes first so lexicographic order is document order — which it is, images being extracted in page order — and the word image comes first of all so a directory of these is never mistaken for a directory of rendered pages.

A page can carry several images and most pages carry none, so there is no one-file-per-page contract here of the kind every render honours. A document carrying no images at all is refused rather than returned as an empty directory: pdfimages exits 0 for it, so the directory would be indistinguishable from an extraction that failed to write anything, and the overwhelmingly common thing a caller does with this function is export it.

WithPageRange narrows it. Note that it renumbers nothing but does restart the image index, that index being poppler’s count for the run rather than for the document: pages 2 through 3 of a document whose first image is on page 1 come back starting at image-0000-page-0002. The page number stays the source document’s, the same promise Split makes.

Return Type
Directory !
Arguments
NameTypeDefault ValueDescription
formatEnum !-

Encoding to write each extracted image in.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 embedded-images
func (m *MyModule) Example(source *dagger.File, format ) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Embeddedimages(format)
}
@function
def example(source: dagger.File, format: ) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.embeddedimages(format)
	)
@func()
example(source: File, format: ): Directory {
	return dag
		.pdf()
		.document(source)
		.embeddedImages(format)
}

fonts() 🔗

Fonts returns what pdffonts reports about every face the document’s pages name: the face’s name, its type, its encoding, whether it is embedded in the file, whether it is a subset, whether it carries a ToUnicode map, and the object it lives in.

The emb column is the one to read. A PDF that names a font without embedding one — the usual shape for anything not born as a scan — is drawn by asking fontconfig for a substitute, and a substitute poppler cannot find is not an error: the glyphs are simply absent from the rendered page and the command exits 0. This report is what that failure looks like before it happens, which makes it the diagnostic to reach for when a render comes out blank or a face comes out wrong. no in that column means the render depends on the image’s own fonts — the packaged family, or one supplied through WithFonts.

The report is pdffonts’ own table, verbatim. Parsing it into structured values is deliberately not this module’s job: the format is stable but wide, and a caller who wants one column can read it out of these lines more cheaply than this module can model all of them.

WithPageRange narrows it, because which faces a document needs is a question about pages: a face used only on page 40 is absent from a report of pages 1 through 3. Which substitute poppler would actually choose for an unembedded face is a different question, answered by pdffonts -subst, and reachable through Container.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 fonts
func (m *MyModule) Example(ctx context.Context, source *dagger.File) string  {
	return dag.
			Pdf().
			Document(source).
			Fonts(ctx)
}
@function
async def example(source: dagger.File) -> str:
	return await (
		dag.pdf()
		.document(source)
		.fonts()
	)
@func()
async example(source: File): Promise<string> {
	return dag
		.pdf()
		.document(source)
		.fonts()
}

info() 🔗

Info returns everything pdfinfo reports about the document: its page count, its page size in points and in a named paper size, its PDF version, whether it is encrypted and under which algorithm, and the metadata it carries.

It describes the whole document regardless of any WithPageRange, because a page range is a property of a conversion and this is not one.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 info
func (m *MyModule) Example(ctx context.Context, source *dagger.File) string  {
	return dag.
			Pdf().
			Document(source).
			Info(ctx)
}
@function
async def example(source: dagger.File) -> str:
	return await (
		dag.pdf()
		.document(source)
		.info()
	)
@func()
async example(source: File): Promise<string> {
	return dag
		.pdf()
		.document(source)
		.info()
}

metadata() 🔗

Metadata returns the document’s XMP packet: the RDF/XML block a producer writes its own record of the document into — title, creator tool, rights, modification history — and the one place a workflow’s own custom properties can live inside a PDF.

It is the packet and nothing else, which is what makes it worth having next to Info. The two carry different things and disagree in the wild: Info reports the PDF’s Info dictionary, whose handful of keys a producer may have left behind when it rewrote the XMP, and the packet is the record a downstream asset system actually reads. The packet is returned exactly as it sits in the file, so a caller can hand it to an XML parser or grep a property out of it.

A document carrying no packet is not an error and does not return nothing: poppler prints nothing at all and exits 0, and an empty string is indistinguishable from a function that never ran. It returns a line saying there is no XMP and naming Info as the report that does carry this document’s metadata.

WithPageRange does not narrow it, the packet being a property of the document rather than of any page — the same reason it does not narrow Info.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 metadata
func (m *MyModule) Example(ctx context.Context, source *dagger.File) string  {
	return dag.
			Pdf().
			Document(source).
			Metadata(ctx)
}
@function
async def example(source: dagger.File) -> str:
	return await (
		dag.pdf()
		.document(source)
		.metadata()
	)
@func()
async example(source: File): Promise<string> {
	return dag
		.pdf()
		.document(source)
		.metadata()
}

pageCount() 🔗

PageCount returns how many pages the document has, by reading the Pages: line out of what Info reports.

It is the page count of the whole document, not of a WithPageRange — which is what makes it usable as the bound a range is checked against.

Return Type
Integer !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 page-count
func (m *MyModule) Example(ctx context.Context, source *dagger.File) int  {
	return dag.
			Pdf().
			Document(source).
			Pagecount(ctx)
}
@function
async def example(source: dagger.File) -> int:
	return await (
		dag.pdf()
		.document(source)
		.pagecount()
	)
@func()
async example(source: File): Promise<number> {
	return dag
		.pdf()
		.document(source)
		.pageCount()
}

signatures() 🔗

Signatures returns what pdfsig reports about the document’s digital signatures: one block per signature naming its field, its signer, when it was signed, which algorithm signed it, how much of the document it covers, and whether the signature validates.

A document with no signatures — which is nearly every document — gets the report saying so rather than an error. That is the point of the function being callable on an arbitrary PDF: “is this signed, and does it check out” is a question asked before the answer is known, so the unsigned answer has to come back as a result. poppler exits 2 for it, and this is the one place the module reads a non-zero exit as a report rather than a failure.

It reports and does not gate. A signature that fails to validate is a report saying so — Signature Validation: Signature is Invalid. — and not an error, for the same reason: a caller asking whether the signatures hold needs the answer, and one that wants to stop on a bad signature reads the verdict out of the report. Only a run that got no report at all — an unreadable file, a document whose password is missing or wrong — fails.

What the report can say about a signer’s certificate is limited by the image rather than by the document. Certificate validation needs a trust database, this image carries none, and pdfsig says so on stderr — which is not part of this report. A caller who needs a validated chain supplies one with -nssdir, through Container.

Neither WithPageRange nor anything else narrows it: a signature covers byte ranges of the file rather than pages, and pdfsig has no page bounds at all. The passwords do reach it, an encrypted document being one it has to open like any other.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 signatures
func (m *MyModule) Example(ctx context.Context, source *dagger.File) string  {
	return dag.
			Pdf().
			Document(source).
			Signatures(ctx)
}
@function
async def example(source: dagger.File) -> str:
	return await (
		dag.pdf()
		.document(source)
		.signatures()
	)
@func()
async example(source: File): Promise<string> {
	return dag
		.pdf()
		.document(source)
		.signatures()
}

split() 🔗

Split writes each page of the document to its own PDF and returns the directory holding them, named page-0001.pdf, page-0002.pdf, and so on.

This is page extraction and not rendering: each file carries the original page’s objects — its text layer, its fonts, its annotations — so a split page is still a PDF a reader can search, and splitting is lossless in a way that rendering to PNG is not.

The names are the same contract the render family honours, and for the same reason: a consumer that sorts this directory has to get page order. They are this module’s and not the tool’s — pdfseparate numbers to whatever width the destination pattern asks for, so the four digits are a promise rather than a consequence of the document’s length. The numbers are the source document’s page numbers, so pages 4 through 6 come out page-0004.pdf through page-0006.pdf and stay traceable to the page they came from.

WithPageRange narrows it. The passwords do not reach it: pdfseparate has no password option at all, so an encrypted document is one it cannot open however the document was bound — see the message a run against one carries.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 split
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Split()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.split()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.split()
}

withOwnerPassword() 🔗

WithOwnerPassword supplies the document’s owner password, which opens an encrypted document as fully as the user password does and additionally clears the permission bits poppler otherwise honours.

Reach for it when a document permits opening but forbids extraction: the user password gets a reader past the encryption and then leaves copy:no in force, and the owner password is what a caller entitled to ignore that has. Setting both is fine and is what an owner-held document carrying a separate reading password needs.

Poppler’s 32-byte password limit applies here too. See WithUserPassword.

Return Type
Document !
Arguments
NameTypeDefault ValueDescription
passwordSecret !-

The document’s owner password.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 with-owner-password --password env:MYSECRET
func (m *MyModule) Example(source *dagger.File, password *dagger.Secret) *dagger.PdfDocument  {
	return dag.
			Pdf().
			Document(source).
			Withownerpassword(password)
}
@function
def example(source: dagger.File, password: dagger.Secret) -> dagger.PdfDocument:
	return (
		dag.pdf()
		.document(source)
		.withownerpassword(password)
	)
@func()
example(source: File, password: Secret): PdfDocument {
	return dag
		.pdf()
		.document(source)
		.withOwnerPassword(password)
}

withPageRange() 🔗

WithPageRange narrows every conversion to a span of pages.

Bounds are 1-based and inclusive, matching poppler’s own -f and -l, so WithPageRange(2, 3) is the second and third pages and WithPageRange(1, 1) is the first page alone. A zero last means “to the last page”, which is the only way to spell an open-ended range without knowing the page count.

The bounds are checked against the document rather than handed straight to poppler, and the check is deferred to the conversion that reads them: it needs the page count, which needs the document opened, which needs the passwords this builder cannot see the future of. A first page past the end of the document is therefore reported by name, at the point of use, instead of silently rendering nothing.

It narrows the text outputs and the raster outputs alike. It does not narrow Info or PageCount, which report on the document rather than on a conversion of it.

Return Type
Document !
Arguments
NameTypeDefault ValueDescription
firstInteger !-

First page to convert, 1-based and inclusive.

lastInteger !-

Last page to convert, 1-based and inclusive. Zero means the last page of the document.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 with-page-range --first integer --last integer
func (m *MyModule) Example(source *dagger.File, first int, last int) *dagger.PdfDocument  {
	return dag.
			Pdf().
			Document(source).
			Withpagerange(first, last)
}
@function
def example(source: dagger.File, first: int, last: int) -> dagger.PdfDocument:
	return (
		dag.pdf()
		.document(source)
		.withpagerange(first, last)
	)
@func()
example(source: File, first: number, last: number): PdfDocument {
	return dag
		.pdf()
		.document(source)
		.withPageRange(first, last)
}

withUserPassword() 🔗

WithUserPassword supplies the password an encrypted document is opened with.

This is the user password — the one that grants reading. A document encrypted at all needs one of these two passwords before any of poppler’s tools will look at it, and without one they report Incorrect password and exit non-zero, having tried the empty password on the caller’s behalf.

It is a *dagger.Secret rather than a string, and reaches the tool as an environment variable the shell expands rather than as a -upw argv word, because argv is visible in every Dagger trace and in every error message quoting the failed command. Same reasoning as mounting apk credentials as a netrc file rather than putting userinfo in a repository URL.

Only the first 32 bytes of the password are used. That is poppler’s limit and not this module’s: it truncates a password to the 32 bytes PDF revisions before 2.0 allowed, even for an AES-256 document whose own limit is 127. A document encrypted under a longer password by a tool that honours the full length — qpdf does — is therefore one poppler reports Incorrect password for no matter what is passed here, and no argument to this function can open it. Truncating the password before it reaches this module makes no difference for the same reason.

Return Type
Document !
Arguments
NameTypeDefault ValueDescription
passwordSecret !-

The document’s user password.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 with-user-password --password env:MYSECRET
func (m *MyModule) Example(source *dagger.File, password *dagger.Secret) *dagger.PdfDocument  {
	return dag.
			Pdf().
			Document(source).
			Withuserpassword(password)
}
@function
def example(source: dagger.File, password: dagger.Secret) -> dagger.PdfDocument:
	return (
		dag.pdf()
		.document(source)
		.withuserpassword(password)
	)
@func()
example(source: File, password: Secret): PdfDocument {
	return dag
		.pdf()
		.document(source)
		.withUserPassword(password)
}

Convert 🔗

Convert is a conversion of a bound document: the options a render reads, and the outputs that read them.

Immutable in the same way Document is, so one Convert branches into several formats — the same page range at the same resolution as PNG and as TIFF — without the branches interfering.

The render options live here rather than on Document because three raster outputs share them, and because they describe a conversion rather than the document being converted. They have no meaning for Text and Txt and are documented no-ops there rather than rejections: a DPI set before asking for text is inapplicable rather than wrong, unlike a bound past the end of the document, which is always a mistake.

bbox() 🔗

Bbox returns the document’s text layer as an XHTML report carrying a bounding box for every word, in points, measured from the top-left of the page.

It is the text-layer analogue of the tesseract module’s Hocr — the same shape of answer, sourced from the document’s own coordinates rather than from recognition, so the boxes are exact rather than estimated. Reach for it when the words are not the whole answer: redacting a region, cropping a figure out of a page, deciding which of two columns a phrase belongs to, or lining extracted text up against a render of the same page.

Every page arrives as a page element naming its width and height, holding one word element per space-separated word with its xMin, yMin, xMax and yMax. Pass withLayout to wrap those in the flow, block and line elements poppler groups them into, each carrying the box around everything under it — which is what a caller reconstructing paragraphs wants, and what a caller who only needs word boxes pays parsing for.

The report is a whole XHTML document, -bbox implying poppler’s -htmlmeta: the boxes sit inside a doc element inside an html element with a doctype ahead of it. Read the doc subtree, not the file’s root.

A page with no text layer is reported as an empty page element rather than as a failure — poppler notes no word list on stderr and exits 0 — which is the same boundary Text draws: no words is the signal to render the page and hand it to OCR, not a sign that anything went wrong.

The render options are ignored, this being an extraction and not a render, and so is LayoutMode: the report’s structure is poppler’s own and is not the text’s reading order. WithPageRange narrows it.

Return Type
File !
Arguments
NameTypeDefault ValueDescription
withLayoutBoolean -

Add the block and line boxes poppler groups the words into.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 bbox
func (m *MyModule) Example(source *dagger.File) *dagger.File  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Bbox()
}
@function
def example(source: dagger.File) -> dagger.File:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.bbox()
	)
@func()
example(source: File): File {
	return dag
		.pdf()
		.document(source)
		.convert()
		.bbox()
}

eps() 🔗

Eps renders each page in range to its own Encapsulated PostScript file and returns the directory holding them, named page-0001.eps, page-0002.eps, and so on.

EPS is the format for placing one page inside another document — a figure in a LaTeX paper, artwork in a layout program — which is why it is one page per file by definition and not by this module’s choice: pdftocairo -eps handed a multi-page document refuses to write anything at all and exits non-zero. The per-page loop is what turns that refusal into the whole document.

The bounding box is the page’s ink, not its media box: poppler crops an EPS to what is drawn, so a page of a US Letter document with a single line of text on it comes out a few inches wide. That is EPS’s own convention — a figure carries its extent so the placing document can size it — and it is the one place these files do not agree with the page geometry Png reports.

WithDpi governs rasterized regions; WithColorMode, WithScaleTo and WithoutAnnotations are ignored: see the note on Ps.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 eps
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Eps()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.eps()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.eps()
}

html() 🔗

Html converts each page in range to its own HTML file and returns the directory holding them, named page-0001.html, page-0002.html, and so on, with each page’s extracted images beside them.

The directory therefore holds more than the pages: a page carrying images gets page-0001-1_1.png and so on next to page-0001.html, referenced from the markup by a relative path. Those names are pdftohtml’s, not this module’s, and are left alone precisely because they are written into the HTML — renaming them to a contract would mean rewriting the markup to match. A consumer walking this directory should select page-*.html, not assume every entry is a page.

The relative reference is the reason the conversion runs with the output directory as its working directory: handed an absolute output base, pdftohtml writes that absolute path into every img src, and the markup then only resolves on the machine that produced it.

Every render option is ignored, pdftohtml having no resolution, colour or annotation switch of any kind. WithPageRange narrows it like everything else.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 html
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Html()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.html()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.html()
}

jpeg() 🔗

Jpeg renders each page in range to a JPEG and returns the directory holding them, named page-0001.jpg, page-0002.jpg, and so on — .jpg being poppler’s own choice of extension for the format.

Lossy, so it is the format for a preview a human looks at rather than for input another program measures: the artefacts a flat page compresses into sit exactly where thin strokes are, which is where OCR reads.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 jpeg
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Jpeg()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.jpeg()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.jpeg()
}

png() 🔗

Png renders each page in range to a PNG and returns the directory holding them, named page-0001.png, page-0002.png, and so on.

PNG is the format to hand another module: it is lossless, so nothing downstream is reading compression artefacts as page content, and it is what every image library reads without a codec question.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 png
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Png()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.png()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.png()
}

ps() 🔗

Ps renders the pages in range to a single PostScript file.

It returns a file rather than a directory because PostScript is a multi-page format: one document with a %%Pages: count and a %%Page: marker per page, which is what a printer queue and every downstream PostScript tool expect. Splitting it into a page-per-file directory would produce fragments that are each individually invalid.

WithDpi governs the rasterized regions the output can still contain. WithColorMode, WithScaleTo and WithoutAnnotations are ignored here and by Svg, Eps and Html, and are documented no-ops rather than rejections for the same reason they are on Text: a conversion configured for the raster outputs should fan out into a vector one without having to un-set an option first. The flags are dropped rather than forwarded because poppler does not ignore them — it exits non-zero with -mono may only be used with the -png, -jpeg, or -tiff output options, and -hide-annotations is not a pdftocairo option at all.

Return Type
File !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 ps
func (m *MyModule) Example(source *dagger.File) *dagger.File  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Ps()
}
@function
def example(source: dagger.File) -> dagger.File:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.ps()
	)
@func()
example(source: File): File {
	return dag
		.pdf()
		.document(source)
		.convert()
		.ps()
}

svg() 🔗

Svg renders each page in range to its own SVG and returns the directory holding them, named page-0001.svg, page-0002.svg, and so on.

This is the output for a caller who wants to keep the type outlines rather than pixels of them: embedding a page in a web view, feeding a plotter, re-flowing it in a vector editor. Text arrives as glyph outlines and not as <text> — poppler converts every glyph to a <path> and references it with <use> — so the page is faithful and is not searchable. Reach for Text when the words are what is wanted.

One SVG per page is this module’s doing and matters. pdftocairo -svg run once over a multi-page document writes a single file holding every page, wrapped in the SVG 1.2 pageSet and page elements — which no browser, librsvg or Inkscape implements, so the file draws page one and silently discards the rest. Rendering each page on its own invocation is what makes every page reachable.

WithDpi governs the rasterized regions a page can still contain — an embedded photograph has no vector form to keep. WithColorMode, WithScaleTo and WithoutAnnotations are ignored: see the note on Ps.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 svg
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Svg()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.svg()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.svg()
}

text() 🔗

Text returns the document’s text layer as a string.

This is extraction and not recognition: it returns the text the PDF already carries, exactly, in the order the chosen layout mode puts it. A document with no text layer — a scan — returns nothing at all, and that empty result is the signal to render the pages with Png and hand them to the tesseract module instead. Nothing here fails in that case, because from poppler’s point of view a page with no text is not an error.

Pages are separated by a form feed (U+000C), including after the last one, which is pdftotext’s own convention. Pass disablePageBreaks to drop them, for a consumer that wants one flat stream of text.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
layoutEnum -

How much of the page’s physical arrangement survives into the text. Defaults to reading order.

disablePageBreaksBoolean -

Emit no form feed between pages.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 text
func (m *MyModule) Example(ctx context.Context, source *dagger.File) string  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Text(ctx)
}
@function
async def example(source: dagger.File) -> str:
	return await (
		dag.pdf()
		.document(source)
		.convert()
		.text()
	)
@func()
async example(source: File): Promise<string> {
	return dag
		.pdf()
		.document(source)
		.convert()
		.text()
}

tiff() 🔗

Tiff renders each page in range to a TIFF and returns the directory holding them, named page-0001.tif, page-0002.tif, and so on — .tif being poppler’s own choice of extension for the format.

It is the format the document-imaging world already speaks, and the one to pair with MONO: a bilevel TIFF is what a scanner emits and what an archive expects.

Return Type
Directory !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 tiff
func (m *MyModule) Example(source *dagger.File) *dagger.Directory  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Tiff()
}
@function
def example(source: dagger.File) -> dagger.Directory:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.tiff()
	)
@func()
example(source: File): Directory {
	return dag
		.pdf()
		.document(source)
		.convert()
		.tiff()
}

tsv() 🔗

Tsv returns the document’s text layer as a tab-separated table with one row per layout element, each naming the page it came from and the box it occupies.

It is the text-layer analogue of the tesseract module’s Tsv, and the format is literally tesseract’s: twelve columns — level, page_num, par_num, block_num, line_num, word_num, left, top, width, height, conf, text — with a header row ahead of them. level says what a row describes: 1 a page, 3 a flow, 4 a line, 5 a word. Poppler emits no level-2 paragraph row despite carrying the column for it. Structural rows name themselves in the text column — ###PAGE###, ###FLOW###, ###LINE### — and conf is -1 on them and 100 on every word, this being extraction and not recognition: there is nothing to be uncertain about.

Reach for it over Bbox when the consumer is a table reader — a dataframe, a spreadsheet, awk — rather than a markup parser, and when the page a row belongs to has to be recoverable from the row itself. That is the one thing this format carries and Bbox does not: a -bbox page element states its size and never its number, so a narrowed report is traceable only by position, while page_num here is the page’s number in the whole document even under WithPageRange.

Coordinates are in points, from the top-left of the page, and a word’s box is the font’s ascender-to-descender span around the baseline rather than the glyphs’ ink — so a one-glyph word is exactly as tall as a long one. Word rows round to two decimals where the structural rows print six.

A page with no text layer yields its page row and no word rows rather than a failure, the same boundary Text and Bbox draw. The render options are ignored, as is LayoutMode. WithPageRange narrows it.

Return Type
File !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 tsv
func (m *MyModule) Example(source *dagger.File) *dagger.File  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Tsv()
}
@function
def example(source: dagger.File) -> dagger.File:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.tsv()
	)
@func()
example(source: File): File {
	return dag
		.pdf()
		.document(source)
		.convert()
		.tsv()
}

txt() 🔗

Txt returns the same text Text returns, as a file rather than a string.

Which of the two to reach for is plumbing and nothing more: a file composes with whatever consumes files — another module, an export, a directory being assembled — without the bytes passing through the caller.

Return Type
File !
Arguments
NameTypeDefault ValueDescription
layoutEnum -

How much of the page’s physical arrangement survives into the text. Defaults to reading order.

disablePageBreaksBoolean -

Emit no form feed between pages.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 txt
func (m *MyModule) Example(source *dagger.File) *dagger.File  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Txt()
}
@function
def example(source: dagger.File) -> dagger.File:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.txt()
	)
@func()
example(source: File): File {
	return dag
		.pdf()
		.document(source)
		.convert()
		.txt()
}

withColorMode() 🔗

WithColorMode sets the colour space pages are rendered in.

Unset, pages render in full colour. GRAY and MONO exist for the OCR path: grayscale and binarized input is what recognition preprocessing wants, and producing it here is cheaper and more predictable than letting the recognizer threshold a colour image itself.

Ignored by Text and Txt.

Return Type
Convert !
Arguments
NameTypeDefault ValueDescription
modeEnum !-

Colour space to render in.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 with-color-mode
func (m *MyModule) Example(source *dagger.File, mode ) *dagger.PdfConvert  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Withcolormode(mode)
}
@function
def example(source: dagger.File, mode: ) -> dagger.PdfConvert:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.withcolormode(mode)
	)
@func()
example(source: File, mode: ): PdfConvert {
	return dag
		.pdf()
		.document(source)
		.convert()
		.withColorMode(mode)
}

withDpi() 🔗

WithDpi sets the resolution pages are rendered at, in dots per inch.

Unset, pages render at 150 dpi — pdftoppm’s own default, and a screen-reading resolution. A caller handing these pages to the tesseract module wants 300, which is the resolution tesseract’s own quality documentation asks for and the one scanners are set to for the same reason: its models were trained on characters of a certain pixel height, and body text below 300 dpi costs accuracy. Cost rises roughly with the square, so 600 dpi is four times the pixels of 300 for very little further gain.

Ignored by Text and Txt, which read a text layer rather than rendering anything. Overridden by WithScaleTo, which fixes the output’s pixel dimensions directly.

Return Type
Convert !
Arguments
NameTypeDefault ValueDescription
dpiInteger !-

Render resolution in dots per inch. Must be positive.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 with-dpi --dpi integer
func (m *MyModule) Example(source *dagger.File, dpi int) *dagger.PdfConvert  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Withdpi(dpi)
}
@function
def example(source: dagger.File, dpi: int) -> dagger.PdfConvert:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.withdpi(dpi)
	)
@func()
example(source: File, dpi: number): PdfConvert {
	return dag
		.pdf()
		.document(source)
		.convert()
		.withDpi(dpi)
}

withScaleTo() 🔗

WithScaleTo fixes the rendered page’s size in pixels rather than its resolution, scaling each page to fit inside a square of the given side while keeping its aspect ratio. A US Letter page scaled to 500 comes out 386x500.

It overrides WithDpi, and does so in this module rather than by relying on poppler’s flag precedence: the resolution flag is left off the command line entirely when a scale is set, so which one wins is a fact about this code and not about an undocumented ordering.

Reach for it when the consumer has a size budget — a thumbnail, a fixed-width preview — and for DPI when the consumer cares about the physical scale of what is on the page, which is every OCR and measurement case.

Ignored by Text and Txt.

Return Type
Convert !
Arguments
NameTypeDefault ValueDescription
pixelsInteger !-

Side of the pixel box each page is scaled to fit. Must be positive.

Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 with-scale-to --pixels integer
func (m *MyModule) Example(source *dagger.File, pixels int) *dagger.PdfConvert  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Withscaleto(pixels)
}
@function
def example(source: dagger.File, pixels: int) -> dagger.PdfConvert:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.withscaleto(pixels)
	)
@func()
example(source: File, pixels: number): PdfConvert {
	return dag
		.pdf()
		.document(source)
		.convert()
		.withScaleTo(pixels)
}

withoutAnnotations() 🔗

WithoutAnnotations renders the page without its annotation layer: no form field contents, no stamps, no sticky notes, no highlights.

It is what a caller comparing two revisions of a drawing wants, and what a caller feeding pages to OCR usually wants too — an annotation drawn over body text is text the recognizer will try to read.

Named for what it removes rather than offered as an annotations bool defaulting to true, because a `the Go SDK: the zero value is dropped before it reaches the API, so no caller could ever turn annotations off.

Ignored by Text and Txt.

Return Type
Convert !
Example
dagger -m github.com/z5labs/devex/daggerverse/pdf@bcec6416aad00f8ced457622705d9541be26518d call \
 document --source file:path \
 convert \
 without-annotations
func (m *MyModule) Example(source *dagger.File) *dagger.PdfConvert  {
	return dag.
			Pdf().
			Document(source).
			Convert().
			Withoutannotations()
}
@function
def example(source: dagger.File) -> dagger.PdfConvert:
	return (
		dag.pdf()
		.document(source)
		.convert()
		.withoutannotations()
	)
@func()
example(source: File): PdfConvert {
	return dag
		.pdf()
		.document(source)
		.convert()
		.withoutAnnotations()
}