Skip to main content

Chapter 5.4 · Part II · The craft

Document format: pictures, tables and videos

Resources in the text: references and embeds, numbering, placement and where a float lands, text wrap, bitmap size, safe areas, comic anchors and videos

Updated 2026-10-1639 minIn chapter 5 Document Formatenescadefritptzhjaar

In short

This page covers the pictures, tables and videos of a book. You add them once and mention them in the text, and Postext puts each one near the place where it is first mentioned. You can choose where it goes: at the top of a page, across all the columns, or exactly where it is written. Text can flow around a picture. A picture can mark the part that must always be seen when it is cut to fit. Videos play in the web view and in e-books, and show a picture in print.

#Resources

Images, SVGs, tables and videos are not written inline. They are declared once as resources (managed in the Sandbox's Resources panel, which handles image and SVG upload, an interactive table editor, YouTube, Vimeo and uploaded videos, and caption and placement editing) and then connected to your prose by id. Referencing a resource is enough to incorporate it — you mention it once with an inline :ref{id="…"}, and the engine floats the figure or table into the first free slot after that reference — the bottom of the column you mention it in, the top of the next column, a band of the next page — just as a print typesetter would. You do not place it a second time.

Both forms below are net-new syntax that does not collide with CommonMark, so a document using them still reads as plain text in any other markdown viewer.

#Inline reference (the primary form)

Refer to a resource from within prose with :ref{id="…"}. The first reference both incorporates the resource (so it gets placed on the page) and renders its computed number, prefixed by the type's short label by default:

As shown in :ref{id="lighthouse-diagram"}, the lantern room sits above the gallery.

renders as: As shown in Fig. 1.7, the lantern room sits above the gallery. — and the diagram itself floats to the nearest free slot after the sentence (the bottom of this column, the top of the next, or a band of the next page), while this sentence and the text after it flow on uninterrupted.

The running text is never broken at the reference point. Where the resource lands — the first free slot, or only a top or bottom slot; within a single column or across the full width — is governed by its placement (see Placement below), and by where you mention it: the search starts right after the reference.

#Block embed (optional, explicit inline placement)

Occasionally you want a resource to sit at an exact point in the flow rather than float. Opt out of floating by giving the resource placement.position: "here" and embedding it with ::resource{id="…"} on its own line:

Here is the floor plan we discussed.
 
::resource{id="lighthouse-diagram"}
 
The keeper's quarters occupy the eastern wing.

For a floated resource the ::resource directive is unnecessary — the :ref already placed it, and a redundant ::resource for the same id is simply treated as another reference, not a second copy. A ::resource only renders the resource inline when its resolved placement is "here". An inline resource keeps a line of space above it (the float gap), like a float would, unless the block before asks for more. In the running text it keeps the same space below it; then the text after it goes back onto the baseline grid, which can add up to a line more. A heading, a list, a box or another inline resource right after it shares that space with its own space above: the larger of the two applies, not both. (Up to postext 1.4 the space below was only what the grid snap left, anywhere from nothing to a line; layout.inlineResourceGap: 'above' keeps that rule, and books saved before 1.5 are read with it.) Inside a box (:::callout) the resource keeps the same space, a line of the box's own text above it and, with 'around', below it; at the top or the foot of the box the padding sets it off instead. (Up to postext 1.4 it sat right against the text of the box; layout.inlineResourceGapInBoxes: false keeps that, and books saved before 1.5 whose boxes embed a resource are read with it.)

An inline resource whose placement also says span: 'page' (placement: { position: 'here', span: 'page' }) spans the page at the point of its directive in a layout of several columns, as a page-span box does: the columns above it close level (balanced, or the resource moves to the head of the next page when they cannot be), it is set across the content width with the same space above and below, and the text goes on in every column under it. On a one-column page it is set as before, at the full measure. A span: 'column' resource with columns set keeps to its column when it is embedded inline: the columns count only for floats. Since postext 1.19.

The id must match a resource defined in the Resources panel. The engine renders the resource (bitmap, SVG, or table) with its caption drawn underneath as a figure/table foot. The caption text is composed from the resource type's captionPrefix, the computed number, and the resource's own caption — e.g. Figure 1.7. The original lighthouse plan. Its typography is governed by Configuration › Caption style: label and description share one typeface and size, while the label keeps independent bold/italic/colour settings; the gap above the caption defaults to 0.75em and the alignment to left. The caption can instead sit above the resource (captionStyle.position: 'above', globally or per resource type), optionally on a coloured bar. A resource may also carry a note — a short source line or credit, with the same inline formatting and :ref marks as the caption — set in a smaller run under the resource (under the caption when the caption is below, under the body when it is above) and styled through captionStyle.note.

Line breaks in captions and notes. A caption or a note is one paragraph set at the measure of its slot, and a newline typed in it is a space. To start a new line, write \\ — the forced break of titles — or end the line with a backslash, Markdown's hard break: ¹ Measured at 20 °C. \\ ² Mean of three runs. sets each footnote of a wide table on a line of its own. The line before a break keeps its natural width, never stretched to the measure, and two breaks in a row make no empty line (a no-break space between them does). In a table cell \\ does what a newline does: it opens a new paragraph; the indentation of the line after it is kept, so two leading spaces still nest a list item. In the text itself the two backslashes are always a break, with no escape: to print them, set them in inline code, where backslashes print as written. In a link's destination they stay part of the URL, and in a directive's attributes (a :ref's text) part of the value; inside a chip, which is set on one line, a break is a space. (Up to postext 1.4 the backslashes printed, and a caption or a note broke only where the measure ran out.)

