Readium Logo

Publication Helpers and Services

Summary

Our goal is to improve extensibility and customizability of the Publication type for reading apps. To achieve that, this proposal introduces two structured ways to extend a Publication with additional features: helpers and services.

Motivation

Computed Metadata

Readium components need to make decisions based on the metadata available in the Publication, which sometimes involves computing additional data. For example, it wouldn’t make sense to show the font size setting for a full fixed-layout EPUB publication, so a Publication::allReadingOrderIsFixedLayout helper is useful.

These computed metadata are not part of the core models specification, so they should be added as extensions. Reading apps should be able to inject additional helpers to address custom needs.

RWPM extensions are already implemented as helpers in the mobile toolkits, by converting JSON values to type-safe data structures.

Swappable Implementations

For more complex and opiniated APIs, it would be useful to allow reading apps to swap Readium default implementations with their own. For example, calculating the list of positions can be done in many different ways, and depends on the publication format.

By using a set of specified interfaces as contracts, we can have different Readium components use these services without caring for the concrete implementation.

Developer Guide

The Publication shared models support extensibility through two structured ways: helpers and services.

Publication Helpers

A helper extends the shared models by computing additional metadata when requested, such as:

They are simple syntactic constructs, using the capabilities of each language, for example:

// Swift
extension Array where Element == Link {

    var allAreAudio: Bool {
        allSatisfy { $0.mediaType?.isAudio == true }
    }

}
// Kotlin
val List<Link>.allAreAudio: Boolean get() = all {
    it.mediaType?.isAudio == true
}
// JavaScript
LinkArray.prototype.everyAreAudio() = function() {
  return this.every(link => link.mediaType.isAudio())
};

Publication Services

A service is a contract between Readium components, defined as an interface with swappable implementations stored in the Publication object, such as:

While the known service interfaces are defined in r2-shared, their implementations are usually provided by other components, such as r2-streamer or the reading app itself.

Publication services are a point of customizability for reading apps, because you can swap a default implementation with your own, or decorate it to add cross-cutting concerns. A reading app can also add custom service interfaces for internal needs.

Getting a Service Instance

The Publication object holds a list of service instances. To get an instance of a service, you can use Publication::findService(), which will return the first service implementing the interface S. However, you rarely need to access directly a service instance, since a service interface usually also defines helpers on Publication for convenience.

For example, the PositionsService declares the helper property Publication::positions to directly access the list of positions.

Consuming a Service on the Web

Some publication services expose a web version of their API, to be consumed for example by a JavaScript app or a remote client. In which case, you can retrieve the WS routes from Publication::links, using the WS custom media types or link relations. Then, the response can be fetched using Publication::get(), which can be exposed through HTTP with a Publication Server.

If the web service takes parameters, then its Link object will be templated.

The media types, formats and parameters depend on the specification of each publication service.

let searchLink = publication.links.firstWithMediaType("application/vnd.readium.search+json")
// searchLink == Link(
//   href: "/~readium/search{?text}",
//   type: "application/vnd.readium.search+json",
//   templated: true
// )

if (searchLink) {
  // href == "/~readium/search?text=banana"
  let queryLink = searchLink.expandTemplate({"text": "banana"})
  // `results` is a JSON collection of Locator objects.
  let results = await publication.get(queryLink).readAsJSON()
}

Creating a New Service

To create your own service, you must declare:

You should also provide helpers on Publication for a more convenient access to your service API, with fallbacks if no instances of your service is attached to the Publication.

Here’s an example for PositionsService:

// The service contract.
interface PositionsService : Publication.Service {
    
    val positions: List<Locator>
    
}

// Defines a convenient `publication.positions` helper with a fallback value.
val Publication.positions: List<Locator> get() {
    val service = findService(PositionsService::class)
    return service?.positions ?: emptyList()
}


// A concrete implementation of the service.
class EPUBPositionsService(val readingOrder: List<Link>, val fetcher: Fetcher) : PositionsService {
    
    override val positions: List<Locator> by lazy {
        // Lazily computes the position list...
    }
    
    companion object {
        
        // The service factory.
        fun create(context: Publication.Service.Context): EPUBPositionsService {
            return EPUBPositionsService(context.manifest.readingOrder, context.fetcher)
        }
        
    }
}

val publication = Publication(manifest, fetcher, servicesBuilder = Publication.ServicesBuilder(
    positions = (EPUBPositionsService)::create
))

Backward Compatibility and Migration

Mobile (Swift and Kotlin)

If you were providing your own implementation of the PositionListFactory, you will have to adapt it to use PositionsService instead.

