Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace the Streamer's File with PublicationAsset #147

Merged
merged 1 commit into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 50 additions & 42 deletions proposals/005-streamer-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,21 @@ The Streamer is one of the main components of the Readium Architecture, whose re

### Usage

#### Opening a Publication File
#### Opening a Publication Asset

Opening a `Publication` is really simple with an instance of `Streamer`.

```kotlin
file = File(path)
asset = FileAsset(path)
streamer = Streamer()
publication = streamer.open(file)
publication = streamer.open(asset)
```

Your app will automatically support parsing new formats added in Readium. However, if you wish to limit the supported formats to a subset of what Readium offers, simply guard the call to `open()` by checking the value of `file.format` first.
Your app will automatically support parsing new formats added in Readium. However, if you wish to limit the supported formats to a subset of what Readium offers, simply guard the call to `open()` by checking the value of `asset.mediaType` first.

```kotlin
supportedFormats = [Format.EPUB, Format.PDF]
if (!supportedFormats.contains(file.format)) {
supportedMediaTypes = [MediaType.EPUB, MediaType.PDF]
if (!supportedMediaTypes.contains(asset.mediaType)) {
return
}
```
Expand Down Expand Up @@ -107,9 +107,9 @@ The Streamer accepts a `Publication.Builder.Transform` closure which will be cal

