Skip to content
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
94 changes: 56 additions & 38 deletions Documentation/Guides/Getting Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ The toolkit has been designed following these core tenets:

### Main packages

* `R2Shared` contains shared `Publication` models and utilities.
* `R2Streamer` parses publication files (e.g. an EPUB) into a `Publication` object.
* [`R2Navigator` renders the content of a publication](Navigator/Navigator.md).
* `ReadiumShared` contains shared `Publication` models and utilities.
* `ReadiumStreamer` parses publication files (e.g. an EPUB) into a `Publication` object.
* [`ReadiumNavigator` renders the content of a publication](Navigator/Navigator.md).

### Specialized packages

Expand All @@ -30,7 +30,7 @@ The toolkit has been designed following these core tenets:
* `ReadiumAdapterGCDWebServer` provides an HTTP server built with [GCDWebServer](https://github.com/swisspol/GCDWebServer).
* `ReadiumAdapterLCPSQLite` provides implementations of the `ReadiumLCP` license and passphrase repositories using [SQLite.swift](https://github.com/stephencelis/SQLite.swift).

## Overview of the shared models (`R2Shared`)
## Overview of the shared models (`ReadiumShared`)

The Readium toolkit provides models used as exchange types between packages.

Expand All @@ -48,7 +48,6 @@ A `Publication` instance:

#### Link


A [`Link` object](https://readium.org/webpub-manifest/#24-the-link-object) holds a pointer (URL) to a resource or service along with additional metadata, such as its media type or title.

The `Publication` contains several `Link` collections, for example:
Expand All @@ -70,68 +69,87 @@ A [`Locator` object](https://readium.org/architecture/models/locators/) represen

### Data models

#### Publication Asset
#### Asset

A `PublicationAsset` is an interface representing a single file or package holding the content of a `Publication`. A default implementation `FileAsset` grants access to a publication stored locally.
An `Asset` represents a single file or package and provides access to its content. There are two types of `Asset`:

#### Resource
* `ContainerAsset` for packages which contains several resources, such as a ZIP archive.
* `ResourceAsset` for accessing a single resource, such as a JSON or PDF file.

A `Resource` provides read access to a single resource of a publication, such as a file or an entry in an archive.
`Asset` instances are obtained through an `AssetRetriever`.

`Resource` instances are usually created by a `Fetcher`. The toolkit ships with various implementations supporting different data access protocols such as local files, HTTP, etc.
You can use the `asset.format` to identify the media type and capabilities of the asset.

#### Fetcher
```swift
if asset.format.conformsTo(.lcp) {
// The asset is protected with LCP.
}
if asset.format.conformsTo(.epub) {
// The asset represents an EPUB publication.
}
```

A `Fetcher` provides read access to a collection of resources. `Fetcher` instances are created by a `PublicationAsset` to provide access to the content of a publication.
#### Resource

`Publication` objects internally use a `Fetcher` to expose their content.
A `Resource` provides read access to a single resource, such as a file or an entry in an archive.

## Opening a publication (`R2Streamer`)
`Resource` instances are usually created by a `ResourceFactory`. The toolkit ships with various implementations supporting different data access protocols such as local files or HTTP.

To retrieve a `Publication` object from a publication file like an EPUB or audiobook, begin by creating a `PublicationAsset` object used to read the file. Readium provides a `FileAsset` implementation for reading a publication stored on the local file system.
#### Container

```swift
let file = URL(fileURLWithPath: "path/to/book.epub")
let asset = FileAsset(file: file)
```
A `Container` provides read access to a collection of resources. `Container` instances representing an archive are usually created by an `ArchiveOpener`. The toolkit ships with a `ZIPArchiveOpener` supporting local ZIP files.

`Publication` objects internally use a `Container` to expose its content.

## Opening a publication (`ReadiumStreamer`)

Then, use a `Streamer` instance to parse the asset and create a `Publication` object.
To retrieve a `Publication` object from a publication file like an EPUB or audiobook, you can use an `AssetRetriever` and `PublicationOpener`.

```swift
let streamer = Streamer()
// Instantiate the required components.
let httpClient = DefaultHTTPClient()
let assetRetriever = AssetRetriever(
httpClient: httpClient
)
let publicationOpener = PublicationOpener(
publicationParser: DefaultPublicationParser(
httpClient: httpClient,
assetRetriever: assetRetriever,
pdfFactory: DefaultPDFDocumentFactory()
)
)

let url: URL = URL(...)

streamer.open(asset: asset, allowUserInteraction: false) { result in
switch result {
// Retrieve an `Asset` to access the file content.
switch await assetRetriever.retrieve(url: url.anyURL.absoluteURL!) {
case .success(let asset):
// Open a `Publication` from the `Asset`.
switch await publicationOpener.open(asset: asset, allowUserInteraction: true, sender: view) {
case .success(let publication):
print("Opened \(publication.metadata.title)")

case .failure(let error):
alert(error.localizedDescription)
case .cancelled:
// The user cancelled the opening, for example by dismissing a password pop-up.
break
// Failed to access or parse the publication
}

case .failure(let error):
// Failed to retrieve the asset
}
```

The `allowUserInteraction` parameter is useful when supporting a DRM like Readium LCP. It indicates if the toolkit can prompt the user for credentials when the publication is protected.

[See the dedicated user guide for more information](Open%20Publication.md).

## Accessing the metadata of a publication

After opening a publication, you may want to read its metadata to insert a new entity into your bookshelf database, for instance. The `publication.metadata` object contains everything you need, including `title`, `authors` and the `published` date.

You can retrieve the publication cover using `publication.cover`. Avoid calling this from the main thread to prevent blocking the user interface.
You can retrieve the publication cover using `await publication.cover()`.

## Rendering the publication on the screen (`R2Navigator`)
## Rendering the publication on the screen (`ReadiumNavigator`)

You can use a Readium navigator to present the publication to the user. The `Navigator` renders resources on the screen and offers APIs and user interactions for navigating the contents.

```swift
let navigator = try EPUBNavigatorViewController(
publication: publication,
initialLocation: lastReadLocation,
httpServer: GCDHTTPServer.shared
)

hostViewController.present(navigator, animated: true)
```
Please refer to the [Navigator guide](Navigator/Navigator.md) for more information.
92 changes: 92 additions & 0 deletions Documentation/Guides/Open Publication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Opening a publication

To open a publication with Readium, you need to instantiate a couple of components: an `AssetRetriever` and a `PublicationOpener`.

## `AssetRetriever`

The `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license.

### Constructing an `AssetRetriever`

You can create an instance of `AssetRetriever` with:

* An `HTTPClient` to enable the toolkit to perform HTTP requests and support the `http` and `https` URL schemes. You can use `DefaultHTTPClient` which provides callbacks for handling authentication when needed.

```swift
let assetRetriever = AssetRetriever(httpClient: DefaultHTTPClient())
```

### Retrieving an `Asset`

With your fresh instance of `AssetRetriever`, you can open an `Asset` from any `AbsoluteURL`.

```swift
// From a local file.
let url = FileURL(string: "file:///path/to/book.epub")
// or from an HTTP URL.
let url = HTTPURL(string: "https://domain/book.epub")

switch await assetRetriever.retrieve(url: url) {
case .success(let asset):
...
case .failure(let error):
// Failed to retrieve the asset.
}
```

The `AssetRetriever` will sniff the media type of the asset, which you can store in your bookshelf database to speed up the process next time you retrieve the `Asset`. This will improve performance, especially with HTTP URL schemes.

```swift
let mediaType = asset.format.mediaType

// Speed up the retrieval with a known media type.
let result = await assetRetriever.retrieve(url: url, mediaType: mediaType)
```

## `PublicationOpener`

`PublicationOpener` builds a `Publication` object from an `Asset` using:

* A `PublicationParser` to parse the asset structure and publication metadata.
* The `DefaultPublicationParser` handles all the formats supported by Readium out of the box.
* An optional list of `ContentProtection` to decrypt DRM-protected publications.
* If you support Readium LCP, you can get one from the `LCPService`.

```swift
let publicationOpener = PublicationOpener(
parser: DefaultPublicationParser(
httpClient: httpClient,
assetRetriever: assetRetriever
),
contentProtections: [
lcpService.contentProtection(with: LCPDialogAuthentication()),
]
)
```

### Opening a `Publication`

Now that you have a `PublicationOpener` ready, you can use it to create a `Publication` from an `Asset` that was previously obtained using the `AssetRetriever`.

The `allowUserInteraction` parameter is useful when supporting Readium LCP. When enabled and using a `LCPDialogAuthentication`, the toolkit will prompt the user if the passphrase is missing.

```swift
let result = await readium.publicationOpener.open(
asset: asset,
allowUserInteraction: true,
sender: sender
)
```

## Supporting additional formats or URL schemes

`DefaultPublicationParser` accepts additional parsers. You also have the option to use your own parser list by using `CompositePublicationParser` or create your own `PublicationParser` for a fully customized parsing resolution strategy.

The `AssetRetriever` offers an additional constructor that provides greater extensibility options, using:

* `ResourceFactory` which handles the URL schemes through which you can access content.
* `ArchiveOpener` which determines the types of archives (ZIP, RAR, etc.) that can be opened by the `AssetRetriever`.
* `FormatSniffer` which identifies the file formats that `AssetRetriever` can recognize.

You can use either the default implementations or implement your own for each of these components using the composite pattern. The toolkit's `CompositeResourceFactory`, `CompositeArchiveOpener`, and `CompositeFormatSniffer` provide a simple resolution strategy.

1 change: 1 addition & 0 deletions Documentation/Guides/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# User guides

* [Getting Started](Getting%20Started.md)
* [Opening a publication](Open%20Publication.md)
* [Extracting the content of a publication](Content.md)
* [Text-to-speech](TTS.md)
* [Supporting Readium LCP](Readium%20LCP.md)
Expand Down
68 changes: 65 additions & 3 deletions Documentation/Migration Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,64 @@ All migration steps necessary in reading apps to upgrade to major versions of th

## Unreleased

### Async APIs
### Opening a `Publication`

Plenty of completion-based APIs were changed to use `async` functions instead. Follow the deprecation warnings to update your codebase.
The `Streamer` object has been deprecated in favor of components with smaller responsibilities:

### Readium LCP SQLite adapter
* `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license
* `PublicationOpener` uses a publication parser and a set of content protections to create a `Publication` object from an `Asset`.

[See the user guide for a detailed explanation on how to use these new APIs](Guides/Open%20Publication.md).

### Typed URLs

The toolkit now includes a new set of URL types (`RelativeURL`, `AbsoluteURL`, `FileURL`, `HTTPURL`, etc.). These new types ensure that you only pass URLs supported by our APIs.

You can create an instance of such `URL` from its string representation:

```swift
FileURL(string: "file:///path/to%20a%20file")
FileURL(path: "/path/to a file")
HTTPURL(string: "https://domain.com/file")
```

Or convert an existing Foundation `URL`:

```swift
let url: URL
url.fileURL
url.httpURL
```

### Sniffing a `Format`

`MediaType` no longer has static helpers for sniffing it from a file or URL. Instead, you can use an `AssetRetriever` to retrieve the format of a file.

```swift
let assetRetriever = AssetRetriever(httpClient: DefaultHTTPClient())

switch await assetRetriever.sniffFormat(of: FileURL(string: ...)) {
case .success(let format):
print("Sniffed media type: \(format.mediaType)")
case .failure(let error):
// Failed to access the asset or recognize its format
}
```

The `MediaType` struct has been simplified. It now only holds the actual media type string. The name has been removed, and the file extension has been moved to `Format`.

### Navigator

All the navigator `go` APIs are now asynchronous and take an `options` argument instead of the `animated` boolean.

```diff
-navigator.go(to: locator, animated: true, completion: { }
+await navigator.go(to: locator, options: NavigatorGoOptions(animated: true))
```

### Readium LCP

#### Readium LCP SQLite adapter

The Readium LCP persistence layer was extracted to allow applications to provide their own implementations. The previous implementation is now part of a new package, `ReadiumAdapterLCPSQLite`, which you need to use to maintain the same behavior as before.

Expand Down Expand Up @@ -38,6 +91,15 @@ let lcpService = LCPService(
)
```

#### Introducing `LicenseDocumentSource`

The LCP APIs now accept a `LicenseDocumentSource` enum instead of a URL to an LCPL file. This approach is more flexible, as it doesn't require the LCPL file to be stored on the file system.

```diff
-lcpService.acquirePublication(from: url) { ... }
+await lcpService.acquirePublication(from: .file(FileURL(url: url)))
```


## 3.0.0-alpha.1

Expand Down
10 changes: 10 additions & 0 deletions Sources/LCP/LCPLicense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ public extension LCPLicense {
func renewLoan(with delegate: LCPRenewDelegate) async -> Result<Void, LCPError> {
await renewLoan(with: delegate, prefersWebPage: false)
}

@available(*, unavailable, message: "Use the async variant.")
func renewLoan(with delegate: LCPRenewDelegate, prefersWebPage: Bool, completion: @escaping (CancellableResult<Void, LCPError>) -> Void) {
fatalError()
}

@available(*, unavailable, message: "Use the async variant.")
func returnPublication(completion: @escaping (LCPError?) -> Void) {
fatalError()
}
}
5 changes: 4 additions & 1 deletion Sources/Shared/Publication/Asset/FileAsset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ import Foundation

/// Represents a publication stored as a file on the local file system.
@available(*, unavailable, message: "Use an `AssetRetriever` instead. See the migration guide.")
public final class FileAsset: PublicationAsset {}
public final class FileAsset: PublicationAsset {
public init(url: URL, mediaType: String? = nil) {}
public init(url: URL, mediaType: MediaType?) {}
}
5 changes: 5 additions & 0 deletions Sources/Shared/Toolkit/Format/MediaType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public struct MediaType: Hashable, Loggable, Sendable {
fatalError()
}

@available(*, unavailable, message: "File extension was moved to `Format`")
public var fileExtension: String {
fatalError()
}

/// Returns the UTI (Uniform Type Identifier) matching this media type, if any.
public var uti: String? {
UTI.findFrom(mediaTypes: [string], fileExtensions: [])?.string
Expand Down
9 changes: 3 additions & 6 deletions Sources/Shared/Toolkit/URL/Absolute URL/FileURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ public struct FileURL: AbsoluteURL, Hashable, Sendable {
}

/// Returns new `FileURL` with symlinks resolved
// FIXME: Async
public func resolvingSymlinks() -> Self {
public func resolvingSymlinks() async -> Self {
Self(url: url.resolvingSymlinksInPath())!
}

Expand All @@ -56,14 +55,12 @@ public struct FileURL: AbsoluteURL, Hashable, Sendable {
}

/// Returns whether the file exists on the file system.
// FIXME: Async
public func exists() throws -> Bool {
public func exists() async throws -> Bool {
try url.checkResourceIsReachable()
}

/// Returns whether the file is a directory.
// FIXME: Async
public func isDirectory() throws -> Bool {
public func isDirectory() async throws -> Bool {
try (url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
}

Expand Down
Loading