VMPrint AST Layout Skill
A practitioner's guide to constructing sophisticated layouts with the VMPrint JSON AST version 1.1. Use this alongside the regression fixtures in engine/tests/fixtures/regression/ and engine/tests/fixtures/scripting/ (working examples for every feature).
Schema authority: The interfaces embedded in this skill (§ "Exact … interface" blocks) are the canonical contracts for allowed keys. Do not guess at key names — if a key is not listed in one of those interface blocks, it does not exist. The pitfalls section documents keys that have been hallucinated in the past.
AST 1.1 breaking changes from 1.0:
placementreplacesproperties.layout;image,dropCap,columnSpan,tableare now top-level element keys instead of nested insideproperties.
1. Root Structure
Every document is a single JSON object:
{
"documentVersion": "1.1",
"layout": { ... },
"fonts": { ... },
"styles": { "myType": { ... } },
"elements": [ ... ],
"header": { ... },
"footer": { ... }
}
documentVersion— always"1.1"layout— page geometry and typographic defaultsfonts— optional; only needed for custom font filesstyles— maps elementtypestrings to base styles; anything not here defaults to layout valueselements— the content treeheader/footer— optional running regions
Optional scripting keys at root level: methods, scriptVars, onBeforeLayout, onAfterSettle.
2. Page Geometry
"layout": {
"pageSize": { "width": 720, "height": 405 },
"orientation": "landscape",
"margins": { "top": 34, "right": 50, "bottom": 34, "left": 50 },
"pageTemplates": [
{
"pageIndex": 1,
"pageSize": { "width": 280, "height": 420 },
"margins": { "top": 34, "right": 22, "bottom": 34, "left": 22 }
}
],
"fontFamily": "Arimo",
"fontSize": 10,
"lineHeight": 1.35,
"pageBackground": "#fdf6e3"
}
pageSize accepts "A4", "LETTER", or { "width": N, "height": N } in points. Standard sizes:
| Name | Points |
|---|---|
| A4 portrait | 595 × 842 |
| LETTER portrait | 612 × 792 |
| 16:9 landscape | 720 × 405 |
pageTemplates can override pageSize, orientation, and/or margins for
matching pages. pageIndex is zero-based; selectors such as "first", "odd",
"even", and "all" are also supported. The engine resolves the active page
geometry before measuring that page, so odd-sized pages change real flow space
and render as matching PDF media boxes.
Content area math (critical for fitting content on page):
contentWidth = pageWidth - marginLeft - marginRight
contentHeight = pageHeight - marginTop - marginBottom
For a 720 × 405 page with margins {top:34, right:50, bottom:34, left:50}:
contentWidth = 720 - 50 - 50 = 620 ptcontentHeight = 405 - 34 - 34 = 337 pt
In a 3-column story with gutter 12:
columnWidth = (620 - 12 × 2) / 3 = 198.67 pt
Line height in points = fontSize × lineHeight. For 10pt / 1.35: 13.5 pt per line.
Lines per column = floor(contentHeight / lineHeightPt).
3. Fonts
Built-in font families available without registration:
Arimo, Tinos, Cousine, Caladea, Carlito, Noto Sans JP (CJK), Noto Sans Arabic, Noto Sans Thai, Noto Sans Devanagari.
Reference a built-in family by name in fontFamily; no fonts block needed:
"fonts": { "regular": "Arimo" }
For custom font files, register by role:
"fonts": {
"regular": "path/to/font.ttf",
"bold": "path/to/font-bold.ttf",
"italic": "path/to/font-italic.ttf",
"bolditalic": "path/to/font-bolditalic.ttf"
}
The engine maps fontWeight/fontStyle to these slots at render time. For named non-system fonts in inline spans use fontFamily on properties.style of a "text" child.
Standard PDF 1.4 built-in fonts (zero-embed, Latin-only docs via StandardFontManager):
Helvetica, Times New Roman, Courier — common aliases like Arial → Helvetica resolve automatically.
4. Styles Table
Every type string is a key into styles. Style resolution: styles[element.type] (base) merged with properties.style (override).
"styles": {
"heading": {
"fontSize": 22, "fontWeight": "bold",
"marginBottom": 14, "keepWithNext": true,
"hyphenation": "off"
},
"body": {
"fontSize": 10, "marginBottom": 10,
"allowLineSplit": true, "orphans": 2, "widows": 2,
"textAlign": "justify"
},
"kicker": {
"fontSize": 6.5, "letterSpacing": 1.2, "fontFamily": "Cousine",
"textAlign": "center", "marginBottom": 9, "keepWithNext": true
},
"table-cell": {
"fontFamily": "Cousine", "fontSize": 7,
"paddingTop": 3, "paddingBottom": 3, "paddingLeft": 4, "paddingRight": 4
}
}
Any element type you invent is valid — just add it to styles.
5. Block Element Types
{ "type": "heading", "content": "My Title" }
{ "type": "body", "content": "Plain paragraph." }
{ "type": "body", "content": "", "children": [ ... ] }
Special structural types handled by the engine:
type | Role |
|---|---|
"story" | Multi-column DTP zone; carries columns, gutter, balance as top-level keys |
"table" | Table container; optional table top-level key for config |
"table-row" | Row inside a table |
"table-cell" | Cell inside a row; supports colSpan, rowSpan in properties |
"zone-map" | Independent-region layout; zoneLayout + zones[] at top level |
"strip" | Horizontal slot layout; stripLayout + slots[] at top level |
"image" | Block or inline image; image data at top level |
All other type strings are user-defined and look up styles only.
Use "content": "" (empty string) on container elements (table, story, zone-map, strip). Container elements and image elements do not require content but it's harmless to include it.
6. Top-Level Element Keys (AST 1.1)
In AST 1.1, several configuration objects moved out of properties and became top-level keys on the element. This is the most important change from 1.0.
| Key | Element type(s) | Purpose |
|---|---|---|
content | all | Text content string |
children | all block | Inline runs or child block elements |
name | any | Scripting target name |
type | all | Element type string |
image | "image" | Image data (data, mimeType, fit) |
table | "table" | Table config (headerRows, repeatHeader, columns, etc.) |
dropCap | any block | Drop cap config |
columnSpan | story children | "all" or number |
placement | story children | Float/absolute placement directive |
columns | "story" | Column count |
gutter | "story" | Inter-column gap |
balance | "story" | Equal-height column balancing |
zones | "zone-map" | Array of zone definitions |
zoneLayout | "zone-map" | Column sizing + gap config |
slots | "strip" | Array of slot definitions |
stripLayout | "strip" | Track sizing + gap config |
properties | all | style, sourceId, colSpan, rowSpan, semanticRole, etc. |
Exact ElementProperties interface (no other keys are valid):
interface ElementProperties {
style?: Partial<ElementStyle>; // inline style overrides
colSpan?: number; // table-cell only
rowSpan?: number; // table-cell only
sourceId?: string;
linkTarget?: string; // inline text/inline elements
semanticRole?: string; // "header" on table-row
reflowKey?: string;
keepWithNext?: boolean;
marginTop?: number;
marginBottom?: number;
pageOverrides?: {
header?: PageRegionContent | null;
footer?: PageRegionContent | null;
};
language?: string; // code blocks
}
colSpan and rowSpan remain inside properties (not top-level).