From 0a41337ed04ba7e4cad84f1185bdda20773ab70b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 15 Apr 2020 12:31:57 +0800 Subject: [PATCH] Try to solve the SwiftUI bug of rendering EXIF UIImage in WebImage. Now we use the Image(decorative:) API to grab the CGImage and orientation instead. For vector image, draw vector image into bitmap for rescue --- .../AppDelegate.swift | 4 - .../AppDelegate.swift | 4 - .../ExtensionDelegate.swift | 7 -- .../SDWebImageSwiftUIDemo/AppDelegate.swift | 4 - .../SDWebImageSwiftUIDemo/ContentView.swift | 1 + SDWebImageSwiftUI.xcodeproj/project.pbxproj | 10 +++ SDWebImageSwiftUI/Classes/Image.swift | 77 +++++++++++++++++++ .../Classes/SDWebImageSwiftUI.swift | 13 ---- SDWebImageSwiftUI/Classes/WebImage.swift | 61 ++++++++++++--- 9 files changed, 138 insertions(+), 43 deletions(-) create mode 100644 SDWebImageSwiftUI/Classes/Image.swift 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..0d7597a 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://dv6mh24acw2xi.cloudfront.net/public/moments/gabbr/tmpImg9143171451882065582.jpeg", "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..be13ba7 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; }; 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..36b432d 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,47 @@ 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 { + var image = image + // Actual rendering SwiftUI image + let result: Image + // NSImage works well with SwiftUI, include Animated and Vector Image + #if os(macOS) + result = Image(nsImage: image) + #else + // Fix the SwiftUI.Image rendering issue when use UIImage based, the `.aspectRatio` does not works. SwiftUI's Bug :) + // See issue #101 + // Case 1: UIAnimatedImage, grab poster image + if image.sd_isAnimated { + // check images property + if let images = image.images, images.count > 0 { + image = images[0] + } + } + // Case 2: Vector Image, draw bitmap image + else if image.sd_isVector { + // ensure CGImage is nil + if image.cgImage == nil { + // draw vector into bitmap with the screen scale (behavior like AppKit) + UIGraphicsBeginImageContextWithOptions(image.size, false, UIScreen.main.scale) + image.draw(at: .zero) + image = UIGraphicsGetImageFromCurrentImageContext() ?? .empty + UIGraphicsEndImageContext() + } + } + // If we have CGImage, use CGImage based API, else use UIImage based API + if let cgImage = image.cgImage { + let orientation = image.imageOrientation.toSwiftUI + result = Image(decorative: cgImage, scale: image.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 +173,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 +297,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) + } } }