Skip to content

Commit 10ab2a2

Browse files
authored
Merge pull request #102 from SDWebImage/hack_exif_webimage_rendering_aspect_ratio
Try to solve the SwiftUI bug of rendering EXIF UIImage in WebImage, as well as vector images
2 parents 14a3cce + 3982797 commit 10ab2a2

File tree

11 files changed

+165
-46
lines changed

11 files changed

+165
-46
lines changed

Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift

-4
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3939
// Dynamic check to support vector format for both WebImage/AnimatedImage
4040
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
4141
var options = options
42-
var context = context
4342
if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
4443
// AnimatedImage supports vector rendering, should not force decode
4544
options.insert(.avoidDecodeImage)
46-
} else {
47-
// WebImage supports bitmap rendering only
48-
context?[.imageThumbnailPixelSize] = CGSize.zero
4945
}
5046
return SDWebImageOptionsResult(options: options, context: context)
5147
}

Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift

-4
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4747
// Dynamic check to support vector format for both WebImage/AnimatedImage
4848
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
4949
var options = options
50-
var context = context
5150
if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
5251
// AnimatedImage supports vector rendering, should not force decode
5352
options.insert(.avoidDecodeImage)
54-
} else {
55-
// WebImage supports bitmap rendering only
56-
context?[.imageThumbnailPixelSize] = CGSize.zero
5753
}
5854
return SDWebImageOptionsResult(options: options, context: context)
5955
}

Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift

-7
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
2020
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
2121
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
2222
SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
23-
// Dynamic check to support vector format for WebImage
24-
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
25-
var context = context
26-
// WebImage supports bitmap rendering only
27-
context?[.imageThumbnailPixelSize] = CGSize.zero
28-
return SDWebImageOptionsResult(options: options, context: context)
29-
}
3023
}
3124

3225
func applicationDidBecomeActive() {

Example/SDWebImageSwiftUIDemo/AppDelegate.swift

-4
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2626
// Dynamic check to support vector format for both WebImage/AnimatedImage
2727
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
2828
var options = options
29-
var context = context
3029
if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
3130
// AnimatedImage supports vector rendering, should not force decode
3231
options.insert(.avoidDecodeImage)
33-
} else {
34-
// WebImage supports bitmap rendering only
35-
context?[.imageThumbnailPixelSize] = CGSize.zero
3632
}
3733
return SDWebImageOptionsResult(options: options, context: context)
3834
}

Example/SDWebImageSwiftUIDemo/ContentView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ struct ContentView: View {
8181
"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png",
8282
"https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg",
8383
"https://via.placeholder.com/200x200.jpg",
84+
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg",
8485
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/w3c.svg",
8586
"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.svg",
8687
"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",

SDWebImageSwiftUI.xcodeproj/project.pbxproj

+11-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
7676
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
7777
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */; };
78+
32D26A022446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
79+
32D26A032446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
80+
32D26A042446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
81+
32D26A052446B546005905DA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D26A012446B546005905DA /* Image.swift */; };
7882
32ED4826242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
7983
32ED4827242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
8084
32ED4828242A13030053338E /* ImageManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED4825242A13030053338E /* ImageManagerTests.swift */; };
@@ -133,6 +137,7 @@
133137
32C43E2922FD586200BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/tvOS/SDWebImage.framework; sourceTree = "<group>"; };
134138
32C43E2D22FD586E00BE87F5 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/watchOS/SDWebImage.framework; sourceTree = "<group>"; };
135139
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageSwiftUI.swift; sourceTree = "<group>"; };
140+
32D26A012446B546005905DA /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
136141
32ED4825242A13030053338E /* ImageManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManagerTests.swift; sourceTree = "<group>"; };
137142
/* End PBXFileReference section */
138143

@@ -278,6 +283,7 @@
278283
32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */,
279284
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */,
280285
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */,
286+
32D26A012446B546005905DA /* Image.swift */,
281287
);
282288
path = Classes;
283289
sourceTree = "<group>";
@@ -698,6 +704,7 @@
698704
326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */,
699705
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
700706
32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */,
707+
32D26A022446B546005905DA /* Image.swift in Sources */,
701708
);
702709
runOnlyForDeploymentPostprocessing = 0;
703710
};
@@ -714,6 +721,7 @@
714721
326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */,
715722
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
716723
32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */,
724+
32D26A032446B546005905DA /* Image.swift in Sources */,
717725
);
718726
runOnlyForDeploymentPostprocessing = 0;
719727
};
@@ -730,6 +738,7 @@
730738
326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */,
731739
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
732740
32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */,
741+
32D26A042446B546005905DA /* Image.swift in Sources */,
733742
);
734743
runOnlyForDeploymentPostprocessing = 0;
735744
};
@@ -746,6 +755,7 @@
746755
326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */,
747756
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
748757
32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */,
758+
32D26A052446B546005905DA /* Image.swift in Sources */,
749759
);
750760
runOnlyForDeploymentPostprocessing = 0;
751761
};
@@ -1369,7 +1379,7 @@
13691379
repositoryURL = "https://github.com/nalexn/ViewInspector.git";
13701380
requirement = {
13711381
kind = upToNextMajorVersion;
1372-
minimumVersion = 0.3.5;
1382+
minimumVersion = 0.3.11;
13731383
};
13741384
};
13751385
/* End XCRemoteSwiftPackageReference section */

SDWebImageSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"repositoryURL": "https://github.com/nalexn/ViewInspector.git",
77
"state": {
88
"branch": null,
9-
"revision": "ec943ed718cd293b95f17a2b81e8917d6ed70752",
10-
"version": "0.3.8"
9+
"revision": "7d55eb940242512aad2bf28db354d09d5de43893",
10+
"version": "0.3.11"
1111
}
1212
}
1313
]

SDWebImageSwiftUI/Classes/Image.swift

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* This file is part of the SDWebImage package.
3+
* (c) DreamPiggy <[email protected]>
4+
*
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*/
8+
9+
import Foundation
10+
import SwiftUI
11+
12+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
13+
extension Image {
14+
@inlinable init(platformImage: PlatformImage) {
15+
#if os(macOS)
16+
self.init(nsImage: platformImage)
17+
#else
18+
self.init(uiImage: platformImage)
19+
#endif
20+
}
21+
}
22+
23+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
24+
extension PlatformImage {
25+
static var empty = PlatformImage()
26+
}
27+
28+
#if os(iOS) || os(tvOS) || os(watchOS)
29+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
30+
extension PlatformImage.Orientation {
31+
@inlinable var toSwiftUI: Image.Orientation {
32+
switch self {
33+
case .up:
34+
return .up
35+
case .upMirrored:
36+
return .upMirrored
37+
case .down:
38+
return .down
39+
case .downMirrored:
40+
return .downMirrored
41+
case .left:
42+
return .left
43+
case .leftMirrored:
44+
return .leftMirrored
45+
case .right:
46+
return .right
47+
case .rightMirrored:
48+
return .rightMirrored
49+
@unknown default:
50+
return .up
51+
}
52+
}
53+
}
54+
55+
extension Image.Orientation {
56+
@inlinable var toPlatform: PlatformImage.Orientation {
57+
switch self {
58+
case .up:
59+
return .up
60+
case .upMirrored:
61+
return .upMirrored
62+
case .down:
63+
return .down
64+
case .downMirrored:
65+
return .downMirrored
66+
case .left:
67+
return .left
68+
case .leftMirrored:
69+
return .leftMirrored
70+
case .right:
71+
return .right
72+
case .rightMirrored:
73+
return .rightMirrored
74+
}
75+
}
76+
}
77+
#endif

SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift

-13
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,6 @@ public typealias PlatformImage = NSImage
1818
public typealias PlatformImage = UIImage
1919
#endif
2020

21-
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
22-
extension Image {
23-
init(platformImage: PlatformImage) {
24-
#if os(macOS)
25-
self.init(nsImage: platformImage)
26-
#else
27-
self.init(uiImage: platformImage)
28-
#endif
29-
}
30-
31-
static var empty = Image(platformImage: PlatformImage())
32-
}
33-
3421
#if os(macOS)
3522
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
3623
public typealias PlatformView = NSView

SDWebImageSwiftUI/Classes/WebImage.swift

+55-11
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ public struct WebImage : View {
6363
// this prefetch the memory cache of image, to immediately render it on screen
6464
// this solve the case when `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :)
6565
if imageManager.isFirstPrefetch {
66-
self.imageManager.prefetch()
66+
imageManager.prefetch()
6767
}
6868
return Group {
6969
if imageManager.image != nil {
70-
if isAnimating && !self.imageManager.isIncremental {
70+
if isAnimating && !imageManager.isIncremental {
7171
if currentFrame != nil {
72-
configure(image: Image(platformImage: currentFrame!))
72+
configure(image: currentFrame!)
7373
.onAppear {
7474
self.imagePlayer?.startPlaying()
7575
}
@@ -84,16 +84,16 @@ public struct WebImage : View {
8484
}
8585
}
8686
} else {
87-
configure(image: Image(platformImage: imageManager.image!))
87+
configure(image: imageManager.image!)
8888
.onReceive(imageManager.$image) { image in
8989
self.setupPlayer(image: image)
9090
}
9191
}
9292
} else {
9393
if currentFrame != nil {
94-
configure(image: Image(platformImage: currentFrame!))
94+
configure(image: currentFrame!)
9595
} else {
96-
configure(image: Image(platformImage: imageManager.image!))
96+
configure(image: imageManager.image!)
9797
}
9898
}
9999
} else {
@@ -122,10 +122,52 @@ public struct WebImage : View {
122122
}
123123
}
124124

125-
func configure(image: Image) -> some View {
125+
/// Configure the platform image into the SwiftUI rendering image
126+
func configure(image: PlatformImage) -> some View {
127+
// Actual rendering SwiftUI image
128+
let result: Image
129+
// NSImage works well with SwiftUI, include Vector and EXIF images.
130+
#if os(macOS)
131+
result = Image(nsImage: image)
132+
#else
133+
// Fix the SwiftUI.Image rendering issue, like when use EXIF UIImage, the `.aspectRatio` does not works. SwiftUI's Bug :)
134+
// See issue #101
135+
var cgImage: CGImage?
136+
// Case 1: Vector Image, draw bitmap image
137+
if image.sd_isVector {
138+
// ensure CGImage is nil
139+
if image.cgImage == nil {
140+
// draw vector into bitmap with the screen scale (behavior like AppKit)
141+
#if os(iOS) || os(tvOS)
142+
let scale = UIScreen.main.scale
143+
#else
144+
let scale = WKInterfaceDevice.current().screenScale
145+
#endif
146+
UIGraphicsBeginImageContextWithOptions(image.size, false, scale)
147+
image.draw(at: .zero)
148+
cgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
149+
UIGraphicsEndImageContext()
150+
} else {
151+
cgImage = image.cgImage
152+
}
153+
}
154+
// Case 2: Image with EXIF orientation (only EXIF 5-8 contains bug)
155+
else if [.left, .leftMirrored, .right, .rightMirrored].contains(image.imageOrientation) {
156+
cgImage = image.cgImage
157+
}
158+
// If we have CGImage, use CGImage based API, else use UIImage based API
159+
if let cgImage = cgImage {
160+
let scale = image.scale
161+
let orientation = image.imageOrientation.toSwiftUI
162+
result = Image(decorative: cgImage, scale: scale, orientation: orientation)
163+
} else {
164+
result = Image(uiImage: image)
165+
}
166+
#endif
167+
126168
// Should not use `EmptyView`, which does not respect to the container's frame modifier
127169
// Using a empty image instead for better compatible
128-
configurations.reduce(image) { (previous, configuration) in
170+
return configurations.reduce(result) { (previous, configuration) in
129171
configuration(previous)
130172
}
131173
}
@@ -136,12 +178,12 @@ public struct WebImage : View {
136178
if let placeholder = placeholder {
137179
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
138180
if imageManager.options.contains(.delayPlaceholder) && imageManager.isLoading {
139-
return AnyView(configure(image: Image.empty))
181+
return AnyView(configure(image: .empty))
140182
} else {
141183
return placeholder
142184
}
143185
} else {
144-
return AnyView(configure(image: Image.empty))
186+
return AnyView(configure(image: .empty))
145187
}
146188
}
147189

@@ -260,7 +302,9 @@ extension WebImage {
260302
/// - Parameter image: A Image view that describes the placeholder.
261303
public func placeholder(_ image: Image) -> WebImage {
262304
return placeholder {
263-
configure(image: image)
305+
configurations.reduce(image) { (previous, configuration) in
306+
configuration(previous)
307+
}
264308
}
265309
}
266310

Tests/WebImageTests.swift

+19
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,23 @@ class WebImageTests: XCTestCase {
133133
ViewHosting.expel()
134134
}
135135

136+
func testWebImageEXIFImage() throws {
137+
let expectation = self.expectation(description: "WebImage EXIF image url")
138+
// EXIF 5, Left Mirrored
139+
let imageUrl = URL(string: "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg")
140+
let imageView = WebImage(url: imageUrl)
141+
let introspectView = imageView.onSuccess { image, cacheType in
142+
let displayImage = try? imageView.inspect().group().image(0).cgImage()
143+
let orientation = try! imageView.inspect().group().image(0).orientation()
144+
XCTAssertNotNil(displayImage)
145+
XCTAssertEqual(orientation, .leftMirrored)
146+
expectation.fulfill()
147+
}.onFailure { error in
148+
XCTFail(error.localizedDescription)
149+
}
150+
_ = try introspectView.inspect()
151+
ViewHosting.host(view: introspectView)
152+
self.waitForExpectations(timeout: 5, handler: nil)
153+
ViewHosting.expel()
154+
}
136155
}

0 commit comments

Comments
 (0)