From 3ed8dd8ab3da7fc650d140bc305638896379fa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Wed, 21 Jan 2026 15:29:49 +0100 Subject: [PATCH 1/2] Add support for JXL --- .github/workflows/checks.yml | 2 +- CHANGELOG.md | 4 ++++ Sources/Shared/Toolkit/Format/Format.swift | 1 + Sources/Shared/Toolkit/Format/MediaType.swift | 3 ++- .../Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift | 3 +++ .../Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift | 1 + Sources/Streamer/Parser/Image/ImageParser.swift | 1 + Tests/SharedTests/Toolkit/Format/FormatSniffersTests.swift | 5 +++++ Tests/SharedTests/Toolkit/Format/MediaTypeTests.swift | 3 ++- 9 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 031cd4990..95702d962 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,7 +7,7 @@ on: env: platform: ${{ 'iOS Simulator' }} - device: ${{ 'iPhone 17' }} + device: ${{ 'iPad (A16)' }} commit_sha: ${{ github.sha }} DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c2dc68b..3ad15fcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. Take a look ### Added +#### Shared + +* Added support for JXL (JPEG XL) bitmap images. JXL is decoded natively on iOS 17+. + #### Navigator * Support for displaying Divina (image-based publications like CBZ) in the fixed-layout EPUB navigator. diff --git a/Sources/Shared/Toolkit/Format/Format.swift b/Sources/Shared/Toolkit/Format/Format.swift index 2594abdf5..4f395e703 100644 --- a/Sources/Shared/Toolkit/Format/Format.swift +++ b/Sources/Shared/Toolkit/Format/Format.swift @@ -164,6 +164,7 @@ public struct FormatSpecification: RawRepresentable, Hashable { public static let bmp = FormatSpecification(rawValue: "bmp") public static let gif = FormatSpecification(rawValue: "gif") public static let jpeg = FormatSpecification(rawValue: "jpeg") + public static let jxl = FormatSpecification(rawValue: "jxl") public static let png = FormatSpecification(rawValue: "png") public static let tiff = FormatSpecification(rawValue: "tiff") public static let webp = FormatSpecification(rawValue: "webp") diff --git a/Sources/Shared/Toolkit/Format/MediaType.swift b/Sources/Shared/Toolkit/Format/MediaType.swift index 7441861b5..45212bb79 100644 --- a/Sources/Shared/Toolkit/Format/MediaType.swift +++ b/Sources/Shared/Toolkit/Format/MediaType.swift @@ -188,7 +188,7 @@ public struct MediaType: Hashable, Loggable, Sendable { /// Returns whether this media type is of a bitmap image, so excluding vectorial formats. public var isBitmap: Bool { - matchesAny(.bmp, .gif, .jpeg, .png, .tiff, .webp) + matchesAny(.bmp, .gif, .jpeg, .jxl, .png, .tiff, .webp) } /// Returns whether this media type is of an audio clip. @@ -228,6 +228,7 @@ public struct MediaType: Hashable, Loggable, Sendable { public static let javascript = MediaType("text/javascript")! public static let jpeg = MediaType("image/jpeg")! public static let json = MediaType("application/json")! + public static let jxl = MediaType("image/jxl")! public static let lcpLicenseDocument = MediaType("application/vnd.readium.lcp.license.v1.0+json")! public static let lcpProtectedAudiobook = MediaType("application/audiobook+lcp")! public static let lcpProtectedPDF = MediaType("application/pdf+lcp")! diff --git a/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift index 310e5eb66..165e44a59 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/BitmapFormatSniffer.swift @@ -23,6 +23,9 @@ public class BitmapFormatSniffer: FormatSniffer { if hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || hints.hasMediaType("image/jpeg") { return Format(specifications: .jpeg, mediaType: .jpeg, fileExtension: "jpg") } + if hints.hasFileExtension("jxl") || hints.hasMediaType("image/jxl") { + return Format(specifications: .jxl, mediaType: .jxl, fileExtension: "jxl") + } if hints.hasFileExtension("png") || hints.hasMediaType("image/png") { return Format(specifications: .png, mediaType: .png, fileExtension: "png") } diff --git a/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift b/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift index bee791a34..1972d4972 100644 --- a/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift +++ b/Sources/Shared/Toolkit/Format/Sniffers/ComicFormatSniffer.swift @@ -25,6 +25,7 @@ public struct ComicFormatSniffer: FormatSniffer { "jfif", "jpg", "jpeg", + "jxl", "png", "tif", "tiff", diff --git a/Sources/Streamer/Parser/Image/ImageParser.swift b/Sources/Streamer/Parser/Image/ImageParser.swift index 8d55922cf..eea46d692 100644 --- a/Sources/Streamer/Parser/Image/ImageParser.swift +++ b/Sources/Streamer/Parser/Image/ImageParser.swift @@ -25,6 +25,7 @@ public final class ImageParser: PublicationParser { .bmp, .gif, .jpeg, + .jxl, .png, .tiff, .webp, diff --git a/Tests/SharedTests/Toolkit/Format/FormatSniffersTests.swift b/Tests/SharedTests/Toolkit/Format/FormatSniffersTests.swift index 977003d16..c7e541d9a 100644 --- a/Tests/SharedTests/Toolkit/Format/FormatSniffersTests.swift +++ b/Tests/SharedTests/Toolkit/Format/FormatSniffersTests.swift @@ -157,6 +157,11 @@ class FormatSniffersTests: XCTestCase { XCTAssertEqual(sut.sniffHints(fileExtension: "jfi"), jpeg) XCTAssertEqual(sut.sniffHints(mediaType: "image/jpeg"), jpeg) + // JXL + let jxl = Format(specifications: .jxl, mediaType: .jxl, fileExtension: "jxl") + XCTAssertEqual(sut.sniffHints(fileExtension: "jxl"), jxl) + XCTAssertEqual(sut.sniffHints(mediaType: "image/jxl"), jxl) + // PNG let png = Format(specifications: .png, mediaType: .png, fileExtension: "png") XCTAssertEqual(sut.sniffHints(fileExtension: "png"), png) diff --git a/Tests/SharedTests/Toolkit/Format/MediaTypeTests.swift b/Tests/SharedTests/Toolkit/Format/MediaTypeTests.swift index 614e70ee7..b655fa25e 100644 --- a/Tests/SharedTests/Toolkit/Format/MediaTypeTests.swift +++ b/Tests/SharedTests/Toolkit/Format/MediaTypeTests.swift @@ -300,9 +300,10 @@ class MediaTypeTests: XCTestCase { XCTAssertTrue(MediaType("image/bmp")!.isBitmap) XCTAssertTrue(MediaType("image/gif")!.isBitmap) XCTAssertTrue(MediaType("image/jpeg")!.isBitmap) + XCTAssertTrue(MediaType("image/jxl")!.isBitmap) XCTAssertTrue(MediaType("image/png")!.isBitmap) XCTAssertTrue(MediaType("image/tiff")!.isBitmap) - XCTAssertTrue(MediaType("image/tiff")!.isBitmap) + XCTAssertTrue(MediaType("image/webp")!.isBitmap) XCTAssertTrue(MediaType("image/tiff;charset=utf-8")!.isBitmap) } From cf7a15960b335a7d61e54a59771c52ed87c9bcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Wed, 21 Jan 2026 15:52:50 +0100 Subject: [PATCH 2/2] Fix workflow --- .github/workflows/checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 95702d962..71ec1b139 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,7 +7,7 @@ on: env: platform: ${{ 'iOS Simulator' }} - device: ${{ 'iPad (A16)' }} + device: ${{ 'iPhone 16 Pro' }} commit_sha: ${{ github.sha }} DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer @@ -33,6 +33,7 @@ jobs: git diff --exit-code Support/Carthage/Readium.xcodeproj - name: Build run: | + xcrun simctl list set -eo pipefail xcodebuild build-for-testing -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi - name: Test @@ -55,6 +56,7 @@ jobs: run: | set -eo pipefail make navigator-ui-tests-project + xcrun simctl list xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi lint: