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

  1. its kind’s html write tag (img / audio / video),
  2. else the major type of its type attribute (image/… / audio/… / video/…),
  3. else media-type inference from the src extension.

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 ![alt](src "title") 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 trackmeta.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 / end are seconds (fractional). narration.src overrides 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 id just 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:

  1. A relative path — the canonical, preferred form. POSIX separators, resolved against the containing file’s folder (plain browser semantics: a .spub.html must 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.
  2. A data: URI — valid everywhere a reference is; the self-contained form. An exporter MAY embed a document’s assets as data: URIs to produce a single portable file; tooling SHOULD warn above ~1 MB per asset. Adapters pass data: URIs through untouched, never inflate or extract them.
  3. 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 folderMoth-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 collectioncollectAssets(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 script elements, 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 (![alt](src)); audio/video/figure via media-element passthrough full (![alt](src "title")) 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: 2 unless it contains any media construct — an object run, any media attribute (type width height poster controls loop muted narration), a mark title, or the coverAlt / narration / narrator meta 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.