diff --git a/Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift b/Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift index 586eff2..4f4aa5d 100644 --- a/Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift +++ b/Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift @@ -39,13 +39,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Dynamic check to support vector format for both WebImage/AnimatedImage SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in var options = options - var context = context if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type { // AnimatedImage supports vector rendering, should not force decode options.insert(.avoidDecodeImage) - } else { - // WebImage supports bitmap rendering only - context?[.imageThumbnailPixelSize] = CGSize.zero } return SDWebImageOptionsResult(options: options, context: context) } diff --git a/Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift b/Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift index 14620e7..3def1a3 100644 --- a/Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift +++ b/Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift @@ -47,13 +47,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Dynamic check to support vector format for both WebImage/AnimatedImage SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in var options = options - var context = context if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type { // AnimatedImage supports vector rendering, should not force decode options.insert(.avoidDecodeImage) - } else { - // WebImage supports bitmap rendering only - context?[.imageThumbnailPixelSize] = CGSize.zero } return SDWebImageOptionsResult(options: options, context: context) } diff --git a/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift b/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift index 951ad88..efdaa49 100644 --- a/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift +++ b/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift @@ -20,13 +20,6 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared) - // Dynamic check to support vector format for WebImage - SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in - var context = context - // WebImage supports bitmap rendering only - context?[.imageThumbnailPixelSize] = CGSize.zero - return SDWebImageOptionsResult(options: options, context: context) - } } func applicationDidBecomeActive() { diff --git a/Example/SDWebImageSwiftUIDemo/AppDelegate.swift b/Example/SDWebImageSwiftUIDemo/AppDelegate.swift index 2cb4e53..4b1f1fc 100644 --- a/Example/SDWebImageSwiftUIDemo/AppDelegate.swift +++ b/Example/SDWebImageSwiftUIDemo/AppDelegate.swift @@ -26,13 +26,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Dynamic check to support vector format for both WebImage/AnimatedImage SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in var options = options - var context = context if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type { // AnimatedImage supports vector rendering, should not force decode options.insert(.avoidDecodeImage) - } else { - // WebImage supports bitmap rendering only - context?[.imageThumbnailPixelSize] = CGSize.zero } return SDWebImageOptionsResult(options: options, context: context) } diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 3aa2062..345d16a 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -81,6 +81,7 @@ struct ContentView: View { "https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png", "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg", "https://via.placeholder.com/200x200.jpg", + "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg", "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/w3c.svg", "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.svg", "https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf", diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index 0d93241..6d4d746 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -75,6 +75,10 @@ 32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; }; 32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; }; 32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; }; + 32D26A022446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; }; + 32D26A032446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; }; + 32D26A042446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; }; + 32D26A052446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; }; 32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; }; 32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; }; 32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; }; @@ -133,6 +137,7 @@ 32C43E2922FD586200BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/tvOS/SDWebImage.framework; sourceTree = ""; }; 32C43E2D22FD586E00BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/watchOS/SDWebImage.framework; sourceTree = ""; }; 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageSwiftUI.swift; sourceTree = ""; }; + 32D26A012446B546005905DA /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 32ED4825242A13030053338E /* ImageManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManagerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -278,6 +283,7 @@ 32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */, 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */, 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */, + 32D26A012446B546005905DA /* Image.swift */, ); path = Classes; sourceTree = ""; @@ -698,6 +704,7 @@ 326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */, + 32D26A022446B546005905DA /* Image.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -714,6 +721,7 @@ 326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */, + 32D26A032446B546005905DA /* Image.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -730,6 +738,7 @@ 326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */, + 32D26A042446B546005905DA /* Image.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -746,6 +755,7 @@ 326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */, + 32D26A052446B546005905DA /* Image.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1369,7 +1379,7 @@ repositoryURL = "https://github.com/nalexn/ViewInspector.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.3.5; + minimumVersion = 0.3.11; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3b83945..03bf90e 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/nalexn/ViewInspector.git", "state": { "branch": null, - "revision": "ec943ed718cd293b95f17a2b81e8917d6ed70752", - "version": "0.3.8" + "revision": "7d55eb940242512aad2bf28db354d09d5de43893", + "version": "0.3.11" } } ] diff --git a/SDWebImageSwiftUI/Classes/Image.swift b/SDWebImageSwiftUI/Classes/Image.swift new file mode 100644 index 0000000..1ed4034 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Image.swift @@ -0,0 +1,77 @@ +/* +* This file is part of the SDWebImage package. +* (c) DreamPiggy +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +import Foundation +import SwiftUI + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension Image { + @inlinable init(platformImage: PlatformImage) { + #if os(macOS) + self.init(nsImage: platformImage) + #else + self.init(uiImage: platformImage) + #endif + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension PlatformImage { + static var empty = PlatformImage() +} + +#if os(iOS) || os(tvOS) || os(watchOS) +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension PlatformImage.Orientation { + @inlinable var toSwiftUI: Image.Orientation { + switch self { + case .up: + return .up + case .upMirrored: + return .upMirrored + case .down: + return .down + case .downMirrored: + return .downMirrored + case .left: + return .left + case .leftMirrored: + return .leftMirrored + case .right: + return .right + case .rightMirrored: + return .rightMirrored + @unknown default: + return .up + } + } +} + +extension Image.Orientation { + @inlinable var toPlatform: PlatformImage.Orientation { + switch self { + case .up: + return .up + case .upMirrored: + return .upMirrored + case .down: + return .down + case .downMirrored: + return .downMirrored + case .left: + return .left + case .leftMirrored: + return .leftMirrored + case .right: + return .right + case .rightMirrored: + return .rightMirrored + } + } +} +#endif diff --git a/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift b/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift index 49cfe8f..9c0e71a 100644 --- a/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift +++ b/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift @@ -18,19 +18,6 @@ public typealias PlatformImage = NSImage public typealias PlatformImage = UIImage #endif -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) -extension Image { - init(platformImage: PlatformImage) { - #if os(macOS) - self.init(nsImage: platformImage) - #else - self.init(uiImage: platformImage) - #endif - } - - static var empty = Image(platformImage: PlatformImage()) -} - #if os(macOS) @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public typealias PlatformView = NSView diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index ace9077..944be51 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -63,13 +63,13 @@ public struct WebImage : View { // this prefetch the memory cache of image, to immediately render it on screen // this solve the case when `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :) if imageManager.isFirstPrefetch { - self.imageManager.prefetch() + imageManager.prefetch() } return Group { if imageManager.image != nil { - if isAnimating && !self.imageManager.isIncremental { + if isAnimating && !imageManager.isIncremental { if currentFrame != nil { - configure(image: Image(platformImage: currentFrame!)) + configure(image: currentFrame!) .onAppear { self.imagePlayer?.startPlaying() } @@ -84,16 +84,16 @@ public struct WebImage : View { } } } else { - configure(image: Image(platformImage: imageManager.image!)) + configure(image: imageManager.image!) .onReceive(imageManager.$image) { image in self.setupPlayer(image: image) } } } else { if currentFrame != nil { - configure(image: Image(platformImage: currentFrame!)) + configure(image: currentFrame!) } else { - configure(image: Image(platformImage: imageManager.image!)) + configure(image: imageManager.image!) } } } else { @@ -122,10 +122,52 @@ public struct WebImage : View { } } - func configure(image: Image) -> some View { + /// Configure the platform image into the SwiftUI rendering image + func configure(image: PlatformImage) -> some View { + // Actual rendering SwiftUI image + let result: Image + // NSImage works well with SwiftUI, include Vector and EXIF images. + #if os(macOS) + result = Image(nsImage: image) + #else + // Fix the SwiftUI.Image rendering issue, like when use EXIF UIImage, the `.aspectRatio` does not works. SwiftUI's Bug :) + // See issue #101 + var cgImage: CGImage? + // Case 1: Vector Image, draw bitmap image + if image.sd_isVector { + // ensure CGImage is nil + if image.cgImage == nil { + // draw vector into bitmap with the screen scale (behavior like AppKit) + #if os(iOS) || os(tvOS) + let scale = UIScreen.main.scale + #else + let scale = WKInterfaceDevice.current().screenScale + #endif + UIGraphicsBeginImageContextWithOptions(image.size, false, scale) + image.draw(at: .zero) + cgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage + UIGraphicsEndImageContext() + } else { + cgImage = image.cgImage + } + } + // Case 2: Image with EXIF orientation (only EXIF 5-8 contains bug) + else if [.left, .leftMirrored, .right, .rightMirrored].contains(image.imageOrientation) { + cgImage = image.cgImage + } + // If we have CGImage, use CGImage based API, else use UIImage based API + if let cgImage = cgImage { + let scale = image.scale + let orientation = image.imageOrientation.toSwiftUI + result = Image(decorative: cgImage, scale: scale, orientation: orientation) + } else { + result = Image(uiImage: image) + } + #endif + // Should not use `EmptyView`, which does not respect to the container's frame modifier // Using a empty image instead for better compatible - configurations.reduce(image) { (previous, configuration) in + return configurations.reduce(result) { (previous, configuration) in configuration(previous) } } @@ -136,12 +178,12 @@ public struct WebImage : View { if let placeholder = placeholder { // If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :) if imageManager.options.contains(.delayPlaceholder) && imageManager.isLoading { - return AnyView(configure(image: Image.empty)) + return AnyView(configure(image: .empty)) } else { return placeholder } } else { - return AnyView(configure(image: Image.empty)) + return AnyView(configure(image: .empty)) } } @@ -260,7 +302,9 @@ extension WebImage { /// - Parameter image: A Image view that describes the placeholder. public func placeholder(_ image: Image) -> WebImage { return placeholder { - configure(image: image) + configurations.reduce(image) { (previous, configuration) in + configuration(previous) + } } } diff --git a/Tests/WebImageTests.swift b/Tests/WebImageTests.swift index 8be68fb..6f17900 100644 --- a/Tests/WebImageTests.swift +++ b/Tests/WebImageTests.swift @@ -133,4 +133,23 @@ class WebImageTests: XCTestCase { ViewHosting.expel() } + func testWebImageEXIFImage() throws { + let expectation = self.expectation(description: "WebImage EXIF image url") + // EXIF 5, Left Mirrored + let imageUrl = URL(string: "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg") + let imageView = WebImage(url: imageUrl) + let introspectView = imageView.onSuccess { image, cacheType in + let displayImage = try? imageView.inspect().group().image(0).cgImage() + let orientation = try! imageView.inspect().group().image(0).orientation() + XCTAssertNotNil(displayImage) + XCTAssertEqual(orientation, .leftMirrored) + expectation.fulfill() + }.onFailure { error in + XCTFail(error.localizedDescription) + } + _ = try introspectView.inspect() + ViewHosting.host(view: introspectView) + self.waitForExpectations(timeout: 5, handler: nil) + ViewHosting.expel() + } }