Table resources draw their own grid, styled through Configuration › Table style: body and header cells have fully independent typography, the header background defaults to #f0f0f0, borders to 0.75pt, and cellPadding to 0.375em — any field left unset inherits from the body text. Column widths are part of the table itself: TableModel.columnWidths holds one relative weight per column ([2, 1, 1] gives the first column half the width); when unset, columns split the width equally. A table can also be set in a named variant: table.styleId picks one of the document's tableStyles (see Configuration › Named table styles), whose unset fields inherit tableStyle; an unknown or missing id keeps tableStyle.

A cell places its content with TableCell.align (left, center, right; list items stay flush left) and TableCell.verticalAlign (top, the default, middle or bottom). The vertical alignment moves the cell's whole content — the image and the text under it, as one unit — inside a cell taller than it: a row stretched by a longer neighbour, or the rows a rowSpan covers. It applies in every output (canvas, HTML, PDF), in rotated tables and in each slice of a table split across pages. Both are set per cell from the alignment buttons of the sandbox's table editor toolbar.

A table cell can also hold an image. TableCell.image names a bitmap or SVG resource by id ({ "resourceId": "fig-arm", "width": 0.7 }): the image is drawn inside the cell — never numbered, floated or captioned — fitted to the cell's inner width (or the fraction of it given by width, default 1) with its aspect ratio kept, aligned like the cell's text, and any cell text runs under it. The row grows to hold it. In the sandbox's table editor the image button of the toolbar picks the resource for the active cell and a width field sets the fraction. An id that matches no image resource leaves the cell text-only.

A cell can carry its own fill. TableCell.background is a colour value ({ "hex": "#c1dfd6", "model": "hex" }, optionally linked to a document palette entry with paletteId) painted instead of the style's header or body background — a compatibility matrix shades its cells green, red and yellow this way. The sandbox's table editor sets it from the fill control of the toolbar. To key such fills, the caption, the note and any text block accept an inline colour swatch: :swatch{color="#c1dfd6"} (a hex, or the id of a palette entry — :swatch{color="table-compatible"}) sets a small square on the baseline, three quarters of the font size, filled with the colour and outlined in the text colour, so a note can read :swatch{color="ok"}: compatible; :swatch{color="no"}: incompatible. A colour that resolves to nothing draws an empty outline. The hex may carry an alpha channel (#rrggbbaa, #rgba), and rgb() / rgba() colours work too, so a translucent cell fill has a matching translucent key (see Configuration › Transparency).

SVG resources can additionally be recoloured for single-spot-colour printing via diagramStyle.singleInk (default false). When enabled, every colour in an SVG diagram is remapped to a luminance-mapped tint of diagramStyle.inkColor (which defaults to the main palette colour, #295AA3) — white maps to paper, black to the full ink — so figures reproduce faithfully when the document is printed with a single spot colour. See Configuration › Diagram style.

A malformed embed (missing or empty id, an unquoted or single-quoted id, extra attributes) is not promoted to a resource block; it falls through to ordinary paragraph parsing and remains visible in the output. So does a well-formed embed line glued under a paragraph line without a blank line between them: it is read as part of that paragraph. Both raise a malformedEmbed warning — an Embed set as text entry in the sandbox's Checks panel.

Inline references are recognised inside any text block — paragraphs, headings, blockquotes, and list items — and may sit alongside bold, italic, inline code, and inline math.

Reference options

The :ref directive accepts three optional attributes, in any order. style selects how the computed label is rendered: style="number" prints the bare number (1.7), style="full" prints the type's full name plus the number (Figure 1.7), and when style is unset the type's shortLabel plus the number is used (Fig. 1.7). case changes the letter case of the label part only — lower, upper or capitalize — leaving the number untouched. text is a verbatim override that replaces any computed label and takes precedence over both style and case. The rendering options side by side:

SyntaxRendersNotes
:ref{id="…"}Fig. 1.7Default style: the type's shortLabel followed by the number, joined with a non-breaking space so they never wrap apart.
:ref{id="…" style="number"}1.7The bare computed number, no label.
:ref{id="…" style="full"}Figure 1.7The type's full name followed by the number. Use at the start of a sentence or where the abbreviation reads poorly.
:ref{id="…" case="lower"}fig. 1.7Recases the label only: lower (fig. 1.7), upper (FIG. 1.7) or capitalize (first letter upper-cased). Combines with style="full" (figure 1.7); the number is never touched, and an unrecognised value is ignored.
:ref{id="…" text="see the plan"}see the planAn explicit override. The given text is used verbatim instead of any computed label — useful for prose links like "as we saw earlier". When present, text takes precedence over style and case.

If a :ref (or ::resource) names an id with no matching resource, the label falls back to ? (a text= label is printed instead, still with no number or link) and the sandbox raises an unknown resource warning. The engine records it as an unknownResourceId entry in the document's contentWarnings, with the reference's source range and page — also for a :ref inside a caption, a note or a table cell of a resource the text uses.

#First-reference numbering

A resource's number is assigned the first time it is mentioned in reading order — whether that first mention is a ::resource block embed or an inline :ref. From then on, every reference to the same id prints that same number.

This means numbers follow the order the reader meets them, not the order resources were created in the panel:

  • If you :ref a figure in the introduction and only embed it (::resource) two pages later, it still takes the introduction's number — the reference came first.
  • Inserting a new reference earlier in the document automatically renumbers everything after it. There is no manual numbering to keep in sync.

Numbering is per resource type and respects each type's reset scope and counter format — see Configuration › Resource types for the template tokens ({h1}, {n}), resetOn, and counterFormat.

Only what the text references is counted: a resource that only a design draws (an image element of a chapter opener, say) gets no number and leaves the counter as it was. And {h1} counts every level-1 heading whose style is not numbered: false, even one whose numberingTemplate is empty, so an article whose only H1 is its title numbers its figures 1.1, 1.2… by default. Configuration › What is numbered has both rules and the settings for Figure 1, 2….

#Placement

Each resource has a placement that decides where its float lands, resolved per resource (its own placement), then its type's defaultPlacement, then the built-in default of auto / column:

FieldValuesMeaning
position"auto" · "top" · "bottom" · "here""auto" (the default) takes the first free slot after the reference, top or bottom; "top" / "bottom" only accept slots of that kind; "here" opts out of floating and embeds inline at the ::resource directive (a table placed there is cut between rows when it does not fit the rest of its column, see Tables taller than the page).
width, align0 < width < 1; "left" · "center" · "right"A resource narrower than its slot: width is the fraction of the column (or page) width it takes, align where it sits in the slot (a small table centred in its column; a page-wide band whose picture spans one column). A picture narrower than its slot — a bitmap smaller than the column, or one layout.fitFiguresToPage shrank — also sits there per align, under a caption that keeps the slot's measure. Floats and inline ::resource embeds alike.
captionSidetrue · falseIn a one-and-a-half layout whose side column is reserved for floats (layout.sideColumnRole: 'floats'), a "column" float with captionSide keeps its body in the main column and sets its caption (and note) in the side column, level with the figure's top — or its bottom for a bottom float; the side column gives up that band. A page without such a column keeps the caption under the figure. Since postext 1.28 a resource set in the text ("here"), one column wide, takes it too: its caption and note stand in the side column, level with the top of its body, the block in the text is as tall as the body alone, and the side figures and boxes set after it keep clear of the caption. Where the side column is taken beside the embed, the caption stays under the body; an embed that moves to the next column or page decides again there. Not inside a box, on a vertical page, or for a picture the text wraps round. layout.inlineCaptionSide: false keeps side captions to column floats.
span"column" · "page"Occupy a single column, or break the column flow and span the full content width across all columns. In a single-column layout the two are identical.
columnsa whole number, default 1On a page of several columns, a "column" float across that many adjacent columns, gutters included: a picture across two of a newspaper's five columns. It takes the head of a run of empty columns that start level, or the foot of the citing column and of the empty columns after it. As many columns as the page has, or more, make it a page-span float. Ignored by "page" and "side" spans, by a turned resource and by an inline ("here") embed; captionSide applies only to a float one column wide. Since postext 1.18.
rotate"ccw" · "cw"Set the resource turned a quarter turn — a landscape table in a portrait book. "ccw" turns it counter-clockwise, its top facing the left edge of the page (the reader turns the book clockwise), the usual convention; "cw" the other way. A turned resource is always a page-span float on a page of its own: it is laid out along the height of the content area, rounded down to whole lines of the baseline grid and less one body line, the float gap every float band keeps (a 237 mm text area on a 14 pt grid holds 47 lines, 232.1 mm, so the resource gets 46 of them, 227.2 mm), sits flush to the spine when the margins are mirrored (flush left otherwise), and a table too wide for one page is cut between rows and continues, turned, on the following pages with its header repeated, exactly like an upright table taller than a page. A turned figure is scaled to fit the page. Ignored for an inline ("here") embed.
shrink, minScale"never" · "page" · "slot"; 0 < minScale ≤ 1, default 0.7Scale a floated picture (bitmap, SVG, a video's poster) down, keeping its proportions, to the room a slot has instead of moving it on. "never" (the default) sets it at its size. "page" shrinks it only when it is too tall for the band of a fresh page: the content area less what already stands there (an opener, other floats, footnotes) and the float gap, as for a catalogue plate sized to its page. "slot" also shrinks it into a slot of the page that cites it, or into the room left beside another band, when it fits there at minScale or more; otherwise it moves on as before. The slots are tried in their usual order, and the band still ends on the baseline grid. A picture with a safe area is cropped within it before it is scaled (see Safe area). On a fresh page, where it has nowhere else to go, a picture that does not fit even at minScale is set at that scale and runs past the band. The layout reports each shrunk float as a floatShrunk warning. Tables (which split), floated boxes, comic strips, turned resources and inline embeds are never scaled. The document default is layout.floatShrink. Since postext 1.24.
captionMeasure"slot" · "body"The measure of the caption and note of a picture narrower than its slot (shrunk, or a bitmap smaller than the column): "slot" (the default) keeps the slot's measure; "body" sets them as wide as the picture, under it per align, so a centred plate has its caption centred under it. Floats and inline embeds alike. Since postext 1.24.
wrap, wrapGap"none" · "left" · "right" · "start" · "end"; a lengthRun the text beside the resource: it sits at that side of its column, and the lines beside it are set to the measure left over, wrapGap clear of its box (caption included), then return to the full measure under it. An inline ("here") embed and a one-column float at the head or foot of a column take it; page-span, multi-column, side-column and turned floats keep their band. The resource takes width of the column, or layout.wrap.defaultWidth (0.45). wrapGap defaults to layout.wrap.gap, else one body line. See Text wrap. Since postext 1.24.
citingPagetrue · false, default falseLet a "top" or "auto" float take the head of the page (a page-span float) or of the column (a column float, across columns too) where the line that first cites it lands, as LaTeX's t placement does, instead of the first free slot after that line. The text above the reference moves down under the float. It does so only when the float fits there within layout.maxTopFraction of the column (0.7 by default), when the earlier floats of its numbering sequence are already set, when no explicit break opens the page (a chapter's first page, a part, a :::pagebreak) and nothing spanning the page stands above the citing line, and when, so set, the citing line still lands on that page (in a column the float heads); otherwise the float takes its usual slot. Turned floats, side-column floats and the rest of a table split across pages never move this way. Default: the type's defaultPlacement, then layout.floatsAtCitingPage. Since postext 1.25.

A float goes into the first free slot after its first reference, in reading order: the bottom of the column the reference sits in, then the top and the bottom of the next empty column of the same page, then the bands of the next page the flow opens (a page-span float takes the bottom of the page when every column still has room for it, else a band of the next page). A page an inline figure or table opens counts too: a float waiting for that page takes its head, above the figure or table, when both fit on it; when they do not, the figure or table keeps the page and the float waits for the next one. Up to postext 1.4 such a page was skipped, and the float waited for the page after it even when both fitted. It is set at its size unless its shrink lets it scale to the room of a slot (see the table above), and it never lands before its reference. Floats of one numbering sequence appear in reference order: a figure that fits nowhere on a page holds up the figures behind it (a waiting table does not hold up a figure, nor the other way round), so figure 12 never appears before figure 11. A table that would otherwise wait is cut instead: offered the head of an empty column, it takes the rows that fit and continues in the next slot — the column beside it or the next page — with its header rows repeated (see tableStyle.overflow). A top or auto float with citingPage on may instead head the page (or the column) where its citing line lands, the text above the reference moving down under it: it is then painted above its reference on the same page, never on an earlier one, and still read after it in a tagged PDF and in the HTML. The engine settles this over a few extra passes and keeps it only while the citing line stays on that page.

Floats never escape their chapter: at a chapter opener (a heading level with breakBefore or span: 'page'), a :::part, a callout style with floatBarrier: true (the closing "key points" box of a chapter) and at the end of the document, every float still pending is placed first — in the page's free slots, or on pages opened ahead of the boundary. A float first cited by the opener itself (a chapter title that names its figure), or by the first block after a :::part, belongs to the new chapter: it lands after that line, like any other. A figure or table that asked for the head of a page may then take the foot of the chapter's closing page, under its balanced columns, rather than a page of its own. A :::pagebreak simply sends pending floats to the page that follows it.

Under a page-wide opener set over two or more text columns, the opener's own column counts as empty at its head while nothing follows the opener in it: a floated box fenced right after the opener, or a resource embedded there with ::resource, takes the head of the first column, under the opener and level with the heads of the other columns (across several columns from it with columns), and the column's text starts under it. A float cited in running text still follows its citing line. Up to postext 1.24 the first column offered no slot there and such a float landed from the second column on; layout.floatsUnderOpener: false keeps that (see Configuration › Layout).

Side boxes (span="side") keep to their chapter too. A box the side column of its page cannot hold takes the side column of the next page; where the chapter, or the document, ends first, each box still waiting is set in the side column of a page opened after the text, in the order of their fences, and a side figure cited after them stands behind them. Such a page holds no text, so the build reports every box and figure on it (afterText). A box taller than an empty side column is set anyway and reported (calloutOverflow), and anything that still finds no page is reported too (unplaced) rather than dropped. Up to postext 1.24 only the first of those pages was opened and the boxes left were lost without a warning.

Float bands are corrected against the baseline grid so the surrounding text keeps the page-wide vertical rhythm. A top band grows its bottom margin to the next grid line, so the text below the float stays aligned with the neighbouring columns and the facing page. A bottom float is anchored so its caption's last line shares a baseline with the last text line of the other columns (captionless content aligns its bottom edge to the last grid slot) — full pages therefore end at the same height across columns and across facing pages.

On the closing page of a chapter — and of the document — nothing follows the page-wide figures and tables set below the last band of text, so they move up to sit one float gap under it, stacked in their order, instead of leaving white between the text and a figure at the page foot. That holds for a position: 'bottom' float too. To keep them at the foot, as on every other page, set layout.hugClosingFloats: false (see Configuration › Layout). Pages with a side column never move them.

All three backends — the canvas preview, the HTML viewer, and the PDF output — render resources. In the HTML backend, image payloads live out-of-band, so the host supplies them through the resourceImageUrl(fileId) resolver option; when the resolver is missing (or returns nothing for a file), the resource renders as a neutral placeholder box so the layout stays stable. A video's own file comes through resourceVideoUrl(fileId) the same way (see Videos).

Where a float can land

"Never before its reference" counts from the line that cites the float: the float waits until that line is set, and takes the first free slot after it in reading order — the head of the page the line is set on comes before it. A side float (span: 'side', in the float-only side column of a one-and-a-half layout) is the exception: it stacks in the side column beside the text that cites it, and may sit higher on the page than the citing line. For a figure cited on page 5:

PlacementOn page 5Otherwise
top, span: 'page'Never: the band at the head of the page is above the citing line.The top band of page 6.
top, span: 'column'The head of a later column that is still empty — cited in the first of two columns, it can open the second.The head of a column of page 6.
auto or bottom, span: 'page'The foot band of page 5, when every column still has room for it.A band of page 6.
auto or bottom, span: 'column'The foot of the citing column; then the foot (either) or the head (auto only) of a later empty column.Page 6.
  • A chapter opener page follows the same rules: a page-span auto or bottom float cited in the opener's first paragraph can take that page's foot band, under its text; a top one opens the next page. To have the picture on the page that cites it, use auto or bottom, or draw it as an image element of the opener's design.
  • Two column floats cited in the same paragraph take the next two slots: typically the first the foot of the citing column, the second the head of the next empty column — side by side on the page.
  • In a single-column layout a page-span float is a column float, and the same rules hold: a top float cited on a page lands at the head of the next one.
  • A paragraph that runs from one page onto the next counts from its citing line too. When the paragraph begins at the foot of page 5 and the reference falls on page 6 — or the whole paragraph moves to page 6, because not enough of its lines fit at the foot of page 5 — the reference is on page 6: a top float opens page 7, and an auto one takes the foot of page 6 when there is room for it. The same holds between the columns of a page, and for a box split across pages: a figure its second part cites waits for that part.
  • Changed in postext 1.5. Up to postext 1.4 a float was queued when the layout reached the paragraph that cites it, so the page such a paragraph ran onto (or moved to) could open with the float, above the line holding the reference. Such a figure now lands a page later, or at the foot of the page when it asks for auto; a page-count change can follow.

#Text wrap

A picture, a table or a box narrower than its column can stand at one side of it with the text running beside it, as in a magazine or a newsletter. Give its placement a wrap side and, if you like, a width:

{ "id": "bed", "typeId": "figure", "kind": "svg", "caption": "The new planting bed.",
  "placement": { "position": "here", "wrap": "right", "width": 0.4 } }
  • Inline ("here"). The resource sits at its side of the column, its top level with the top of the next line at the ::resource line, and the paragraphs, quotations and list items that follow run beside it at the measure left over, then under it at the full measure. A paragraph that starts beside it and ends under it stays one paragraph: no break, no extra space at the picture's foot. Every line stays on the baseline grid; the room under the picture is its gap rounded up to whole grid lines.
  • A column float. A span: "column" float one column wide, at the head ("top", or "auto" at the head of a column) or the foot ("bottom", "auto") of a column, is placed in its band as any float; the column's first lines, or its last ones, then run beside it. On a page of two columns the float belongs to its column, and the other runs full.
  • A box. A :::callout takes the same settings as attributes: :::callout{wrap="right" width=0.4} sets a pull quote or a sidebar where it occurs, with the text after it beside it, and placement="top" or "bottom" with wrap floats it to the head or foot of a column. A wrapped box is laid out whole and never splits.
  • What goes under it. A heading, a display formula, a figure or table, a box and a poem do not run beside a wrapped item: they go under it, and the white beside it above them stays empty. Leaving the column (its foot, a :::columnbreak, a page break) ends the wrap there: the item belongs to its column. When the block right after an inline resource cannot run beside it, the resource takes its band, as without wrap.
  • Gap and limits. wrapGap (else layout.wrap.gap, else one body line) separates the item, caption included, from the text beside it and from the text under it. Where the text beside it would be narrower than layout.wrap.minTextWidth (12 em of body text), or the item is shorter than layout.wrap.minLinesBeside lines (2), it takes its band whole and a textWrap warning says so. An inline one too tall for the room left in its column moves on to the next with its anchor, with the same warning.
  • Sides. left and right are sides of the body flow, as for align: in a right-to-left book left is the sheet's right. start and end name the same sides.
  • Outputs. The canvas, the HTML viewer, the PDF and the fixed-layout EPUB set the lines as they were laid out; a tagged PDF reads the picture or the box before the paragraph that runs beside it. A reflowable EPUB floats it to its side with CSS, at the same share of the text width.
  • Not yet. Text on both sides of a picture, contour wrapping, wrapping inside a box's own content, and wrapping in vertical text (the item keeps its band, with a warning). Columns with a wrapped item are left out of column balancing.

#Bitmap size

A bitmap declares its pixels: bitmap.width and bitmap.height are the file's pixel counts, not a print size. How many of them go to an inch decides how large the picture is set before its slot caps it. There are three ways to size one:

  • Its pixels at the page's dpi (the default). Without a resolution, one pixel of the picture is one pixel of the layout (page.dpi, 300 unless set): a 2400 px photograph is 8 in (203.2 mm) wide on a 300 dpi page, and twice that on a 150 dpi one.
  • A resolution. bitmap.resolution is the picture's ppi at its natural print size, so its natural width is width × page.dpi / resolution on a page of any dpi: 2400 px at resolution: 300 are 203.2 mm wide on a page laid out at 150 dpi or at 300. layout.bitmapResolution gives every bitmap without one of its own a resolution: a number (300), or 'file', the resolution its file states (bitmap.fileResolution: a PNG's pHYs, a JPEG's JFIF or EXIF density, a WebP's EXIF; for a picture imported from Word, the size Word printed it at). Cameras, screenshots and web exports often state 72 or 96 ppi when they know nothing of a print size, so under 'file' those two count as unset and the picture keeps its pixels at the page's dpi; a 72 declared on the resource itself is taken as written. 'document', the default, is the first way.
  • A share of the column. placement.width narrows a float or an inline embed to a fraction of its column (or page), and a picture larger than that share is scaled down to it, whatever its pixels (see Placement).

In every case the slot caps the picture, keeping its proportions, and a smaller picture is never enlarged. The same natural size applies to a picture in a table cell and to a design's image element with an 'auto' size. The PDF embeds the file as it is, at its placed size. The preflight checks the resolution each picture prints at, and the Sandbox's Resources panel shows it for each picture. Since postext 1.24.

{ "id": "harbour", "typeId": "figure", "kind": "bitmap", "caption": "The harbour at dawn.",
  "bitmap": { "fileId": "harbour.jpg", "format": "jpeg", "width": 2400, "height": 1600, "resolution": 300 } }

bitmapInfo(bytes) (from postext and postext/bundle) reads a file's pixels and the resolution it states, { width, height, resolution?: { x, y, source } }; a bundle opened under 'file' fills fileResolution with it, and the Sandbox keeps it for every picture it uploads.

#Safe area

A bitmap or SVG resource can mark a safe area: the rectangle of the picture that holds what matters (a person and a strip of the landscape behind them, the plotted part of a chart) and is always shown. It is the resource's safeArea, four fractions (0–1) of the picture's intrinsic size measured from its top-left corner: x and width are fractions of its width, y and height of its height. The name says what has to be kept; a crop box would say where to cut.

const harbour: Resource = {
  id: 'harbour', typeId: 'figure', kind: 'bitmap', caption: 'The harbour at dawn.',
  createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'harbour.jpg', format: 'jpeg', width: 2400, height: 1600 },
  // the boats and the quay: 18–68 % of the width, 25–85 % of the height
  safeArea: { x: 0.18, y: 0.25, width: 0.5, height: 0.6 },
};

Only a picture with a safe area is ever cropped. The engine may then show it at any aspect ratio between the whole picture and the safe area: taller than the picture's own ratio by cropping the sides, at most down to the safe area's width, or shorter by cropping top and bottom, at most down to its height. The width of the figure never changes, only its height. What lies outside the safe area is cropped on both sides in proportion to the margins there, so a subject left of centre stays left of centre. Set 60 mm wide, the harbour above is 40 mm tall when whole, and can be shown anywhere from 24 mm tall (top and bottom cropped down to 60 % of the height) to 80 mm tall (the sides cropped down to half the width). Values past the picture's edges are clamped to it; an area with a value that is not a number or a side under 2 % of the picture, or one that covers the whole picture, is ignored, and so is the field on a table. Without a safe area a picture is always shown whole, as before.

The engine uses that room in four places:

  • The room left in a column. An inline picture (position: 'here') a little too tall for the room left in its column, including one whose caption would run past the foot of the page, is cropped within its safe area to stay there, instead of moving on to the next column or page and leaving this one short.
  • Pages shorter than the figure. With layout.fitFiguresToPage, a picture too tall for the page is first cropped within its safe area, keeping its width, and only scaled down if it is still too tall (see Configuration › Layout).
  • Bands shorter than the figure. A float whose placement.shrink scales it to the room of its slot is cropped within its safe area first, keeping its width, and only then scaled down; the flexFigure lever below never grows it past that room.
  • Column balancing. A picture with a safe area set inline in a short column, or floated at the head or the foot of that column over that column alone, grows by whole baseline-grid lines to fill the column's empty lines. A taller picture leaves no visible hole, so this lever, flexFigure, is tried right after the box closing the column and before any space is added above headings or after lists. A float at the head of a column on a chapter's closing page or in a closing band does not grow, since those column heads stay level (see Configuration › Column balancing).

