You can use the Readium Swift toolkit to download and read publications that are protected with the Readium LCP DRM.
Important
To use LCP with the Readium toolkit, you must first obtain the R2LCPClient
private library by contacting EDRLab.
An LCP publication is protected with a user passphrase and distributed using an LCP License Document (.lcpl
) .
The user flow typically goes as follows:
- The user imports a
.lcpl
file into your application. - 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. - The user opens the protected publication from the bookshelf.
- If the passphrase isn't already recorded in the
ReadiumLCP
internal database, the user will be asked to enter it to unlock the contents. - The publication is decrypted and rendered on the screen.
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 asR2Navigator
. - The private
R2LCPClient
library customized for your application is available from EDRLab. They will provide instructions for integrating theR2LCPClient
framework into your application.
Readium LCP specifies new file formats.
Name | File extension | Media type |
---|---|---|
License Document | .lcpl |
application/vnd.readium.lcp.license.v1.0+json |
LCP for PDF package | .lcpdf |
application/pdf+lcp |
LCP for Audiobooks package | .lcpa |
application/audiobook+lcp |
Note
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
as imported types.
<dict>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>org.readium.lcpl</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.content</string>
<string>public.data</string>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>LCP License Document</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>lcpl</string>
</array>
<key>public.mime-type</key>
<string>application/vnd.readium.lcp.license.v1.0+json</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>org.readium.lcpdf</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.content</string>
<string>public.data</string>
<string>public.archive</string>
<string>public.zip-archive</string>
</array>
<key>UTTypeDescription</key>
<string>LCP for PDF package</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>lcpdf</string>
</array>
<key>public.mime-type</key>
<string>application/pdf+lcp</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>org.readium.lcpa</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.content</string>
<string>public.data</string>
<string>public.archive</string>
<string>public.zip-archive</string>
</array>
<key>UTTypeDescription</key>
<string>LCP for Audiobooks package</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>lcpa</string>
</array>
<key>public.mime-type</key>
<string>application/audiobook+lcp</string>
</dict>
</dict>
</array>
</dict>
Next, declare the imported types as Document Types in the Info.plist
to have your application listed in the "Open with..." dialogs.
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>LCP License Document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>org.readium.lcpl</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>LCP for PDF package</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>org.readium.lcpdf</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>LCP for Audiobooks package</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>org.readium.lcpa</string>
</array>
</dict>
</array>
</dict>
Tip
If EPUB is not included in your document types, now is a good time to add it.
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
.
import R2LCPClient
import ReadiumLCP
let lcpService = LCPService(client: LCPClientAdapter())
/// Facade to the private R2LCPClient.framework.
class LCPClientAdapter: ReadiumLCP.LCPClient {
func createContext(jsonLicense: String, hashedPassphrase: LCPPassphraseHash, 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: [LCPPassphraseHash]) -> LCPPassphraseHash? {
R2LCPClient.findOneValidPassphrase(jsonLicense: jsonLicense, hashedPassphrases: hashedPassphrases)
}
}
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.
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.
A publication protected with LCP can be opened using the PublicationOpener
component, just like a non-protected publication. However, you must provide a ContentProtection
implementation when initializing the PublicationOpener
to enable LCP. Luckily, LCPService
has you covered.
let httpClient = DefaultHTTPClient()
let authentication = LCPDialogAuthentication()
let publicationOpener = PublicationOpener(
parser: DefaultPublicationParser(
httpClient: httpClient,
assetRetriever: AssetRetriever(httpClient: httpClient),
pdfFactory: DefaultPDFDocumentFactory()
),
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.
Note
The user will be prompted once per passphrase since ReadiumLCP
stores known passphrases on the device.
You are now ready to open the publication file with your PublicationOpener
instance.
// Retrieve an `Asset` to access the file content.
let url = FileURL(path: "/path/to/lcp-protected-book.epub", isDirectory: false)
let asset = try await assetRetriever.retrieve(url: url).get()
// Open a `Publication` from the `Asset`.
let result = await publicationOpener.open(
asset: asset,
allowUserInteraction: true,
sender: hostViewController
)
switch result {
case .success(let publication):
// Import or present the publication.
case .failure(let error):
// Present the error.
}
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.
Tip
To check if a publication is protected with LCP before opening it, you can use LCPService.isLCPProtected()
.
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.).
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.
}
If the server hosting the LCP protected package supports the HTTP HEAD
method and HTTP Range requests, it is possible to stream directly an LCP protected publication from a License Document (.lcpl
) file, without downloading the whole publication first.
Simply open the License Document directly using the PublicationOpener
. Make sure you provide an HTTPClient
(or an HTTPResourceFactory
for additional customization) to the AssetRetriever
.
// Instantiate the required components.
let httpClient = DefaultHTTPClient()
let assetRetriever = AssetRetriever(httpClient: httpClient)
let publicationOpener = PublicationOpener(
parser: DefaultPublicationParser(
httpClient: httpClient,
assetRetriever: assetRetriever
),
contentProtections: [
lcpService.contentProtection(with: LCPDialogAuthentication()),
]
)
// Retrieve an `Asset` to access the LCPL content.
let url = FileURL(path: "/path/to/license.lcpl", isDirectory: false)
let asset = try await assetRetriever.retrieve(url: url).get()
// Open a `Publication` from the LCPL `Asset`.
let publication = try await publicationOpener.open(
asset: asset,
allowUserInteraction: true,
sender: hostViewController
).get()
print("Opened \(publication.metadata.title)")
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.
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
.
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
.
Some loans can be returned before the end date. You can confirm this by using lcpLicense.canReturnPublication
. To return the publication, execute:
lcpLicense.returnPublication { error in
if let error = error {
// Present the error.
} else {
// The publication was returned.
}
}
The loan end date may also be extended. You can confirm this by using lcpLicense.canRenewLoan
.
Readium LCP supports two types of renewal interactions:
- 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
.
lcpLicense.renewLoan(
with: LCPDefaultRenewDelegate(
presentingViewController: hostViewController
)
) { result in
switch result {
case .success, .cancelled:
// The publication was renewed.
case let .failure(error):
// Display the error.
}
}
The APIs may fail with an LCPError
. These errors must be displayed to the user with a suitable message.
For an example, take a look at the Test App.