forked from readium/swift-toolkit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathStreamer.swift
199 lines (178 loc) · 9.98 KB
/
Streamer.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//
// Streamer.swift
// r2-streamer-swift
//
// Created by Mickaël Menu on 14/07/2020.
//
// Copyright 2020 Readium Foundation. All rights reserved.
// Use of this source code is governed by a BSD-style license which is detailed
// in the LICENSE file present in the project repository where this source code is maintained.
//
import Foundation
import R2Shared
/// Opens a `Publication` using a list of parsers.
public final class Streamer: Loggable {
/// Creates the default parsers provided by Readium.
public static func makeDefaultParsers(
pdfFactory: PDFDocumentFactory = DefaultPDFDocumentFactory(),
httpClient: HTTPClient = DefaultHTTPClient()
) -> [PublicationParser] {
[
EPUBParser(),
PDFParser(pdfFactory: pdfFactory),
ReadiumWebPubParser(pdfFactory: pdfFactory, httpClient: httpClient),
ImageParser(),
AudioParser()
]
}
/// `Streamer` is configured to use Readium's default parsers, which you can bypass using
/// `ignoreDefaultParsers`. However, you can provide additional `parsers` which will take
/// precedence over the default ones. This can also be used to provide an alternative
/// configuration of a default parser.
///
/// - Parameters:
/// - parsers: Parsers used to open a publication, in addition to the default parsers.
/// - ignoreDefaultParsers: When true, only parsers provided in parsers will be used.
/// - contentProtections: List of `ContentProtection` used to unlock publications. Each
/// `ContentProtection` is tested in the given order.
/// - archiveFactory: Opens an archive (e.g. ZIP, RAR), optionally protected by credentials.
/// - pdfFactory: Parses a PDF document, optionally protected by password.
/// - httpClient: Service performing HTTP requests.
/// - onCreatePublication: Transformation which will be applied on every parsed Publication
/// Builder. It can be used to modify the `Manifest`, the root `Fetcher` or the list of
/// service factories of a `Publication`.
public init(
parsers: [PublicationParser] = [],
ignoreDefaultParsers: Bool = false,
contentProtections: [ContentProtection] = [],
archiveFactory: ArchiveFactory = DefaultArchiveFactory(),
pdfFactory: PDFDocumentFactory = DefaultPDFDocumentFactory(),
httpClient: HTTPClient = DefaultHTTPClient(),
onCreatePublication: Publication.Builder.Transform? = nil
) {
self.parsers = parsers + (ignoreDefaultParsers ? [] : Streamer.makeDefaultParsers(pdfFactory: pdfFactory, httpClient: httpClient))
self.contentProtections = contentProtections
self.archiveFactory = archiveFactory
self.onCreatePublication = onCreatePublication
}
private let parsers: [PublicationParser]
private let contentProtections: [ContentProtection]
private let archiveFactory: ArchiveFactory
private let onCreatePublication: Publication.Builder.Transform?
/// Parses a `Publication` from the given asset.
///
/// If you are opening the publication to render it in a Navigator, you must set
/// `allowUserInteraction`to true to prompt the user for its credentials when the publication is
/// protected. However, set it to false if you just want to import the `Publication` without
/// reading its content, to avoid prompting the user.
///
/// When using Content Protections, you can use `sender` to provide a free object which can be
/// used to give some context. For example, it could be the source `UIViewController` which
/// would be used to present a credentials dialog.
///
/// The `warnings` logger can be used to observe non-fatal parsing warnings, caused by
/// publication authoring mistakes. This can be useful to warn users of potential rendering
/// issues.
///
/// - Parameters:
/// - asset: Digital medium (e.g. a file) used to access the publication.
/// - credentials: Credentials that Content Protections can use to attempt to unlock a
/// publication, for example a password.
/// - allowUserInteraction: Indicates whether the user can be prompted during opening, for
/// example to ask their credentials.
/// - sender: Free object that can be used by reading apps to give some UX context when
/// presenting dialogs.
/// - onCreatePublication: Transformation which will be applied on the Publication Builder.
/// It can be used to modify the `Manifest`, the root `Fetcher` or the list of service
/// factories of the `Publication`.
/// - warnings: Logger used to broadcast non-fatal parsing warnings.
public func open(
asset: PublicationAsset,
credentials: String? = nil,
allowUserInteraction: Bool,
sender: Any? = nil,
warnings: WarningLogger? = nil,
onCreatePublication: Publication.Builder.Transform? = nil,
completion: @escaping (CancellableResult<Publication, Publication.OpeningError>) -> Void
) {
log(.info, "Open \(asset)")
return makeFetcher(for: asset, allowUserInteraction: allowUserInteraction, credentials: credentials, sender: sender)
.flatMap { fetcher in
// Unlocks any protected asset with the Content Protections.
self.unlockAsset(asset, with: fetcher, credentials: credentials, allowUserInteraction: allowUserInteraction, sender: sender)
}
.flatMap { asset in
// Parses the Publication using the parsers.
self.parsePublication(from: asset, warnings: warnings, onCreatePublication: onCreatePublication)
}
.resolve(on: .main, completion)
}
/// Creates the leaf fetcher which will be passed to the content protections and parsers.
private func makeFetcher(for asset: PublicationAsset, allowUserInteraction: Bool, credentials: String?, sender: Any?) -> Deferred<Fetcher, Publication.OpeningError> {
deferred { completion in
asset.makeFetcher(using: .init(archiveFactory: self.archiveFactory), credentials: credentials, completion: completion)
}
}
/// Unlocks any protected asset with the provided Content Protections.
private func unlockAsset(_ asset: PublicationAsset, with fetcher: Fetcher, credentials: String?, allowUserInteraction: Bool, sender: Any?) -> Deferred<OpenedAsset, Publication.OpeningError> {
func unlock(using protections: [ContentProtection]) -> Deferred<ProtectedAsset?, Publication.OpeningError> {
return deferred {
var protections = protections
guard let protection = protections.popFirst() else {
// No Content Protection applied, this asset is probably not protected.
return .success(nil)
}
return protection
.open(asset: asset, fetcher: fetcher, credentials: credentials, allowUserInteraction: allowUserInteraction, sender: sender)
.flatMap {
if let protectedAsset = $0 {
return .success(protectedAsset)
} else {
return unlock(using: protections)
}
}
}
}
return unlock(using: contentProtections)
.map { protectedAsset in
protectedAsset ?? OpenedAsset(asset, fetcher, nil)
}
}
/// Parses the `Publication` from the provided asset and the `parsers`.
private func parsePublication(from openedAsset: OpenedAsset, warnings: WarningLogger?, onCreatePublication: Publication.Builder.Transform?) -> Deferred<Publication, Publication.OpeningError> {
return deferred(on: .global(qos: .userInitiated)) {
var parsers = self.parsers
var parsedBuilder: Publication.Builder?
while parsedBuilder == nil, let parser = parsers.popFirst() {
do {
parsedBuilder = try parser.parse(asset: openedAsset.asset, fetcher: openedAsset.fetcher, warnings: warnings)
} catch {
return .failure(.parsingFailed(error))
}
}
guard var builder = parsedBuilder else {
return .failure(.unsupportedFormat)
}
// Transform from the Content Protection.
builder.apply(openedAsset.onCreatePublication)
// Transform provided by the reading app during the construction of the `Streamer`.
builder.apply(self.onCreatePublication)
// Transform provided by the reading app in `Streamer.open()`.
if let onCreatePublication = onCreatePublication {
builder.apply(onCreatePublication)
}
return .success(builder.build())
}
}
@available(*, unavailable, message: "Provide a `FileAsset` instead", renamed: "open(asset:credentials:allowUserInteraction:sender:warnings:onCreatePublication:completion:)")
public func open(file: File, credentials: String? = nil, allowUserInteraction: Bool, sender: Any? = nil, warnings: WarningLogger? = nil, onCreatePublication: Publication.Builder.Transform? = nil, completion: @escaping (CancellableResult<Publication, Publication.OpeningError>) -> Void) {}
}
private typealias OpenedAsset = (asset: PublicationAsset, fetcher: Fetcher, onCreatePublication: Publication.Builder.Transform?)
private extension ContentProtection {
/// Wrapper to use `Deferred` with `ContentProtection.open()`.
func open(asset: PublicationAsset, fetcher: Fetcher, credentials: String?, allowUserInteraction: Bool, sender: Any?) -> Deferred<ProtectedAsset?, Publication.OpeningError> {
return deferred { completion in
self.open(asset: asset, fetcher: fetcher, credentials: credentials, allowUserInteraction: allowUserInteraction, sender: sender, completion: completion)
}
}
}