From ca9ebb77498b113b33eff8b59c0c8c1bf092e807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Fri, 19 Jan 2024 19:05:54 +0100 Subject: [PATCH 1/9] Add a guide to interface with SwiftUI --- .../{ => Navigator}/Assets/settings-flow.svg | 0 .../Guides/{ => Navigator}/EPUB Fonts.md | 0 .../Preferences.md} | 0 Documentation/Guides/Navigator/SwiftUI.md | 136 ++++++++++++++++++ Documentation/Guides/README.md | 8 ++ 5 files changed, 144 insertions(+) rename Documentation/Guides/{ => Navigator}/Assets/settings-flow.svg (100%) rename Documentation/Guides/{ => Navigator}/EPUB Fonts.md (100%) rename Documentation/Guides/{Navigator Preferences.md => Navigator/Preferences.md} (100%) create mode 100644 Documentation/Guides/Navigator/SwiftUI.md create mode 100644 Documentation/Guides/README.md diff --git a/Documentation/Guides/Assets/settings-flow.svg b/Documentation/Guides/Navigator/Assets/settings-flow.svg similarity index 100% rename from Documentation/Guides/Assets/settings-flow.svg rename to Documentation/Guides/Navigator/Assets/settings-flow.svg diff --git a/Documentation/Guides/EPUB Fonts.md b/Documentation/Guides/Navigator/EPUB Fonts.md similarity index 100% rename from Documentation/Guides/EPUB Fonts.md rename to Documentation/Guides/Navigator/EPUB Fonts.md diff --git a/Documentation/Guides/Navigator Preferences.md b/Documentation/Guides/Navigator/Preferences.md similarity index 100% rename from Documentation/Guides/Navigator Preferences.md rename to Documentation/Guides/Navigator/Preferences.md diff --git a/Documentation/Guides/Navigator/SwiftUI.md b/Documentation/Guides/Navigator/SwiftUI.md new file mode 100644 index 000000000..5ca3b13fe --- /dev/null +++ b/Documentation/Guides/Navigator/SwiftUI.md @@ -0,0 +1,136 @@ +# Integrating the Navigator with SwiftUI + +The Navigator is built with UIKit and provides `UIViewController` implementations. Nevertheless, you can integrate them into a SwiftUI view hierarchy using Apple's [`UIViewRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewrepresentable). + +## SwiftUI wrapper for a Navigator's `UIViewController` + +Here is a basic example of a `UIViewControllerRepresentable` implementation that hosts a Navigator. + +```swift +/// SwiftUI wrapper for the `ReaderViewController`. +struct ReaderViewControllerWrapper: UIViewControllerRepresentable { + let viewController: ReaderViewController + + func makeUIViewController(context: Context) -> ReaderViewController { + viewController + } + + func updateUIViewController(_ uiViewController: ReaderViewController, context: Context) {} +} + +/// Host view controller for a Readium Navigator. +class ReaderViewController: UIViewController { + + /// View model provided by your application. + private let viewModel: ReaderViewModel + + /// Readium Navigator instance. + private let navigator: Navigator & UIViewController + + init(viewModel: ReaderViewModel, navigator: Navigator & UIViewController) { + self.viewModel = viewModel + self.navigator = navigator + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init?(coder: NSCoder) not implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addChild(navigator) + navigator.view.frame = view.bounds + navigator.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(navigator.view) + navigator.didMove(toParent: self) + } + + /// Handler for a custom editing action. + @objc func makeHighlight(_ sender: Any) { + viewModel.makeHighlight() + } +} +``` + +Note that we could use a `Navigator` instance directly, without a parent `ReaderViewController`. However, a host view controller is necessary if you want to use custom text selection menu items and capture events in the UIKit responder chain. For instance, when configuring your EPUB Navigator with: + +```swift +var config = EPUBNavigatorViewController.Configuration() +config.editingActions.append( + EditingAction( + title: "Highlight", + action: #selector(makeHighlight) + ) +) + +let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: locator, + config: config, + ... +) +``` + +## Embedding the `ReaderViewControllerWrapper` in a SwiftUI view + +```swift +struct ReaderView: View { + + /// View model provided by your application. + @ObservedObject var viewModel: ReaderViewModel + + let viewControllerWrapper: ReaderViewControllerWrapper + + var body: some View { + viewControllerWrapper + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + .navigationTitle(viewModel.book.title) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(viewModel.isFullscreen) + .statusBarHidden(viewModel.isFullscreen) + } +} +``` + +## Assembling the Navigator and SwiftUI objects + +Now, let's construct an EPUB navigator and assemble the SwiftUI view hierarchy to bring all the pieces together. + +```swift +var config = EPUBNavigatorViewController.Configuration() +config.editingActions.append( + EditingAction( + title: "Highlight", + action: #selector(highlightSelection) + ) +) + +let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: locator, + config: config, + ... +) + +// View model provided by your application. +let viewModel = ReaderViewModel() + +let view = ReaderView( + viewModel: viewModel, + viewControllerWrapper: ReaderViewControllerWrapper( + viewController: ReaderViewController( + viewModel: viewModel, + navigator: navigator + ) + ) +) +``` + +## Handling touch and keyboard events + +You still need to implement the `VisualNavigatorDelegate` protocol to handle gestures in the navigator. Avoid using SwiftUI touch modifiers, as they will prevent the user from interacting with the book. diff --git a/Documentation/Guides/README.md b/Documentation/Guides/README.md new file mode 100644 index 000000000..de276442e --- /dev/null +++ b/Documentation/Guides/README.md @@ -0,0 +1,8 @@ +# User guides + +* [Extracting the content of a publication](Content.md) +* [Text-to-speech](TTS.md) +* Navigator + * [Configuring the Navigator](Navigator/Preferences.md) + * [Font families in the EPUB navigator](Navigator/EPUB%20Fonts.md) + * [Integrating the Navigator with SwiftUI](Navigator/SwiftUI.md) \ No newline at end of file From 1798b3d52f1a40e7add97562d6c394d9bd0b8a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Fri, 19 Jan 2024 21:25:02 +0100 Subject: [PATCH 2/9] Getting started --- Documentation/Guides/Getting Started.md | 166 ++++++++++++++++++++++++ Documentation/Guides/README.md | 1 + README.md | 2 +- 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 Documentation/Guides/Getting Started.md diff --git a/Documentation/Guides/Getting Started.md b/Documentation/Guides/Getting Started.md new file mode 100644 index 000000000..c09be6521 --- /dev/null +++ b/Documentation/Guides/Getting Started.md @@ -0,0 +1,166 @@ +# Getting started + +The Readium Swift toolkit enables you to develop reading apps for iOS and iPadOS. It provides built-in support for multiple publication formats such as EPUB, PDF, audiobooks, and comics. + +:warning: Readium offers only low-level tools. You are responsible for creating a user interface for reading and managing books, as well as a data layer to store the user's publications. The Test App is an example of such integration. + +The toolkit is divided into separate packages that can be used independently. + +### 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. + +### Specialized packages + +* `ReadiumOPDS` parses [OPDS catalog feeds](https://opds.io) (both OPDS 1 and 2). +* `ReadiumLCP` downloads and decrypts [LCP-protected publications](https://www.edrlab.org/readium-lcp/). + +### Adapters to third-party dependencies + +* `ReadiumAdapterGCDWebServer` provides an HTTP server built with [GCDWebServer](https://github.com/swisspol/GCDWebServer). + +## Overview of the shared models (`R2Shared`) + +The Readium toolkit provides models used as exchange types between packages. + +### Publication + +`Publication` and its sub-components represent a single publication – ebook, audiobook or comic. It is loosely based on the [Readium Web Publication Manifest](https://readium.org/webpub-manifest/). + +A `Publication` instance: + +* holds the metadata of a publication, such as its author or table of contents, +* allows to read the contents of a publication, e.g. XHTML or audio resources, +* provides additional services, for example content extraction or text search + +### Link + +A [`Link` object](https://readium.org/webpub-manifest/#24-the-link-object) holds a pointer (URL) to a `Publication` resource along with additional metadata, such as its media type or title. + +The `Publication` contains several `Link` collections, for example: + +* `readingOrder` lists the publication resources arranged in the order they should be read. +* `resources` contains secondary resources necessary for rendering the `readingOrder`, such as an image or a font file. +* `tableOfContents` represents the table of contents as a tree of `Link` objects. + +### Locator + +A [`Locator` object](https://readium.org/architecture/models/locators/) represents a precise location in a publication resource in a format that can be stored and shared across reading systems. It is more accurate than a `Link` and contains additional information about the location, e.g. progression percentage, position or textual context. + +`Locator` objects are used for various features, including: + +* reporting the current progression in the publication +* saving bookmarks, highlights and annotations +* navigating search results + +## Opening a publication (`R2Streamer`) + +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. + +```swift +let file = URL(fileURLWithPath: "path/to/book.epub") +let asset = FileAsset(file: file) +``` + +Then, use a `Streamer` instance to parse the asset and create a `Publication` object. + +```swift +let streamer = Streamer() + +streamer.open(asset: asset, allowUserInteraction: false) { result in + switch result { + 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 + } +} +``` + +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. + +## 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. + +## Rendering the publication on the screen (`R2Navigator`) + +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. + +:warning: The `Navigator` does not have a user interface other than the view that displays the publication. The application is responsible for providing a user interface with bookmark buttons, a progress bar, etc. + +The Readium toolkit ships with one `Navigator` implementation per [publication profile](https://readium.org/webpub-manifest/profiles/). You can use `publication.conformsTo()` to determine the profile of a publication. + +| Profile | Navigator | Formats | +|-------------|-------------------------------|-----------------------------------------------------------------------| +| `epub` | `EPUBNavigatorViewController` | EPUB (`.epub`), Readium Web Publication (`.webpub`) | +| `pdf` | `PDFNavigatorViewController` | PDF (`.pdf`), LCP-protected PDF (`.lcpdf`) | +| `audiobook` | `AudioNavigator` | Zipped Audio Book (`.zab`), Readium Audiobook (`.audiobook`, `.lcpa`) | +| `divina` | `CBZNavigatorViewController` | Zipped Comic Book (`cbz`), Readium Divina (`.divina`) | + +```swift +if publication.conformsTo(.epub) { + let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: lastReadLocation, + httpServer: GCDHTTPServer.shared + ) + + hostViewController.present(navigator, animated: true) +} +``` + +:point_up: The HTTP server is used to serve the publication resources to the navigator's web views. You may use your own implementation, or the recommended `GCDHTTPServer` which is part of the `ReadiumAdapterGCDWebServer` package. + +## Navigating the contents of the publication (`R2Navigator`) + +The `Navigator` offers various `go` APIs for navigating the publication. For instance: + +* to the previous or next pages: `navigator.goForward()` or `navigator.goBackward()` +* to a link from the `publication.tableOfContents` or `publication.readingOrder`: `navigator.go(to: link)` +* to a locator from a search result: `navigator.go(to: locator)` + +## Saving and restoring the last read location (`R2Navigator`) + +You can observe the current position in the publication by implementing a `NavigatorDelegate`. + +```swift +navigator.delegate = MyNavigatorDelegate() + +class MyNavigatorDelegate: NavigatorDelegate { + + override func navigator(_ navigator: Navigator, locationDidChange locator: Locator) { + if let position = locator.locations.position { + print("At position \(position) on \(publication.positions.count)") + } + if let progression = locator.locations.progression { + return "Progression in the current resource: \(progression)%" + } + if let totalProgression = locator.locations.totalProgression { + return "Total progression in the publication: \(progression)%" + } + + // Save the position in your bookshelf database + database.saveLastReadLocation(locator.jsonString) + } +} +``` + +The `Locator` object may be serialized to JSON in your database, and deserialized to set the initial location when creating the navigator. + +```swift +let lastReadLocation = Locator(jsonString: dabase.lastReadLocation()) + +let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: lastReadLocation, + httpServer: GCDHTTPServer.shared +) +``` diff --git a/Documentation/Guides/README.md b/Documentation/Guides/README.md index de276442e..d2f362d26 100644 --- a/Documentation/Guides/README.md +++ b/Documentation/Guides/README.md @@ -1,5 +1,6 @@ # User guides +* [Getting Started](Getting%20Started.md) * [Extracting the content of a publication](Content.md) * [Text-to-speech](TTS.md) * Navigator diff --git a/README.md b/README.md index 3f8b1db56..3982d699b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This toolkit is a modular project, which follows the [Readium Architecture](http * [`ReadiumOPDS`](Sources/OPDS) – Parsers for OPDS catalog feeds * [`ReadiumLCP`](Sources/LCP) – Service and models for [Readium LCP](https://www.edrlab.org/readium-lcp/) -A [Test App](TestApp) demonstrates how to integrate the Readium Swift toolkit in your own reading app +Take a look at the [guide to get started](Documentation/Guides/Getting%20Started.md). A [Test App](TestApp) demonstrates how to integrate the Readium Swift toolkit in your own reading app. ## Minimum Requirements From 72db04f29e8d603a09847bc1f57c89d94b970469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Thu, 25 Jan 2024 15:05:35 +0100 Subject: [PATCH 3/9] Maintaining --- MAINTAINING.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 MAINTAINING.md diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 000000000..af381da8a --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,41 @@ +# Maintaining the Readium Swift toolkit + +## Releasing a new version + +You are ready to release a new version of the Swift toolkit? Great, follow these steps: + +1. Figure out the next version using the [semantic versioning scheme](https://semver.org). +2. Test a migration from the last released version. + 1. Create a **temporary** Git tag for `develop` with the next version tag (e.g. `3.0.1`). + 2. Clone the `swift-toolkit` from the previous version (`main` branch). + 3. Under `TestApp`, initialize it with the next toolkit version: + ```shell + make spm version=3.0.1 lcp=... + ``` + 4. Try to run the Test App, adjusting the integration if needed. + 5. Delete the Git tag created previously. +3. Update the [migration guide](Documentation/Migration%20Guide.md) in case of breaking changes. +4. Issue the new release. + 1. Create a branch with the same name as the future tag, from `develop`. + 2. Bump the version numbers in the `Support/CocoaPods/*.podspec` files. + * :warning: Don't forget to use `:tag` in the `Podspec` files instead of `:branch`, [for example](https://github.com/readium/swift-toolkit/pull/353/commits/a0714589b3da928dd923ba78f379116715797333#diff-b726fa4aff3ea878dedf3e0f78607c09975ef5412966dc1b547d9b5e9e4b0d9cL9). + 3. Bump the version numbers in `README.md`. + 4. Bump the version numbers in `TestApp/Sources/Info.plist`. + 5. Close the version in the `CHANGELOG.md`, [for example](https://github.com/readium/swift-toolkit/pull/353/commits/a0714589b3da928dd923ba78f379116715797333#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). + 6. Create a PR to merge in `develop` and verify the CI workflows. + 7. Squash and merge the PR. + 8. Tag the new version from `develop`. + ```shell + git checkout develop + git pull + git tag -a 3.0.1 -m 3.0.1 + git push --tags + ``` +5. Verify you can fetch the new version from the latest Test App with `make spm|carthage|cocoapods version=3.0.1` +7. Announce the release. + 1. Create a new release on GitHub. + 2. Publish a new TestFlight beta with LCP enabled. + * Click on "External Groups" > "Public Beta", then add the new build so that it's available to everyone. +8. Merge `develop` into `main`. +9. :warning: Revert to `:branch => "develop"` in the `Podspec` files in `develop`. + From 68fe1ed44762bb707b46016f3f4e91c6e4c6f019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Wed, 21 Feb 2024 17:51:13 +0100 Subject: [PATCH 4/9] Supporting Readium LCP --- Documentation/Guides/README.md | 1 + Documentation/Guides/Readium LCP.md | 375 ++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 Documentation/Guides/Readium LCP.md diff --git a/Documentation/Guides/README.md b/Documentation/Guides/README.md index d2f362d26..03794df23 100644 --- a/Documentation/Guides/README.md +++ b/Documentation/Guides/README.md @@ -3,6 +3,7 @@ * [Getting Started](Getting%20Started.md) * [Extracting the content of a publication](Content.md) * [Text-to-speech](TTS.md) +* [Supporting Readium LCP](Readium%20LCP.md) * Navigator * [Configuring the Navigator](Navigator/Preferences.md) * [Font families in the EPUB navigator](Navigator/EPUB%20Fonts.md) diff --git a/Documentation/Guides/Readium LCP.md b/Documentation/Guides/Readium LCP.md new file mode 100644 index 000000000..5c3a5a4cb --- /dev/null +++ b/Documentation/Guides/Readium LCP.md @@ -0,0 +1,375 @@ +# Supporting Readium LCP + +You can use the Readium Swift toolkit to download and read publications that are protected with the [Readium LCP](https://www.edrlab.org/readium-lcp/) DRM. + +:point_up: To use LCP with the Readium toolkit, you must first obtain the `R2LCPClient` private library by contacting [EDRLab](https://www.edrlab.org/contact/). + +## Overview + +An LCP publication is distributed using an LCP License Document (`.lcpl`) protected with a *user passphrase*. + +The user flow typically goes as follows: + +1. The user imports a `.lcpl` file into your application. +2. The application uses the Readium toolkit to download the protected publication from the `.lcpl` file to the user's bookshelf. The downloaded file can be a `.epub`, `.lcpdf` (PDF), or `.lcpa` (audiobook) package. +3. The user opens the protected publication from the bookshelf. +4. If the passphrase isn't saved, the user will be asked to enter it to unlock the contents. +5. The publication is decrypted and rendered on the screen. + +## Setup + +To support LCP in your application, you require two components: + +* The `ReadiumLCP` package from the toolkit provides APIs for downloading and decrypting protected publications. Import it as you would any other Readium package, such as `R2Navigator`. +* The private `R2LCPClient` library customized for your application [is available from EDRLab](https://www.edrlab.org/contact/). They will provide instructions for integrating the `R2LCPClient` framework into your application. + +### File formats + +Readium LCP specifies new file formats. + +| Name | File extension | Media type | +|------|----------------|------------| +| [License Document](https://readium.org/lcp-specs/releases/lcp/latest.html#32-content-conformance) | `.lcpl` | `application/vnd.readium.lcp.license.v1.0+json` | +| [LCP for PDF package](https://readium.org/lcp-specs/notes/lcp-for-pdf.html) | `.lcpdf` | `application/pdf+lcp` | +| [LCP for Audiobooks package](https://readium.org/lcp-specs/notes/lcp-for-audiobooks.html) | `.lcpa` | `application/audiobook+lcp` | + +:point_up: EPUB files protected by LCP are supported without a special file extension or media type because EPUB accommodates any DRM scheme in its specification. + +To support these formats in your application, you need to [register them in your `Info.plist`](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app) as imported types. + +```xml + + UTImportedTypeDeclarations + + + UTTypeIdentifier + org.readium.lcpl + UTTypeConformsTo + + public.content + public.data + public.json + + UTTypeDescription + LCP License Document + UTTypeTagSpecification + + public.filename-extension + + lcpl + + public.mime-type + application/vnd.readium.lcp.license.v1.0+json + + + + UTTypeIdentifier + org.readium.lcpdf + UTTypeConformsTo + + public.content + public.data + public.archive + public.zip-archive + + UTTypeDescription + LCP for PDF package + UTTypeTagSpecification + + public.filename-extension + + lcpdf + + public.mime-type + application/pdf+lcp + + + + UTTypeIdentifier + org.readium.lcpa + UTTypeConformsTo + + public.content + public.data + public.archive + public.zip-archive + + UTTypeDescription + LCP for Audiobooks package + UTTypeTagSpecification + + public.filename-extension + + lcpa + + public.mime-type + application/audiobook+lcp + + + + +``` + +Next, declare the imported types as [Document Types](https://help.apple.com/xcode/mac/current/#/devddd273fdd) in the `Info.plist` to have your application listed in the "Open with..." dialogs. + +```xml + + CFBundleDocumentTypes + + + CFBundleTypeName + LCP License Document + CFBundleTypeRole + Viewer + LSItemContentTypes + + org.readium.lcpl + + + + CFBundleTypeName + LCP for PDF package + CFBundleTypeRole + Viewer + LSItemContentTypes + + org.readium.lcpdf + + + + CFBundleTypeName + LCP for Audiobooks package + CFBundleTypeRole + Viewer + LSItemContentTypes + + org.readium.lcpa + + + + +``` + +:point_up: If EPUB is not included in your document types, now is a good time to add it. + +## Initializing the `LCPService` + +`ReadiumLCP` offers an `LCPService` object that exposes its API. Since the `ReadiumLCP` package is not linked with `R2LCPClient`, you need to create your own adapter when setting up the `LCPService`. + +```swift +import R2LCPClient +import ReadiumLCP + +let lcpService = LCPService(client: LCPClientAdapter()) + +/// Facade to the private R2LCPClient.framework. +class LCPClientAdapter: ReadiumLCP.LCPClient { + func createContext(jsonLicense: String, hashedPassphrase: String, pemCrl: String) throws -> LCPClientContext { + try R2LCPClient.createContext(jsonLicense: jsonLicense, hashedPassphrase: hashedPassphrase, pemCrl: pemCrl) + } + + func decrypt(data: Data, using context: LCPClientContext) -> Data? { + R2LCPClient.decrypt(data: data, using: context as! DRMContext) + } + + func findOneValidPassphrase(jsonLicense: String, hashedPassphrases: [String]) -> String? { + R2LCPClient.findOneValidPassphrase(jsonLicense: jsonLicense, hashedPassphrases: hashedPassphrases) + } +} +``` + +## Acquiring a publication from a License Document (LCPL) + +Users need to import a License Document into your application to download the protected publication (`.epub`, `.lcpdf`, or `.lcpa`). + +The `LCPService` offers an API to retrieve the full publication from an LCPL on the filesystem. + +```swift +let acquisition = lcpService.acquirePublication( + from: lcplURL, + onProgress: { progress in + switch progress { + case .indefinite: + // Display an activity indicator. + case .percent(let percent): + // Display a progress bar with percent from 0 to 1. + } + }, + completion: { result in + switch result { + case let .success(publication): + // Import the `publication.localURL` file as any publication. + case let .failure(error): + // Display the error message + case .cancelled: + // The acquisition was cancelled before completion. + } + } +) +``` + +If the user wants to cancel the download, call `cancel()` on the object returned by `LCPService.acquirePublication()`. + +After the download is completed, import the `publication.localURL` file into the bookshelf like any other publication file. + +## Opening a publication protected with LCP + +### Initializing the `Streamer` + +A publication protected with LCP can be opened using the `Streamer` component, just like a non-protected publication. However, you must provide a [`ContentProtection`](https://readium.org/architecture/proposals/006-content-protection.html) implementation when initializing the `Streamer` to enable LCP. Luckily, `LCPService` has you covered. + +```swift +let authentication = LCPDialogAuthentication() + +let streamer = Streamer( + contentProtections: [ + lcpService.contentProtection(with: authentication) + ] +) +``` + +An LCP package is secured with a *user passphrase* for decrypting the content. The `LCPAuthenticating` protocol used by `LCPService.contentProtection(with:)` provides the passphrase when needed. You can use the default `LCPDialogAuthentication` which displays a pop-up to enter the passphrase, or implement your own method for passphrase retrieval. + +:point_up: The user will be prompted once per passphrase since `ReadiumLCP` stores known passphrases on the device. + +### Opening the publication + +You are now ready to open the publication file with your `Streamer` instance. + +```swift +streamer.open( + asset: FileAsset(url: publicationURL), + allowUserInteraction: true, + sender: hostViewController, + completion: { result in + switch result { + case .success(let publication): + // Import or present the publication. + case .failure(let error): + // Present the error. + case .cancelled: + // The operation was cancelled. + } + } +) +``` + +The `allowUserInteraction` and `sender` arguments are forwarded to the `LCPAuthenticating` implementation when the passphrase unknown. `LCPDialogAuthentication` shows a pop-up only if `allowUserInteraction` is `true`, using the `sender` as the pop-up's host `UIViewController`. + +When importing the publication to the bookshelf, set `allowUserInteraction` to `false` as you don't need the passphrase for accessing the publication metadata and cover. If you intend to present the publication using a Navigator, set `allowUserInteraction` to `true` as decryption will be required. + +:point_up: To check if a publication is protected with LCP before opening it, you can use `LCPService.isLCPProtected()`. + +### Using the opened `Publication` + +After obtaining a `Publication` instance, you can access the publication's metadata to import it into the user's bookshelf. The user passphrase is not needed for reading the metadata or cover. + +However, if you want to display the publication with a Navigator, verify it is not restricted. It could be restricted if the user passphrase is unknown or if the license is no longer valid (e.g., expired loan, revoked purchase, etc.). + +```swift +if publication.isRestricted { + if let error = publication.protectionError as? LCPError { + // The user is not allowed to open the publication. + // You should display the error. + } else { + // We don't have the user passphrase. + // You may use `publication` to access its + // metadata, but not to render its content. + } +} else { + // The publication is not restricted, you may + // render it with a Navigator component. +} +``` + +## Obtaining information on an LCP license + +An LCP License Document contains metadata such as its expiration date, the remaining number of characters to copy and the user name. You can access this information using an `LCPLicense` object. + +Use the `LCPService` to retrieve the `LCPLicense` instance for a publication. + +```swift +lcpService.retrieveLicense( + from: publicationURL, + authentication: LCPDialogAuthentication(), + allowUserInteraction: true, + sender: hostViewController +) { result in + switch result { + case .success(let lcpLicense): + if let lcpLicense = lcpLicense { + if let user = lcpLicense.license.user.name { + print("The publication was acquired by \(user)") + } + if let endDate = lcpLicense.license.rights.end { + print("The loan expires on \(endDate)") + } + if let copyLeft = lcpLicense.charactersToCopyLeft { + print("You can copy up to \(copyLeft) characters remaining.") + } + } else { + // The file was not protected by LCP. + } + case .failure(let error): + // Display the error. + case .cancelled: + // The operation was cancelled. + } +} +``` + +If you have already opened a `Publication` with the `Streamer`, you can directly obtain the `LCPLicense` using `publication.lcpLicense`. + +## Managing a loan + +Readium LCP allows borrowing publications for a specific period. Use the `LCPLicense` object to manage a loan and retrieve its end date using `lcpLicense.license.rights.end`. + +### Returning a loan + +Some loans can be returned before the end date. You can confirm this by using `lcpLicense.canReturnPublication`. To return the publication, execute: + +```swift +lcpLicense.returnPublication { error in + if let error = error { + // Present the error. + } else { + // The publication was returned. + } +} +``` + +### Renewing a loan + +The loan end date may also be extended. You can confirm this by using `lcpLicense.canRenewLoan`. + +Readium LCP supports [two types of renewal interactions](https://readium.org/lcp-specs/releases/lsd/latest#35-renewing-a-license): + +* Programmatic: You show your own user interface. +* Interactive: You display a web view, and the Readium LSD server manages the renewal for you. + +You need to support both interactions by implementing the `LCPRenewDelegate` protocol. A default implementation is available with `LCPDefaultRenewDelegate`. + +```swift +lcpLicense.renewLoan( + with: LCPDefaultRenewDelegate( + presentingViewController: hostViewController + ) +) { result in + switch result { + case .success, .cancelled: + // The publication was renewed. + case let .failure(error): + // Display the error. + } +} +``` + +## Handling `LCPError` + +The APIs may fail with an `LCPError`. These errors **must** be displayed to the user with a suitable message. + +LCPError implements LocalizedError, enabling you to retrieve a user-friendly message. It's advised to customize the LCP localized strings in your app for translation. These strings can be found at Sources/LCP/Resources/en.lproj/Localizable.strings. + +`LCPError` implements `LocalizedError`, enabling you to retrieve a user-friendly message. It's recommended to override the LCP localized strings in your app to translate them. These strings can be found at [Sources/LCP/Resources/en.lproj/Localizable.strings](https://github.com/readium/swift-toolkit/blob/main/Sources/LCP/Resources/en.lproj/Localizable.strings). + +:warning: In the next major update, `LCPError` will no longer be localized. Applications will need to provide their own localized error messages. If you are adding LCP to a new app, consider treating `LCPError` as non-localized from the start to ease future migration. \ No newline at end of file From 1f15efa2753825b39e8b1477101edea42ee33863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Thu, 22 Feb 2024 16:38:14 +0100 Subject: [PATCH 5/9] Add a Navigator guide --- Documentation/Guides/Getting Started.md | 70 +------ Documentation/Guides/Navigator/Navigator.md | 194 ++++++++++++++++++ Documentation/Guides/Navigator/Preferences.md | 2 - README.md | 10 +- 4 files changed, 200 insertions(+), 76 deletions(-) create mode 100644 Documentation/Guides/Navigator/Navigator.md diff --git a/Documentation/Guides/Getting Started.md b/Documentation/Guides/Getting Started.md index c09be6521..feed553d2 100644 --- a/Documentation/Guides/Getting Started.md +++ b/Documentation/Guides/Getting Started.md @@ -10,12 +10,12 @@ The toolkit is divided into separate packages that can be used independently. * `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. +* [`R2Navigator` renders the content of a publication](Navigator/Navigator.md). ### Specialized packages * `ReadiumOPDS` parses [OPDS catalog feeds](https://opds.io) (both OPDS 1 and 2). -* `ReadiumLCP` downloads and decrypts [LCP-protected publications](https://www.edrlab.org/readium-lcp/). +* [`ReadiumLCP` downloads and decrypts LCP-protected publications](Readium%20LCP.md). ### Adapters to third-party dependencies @@ -94,73 +94,13 @@ You can retrieve the publication cover using `publication.cover`. Avoid calling 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. -:warning: The `Navigator` does not have a user interface other than the view that displays the publication. The application is responsible for providing a user interface with bookmark buttons, a progress bar, etc. - -The Readium toolkit ships with one `Navigator` implementation per [publication profile](https://readium.org/webpub-manifest/profiles/). You can use `publication.conformsTo()` to determine the profile of a publication. - -| Profile | Navigator | Formats | -|-------------|-------------------------------|-----------------------------------------------------------------------| -| `epub` | `EPUBNavigatorViewController` | EPUB (`.epub`), Readium Web Publication (`.webpub`) | -| `pdf` | `PDFNavigatorViewController` | PDF (`.pdf`), LCP-protected PDF (`.lcpdf`) | -| `audiobook` | `AudioNavigator` | Zipped Audio Book (`.zab`), Readium Audiobook (`.audiobook`, `.lcpa`) | -| `divina` | `CBZNavigatorViewController` | Zipped Comic Book (`cbz`), Readium Divina (`.divina`) | - ```swift -if publication.conformsTo(.epub) { - let navigator = try EPUBNavigatorViewController( - publication: publication, - initialLocation: lastReadLocation, - httpServer: GCDHTTPServer.shared - ) - - hostViewController.present(navigator, animated: true) -} -``` - -:point_up: The HTTP server is used to serve the publication resources to the navigator's web views. You may use your own implementation, or the recommended `GCDHTTPServer` which is part of the `ReadiumAdapterGCDWebServer` package. - -## Navigating the contents of the publication (`R2Navigator`) - -The `Navigator` offers various `go` APIs for navigating the publication. For instance: - -* to the previous or next pages: `navigator.goForward()` or `navigator.goBackward()` -* to a link from the `publication.tableOfContents` or `publication.readingOrder`: `navigator.go(to: link)` -* to a locator from a search result: `navigator.go(to: locator)` - -## Saving and restoring the last read location (`R2Navigator`) - -You can observe the current position in the publication by implementing a `NavigatorDelegate`. - -```swift -navigator.delegate = MyNavigatorDelegate() - -class MyNavigatorDelegate: NavigatorDelegate { - - override func navigator(_ navigator: Navigator, locationDidChange locator: Locator) { - if let position = locator.locations.position { - print("At position \(position) on \(publication.positions.count)") - } - if let progression = locator.locations.progression { - return "Progression in the current resource: \(progression)%" - } - if let totalProgression = locator.locations.totalProgression { - return "Total progression in the publication: \(progression)%" - } - - // Save the position in your bookshelf database - database.saveLastReadLocation(locator.jsonString) - } -} -``` - -The `Locator` object may be serialized to JSON in your database, and deserialized to set the initial location when creating the navigator. - -```swift -let lastReadLocation = Locator(jsonString: dabase.lastReadLocation()) - 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. \ No newline at end of file diff --git a/Documentation/Guides/Navigator/Navigator.md b/Documentation/Guides/Navigator/Navigator.md new file mode 100644 index 000000000..59182d477 --- /dev/null +++ b/Documentation/Guides/Navigator/Navigator.md @@ -0,0 +1,194 @@ +# Navigator + +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. + +:warning: Navigators do not have user interfaces besides the view that displays the publication. Applications are responsible for providing a user interface with bookmark buttons, a progress bar, etc. + +## Default implementations + +The Readium toolkit comes with several `Navigator` implementations for different publication profiles. Some are `UIViewController`s, designed to be added to your view hierarchy, while others are chromeless and can be used in the background. + +| Navigator | Profile | Formats | +|-------------------------------|-------------|-----------------------------------------------------------------------| +| `EPUBNavigatorViewController` | `epub` | EPUB (`.epub`), Readium Web Publication (`.webpub`) | +| `PDFNavigatorViewController` | `pdf` | PDF (`.pdf`), LCP-protected PDF (`.lcpdf`) | +| `CBZNavigatorViewController` | `divina` | Zipped Comic Book (`cbz`), Readium Divina (`.divina`) | +| `AudioNavigator` | `audiobook` | Zipped Audio Book (`.zab`), Readium Audiobook (`.audiobook`, `.lcpa`) | + +To find out which Navigator is compatible with a publication, refer to its [profile](https://readium.org/webpub-manifest/profiles/). Use `publication.conformsTo()` to identify the publication's profile. + +```swift +if publication.conformsTo(.epub) { + let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: lastReadLocation, + httpServer: GCDHTTPServer.shared + ) + + hostViewController.present(navigator, animated: true) +} +``` + +## Navigator APIs + +Navigators implement a set of shared interfaces to help reuse the reading logic across publication profiles. For example, instead of using specific implementations like `EPUBNavigatorViewController`, use the `Navigator` interface to create a location history manager compatible with all Navigator types. + +You can create custom Navigators and easily integrate them into your app with minimal modifications by implementing these interfaces. + +### `Navigator` interface + +All Navigators implement the `Navigator` interface, which provides the foundation for navigating resources in a `Publication`. You can use it to move through the publication's content or to find the current position. + +Note that this interface does not specify how the content is presented to the user. + +### `VisualNavigator` interface + +Navigators rendering the content visually on the screen implement the `VisualNavigator` interface. This interface offers details about the presentation style (e.g., scrolled, right-to-left, etc.) and allows monitoring input events like taps or keyboard strokes. + +### `SelectableNavigator` interface + +Navigators enabling users to select parts of the content implement `SelectableNavigator`. You can use it to extract the `Locator` and content of the selected portion. + +### `DecorableNavigator` interface + +A Decorable Navigator is able to render decorations over a publication, such as highlights or margin icons. + +[See the corresponding proposal for more information](https://readium.org/architecture/proposals/008-decorator-api.html). + +## Instantiating a Navigator + +### Visual Navigators + +The Visual Navigators are implemented as `UIViewController` and must be added to your iOS view hierarchy to render the publication contents. + +```swift +let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: lastReadLocation, + httpServer: GCDHTTPServer.shared +) + +hostViewController.present(navigator, animated: true) +``` + +:point_up: The HTTP server is used to serve the publication resources to the Navigator. You may use your own implementation, or the recommended `GCDHTTPServer` which is part of the `ReadiumAdapterGCDWebServer` package. + +### Audio Navigator + +The `AudioNavigator` is chromeless and does not provide any user interface, allowing you to create your own custom UI. + +```swift +let navigator = AudioNavigator( + publication: publication, + initialLocation: lastReadLocation +) + +navigator.play() +``` + +## Navigating the contents of the publication + +The `Navigator` offers various `go` APIs for navigating the publication. For instance: + +* to the previous or next pages: `navigator.goForward()` or `navigator.goBackward()` +* to a link from the `publication.tableOfContents` or `publication.readingOrder`: `navigator.go(to: link)` +* to a locator from a search result: `navigator.go(to: locator)` + +## Reading progression + +## Saving and restoring the last read location + +Navigators don't store any data permanently. Therefore, it is your responsibility to save the last read location in your database and restore it when creating a new Navigator. + +You can observe the current position in the publication by implementing a `NavigatorDelegate`. + +```swift +navigator.delegate = MyNavigatorDelegate() + +class MyNavigatorDelegate: NavigatorDelegate { + + override func navigator(_ navigator: Navigator, locationDidChange locator: Locator) { + if let position = locator.locations.position { + print("At position \(position) on \(publication.positions.count)") + } + if let progression = locator.locations.progression { + return "Progression in the current resource: \(progression)%" + } + if let totalProgression = locator.locations.totalProgression { + return "Total progression in the publication: \(progression)%" + } + + // Save the position in your bookshelf database + database.saveLastReadLocation(locator.jsonString) + } +} +``` + +The `Locator` object may be serialized to JSON in your database, and deserialized to set the initial location when creating the navigator. + +```swift +let lastReadLocation = Locator(jsonString: dabase.lastReadLocation()) + +let navigator = try EPUBNavigatorViewController( + publication: publication, + initialLocation: lastReadLocation, + httpServer: GCDHTTPServer.shared +) +``` + +### Bookmarking the current location + +Use a Navigator's `currentLocation` property to persists the current position, for instance as a bookmark. + +After the user selects a bookmark from your user interface, navigate to it using `navigator.go(bookmark.locator)`. + +### Displaying a progression slider + +To display a percentage-based progression slider, use the `locations.totalProgression` property of the `currentLocation`. This property holds the total progression across an entire publication. + +Given a progression from 0 to 1, you can obtain a `Locator` object from the `Publication`. This can be used to navigate to a specific percentage within the publication. + +```swift +if let locator = publication.locate(progression: 0.5) { + navigator.go(to: locator) +} +``` + +### Displaying the number of positions + +:warning: Readium does not have the concept of pages, as they are not useful when dealing with reflowable publications across different screen sizes. Instead, we use [**positions**](https://readium.org/architecture/models/locators/positions/) which remain stable even when the user changes the font size or device. + +Not all Navigators provide positions, but most `VisualNavigator` implementations do. Verify if `publication.positions` is not empty to determine if it is supported. + +To find the total positions in the publication, use `publication.positions.count`. You can get the current position with `navigator.currentLocation?.locations.position`. + +## Navigating with edge taps and keyboard arrows + +Readium provides a `DirectionalNavigationAdapter` helper to turn pages using arrow and space keys or screen taps. + +You can use it from your `VisualNavigatorDelegate` implementation: + +```swift +extension MyReader: VisualNavigatorDelegate { + + func navigator(_ navigator: VisualNavigator, didTapAt point: CGPoint) { + // Turn pages when tapping the edge of the screen. + guard !DirectionalNavigationAdapter(navigator: navigator).didTap(at: point) else { + return + } + + toggleNavigationBar() + } + + func navigator(_ navigator: VisualNavigator, didPressKey event: KeyEvent) { + // Turn pages when pressing the arrow keys. + DirectionalNavigationAdapter(navigator: navigator).didPressKey(event: event) + } +} +``` + +`DirectionalNavigationAdapter` offers a lot of customization options. Take a look at its API. + +## User preferences + +Readium Navigators support user preferences, such as font size or background color. Take a look at [the Preferences API guide](Preferences.md) for more information. diff --git a/Documentation/Guides/Navigator/Preferences.md b/Documentation/Guides/Navigator/Preferences.md index 2b3890994..74355862e 100644 --- a/Documentation/Guides/Navigator/Preferences.md +++ b/Documentation/Guides/Navigator/Preferences.md @@ -1,7 +1,5 @@ # Configuring the Navigator -:warning: The Navigator Setting API is brand new and currently only available with `EPUBNavigatorViewController` and `PDFNavigatorViewController`. - Take a look at the [migration guide](../Migration%20Guide.md) if you are already using the legacy EPUB settings. ## Overview diff --git a/README.md b/README.md index 3982d699b..528459145 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,7 @@ [Readium Mobile](https://github.com/readium/mobile) is a toolkit for ebooks, audiobooks and comics written in Swift & Kotlin. -This toolkit is a modular project, which follows the [Readium Architecture](https://github.com/readium/architecture). - -* [`R2Shared`](Sources/Shared) – Shared `Publication` models and utilities -* [`R2Streamer`](Sources/Streamer) – Publication parsers and local HTTP server -* [`R2Navigator`](Sources/Navigator) – Plain `UIViewController` classes rendering publications -* [`ReadiumOPDS`](Sources/OPDS) – Parsers for OPDS catalog feeds -* [`ReadiumLCP`](Sources/LCP) – Service and models for [Readium LCP](https://www.edrlab.org/readium-lcp/) - -Take a look at the [guide to get started](Documentation/Guides/Getting%20Started.md). A [Test App](TestApp) demonstrates how to integrate the Readium Swift toolkit in your own reading app. +:point_up: **Take a look at the [guide to get started](Documentation/Guides/Getting%20Started.md).** A [Test App](TestApp) demonstrates how to integrate the Readium Swift toolkit in your own reading app. ## Minimum Requirements From 58f47ac72be6817bef859403afd25eeff497bf0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Thu, 22 Feb 2024 17:11:04 +0100 Subject: [PATCH 6/9] Minor fixes --- Documentation/Guides/Navigator/Navigator.md | 4 ++-- Documentation/Guides/README.md | 2 +- Documentation/Guides/Readium LCP.md | 13 ++++--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Documentation/Guides/Navigator/Navigator.md b/Documentation/Guides/Navigator/Navigator.md index 59182d477..736713535 100644 --- a/Documentation/Guides/Navigator/Navigator.md +++ b/Documentation/Guides/Navigator/Navigator.md @@ -88,7 +88,7 @@ navigator.play() ## Navigating the contents of the publication -The `Navigator` offers various `go` APIs for navigating the publication. For instance: +The `Navigator` interface offers various `go` APIs for navigating the publication. For instance: * to the previous or next pages: `navigator.goForward()` or `navigator.goBackward()` * to a link from the `publication.tableOfContents` or `publication.readingOrder`: `navigator.go(to: link)` @@ -96,7 +96,7 @@ The `Navigator` offers various `go` APIs for navigating the publication. For ins ## Reading progression -## Saving and restoring the last read location +### Saving and restoring the last read location Navigators don't store any data permanently. Therefore, it is your responsibility to save the last read location in your database and restore it when creating a new Navigator. diff --git a/Documentation/Guides/README.md b/Documentation/Guides/README.md index 03794df23..bf45e146c 100644 --- a/Documentation/Guides/README.md +++ b/Documentation/Guides/README.md @@ -4,7 +4,7 @@ * [Extracting the content of a publication](Content.md) * [Text-to-speech](TTS.md) * [Supporting Readium LCP](Readium%20LCP.md) -* Navigator +* [Navigator](Navigator/Navigator.md) * [Configuring the Navigator](Navigator/Preferences.md) * [Font families in the EPUB navigator](Navigator/EPUB%20Fonts.md) * [Integrating the Navigator with SwiftUI](Navigator/SwiftUI.md) \ No newline at end of file diff --git a/Documentation/Guides/Readium LCP.md b/Documentation/Guides/Readium LCP.md index 5c3a5a4cb..d36ca9d8e 100644 --- a/Documentation/Guides/Readium LCP.md +++ b/Documentation/Guides/Readium LCP.md @@ -254,7 +254,7 @@ streamer.open( ) ``` -The `allowUserInteraction` and `sender` arguments are forwarded to the `LCPAuthenticating` implementation when the passphrase unknown. `LCPDialogAuthentication` shows a pop-up only if `allowUserInteraction` is `true`, using the `sender` as the pop-up's host `UIViewController`. +The `allowUserInteraction` and `sender` arguments are forwarded to the `LCPAuthenticating` implementation when the passphrase is unknown. `LCPDialogAuthentication` shows a pop-up only if `allowUserInteraction` is `true`, using the `sender` as the pop-up's host `UIViewController`. When importing the publication to the bookshelf, set `allowUserInteraction` to `false` as you don't need the passphrase for accessing the publication metadata and cover. If you intend to present the publication using a Navigator, set `allowUserInteraction` to `true` as decryption will be required. @@ -269,16 +269,13 @@ However, if you want to display the publication with a Navigator, verify it is n ```swift if publication.isRestricted { if let error = publication.protectionError as? LCPError { - // The user is not allowed to open the publication. - // You should display the error. + // The user is not allowed to open the publication. You should display the error. } else { // We don't have the user passphrase. - // You may use `publication` to access its - // metadata, but not to render its content. + // You may use `publication` to access its metadata, but not to render its content. } } else { - // The publication is not restricted, you may - // render it with a Navigator component. + // The publication is not restricted, you may render it with a Navigator component. } ``` @@ -368,8 +365,6 @@ lcpLicense.renewLoan( The APIs may fail with an `LCPError`. These errors **must** be displayed to the user with a suitable message. -LCPError implements LocalizedError, enabling you to retrieve a user-friendly message. It's advised to customize the LCP localized strings in your app for translation. These strings can be found at Sources/LCP/Resources/en.lproj/Localizable.strings. - `LCPError` implements `LocalizedError`, enabling you to retrieve a user-friendly message. It's recommended to override the LCP localized strings in your app to translate them. These strings can be found at [Sources/LCP/Resources/en.lproj/Localizable.strings](https://github.com/readium/swift-toolkit/blob/main/Sources/LCP/Resources/en.lproj/Localizable.strings). :warning: In the next major update, `LCPError` will no longer be localized. Applications will need to provide their own localized error messages. If you are adding LCP to a new app, consider treating `LCPError` as non-localized from the start to ease future migration. \ No newline at end of file From 349814decb33b3c42eb52a620d12970735136c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Tue, 9 Apr 2024 13:53:58 +0200 Subject: [PATCH 7/9] Add CONTRIBUTING.md --- CONTRIBUTING.md | 24 ++++++++++++++ Documentation/Guides/Getting Started.md | 40 ++++++++++++++++++++---- Sources/Navigator/EPUB/Scripts/README.md | 9 ------ 3 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..bd2f016ed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to the Readium Swift Toolkit + +First and foremost, thanks for your interest! 🙏 We need contributors like you to help bring this project to fruition. + +We welcome many kind of contributions such as improving the documentation, submitting bug reports and feature requests, or writing code. + +## Writing code + +### Coding standard + +We use [`SwiftFormat`](https://github.com/nicklockwood/SwiftFormat) to ensure code formatting and avoid bikeshedding. + +Before submitting a PR, save yourself some trouble by automatically formatting the code with `make format` from the project's root directory. + +### Modifying the EPUB Navigator's JavaScript layer + +The EPUB navigator injects a set of JavaScript files into a publication's resources, exposing a JavaScript API under the `readium.` global namespace. The scripts are located under [`Sources/Navigator/EPUB/Scripts`](Sources/Navigator/EPUB/Scripts). + +`index-reflowable.js` is the root of the bundle injected in a reflowable EPUB's resources, while `index-fixed.js` is used for a fixed-layout EPUB's resources. + +In the case of fixed-layout EPUBs, the publication resources are actually loaded inside an `iframe` in one of [our HTML wrapper pages](Sources/Navigator/EPUB/Assets/) (`fxl-spread-one.html` for single pages, `fxl-spread-two.html` when displaying two pages side-by-side). The matching `index-fixed-wrapper-one.js` and `index-fixed-wrapper-two.js` are injected in the HTML wrappers. + +If you make any changes to the JavaScript files, you must regenerate the bundles embedded in the application. First, make sure you have [`corepack` installed](https://pnpm.io/installation#using-corepack). Then, run `make scripts` from the project's root directory. + diff --git a/Documentation/Guides/Getting Started.md b/Documentation/Guides/Getting Started.md index feed553d2..6949bc39b 100644 --- a/Documentation/Guides/Getting Started.md +++ b/Documentation/Guides/Getting Started.md @@ -4,7 +4,15 @@ The Readium Swift toolkit enables you to develop reading apps for iOS and iPadOS :warning: Readium offers only low-level tools. You are responsible for creating a user interface for reading and managing books, as well as a data layer to store the user's publications. The Test App is an example of such integration. -The toolkit is divided into separate packages that can be used independently. +## Design principles + +The toolkit has been designed following these core tenets: + +* **Modular**: It is divided into separate modules that can be used independently. +* **Extensible**: Integrators should be able to support a custom DRM, publication format or inject their own stylesheets without modifying the toolkit itself. +* **Opiniated**: We adhere to open standards but sometimes interpret them for practicality. + +## Packages ### Main packages @@ -25,7 +33,9 @@ The toolkit is divided into separate packages that can be used independently. The Readium toolkit provides models used as exchange types between packages. -### Publication +### Publication models + +#### Publication `Publication` and its sub-components represent a single publication – ebook, audiobook or comic. It is loosely based on the [Readium Web Publication Manifest](https://readium.org/webpub-manifest/). @@ -33,9 +43,9 @@ A `Publication` instance: * holds the metadata of a publication, such as its author or table of contents, * allows to read the contents of a publication, e.g. XHTML or audio resources, -* provides additional services, for example content extraction or text search +* provides additional services, for example content extraction or text search. -### Link +#### Link A [`Link` object](https://readium.org/webpub-manifest/#24-the-link-object) holds a pointer (URL) to a `Publication` resource along with additional metadata, such as its media type or title. @@ -45,7 +55,7 @@ The `Publication` contains several `Link` collections, for example: * `resources` contains secondary resources necessary for rendering the `readingOrder`, such as an image or a font file. * `tableOfContents` represents the table of contents as a tree of `Link` objects. -### Locator +#### Locator A [`Locator` object](https://readium.org/architecture/models/locators/) represents a precise location in a publication resource in a format that can be stored and shared across reading systems. It is more accurate than a `Link` and contains additional information about the location, e.g. progression percentage, position or textual context. @@ -55,6 +65,24 @@ A [`Locator` object](https://readium.org/architecture/models/locators/) represen * saving bookmarks, highlights and annotations * navigating search results +### Data models + +#### Publication 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. + +#### Resource + +A `Resource` provides read access to a single resource of a publication, such as a file or an entry in an archive. + +`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. + +#### Fetcher + +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. + +`Publication` objects internally use a `Fetcher` to expose their content. + ## Opening a publication (`R2Streamer`) 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. @@ -103,4 +131,4 @@ let navigator = try EPUBNavigatorViewController( hostViewController.present(navigator, animated: true) ``` -Please refer to the [Navigator guide](Navigator/Navigator.md) for more information. \ No newline at end of file +Please refer to the [Navigator guide](Navigator/Navigator.md) for more information. diff --git a/Sources/Navigator/EPUB/Scripts/README.md b/Sources/Navigator/EPUB/Scripts/README.md index 59a1551d3..fc706061a 100644 --- a/Sources/Navigator/EPUB/Scripts/README.md +++ b/Sources/Navigator/EPUB/Scripts/README.md @@ -1,12 +1,3 @@ # Readium JS (Swift) A set of JavaScript files used by the Swift EPUB navigator. - -## Scripts - -Run `npm install`, then use one of the following: - -* `yarn run bundle` Rebuild the assets after any changes in the `src/` folder. -* `yarn run lint` Check code quality. -* `yarn run checkformat` Check if there's any formatting issues. -* `yarn run format` Automatically format JavaScript sources. From 72203d9b0a78ed93b6894bf056b83eada5829669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Mon, 6 May 2024 16:11:54 +0200 Subject: [PATCH 8/9] Minor changes --- CONTRIBUTING.md | 2 +- Documentation/Guides/Getting Started.md | 4 +++- Documentation/Guides/Readium LCP.md | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd2f016ed..9753e914a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Before submitting a PR, save yourself some trouble by automatically formatting t ### Modifying the EPUB Navigator's JavaScript layer -The EPUB navigator injects a set of JavaScript files into a publication's resources, exposing a JavaScript API under the `readium.` global namespace. The scripts are located under [`Sources/Navigator/EPUB/Scripts`](Sources/Navigator/EPUB/Scripts). +The EPUB navigator injects a set of JavaScript files into a publication's resources, exposing a JavaScript API to the `WKWebView` under the `readium` global namespace. The JavaScript source code is located under [`Sources/Navigator/EPUB/Scripts`](Sources/Navigator/EPUB/Scripts). `index-reflowable.js` is the root of the bundle injected in a reflowable EPUB's resources, while `index-fixed.js` is used for a fixed-layout EPUB's resources. diff --git a/Documentation/Guides/Getting Started.md b/Documentation/Guides/Getting Started.md index 6949bc39b..d94f614a2 100644 --- a/Documentation/Guides/Getting Started.md +++ b/Documentation/Guides/Getting Started.md @@ -47,13 +47,15 @@ A `Publication` instance: #### Link -A [`Link` object](https://readium.org/webpub-manifest/#24-the-link-object) holds a pointer (URL) to a `Publication` resource along with additional metadata, such as its media type or title. + +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: * `readingOrder` lists the publication resources arranged in the order they should be read. * `resources` contains secondary resources necessary for rendering the `readingOrder`, such as an image or a font file. * `tableOfContents` represents the table of contents as a tree of `Link` objects. +* `links` exposes additional resources, such as a canonical link to the manifest or a search web service. #### Locator diff --git a/Documentation/Guides/Readium LCP.md b/Documentation/Guides/Readium LCP.md index d36ca9d8e..01d01b688 100644 --- a/Documentation/Guides/Readium LCP.md +++ b/Documentation/Guides/Readium LCP.md @@ -6,14 +6,14 @@ You can use the Readium Swift toolkit to download and read publications that are ## Overview -An LCP publication is distributed using an LCP License Document (`.lcpl`) protected with a *user passphrase*. +An LCP publication is protected with a *user passphrase* and distributed using an LCP License Document (`.lcpl`) . The user flow typically goes as follows: 1. The user imports a `.lcpl` file into your application. 2. The application uses the Readium toolkit to download the protected publication from the `.lcpl` file to the user's bookshelf. The downloaded file can be a `.epub`, `.lcpdf` (PDF), or `.lcpa` (audiobook) package. 3. The user opens the protected publication from the bookshelf. -4. If the passphrase isn't saved, the user will be asked to enter it to unlock the contents. +4. If the passphrase isn't already recorded in the `ReadiumLCP` internal database, the user will be asked to enter it to unlock the contents. 5. The publication is decrypted and rendered on the screen. ## Setup @@ -367,4 +367,4 @@ The APIs may fail with an `LCPError`. These errors **must** be displayed to the `LCPError` implements `LocalizedError`, enabling you to retrieve a user-friendly message. It's recommended to override the LCP localized strings in your app to translate them. These strings can be found at [Sources/LCP/Resources/en.lproj/Localizable.strings](https://github.com/readium/swift-toolkit/blob/main/Sources/LCP/Resources/en.lproj/Localizable.strings). -:warning: In the next major update, `LCPError` will no longer be localized. Applications will need to provide their own localized error messages. If you are adding LCP to a new app, consider treating `LCPError` as non-localized from the start to ease future migration. \ No newline at end of file +:warning: In the next major update, `LCPError` will no longer be localized. Applications will need to provide their own localized error messages. If you are adding LCP to a new app, consider treating `LCPError` as non-localized from the start to ease future migration. From 7717bfcf90e7a6db306853d2cf6952801052bbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Mon, 6 May 2024 17:27:06 +0200 Subject: [PATCH 9/9] Increase test expectations timeout --- Tests/StreamerTests/Parser/PublicationParsingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/StreamerTests/Parser/PublicationParsingTests.swift b/Tests/StreamerTests/Parser/PublicationParsingTests.swift index f3b02cb4d..c517fd34c 100644 --- a/Tests/StreamerTests/Parser/PublicationParsingTests.swift +++ b/Tests/StreamerTests/Parser/PublicationParsingTests.swift @@ -34,6 +34,6 @@ class PublicationParsingTests: XCTestCase, Loggable { } } - waitForExpectations(timeout: 2, handler: nil) + waitForExpectations(timeout: 30, handler: nil) } }