Readium Logo

Decorator API

Summary

This proposal introduces a new Navigator API to draw decorations on top of publications, in a media type agnostic way. This new API is a building block for higher-level features such as highlights.

Motivation

A variety of features need to draw user interface elements (decorations) over a publication’s content, such as:

The actual rendering routines depend on the media type of the decorated resources. We can simplify reading apps significantly by providing a media type agnostic Navigator API to handle decorations.

Developer Guide

Terms

Creating a New Decoration

The Decoration object associates a location (Locator) in a publication with a decoration style to render. By relying on the style to indicate its look and feel, Decoration avoids any specific rendering instructions. This allows to reuse the same decorating code for any kind of publication or different Navigators.

Each Decoration object has an identifier which must be unique in the decoration’s group. This is necessary to compute efficiently changes when applying updates. A database ID is a perfect candidate.

Decoration(
    id: "12",
    locator: Locator(...),
    style: Decoration.Style.MarginIcon(image: "bookmark")
)

Note that a single entity might be rendered using several Decoration objects. For example, a highlight with a text annotation needs two Decoration objects: one for the actual highlight and one to draw the “text annotation” icon in the page margin.

Applying Decorations to a Navigator

A reading app never explicitly asks the Navigator to render a decoration. Instead, the app declares the current state of the decorations for a given decoration group and the Navigator will decide when to render each decoration. Navigators use a diff algorithm to determine the actual changes since the last applied state to run efficient rendering instructions.

// First, fetch highlight models from your database.
let highlights = ...

// Then, create one new decoration per highlight.
let decorations = highlights.map { highlight in
    Decoration(
        id: highlight.id,
        locator: highlight.locator,
        style: Decoration.Style.Highlight(tint: highlight.color)
    )
}

// Finally, apply the decorations in a group named "user-highlights".
navigator.applyDecorations(decorations: decorations, in: "user-highlights")

Handling User Clicks on Decorations

To handle user clicks/taps on a decoration, implement the DecorationObserver interface:

class MyObserver: DecorationObserver {
    func onDecorationActivated(event: OnActivatedEvent) -> Boolean {
        // Present a highlight pop-up for `event.decoration`, for example.
    }
}

Then, register your observer for the group of decorations you want to be interactive.

navigator.registerDecorationObserver(group: "user-highlights", observer: MyObserver())

Registering Decoration Styles

Reading apps are welcome to register new decoration styles for custom use cases. The API depends on each Navigator, but here’s an example using an HTML Navigator (e.g. for EPUB) to implement a sidemark:

// Declare a new style with associated configuration values.
class Decoration.Style.Sidemark {
    let tint: Color?
}

// Create a new HTML decoration template which will be used with our new style.
let sidemarkTemplate = HTMLDecorationTemplate(
    layout: .bounds,
    width: .page,
    element: { decoration in
        let style = decoration.style as? Decoration.Style.Sidemark
        let tint = style?.tint ?? Color.red
        """
        <div><div class="sidemark" style="--tint: \(tint.cssColor)"/></div>
        """,
    },
    stylesheet: """
        .sidemark {
            float: left;
            width: 5px;
            height: 100%;
            background-color: var(--tint);
            margin-left: 20px;
            border-radius: 3px;
        }
        [dir=rtl] .sidemark {
            float: right;
            margin-left: 0px;
            margin-right: 20px;
        }
        """
)

// Associate the style with our template when creating the HTML navigator.
var config = HTMLNavigator.Configuration()
config.decorationStyles[Decoration.Style.Sidemark] = sidemarkTemplate
let navigator = HTMLNavigator(config)

Checking Whether a Navigator Supports a Decoration Style

You should check whether the Navigator supports drawing the decoration styles required by a particular feature before enabling it. For example, underlining an audiobook does not make sense, so an Audiobook Navigator would not support the underline decoration style.

navigator.supportsDecorationStyle(Decoration.Style.Underline)

Backward Compatibility and Migration

Kotlin

