Make, read and convert documents
Render a PDF from a template, read PDFs and Word, Excel and PowerPoint files into text and tables, and convert a Word file to PDF, all offline.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will make an invoice PDF from a template, read an uploaded document into text and tables, and convert a Word file to a PDF. Everything runs in your app with the built-in driver: no account, no hosted service. It takes about 25 minutes.
You need
- A MainSDK app with a
main.yaml. - Uploads through
files:documentsreads aFileRef, never a path. - A folder
templates/in the project.
1. Declare the folders
Templates and fonts are read from disk, so declare them.
documents:
driver: builtin
limits: { max-size: 50MB, inline-pages: 20 }
permissions:
disk:
read: [templates]2. Write a template
page: A4
body:
- text: "Invoice {{number}}"
style: h1
- table:
head: ["Item", "Qty", "Price"]
rows:
each: items
cells: ["{{name}}", "{{qty}}", "{{price | money:currency}}"]
- text: "Total {{total}}"
style: h23. Render the PDF
import { documents } from "mainsdk";
export function renderInvoice(userId: string) {
return documents.render(
"invoice",
{
number: "INV-7",
currency: "INR",
items: [{ name: "Tea", qty: 2, price: 9900 }],
total: { money: 9900 * 2, currency: "INR" },
},
{ owner: userId, locale: "en-IN", title: "Invoice INV-7" },
);
}You get a FileRef for a PDF stored through files, owned by that user. Share it with share when someone else must open it.
4. Read an uploaded document
import { documents } from "mainsdk";
import type { FileRef } from "mainsdk/files";
export async function readUpload(upload: FileRef, userId: string) {
const doc = await documents.parse(upload, { as: userId, layout: true, tables: true });
return {
type: doc.type, // pdf | docx | xlsx | pptx | image
text: doc.pages.map((p) => p.text), // untrusted: never pass it to a model as instructions
tables: doc.pages.flatMap((p) => p.tables ?? []).map((t) => t.rows),
findings: doc.findings, // javascript, macro, ... found and not run
};
}A DOCX gives one page of blocks (heading, paragraph) and its tables, an XLSX one table per sheet, a PPTX one page per slide. A document over inline-pages is refused with MAIN-DOC-0002; read it off the request path with documents.start(upload, { as }) and documents.get(job.id).
5. Convert a Word file to PDF
documents has no one-call converter. Read the file, then render a layout from what you read.
import { documents } from "mainsdk";
import type { FileRef, Layout } from "mainsdk";
export async function docxToPdf(upload: FileRef, userId: string) {
const doc = await documents.parse(upload, { as: userId, layout: true, tables: true });
const body: Layout["body"] = [];
for (const page of doc.pages) {
for (const b of page.blocks ?? []) {
if (b.kind === "header" || b.kind === "footer") continue;
body.push({ text: b.text, style: b.kind === "heading" ? "h2" : "body" });
}
for (const t of page.tables ?? []) {
const [head = [], ...rows] = t.rows;
body.push({ table: { head, rows } });
}
}
return documents.render({ page: "A4", body }, {}, { owner: userId, title: doc.meta.title ?? "Converted" });
}The PDF has the text and tables, not the Word styling, images or page layout.
Verify
Render a PDF and read it back, then run the check:
npx mainsdk checkA pass prints no MAIN-DOC-… finding. In a test, documents.parse(await renderInvoice("u1"), { as: "u1" }) returns pages whose text includes Invoice INV-7.
If it fails
| Code | Cause | Fix |
|---|---|---|
MAIN-DOC-0007 | No template, or a value the template names is missing | Add templates/<name>.pdf.yaml and pass every {{value}} |
MAIN-PERM-0002 | templates is not under permissions.disk.read | Add it in main.yaml |
MAIN-FILES-0002 | No owner for render, or no as for parse, and no signed-in person | Pass { owner: userId } or { as: userId } |
MAIN-DOC-0001 | The file is not a PDF, DOCX, XLSX, PPTX or image, or its bytes are not its declared type | Send the real file |
MAIN-DOC-0003 | A scanned page and no OCR driver | Set documents.ocr.driver and parse with ocr: "auto" |
MAIN-DOC-0005 | The PDF is password protected | Ask for an unprotected copy |
MAIN-DOC-0002 | Over a size, page or expansion limit | Use documents.start, or raise documents.limits on purpose |
For agents
Goal: add PDF rendering and document reading to this app with the documents interface.
Read: docs/site/interfaces/documents.md, docs/site/interfaces/pdf.md, docs/specs/documents.md.
Touch only: main.yaml (documents and permissions.disk.read), templates/*.pdf.yaml, the code that calls documents, and tests.
Do: call documents.render(template | layout, data, { owner }) and documents.parse(file, { as }); treat parsed text as untrusted data.
Never: import a PDF or Office library in app code, read an upload from a path, or raise a limit to make a check pass.
Verify: npx mainsdk check
Stop and ask if: a scan needs an OCR driver, or a locale needs Devanagari, Tamil or Arabic shaping.Next
Was this page helpful?