Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8a08f08
Add Container and Readable
mickael-menu Jun 4, 2024
d5cea48
Refactor MediaType
mickael-menu Jun 4, 2024
5e0c401
XML tests
mickael-menu Jun 4, 2024
b2a6822
Add `FileExtension`
mickael-menu Jun 4, 2024
209d82c
Add `Format`
mickael-menu Jun 4, 2024
e8892b6
Refactor Resource and Fetcher
mickael-menu Jun 4, 2024
1363da1
More container
mickael-menu Jun 5, 2024
024fdc0
Add Asset and ArchiveOpener
mickael-menu Jun 6, 2024
bd6accd
Add resource factories
mickael-menu Jun 6, 2024
d50363b
Various changes
mickael-menu Jun 7, 2024
1a511a9
Various changes
mickael-menu Jun 7, 2024
61e1beb
Various changes
mickael-menu Jun 7, 2024
fe4ce71
Refactor ReadiumLCP
mickael-menu Jun 8, 2024
e402c82
More LCP
mickael-menu Jun 8, 2024
e90e1d8
More LCP
mickael-menu Jun 8, 2024
eb53be1
UTI fixes
mickael-menu Jun 8, 2024
507262e
`AsyncCloseable` -> `Closeable`
mickael-menu Jun 11, 2024
efe0a82
TTS
mickael-menu Jun 12, 2024
a8ad712
EPUB navigator
mickael-menu Jun 12, 2024
46edc87
Navigator
mickael-menu Jun 17, 2024
9066c40
Remove GCDWebServer from the Streamer
mickael-menu Jun 20, 2024
4392efa
Various changes
mickael-menu Jun 20, 2024
19b1381
Merge branch 'develop' into refactoring-v3
mickael-menu Jun 20, 2024
7489f6a
Streamer fixes
mickael-menu Jun 20, 2024
f505bed
Add the `PublicationOpener`
mickael-menu Jun 21, 2024
085944a
Fix LCP
mickael-menu Jun 21, 2024
ac12947
Shared tests
mickael-menu Jun 21, 2024
daf8d12
Fix Streamer tests
mickael-menu Jun 24, 2024
7def77d
Implement the `AssetRetriever`
mickael-menu Jun 27, 2024
93681d2
FormatSnifferTests
mickael-menu Jun 27, 2024
956c095
More format sniffers
mickael-menu Jun 29, 2024
2b679c9
Comic and audiobook sniffers
mickael-menu Jun 29, 2024
1798312
OPDS format sniffer
mickael-menu Jun 29, 2024
48e95b3
Add language format sniffer
mickael-menu Jun 29, 2024
6874692
Fix tests
mickael-menu Jul 1, 2024
fb3d25d
Force-deprecate the legacy user settings API
mickael-menu Jul 3, 2024
7e29ecc
Adapt the test app to the new asynchronous APIs
mickael-menu Jul 3, 2024
23ed811
Fix EPUB navigator
mickael-menu Jul 7, 2024
18687b1
Address various FIXMEs
mickael-menu Jul 8, 2024
babcd5d
Address more fixmes
mickael-menu Jul 9, 2024
ff547f5
Fix memory leaks
mickael-menu Jul 10, 2024
4107a47
Fix audiobook issue
mickael-menu Jul 10, 2024
60a6ec1
Fix EPUB location
mickael-menu Jul 10, 2024
ce78cb3
Remove unused code
mickael-menu Jul 10, 2024
5b41ea9
Fix tests
mickael-menu Jul 10, 2024
548b29c
Fix asynchronoucity with the TTS
mickael-menu Jul 12, 2024
be843c6
Remove the need for a semaphore in `ResourceResponse`
mickael-menu Jul 12, 2024
77fb9b8
Various fixes
mickael-menu Jul 15, 2024
1846407
Improve test app errors
mickael-menu Jul 15, 2024
9e1d25c
Fix serving files with unknown media types
mickael-menu Jul 30, 2024
451c360
Disable PDF reading progression when scroll mode is enabled
mickael-menu Jul 31, 2024
a043ad8
Fix TTS regression
mickael-menu Aug 1, 2024
1314024
Fix loosing position when rotating twice the device
mickael-menu Aug 2, 2024
93a9f2b
Fix issue with the EPUB loading indicator
mickael-menu Aug 2, 2024
6a5baff
Merge branch 'develop' into refactoring-v3
mickael-menu Aug 19, 2024
4578eb4
Minor fixes
mickael-menu Aug 19, 2024
dfec043
Update to Xcode 15.4
mickael-menu Aug 19, 2024
940c60a
Fix build for Carthage and CocoaPods
mickael-menu Aug 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ env:
platform: ${{ 'iOS Simulator' }}
device: ${{ 'iPhone 15' }}
commit_sha: ${{ github.sha }}
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer

jobs:
build:
name: Build
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
env:
scheme: ${{ 'Readium-Package' }}
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:

lint:
name: Lint
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
env:
scripts: ${{ 'Sources/Navigator/EPUB/Scripts' }}
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:

int-dev:
name: Integration (Local)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand All @@ -98,7 +98,7 @@ jobs:

int-spm:
name: Integration (Swift Package Manager)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:

int-carthage:
name: Integration (Carthage)
runs-on: macos-13
runs-on: macos-14
if: ${{ !github.event.pull_request.draft }}
defaults:
run:
Expand Down Expand Up @@ -157,7 +157,7 @@ jobs:
int-cocoapods:
name: Integration (CocoaPods)
if: github.event_name == 'push'
runs-on: macos-13
runs-on: macos-14
defaults:
run:
working-directory: TestApp
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ All notable changes to this project will be documented in this file. Take a look

#### Shared

* A new `Format` type was introduced to augment `MediaType` with more precise information about the format specifications of an `Asset`.
* `Fetcher` was replaced with a simpler `Container` type.
* `PublicationAsset` was replaced by `Asset`, which contains a `Format` and access to the underlying `Container` or `Resource`.
* The `ResourceError` hierarchy was revamped and simplified (see `ReadError`). Now it is your responsibility to provide a localized user message for each error case.
* The `Link` property key for archive-based publication assets (e.g. an EPUB/ZIP) is now `https://readium.org/webpub-manifest/properties#archive` instead of `archive`.
* The API of `HTTPServer` slightly changed to be more future-proof.

#### Streamer

* The `Streamer` object was deprecated in favor of smaller segregated APIs: `AssetRetriever` and `PublicationOpener`.

#### Navigator