The VDT records the crop on the resource block: bodySource is the part of the picture shown in bodyRect, in the same fractions (absent when the whole picture fills it), and bodyFlex gives the px the body could still shrink and grow and the px the levers already changed it by ({ shrink, grow, delta }). The canvas preview and the PDF clip to the body and draw the whole picture at its uncropped size; the HTML viewer and fixed-layout EPUB set the image with object-fit: cover and a matching object-position; a reflowable EPUB, which has no page to fill, shows the whole picture. A host that draws pictures itself can use the helpers postext exports: resourceSafeArea and normalizeSafeArea (the area as the engine reads it, or undefined), safeAreaHeightRange (the shortest and tallest body at a width), safeAreaSource (the part shown at a given height) and uncroppedPictureBox (where to draw the whole picture before clipping), with the type ResourceSafeArea. In the Sandbox the field is Safe area on a picture in the Resources panel.

Recipe Nº 116 in the Cookbook, Photos that grow to fill a short column, sets a two-column magazine feature twice, without and with safe areas, so you can compare the pages one beside the other.

#Speaker anchors and avoid zones

A picture that is a comic panel's art can also say where its people are, for the lettering (see Comics). anchors lists one point per speaker and avoid the regions no balloon covers, both in fractions of the picture like the safe area:

const radio: Resource = {
  id: 'lh-radio', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'lh-radio.jpg', format: 'jpeg', width: 1000, height: 1000 },
  safeArea: { x: 0, y: 0.36, width: 0.62, height: 0.56 },
  anchors: [
    // the keeper: mouth, head (for thought balloons) and face
    { id: 'tomas', x: 0.345, y: 0.52, head: { x: 0.38, y: 0.44 }, face: { x: 0.29, y: 0.4, width: 0.16, height: 0.18 } },
  ],
  avoid: [{ x: 0, y: 0.44, width: 0.22, height: 0.26 }], // the radio
};

