Working with documents
The fuller tour. Everything here follows the same few conventions—once you've used one part of the API, you've used them all (API design explains the grammar).
New documents
Start from nothing, or from a document you have:
const doc = NumbersDocument.blank(); // one sheet, one table, A4
const deck = KeynoteDocument.blank(); // one slide, 16:9
const page = PagesDocument.blank(); // empty body, A4
const from = PagesDocument.blankFrom(templateBytes); // any document as templateblank() needs no template file; blankFrom keeps the given document's design.
Text
All text is the same kind of thing. Body, headers, footers, table cells, presenter notes, text boxes—each is a TextStorage, with one editing model:
const storage = doc.textStorages()[0];
storage.text;
storage.paragraphs(); // { start, end, text, styleId }
storage.replaceRange(10, 14, "new"); // UTF-16 offsets == JS string indexes
storage.replaceAll("find", "replace");
storage.appendParagraph("More");
storage.setParagraphStyle(0, styleId);Edits keep styles, comments, footnotes, and fields attached to the right text.
For richer flows, doc.find(pattern) returns TextRange handles:
doc.find("Revenue")[0].bold().link("https://example.com");
doc.paragraph(3).setStyle("Heading 2");Styles
const sheet = doc.stylesheet;
sheet.paragraphStyles(); // named styles with ids/identifiers
sheet.createParagraphStyle({
name: "Note", basedOn: "Body",
character: { italic: true, fontSize: 11 },
paragraph: { leftIndent: 24, spaceBefore: 6 },
});
const heading = sheet.style("Heading 1")!;
heading.character(); // what this style overrides
heading.resolved().character; // …with the parent chain folded in
heading.setParagraph({
border: solidStroke({ r: 0, g: 0, b: 0 }, 1),
borderPositions: BorderPosition.BOTTOM,
backgroundColor: hexColor("#f5f5f0"),
tabs: [{ position: 216, alignment: TabAlignment.DECIMAL, leader: "." }],
});Fills, gradients, strokes, and shadows are one shared vocabulary—the same values style text, table cells, and shapes: colorFill(r, g, b), linearGradient(a, b), solidStroke(color, width), hexColor("#0066ff").
A created style appears in the app's styles panel, ready to reapply.
Tables
const table = doc.tables()[0]!;
table.grid(); // (CellValue | null)[][]
table.setCell(1, 0, { type: "text", value: "Revenue" });
table.setRow(2, [{ type: "date", value: new Date() }, { type: "bool", value: true }]);
table.setCellFormatting(1, 0, {
fill: colorFill(0.95, 0.95, 1),
borders: allBorders(solidStroke({ r: 0.2, g: 0.2, b: 0.2 }, 0.5)),
padding: { left: 6, right: 6, top: 3, bottom: 3 },
});
table.setBands({ headerRows: 1, freezeHeaderRows: true, repeatHeaderRows: true });
table.insertRows(3, 2);
table.setCellFormat(1, 1, { category: "currency", currencyCode: "EUR" });
table.cellFormula(1, 3); // "=B2*C2"
table.setFormula(1, 4, "=SUM(B2:B9)", { value: 1500 }); // nothing evaluates; pass the cache
table.merges(); // [{ row, column, rowCount, columnCount }]
table.mergeCells(0, 0, 1, 3); // anchor keeps its value, covered cells go
table.unmergeCells(0, 0);
table.cellComments(); // the app's comments on cells, replies includedWorth knowing: writing into a merge-covered cell throws; formulas work in Pages and Keynote tables too; nothing evaluates, so pass the value to display as { value }. 272 functions are supported by name—an unknown one renders as FUNCTION_<id> and is refused for writing.
Numbers: conditional formatting, filters, categories
table.conditionalRules(4, 2); // [{ predicate: { operator: "<", text: "C5<0" }, … }]
table.filterSets(); // { rows, columns }
table.activeCategories()?.groups(); // tree: [{ value, label, rows, children }]
doc.setActiveSheet(1); // re-point the stored tab selections (best-effort)Nothing evaluates: the API reports what the rules are, not which currently matches. setActiveSheet re-points the sheet selections Numbers consults when choosing the tab to open on; the state that decides it is still unmeasured, so treat the call as best-effort.
Fields, comments, footnotes, bookmarks
body.addComment(10, 20, "Looks great. Ship it.");
body.addFootnote(30, "See the appendix.");
footer.insertPageNumber(footer.text.length);
body.insertLink(5, 12, "https://example.com");
body.addBookmark(10, 20, "Introduction");The conventions the apps expect—link styling, footnote marks, comment authors—are applied by default; skip with { characterStyle: false } or pass your own.
Slides
const slide = deck.slides()[0]!;
slide.title = "Q3";
slide.body = "Revenue\nCosts";
slide.notes = "Pause here.";
slide.isSkipped = true;
slide.setTransition({ automatic: true, delay: 2 });
deck.addSlide({ copyOf: 0 });
deck.duplicateSlide(0);
deck.moveSlide(0, 2);
deck.setSlideSize(1024, 768);
deck.addImage(1, pngBytes, { fileName: "chart.png" }); // a picture on the slide, centred unless placedDrawables, images, charts
doc.drawables()[1].setGeometry({ x: 100, y: 50 });
doc.drawables()[0].setGeometry({ width: 600, height: 80 });
doc.drawables()[0].setPathRectangle(600, 80); // a text box's path is its text's coordinate space
doc.images()[0].setCrop({ x: 20, y: 0, width: 200, height: 150 });
doc.images()[0].style()!.set({ shadow: { angle: 90, offset: 10, radius: 20, opacity: 0.7, enabled: true } });
const chart = doc.charts()[0]!;
chart.setValue(0, 2, { type: "number", value: 99 });
chart.addSeries("Region 3", values);Chart data and appearance are both editable—type, series colours, axis gridlines, legend—and styling one chart never restyles its siblings. Crops are non-destructive. A text box's path renders scaled to its frame, text included, so resize the path with setPathRectangle beside setGeometry or the text scales with the frame.
Before you save
doc.audit(); // [{ severity, code, message }], the faults a person otherwise finds by opening the file
doc.save(); // refuses an archive missing a required field, naming the object and fieldaudit() is offline and cheap: malformed table anchors, missing calc-engine registration, cross-table references whose target no table carries, conditional rules the engine never evaluates, and values without display formats. save() refuses an archive missing a required field rather than producing a file the app would report as damaged.
Low level
import { IWorkContainer, parseIwaFile, RawMessage, typeName } from "cupertino-files";
const container = IWorkContainer.fromBytes(bytes);
const objects = parseIwaFile(container.iwaFiles.get("Index/Document.iwa")!);
typeName(objects[0].type, "pages"); // "TP.DocumentArchive"
objects[0].message.getString(3); // schema-light protobuf accessThe format specification documents every layer beneath this API, and Fidelity explains how round trips stay exact.