The Readium Kotlin toolkit currently ships with a highlighting API. It will be deprecated in favor of the new Decorator API. The old APIs will internally use the Decorator implementation, so it will not be a breaking change.

Swift

The Readium Swift toolkit did not yet have decoration capabilities, so there’s no impact on existing implementations.

Reference Guide

DecorableNavigator Interface

A navigator able to render arbitrary decorations over a publication.

Methods

DecorationObserver Interface

Receives interaction events for decorations.

Methods

OnActivatedEvent Class

Holds the metadata about a decoration activation interaction.

Properties

Decoration Class

A decoration is a user interface element drawn on top of a publication. It associates a style to be rendered with a precise location (Locator) in the publication.

For example, decorations can be used to draw highlights, images or buttons.

Properties

Decoration.Style Interface

The Decoration Style determines the look and feel of a decoration once rendered by a Navigator. It is media type agnostic, meaning that each Navigator will translate the style into a set of rendering instructions which makes sense for the resource type.

The Readium toolkit supports two default styles:

Note: This can be implemented differently depending on the platform capabilities. Ideally, this is a marker interface and each concrete type is used to identify the style.

Decoration Templates

Decoration Templates translate a Decoration object and its associated style into concrete rendering instructions specific to a Navigator and resource type. Each concrete implementation of DecorableNavigator can support its own kind of decoration templates.

HTMLDecorationTemplate Class

An HTMLDecorationTemplate renders a Decoration into a set of HTML elements and associated stylesheet. It is used to render decorations in EPUB publications, for example.

Properties
Cheatsheet
width↓  layout→ boxes bounds
wrap
bounds
page
viewport

Future Possibilities

Rendering Images and Text

This proposal introduces only two default styles: highlight and underline. These are useful for the main use cases of Readium: highlights, search and TTS. However, the API is generic enough to represent any kind of decorations.

In particular, reading apps might be interested in drawing images or text. Offering a generic style for such decorations could prove challenging because we need the layout, style and positioning to be flexible enough while still being media-type agnostic.

A better alternative could be to provide generic decoration templates, exploiting the characteristics of each media type. Integrators would be responsible to create new high-level decoration styles and configure the matching template for each Navigator.

Example: Margin Icon

Consider a reading app which needs to draw an icon in the page margin to show that a highlight has an associated text note.

It could create a new decoration style extending the abstract Image style.

class Decoration.Style.MarginIcon: Decoration.Style.Image {}

Then, associate this custom style with an HTML decoration template for the EPUB navigator:

decorationStyles[Decoration.Style.MarginIcon] = HTMLDecorationTemplate.Image(
    position: .topLeft,
    mask: .circle,
    size: Size(100, 100),
    margin: 20
)

Finally, apply the new decoration style:

let decorations = [
    Decoration(
        id: highlight.id + "-icon",
        locator: highlight.locator,
        style: Decoration.Style.MarginIcon(image: "note")
    ),
    Decoration(
        id: highlight.id + "-highlight",
        locator: highlight.locator,
        style: Decoration.Style.Highlight(tint: highlight.color)
    )
]

navigator.applyDecorations(decorations: decorations, in: "user-highlights")

Media-Based Decorations

This proposal focuses on the core use case which is highlighting a text range. But the Decorator API is not limited to static or even visual decorations.

For example, we can “highlight” a portion of an audio resource by raising the volume or playing an audio cue when reaching the target time. Or display subtitles over a movie resource. Both of these examples rely on Media Fragments in the Locator to decorate a portion of the media.

Additional Interactions

This proposal introduces only a single decoration interaction: activation. Here are some examples of additional interactions that could be implemented:

A new HTML template layout for continuous boxes bounds

The specified HTML template layouts are not sufficient to render side marks when two columns are enabled. If a locator is overlapping both columns, using bounds would result in a decoration spanning the whole viewport.

We could solve this by adding a third layout for “continuous boxes bounds”, which would coalesce boxes together only if they are close enough. This requires some heuristics and is not so straightforward to implement.