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.
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.
search
, highlights
, tts
).highlight
, underline
or margin icon
. It is media type agnostic.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.
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")
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())
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)
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)
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.
The Readium Swift toolkit did not yet have decoration capabilities, so there’s no impact on existing implementations.
DecorableNavigator
InterfaceA navigator able to render arbitrary decorations over a publication.
applyDecorations(decorations: [Decoration], group: String)
group
.annotation
, search
, tts
, etc.supportsDecorationStyle(style: Decoration.Style) -> Boolean
style
.underline
decoration style.registerDecorationObserver(group: String, observer: DecorationObserver)
observer
for decoration interactions in the given group
.unregisterDecorationObserver(observer: DecorationObserver)
observer
for all decoration interactions.DecorationObserver
InterfaceReceives interaction events for decorations.
onDecorationActivated(event: OnActivatedEvent) -> Boolean
OnActivatedEvent
ClassHolds the metadata about a decoration activation interaction.
decoration: Decoration
group: String
rect: Rect?
VisualNavigator
.point: Point?
VisualNavigator
.Decoration
ClassA 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.
id: String
locator: Locator
style: Decoration.Style
extras: Map<String, Any>
Decoration.Style
InterfaceThe 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:
Highlight(tint: Color?, isActive: Boolean)
Underline(tint: Color?, isActive: Boolean)
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 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
ClassAn HTMLDecorationTemplate
renders a Decoration
into a set of HTML elements and associated stylesheet. It is used to render decorations in EPUB publications, for example.
layout: Layout
boxes
: One HTML element for each CSS border box (e.g. line of text). Uses JS’s Range.getClientRects()
.bounds
: A single HTML element covering the smallest region containing all CSS border boxes. Uses JS’s Range.getBoundingClientRect()
.width: Width
wrap
: Smallest width fitting the CSS border box.bounds
: Fills the bounds layout.page
: Fills the anchor page, useful for dual page.viewport
: Fills the whole viewport.element: (Decoration) -> String
Decoration
.boxes
layout.Locator
. The template is only responsible for the look and feel of the generated elements.stylesheet: String?
r2-
and readium-
are reserved by the Readium toolkit.width↓  layout→ | boxes |
bounds |
---|---|---|
wrap |
||
bounds |
||
page |
||
viewport |
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.
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")
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.
This proposal introduces only a single decoration interaction: activation. Here are some examples of additional interactions that could be implemented:
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.