Media
Media — images first, audio and video beside them — enters the model through
two doors, both already in the format’s grain: a media block is a
void-shaped block whose attributes carry
the source, and an inline image is an atomic
object run inside a text block. The bytes
themselves are never in the model: the format stores references (paths),
and resolving a reference to bytes is a store
concern — the same division of labor as everywhere else in the standard.
Media constructs are wire version 3 — see versioning at the bottom of this page. A document that uses none of them keeps parsing and saving as version 2.
Media blocks
A media block is a block type with shape: "void" whose instances carry a
src. There is no new shape and no new BlockDef field: image, audio, and
video are ordinary block types, declared in config.blocks
like any other, with their identity in their per-syntax spelling.
The recommended kinds — shipped by the built-in presets, but just data:
{ "id": "image", "shape": "void",
"style": { "max-width": "100%", "height": "auto", "display": "block" },
"syntax": { "html": { "match": [{ "construct": "img" }], "write": { "tag": "img" } },
"md": { "match": [{ "construct": "image" }], "write": { "construct": "image" } } } },
{ "id": "audio", "shape": "void",
"style": { "width": "100%", "display": "block" },
"syntax": { "html": { "match": [{ "construct": "audio" }], "write": { "tag": "audio" } } } },
{ "id": "video", "shape": "void",
"style": { "max-width": "100%", "height": "auto", "display": "block" },
"syntax": { "html": { "match": [{ "construct": "video" }], "write": { "tag": "video" } } } }
Three kinds, not one media kind — they style differently, edit
differently (only video takes a poster), and degrade differently. But a
renderer never keys on the kind ids: what a media block is resolves, in
order, from
- its kind’s html write tag (
img/audio/video), - else the major type of its
typeattribute (image/…/audio/…/video/…), - else media-type inference from the
srcextension.
So a format that renames or re-declares the kinds loses nothing, and a void
block with a src and no recognizable tag still renders as something.
Inline images — the object run
A run is either a stretch of text or an atomic inline
object — exactly one of text / object is present:
Run = { text?: string, object?: InlineObject, marks: MarkUse[] }
InlineObject = {
type: "image", // closed vocabulary; future members are additive
src: string, // same value grammar as a block src (below)
alt?: string,
title?: string, // CommonMark  parity
width?: number, // intrinsic CSS px
height?: number,
}
An object run may carry marks like any run — a linked inline image is an
object run under an href mark. Two projections are normative:
- As an editing position the object is one caret position and fully atomic: a splice overlapping it removes it whole, a slice never cuts it, and a mark applied across it applies to the whole object.
- As plain text — what kind inference predicates and
content-derived serialization see — the two uses differ deliberately:
inference projects the empty string (a paragraph opening with a
decorative image still matches its prefix/caps rules), while
content-derived syntaxes serialize
alt ?? ""(a plain-text reader gets the description, not a silent hole).
Inline media stops at images: inline audio or video is not representable, by
design. InlineObject.type is the extension point if that ever changes.
The media attributes
The attribute vocabulary additions, all on blocks:
| Attribute | Type | On | Description |
|---|---|---|---|
src |
string |
void (media) blocks | The media source — a reference, never bytes. |
alt |
string |
void (media) blocks | Accessible description. Images: the alt attribute; audio/video: an aria-label. |
type |
string |
void (media) blocks | Explicit media type (MIME), e.g. "image/svg+xml". Usually inferred from the src extension; state it for extensionless sources and unusual data: payloads. Feeds the EPUB manifest. |
width, height |
number |
void (media) blocks | Intrinsic dimensions in CSS px (integers). A content fact, not styling — they keep layout stable before the media loads and size media on paged/e-ink surfaces. Presentation sizing stays in style. |
poster |
string |
video blocks | Poster frame (same value grammar as src). Also the video’s visual stand-in on surfaces that can’t play it. |
controls |
boolean |
audio/video blocks | Playback controls. Defaults true — a reader must be able to operate the media. |
loop, muted |
boolean |
audio/video blocks | Default false. |
narration |
{ src?, start, end } |
any block | A narration clip — this block’s slice of the work’s narration audio. |
There is deliberately no autoplay (an accessibility hazard that EPUB
reading systems ignore anyway) and no srcset / multi-source list in this
revision — one source per block; the EPUB core media
types make a single source safe. On parse,
an HTML <picture> resolves to its <img>, and an <audio>/<video> with
<source> children promotes the first source’s src + type — the rest
drop, a documented lossy import.
Figures & captions
A captioned figure is not a new construct — it is the existing single-cell
grid: a figure grid block whose one cell
holds the void media block plus a figcaption-tagged text block. The media
attributes stay on the inner void block; the figure grid carries none of them.
{ "kind": "figure", "rows": [[ { "blocks": [
{ "kind": "image", "attrs": { "src": "assets/moth.png", "alt": "A moth on linen",
"width": 800, "height": 600 } },
{ "kind": "caption", "runs": [ { "text": "Fig. 1 — the moth.", "marks": [] } ] }
] } ]] }
In HTML this is <figure><img …/><figcaption>…</figcaption></figure>,
exactly as a browser expects. The same shape captions audio and video.
Narration alignment
The bridge to audiobooks. A work may carry a narration track —
meta.narration, a source reference to the master audio — plus, on any block,
a clip into it:
// config.meta
{ "title": "…", "narration": "assets/narration/book.m4a", "narrator": "Ruby Dee" }
// a narrated block
{ "kind": "paragraph", "runs": [ … ],
"attrs": { "narration": { "start": 12.25, "end": 17.5 } } }
start/endare seconds (fractional).narration.srcoverrides the document-level track per block — real audiobooks clip one master file per chapter, so most blocks state only their times.- The clip lives in attrs, per block — not in a config-side map — so
alignment survives block moves, splits, and merges through ordinary editing,
and no block needs a persisted
idjust to be narrated. - This is exactly the granularity of an EPUB 3 Media Overlay: one SMIL
<par>per narrated block. The epub package defines the mapping; emitting it is an encoder capability, not a model concern. A renderer with audio support MAY offer read-aloud (play the clip, highlight the block); every other renderer ignores the attribute.
meta.narrator names the narrator (→ EPUB media:narrator). Like every
attribute, narration persistence is tiered — it
survives html and json, and drops in the plain-text syntaxes.
Asset references & resolution
The reference grammar
Every media reference — block src, poster, narration.src, inline
object.src, meta.cover, meta.narration — takes one of three forms:
- A relative path — the canonical, preferred form. POSIX separators,
resolved against the containing file’s folder (plain browser
semantics: a
.spub.htmlmust open from disk, unmodified, with its images working)...segments are legal but resolution MUST NOT escape the store root; tooling warns, packaging errors. In a multi-file work a part’s references resolve against the part’s folder, and assembly rewrites them index-relative. - A
data:URI — valid everywhere a reference is; the self-contained form. An exporter MAY embed a document’s assets asdata:URIs to produce a single portable file; tooling SHOULD warn above ~1 MB per asset. Adapters passdata:URIs through untouched, never inflate or extract them. - An absolute
http(s)URL — valid but non-portable; see the EPUB packaging rules for how remote references export.
The assets/ folder
By convention a work’s asset files live in an assets/ folder inside its
work folder — Moth-Stories-by-A-Author/assets/moth.png, referenced as
assets/moth.png. The folder is inert cargo:
**/assets/** joins the default scan excludes, so asset files are never
indexed as works, yet always travel with the folder — through library export,
copy-between-libraries, and static serving alike. A cover is just an asset
(meta.cover: "assets/cover.jpg"); nothing about covers is special except
the meta key that points at one.
The resolver contract
The format never reads bytes. The engine’s part is pure reference
collection — collectAssets(doc) walks a document (content, grid cells,
inline objects, meta.cover, meta.narration, every poster and
narration.src) and returns the deduplicated relative references in document
order, skipping data: and absolute URLs. Everything after that is tooling
over the store’s read:
collectAssets(doc: ParsedDocument): string[] // pure, in @script-pub/format
PackagedAsset = { // what tooling hands an encoder
src: string, // the reference exactly as the document states it
name: string, // the archive/output path the encoder should use
data: Uint8Array,
mediaType: string,
}
A package encoder receives resolved assets and stays store-blind, synchronous, and deterministic — the same contract the EPUB cover already uses, generalized to all media.
Media-type inference
A shared, normative extension → MIME table; an explicit type attribute
always wins:
| Extensions | Media type |
|---|---|
png |
image/png |
jpg jpeg |
image/jpeg |
gif |
image/gif |
webp |
image/webp |
avif |
image/avif |
svg |
image/svg+xml |
mp3 |
audio/mpeg |
m4a aac |
audio/mp4 |
wav |
audio/wav |
flac |
audio/flac |
ogg oga |
audio/ogg |
mp4 m4v |
video/mp4 |
webm |
video/webm |
smil |
application/smil+xml |
The epub package warns when a packaged type falls outside the EPUB core media types.
SVG
SVG is a first-class image format — referenced like any raster
(src: "assets/diagram.svg", type image/svg+xml), and the right choice for
line art on e-ink. Because SVG is markup that can carry script, the rules are:
- The format never embeds SVG markup inline — reference only; there is no raw-markup block.
- Rendering contexts treat a referenced SVG as an image (an
<img src>), which browsers script-neuter. - Import tooling SHOULD sanitize SVG entering a store — strip
scriptelements, event attributes, and external references. (Informative; tooling-level, like all byte handling.)
Degradation by syntax
Media follows the standard tiered-persistence posture — the full table:
| Syntax | Block media | Inline images | Media attrs | narration |
|---|---|---|---|---|
html |
full (<img> <audio> <video>) |
full (inline <img>) |
full | full (data-narration) |
json |
full | full | full | full |
md |
image blocks full (); audio/video/figure via media-element passthrough |
full () |
width/height/type/flags drop |
drops |
txt / fountain / fdx |
drops (nothing emitted) | serialized as alt ?? "" |
drop | drops |
No syntax invents a convention it doesn’t own (there is no Fountain
[[image: …]]); the words a reader sees always survive, and everything else
survives exactly where it can be stored.
Covers & cards
meta.cover names a work’s cover image (Configuration —
the EPUB-convention <meta name="cover">); meta.coverAlt describes it. How
a catalog surface renders one work’s entry follows the normative preference
chain defined in Libraries:
cover image → cover view → generated card. The card itself, the shelf, and
the display defaults are catalog concerns — see that page and
library.display.
Media and the wire version
The constructs on this page are what moves the wire counter to
scriptpub: 3: an object run has no text, and a version-2 engine
assuming one would fail on it rather than degrade; the new attributes would be
silently stripped by a version-2 engine’s resave — not tiering, corruption.
The gate makes both failures loud instead. The rules:
- Engines of this revision accept versions 2 and 3.
- Serializers emit the lowest version that expresses the document:
2unless it contains any media construct — an object run, any media attribute (typewidthheightpostercontrolsloopmutednarration), a marktitle, or thecoverAlt/narration/narratormeta keys. - So every existing document — and every new one that never touches media — stays version 2 and opens in any older engine. The first media insertion flips a document to 3; an editor SHOULD say so.
New block kinds, new CSS properties, and new meta keys need no gate — they were additive already.