The Publication models are not yet immutable in the Swift toolkit, which is required for this proposal to work properly. Since reading apps usually don’t create or modify a Publication, this should have a minimal impact.

Reference Guide

Helpers and services rely on the fact that a Publication is immutable. If that was not the case, then any cached value could become invalid.

Publication Helpers

If a helper performs an expensive task, a warning should be logged if it is called from the UI thread. This is typically the case for helpers accessing resources from the Fetcher.

Publication Services

Publication.Service Interface

Base interface to be inherited by all publication services.

Properties
Methods
Copy and Factory

Since a service might cache values computed from the current manifest and fetcher, we can’t reuse its instance when copying the Publication object. To circumvent this issue, the Publication is given service factories, which will be forwarded to its copies. During the construction of a Publication object, the provided factories are used to create the service instances.

typealias Publication.Service.Factory = (Publication.Service.Context) -> Publication.Service?

class Publication.Service.Context {
    val manifest: Manifest,
    val fetcher: Fetcher
}

A Publication.Service.Context is used instead of passing directly the arguments, to be able to add more parameters later on without modifying all the existing service factories.

Publication.ServicesBuilder Class

Builds a list of Publication.Service using Publication.Service.Factory instances.

Provides helpers to manipulate the list of services of a Publication.

This class holds a map between a key – computed from a service interface – and a factory instance for this service.

Constructors
Properties

Each publication service should define helpers on Publication.ServicesBuilder to set its factory.

Methods

Publication Additions

Methods

Rationale and Alternatives

Two alternatives to extend Publication were considered: decoration and inheritance.

Decorating Publication

This doesn’t require any change in the Readium toolkit. With this technique, the reading app creates a new type that will embed and proxy the Publication object, while adding more features.

While this solution is easy to implement, it doesn’t allow customizing the behavior of Readium since other components are expecting a Publication object.

Subclassing Publication

We could allow the reading app to provide its own Publication subclass to the parsers. In this case, the reading app could override:

Subclassing Publication might look more straightforward, but it could transform Publication into a bulky god object. The solution introduced in this proposal has the advantage of offering a clear structure to encapsulate different services, and the possibility of swapping service implementations easily – e.g. the positions are computed differently for each publication format.

Drawbacks and Limitations

Future Possibilities

Specifying new publication services and helpers should become a natural part of future proposals related to the Publication models.

Transforming a Web Service into a Native Publication Service

We could encounter web publications served by a remote HTTP Streamer providing web services, such as a positions list. In which case, a native toolkit might fetch the positions from the web service and convert them to in-memory Locator models, as a fallback if no instance of PositionsService is provided.

Future Helpers?

Future Services?

Appendix A: Services Provided by Readium

PositionsService

Provides a list of discrete locations in the publication, no matter what the original format is.

This service is described in more details in this specification.

Properties

Publication Helpers

Web Service

PositionsService exposes the positions list as a web service.

It provides a default implementation to avoid rewriting the JSON serialization for all concrete implementations of PositionsService.

private const val positionsLink = Link(
    href = "/~readium/positions",
    type = "application/vnd.readium.position-list+json"
)

interface PositionsService : Publication.Service {

    /* List of all the positions in the publication as Locator objects. */
    val positions: List<Locator>

    /* List of all the positions in the publication, grouped by the resource reading order index. */
    val positionsByReadingOrder: List<List<Locator>>

    override val links: List<Link>
        get() = listOf(positionsLink)

    override fun get(link: Link): Resource? {
        if (link.href != positionsLink.href) {
            return null
        }

        val json = JSONObject().apply {
            put("total", positions.size)
            put("positions", positions.map { it.toJSON() })
        }
        return BytesResource(positionsLink, json.toString())
    }

}

CoverService

Provides an easy access to a bitmap version of the publication cover.

While at first glance, getting the cover could be seen as a helper, the implementation actually depends on the publication format:

Furthermore, a reading app might want to use a custom strategy to choose the cover image, for example by:

Properties

Methods

Publication Helpers

Web Service

If the cover selected by the CoverService is not already part of Publication::links with a cover relation, then the CoverService should add it to CoverService::links and serve the bitmap in CoverService::get() at the maximum available size. If the source format is a vector image, it will be fitted to the device’s screen size.

Appendix B: Helpers Provided by Readium

This is an overview of the helpers implemented natively by the Readium toolkit.

Publication Helpers

EPUB

OPDS

Manifest Helpers

Metadata Helpers

Presentation

Properties Helpers

Presentation

Encryption

EPUB

OPDS

Presentation Helpers

EPUB