* EPUB: The `scroll` preference is now forced to `true` when rendering vertical text (e.g. CJK vertical). [See this discussion for the rationale](https://github.com/readium/swift-toolkit/discussions/370).
Expand Down
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ github "dexman/Minizip" ~> 1.4.0
github "krzyzanowskim/CryptoSwift" ~> 1.8.0
github "ra1028/DifferenceKit" ~> 1.3.0
github "readium/GCDWebServer" ~> 4.0.0
github "scinfu/SwiftSoup" ~> 2.7.0
# There's a regression with 2.7.4 in SwiftSoup, because they used iOS 13 APIs without bumping the deployment target.
github "scinfu/SwiftSoup" == 2.7.1
github "stephencelis/SQLite.swift" ~> 0.15.0
github "weichsel/ZIPFoundation" ~> 0.9.0
5 changes: 2 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let package = Package(
path: "Sources/Shared",
exclude: [
// Support for ZIPFoundation is not yet achieved.
"Toolkit/Archive/ZIPFoundation.swift",
"Toolkit/ZIP/ZIPFoundation.swift",
],
resources: [
.process("Resources"),
Expand All @@ -63,7 +63,6 @@ let package = Package(
dependencies: [
"CryptoSwift",
"Fuzi",
.product(name: "ReadiumGCDWebServer", package: "GCDWebServer"),
"Zip",
"ReadiumShared",
],
Expand Down Expand Up @@ -134,7 +133,7 @@ let package = Package(
]
),
// These tests require a R2LCPClient.framework to run.
// FIXME: Find a solution to run the tests with GitHub action.
// TODO: Find a solution to run the tests with GitHub action.
// .testTarget(
// name: "ReadiumLCPTests",
// dependencies: ["ReadiumLCP"],
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ This toolkit is a modular project, which follows the [Readium Architecture](http

<!-- https://swiftversion.net/ -->

| Readium | iOS | Swift compiler | Xcode |
|-----------|------|----------------|--------|
| `develop` | 13.0 | 5.9 | 15.0.1 |
| 3.0.0 | 13.0 | 5.9 | 15.0.1 |
| 2.5.1 | 11.0 | 5.6.1 | 13.4 |
| 2.5.0 | 10.0 | 5.6.1 | 13.4 |
| 2.4.0 | 10.0 | 5.3.2 | 12.4 |
| Readium | iOS | Swift compiler | Xcode |
|-----------|------|----------------|-------|
| `develop` | 13.0 | 5.10 | 15.4 |
| 3.0.0 | 13.0 | 5.10 | 15.4 |
| 2.5.1 | 11.0 | 5.6.1 | 13.4 |
| 2.5.0 | 10.0 | 5.6.1 | 13.4 |
| 2.4.0 | 10.0 | 5.3.2 | 12.4 |

## Using Readium

Expand Down
141 changes: 94 additions & 47 deletions Sources/Adapters/GCDWebServer/GCDHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import Foundation
import ReadiumGCDWebServer
import ReadiumInternal
import ReadiumShared
import UIKit

Expand All @@ -19,7 +20,8 @@ public enum GCDHTTPServerError: Error {
/// Implementation of `HTTPServer` using ReadiumGCDWebServer under the hood.
public class GCDHTTPServer: HTTPServer, Loggable {
/// Shared instance of the HTTP server.
public static let shared = GCDHTTPServer()
@available(*, unavailable, message: "Create your own shared instance")
public static var shared: GCDHTTPServer { fatalError() }

/// The actual underlying HTTP server instance.
private let server = ReadiumGCDWebServer()
Expand All @@ -30,6 +32,8 @@ public class GCDHTTPServer: HTTPServer, Loggable {
/// Mapping between endpoints and resource transformers.
private var transformers: [HTTPURL: [ResourceTransformer]] = [:]

private let assetRetriever: AssetRetriever

private enum State {
case stopped
case started(port: UInt, baseURL: HTTPURL)
Expand All @@ -47,7 +51,12 @@ public class GCDHTTPServer: HTTPServer, Loggable {
/// Creates a new instance of the HTTP server.
///
/// - Parameter logLevel: See `ReadiumGCDWebServer.setLogLevel`.
public init(logLevel: Int = 3) {
public init(
assetRetriever: AssetRetriever,
logLevel: Int = 3
) {
self.assetRetriever = assetRetriever

ReadiumGCDWebServer.setLogLevel(Int32(logLevel))

NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
Expand Down Expand Up @@ -85,31 +94,40 @@ public class GCDHTTPServer: HTTPServer, Loggable {
}

private func handle(request: ReadiumGCDWebServerRequest, completion: @escaping ReadiumGCDWebServerCompletionBlock) {
responseResource(for: request) { httpServerRequest, resource, failureHandler in
let response: ReadiumGCDWebServerResponse
switch resource.length {
case let .success(length):
response = ResourceResponse(
resource: resource,
length: length,
range: request.hasByteRange() ? request.byteRange : nil
)
case let .failure(error):
self.log(.error, error)
failureHandler?(httpServerRequest, error)
response = ReadiumGCDWebServerErrorResponse(
statusCode: error.httpStatusCode,
error: error
)
}
responseResource(for: request) { httpServerRequest, httpServerResponse, failureHandler in
Task {
let response: ReadiumGCDWebServerResponse
let resource = httpServerResponse.resource

func fail(_ error: ReadError) -> ReadiumGCDWebServerResponse {
self.log(.error, error)
failureHandler?(httpServerRequest, error)
return ReadiumGCDWebServerErrorResponse(
statusCode: 500,
error: error
)
}

switch await resource.length() {
case let .success(length):
response = await ResourceResponse(
resource: httpServerResponse.resource,
length: length,
range: request.hasByteRange() ? request.byteRange : nil,
mediaType: httpServerResponse.mediaType(using: self.assetRetriever)
)
case let .failure(error):
response = fail(error)
}

completion(response) // goes back to ReadiumGCDWebServerConnection.m
completion(response) // goes back to ReadiumGCDWebServerConnection.m
}
}
}

private func responseResource(
for request: ReadiumGCDWebServerRequest,
completion: @escaping (HTTPServerRequest, Resource, HTTPRequestHandler.OnFailure?) -> Void
completion: @escaping (HTTPServerRequest, HTTPServerResponse, HTTPRequestHandler.OnFailure?) -> Void
) {
let completion = { request, resource, failureHandler in
// Escape the queue to avoid deadlocks if something is using the
Expand All @@ -124,52 +142,40 @@ public class GCDHTTPServer: HTTPServer, Loggable {
fatalError("Expected an HTTP URL")
}

func transform(resource: Resource, at endpoint: HTTPURL) -> Resource {
func transform(resource: Resource, request: HTTPServerRequest, at endpoint: HTTPURL) -> Resource {
guard let transformers = transformers[endpoint], !transformers.isEmpty else {
return resource
}
let href = request.href?.anyURL ?? request.url.anyURL
var resource = resource
for transformer in transformers {
resource = transformer(resource)
resource = transformer(href, resource)
}
return resource
}

let pathWithoutAnchor = url.removingQuery().removingFragment()

for (endpoint, handler) in handlers {
let request: HTTPServerRequest
if endpoint.isEquivalentTo(pathWithoutAnchor) {
let request = HTTPServerRequest(url: url, href: nil)
let resource = handler.onRequest(request)
completion(
request,
transform(resource: resource, at: endpoint),
handler.onFailure
)
return

request = HTTPServerRequest(url: url, href: nil)
} else if let href = endpoint.relativize(url) {
let request = HTTPServerRequest(
url: url,
href: href
)
let resource = handler.onRequest(request)
completion(
request,
transform(resource: resource, at: endpoint),
handler.onFailure
)
return
request = HTTPServerRequest(url: url, href: href)
} else {
continue
}

var response = handler.onRequest(request)
response.resource = transform(resource: response.resource, request: request, at: endpoint)
completion(request, response, handler.onFailure)
return
}

log(.warning, "Resource not found for request \(request)")
completion(
HTTPServerRequest(url: url, href: nil),
FailureResource(
link: Link(href: request.url.absoluteString),
error: .notFound(nil)
),
HTTPServerResponse(error: .notFound),
nil
)
}
Expand Down Expand Up @@ -320,3 +326,44 @@ public class GCDHTTPServer: HTTPServer, Loggable {
return true
}
}

private extension Resource {
func length() async -> ReadResult<UInt64> {
await estimatedLength()
.flatMap { length in
if let length = length {
return .success(length)
} else {
return await read().map { UInt64($0.count) }
}
}
}
}

private extension HTTPServerResponse {
func mediaType(using assetRetriever: AssetRetriever) async -> MediaType {
if let mediaType = mediaType {
return mediaType
}

if let properties = try? await resource.properties().get() {
if let mediaType = properties.mediaType {
return mediaType
}
if
let filename = properties.filename,
let uti = UTI.findFrom(mediaTypes: [], fileExtensions: [URL(fileURLWithPath: filename).pathExtension]),
let type = uti.preferredTag(withClass: .mediaType),
let mediaType = MediaType(type)
{
return mediaType
}
}

if let mediaType = try? await assetRetriever.sniffFormat(of: resource).get().mediaType {
return mediaType
}

return .binary
}
}
Loading