An anchor's id is the speaker key of the script (tomas: …), or sfx for the point the panel's sound effects gather at; x and y are the mouth a speech balloon's tail points at, head the point a thought balloon's bubbles point at (the mouth when unset) and face a rectangle no balloon covers. The engine maps every point through the panel's crop and mirror, so a tail stays on its speaker whatever shape the panel takes; an anchor outside the safe area raises comicAnchorOutsideSafeArea, because a crop may cut it off. A third list, faces, holds the faces of people who do not speak, as rectangles like those of avoid: no balloon covers them, where an avoid zone gives way when a panel has no other room. Anchors belong to the picture and serve every translation of the comic. Only comic panels read them; a figure ignores them. In the Sandbox they are marked in the Safe area & lettering dialog of the Resources panel.

#Videos

A video resource (kind: 'video') is a YouTube or Vimeo video, or a video of your own: a file kept in the book (MP4, WebM) or one at a web address, a file on a server or an HLS stream (an .m3u8 playlist). It is placed, captioned, floated and cited like a picture, and numbered in a sequence of its own: the built-in video type prints Video 1.1, Video 1.2… beside Figure 1.1 and Table 1.1 (see Configuration › Resource types). Each output shows it as it can:

OutputWhat it shows
Canvas, PDFThe poster, a frame of the video, with a play mark and a QR code that opens the video (Configuration › Video style). In the PDF the poster is also a link to the video.
FolioThe poster, as printed. A click on it plays the video on the page itself, and it goes on playing while the leaf turns (see Videos on Folio's pages below). YouTube and Vimeo videos stay posters.
HTML viewerThe video's player: YouTube's or Vimeo's, or the browser's HTML5 player for a file or an address. An HLS stream plays natively in Safari and in recent Chrome; elsewhere the host attaches a player such as hls.js, as the Sandbox does. With videoStyle.html: 'poster' it shows the printed poster instead.
EPUBA file plays in the reading system's own player, packed in the book or read from its production address. A YouTube or Vimeo video is its poster, linked to the video: an EPUB may not embed a web page's player. An HLS stream is its linked poster too: an HLS playlist is no media type an EPUB may play. Where the reading system runs scripts, the files of one page or chapter keep to player.exclusive: starting one pauses the others it does not play alongside.
const talk: Resource = {
  id: 'keeper-talk', typeId: 'video', kind: 'video',
  caption: 'The keeper explains the lamp.',
  altText: 'A lighthouse keeper beside the lamp',
  createdAt: 0, updatedAt: 0,
  video: {
    source: 'youtube',                        // 'youtube' | 'vimeo' | 'file'
    url: 'https://youtu.be/aqz-KE-bpKQ',
    poster: { fileId: 'talk.jpg', format: 'jpeg', width: 1280, height: 720 },
    start: 30,                                // plays from 0:30; the printed link starts there too
  },
};

The text cites a video as it cites a figure: :ref{id="keeper-talk"} prints Video 1.1 and floats the poster to the first free slot after the reference, and ::resource{id="keeper-talk"} with placement.position: 'here' sets it inline. The fields of Resource.video (ResourceVideo):

FieldTypeMeaning
source'youtube' · 'vimeo' · 'file'Where the video plays from: a YouTube or Vimeo page, or a video of your own ('file'), uploaded or at an address.
urlstringYouTube or Vimeo: the video's address, in any form (a watch page, youtu.be, Shorts, an embed link, youtube-nocookie.com; a Vimeo page, channel page or unlisted link with its hash). A file: its production address, where the published book will find it. A video of your own with no file of its own plays from this address: an MP4 or WebM on a server, or an HLS stream (…/master.m3u8).
fileId, formatstringA file: the uploaded video, stored out of band like a bitmap, and its format: 'mp4', 'webm', 'ogv', 'mov', or 'hls' for an HLS stream. Without a format, the address's extension tells it (.m3u8 is 'hls'), else 'mp4'.
posterThe poster frame, a bitmap.
width, height, durationnumberThe video's frame size in px (its ratio when there is no poster) and length in seconds.
posterTimenumberThe second of a file's frame used as the poster, kept so it can be picked again.
start, endnumberPlay from start to end seconds. The QR code and the PDF link start at start too.
playerVideoPlayerOptionsThis video's player options, laid over videoStyle.player.

The poster. It is set at the measure of its slot (or the placement.width fraction of it), in its own ratio, whatever its pixel size: a screen-sized frame is enlarged rather than printed small. Like a bitmap it can carry a safe area and follow placement.align. A video without a poster prints a dark box in the video's ratio (16:9 when nothing says) with its overlays, and raises a videoWithoutPoster content warning. In the Sandbox the poster of a YouTube or Vimeo video is fetched from the platform when its address is entered, and a file's is a frame you pick in the video.

The address. The QR code and the PDF link open the video's page, https://youtu.be/<id> or https://vimeo.com/<id>, from start. A file has no page: they open its production address, which is also where a video with no file of its own plays from (an MP4 on a server, an HLS stream). Without one, print gets no QR code and no link (videoWithoutUrl), and an HTML or EPUB output that does not carry the file shows the poster. A YouTube or Vimeo address that names no video of that platform raises videoUrlInvalid; the poster is shown in place of a player.

The players. The player options (controls, download button, full screen, speed menu, picture in picture, casting, autoplay, a muted start, loop, preload, privacy-enhanced embeds) are set for the whole book in videoStyle.player and for one video in video.player; each player honours what it can (see Configuration › Video style). The HTML backend takes the playable URL of a file through the resourceVideoUrl(fileId) option, as it takes pictures through resourceImageUrl, and falls back to the production address. Its videos: { files, streams, hls } option chooses between player and poster per source, over videoStyle.html (hls, a video played from an HLS address, follows files unless set); the EPUB writer uses it. A <video> that plays an HLS address carries data-pt-hls, so a host can give it a player in browsers without native HLS (Safari and recent Chrome play it as it is). In a right-to-left book the player and the poster's overlays are turned back like a picture, so the QR code reads.

In the VDT. The resource block of a video has kind: 'video', its poster as fileId / format (drawn like a bitmap), and video (VDTResourceVideo): the link a reader is sent to, the YouTube or Vimeo embedUrl with the player options applied, a file's fileId, the mimeType of a video of your own (of its file or of its address alone, application/vnd.apple.mpegurl for HLS), the start / end range, the resolved player, linkPoster, html, and the overlays playMark and qr (the QR modules as rows of '0' / '1'), with rects relative to the body's top-left corner. A host drawing videos itself can use the helpers postext exports: parseVideoUrl, videoWatchUrl, resourceVideoLink, videoEmbedUrl, videoEmbedAllow, videoElementAttributes, mediaFragment, videoMimeType, videoFormatOfUrl, resourceVideoFormat, isHlsMimeType, HLS_MIME_TYPE, youtubePosterUrls, encodeQr, layoutVideo, playMarkTriangle and qrModuleRuns.

In a bundle. A .postext carries a file (resources/<id>.mp4) and the poster (resources/<id>.poster.jpg): the manifest entry names them in file and poster, gives the poster's size in width / height, and keeps the rest in video without file ids. bundleVideoUrl(bundle) is the resourceVideoUrl resolver over a bundle's files. A video played from an address alone carries no file: its entry has only the poster. In a bilingual bundle a language can give a video its own poster and video (another cut, at its own address) in localized[…].resources.

Videos on Folio's pages

In the 3D book a video of your own plays on the page it is printed on. A click on the poster starts it there, whatever the pointer does on the book (turning pages, orbiting the view or selecting text): the page's shader draws the video's frames over the poster, so the picture bends and curls with the paper while the leaf is turned, and it goes on playing during the turn. A click on a playing video pauses it, a click on a paused one resumes it, and Space or Enter does the same for the first video on the open spread. Starting another video stops the one playing (it goes back to its poster), and a video stops when the book comes to rest on a spread that no longer shows it, or at its end. The pointer turns into a hand over a video it can play; a drag that starts on one still turns the page or the view. A video set to play on its own (video.player.autoplay) starts by itself the first time the book comes to rest on its spread, once the book has settled (a host laying it out in steps included), and never again in that viewer.

A video that plays alongside the others (video.player.exclusive: false, since postext 1.18) changes both rules. Starting it stops only the exclusive videos, so several such videos play at once; starting an exclusive video stops it too. With autoplay it starts, muted, each time the book comes to rest on its spread, and stops when the spread is turned away: the silent loops (loop) of a page run together every time the reader turns to it.

  • What plays. A file (the host turns its fileId into a URL with the videoUrl option, an object URL in the Sandbox) or, without one, its address: MP4, WebM or an HLS stream. An HLS stream plays through hls.js wherever the browser has Media Source Extensions (loaded on the first play; an optional peer dependency of postext-folio), capped at the variant nearest the picture's size on screen, so a picture a few hundred pixels tall never pulls a 4K variant; the browser's own HLS is the fallback. Its requests carry a pt-cors query of their own, so the browser's cache never hands them a copy another player fetched without CORS. YouTube and Vimeo play in iframes, which WebGL cannot draw: a click on their poster turns the page as anywhere else.
  • Cross-origin reads. WebGL draws a video only when its server allows the read (CORS, Access-Control-Allow-Origin). A video that cannot be read stays a poster, and onVideo hears error.
  • Sound. The click lets the video play with its sound; a browser that still refuses gets it muted. A video that starts on its own has its sound once the reader has clicked or typed on the page (turning to it counts); before that it starts muted, and a click on it unmutes it before the next one pauses it. video.player.muted starts it muted, loop plays it again, and start / end give the part that plays.
  • On every page. The picture is drawn where the layout puts it: turned with a rotated resource, upright on a vertical page, and unmirrored in a right-to-left book, like the poster. A poster cropped within its safe area crops the video the same way.
  • Without WebGL2, or with reduced motion, the video plays in an HTML <video> laid over the page.
const book = createFolioFromDocument(container, doc, {
  // An uploaded video's playable URL (an object URL over its bytes).
  videoUrl: (fileId) => urls.get(fileId),
  onVideo: ({ resourceId, state }) => console.log(resourceId, state), // playing | paused | stopped | error
});
book.stopVideo();

videos: false turns it off. toggleVideoAt({ page, x, y }) does what a click there does, and pageVideoSpots(page, doc) lists where each video lies on a page, for a host that draws its own controls.

Two Cookbook recipes set videos: Nº 131, A film club programme with QR codes on the posters, prints each film as a title card with a play mark and a QR code, and Nº 132, A lab sheet whose clips play on screen and in the EPUB, sets the player options for the HTML edition and packs its own clips in a fixed-layout EPUB.