```kotlin
streamer = Streamer(
onCreatePublication = { format, manifest, fetcher, services ->
onCreatePublication = { mediaType, manifest, fetcher, services ->
// Minifies the HTML resources in an EPUB.
if (format == Format.EPUB) {
if (mediaType == MediaType.EPUB) {
fetcher = TransformingFetcher(fetcher, minifyHTML)
}

Expand All @@ -120,7 +120,7 @@ streamer = Streamer(
}

// Sets a custom SearchService implementation for PDF.
is (format == Format.PDF) {
is (mediaType == MediaType.PDF) {
services.searchServiceFactory = PDFSearchService.create
}
}
Expand All @@ -144,25 +144,25 @@ If you just want to add HTTP headers or set up caching and networking policies f

The Readium Architecture is opened to support additional publication formats.

1. [Register your new format and add a sniffer](https://github.com/readium/architecture/blob/master/proposals/001-format-api.md#supporting-a-custom-format). This step is optional but recommended to make your format a first-class citizen in the toolkit.
1. [Register your new format and add a sniffer](https://readium.org/architecture/proposals/001-media-type.html#supporting-a-custom-media-type). This step is optional but recommended to make your format a first-class citizen in the toolkit.
2. Implement a `PublicationParser` to parse the publication format into a `Publication` object. Then, provide an instance to the Streamer.

```swift
class CustomParser: PublicationParser {

func parse(file: File, fetcher: Fetcher, warnings: WarningLogger?) -> Publication.Builder? {
if (file.format != Format.MyCustomFormat) {
func parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?) -> Publication.Builder? {
if (asset.mediaType != MediaType.MyCustomFormat) {
return null
}

return Publication.Builder(
manifest = parseManifest(from: file),
manifest = parseManifest(from: asset),
fetcher = fetcher
services = [CustomPositionsServiceFactory()]
)
}

private func parseManifest(from file: File) -> Manifest {
private func parseManifest(from asset: PublicationAsset) -> Manifest {
...
}

Expand All @@ -182,36 +182,44 @@ Reading apps will still be able to use indivual parsers directly. However, we sh

## Reference Guide (`r2-shared`)

### `File` Class
### `PublicationAsset` Interface

Represents a path on the file system.
Represents a digital medium (e.g. a file) offering access to a publication.

Used to cache the `Format` to avoid computing it at different locations.
#### Properties

* `name: String`
* Name of the asset, e.g. a filename.
* (lazy) `mediaType: MediaType?`
* Media type of the asset, when known.
* **Warning:** This should not be called from the UI thread.

#### Methods

* (async) `createFetcher(dependencies: PublicationAssetDependencies, credentials: String?) -> Result<Fetcher, Publication.OpeningError>`
* Creates a fetcher used to access the asset's content.
* `dependencies: PublicationAssetDependencies`
* Platform-specific dependencies which can be used by the implementation to create the `Fetcher`. For example, an `ArchiveFactory`.
* `credentials: String?`
* Credentials provided by the reading app to open the asset. For example, a ZIP password.

### `FileAsset` Class (implements `PublicationAsset`)

Represents a publication stored as a file on the local file system.

#### Constructors

* `File(path: String, mediaType: String? = null)`
* Creates a `File` from a `path` and its known `mediaType`.
* `FileAsset(path: String, mediaType: MediaType? = null)`
* Creates a `FileAsset` from a `path` and its known `mediaType`.
* `path: String`
* Absolute path to the file or directory.
* `mediaType: String? = null`
* `mediaType: MediaType? = null`
* If the file's media type is already known, providing it will improve performances.
* `File(path: String, format: Format)`
* Creates a `File` from a `path` and an already resolved `format`.

#### Properties

* `path: String`
* Absolute path on the file system.
* `name: String`
* Last path component, or filename.
* (lazy) `format: Format?`
* Sniffed format, if the path points to a file.
* **Warning:** This should not be called from the UI thread.
* (lazy) `isDirectory: Boolean`
* Whether the path points to a directory.
* This can be used to open exploded publication archives.
* **Warning:** This should not be called from the UI thread.

### `Publication.OpeningError` Enum

Expand Down Expand Up @@ -293,7 +301,7 @@ A `Publication`'s construction is distributed over the Streamer and its parsers,

```kotlin
typealias Publication.Builder.Transform = (
format: Format,
mediaType: MediaType,
manifest: *Manifest,
fetcher: *Fetcher,
services: *Publication.ServicesBuilder
Expand All @@ -306,21 +314,21 @@ The signature depends on the capabilities of the platform: `manifest`, `fetcher`

### `PublicationParser` Interface

Parses a `Publication` from a file.
Parses a `Publication` from an asset.

#### Methods

* `parse(file: File, fetcher: Fetcher, warnings: WarningLogger?) -> Publication.Builder?`
* `parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?) -> Publication.Builder?`
* Constructs a `Publication.Builder` to build a `Publication` from a publication file.
* Returns `null` if the file format is not supported by this parser, or throws an error if the parsing fails.
* `file: File`
* Path to the publication file.
* `asset: PublicationAsset`
* Digital medium (e.g. a file) used to access the publication.
* `fetcher: Fetcher`
* Initial leaf fetcher which should be used to read the publication's resources.
* This can be used to:
* support content protection technologies
* parse exploded archives or in archiving formats unknown to the parser, e.g. RAR
* If the file is not an archive, it will be reachable at the HREF `/<file.name>`, e.g. with a PDF.
* If the asset is not an archive, it will be reachable at the HREF `/<asset.name>`, e.g. with a PDF.
* `warnings: WarningLogger?`
* Logger used to broadcast non-fatal parsing warnings.
* Can be used to report publication authoring mistakes, to warn users of potential rendering issues or help authors debug their publications.
Expand Down Expand Up @@ -359,9 +367,9 @@ The specification of `HTTPClient`, `Archive`, `XMLDocument` and `PDFDocument` is

#### Methods

* (async) `open(file: File, onCreatePublication: Publication.Builder.Transform? = null, warnings: WarningLogger? = null) -> Result<Publication?, Publication.OpeningError>`
* Parses a `Publication` from the given `file`.
* Returns `null` if the file was not recognized by any parser, or a `Publication.OpeningError` in case of failure.
* (async) `open(asset: PublicationAsset, onCreatePublication: Publication.Builder.Transform? = null, warnings: WarningLogger? = null) -> Result<Publication?, Publication.OpeningError>`
* Parses a `Publication` from the given `asset`.
* Returns `null` if the asset was not recognized by any parser, or a `Publication.OpeningError` in case of failure.
* `onCreatePublication: Publication.Builder.Transform? = null`
* Transformation which will be applied on the Publication Builder. It can be used to modify the `Manifest`, the root `Fetcher` or the list of service factories of the `Publication`.
* `warnings: WarningLogger? = null`
Expand Down Expand Up @@ -398,11 +406,11 @@ Parses a `Publication` from a PDF document.

##### Reading Order

The reading order contains a single link pointing to the PDF document, with the HREF `/<file.name>`.
The reading order contains a single link pointing to the PDF document, with the HREF `/<asset.name>`.

##### Table of Contents

The Document Outline (i.e. section 12.3.3) can be used to create a table of contents. The HREF of each link should use a `page=` fragment identifier, following this template: `/<file.name>#page=<pageNumber>`, where `pageNumber` starts from 1.
The Document Outline (i.e. section 12.3.3) can be used to create a table of contents. The HREF of each link should use a `page=` fragment identifier, following this template: `/<asset.name>#page=<pageNumber>`, where `pageNumber` starts from 1.

##### Cover

Expand Down
42 changes: 21 additions & 21 deletions proposals/006-content-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ Since it's usually possible to read the metadata of a publication without unlock
However, if you need to render the publication to the user, you can set the `allowUserInteraction` parameter to `true`. If the given credentials are incorrect, then the Content Protection will be allowed to ask the user for its credentials.

```kotlin
streamer.open(file, allowUserInteraction = true, credentials = "open sesame")
streamer.open(asset, allowUserInteraction = true, credentials = "open sesame")
```

Some Content Protections – e.g. ZIP encryption – prevent reading a publication's metadata even in a *restricted* state without the proper credentials. In this case, a `Publication.OpeningError.IncorrectCredentials` error will be returned.

### Using Third-Party Protections

Format-specific protections are natively handled by the Streamer, since they are tied to the file format. However, for third-party technologies such as a DRM, you need to register them by providing a `ContentProtection` instance to the Streamer. Here's an example for LCP:
Format-specific protections are natively handled by the Streamer, since they are tied to the asset format. However, for third-party technologies such as a DRM, you need to register them by providing a `ContentProtection` instance to the Streamer. Here's an example for LCP:

```swift
streamer = Streamer(
Expand Down Expand Up @@ -111,7 +111,7 @@ Readium supports only a single enabled Content Protection per publication, becau

There are currently only two format-specific protections recognized by Readium: PDF and ZIP.

When a password protection is used, the `credentials` parameter provided to the Streamer is used to unlock the protected file. In case of incorrect credentials:
When a password protection is used, the `credentials` parameter provided to the Streamer is used to unlock the protected asset. In case of incorrect credentials:

* If `allowUserInteraction` is `true`, then the `onAskCredentials()` callback provided to the Streamer is used to request the password.
* Otherwise, an `IncorrectCredentials` error is returned because format-specific protections don't support reading partial metadata.
Expand Down Expand Up @@ -321,7 +321,7 @@ Two new arguments are added to the constructor: `contentProtections` and `onAskC

There are three new parameters added to `Streamer::open()`: `allowUserInteraction`, `credentials`, and `sender`.

* (async) `open(file: File, allowUserInteraction: Boolean, credentials: String? = null, sender: Any? = null, onCreatePublication: Publication.Builder.Transform? = null, warnings: WarningLogger? = null) -> Publication`
* (async) `open(asset: PublicationAsset, allowUserInteraction: Boolean, credentials: String? = null, sender: Any? = null, onCreatePublication: Publication.Builder.Transform? = null, warnings: WarningLogger? = null) -> Publication`
* `allowUserInteraction: Boolean`
* Indicates whether the user can be prompted during opening, for example to ask their credentials.
* This should be set to `true` when you want to render a publication in a Navigator.
Expand All @@ -346,28 +346,28 @@ Its responsibilities are to:

##### Methods

* (async) `open(file: File, fetcher: Fetcher, allowUserInteraction: Boolean, credentials: String?, sender: Any?, onAskCredentials: OnAskCredentialsCallback?) -> ProtectedFile?`
* Attempts to unlock a potentially protected file.
* (async) `open(asset: PublicationAsset, fetcher: Fetcher, allowUserInteraction: Boolean, credentials: String?, sender: Any?, onAskCredentials: OnAskCredentialsCallback?) -> ProtectedAsset?`
* Attempts to unlock a potentially protected publication asset.
* `fetcher: Fetcher`
* The Streamer will create a leaf `Fetcher` for the low-level file access (e.g. `ArchiveFetcher` for a ZIP archive), to avoid having each Content Protection open the `file` to check if it's protected or not.
* The Streamer will create a leaf `Fetcher` for the low-level asset access (e.g. `ArchiveFetcher` for a ZIP archive), to avoid having each Content Protection open the `asset` to check if it's protected or not.
* A publication might be protected in such a way that the package format can't be recognized, in which case the Content Protection will have the responsibility of creating a new leaf `Fetcher`.
* Returns:
* a `ProtectedFile` in case of success,
* `null` if the `file` is not protected by this technology,
* a `Publication.OpeningError` if the file can't be successfully opened.
* a `ProtectedAsset` in case of success,
* `null` if the `asset` is not protected by this technology,
* a `Publication.OpeningError` if the asset can't be successfully opened.

##### `ProtectedFile` Class
##### `ProtectedAsset` Class

Holds the result of opening a `File` with a `ContentProtection`.
Holds the result of opening a `PublicationAsset` with a `ContentProtection`.

*All the constructor parameters are public.*

* `ProtectedFile(file: File, fetcher: Fetcher, onCreatePublication: Publication.Builder.Transform?)`
* `file: File`
* Protected file which will be provided to the parsers.
* In most cases, this will be the `file` provided to `ContentProtection::open()`, but a Content Protection might modify it in some cases:
* If the original file has a media type that can't be recognized by parsers, the Content Protection must return a `file` with the matching unprotected media type.
* If the Content Protection technology needs to redirect the Streamer to a different file. For example, this could be used to decrypt a publication to a temporary secure location.
* `ProtectedAsset(asset: PublicationAsset, fetcher: Fetcher, onCreatePublication: Publication.Builder.Transform?)`
* `asset: PublicationAsset`
* Protected publication asset which will be provided to the parsers.
* In most cases, this will be the `asset` provided to `ContentProtection::open()`, but a Content Protection might modify it in some cases:
* If the original asset has a media type that can't be recognized by parsers, the Content Protection must return an `asset` with the matching unprotected media type.
* If the Content Protection technology needs to redirect the Streamer to a different asset. For example, this could be used to decrypt a publication to a temporary secure location.
* `fetcher: Fetcher`
* Primary leaf fetcher to be used by parsers.
* The Content Protection can unlock resources by modifying the `Fetcher` provided to `ContentProtection::open()`, for example by:
Expand Down Expand Up @@ -450,11 +450,11 @@ Besides, these features don't live in Readium components but in reading apps. Yo

## Future Possibilities

### Invalidating the Publication File
### Invalidating the Publication Asset

A Content Protection technology might alter the publication file during the lifetime of the `Publication` object. For example, by injecting an updated license file after renewing a loan. This could potentially be an issue for opened file handlers in the leaf `Fetcher` accessing the publication file.
A Content Protection technology might alter the publication asset during the lifetime of the `Publication` object. For example, by injecting an updated license file after renewing a loan. This could potentially be an issue for opened file handlers in the leaf `Fetcher` accessing the publication asset.

A solution would be the introduction of an `InvalidatingFetcher`, which would be able to recreate its child `Fetcher` on-demand. The `ContentProtectionService` would keep a reference to this `InvalidatingFetcher`, and call `invalidate()` every time the publication file is updated.
A solution would be the introduction of an `InvalidatingFetcher`, which would be able to recreate its child `Fetcher` on-demand. The `ContentProtectionService` would keep a reference to this `InvalidatingFetcher`, and call `invalidate()` every time the publication asset is updated.

#### `InvalidatingFetcher` Class

Expand Down