Skip to content

Commit abe4045

Browse files
authored
Documentation for the v3 (#475)
1 parent 892d5f5 commit abe4045

File tree

12 files changed

+249
-52
lines changed

12 files changed

+249
-52
lines changed

Documentation/Guides/Getting Started.md

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ The toolkit has been designed following these core tenets:
1616

1717
### Main packages
1818

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

2323
### Specialized packages
2424

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

33-
## Overview of the shared models (`R2Shared`)
33+
## Overview of the shared models (`ReadiumShared`)
3434

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

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

4949
#### Link
5050

51-
5251
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.
5352

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

7170
### Data models
7271

73-
#### Publication Asset
72+
#### Asset
7473

75-
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.
74+
An `Asset` represents a single file or package and provides access to its content. There are two types of `Asset`:
7675

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

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

81-
`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.
81+
You can use the `asset.format` to identify the media type and capabilities of the asset.
8282

83-
#### Fetcher
83+
```swift
84+
if asset.format.conformsTo(.lcp) {
85+
// The asset is protected with LCP.
86+
}
87+
if asset.format.conformsTo(.epub) {
88+
// The asset represents an EPUB publication.
89+
}
90+
```
8491

85-
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.
92+
#### Resource
8693

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

89-
## Opening a publication (`R2Streamer`)
96+
`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.
9097

91-
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.
98+
#### Container
9299

93-
```swift
94-
let file = URL(fileURLWithPath: "path/to/book.epub")
95-
let asset = FileAsset(file: file)
96-
```
100+
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.
101+
102+
`Publication` objects internally use a `Container` to expose its content.
103+
104+
## Opening a publication (`ReadiumStreamer`)
97105

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

100108
```swift
101-
let streamer = Streamer()
109+
// Instantiate the required components.
110+
let httpClient = DefaultHTTPClient()
111+
let assetRetriever = AssetRetriever(
112+
httpClient: httpClient
113+
)
114+
let publicationOpener = PublicationOpener(
115+
publicationParser: DefaultPublicationParser(
116+
httpClient: httpClient,
117+
assetRetriever: assetRetriever,
118+
pdfFactory: DefaultPDFDocumentFactory()
119+
)
120+
)
121+
122+
let url: URL = URL(...)
102123

103-
streamer.open(asset: asset, allowUserInteraction: false) { result in
104-
switch result {
124+
// Retrieve an `Asset` to access the file content.
125+
switch await assetRetriever.retrieve(url: url.anyURL.absoluteURL!) {
126+
case .success(let asset):
127+
// Open a `Publication` from the `Asset`.
128+
switch await publicationOpener.open(asset: asset, allowUserInteraction: true, sender: view) {
105129
case .success(let publication):
106130
print("Opened \(publication.metadata.title)")
131+
107132
case .failure(let error):
108-
alert(error.localizedDescription)
109-
case .cancelled:
110-
// The user cancelled the opening, for example by dismissing a password pop-up.
111-
break
133+
// Failed to access or parse the publication
112134
}
135+
136+
case .failure(let error):
137+
// Failed to retrieve the asset
113138
}
114139
```
115140

116141
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.
117142

143+
[See the dedicated user guide for more information](Open%20Publication.md).
144+
118145
## Accessing the metadata of a publication
119146

120147
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.
121148

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

124-
## Rendering the publication on the screen (`R2Navigator`)
151+
## Rendering the publication on the screen (`ReadiumNavigator`)
125152

126153
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.
127154

128-
```swift
129-
let navigator = try EPUBNavigatorViewController(
130-
publication: publication,
131-
initialLocation: lastReadLocation,
132-
httpServer: GCDHTTPServer.shared
133-
)
134-
135-
hostViewController.present(navigator, animated: true)
136-
```
137155
Please refer to the [Navigator guide](Navigator/Navigator.md) for more information.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Opening a publication
2+
3+
To open a publication with Readium, you need to instantiate a couple of components: an `AssetRetriever` and a `PublicationOpener`.
4+
5+
## `AssetRetriever`
6+
7+
The `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license.
8+
9+
### Constructing an `AssetRetriever`
10+
11+
You can create an instance of `AssetRetriever` with:
12+
13+
* 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.
14+
15+
```swift
16+
let assetRetriever = AssetRetriever(httpClient: DefaultHTTPClient())
17+
```
18+
19+
### Retrieving an `Asset`
20+
21+
With your fresh instance of `AssetRetriever`, you can open an `Asset` from any `AbsoluteURL`.
22+
23+
```swift
24+
// From a local file.
25+
let url = FileURL(string: "file:///path/to/book.epub")
26+
// or from an HTTP URL.
27+
let url = HTTPURL(string: "https://domain/book.epub")
28+
29+
switch await assetRetriever.retrieve(url: url) {
30+
case .success(let asset):
31+
...
32+
case .failure(let error):
33+
// Failed to retrieve the asset.
34+
}
35+
```
36+
37+
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.
38+
39+
```swift
40+
let mediaType = asset.format.mediaType
41+
42+
// Speed up the retrieval with a known media type.
43+
let result = await assetRetriever.retrieve(url: url, mediaType: mediaType)
44+
```
45+
46+
## `PublicationOpener`
47+
48+
`PublicationOpener` builds a `Publication` object from an `Asset` using:
49+
50+
* A `PublicationParser` to parse the asset structure and publication metadata.
51+
* The `DefaultPublicationParser` handles all the formats supported by Readium out of the box.
52+
* An optional list of `ContentProtection` to decrypt DRM-protected publications.
53+
* If you support Readium LCP, you can get one from the `LCPService`.
54+
55+
```swift
56+
let publicationOpener = PublicationOpener(
57+
parser: DefaultPublicationParser(
58+
httpClient: httpClient,
59+
assetRetriever: assetRetriever
60+
),
61+
contentProtections: [
62+
lcpService.contentProtection(with: LCPDialogAuthentication()),
63+
]
64+
)
65+
```
66+
67+
### Opening a `Publication`
68+
69+
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`.
70+
71+
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.
72+
73+
```swift
74+
let result = await readium.publicationOpener.open(
75+
asset: asset,
76+
allowUserInteraction: true,
77+
sender: sender
78+
)
79+
```
80+
81+
## Supporting additional formats or URL schemes
82+
83+
`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.
84+
85+
The `AssetRetriever` offers an additional constructor that provides greater extensibility options, using:
86+
87+
* `ResourceFactory` which handles the URL schemes through which you can access content.
88+
* `ArchiveOpener` which determines the types of archives (ZIP, RAR, etc.) that can be opened by the `AssetRetriever`.
89+
* `FormatSniffer` which identifies the file formats that `AssetRetriever` can recognize.
90+
91+
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.
92+

Documentation/Guides/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# User guides
22

33
* [Getting Started](Getting%20Started.md)
4+
* [Opening a publication](Open%20Publication.md)
45
* [Extracting the content of a publication](Content.md)
56
* [Text-to-speech](TTS.md)
67
* [Supporting Readium LCP](Readium%20LCP.md)

Documentation/Migration Guide.md

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,64 @@ All migration steps necessary in reading apps to upgrade to major versions of th
44

55
## Unreleased
66

7-
### Async APIs
7+
### Opening a `Publication`
88

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

11-
### Readium LCP SQLite adapter
11+
* `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license
12+
* `PublicationOpener` uses a publication parser and a set of content protections to create a `Publication` object from an `Asset`.
13+
14+
[See the user guide for a detailed explanation on how to use these new APIs](Guides/Open%20Publication.md).
15+
16+
### Typed URLs
17+
18+
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.
19+
20+
You can create an instance of such `URL` from its string representation:
21+
22+
```swift
23+
FileURL(string: "file:///path/to%20a%20file")
24+
FileURL(path: "/path/to a file")
25+
HTTPURL(string: "https://domain.com/file")
26+
```
27+
28+
Or convert an existing Foundation `URL`:
29+
30+
```swift
31+
let url: URL
32+
url.fileURL
33+
url.httpURL
34+
```
35+
36+
### Sniffing a `Format`
37+
38+
`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.
39+
40+
```swift
41+
let assetRetriever = AssetRetriever(httpClient: DefaultHTTPClient())
42+
43+
switch await assetRetriever.sniffFormat(of: FileURL(string: ...)) {
44+
case .success(let format):
45+
print("Sniffed media type: \(format.mediaType)")
46+
case .failure(let error):
47+
// Failed to access the asset or recognize its format
48+
}
49+
```
50+
51+
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`.
52+
53+
### Navigator
54+
55+
All the navigator `go` APIs are now asynchronous and take an `options` argument instead of the `animated` boolean.
56+
57+
```diff
58+
-navigator.go(to: locator, animated: true, completion: { }
59+
+await navigator.go(to: locator, options: NavigatorGoOptions(animated: true))
60+
```
61+
62+
### Readium LCP
63+
64+
#### Readium LCP SQLite adapter
1265

1366
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.
1467

@@ -38,6 +91,15 @@ let lcpService = LCPService(
3891
)
3992
```
4093

94+
#### Introducing `LicenseDocumentSource`
95+
96+
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.
97+
98+
```diff
99+
-lcpService.acquirePublication(from: url) { ... }
100+
+await lcpService.acquirePublication(from: .file(FileURL(url: url)))
101+
```
102+
41103

42104
## 3.0.0-alpha.1
43105

Sources/LCP/LCPLicense.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,14 @@ public extension LCPLicense {
5252
func renewLoan(with delegate: LCPRenewDelegate) async -> Result<Void, LCPError> {
5353
await renewLoan(with: delegate, prefersWebPage: false)
5454
}
55+
56+
@available(*, unavailable, message: "Use the async variant.")
57+
func renewLoan(with delegate: LCPRenewDelegate, prefersWebPage: Bool, completion: @escaping (CancellableResult<Void, LCPError>) -> Void) {
58+
fatalError()
59+
}
60+
61+
@available(*, unavailable, message: "Use the async variant.")
62+
func returnPublication(completion: @escaping (LCPError?) -> Void) {
63+
fatalError()
64+
}
5565
}

Sources/Shared/Publication/Asset/FileAsset.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ import Foundation
88

99
/// Represents a publication stored as a file on the local file system.
1010
@available(*, unavailable, message: "Use an `AssetRetriever` instead. See the migration guide.")
11-
public final class FileAsset: PublicationAsset {}
11+
public final class FileAsset: PublicationAsset {
12+
public init(url: URL, mediaType: String? = nil) {}
13+
public init(url: URL, mediaType: MediaType?) {}
14+
}

Sources/Shared/Toolkit/Format/MediaType.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public struct MediaType: Hashable, Loggable, Sendable {
6464
fatalError()
6565
}
6666

67+
@available(*, unavailable, message: "File extension was moved to `Format`")
68+
public var fileExtension: String {
69+
fatalError()
70+
}
71+
6772
/// Returns the UTI (Uniform Type Identifier) matching this media type, if any.
6873
public var uti: String? {
6974
UTI.findFrom(mediaTypes: [string], fileExtensions: [])?.string

Sources/Shared/Toolkit/URL/Absolute URL/FileURL.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ public struct FileURL: AbsoluteURL, Hashable, Sendable {
4545
}
4646

4747
/// Returns new `FileURL` with symlinks resolved
48-
// FIXME: Async
49-
public func resolvingSymlinks() -> Self {
48+
public func resolvingSymlinks() async -> Self {
5049
Self(url: url.resolvingSymlinksInPath())!
5150
}
5251

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

5857
/// Returns whether the file exists on the file system.
59-
// FIXME: Async
60-
public func exists() throws -> Bool {
58+
public func exists() async throws -> Bool {
6159
try url.checkResourceIsReachable()
6260
}
6361

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

0 commit comments

Comments
 (0)