From fe0e42afc25d66efeb0ceaefbec089b99f93cf90 Mon Sep 17 00:00:00 2001 From: zxiou Date: Thu, 21 Nov 2019 19:17:44 +0800 Subject: [PATCH 001/181] fix the core render of SDAnimatedImagePlayer and fix assignment animationRepeatCount does not work Signed-off-by: zxiou --- .DS_Store | Bin 0 -> 6148 bytes Examples/.DS_Store | Bin 0 -> 6148 bytes .../SDWebImage Demo/DetailViewController.m | 2 + .../SDWebImage Demo/MasterViewController.m | 1 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ SDWebImage/.DS_Store | Bin 0 -> 6148 bytes SDWebImage/Core/SDAnimatedImagePlayer.m | 118 ++++++++++-------- SDWebImage/Core/SDAnimatedImageView.m | 13 ++ 8 files changed, 90 insertions(+), 52 deletions(-) create mode 100644 .DS_Store create mode 100644 Examples/.DS_Store create mode 100644 SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SDWebImage/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2d16dfcc10fe0dc9f4cb84573ae26c03191498de GIT binary patch literal 6148 zcmeHKL2uJA6n^dsq(g`vCfLyzPODI>!nz$Mg%S?CAu?^!0By+{(reAtH(crbu)ZnhaP3K#`$ngXa`9OXs3{gc(! z>bGv+u^RZhXT5cYqI3(d80UR2dreO-y<+HV|3HZNx9|CfQCOZdH=c{6@S-HlBsmH~ zobu)##W)FaxsIy>l&5jh+?-6dySpv^w7WBH@ky`OZt*AG?dh~(J$%&J+dmzB z`aJnE{d%s{W%x3Zc3TX~D&fIj>MpU~4BkVIwR@;TI8!_H zj+f=nkydMF*xYA53Rv`i8<}c3hv!>A-A-rZ);01!fgk zQ;$tL|6lz1{y$4HPeuWwz<;Fxt2?gK$CC8fI#(Q>wH(_iHZq!5DU=lK^l>a5I*K>3 bNy8W?2hrA8DZ~*p^C2K*Fr86gr3(B460Dw@ literal 0 HcmV?d00001 diff --git a/Examples/.DS_Store b/Examples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fa4d26ea88797260ea849d0f24dece6255454310 GIT binary patch literal 6148 zcmeHKyG{c^3>-s>NED=`++W}iR?$&VUqK6qC=DbM5~;7^yZAK54R+`PKil})$n0;v()XH#$nA@s@^(#Iq$WNeoyzB54s!I oL1Bn?OpJESjkn{gD9XC#Yd-IVQ)1AW4?0ml1Fnlq3jDPKpS%Yac>n+a literal 0 HcmV?d00001 diff --git a/Examples/SDWebImage Demo/DetailViewController.m b/Examples/SDWebImage Demo/DetailViewController.m index 4327c9dd..ee9be9df 100644 --- a/Examples/SDWebImage Demo/DetailViewController.m +++ b/Examples/SDWebImage Demo/DetailViewController.m @@ -24,6 +24,8 @@ [self.imageView sd_setImageWithURL:self.imageURL placeholderImage:nil options:SDWebImageProgressiveLoad]; + self.imageView.shouldCustomLoopCount = YES; + self.imageView.animationRepeatCount = NSIntegerMax; } - (void)viewDidLoad { diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index 14aea5ca..bb86ba17 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -63,6 +63,7 @@ [SDWebImageDownloader sharedDownloader].config.executionOrder = SDWebImageDownloaderLIFOExecutionOrder; self.objects = [NSMutableArray arrayWithObjects: + @"https://s2.ax1x.com/2019/11/01/KHYIgJ.gif", @"http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default.aspx?0.35786508303135633", // requires HTTP auth, used to demo the NTLM auth @"http://assets.sbnation.com/assets/2512203/dogflops.gif", @"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif", diff --git a/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SDWebImage/.DS_Store b/SDWebImage/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..516153fe4952f37097299528382f83d7f653aab6 GIT binary patch literal 6148 zcmeHK!AiqG5Z!H~CWzRBV2`!GD;y$GSL2X8_|4=Qa!f(^uMNm7f}N`6EC$S?4B zoY~zLOBFnc*qJcsotMhS+WLlR!Lw!FxC1eEb1xrdZ7;pT z-kFyVd~Kf#5zlSUzlg$dud>q-N$y2Sn9AfR2qEP9GD-q5Zi`V8WO5!?4d6OfuTq^% zng`9Ay?<1n*6c~ERR_CyIGtMN?q1{gw14;TIC+{rzbIBYe1KB6HO}A_jCq0gZkog* zxdq=Weilba3=jjvzzQ*-w?K1qg=(f!i2-8ZXAI!}V1pt$8ViMT>wpG-A9373L;)M` z5{R}&M`NK7MnJeq1yreAUop5!2fwZ39F2uSmCm?a8RpR|bA3bMa&_?AGMsToA+^K+ zF|f!$S#>LT{-1n*|6fd^9x*@+{3`~y(RI6RC`q5KOU2oxg eM{yNY3ixd_03D5mLhyjlkAS3s8e-sA8TbIFeo$oq literal 0 HcmV?d00001 diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index 2efd8805..39658b43 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -23,6 +23,7 @@ @property (nonatomic, strong) NSMutableDictionary *frameBuffer; @property (nonatomic, assign) NSTimeInterval currentTime; @property (nonatomic, assign) BOOL bufferMiss; +@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; @property (nonatomic, assign) NSUInteger maxBufferCount; @property (nonatomic, strong) NSOperationQueue *fetchQueue; @property (nonatomic, strong) dispatch_semaphore_t lock; @@ -165,6 +166,7 @@ self.currentLoopCount = 0; self.currentTime = 0; self.bufferMiss = NO; + self.needsDisplayWhenImageBecomesAvailable = NO; [self handleFrameChange]; } @@ -217,8 +219,6 @@ if (!self.isPlaying) { return; } - // Calculate refresh duration - NSTimeInterval duration = self.displayLink.duration; NSUInteger totalFrameCount = self.totalFrameCount; if (totalFrameCount <= 1) { @@ -226,8 +226,6 @@ [self stopPlaying]; return; } - NSUInteger currentFrameIndex = self.currentFrameIndex; - NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; NSTimeInterval playbackRate = self.playbackRate; if (playbackRate <= 0) { @@ -236,7 +234,46 @@ return; } - // Check if we have the frame buffer firstly to improve performance + // Calculate refresh duration + NSTimeInterval duration = self.displayLink.duration; + + NSUInteger currentFrameIndex = self.currentFrameIndex; + NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; + + // Check if we need to display new frame firstly + BOOL bufferFull = NO; + if (self.needsDisplayWhenImageBecomesAvailable) { + UIImage *currentFrame; + SD_LOCK(self.lock); + currentFrame = self.frameBuffer[@(currentFrameIndex)]; + SD_UNLOCK(self.lock); + + // Update the current frame + if (currentFrame) { + SD_LOCK(self.lock); + // Remove the frame buffer if need + if (self.frameBuffer.count > self.maxBufferCount) { + self.frameBuffer[@(currentFrameIndex)] = nil; + } + // Check whether we can stop fetch + if (self.frameBuffer.count == totalFrameCount) { + bufferFull = YES; + } + SD_UNLOCK(self.lock); + + // Update the current frame immediately + self.currentFrame = currentFrame; + [self handleFrameChange]; + + self.bufferMiss = NO; + self.needsDisplayWhenImageBecomesAvailable = NO; + } + else { + self.bufferMiss = YES; + } + } + + // Check if we have the frame buffer if (!self.bufferMiss) { // Then check if timestamp is reached self.currentTime += duration; @@ -246,6 +283,10 @@ // Current frame timestamp not reached, return return; } + + // Otherwise, we shoudle be ready to display next frame + self.needsDisplayWhenImageBecomesAvailable = YES; + self.currentFrameIndex = nextFrameIndex; self.currentTime -= currentDuration; NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex]; nextDuration = nextDuration / playbackRate; @@ -253,45 +294,19 @@ // Do not skip frame self.currentTime = nextDuration; } - } - - // Update the current frame - UIImage *currentFrame; - UIImage *fetchFrame; - SD_LOCK(self.lock); - currentFrame = self.frameBuffer[@(currentFrameIndex)]; - fetchFrame = currentFrame ? self.frameBuffer[@(nextFrameIndex)] : nil; - SD_UNLOCK(self.lock); - BOOL bufferFull = NO; - if (currentFrame) { - SD_LOCK(self.lock); - // Remove the frame buffer if need - if (self.frameBuffer.count > self.maxBufferCount) { - self.frameBuffer[@(currentFrameIndex)] = nil; - } - // Check whether we can stop fetch - if (self.frameBuffer.count == totalFrameCount) { - bufferFull = YES; - } - SD_UNLOCK(self.lock); - self.currentFrame = currentFrame; - [self handleFrameChange]; - self.currentFrameIndex = nextFrameIndex; - self.bufferMiss = NO; - } else { - self.bufferMiss = YES; - } - - // Update the loop count when last frame rendered - if (nextFrameIndex == 0 && !self.bufferMiss) { - // Update the loop count - self.currentLoopCount++; - [self handleLoopChnage]; - // if reached the max loop count, stop animating, 0 means loop indefinitely - NSUInteger maxLoopCount = self.totalLoopCount; - if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { - [self stopPlaying]; - return; + + // Update the loop count when last frame rendered + if (nextFrameIndex == 0) { + // Update the loop count + self.currentLoopCount++; + [self handleLoopChnage]; + + // if reached the max loop count, stop animating, 0 means loop indefinitely + NSUInteger maxLoopCount = self.totalLoopCount; + if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { + [self stopPlaying]; + return; + } } } @@ -301,14 +316,13 @@ } // Check if we should prefetch next frame or current frame - NSUInteger fetchFrameIndex; - if (self.bufferMiss) { - // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame - fetchFrameIndex = currentFrameIndex; - } else { - // Or, most cases, the decode speed is faster than render speed, we fetch next frame - fetchFrameIndex = nextFrameIndex; - } + // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame + // Or, most cases, the decode speed is faster than render speed, we fetch next frame + NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex; + UIImage *fetchFrame; + SD_LOCK(self.lock); + fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)]; + SD_UNLOCK(self.lock); if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { // Prefetch next frame in background queue diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index 181f2db9..78f1cb8b 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -306,6 +306,19 @@ #pragma mark - UIImageView Method Overrides #pragma mark Image Data +- (void)setAnimationRepeatCount:(NSInteger)animationRepeatCount +{ +#if SD_UIKIT + [super setAnimationRepeatCount:animationRepeatCount]; +#else + _animationRepeatCount = animationRepeatCount; +#endif + + if (self.shouldCustomLoopCount) { + self.player.totalLoopCount = animationRepeatCount; + } +} + - (void)startAnimating { if (self.player) { From d5da5dbef7362a683c2bb1d0dc7eac7f5459ab2b Mon Sep 17 00:00:00 2001 From: zxiou Date: Thu, 21 Nov 2019 19:32:11 +0800 Subject: [PATCH 002/181] delete useless files --- .DS_Store | Bin 6148 -> 0 bytes Examples/.DS_Store | Bin 6148 -> 0 bytes SDWebImage/.DS_Store | Bin 6148 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 Examples/.DS_Store delete mode 100644 SDWebImage/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 2d16dfcc10fe0dc9f4cb84573ae26c03191498de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKL2uJA6n^dsq(g`vCfLyzPODI>!nz$Mg%S?CAu?^!0By+{(reAtH(crbu)ZnhaP3K#`$ngXa`9OXs3{gc(! z>bGv+u^RZhXT5cYqI3(d80UR2dreO-y<+HV|3HZNx9|CfQCOZdH=c{6@S-HlBsmH~ zobu)##W)FaxsIy>l&5jh+?-6dySpv^w7WBH@ky`OZt*AG?dh~(J$%&J+dmzB z`aJnE{d%s{W%x3Zc3TX~D&fIj>MpU~4BkVIwR@;TI8!_H zj+f=nkydMF*xYA53Rv`i8<}c3hv!>A-A-rZ);01!fgk zQ;$tL|6lz1{y$4HPeuWwz<;Fxt2?gK$CC8fI#(Q>wH(_iHZq!5DU=lK^l>a5I*K>3 bNy8W?2hrA8DZ~*p^C2K*Fr86gr3(B460Dw@ diff --git a/Examples/.DS_Store b/Examples/.DS_Store deleted file mode 100644 index fa4d26ea88797260ea849d0f24dece6255454310..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s>NED=`++W}iR?$&VUqK6qC=DbM5~;7^yZAK54R+`PKil})$n0;v()XH#$nA@s@^(#Iq$WNeoyzB54s!I oL1Bn?OpJESjkn{gD9XC#Yd-IVQ)1AW4?0ml1Fnlq3jDPKpS%Yac>n+a diff --git a/SDWebImage/.DS_Store b/SDWebImage/.DS_Store deleted file mode 100644 index 516153fe4952f37097299528382f83d7f653aab6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5Z!H~CWzRBV2`!GD;y$GSL2X8_|4=Qa!f(^uMNm7f}N`6EC$S?4B zoY~zLOBFnc*qJcsotMhS+WLlR!Lw!FxC1eEb1xrdZ7;pT z-kFyVd~Kf#5zlSUzlg$dud>q-N$y2Sn9AfR2qEP9GD-q5Zi`V8WO5!?4d6OfuTq^% zng`9Ay?<1n*6c~ERR_CyIGtMN?q1{gw14;TIC+{rzbIBYe1KB6HO}A_jCq0gZkog* zxdq=Weilba3=jjvzzQ*-w?K1qg=(f!i2-8ZXAI!}V1pt$8ViMT>wpG-A9373L;)M` z5{R}&M`NK7MnJeq1yreAUop5!2fwZ39F2uSmCm?a8RpR|bA3bMa&_?AGMsToA+^K+ zF|f!$S#>LT{-1n*|6fd^9x*@+{3`~y(RI6RC`q5KOU2oxg eM{yNY3ixd_03D5mLhyjlkAS3s8e-sA8TbIFeo$oq From dd8ea8f7fbca9c9ba29d55bb945cddc0812cd736 Mon Sep 17 00:00:00 2001 From: zxiou Date: Fri, 22 Nov 2019 11:49:57 +0800 Subject: [PATCH 003/181] delete IDEWorkspaceChecks --- .DS_Store | Bin 0 -> 8196 bytes .../xcshareddata/IDEWorkspaceChecks.plist | 8 -------- SDWebImage/.DS_Store | Bin 0 -> 8196 bytes 3 files changed, 8 deletions(-) create mode 100644 .DS_Store delete mode 100644 SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SDWebImage/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..eeded2ff3a566d60f92b5dce46dd51e480df6b18 GIT binary patch literal 8196 zcmeI1O^?z*7{{Ms(Jh*d95%|a7f)_XSq*S8LADp1=0EN8OVjcc0AQV=Uk9iGfPqzL^&&P4ikKI5 zu9Wo17AiwOKnOj^ydq1yXl^wP5CI}U1c(3;AOh<^0H4{S7-u~9)hx9{fC&7T1jPNp z#45C{b)r<>IXUgTk+DfS<0z_b*fXLl9VGlUO@O(Od=N`@+dr1^#xn+H2rH%5r z^P5HmKU>B#r_T!~ck^M^anmQ_+dVh$d%FLC^XR1SdXK`OIIM2(@i=$GI7p>A^aEV- z^dO9VUUc{{_A|MUqb9_0Du>nDXtdjE?3g#L=6J^(HJcXpZ#2f^igD#?{nmbW@aFC4 z-T3_n6&cfa2e>p7>BspnUpZ+U@wkUG!gT>0u;Bm#@L(SX;Nq){K)uRX8x$>T7sGF~ zRz|Tf*cl+(i-5p&cnwE*wfG1sffRc$qF&uO;)6sYFR^Q=f9xG_+x8*}*m(S-~X?96m)t-fC#KYfR$~>?%)nTe?8A` zCUR{PYZt31V%hL@AD7p%(!X12sh8pAz^1W)Krj literal 0 HcmV?d00001 diff --git a/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/SDWebImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/SDWebImage/.DS_Store b/SDWebImage/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3ed219f226727dcd24292f55e3fccd9848d5849f GIT binary patch literal 8196 zcmeHMT}&KR6u#dA?Tj7DfLN%y*+pz2NG((ZTD0A=w6=w&*rh!57b+J4=8TeQ8XS&fUyC z_nvd^xpThx?%dp2#u(bOS|ekXj4_EWF4a|3U8Qh4zpf}DUsFgDG@hl#ow1Z@r>KAN z3hzJ>AP^uBAP^uBAP^vMJs?1PHZRgL`@XOSb$~#CzzvB2e?LU&;xgvT86mw_2UXq? zfFwTxc!k<3Z;*`oGUm$}A<04oVkk*56#fwdVmRrezF*9jGeU~t4F2H*{><=CDDY;d z_)*WDAtq!{2M7cRT!{c*K7}mKY?fjA!t;A-%rqRgq2X(U3JQx>tdwN>6-y5#M(pu~ zn{=~IdonY~d)-NQB;~hzY}=e~CsVzKK0c(D$8F0^8kU~%1{<1AE{FOIOS8w@?X0Ca zUYvw5@Ok7RwQ_2zwP90zw5hpux;{G9($YZfdzz=GWvOCa2dJ)K>9m3{q2+O`tLaeYWFaVW>#)a}mb zIm)LmZJXIq%NbHj(@8U(HIuHs-88u~=}Me*#x~7v+c8|jwhs2Y`h=Sgw8+#y;Mf^A z?gblC)vTlM*D02f(>o|4g(~B-?Hl7^oL)WSkgSe1i1g$$t1BXR)^6C;a$ju6Y>8T0 zrj#pd`f`S2q)h$5xS_ct2a`@(w=}~V9w1~b*EnM68Mzeoa*vf9)o05p*WP;D?cwlJ z?|hX0Y|1#6HQY(HT9U*ZRpDw`JtrN~=nnVV8jq-XmT*Zp8ZIaq4_vJ*|LRqeCljQ!iJvvIs zI6~JR?vT_A(!+e>uB7Ac8`CYCo2Rz3DjYt@_1iT~*Ca`*j`huW=vBvR1&Y-|Fy;Xw zt9%m*v-PZ%w!0p7gcikP>=ZlA&a(6DL-rZ_id|;kv7gzW>@POQ{sx1L60AiP>QIjj zxEn2K#TK+-7aqiZBrt>#7&r)vv!oO!*ub&{bq#phET;_xZXRClbrv< eklIn-7W3tdkYu6qzkdj@g8e_(|2^v6(A7UOUMBVc literal 0 HcmV?d00001 From d1be404b63ddcf5b6c28028232908bd97bddd7f6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 22 Nov 2019 16:16:20 +0800 Subject: [PATCH 004/181] Remove the untracked DS_Store file --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 2 ++ Examples/SDWebImage Demo/DetailViewController.m | 2 +- Examples/SDWebImage Demo/MasterViewController.m | 2 +- SDWebImage/.DS_Store | Bin 8196 -> 0 bytes 5 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 .DS_Store delete mode 100644 SDWebImage/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index eeded2ff3a566d60f92b5dce46dd51e480df6b18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeI1O^?z*7{{Ms(Jh*d95%|a7f)_XSq*S8LADp1=0EN8OVjcc0AQV=Uk9iGfPqzL^&&P4ikKI5 zu9Wo17AiwOKnOj^ydq1yXl^wP5CI}U1c(3;AOh<^0H4{S7-u~9)hx9{fC&7T1jPNp z#45C{b)r<>IXUgTk+DfS<0z_b*fXLl9VGlUO@O(Od=N`@+dr1^#xn+H2rH%5r z^P5HmKU>B#r_T!~ck^M^anmQ_+dVh$d%FLC^XR1SdXK`OIIM2(@i=$GI7p>A^aEV- z^dO9VUUc{{_A|MUqb9_0Du>nDXtdjE?3g#L=6J^(HJcXpZ#2f^igD#?{nmbW@aFC4 z-T3_n6&cfa2e>p7>BspnUpZ+U@wkUG!gT>0u;Bm#@L(SX;Nq){K)uRX8x$>T7sGF~ zRz|Tf*cl+(i-5p&cnwE*wfG1sffRc$qF&uO;)6sYFR^Q=f9xG_+x8*}*m(S-~X?96m)t-fC#KYfR$~>?%)nTe?8A` zCUR{PYZt31V%hL@AD7p%(!X12sh8pAz^1W)Krj diff --git a/.gitignore b/.gitignore index 8d02b18b..21074f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# OS X +.DS_Store # Xcode # build/ diff --git a/Examples/SDWebImage Demo/DetailViewController.m b/Examples/SDWebImage Demo/DetailViewController.m index ee9be9df..cdcaf1d9 100644 --- a/Examples/SDWebImage Demo/DetailViewController.m +++ b/Examples/SDWebImage Demo/DetailViewController.m @@ -25,7 +25,7 @@ placeholderImage:nil options:SDWebImageProgressiveLoad]; self.imageView.shouldCustomLoopCount = YES; - self.imageView.animationRepeatCount = NSIntegerMax; + self.imageView.animationRepeatCount = 0; } - (void)viewDidLoad { diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index bb86ba17..3a6c1780 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -63,7 +63,6 @@ [SDWebImageDownloader sharedDownloader].config.executionOrder = SDWebImageDownloaderLIFOExecutionOrder; self.objects = [NSMutableArray arrayWithObjects: - @"https://s2.ax1x.com/2019/11/01/KHYIgJ.gif", @"http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default.aspx?0.35786508303135633", // requires HTTP auth, used to demo the NTLM auth @"http://assets.sbnation.com/assets/2512203/dogflops.gif", @"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif", @@ -75,6 +74,7 @@ @"https://isparta.github.io/compare-webp/image/gif_webp/webp/2.webp", @"https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic", @"https://nokiatech.github.io/heif/content/image_sequences/starfield_animation.heic", + @"https://s2.ax1x.com/2019/11/01/KHYIgJ.gif", @"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png", @"http://via.placeholder.com/200x200.jpg", nil]; diff --git a/SDWebImage/.DS_Store b/SDWebImage/.DS_Store deleted file mode 100644 index 3ed219f226727dcd24292f55e3fccd9848d5849f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMT}&KR6u#dA?Tj7DfLN%y*+pz2NG((ZTD0A=w6=w&*rh!57b+J4=8TeQ8XS&fUyC z_nvd^xpThx?%dp2#u(bOS|ekXj4_EWF4a|3U8Qh4zpf}DUsFgDG@hl#ow1Z@r>KAN z3hzJ>AP^uBAP^uBAP^vMJs?1PHZRgL`@XOSb$~#CzzvB2e?LU&;xgvT86mw_2UXq? zfFwTxc!k<3Z;*`oGUm$}A<04oVkk*56#fwdVmRrezF*9jGeU~t4F2H*{><=CDDY;d z_)*WDAtq!{2M7cRT!{c*K7}mKY?fjA!t;A-%rqRgq2X(U3JQx>tdwN>6-y5#M(pu~ zn{=~IdonY~d)-NQB;~hzY}=e~CsVzKK0c(D$8F0^8kU~%1{<1AE{FOIOS8w@?X0Ca zUYvw5@Ok7RwQ_2zwP90zw5hpux;{G9($YZfdzz=GWvOCa2dJ)K>9m3{q2+O`tLaeYWFaVW>#)a}mb zIm)LmZJXIq%NbHj(@8U(HIuHs-88u~=}Me*#x~7v+c8|jwhs2Y`h=Sgw8+#y;Mf^A z?gblC)vTlM*D02f(>o|4g(~B-?Hl7^oL)WSkgSe1i1g$$t1BXR)^6C;a$ju6Y>8T0 zrj#pd`f`S2q)h$5xS_ct2a`@(w=}~V9w1~b*EnM68Mzeoa*vf9)o05p*WP;D?cwlJ z?|hX0Y|1#6HQY(HT9U*ZRpDw`JtrN~=nnVV8jq-XmT*Zp8ZIaq4_vJ*|LRqeCljQ!iJvvIs zI6~JR?vT_A(!+e>uB7Ac8`CYCo2Rz3DjYt@_1iT~*Ca`*j`huW=vBvR1&Y-|Fy;Xw zt9%m*v-PZ%w!0p7gcikP>=ZlA&a(6DL-rZ_id|;kv7gzW>@POQ{sx1L60AiP>QIjj zxEn2K#TK+-7aqiZBrt>#7&r)vv!oO!*ub&{bq#phET;_xZXRClbrv< eklIn-7W3tdkYu6qzkdj@g8e_(|2^v6(A7UOUMBVc From 74526bdde4b080916100261d4301dad5863b67df Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 22 Nov 2019 16:22:42 +0800 Subject: [PATCH 005/181] Bumped version to 5.3.2 Update the CHANGELOG --- CHANGELOG.md | 6 ++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a196fbe..ea18fcfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [5.3 Patch, on Nov 22nd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.2) +See [all tickets marked for the 5.3.2 release](https://github.com/SDWebImage/SDWebImage/milestone/53) + +### Fixes +- Fix animated image playback bugs that cause rendering frame is previous frame index #2895. Thanks @ZXIOU + ## [5.3 Patch, on Nov 9th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.1) See [all tickets marked for the 5.3.1 release](https://github.com/SDWebImage/SDWebImage/milestone/52) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index abe5f7c7..e6c81be4 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.3.1' + s.version = '5.3.2' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index b3d623a4..92626e2a 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.3.1 + 5.3.2 CFBundleSignature ???? CFBundleVersion - 5.3.1 + 5.3.2 NSPrincipalClass From 892a7ad892141812f49221e2983eff69a96aea7b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 23 Nov 2019 18:50:39 +0800 Subject: [PATCH 006/181] Add the feature to allows advanced user to provided extended data associarted with image data, used for scale factor saving, rich link metadata saving, etc --- SDWebImage.xcodeproj/project.pbxproj | 20 +++ SDWebImage/Core/NSData+ExtendedData.h | 21 +++ SDWebImage/Core/NSData+ExtendedData.m | 23 ++++ SDWebImage/Core/SDDiskCache.h | 20 +++ SDWebImage/Core/SDDiskCache.m | 28 ++++ SDWebImage/Core/SDImageCache.m | 10 ++ SDWebImage/Core/SDWebImageCacheSerializer.h | 4 + .../NSFileManager+ExtendedAttributes.h | 19 +++ .../NSFileManager+ExtendedAttributes.m | 128 ++++++++++++++++++ WebImage/SDWebImage.h | 1 + 10 files changed, 274 insertions(+) create mode 100644 SDWebImage/Core/NSData+ExtendedData.h create mode 100644 SDWebImage/Core/NSData+ExtendedData.m create mode 100644 SDWebImage/Private/NSFileManager+ExtendedAttributes.h create mode 100644 SDWebImage/Private/NSFileManager+ExtendedAttributes.m diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index 1174fe97..28197f70 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -89,6 +89,12 @@ 325C46272233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h in Headers */ = {isa = PBXBuildFile; fileRef = 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */; settings = {ATTRIBUTES = (Private, ); }; }; 325C46282233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; 325C46292233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; + 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */; }; + 325F7CCA238942AB00AEDFCC /* NSData+ExtendedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 325F7CCB238942AB00AEDFCC /* NSData+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */; }; + 325F7CCC2389463D00AEDFCC /* NSData+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */; }; + 325F7CCD2389467800AEDFCC /* NSData+ExtendedData.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */; }; 326E2F2E236F0B23006F847F /* SDAnimatedImagePlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 326E2F2F236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; 326E2F30236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; @@ -298,6 +304,7 @@ dstPath = include/SDWebImage; dstSubfolderSpec = 16; files = ( + 325F7CCD2389467800AEDFCC /* NSData+ExtendedData.h in Copy Headers */, 326E2F36236F1E30006F847F /* SDAnimatedImagePlayer.h in Copy Headers */, 3250C9F12355E3DF0093A896 /* SDWebImageDownloaderDecryptor.h in Copy Headers */, 325427662355783C0042BAA4 /* SDWebImageDownloaderResponseModifier.h in Copy Headers */, @@ -407,6 +414,10 @@ 325C461F2233A02E004CAE11 /* UIColor+HexString.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIColor+HexString.m"; sourceTree = ""; }; 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSBezierPath+RoundedCorners.h"; sourceTree = ""; }; 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBezierPath+RoundedCorners.m"; sourceTree = ""; }; + 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+ExtendedAttributes.h"; sourceTree = ""; }; + 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+ExtendedAttributes.m"; sourceTree = ""; }; + 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSData+ExtendedData.h"; path = "Core/NSData+ExtendedData.h"; sourceTree = ""; }; + 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSData+ExtendedData.m"; path = "Core/NSData+ExtendedData.m"; sourceTree = ""; }; 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImagePlayer.h; path = Core/SDAnimatedImagePlayer.h; sourceTree = ""; }; 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImagePlayer.m; path = Core/SDAnimatedImagePlayer.m; sourceTree = ""; }; 326E2F31236F1D58006F847F /* SDDeviceHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDDeviceHelper.h; sourceTree = ""; }; @@ -635,6 +646,8 @@ 325C461F2233A02E004CAE11 /* UIColor+HexString.m */, 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */, 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */, + 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */, + 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */, 329F123F223FAD3400B309FD /* SDInternalMacros.h */, 329F123E223FAD3400B309FD /* SDInternalMacros.m */, 329F1235223FAA3B00B309FD /* SDmetamacros.h */, @@ -744,6 +757,8 @@ children = ( 5D5B9140188EE8DD006D06BD /* NSData+ImageContentType.h */, 5D5B9141188EE8DD006D06BD /* NSData+ImageContentType.m */, + 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */, + 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */, A18A6CC5172DC28500419892 /* UIImage+GIF.h */, A18A6CC6172DC28500419892 /* UIImage+GIF.m */, 329A18571FFF5DFD008C9A2F /* UIImage+Metadata.h */, @@ -849,6 +864,7 @@ 3257EAFA21898AED0097B271 /* SDImageGraphics.h in Headers */, 32D3CDD121DDE87300C4DB49 /* UIImage+MemoryCacheCost.h in Headers */, 328BB6AC2081FEE500760D6C /* SDWebImageCacheSerializer.h in Headers */, + 325F7CCA238942AB00AEDFCC /* NSData+ExtendedData.h in Headers */, 325C46272233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h in Headers */, 321B378F2083290E00C0EA77 /* SDImageLoadersManager.h in Headers */, 329A185B1FFF5DFD008C9A2F /* UIImage+Metadata.h in Headers */, @@ -906,6 +922,7 @@ 32C0FDE32013426C001B8F2D /* SDWebImageIndicator.h in Headers */, 32F7C0712030114C00873181 /* SDImageTransformer.h in Headers */, 32E67311235765B500DB4987 /* SDDisplayLink.h in Headers */, + 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */, 4A2CAE2D1AB4BB7500B6BC39 /* UIImage+GIF.h in Headers */, 4A2CAE291AB4BB7500B6BC39 /* NSData+ImageContentType.h in Headers */, 328BB69E2081FED200760D6C /* SDWebImageCacheKeyFilter.h in Headers */, @@ -1098,6 +1115,7 @@ 3257EAFD21898AED0097B271 /* SDImageGraphics.m in Sources */, 3290FA0C1FA478AF0047D20C /* SDImageFrame.m in Sources */, 325C46232233A02E004CAE11 /* UIColor+HexString.m in Sources */, + 325F7CCB238942AB00AEDFCC /* NSData+ExtendedData.m in Sources */, 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */, 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9F02355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, @@ -1127,6 +1145,7 @@ 321E609C1F38E8ED00405457 /* SDImageIOCoder.m in Sources */, 4A2CAE261AB4BB7000B6BC39 /* SDWebImagePrefetcher.m in Sources */, 328BB6C92082581100760D6C /* SDDiskCache.m in Sources */, + 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */, 3248475F201775F600AF9E5A /* SDAnimatedImageView.m in Sources */, 32D1222C2080B2EB003685A3 /* SDImageCachesManager.m in Sources */, 32B9B53F206ED4230026769D /* SDWebImageDownloaderConfig.m in Sources */, @@ -1198,6 +1217,7 @@ 5376130D155AD0D5005750A4 /* SDWebImagePrefetcher.m in Sources */, 328BB6C72082581100760D6C /* SDDiskCache.m in Sources */, 3248475D201775F600AF9E5A /* SDAnimatedImageView.m in Sources */, + 325F7CCC2389463D00AEDFCC /* NSData+ExtendedData.m in Sources */, 32D1222A2080B2EB003685A3 /* SDImageCachesManager.m in Sources */, 32B9B53D206ED4230026769D /* SDWebImageDownloaderConfig.m in Sources */, 43A9186B1D8308FE00B3925F /* SDImageCacheConfig.m in Sources */, diff --git a/SDWebImage/Core/NSData+ExtendedData.h b/SDWebImage/Core/NSData+ExtendedData.h new file mode 100644 index 00000000..abc80ad4 --- /dev/null +++ b/SDWebImage/Core/NSData+ExtendedData.h @@ -0,0 +1,21 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* (c) Fabrice Aneche +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import +#import "SDWebImageCompat.h" + +@interface NSData (ExtendedData) + +/** + Read and Write the extended data to the image data. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. + The extended data will be write to disk cache as well as the image data. The disk cache preserve both of the data and extended data with the same cache key. + */ +@property (nonatomic, strong, nullable) NSData *sd_extendedData; + +@end diff --git a/SDWebImage/Core/NSData+ExtendedData.m b/SDWebImage/Core/NSData+ExtendedData.m new file mode 100644 index 00000000..e50863cc --- /dev/null +++ b/SDWebImage/Core/NSData+ExtendedData.m @@ -0,0 +1,23 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* (c) Fabrice Aneche +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "NSData+ExtendedData.h" +#import + +@implementation NSData (ExtendedData) + +- (NSData *)sd_extendedData { + return objc_getAssociatedObject(self, @selector(sd_extendedData)); +} + +- (void)setSd_extendedData:(NSData *)sd_extendedData { + objc_setAssociatedObject(self, @selector(sd_extendedData), sd_extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +@end diff --git a/SDWebImage/Core/SDDiskCache.h b/SDWebImage/Core/SDDiskCache.h index ffc440e5..dc5e1fae 100644 --- a/SDWebImage/Core/SDDiskCache.h +++ b/SDWebImage/Core/SDDiskCache.h @@ -54,6 +54,26 @@ */ - (void)setData:(nullable NSData *)data forKey:(nonnull NSString *)key; +/** + Returns the extended data associated with a given key. + This method may blocks the calling thread until file read finished. + + @param key A string identifying the data. If nil, just return nil. + @return The value associated with key, or nil if no value is associated with key. + */ +- (nullable NSData *)extendedDataForKey:(nonnull NSString *)key; + +/** + Set extended data with a given key. + + @discussion You can set any extended data to exist cache key. Without override the exist disk file data. + on UNIX, the common way for this is to use the Extended file attributes (xattr) + + @param extendedData The extended data (pass nil to remove). + @param key The key with which to associate the value. If nil, this method has no effect. +*/ +- (void)setExtendedData:(nullable NSData *)extendedData forKey:(nonnull NSString *)key; + /** Removes the value of the specified key in the cache. This method may blocks the calling thread until file delete finished. diff --git a/SDWebImage/Core/SDDiskCache.m b/SDWebImage/Core/SDDiskCache.m index 1d5ec44e..dff1f37f 100644 --- a/SDWebImage/Core/SDDiskCache.m +++ b/SDWebImage/Core/SDDiskCache.m @@ -8,8 +8,11 @@ #import "SDDiskCache.h" #import "SDImageCacheConfig.h" +#import "NSFileManager+ExtendedAttributes.h" #import +static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDiskCache"; + @interface SDDiskCache () @property (nonatomic, copy) NSString *diskCachePath; @@ -95,6 +98,31 @@ } } +- (NSData *)extendedDataForKey:(NSString *)key { + NSParameterAssert(key); + + // get cache Path for image key + NSString *cachePathForKey = [self cachePathForKey:key]; + + NSData *extendedData = [self.fileManager extendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + + return extendedData; +} + +- (void)setExtendedData:(NSData *)extendedData forKey:(NSString *)key { + NSParameterAssert(key); + // get cache Path for image key + NSString *cachePathForKey = [self cachePathForKey:key]; + + if (!extendedData) { + // Remove + [self.fileManager removeExtendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + } else { + // Override + [self.fileManager setExtendedAttribute:SDDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; + } +} + - (void)removeDataForKey:(NSString *)key { NSParameterAssert(key); NSString *filePath = [self cachePathForKey:key]; diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 4c16f763..b8075ab2 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -14,6 +14,7 @@ #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" +#import "NSData+ExtendedData.h" @interface SDImageCache () @@ -238,6 +239,10 @@ } [self.diskCache setData:imageData forKey:key]; + NSData *extendedData = imageData.sd_extendedData; + if (extendedData) { + [self.diskCache setExtendedData:extendedData forKey:key]; + } } #pragma mark - Query and Retrieve Ops @@ -320,6 +325,11 @@ NSData *data = [self.diskCache dataForKey:key]; if (data) { + // Check extended data + NSData *extendedData = [self.diskCache extendedDataForKey:key]; + if (extendedData) { + data.sd_extendedData = extendedData; + } return data; } diff --git a/SDWebImage/Core/SDWebImageCacheSerializer.h b/SDWebImage/Core/SDWebImageCacheSerializer.h index 84c92a37..3c271b1f 100644 --- a/SDWebImage/Core/SDWebImageCacheSerializer.h +++ b/SDWebImage/Core/SDWebImageCacheSerializer.h @@ -17,6 +17,10 @@ typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull i */ @protocol SDWebImageCacheSerializer +/// Provide the image data associated to the image and store to disk cache +/// @param image The loaded image +/// @param data The original loaded image data +/// @param imageURL The image URL - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL; @end diff --git a/SDWebImage/Private/NSFileManager+ExtendedAttributes.h b/SDWebImage/Private/NSFileManager+ExtendedAttributes.h new file mode 100644 index 00000000..fef3a6cb --- /dev/null +++ b/SDWebImage/Private/NSFileManager+ExtendedAttributes.h @@ -0,0 +1,19 @@ +// +// NSFileManager+ExtendedAttributes.h +// NSFileManager+ExtendedAttributes +// +// Created by Jesús A. Álvarez on 2008-12-17. +// Copyright 2008-2009 namedfork.net. All rights reserved. +// + +#import + +@interface NSFileManager (ExtendedAttributes) + +- (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; +- (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; +- (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; +- (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err; +- (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; + +@end diff --git a/SDWebImage/Private/NSFileManager+ExtendedAttributes.m b/SDWebImage/Private/NSFileManager+ExtendedAttributes.m new file mode 100644 index 00000000..2ec60bf4 --- /dev/null +++ b/SDWebImage/Private/NSFileManager+ExtendedAttributes.m @@ -0,0 +1,128 @@ +// +// NSFileManager+ExtendedAttributes.m +// NSFileManager+ExtendedAttributes +// +// Created by Jesús A. Álvarez on 2008-12-17. +// Copyright 2008-2009 namedfork.net. All rights reserved. +// + +#import "NSFileManager+ExtendedAttributes.h" +#import + +@implementation NSFileManager (ExtendedAttributes) + +- (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { + int flags = follow? 0 : XATTR_NOFOLLOW; + + // get size of name list + ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); + if (nameBuffLen == -1) { + if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String:strerror(errno)], @"error", + @"listxattr", @"function", + path, @":path", + [NSNumber numberWithBool:follow], @":traverseLink", + nil] + ]; + return nil; + } else if (nameBuffLen == 0) return [NSArray array]; + + // get name list + NSMutableData *nameBuff = [NSMutableData dataWithLength:nameBuffLen]; + listxattr([path fileSystemRepresentation], [nameBuff mutableBytes], nameBuffLen, flags); + + // convert to array + NSMutableArray * names = [NSMutableArray arrayWithCapacity:5]; + char *nextName, *endOfNames = [nameBuff mutableBytes] + nameBuffLen; + for(nextName = [nameBuff mutableBytes]; nextName < endOfNames; nextName += 1+strlen(nextName)) + [names addObject:[NSString stringWithUTF8String:nextName]]; + return [NSArray arrayWithArray:names]; +} + +- (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { + int flags = follow? 0 : XATTR_NOFOLLOW; + + // get size of name list + ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); + if (nameBuffLen == -1) { + if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String:strerror(errno)], @"error", + @"listxattr", @"function", + path, @":path", + [NSNumber numberWithBool:follow], @":traverseLink", + nil] + ]; + return NO; + } else if (nameBuffLen == 0) return NO; + + // get name list + NSMutableData *nameBuff = [NSMutableData dataWithLength:nameBuffLen]; + listxattr([path fileSystemRepresentation], [nameBuff mutableBytes], nameBuffLen, flags); + + // find our name + char *nextName, *endOfNames = [nameBuff mutableBytes] + nameBuffLen; + for(nextName = [nameBuff mutableBytes]; nextName < endOfNames; nextName += 1+strlen(nextName)) + if (strcmp(nextName, [name UTF8String]) == 0) return YES; + return NO; +} + +- (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { + int flags = follow? 0 : XATTR_NOFOLLOW; + // get length + ssize_t attrLen = getxattr([path fileSystemRepresentation], [name UTF8String], NULL, 0, 0, flags); + if (attrLen == -1) { + if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String:strerror(errno)], @"error", + @"getxattr", @"function", + name, @":name", + path, @":path", + [NSNumber numberWithBool:follow], @":traverseLink", + nil] + ]; + return nil; + } + + // get attribute data + NSMutableData * attrData = [NSMutableData dataWithLength:attrLen]; + getxattr([path fileSystemRepresentation], [name UTF8String], [attrData mutableBytes], attrLen, 0, flags); + return attrData; +} + +- (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err { + int flags = (follow? 0 : XATTR_NOFOLLOW) | (overwrite? 0 : XATTR_CREATE); + if (0 == setxattr([path fileSystemRepresentation], [name UTF8String], [value bytes], [value length], 0, flags)) return YES; + // error + if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String:strerror(errno)], @"error", + @"setxattr", @"function", + name, @":name", + [NSNumber numberWithUnsignedInteger:[value length]], @":value.length", + path, @":path", + [NSNumber numberWithBool:follow], @":traverseLink", + [NSNumber numberWithBool:overwrite], @":overwrite", + nil] + ]; + return NO; +} + +- (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { + int flags = (follow? 0 : XATTR_NOFOLLOW); + if (0 == removexattr([path fileSystemRepresentation], [name UTF8String], flags)) return YES; + // error + if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String:strerror(errno)], @"error", + @"removexattr", @"function", + name, @":name", + path, @":path", + [NSNumber numberWithBool:follow], @":traverseLink", + nil] + ]; + return NO; +} + +@end diff --git a/WebImage/SDWebImage.h b/WebImage/SDWebImage.h index 6a683cd1..4641b7bd 100644 --- a/WebImage/SDWebImage.h +++ b/WebImage/SDWebImage.h @@ -67,6 +67,7 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; #import #import #import +#import #import #import #import From 7c8d3225c85533167a83b3068fbda72a04f5becb Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 24 Nov 2019 01:33:08 +0800 Subject: [PATCH 007/181] Rename the the extended data to bind it into the UIImage object, which make it compatible for memory cache --- SDWebImage.xcodeproj/project.pbxproj | 24 +++++++++---------- SDWebImage/Core/SDImageCache.m | 22 +++++++++-------- ...+ExtendedData.h => UIImage+ExtendedData.h} | 4 ++-- ...+ExtendedData.m => UIImage+ExtendedData.m} | 4 ++-- WebImage/SDWebImage.h | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) rename SDWebImage/Core/{NSData+ExtendedData.h => UIImage+ExtendedData.h} (75%) rename SDWebImage/Core/{NSData+ExtendedData.m => UIImage+ExtendedData.m} (88%) diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index 28197f70..d70207fd 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -91,10 +91,10 @@ 325C46292233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */; }; - 325F7CCA238942AB00AEDFCC /* NSData+ExtendedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 325F7CCB238942AB00AEDFCC /* NSData+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */; }; - 325F7CCC2389463D00AEDFCC /* NSData+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */; }; - 325F7CCD2389467800AEDFCC /* NSData+ExtendedData.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */; }; + 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */; }; + 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */; }; + 325F7CCD2389467800AEDFCC /* UIImage+ExtendedData.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */; }; 326E2F2E236F0B23006F847F /* SDAnimatedImagePlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 326E2F2F236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; 326E2F30236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; @@ -304,7 +304,7 @@ dstPath = include/SDWebImage; dstSubfolderSpec = 16; files = ( - 325F7CCD2389467800AEDFCC /* NSData+ExtendedData.h in Copy Headers */, + 325F7CCD2389467800AEDFCC /* UIImage+ExtendedData.h in Copy Headers */, 326E2F36236F1E30006F847F /* SDAnimatedImagePlayer.h in Copy Headers */, 3250C9F12355E3DF0093A896 /* SDWebImageDownloaderDecryptor.h in Copy Headers */, 325427662355783C0042BAA4 /* SDWebImageDownloaderResponseModifier.h in Copy Headers */, @@ -416,8 +416,8 @@ 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBezierPath+RoundedCorners.m"; sourceTree = ""; }; 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+ExtendedAttributes.h"; sourceTree = ""; }; 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+ExtendedAttributes.m"; sourceTree = ""; }; - 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSData+ExtendedData.h"; path = "Core/NSData+ExtendedData.h"; sourceTree = ""; }; - 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSData+ExtendedData.m"; path = "Core/NSData+ExtendedData.m"; sourceTree = ""; }; + 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIImage+ExtendedData.h"; path = "Core/UIImage+ExtendedData.h"; sourceTree = ""; }; + 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ExtendedData.m"; path = "Core/UIImage+ExtendedData.m"; sourceTree = ""; }; 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImagePlayer.h; path = Core/SDAnimatedImagePlayer.h; sourceTree = ""; }; 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImagePlayer.m; path = Core/SDAnimatedImagePlayer.m; sourceTree = ""; }; 326E2F31236F1D58006F847F /* SDDeviceHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDDeviceHelper.h; sourceTree = ""; }; @@ -757,8 +757,8 @@ children = ( 5D5B9140188EE8DD006D06BD /* NSData+ImageContentType.h */, 5D5B9141188EE8DD006D06BD /* NSData+ImageContentType.m */, - 325F7CC8238942AB00AEDFCC /* NSData+ExtendedData.h */, - 325F7CC9238942AB00AEDFCC /* NSData+ExtendedData.m */, + 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */, + 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */, A18A6CC5172DC28500419892 /* UIImage+GIF.h */, A18A6CC6172DC28500419892 /* UIImage+GIF.m */, 329A18571FFF5DFD008C9A2F /* UIImage+Metadata.h */, @@ -864,7 +864,7 @@ 3257EAFA21898AED0097B271 /* SDImageGraphics.h in Headers */, 32D3CDD121DDE87300C4DB49 /* UIImage+MemoryCacheCost.h in Headers */, 328BB6AC2081FEE500760D6C /* SDWebImageCacheSerializer.h in Headers */, - 325F7CCA238942AB00AEDFCC /* NSData+ExtendedData.h in Headers */, + 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedData.h in Headers */, 325C46272233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h in Headers */, 321B378F2083290E00C0EA77 /* SDImageLoadersManager.h in Headers */, 329A185B1FFF5DFD008C9A2F /* UIImage+Metadata.h in Headers */, @@ -1115,7 +1115,7 @@ 3257EAFD21898AED0097B271 /* SDImageGraphics.m in Sources */, 3290FA0C1FA478AF0047D20C /* SDImageFrame.m in Sources */, 325C46232233A02E004CAE11 /* UIColor+HexString.m in Sources */, - 325F7CCB238942AB00AEDFCC /* NSData+ExtendedData.m in Sources */, + 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedData.m in Sources */, 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */, 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9F02355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, @@ -1217,7 +1217,7 @@ 5376130D155AD0D5005750A4 /* SDWebImagePrefetcher.m in Sources */, 328BB6C72082581100760D6C /* SDDiskCache.m in Sources */, 3248475D201775F600AF9E5A /* SDAnimatedImageView.m in Sources */, - 325F7CCC2389463D00AEDFCC /* NSData+ExtendedData.m in Sources */, + 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedData.m in Sources */, 32D1222A2080B2EB003685A3 /* SDImageCachesManager.m in Sources */, 32B9B53D206ED4230026769D /* SDWebImageDownloaderConfig.m in Sources */, 43A9186B1D8308FE00B3925F /* SDImageCacheConfig.m in Sources */, diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index b8075ab2..207deab0 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -14,7 +14,7 @@ #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" -#import "NSData+ExtendedData.h" +#import "UIImage+ExtendedData.h" @interface SDImageCache () @@ -198,6 +198,13 @@ data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil]; } [self _storeImageDataToDisk:data forKey:key]; + if (image) { + // Check extended data + NSData *extendedData = image.sd_extendedData; + if (extendedData) { + [self.diskCache setExtendedData:extendedData forKey:key]; + } + } } if (completionBlock) { @@ -239,10 +246,6 @@ } [self.diskCache setData:imageData forKey:key]; - NSData *extendedData = imageData.sd_extendedData; - if (extendedData) { - [self.diskCache setExtendedData:extendedData forKey:key]; - } } #pragma mark - Query and Retrieve Ops @@ -325,11 +328,6 @@ NSData *data = [self.diskCache dataForKey:key]; if (data) { - // Check extended data - NSData *extendedData = [self.diskCache extendedDataForKey:key]; - if (extendedData) { - data.sd_extendedData = extendedData; - } return data; } @@ -356,6 +354,10 @@ - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context { if (data) { UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context); + if (image) { + // Check extended data + image.sd_extendedData = [self.diskCache extendedDataForKey:key]; + } return image; } else { return nil; diff --git a/SDWebImage/Core/NSData+ExtendedData.h b/SDWebImage/Core/UIImage+ExtendedData.h similarity index 75% rename from SDWebImage/Core/NSData+ExtendedData.h rename to SDWebImage/Core/UIImage+ExtendedData.h index abc80ad4..d7de86cb 100644 --- a/SDWebImage/Core/NSData+ExtendedData.h +++ b/SDWebImage/Core/UIImage+ExtendedData.h @@ -10,10 +10,10 @@ #import #import "SDWebImageCompat.h" -@interface NSData (ExtendedData) +@interface UIImage (ExtendedData) /** - Read and Write the extended data to the image data. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. + Read and Write the extended data and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. The extended data will be write to disk cache as well as the image data. The disk cache preserve both of the data and extended data with the same cache key. */ @property (nonatomic, strong, nullable) NSData *sd_extendedData; diff --git a/SDWebImage/Core/NSData+ExtendedData.m b/SDWebImage/Core/UIImage+ExtendedData.m similarity index 88% rename from SDWebImage/Core/NSData+ExtendedData.m rename to SDWebImage/Core/UIImage+ExtendedData.m index e50863cc..21c79ba7 100644 --- a/SDWebImage/Core/NSData+ExtendedData.m +++ b/SDWebImage/Core/UIImage+ExtendedData.m @@ -7,10 +7,10 @@ * file that was distributed with this source code. */ -#import "NSData+ExtendedData.h" +#import "UIImage+ExtendedData.h" #import -@implementation NSData (ExtendedData) +@implementation UIImage (ExtendedData) - (NSData *)sd_extendedData { return objc_getAssociatedObject(self, @selector(sd_extendedData)); diff --git a/WebImage/SDWebImage.h b/WebImage/SDWebImage.h index 4641b7bd..9d17cae2 100644 --- a/WebImage/SDWebImage.h +++ b/WebImage/SDWebImage.h @@ -46,6 +46,7 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; #import #import #import +#import #import #import #import @@ -67,7 +68,6 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; #import #import #import -#import #import #import #import From 46b0c4bae83103209431b2ec1eddae718a55d6df Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 25 Nov 2019 15:43:31 +0800 Subject: [PATCH 008/181] Use the NSCoding object instead of `NSData`, make it possible to directlly get the extended data from memory cache without unarhive by user. --- SDWebImage.xcodeproj/project.pbxproj | 24 ++++++++++----------- SDWebImage/Core/SDImageCache.m | 16 +++++++++----- SDWebImage/Core/SDWebImageManager.m | 4 ++++ SDWebImage/Core/UIImage+ExtendedCacheData.h | 22 +++++++++++++++++++ SDWebImage/Core/UIImage+ExtendedCacheData.m | 23 ++++++++++++++++++++ SDWebImage/Core/UIImage+ExtendedData.h | 21 ------------------ SDWebImage/Core/UIImage+ExtendedData.m | 23 -------------------- WebImage/SDWebImage.h | 2 +- 8 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 SDWebImage/Core/UIImage+ExtendedCacheData.h create mode 100644 SDWebImage/Core/UIImage+ExtendedCacheData.m delete mode 100644 SDWebImage/Core/UIImage+ExtendedData.h delete mode 100644 SDWebImage/Core/UIImage+ExtendedData.m diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index d70207fd..5312d1be 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -91,10 +91,10 @@ 325C46292233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */; }; - 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */; }; - 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */; }; - 325F7CCD2389467800AEDFCC /* UIImage+ExtendedData.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */; }; + 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedCacheData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */; }; + 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */; }; + 325F7CCD2389467800AEDFCC /* UIImage+ExtendedCacheData.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */; }; 326E2F2E236F0B23006F847F /* SDAnimatedImagePlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 326E2F2F236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; 326E2F30236F0B23006F847F /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */; }; @@ -304,7 +304,7 @@ dstPath = include/SDWebImage; dstSubfolderSpec = 16; files = ( - 325F7CCD2389467800AEDFCC /* UIImage+ExtendedData.h in Copy Headers */, + 325F7CCD2389467800AEDFCC /* UIImage+ExtendedCacheData.h in Copy Headers */, 326E2F36236F1E30006F847F /* SDAnimatedImagePlayer.h in Copy Headers */, 3250C9F12355E3DF0093A896 /* SDWebImageDownloaderDecryptor.h in Copy Headers */, 325427662355783C0042BAA4 /* SDWebImageDownloaderResponseModifier.h in Copy Headers */, @@ -416,8 +416,8 @@ 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBezierPath+RoundedCorners.m"; sourceTree = ""; }; 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+ExtendedAttributes.h"; sourceTree = ""; }; 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+ExtendedAttributes.m"; sourceTree = ""; }; - 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIImage+ExtendedData.h"; path = "Core/UIImage+ExtendedData.h"; sourceTree = ""; }; - 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ExtendedData.m"; path = "Core/UIImage+ExtendedData.m"; sourceTree = ""; }; + 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIImage+ExtendedCacheData.h"; path = "Core/UIImage+ExtendedCacheData.h"; sourceTree = ""; }; + 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ExtendedCacheData.m"; path = "Core/UIImage+ExtendedCacheData.m"; sourceTree = ""; }; 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImagePlayer.h; path = Core/SDAnimatedImagePlayer.h; sourceTree = ""; }; 326E2F2D236F0B23006F847F /* SDAnimatedImagePlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImagePlayer.m; path = Core/SDAnimatedImagePlayer.m; sourceTree = ""; }; 326E2F31236F1D58006F847F /* SDDeviceHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDDeviceHelper.h; sourceTree = ""; }; @@ -757,8 +757,8 @@ children = ( 5D5B9140188EE8DD006D06BD /* NSData+ImageContentType.h */, 5D5B9141188EE8DD006D06BD /* NSData+ImageContentType.m */, - 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedData.h */, - 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedData.m */, + 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */, + 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */, A18A6CC5172DC28500419892 /* UIImage+GIF.h */, A18A6CC6172DC28500419892 /* UIImage+GIF.m */, 329A18571FFF5DFD008C9A2F /* UIImage+Metadata.h */, @@ -864,7 +864,7 @@ 3257EAFA21898AED0097B271 /* SDImageGraphics.h in Headers */, 32D3CDD121DDE87300C4DB49 /* UIImage+MemoryCacheCost.h in Headers */, 328BB6AC2081FEE500760D6C /* SDWebImageCacheSerializer.h in Headers */, - 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedData.h in Headers */, + 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedCacheData.h in Headers */, 325C46272233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h in Headers */, 321B378F2083290E00C0EA77 /* SDImageLoadersManager.h in Headers */, 329A185B1FFF5DFD008C9A2F /* UIImage+Metadata.h in Headers */, @@ -1115,7 +1115,7 @@ 3257EAFD21898AED0097B271 /* SDImageGraphics.m in Sources */, 3290FA0C1FA478AF0047D20C /* SDImageFrame.m in Sources */, 325C46232233A02E004CAE11 /* UIColor+HexString.m in Sources */, - 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedData.m in Sources */, + 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */, 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */, 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9F02355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, @@ -1217,7 +1217,7 @@ 5376130D155AD0D5005750A4 /* SDWebImagePrefetcher.m in Sources */, 328BB6C72082581100760D6C /* SDDiskCache.m in Sources */, 3248475D201775F600AF9E5A /* SDAnimatedImageView.m in Sources */, - 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedData.m in Sources */, + 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */, 32D1222A2080B2EB003685A3 /* SDImageCachesManager.m in Sources */, 32B9B53D206ED4230026769D /* SDWebImageDownloaderConfig.m in Sources */, 43A9186B1D8308FE00B3925F /* SDImageCacheConfig.m in Sources */, diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 207deab0..dd39f0da 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -14,7 +14,7 @@ #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" -#import "UIImage+ExtendedData.h" +#import "UIImage+ExtendedCacheData.h" @interface SDImageCache () @@ -200,9 +200,12 @@ [self _storeImageDataToDisk:data forKey:key]; if (image) { // Check extended data - NSData *extendedData = image.sd_extendedData; - if (extendedData) { - [self.diskCache setExtendedData:extendedData forKey:key]; + id extendedObject = image.sd_extendedObject; + if (extendedObject) { + NSData *extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; + if (extendedData) { + [self.diskCache setExtendedData:extendedData forKey:key]; + } } } } @@ -356,7 +359,10 @@ UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context); if (image) { // Check extended data - image.sd_extendedData = [self.diskCache extendedDataForKey:key]; + NSData *extendedData = [self.diskCache extendedDataForKey:key]; + if (extendedData) { + image.sd_extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData]; + } } return image; } else { diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index dece6dbf..9a727e61 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -10,6 +10,7 @@ #import "SDImageCache.h" #import "SDWebImageDownloader.h" #import "UIImage+Metadata.h" +#import "UIImage+ExtendedCacheData.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" @@ -341,6 +342,9 @@ static id _defaultImageLoader; } else { cacheData = (imageWasTransformed ? nil : downloadedData); } + // keep the original image format and extended data + transformedImage.sd_imageFormat = downloadedImage.sd_imageFormat; + transformedImage.sd_extendedObject = downloadedImage.sd_extendedObject; [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil]; } diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.h b/SDWebImage/Core/UIImage+ExtendedCacheData.h new file mode 100644 index 00000000..6ba220e1 --- /dev/null +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.h @@ -0,0 +1,22 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* (c) Fabrice Aneche +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import +#import "SDWebImageCompat.h" + +@interface UIImage (ExtendedCacheData) + +/** + Read and Write the extended object and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. + The extended object should conforms to NSCoding, which we use `NSKeyedArchiver` and `NSKeyedUnarchiver` to archive it to data, and write to disk cache. + @note The disk cache preserve both of the data and extended data with the same cache key. For manual query, use the `SDDiskCache` protocol method `extendedDataForKey:` instead. + */ +@property (nonatomic, strong, nullable) id sd_extendedObject; + +@end diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.m b/SDWebImage/Core/UIImage+ExtendedCacheData.m new file mode 100644 index 00000000..38b58fc1 --- /dev/null +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.m @@ -0,0 +1,23 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* (c) Fabrice Aneche +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "UIImage+ExtendedCacheData.h" +#import + +@implementation UIImage (ExtendedCacheData) + +- (id)sd_extendedObject { + return objc_getAssociatedObject(self, @selector(sd_extendedObject)); +} + +- (void)setSd_extendedObject:(id)sd_extendedObject { + objc_setAssociatedObject(self, @selector(sd_extendedObject), sd_extendedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +@end diff --git a/SDWebImage/Core/UIImage+ExtendedData.h b/SDWebImage/Core/UIImage+ExtendedData.h deleted file mode 100644 index d7de86cb..00000000 --- a/SDWebImage/Core/UIImage+ExtendedData.h +++ /dev/null @@ -1,21 +0,0 @@ -/* -* This file is part of the SDWebImage package. -* (c) Olivier Poitrey -* (c) Fabrice Aneche -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code. -*/ - -#import -#import "SDWebImageCompat.h" - -@interface UIImage (ExtendedData) - -/** - Read and Write the extended data and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. - The extended data will be write to disk cache as well as the image data. The disk cache preserve both of the data and extended data with the same cache key. - */ -@property (nonatomic, strong, nullable) NSData *sd_extendedData; - -@end diff --git a/SDWebImage/Core/UIImage+ExtendedData.m b/SDWebImage/Core/UIImage+ExtendedData.m deleted file mode 100644 index 21c79ba7..00000000 --- a/SDWebImage/Core/UIImage+ExtendedData.m +++ /dev/null @@ -1,23 +0,0 @@ -/* -* This file is part of the SDWebImage package. -* (c) Olivier Poitrey -* (c) Fabrice Aneche -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code. -*/ - -#import "UIImage+ExtendedData.h" -#import - -@implementation UIImage (ExtendedData) - -- (NSData *)sd_extendedData { - return objc_getAssociatedObject(self, @selector(sd_extendedData)); -} - -- (void)setSd_extendedData:(NSData *)sd_extendedData { - objc_setAssociatedObject(self, @selector(sd_extendedData), sd_extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -@end diff --git a/WebImage/SDWebImage.h b/WebImage/SDWebImage.h index 9d17cae2..ab0f43e5 100644 --- a/WebImage/SDWebImage.h +++ b/WebImage/SDWebImage.h @@ -46,7 +46,7 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; #import #import #import -#import +#import #import #import #import From 9f470954c4c2edfc9ddbbe0758458f61dadba837 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 25 Nov 2019 17:05:27 +0800 Subject: [PATCH 009/181] Change the id into id, to support directlly usage like isKindOfClass --- SDWebImage/Core/SDImageCache.m | 4 ++-- SDWebImage/Core/UIImage+ExtendedCacheData.h | 3 ++- SDWebImage/Core/UIImage+ExtendedCacheData.m | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index dd39f0da..481054bb 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -200,8 +200,8 @@ [self _storeImageDataToDisk:data forKey:key]; if (image) { // Check extended data - id extendedObject = image.sd_extendedObject; - if (extendedObject) { + id extendedObject = image.sd_extendedObject; + if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) { NSData *extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; if (extendedData) { [self.diskCache setExtendedData:extendedData forKey:key]; diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.h b/SDWebImage/Core/UIImage+ExtendedCacheData.h index 6ba220e1..e11508dd 100644 --- a/SDWebImage/Core/UIImage+ExtendedCacheData.h +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.h @@ -16,7 +16,8 @@ Read and Write the extended object and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. The extended object should conforms to NSCoding, which we use `NSKeyedArchiver` and `NSKeyedUnarchiver` to archive it to data, and write to disk cache. @note The disk cache preserve both of the data and extended data with the same cache key. For manual query, use the `SDDiskCache` protocol method `extendedDataForKey:` instead. + @note You can specify arbitrary object conforms to NSCoding (NSObject protocol here is used to support object like `dispatch_data_t`, which is not NSObject subclass). If you load image from disk cache, you should check the extended object class to avoid corrupted data. */ -@property (nonatomic, strong, nullable) id sd_extendedObject; +@property (nonatomic, strong, nullable) id sd_extendedObject; @end diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.m b/SDWebImage/Core/UIImage+ExtendedCacheData.m index 38b58fc1..05d29cff 100644 --- a/SDWebImage/Core/UIImage+ExtendedCacheData.m +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.m @@ -12,11 +12,11 @@ @implementation UIImage (ExtendedCacheData) -- (id)sd_extendedObject { +- (id)sd_extendedObject { return objc_getAssociatedObject(self, @selector(sd_extendedObject)); } -- (void)setSd_extendedObject:(id)sd_extendedObject { +- (void)setSd_extendedObject:(id)sd_extendedObject { objc_setAssociatedObject(self, @selector(sd_extendedObject), sd_extendedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } From 1df1d6a3ce425321d1d4381f20ab2629af0ab509 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 25 Nov 2019 19:57:50 +0800 Subject: [PATCH 010/181] Using the new NSKeyedArchive method on iOS 11+, use try catch on the old fireware to protect runtime crash --- SDWebImage/Core/SDImageCache.m | 33 +++++++++++++++++++-- SDWebImage/Core/UIImage+ExtendedCacheData.h | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 481054bb..362a299b 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -202,7 +202,20 @@ // Check extended data id extendedObject = image.sd_extendedObject; if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) { - NSData *extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; + NSData *extendedData; + if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) { + NSError *error; + extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error]; + if (error) { + NSLog(@"NSKeyedArchiver archive failed with error: %@", error); + } + } else { + @try { + extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; + } @catch (NSException *exception) { + NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception); + } + } if (extendedData) { [self.diskCache setExtendedData:extendedData forKey:key]; } @@ -361,7 +374,23 @@ // Check extended data NSData *extendedData = [self.diskCache extendedDataForKey:key]; if (extendedData) { - image.sd_extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData]; + id extendedObject; + if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) { + NSError *error; + NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:extendedData error:&error]; + unarchiver.requiresSecureCoding = NO; + extendedObject = [unarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error]; + if (error) { + NSLog(@"NSKeyedUnarchiver unarchive failed with error: %@", error); + } + } else { + @try { + extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData]; + } @catch (NSException *exception) { + NSLog(@"NSKeyedUnarchiver unarchive failed with exception: %@", exception); + } + } + image.sd_extendedObject = extendedObject; } } return image; diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.h b/SDWebImage/Core/UIImage+ExtendedCacheData.h index e11508dd..429640e0 100644 --- a/SDWebImage/Core/UIImage+ExtendedCacheData.h +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.h @@ -17,6 +17,7 @@ The extended object should conforms to NSCoding, which we use `NSKeyedArchiver` and `NSKeyedUnarchiver` to archive it to data, and write to disk cache. @note The disk cache preserve both of the data and extended data with the same cache key. For manual query, use the `SDDiskCache` protocol method `extendedDataForKey:` instead. @note You can specify arbitrary object conforms to NSCoding (NSObject protocol here is used to support object like `dispatch_data_t`, which is not NSObject subclass). If you load image from disk cache, you should check the extended object class to avoid corrupted data. + @warning This object don't need to implements NSSecureCoding (but it's recommended), because we allows arbitrary class. */ @property (nonatomic, strong, nullable) id sd_extendedObject; From 9aa4ac1ca729e6de91e10491264f6b54f76ae5d4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 25 Nov 2019 21:53:12 +0800 Subject: [PATCH 011/181] Rename the NSFileManager+ExtendedAttributes into the SDFileAttributeHelper, because it does not have any reply on NSFileManager API --- SDWebImage.xcodeproj/project.pbxproj | 16 ++++++++-------- SDWebImage/Core/SDDiskCache.m | 8 ++++---- ...ndedAttributes.h => SDFileAttributeHelper.h} | 15 +++++++-------- ...ndedAttributes.m => SDFileAttributeHelper.m} | 17 ++++++++--------- 4 files changed, 27 insertions(+), 29 deletions(-) rename SDWebImage/Private/{NSFileManager+ExtendedAttributes.h => SDFileAttributeHelper.h} (50%) rename SDWebImage/Private/{NSFileManager+ExtendedAttributes.m => SDFileAttributeHelper.m} (91%) diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index 5312d1be..9635e935 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -89,8 +89,8 @@ 325C46272233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h in Headers */ = {isa = PBXBuildFile; fileRef = 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */; settings = {ATTRIBUTES = (Private, ); }; }; 325C46282233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; 325C46292233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */; }; - 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */; }; + 325F7CC623893B2E00AEDFCC /* SDFileAttributeHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC423893B2E00AEDFCC /* SDFileAttributeHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 325F7CC723893B2E00AEDFCC /* SDFileAttributeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */; }; 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedCacheData.h in Headers */ = {isa = PBXBuildFile; fileRef = 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */; }; 325F7CCC2389463D00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */; }; @@ -414,8 +414,8 @@ 325C461F2233A02E004CAE11 /* UIColor+HexString.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIColor+HexString.m"; sourceTree = ""; }; 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSBezierPath+RoundedCorners.h"; sourceTree = ""; }; 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBezierPath+RoundedCorners.m"; sourceTree = ""; }; - 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+ExtendedAttributes.h"; sourceTree = ""; }; - 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+ExtendedAttributes.m"; sourceTree = ""; }; + 325F7CC423893B2E00AEDFCC /* SDFileAttributeHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDFileAttributeHelper.h; sourceTree = ""; }; + 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDFileAttributeHelper.m; sourceTree = ""; }; 325F7CC8238942AB00AEDFCC /* UIImage+ExtendedCacheData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIImage+ExtendedCacheData.h"; path = "Core/UIImage+ExtendedCacheData.h"; sourceTree = ""; }; 325F7CC9238942AB00AEDFCC /* UIImage+ExtendedCacheData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ExtendedCacheData.m"; path = "Core/UIImage+ExtendedCacheData.m"; sourceTree = ""; }; 326E2F2C236F0B23006F847F /* SDAnimatedImagePlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImagePlayer.h; path = Core/SDAnimatedImagePlayer.h; sourceTree = ""; }; @@ -646,8 +646,8 @@ 325C461F2233A02E004CAE11 /* UIColor+HexString.m */, 325C46242233A0A8004CAE11 /* NSBezierPath+RoundedCorners.h */, 325C46252233A0A8004CAE11 /* NSBezierPath+RoundedCorners.m */, - 325F7CC423893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h */, - 325F7CC523893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m */, + 325F7CC423893B2E00AEDFCC /* SDFileAttributeHelper.h */, + 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */, 329F123F223FAD3400B309FD /* SDInternalMacros.h */, 329F123E223FAD3400B309FD /* SDInternalMacros.m */, 329F1235223FAA3B00B309FD /* SDmetamacros.h */, @@ -922,7 +922,7 @@ 32C0FDE32013426C001B8F2D /* SDWebImageIndicator.h in Headers */, 32F7C0712030114C00873181 /* SDImageTransformer.h in Headers */, 32E67311235765B500DB4987 /* SDDisplayLink.h in Headers */, - 325F7CC623893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.h in Headers */, + 325F7CC623893B2E00AEDFCC /* SDFileAttributeHelper.h in Headers */, 4A2CAE2D1AB4BB7500B6BC39 /* UIImage+GIF.h in Headers */, 4A2CAE291AB4BB7500B6BC39 /* NSData+ImageContentType.h in Headers */, 328BB69E2081FED200760D6C /* SDWebImageCacheKeyFilter.h in Headers */, @@ -1145,7 +1145,7 @@ 321E609C1F38E8ED00405457 /* SDImageIOCoder.m in Sources */, 4A2CAE261AB4BB7000B6BC39 /* SDWebImagePrefetcher.m in Sources */, 328BB6C92082581100760D6C /* SDDiskCache.m in Sources */, - 325F7CC723893B2E00AEDFCC /* NSFileManager+ExtendedAttributes.m in Sources */, + 325F7CC723893B2E00AEDFCC /* SDFileAttributeHelper.m in Sources */, 3248475F201775F600AF9E5A /* SDAnimatedImageView.m in Sources */, 32D1222C2080B2EB003685A3 /* SDImageCachesManager.m in Sources */, 32B9B53F206ED4230026769D /* SDWebImageDownloaderConfig.m in Sources */, diff --git a/SDWebImage/Core/SDDiskCache.m b/SDWebImage/Core/SDDiskCache.m index dff1f37f..1dd9e10b 100644 --- a/SDWebImage/Core/SDDiskCache.m +++ b/SDWebImage/Core/SDDiskCache.m @@ -8,7 +8,7 @@ #import "SDDiskCache.h" #import "SDImageCacheConfig.h" -#import "NSFileManager+ExtendedAttributes.h" +#import "SDFileAttributeHelper.h" #import static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDiskCache"; @@ -104,7 +104,7 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis // get cache Path for image key NSString *cachePathForKey = [self cachePathForKey:key]; - NSData *extendedData = [self.fileManager extendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + NSData *extendedData = [SDFileAttributeHelper extendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; return extendedData; } @@ -116,10 +116,10 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis if (!extendedData) { // Remove - [self.fileManager removeExtendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + [SDFileAttributeHelper removeExtendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; } else { // Override - [self.fileManager setExtendedAttribute:SDDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; + [SDFileAttributeHelper setExtendedAttribute:SDDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; } } diff --git a/SDWebImage/Private/NSFileManager+ExtendedAttributes.h b/SDWebImage/Private/SDFileAttributeHelper.h similarity index 50% rename from SDWebImage/Private/NSFileManager+ExtendedAttributes.h rename to SDWebImage/Private/SDFileAttributeHelper.h index fef3a6cb..1e66ded7 100644 --- a/SDWebImage/Private/NSFileManager+ExtendedAttributes.h +++ b/SDWebImage/Private/SDFileAttributeHelper.h @@ -1,6 +1,5 @@ // -// NSFileManager+ExtendedAttributes.h -// NSFileManager+ExtendedAttributes +// This file is from https://gist.github.com/zydeco/6292773 // // Created by Jesús A. Álvarez on 2008-12-17. // Copyright 2008-2009 namedfork.net. All rights reserved. @@ -8,12 +7,12 @@ #import -@interface NSFileManager (ExtendedAttributes) +@interface SDFileAttributeHelper : NSObject -- (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -- (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -- (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -- (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err; -- (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; ++ (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; ++ (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; ++ (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; ++ (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err; ++ (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; @end diff --git a/SDWebImage/Private/NSFileManager+ExtendedAttributes.m b/SDWebImage/Private/SDFileAttributeHelper.m similarity index 91% rename from SDWebImage/Private/NSFileManager+ExtendedAttributes.m rename to SDWebImage/Private/SDFileAttributeHelper.m index 2ec60bf4..fcb8ad47 100644 --- a/SDWebImage/Private/NSFileManager+ExtendedAttributes.m +++ b/SDWebImage/Private/SDFileAttributeHelper.m @@ -1,17 +1,16 @@ // -// NSFileManager+ExtendedAttributes.m -// NSFileManager+ExtendedAttributes +// This file is from https://gist.github.com/zydeco/6292773 // // Created by Jesús A. Álvarez on 2008-12-17. // Copyright 2008-2009 namedfork.net. All rights reserved. // -#import "NSFileManager+ExtendedAttributes.h" +#import "SDFileAttributeHelper.h" #import -@implementation NSFileManager (ExtendedAttributes) +@implementation SDFileAttributeHelper -- (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list @@ -40,7 +39,7 @@ return [NSArray arrayWithArray:names]; } -- (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list @@ -68,7 +67,7 @@ return NO; } -- (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get length ssize_t attrLen = getxattr([path fileSystemRepresentation], [name UTF8String], NULL, 0, 0, flags); @@ -91,7 +90,7 @@ return attrData; } -- (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err { ++ (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err { int flags = (follow? 0 : XATTR_NOFOLLOW) | (overwrite? 0 : XATTR_CREATE); if (0 == setxattr([path fileSystemRepresentation], [name UTF8String], [value bytes], [value length], 0, flags)) return YES; // error @@ -109,7 +108,7 @@ return NO; } -- (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { int flags = (follow? 0 : XATTR_NOFOLLOW); if (0 == removexattr([path fileSystemRepresentation], [name UTF8String], flags)) return YES; // error From dd2c5263c8482266023033369c049ac1dd6a7492 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 26 Nov 2019 10:22:35 -0800 Subject: [PATCH 012/181] Support using NSCache delegate with SDMemoryCache default implementation --- SDWebImage/Core/SDMemoryCache.h | 9 +++++---- SDWebImage/Core/SDMemoryCache.m | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/SDWebImage/Core/SDMemoryCache.h b/SDWebImage/Core/SDMemoryCache.h index 8b415816..43c39e84 100644 --- a/SDWebImage/Core/SDMemoryCache.h +++ b/SDWebImage/Core/SDMemoryCache.h @@ -15,6 +15,7 @@ @protocol SDMemoryCache @required + /** Create a new memory cache instance with the specify cache config. You can check `maxMemoryCost` and `maxMemoryCount` used for memory cache. @@ -25,7 +26,7 @@ /** Returns the value associated with a given key. - + @param key An object identifying the value. If nil, just return nil. @return The value associated with key, or nil if no value is associated with key. */ @@ -33,7 +34,7 @@ /** Sets the value of the specified key in the cache (0 cost). - + @param object The object to be stored in the cache. If nil, it calls `removeObjectForKey:`. @param key The key with which to associate the value. If nil, this method has no effect. @discussion Unlike an NSMutableDictionary object, a cache does not copy the key @@ -44,7 +45,7 @@ /** Sets the value of the specified key in the cache, and associates the key-value pair with the specified cost. - + @param object The object to store in the cache. If nil, it calls `removeObjectForKey`. @param key The key with which to associate the value. If nil, this method has no effect. @param cost The cost with which to associate the key-value pair. @@ -55,7 +56,7 @@ /** Removes the value of the specified key in the cache. - + @param key The key identifying the value to be removed. If nil, this method has no effect. */ - (void)removeObjectForKey:(nonnull id)key; diff --git a/SDWebImage/Core/SDMemoryCache.m b/SDWebImage/Core/SDMemoryCache.m index e3991994..b354b495 100644 --- a/SDWebImage/Core/SDMemoryCache.m +++ b/SDWebImage/Core/SDMemoryCache.m @@ -30,6 +30,7 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext; #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif + self.delegate = nil; } - (instancetype)init { @@ -54,14 +55,14 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext; SDImageCacheConfig *config = self.config; self.totalCostLimit = config.maxMemoryCost; self.countLimit = config.maxMemoryCount; - + [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) options:0 context:SDMemoryCacheContext]; [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) options:0 context:SDMemoryCacheContext]; - + #if SD_UIKIT self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; self.weakCacheLock = dispatch_semaphore_create(1); - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification From 5f2a9695d866673bcecde7ca9ba8fcd37a91adbe Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 28 Nov 2019 19:49:51 +0800 Subject: [PATCH 013/181] Add the test case for SDFileAttributeHelper, fix the issue that associated object is lost --- SDWebImage/Core/SDWebImageDefine.m | 2 ++ SDWebImage/Core/SDWebImageManager.m | 1 + SDWebImage/Core/UIImage+ExtendedCacheData.h | 2 +- Tests/Tests/SDUtilsTests.m | 29 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 0b2f2b5f..6231e36d 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -9,6 +9,7 @@ #import "SDWebImageDefine.h" #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" +#import "UIImage+ExtendedCacheData.h" #pragma mark - Image scale @@ -112,6 +113,7 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage * } scaledImage.sd_isIncremental = image.sd_isIncremental; scaledImage.sd_imageFormat = image.sd_imageFormat; + scaledImage.sd_extendedObject = image.sd_extendedObject; return scaledImage; } diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 9a727e61..32b41211 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -343,6 +343,7 @@ static id _defaultImageLoader; cacheData = (imageWasTransformed ? nil : downloadedData); } // keep the original image format and extended data + transformedImage.sd_isIncremental = downloadedImage.sd_isIncremental; transformedImage.sd_imageFormat = downloadedImage.sd_imageFormat; transformedImage.sd_extendedObject = downloadedImage.sd_extendedObject; [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil]; diff --git a/SDWebImage/Core/UIImage+ExtendedCacheData.h b/SDWebImage/Core/UIImage+ExtendedCacheData.h index 429640e0..482c8c40 100644 --- a/SDWebImage/Core/UIImage+ExtendedCacheData.h +++ b/SDWebImage/Core/UIImage+ExtendedCacheData.h @@ -16,7 +16,7 @@ Read and Write the extended object and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. The extended object should conforms to NSCoding, which we use `NSKeyedArchiver` and `NSKeyedUnarchiver` to archive it to data, and write to disk cache. @note The disk cache preserve both of the data and extended data with the same cache key. For manual query, use the `SDDiskCache` protocol method `extendedDataForKey:` instead. - @note You can specify arbitrary object conforms to NSCoding (NSObject protocol here is used to support object like `dispatch_data_t`, which is not NSObject subclass). If you load image from disk cache, you should check the extended object class to avoid corrupted data. + @note You can specify arbitrary object conforms to NSCoding (NSObject protocol here is used to support object using `NS_ROOT_CLASS`, which is not NSObject subclass). If you load image from disk cache, you should check the extended object class to avoid corrupted data. @warning This object don't need to implements NSSecureCoding (but it's recommended), because we allows arbitrary class. */ @property (nonatomic, strong, nullable) id sd_extendedObject; diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index e8b8d471..eafeb5e5 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -11,6 +11,7 @@ #import "SDWeakProxy.h" #import "SDDisplayLink.h" #import "SDInternalMacros.h" +#import "SDFileAttributeHelper.h" @interface SDUtilsTests : SDTestCase @@ -74,6 +75,34 @@ expect(duration).beLessThan(0.02); } +- (void)testSDFileAttributeHelper { + NSData *fileData = [@"File Data" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *extendedData = [@"Extended Data" dataUsingEncoding:NSUTF8StringEncoding]; + NSString *filePath = @"/tmp/file.dat"; + [NSFileManager.defaultManager removeItemAtPath:filePath error:nil]; + [fileData writeToFile:filePath atomically:YES]; + BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:filePath]; + expect(exist).beTruthy(); + + NSArray *names = [SDFileAttributeHelper extendedAttributeNamesAtPath:filePath traverseLink:NO error:nil]; + expect(names.count).equal(0); + + NSString *attr = @"com.com.hackemist.test"; + [SDFileAttributeHelper setExtendedAttribute:@"com.com.hackemist.test" value:extendedData atPath:filePath traverseLink:NO overwrite:YES error:nil]; + + BOOL hasAttr =[SDFileAttributeHelper hasExtendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; + expect(hasAttr).beTruthy(); + + NSData *queriedData = [SDFileAttributeHelper extendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; + expect(extendedData).equal(queriedData); + + BOOL removed = [SDFileAttributeHelper removeExtendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; + expect(removed).beTruthy(); + + hasAttr = [SDFileAttributeHelper hasExtendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; + expect(hasAttr).beFalsy(); +} + - (void)testSDScaledImageForKey { // Test nil expect(SDScaledImageForKey(nil, nil)).beNil(); From 5c1351a2fdc3bfc054efb63d28a397c5c2813e6f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 28 Nov 2019 20:54:25 +0800 Subject: [PATCH 014/181] Added `test47DiskCacheExtendedData` test case --- Tests/Tests/SDImageCacheTests.m | 22 ++++++++++++++++++++++ Tests/Tests/SDUtilsTests.m | 8 ++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index 3e54963e..516a7f36 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -491,6 +491,28 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; } #endif +- (void)test47DiskCacheExtendedData { + XCTestExpectation *expectation = [self expectationWithDescription:@"SDImageCache extended data read/write works"]; + UIImage *image = [self testPNGImage]; + NSDictionary *extendedObject = @{@"Test" : @"Object"}; + image.sd_extendedObject = extendedObject; + [SDImageCache.sharedImageCache removeImageFromMemoryForKey:kTestImageKeyPNG]; + [SDImageCache.sharedImageCache removeImageFromDiskForKey:kTestImageKeyPNG]; + // Write extended data + [SDImageCache.sharedImageCache storeImage:image forKey:kTestImageKeyPNG completion:^{ + NSData *extendedData = [SDImageCache.sharedImageCache.diskCache extendedDataForKey:kTestImageKeyPNG]; + expect(extendedData).toNot.beNil(); + // Read extended data + UIImage *newImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kTestImageKeyPNG]; + id newExtendedObject = newImage.sd_extendedObject; + expect(extendedObject).equal(newExtendedObject); + // Remove extended data + [SDImageCache.sharedImageCache.diskCache setExtendedData:nil forKey:kTestImageKeyPNG]; + [expectation fulfill]; + }]; + [self waitForExpectationsWithCommonTimeout]; +} + #pragma mark - SDImageCache & SDImageCachesManager - (void)test50SDImageCacheQueryOp { XCTestExpectation *expectation = [self expectationWithDescription:@"SDImageCache query op works"]; diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index eafeb5e5..89012e20 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -87,12 +87,16 @@ NSArray *names = [SDFileAttributeHelper extendedAttributeNamesAtPath:filePath traverseLink:NO error:nil]; expect(names.count).equal(0); - NSString *attr = @"com.com.hackemist.test"; - [SDFileAttributeHelper setExtendedAttribute:@"com.com.hackemist.test" value:extendedData atPath:filePath traverseLink:NO overwrite:YES error:nil]; + NSString *attr = @"com.hackemist.test"; + [SDFileAttributeHelper setExtendedAttribute:attr value:extendedData atPath:filePath traverseLink:NO overwrite:YES error:nil]; BOOL hasAttr =[SDFileAttributeHelper hasExtendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; expect(hasAttr).beTruthy(); + names = [SDFileAttributeHelper extendedAttributeNamesAtPath:filePath traverseLink:NO error:nil]; + expect(names.count).equal(1); + expect(names.firstObject).equal(attr); + NSData *queriedData = [SDFileAttributeHelper extendedAttribute:attr atPath:filePath traverseLink:NO error:nil]; expect(extendedData).equal(queriedData); From e37d07bda8957d0b86604c26a462db2ed8643fd6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 29 Nov 2019 12:48:36 +0800 Subject: [PATCH 015/181] Add `SDWebImageWaitStoreCache`, which wait for all the async disk cache written finished and then callback, useful for advanced user who want to touch the cache right in completion block --- SDWebImage/Core/SDWebImageDefine.h | 8 +++ SDWebImage/Core/SDWebImageManager.m | 84 ++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 8e60469b..a9a34367 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -184,6 +184,14 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { * Note this options is not compatible with `SDWebImageDecodeFirstFrameOnly`, which always produce a UIImage/NSImage. */ SDWebImageMatchAnimatedImageClass = 1 << 21, + + /** + * By default, when we load the image from network, the image will be written to the cache (memory and disk, controlled by your `storeCacheType` context option) + * This maybe an asynchronously operation and the final `SDInternalCompletionBlock` callback does not gurantee the disk cache written is finished and may cause logic error. (For example, you modify the disk data just in completion block, however, the disk cache is not ready) + * If you need to process with the disk cache in the completion block, you should use this option to ensure the disk cache already been written when callback. + * Note if you use this when using the custom cache serializer, or using the transformer, we will also wait until the output image data written is finished. + */ + SDWebImageWaitStoreCache = 1 << 22, }; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 32b41211..f51f2ca9 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -265,7 +265,7 @@ static id _defaultImageLoader; [self.failedURLs removeObject:url]; SD_UNLOCK(self.failedURLsLock); } - + // Continue store cache process [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; } @@ -310,6 +310,7 @@ static id _defaultImageLoader; BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; BOOL shouldCacheOriginal = downloadedImage && finished; + BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store original image to cache if (shouldCacheOriginal) { @@ -319,41 +320,75 @@ static id _defaultImageLoader; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url]; - [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType completion:nil]; + [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ + // Continue transform process + [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; + }]; } }); } else { - [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType completion:nil]; + [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ + // Continue transform process + [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; + }]; } + } else { + // Continue transform process + [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; } +} + +// Transform process +- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation + url:(nonnull NSURL *)url + options:(SDWebImageOptions)options + context:(SDWebImageContext *)context + originalImage:(nullable UIImage *)originalImage + originalData:(nullable NSData *)originalData + finished:(BOOL)finished + progress:(nullable SDImageLoaderProgressBlock)progressBlock + completed:(nullable SDInternalCompletionBlock)completedBlock { + // the target image store cache type + SDImageCacheType storeCacheType = SDImageCacheTypeAll; + if (context[SDWebImageContextStoreCacheType]) { + storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; + } + id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + id transformer = context[SDWebImageContextImageTransformer]; + id cacheSerializer = context[SDWebImageContextCacheSerializer]; + BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; + BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store transformed image to cache if (shouldTransformImage) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { - UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key]; + UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key]; if (transformedImage && finished) { NSString *transformerKey = [transformer transformerKey]; NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey); - BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; + BOOL imageWasTransformed = ![transformedImage isEqual:originalImage]; NSData *cacheData; // pass nil if the image was transformed, so we can recalculate the data from the image if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) { - cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url]; + cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url]; } else { - cacheData = (imageWasTransformed ? nil : downloadedData); + cacheData = (imageWasTransformed ? nil : originalData); } // keep the original image format and extended data - transformedImage.sd_isIncremental = downloadedImage.sd_isIncremental; - transformedImage.sd_imageFormat = downloadedImage.sd_imageFormat; - transformedImage.sd_extendedObject = downloadedImage.sd_extendedObject; - [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil]; + transformedImage.sd_isIncremental = originalImage.sd_isIncremental; + transformedImage.sd_imageFormat = originalImage.sd_imageFormat; + transformedImage.sd_extendedObject = originalImage.sd_extendedObject; + [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType waitStoreCache:waitStoreCache completion:^{ + [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; + }]; + } else { + [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } - - [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } }); } else { - [self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; + [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } } @@ -368,6 +403,27 @@ static id _defaultImageLoader; SD_UNLOCK(self.runningOperationsLock); } +- (void)storeImage:(nullable UIImage *)image + imageData:(nullable NSData *)data + forKey:(nullable NSString *)key + cacheType:(SDImageCacheType)cacheType + waitStoreCache:(BOOL)waitStoreCache + completion:(nullable SDWebImageNoParamsBlock)completion { + // Check whether we should wait the store cache finished. If not, callback immediately + [self.imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{ + if (waitStoreCache) { + if (completion) { + completion(); + } + } + }]; + if (!waitStoreCache) { + if (completion) { + completion(); + } + } +} + - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation completion:(nullable SDInternalCompletionBlock)completionBlock error:(nullable NSError *)error From 9f6422b506b377fe35d36c2d81cf81613d86ac7e Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 26 Nov 2019 10:22:35 -0800 Subject: [PATCH 016/181] Support using NSCache delegate with SDMemoryCache default implementation --- SDWebImage/Core/SDMemoryCache.h | 9 +++++---- SDWebImage/Core/SDMemoryCache.m | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/SDWebImage/Core/SDMemoryCache.h b/SDWebImage/Core/SDMemoryCache.h index 8b415816..43c39e84 100644 --- a/SDWebImage/Core/SDMemoryCache.h +++ b/SDWebImage/Core/SDMemoryCache.h @@ -15,6 +15,7 @@ @protocol SDMemoryCache @required + /** Create a new memory cache instance with the specify cache config. You can check `maxMemoryCost` and `maxMemoryCount` used for memory cache. @@ -25,7 +26,7 @@ /** Returns the value associated with a given key. - + @param key An object identifying the value. If nil, just return nil. @return The value associated with key, or nil if no value is associated with key. */ @@ -33,7 +34,7 @@ /** Sets the value of the specified key in the cache (0 cost). - + @param object The object to be stored in the cache. If nil, it calls `removeObjectForKey:`. @param key The key with which to associate the value. If nil, this method has no effect. @discussion Unlike an NSMutableDictionary object, a cache does not copy the key @@ -44,7 +45,7 @@ /** Sets the value of the specified key in the cache, and associates the key-value pair with the specified cost. - + @param object The object to store in the cache. If nil, it calls `removeObjectForKey`. @param key The key with which to associate the value. If nil, this method has no effect. @param cost The cost with which to associate the key-value pair. @@ -55,7 +56,7 @@ /** Removes the value of the specified key in the cache. - + @param key The key identifying the value to be removed. If nil, this method has no effect. */ - (void)removeObjectForKey:(nonnull id)key; diff --git a/SDWebImage/Core/SDMemoryCache.m b/SDWebImage/Core/SDMemoryCache.m index e3991994..b354b495 100644 --- a/SDWebImage/Core/SDMemoryCache.m +++ b/SDWebImage/Core/SDMemoryCache.m @@ -30,6 +30,7 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext; #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif + self.delegate = nil; } - (instancetype)init { @@ -54,14 +55,14 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext; SDImageCacheConfig *config = self.config; self.totalCostLimit = config.maxMemoryCost; self.countLimit = config.maxMemoryCount; - + [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) options:0 context:SDMemoryCacheContext]; [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) options:0 context:SDMemoryCacheContext]; - + #if SD_UIKIT self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; self.weakCacheLock = dispatch_semaphore_create(1); - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification From 1f801b44effa48efe72deab39cdb1140935901d2 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 3 Dec 2019 19:40:36 +0800 Subject: [PATCH 017/181] Bumped version to 5.3.3 Update the CHANGELOG --- CHANGELOG.md | 6 ++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea18fcfb..6d2a9021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [5.3 Patch, on Dec 3rd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.3) +See [all tickets marked for the 5.3.3 release](https://github.com/SDWebImage/SDWebImage/milestone/54) + +### Fixes +- Fix the crash when using NSCache delegate with SDMemoryCache default implementation on dealloc #2899 + ## [5.3 Patch, on Nov 22nd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.2) See [all tickets marked for the 5.3.2 release](https://github.com/SDWebImage/SDWebImage/milestone/53) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index e6c81be4..80187e75 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.3.2' + s.version = '5.3.3' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 92626e2a..62181bd0 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.3.2 + 5.3.3 CFBundleSignature ???? CFBundleVersion - 5.3.2 + 5.3.3 NSPrincipalClass From 936d04f726d012f63fb397856d9aa02bdfa681c0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 3 Dec 2019 20:42:46 +0800 Subject: [PATCH 018/181] Fix the build issue on SDWebImage Static library target --- SDWebImage.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index 9635e935..1020e639 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */ = {isa = PBXBuildFile; fileRef = 321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */; }; 3237F9E820161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; 3237F9EB20161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; + 3240BB6523968FA1003BA07D /* SDFileAttributeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */; }; 3244062C2296C5F400A36084 /* SDWebImageOptionsProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3244062D2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; @@ -1190,6 +1191,7 @@ 321E60C41F38E91700405457 /* UIImage+ForceDecode.m in Sources */, 3244062D2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9EF2355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, + 3240BB6523968FA1003BA07D /* SDFileAttributeHelper.m in Sources */, 328BB6A22081FED200760D6C /* SDWebImageCacheKeyFilter.m in Sources */, 32E67312235765B500DB4987 /* SDDisplayLink.m in Sources */, 53761309155AD0D5005750A4 /* SDImageCache.m in Sources */, From 69d163fc37216bd704ef61df8b0bc485404d1ffc Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 3 Dec 2019 21:20:20 +0800 Subject: [PATCH 019/181] Using one global function to ensure we always sync all the UIImage category assocaited object status correctly inside our framework --- SDWebImage.xcodeproj/project.pbxproj | 10 +++++++++ SDWebImage/Core/SDImageCoderHelper.m | 6 ++--- SDWebImage/Core/SDWebImageDefine.m | 5 ++--- SDWebImage/Core/SDWebImageManager.m | 6 ++--- SDWebImage/Private/SDAssociatedObject.h | 14 ++++++++++++ SDWebImage/Private/SDAssociatedObject.m | 29 +++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 SDWebImage/Private/SDAssociatedObject.h create mode 100644 SDWebImage/Private/SDAssociatedObject.m diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index 1020e639..fefb9564 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -46,6 +46,9 @@ 3237F9E820161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; 3237F9EB20161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; 3240BB6523968FA1003BA07D /* SDFileAttributeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */; }; + 3240BB6823968FE7003BA07D /* SDAssociatedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 3240BB6623968FE6003BA07D /* SDAssociatedObject.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3240BB6923968FE7003BA07D /* SDAssociatedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */; }; + 3240BB6A23968FE7003BA07D /* SDAssociatedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */; }; 3244062C2296C5F400A36084 /* SDWebImageOptionsProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3244062D2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; @@ -387,6 +390,8 @@ 321E60A11F38E8F600405457 /* SDImageGIFCoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDImageGIFCoder.m; path = Core/SDImageGIFCoder.m; sourceTree = ""; }; 321E60BC1F38E91700405457 /* UIImage+ForceDecode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+ForceDecode.h"; path = "Core/UIImage+ForceDecode.h"; sourceTree = ""; }; 321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ForceDecode.m"; path = "Core/UIImage+ForceDecode.m"; sourceTree = ""; }; + 3240BB6623968FE6003BA07D /* SDAssociatedObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDAssociatedObject.h; sourceTree = ""; }; + 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAssociatedObject.m; sourceTree = ""; }; 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDWebImageOptionsProcessor.h; path = Core/SDWebImageOptionsProcessor.h; sourceTree = ""; }; 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDWebImageOptionsProcessor.m; path = Core/SDWebImageOptionsProcessor.m; sourceTree = ""; }; 32484757201775F600AF9E5A /* SDAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImageView.m; path = Core/SDAnimatedImageView.m; sourceTree = ""; }; @@ -631,6 +636,8 @@ children = ( 32B5CC5E222F89C2005EB74E /* SDAsyncBlockOperation.h */, 32B5CC5F222F89C2005EB74E /* SDAsyncBlockOperation.m */, + 3240BB6623968FE6003BA07D /* SDAssociatedObject.h */, + 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */, 325C460622339426004CAE11 /* SDWeakProxy.h */, 325C460722339426004CAE11 /* SDWeakProxy.m */, 32E6730F235765B500DB4987 /* SDDisplayLink.h */, @@ -886,6 +893,7 @@ 326E2F2E236F0B23006F847F /* SDAnimatedImagePlayer.h in Headers */, 807A122A1F89636300EC2A9B /* SDImageCodersManager.h in Headers */, 3244062C2296C5F400A36084 /* SDWebImageOptionsProcessor.h in Headers */, + 3240BB6823968FE7003BA07D /* SDAssociatedObject.h in Headers */, 4A2CAE211AB4BB7000B6BC39 /* SDWebImageManager.h in Headers */, 4A2CAE1F1AB4BB6C00B6BC39 /* SDImageCache.h in Headers */, 4A2CAE351AB4BB7500B6BC39 /* UIImageView+WebCache.h in Headers */, @@ -1124,6 +1132,7 @@ 32E67313235765B500DB4987 /* SDDisplayLink.m in Sources */, 4A2CAE2E1AB4BB7500B6BC39 /* UIImage+GIF.m in Sources */, 326E2F35236F1D58006F847F /* SDDeviceHelper.m in Sources */, + 3240BB6A23968FE7003BA07D /* SDAssociatedObject.m in Sources */, 80B6DF822142B44400BCB334 /* NSButton+WebCache.m in Sources */, 32D3CDCF21DDE87300C4DB49 /* UIImage+MemoryCacheCost.m in Sources */, 329F1241223FAD3400B309FD /* SDInternalMacros.m in Sources */, @@ -1196,6 +1205,7 @@ 32E67312235765B500DB4987 /* SDDisplayLink.m in Sources */, 53761309155AD0D5005750A4 /* SDImageCache.m in Sources */, 326E2F34236F1D58006F847F /* SDDeviceHelper.m in Sources */, + 3240BB6923968FE7003BA07D /* SDAssociatedObject.m in Sources */, 80B6DF832142B44500BCB334 /* NSButton+WebCache.m in Sources */, 32D3CDCE21DDE87300C4DB49 /* UIImage+MemoryCacheCost.m in Sources */, 329F1240223FAD3400B309FD /* SDInternalMacros.m in Sources */, diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 52ab2ea3..3cc0c7ea 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -12,7 +12,7 @@ #import "NSData+ImageContentType.h" #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" -#import "UIImage+Metadata.h" +#import "SDAssociatedObject.h" #if SD_UIKIT || SD_WATCH static const size_t kBytesPerPixel = 4; @@ -291,8 +291,8 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation]; CGImageRelease(imageRef); + SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; - decodedImage.sd_imageFormat = image.sd_imageFormat; return decodedImage; #endif } @@ -425,8 +425,8 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (destImage == nil) { return image; } + SDImageCopyAssociatedObject(image, destImage); destImage.sd_isDecoded = YES; - destImage.sd_imageFormat = image.sd_imageFormat; return destImage; } #endif diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 6231e36d..173f092a 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -10,6 +10,7 @@ #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" #import "UIImage+ExtendedCacheData.h" +#import "SDAssociatedObject.h" #pragma mark - Image scale @@ -111,9 +112,7 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage * scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:kCGImagePropertyOrientationUp]; #endif } - scaledImage.sd_isIncremental = image.sd_isIncremental; - scaledImage.sd_imageFormat = image.sd_imageFormat; - scaledImage.sd_extendedObject = image.sd_extendedObject; + SDImageCopyAssociatedObject(image, scaledImage); return scaledImage; } diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index f51f2ca9..61c0a36c 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -10,7 +10,7 @@ #import "SDImageCache.h" #import "SDWebImageDownloader.h" #import "UIImage+Metadata.h" -#import "UIImage+ExtendedCacheData.h" +#import "SDAssociatedObject.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" @@ -376,9 +376,7 @@ static id _defaultImageLoader; cacheData = (imageWasTransformed ? nil : originalData); } // keep the original image format and extended data - transformedImage.sd_isIncremental = originalImage.sd_isIncremental; - transformedImage.sd_imageFormat = originalImage.sd_imageFormat; - transformedImage.sd_extendedObject = originalImage.sd_extendedObject; + SDImageCopyAssociatedObject(originalImage, transformedImage); [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType waitStoreCache:waitStoreCache completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; }]; diff --git a/SDWebImage/Private/SDAssociatedObject.h b/SDWebImage/Private/SDAssociatedObject.h new file mode 100644 index 00000000..199cf4fc --- /dev/null +++ b/SDWebImage/Private/SDAssociatedObject.h @@ -0,0 +1,14 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "SDWebImageCompat.h" + +/// Copy the associated object from source image to target image. The associated object including all the category read/write properties. +/// @param source source +/// @param target target +FOUNDATION_EXPORT void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable target); diff --git a/SDWebImage/Private/SDAssociatedObject.m b/SDWebImage/Private/SDAssociatedObject.m new file mode 100644 index 00000000..18355fed --- /dev/null +++ b/SDWebImage/Private/SDAssociatedObject.m @@ -0,0 +1,29 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "SDAssociatedObject.h" +#import "UIImage+Metadata.h" +#import "UIImage+ExtendedCacheData.h" +#import "UIImage+MemoryCacheCost.h" +#import "UIImage+ForceDecode.h" + +void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable target) { + if (!source || !target) { + return; + } + // Image Metadata + target.sd_isIncremental = source.sd_isIncremental; + target.sd_imageLoopCount = source.sd_imageLoopCount; + target.sd_imageFormat = source.sd_imageFormat; + // Force Decode + target.sd_isDecoded = source.sd_isDecoded; + // Extended Cache Data + target.sd_extendedObject = source.sd_extendedObject; + // Memory Cache Cost + target.sd_memoryCost = source.sd_memoryCost; +} From 7f0789aca9e9a92bddd0900a212f43ec16b1024b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 4 Dec 2019 16:18:52 +0800 Subject: [PATCH 020/181] memory cost should not be copied between different UIImage, it's a getter-only method in general --- SDWebImage/Private/SDAssociatedObject.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/SDWebImage/Private/SDAssociatedObject.m b/SDWebImage/Private/SDAssociatedObject.m index 18355fed..a7c70763 100644 --- a/SDWebImage/Private/SDAssociatedObject.m +++ b/SDWebImage/Private/SDAssociatedObject.m @@ -24,6 +24,4 @@ void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable target.sd_isDecoded = source.sd_isDecoded; // Extended Cache Data target.sd_extendedObject = source.sd_extendedObject; - // Memory Cache Cost - target.sd_memoryCost = source.sd_memoryCost; } From bc9b488bf34b1077af990f5ee312b68845e6d6c3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Dec 2019 19:26:18 +0800 Subject: [PATCH 021/181] Bumped version to 5.4.0 Update the CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2a9021..69558fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [5.4.0 Extended Cache Metadata, on Dec 5th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.0) +See [all tickets marked for the 5.4.0 release](https://github.com/SDWebImage/SDWebImage/milestone/51) + +### Features + +#### Cache +- Allows advanced user to read/write extended metadata associated with image data from disk cache #2898 +- This metadata will be processed at the same time when store or query the image. The metadata should conforms to `NSCoding` for archive and unarchive. + +#### Manager +- Add `SDWebImageWaitStoreCache`, which wait for all the async disk cache written finished and then callback, useful for advanced user who want to touch the cache right in completion block #2900 + +### Fixes +- Using one global function to ensure we always sync all the UIImage category associated object status correctly inside our framework #2902 + ## [5.3 Patch, on Dec 3rd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.3) See [all tickets marked for the 5.3.3 release](https://github.com/SDWebImage/SDWebImage/milestone/54) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 80187e75..6e7c4229 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.3.3' + s.version = '5.4.0' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 62181bd0..7c51b93f 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.3.3 + 5.4.0 CFBundleSignature ???? CFBundleVersion - 5.3.3 + 5.4.0 NSPrincipalClass From 9dae0e7b961ba5c6dda545ec2c6a40733babdd79 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Dec 2019 19:33:10 +0800 Subject: [PATCH 022/181] Fix the thread safe issue with Downloader and DownloaderOperation during cancel --- SDWebImage/Core/SDWebImageDownloader.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 80d30b59..5b931ea2 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -403,7 +403,12 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; NSOperation *returnOperation = nil; for (NSOperation *operation in self.downloadQueue.operations) { if ([operation respondsToSelector:@selector(dataTask)]) { - if (operation.dataTask.taskIdentifier == task.taskIdentifier) { + // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. + NSURLSessionTask *operationTask; + @synchronized (operation) { + operationTask = operation.dataTask; + } + if (operationTask.taskIdentifier == task.taskIdentifier) { returnOperation = operation; break; } From 1601418d51e947cf4f8a9884820e8114eb334a43 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Dec 2019 19:40:45 +0800 Subject: [PATCH 023/181] Remove one unused import --- SDWebImage/Core/SDWebImageDefine.m | 1 - 1 file changed, 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 173f092a..496392c4 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -9,7 +9,6 @@ #import "SDWebImageDefine.h" #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" -#import "UIImage+ExtendedCacheData.h" #import "SDAssociatedObject.h" #pragma mark - Image scale From 7ef9a314b12c1a31edb0d09d41fcba93143fe772 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 6 Dec 2019 15:44:40 +0800 Subject: [PATCH 024/181] Update the CHANGELOG for 5.4.0 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69558fea..ed842941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [5.4.0 Extended Cache Metadata, on Dec 5th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.0) +## [5.4.0 Extended Cache Metadata, on Dec 6th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.0) See [all tickets marked for the 5.4.0 release](https://github.com/SDWebImage/SDWebImage/milestone/51) ### Features @@ -12,6 +12,7 @@ See [all tickets marked for the 5.4.0 release](https://github.com/SDWebImage/SDW ### Fixes - Using one global function to ensure we always sync all the UIImage category associated object status correctly inside our framework #2902 +- Fix the thread safe issue with Downloader and DownloaderOperation during cancel #2903 ## [5.3 Patch, on Dec 3rd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.3) See [all tickets marked for the 5.3.3 release](https://github.com/SDWebImage/SDWebImage/milestone/54) From 5cf8ddc6cb0d2fda77b9bad760650808fd88d7c2 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 13 Dec 2019 17:26:20 +0800 Subject: [PATCH 025/181] update the Readme with PDF/SVG/Link plugin repos from SDWebImage organization --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6199e699..f18658fb 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,13 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format - [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format +- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format image +- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format image - and more from community! #### Loaders - [SDWebImagePhotosPlugin](https://github.com/SDWebImage/SDWebImagePhotosPlugin) - plugin to support loading images from Photos (using `Photos.framework`) +- [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs From 0b120584274741fd0b297f012783e2061ba088af Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 16 Dec 2019 12:44:52 +0800 Subject: [PATCH 026/181] Fix the test case compile warning of `SDWebImageTestDiskCache` --- Tests/Tests/SDWebImageTestCache.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/Tests/SDWebImageTestCache.m b/Tests/Tests/SDWebImageTestCache.m index 7ef29cf7..68b2a154 100644 --- a/Tests/Tests/SDWebImageTestCache.m +++ b/Tests/Tests/SDWebImageTestCache.m @@ -9,6 +9,9 @@ #import "SDWebImageTestCache.h" #import +#import "SDFileAttributeHelper.h" + +static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hackemist.SDWebImageTestDiskCache"; @implementation SDWebImageTestMemoryCache @@ -104,4 +107,18 @@ return size; } +- (nullable NSData *)extendedDataForKey:(nonnull NSString *)key { + NSString *cachePathForKey = [self cachePathForKey:key]; + return [SDFileAttributeHelper extendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; +} + +- (void)setExtendedData:(nullable NSData *)extendedData forKey:(nonnull NSString *)key { + NSString *cachePathForKey = [self cachePathForKey:key]; + if (!extendedData) { + [SDFileAttributeHelper removeExtendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + } else { + [SDFileAttributeHelper setExtendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; + } +} + @end From 6ff83fde6bba99491a78096cdbc8ac5711f4666b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 15:54:15 +0800 Subject: [PATCH 027/181] Added SDGraphicsImageRenderer (which bridge to UIGraphicsImageRenderer on iOS 10+), prepare to replace old CGContext create code --- SDWebImage.xcodeproj/project.pbxproj | 12 ++ SDWebImage/Core/SDGraphicsImageRenderer.h | 44 +++++++ SDWebImage/Core/SDGraphicsImageRenderer.m | 140 ++++++++++++++++++++++ WebImage/SDWebImage.h | 1 + 4 files changed, 197 insertions(+) create mode 100644 SDWebImage/Core/SDGraphicsImageRenderer.h create mode 100644 SDWebImage/Core/SDGraphicsImageRenderer.m diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index fefb9564..39537274 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -52,6 +52,9 @@ 3244062C2296C5F400A36084 /* SDWebImageOptionsProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3244062D2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */; }; + 3246A70323A567AC00FBEA10 /* SDGraphicsImageRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 3246A70123A567AC00FBEA10 /* SDGraphicsImageRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3246A70423A567AC00FBEA10 /* SDGraphicsImageRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3246A70223A567AC00FBEA10 /* SDGraphicsImageRenderer.m */; }; + 3246A70523A567AC00FBEA10 /* SDGraphicsImageRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3246A70223A567AC00FBEA10 /* SDGraphicsImageRenderer.m */; }; 3248475D201775F600AF9E5A /* SDAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 32484757201775F600AF9E5A /* SDAnimatedImageView.m */; }; 3248475F201775F600AF9E5A /* SDAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 32484757201775F600AF9E5A /* SDAnimatedImageView.m */; }; 32484765201775F600AF9E5A /* SDAnimatedImageView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 32484758201775F600AF9E5A /* SDAnimatedImageView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -121,6 +124,7 @@ 328BB6CF2082581100760D6C /* SDMemoryCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 328BB6BF2082581100760D6C /* SDMemoryCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 328BB6D32082581100760D6C /* SDMemoryCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 328BB6C02082581100760D6C /* SDMemoryCache.m */; }; 328BB6D52082581100760D6C /* SDMemoryCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 328BB6C02082581100760D6C /* SDMemoryCache.m */; }; + 328E9DE523A61DD30051C893 /* SDGraphicsImageRenderer.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3246A70123A567AC00FBEA10 /* SDGraphicsImageRenderer.h */; }; 3290FA061FA478AF0047D20C /* SDImageFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 3290FA021FA478AF0047D20C /* SDImageFrame.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3290FA0A1FA478AF0047D20C /* SDImageFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 3290FA031FA478AF0047D20C /* SDImageFrame.m */; }; 3290FA0C1FA478AF0047D20C /* SDImageFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 3290FA031FA478AF0047D20C /* SDImageFrame.m */; }; @@ -308,6 +312,7 @@ dstPath = include/SDWebImage; dstSubfolderSpec = 16; files = ( + 328E9DE523A61DD30051C893 /* SDGraphicsImageRenderer.h in Copy Headers */, 325F7CCD2389467800AEDFCC /* UIImage+ExtendedCacheData.h in Copy Headers */, 326E2F36236F1E30006F847F /* SDAnimatedImagePlayer.h in Copy Headers */, 3250C9F12355E3DF0093A896 /* SDWebImageDownloaderDecryptor.h in Copy Headers */, @@ -394,6 +399,8 @@ 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAssociatedObject.m; sourceTree = ""; }; 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDWebImageOptionsProcessor.h; path = Core/SDWebImageOptionsProcessor.h; sourceTree = ""; }; 3244062A2296C5F400A36084 /* SDWebImageOptionsProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDWebImageOptionsProcessor.m; path = Core/SDWebImageOptionsProcessor.m; sourceTree = ""; }; + 3246A70123A567AC00FBEA10 /* SDGraphicsImageRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDGraphicsImageRenderer.h; path = Core/SDGraphicsImageRenderer.h; sourceTree = ""; }; + 3246A70223A567AC00FBEA10 /* SDGraphicsImageRenderer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SDGraphicsImageRenderer.m; path = Core/SDGraphicsImageRenderer.m; sourceTree = ""; }; 32484757201775F600AF9E5A /* SDAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImageView.m; path = Core/SDAnimatedImageView.m; sourceTree = ""; }; 32484758201775F600AF9E5A /* SDAnimatedImageView+WebCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SDAnimatedImageView+WebCache.h"; path = "Core/SDAnimatedImageView+WebCache.h"; sourceTree = ""; }; 32484759201775F600AF9E5A /* SDAnimatedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImageView.h; path = Core/SDAnimatedImageView.h; sourceTree = ""; }; @@ -577,6 +584,8 @@ 32CF1C061FA496B000004BD1 /* SDImageCoderHelper.m */, 3257EAF721898AED0097B271 /* SDImageGraphics.h */, 3257EAF821898AED0097B271 /* SDImageGraphics.m */, + 3246A70123A567AC00FBEA10 /* SDGraphicsImageRenderer.h */, + 3246A70223A567AC00FBEA10 /* SDGraphicsImageRenderer.m */, ); name = Decoder; sourceTree = ""; @@ -905,6 +914,7 @@ 4A2CAE1D1AB4BB6800B6BC39 /* SDWebImageDownloaderOperation.h in Headers */, 4A2CAE2B1AB4BB7500B6BC39 /* UIButton+WebCache.h in Headers */, 4A2CAE251AB4BB7000B6BC39 /* SDWebImagePrefetcher.h in Headers */, + 3246A70323A567AC00FBEA10 /* SDGraphicsImageRenderer.h in Headers */, 328BB6CF2082581100760D6C /* SDMemoryCache.h in Headers */, 325C460F223394D8004CAE11 /* SDImageCachesManagerOperation.h in Headers */, 321E60881F38E8C800405457 /* SDImageCoder.h in Headers */, @@ -1125,6 +1135,7 @@ 3290FA0C1FA478AF0047D20C /* SDImageFrame.m in Sources */, 325C46232233A02E004CAE11 /* UIColor+HexString.m in Sources */, 325F7CCB238942AB00AEDFCC /* UIImage+ExtendedCacheData.m in Sources */, + 3246A70523A567AC00FBEA10 /* SDGraphicsImageRenderer.m in Sources */, 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */, 3244062E2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9F02355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, @@ -1198,6 +1209,7 @@ 3290FA0A1FA478AF0047D20C /* SDImageFrame.m in Sources */, 325C46222233A02E004CAE11 /* UIColor+HexString.m in Sources */, 321E60C41F38E91700405457 /* UIImage+ForceDecode.m in Sources */, + 3246A70423A567AC00FBEA10 /* SDGraphicsImageRenderer.m in Sources */, 3244062D2296C5F400A36084 /* SDWebImageOptionsProcessor.m in Sources */, 3250C9EF2355D9DA0093A896 /* SDWebImageDownloaderDecryptor.m in Sources */, 3240BB6523968FA1003BA07D /* SDFileAttributeHelper.m in Sources */, diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.h b/SDWebImage/Core/SDGraphicsImageRenderer.h new file mode 100644 index 00000000..2899b3b4 --- /dev/null +++ b/SDWebImage/Core/SDGraphicsImageRenderer.h @@ -0,0 +1,44 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "SDWebImageCompat.h" + +typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context); + +typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) { + SDGraphicsImageRendererFormatRangeUnspecified = -1, + SDGraphicsImageRendererFormatRangeAutomatic = 0, + SDGraphicsImageRendererFormatRangeExtended, + SDGraphicsImageRendererFormatRangeStandard +}; + +@interface SDGraphicsImageRendererFormat : NSObject + +@property (nonatomic) CGFloat scale; +@property (nonatomic) BOOL opaque; + +/** + For iOS 12+, the value is from system API + For iOS 10-11, the value is from `prefersExtendedRange` property + For iOS 9, the value is `.unspecified` + */ +@property (nonatomic) SDGraphicsImageRendererFormatRange preferredRange; + +- (nonnull instancetype)init; ++ (nonnull instancetype)preferredFormat; + +@end + +@interface SDGraphicsImageRenderer : NSObject + +- (nonnull instancetype)initWithSize:(CGSize)size; +- (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull SDGraphicsImageRendererFormat *)format; + +- (nonnull UIImage *)imageWithActions:(nonnull NS_NOESCAPE SDGraphicsImageDrawingActions)actions; + +@end diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.m b/SDWebImage/Core/SDGraphicsImageRenderer.m new file mode 100644 index 00000000..980ef2b4 --- /dev/null +++ b/SDWebImage/Core/SDGraphicsImageRenderer.m @@ -0,0 +1,140 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "SDGraphicsImageRenderer.h" +#import "SDImageGraphics.h" + +@interface SDGraphicsImageRendererFormat () +@property (nonatomic, strong) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0)); +@end + +@implementation SDGraphicsImageRendererFormat + +- (instancetype)init { + self = [super init]; + if (self) { + if (@available(iOS 10.0, *)) { + UIGraphicsImageRendererFormat *uiformat = [[UIGraphicsImageRendererFormat alloc] init]; + self.uiformat = uiformat; + self.scale = uiformat.scale; + self.opaque = uiformat.opaque; + if (@available(iOS 12.0, *)) { + self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; + } else { + if (uiformat.prefersExtendedRange) { + self.preferredRange = SDGraphicsImageRendererFormatRangeExtended; + } else { + self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; + } + } + } else { + self.scale = 1.0; + self.opaque = NO; + self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; + } + } + return self; +} + +- (instancetype)initForMainScreen { + self = [super init]; + if (self) { + if (@available(iOS 10.0, *)) { + UIGraphicsImageRendererFormat *uiformat; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + // iOS 11.0.0 GM does have `preferredFormat`, but iOS 11 betas did not (argh!) + if ([UIGraphicsImageRenderer respondsToSelector:@selector(preferredFormat)]) { + uiformat = [UIGraphicsImageRendererFormat preferredFormat]; + } else { + uiformat = [UIGraphicsImageRendererFormat defaultFormat]; + } + self.uiformat = uiformat; + self.scale = uiformat.scale; + self.opaque = uiformat.opaque; + if (@available(iOS 12.0, *)) { + self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; + } else { + if (uiformat.prefersExtendedRange) { + self.preferredRange = SDGraphicsImageRendererFormatRangeExtended; + } else { + self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; + } + } +#pragma clang diagnostic pop + } else { +#if SD_WATCH + CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale; +#elif SD_UIKIT + CGFloat screenScale = [UIScreen mainScreen].scale; +#elif SD_MAC + CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor; +#endif + self.scale = screenScale; + self.opaque = NO; + self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; + } + } + return self; +} + ++ (instancetype)preferredFormat { + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] initForMainScreen]; + return format; +} + +@end + +@interface SDGraphicsImageRenderer () +@property (nonatomic, assign) CGSize size; +@property (nonatomic, strong) SDGraphicsImageRendererFormat *format; +@property (nonatomic, strong) UIGraphicsImageRenderer *uirenderer API_AVAILABLE(ios(10.0)); +@end + +@implementation SDGraphicsImageRenderer + +- (instancetype)initWithSize:(CGSize)size { + return [self initWithSize:size format:SDGraphicsImageRendererFormat.preferredFormat]; +} + +- (instancetype)initWithSize:(CGSize)size format:(SDGraphicsImageRendererFormat *)format { + NSParameterAssert(format); + self = [super init]; + if (self) { + self.size = size; + self.format = format; + if (@available(iOS 10.0, *)) { + UIGraphicsImageRendererFormat *uiformat = format.uiformat; + self.uirenderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:uiformat]; + } + } + return self; +} + +- (UIImage *)imageWithActions:(NS_NOESCAPE SDGraphicsImageDrawingActions)actions { + NSParameterAssert(actions); + if (@available(iOS 10.0, *)) { + UIGraphicsImageDrawingActions uiactions = ^(UIGraphicsImageRendererContext *rendererContext) { + if (actions) { + actions(rendererContext.CGContext); + } + }; + return [self.uirenderer imageWithActions:uiactions]; + } else { + SDGraphicsBeginImageContextWithOptions(self.size, self.format.opaque, self.format.scale); + CGContextRef context = SDGraphicsGetCurrentContext(); + if (actions) { + actions(context); + } + UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); + SDGraphicsEndImageContext(); + return image; + } +} + +@end diff --git a/WebImage/SDWebImage.h b/WebImage/SDWebImage.h index ab0f43e5..f219978e 100644 --- a/WebImage/SDWebImage.h +++ b/WebImage/SDWebImage.h @@ -65,6 +65,7 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; #import #import #import +#import #import #import #import From ee0aa220e051f6807017afe98c34ba2a1a3643e6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 16:36:13 +0800 Subject: [PATCH 028/181] Fix the compile issue on watchOS/macOS --- SDWebImage/Core/SDGraphicsImageRenderer.m | 40 ++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.m b/SDWebImage/Core/SDGraphicsImageRenderer.m index 980ef2b4..029a4f40 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.m +++ b/SDWebImage/Core/SDGraphicsImageRenderer.m @@ -10,7 +10,9 @@ #import "SDImageGraphics.h" @interface SDGraphicsImageRendererFormat () -@property (nonatomic, strong) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0)); +#if SD_UIKIT +@property (nonatomic, strong) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0), tvos(10.0)); +#endif @end @implementation SDGraphicsImageRendererFormat @@ -18,12 +20,13 @@ - (instancetype)init { self = [super init]; if (self) { - if (@available(iOS 10.0, *)) { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { UIGraphicsImageRendererFormat *uiformat = [[UIGraphicsImageRendererFormat alloc] init]; self.uiformat = uiformat; self.scale = uiformat.scale; self.opaque = uiformat.opaque; - if (@available(iOS 12.0, *)) { + if (@available(iOS 12.0, tvOS 12.0, *)) { self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; } else { if (uiformat.prefersExtendedRange) { @@ -33,21 +36,25 @@ } } } else { +#endif self.scale = 1.0; self.opaque = NO; self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; +#if SD_UIKIT } +#endif } return self; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" - (instancetype)initForMainScreen { self = [super init]; if (self) { - if (@available(iOS 10.0, *)) { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageRendererFormat *uiformat; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunguarded-availability" // iOS 11.0.0 GM does have `preferredFormat`, but iOS 11 betas did not (argh!) if ([UIGraphicsImageRenderer respondsToSelector:@selector(preferredFormat)]) { uiformat = [UIGraphicsImageRendererFormat preferredFormat]; @@ -57,7 +64,7 @@ self.uiformat = uiformat; self.scale = uiformat.scale; self.opaque = uiformat.opaque; - if (@available(iOS 12.0, *)) { + if (@available(iOS 12.0, tvOS 12.0, *)) { self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; } else { if (uiformat.prefersExtendedRange) { @@ -66,8 +73,8 @@ self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; } } -#pragma clang diagnostic pop } else { +#endif #if SD_WATCH CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale; #elif SD_UIKIT @@ -78,10 +85,13 @@ self.scale = screenScale; self.opaque = NO; self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; +#if SD_UIKIT } +#endif } return self; } +#pragma clang diagnostic pop + (instancetype)preferredFormat { SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] initForMainScreen]; @@ -93,7 +103,9 @@ @interface SDGraphicsImageRenderer () @property (nonatomic, assign) CGSize size; @property (nonatomic, strong) SDGraphicsImageRendererFormat *format; -@property (nonatomic, strong) UIGraphicsImageRenderer *uirenderer API_AVAILABLE(ios(10.0)); +#if SD_UIKIT +@property (nonatomic, strong) UIGraphicsImageRenderer *uirenderer API_AVAILABLE(ios(10.0), tvos(10.0)); +#endif @end @implementation SDGraphicsImageRenderer @@ -108,17 +120,20 @@ if (self) { self.size = size; self.format = format; - if (@available(iOS 10.0, *)) { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageRendererFormat *uiformat = format.uiformat; self.uirenderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:uiformat]; } +#endif } return self; } - (UIImage *)imageWithActions:(NS_NOESCAPE SDGraphicsImageDrawingActions)actions { NSParameterAssert(actions); - if (@available(iOS 10.0, *)) { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageDrawingActions uiactions = ^(UIGraphicsImageRendererContext *rendererContext) { if (actions) { actions(rendererContext.CGContext); @@ -126,6 +141,7 @@ }; return [self.uirenderer imageWithActions:uiactions]; } else { +#endif SDGraphicsBeginImageContextWithOptions(self.size, self.format.opaque, self.format.scale); CGContextRef context = SDGraphicsGetCurrentContext(); if (actions) { @@ -134,7 +150,9 @@ UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); SDGraphicsEndImageContext(); return image; +#if SD_UIKIT } +#endif } @end From 8fa6c7519c2e41b5f8a5031c3b0e730d0a21ae2c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 16:58:20 +0800 Subject: [PATCH 029/181] Replace the SDGraphicsBeginImageContextWithOptions with SDGraphicsImageRenderer --- SDWebImage/Core/UIImage+Transform.m | 96 +++++++++++++++-------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 8637b1a2..723d48be 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -9,6 +9,7 @@ #import "UIImage+Transform.h" #import "NSImage+Compatibility.h" #import "SDImageGraphics.h" +#import "SDGraphicsImageRenderer.h" #import "NSBezierPath+RoundedCorners.h" #import #if SD_UIKIT || SD_MAC @@ -165,11 +166,10 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma @implementation UIImage (Transform) -- (void)sd_drawInRect:(CGRect)rect withScaleMode:(SDImageScaleMode)scaleMode clipsToBounds:(BOOL)clips { +- (void)sd_drawInRect:(CGRect)rect context:(CGContextRef)context scaleMode:(SDImageScaleMode)scaleMode clipsToBounds:(BOOL)clips { CGRect drawRect = SDCGRectFitWithScaleMode(rect, self.size, scaleMode); if (drawRect.size.width == 0 || drawRect.size.height == 0) return; if (clips) { - CGContextRef context = SDGraphicsGetCurrentContext(); if (context) { CGContextSaveGState(context); CGContextAddRect(context, rect); @@ -184,10 +184,12 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma - (nullable UIImage *)sd_resizedImageWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode { if (size.width <= 0 || size.height <= 0) return nil; - SDGraphicsBeginImageContextWithOptions(size, NO, self.scale); - [self sd_drawInRect:CGRectMake(0, 0, size.width, size.height) withScaleMode:scaleMode clipsToBounds:NO]; - UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); - SDGraphicsEndImageContext(); + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; + format.scale = self.scale; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + [self sd_drawInRect:CGRectMake(0, 0, size.width, size.height) context:context scaleMode:scaleMode clipsToBounds:NO]; + }]; return image; } @@ -213,43 +215,43 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma - (nullable UIImage *)sd_roundedCornerImageWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor { if (!self.CGImage) return nil; - SDGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); - CGContextRef context = SDGraphicsGetCurrentContext(); - CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); - - CGFloat minSize = MIN(self.size.width, self.size.height); - if (borderWidth < minSize / 2) { -#if SD_UIKIT || SD_WATCH - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(cornerRadius, cornerRadius)]; -#else - NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadius:cornerRadius]; -#endif - [path closePath]; + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; + format.scale = self.scale; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); - CGContextSaveGState(context); - [path addClip]; - [self drawInRect:rect]; - CGContextRestoreGState(context); - } - - if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { - CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; - CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); - CGFloat strokeRadius = cornerRadius > self.scale / 2 ? cornerRadius - self.scale / 2 : 0; + CGFloat minSize = MIN(self.size.width, self.size.height); + if (borderWidth < minSize / 2) { #if SD_UIKIT || SD_WATCH - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, strokeRadius)]; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(cornerRadius, cornerRadius)]; #else - NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadius:strokeRadius]; + NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadius:cornerRadius]; #endif - [path closePath]; + [path closePath]; + + CGContextSaveGState(context); + [path addClip]; + [self drawInRect:rect]; + CGContextRestoreGState(context); + } - path.lineWidth = borderWidth; - [borderColor setStroke]; - [path stroke]; - } - - UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); - SDGraphicsEndImageContext(); + if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { + CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; + CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); + CGFloat strokeRadius = cornerRadius > self.scale / 2 ? cornerRadius - self.scale / 2 : 0; +#if SD_UIKIT || SD_WATCH + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, strokeRadius)]; +#else + NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadius:strokeRadius]; +#endif + [path closePath]; + + path.lineWidth = borderWidth; + [borderColor setStroke]; + [path stroke]; + } + }]; return image; } @@ -347,15 +349,15 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing CGBlendMode blendMode = kCGBlendModeSourceAtop; - SDGraphicsBeginImageContextWithOptions(size, NO, scale); - CGContextRef context = SDGraphicsGetCurrentContext(); - [self drawInRect:rect]; - CGContextSetBlendMode(context, blendMode); - CGContextSetFillColorWithColor(context, tintColor.CGColor); - CGContextFillRect(context, rect); - UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); - SDGraphicsEndImageContext(); - + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; + format.scale = scale; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + [self drawInRect:rect]; + CGContextSetBlendMode(context, blendMode); + CGContextSetFillColorWithColor(context, tintColor.CGColor); + CGContextFillRect(context, rect); + }]; return image; } From d5734cd6cd2d67cc2eb08368b7150f2e793c6a72 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 19:41:52 +0800 Subject: [PATCH 030/181] Fix the implementation of SDGraphicsImageRendererFormat, now use the dynamic getter/setter to forward to UIGraphicsImageRendererFormat --- SDWebImage/Core/SDGraphicsImageRenderer.m | 120 ++++++++++++++++++---- 1 file changed, 98 insertions(+), 22 deletions(-) diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.m b/SDWebImage/Core/SDGraphicsImageRenderer.m index 029a4f40..141da2ff 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.m +++ b/SDWebImage/Core/SDGraphicsImageRenderer.m @@ -16,6 +16,104 @@ @end @implementation SDGraphicsImageRendererFormat +@synthesize scale = _scale; +@synthesize opaque = _opaque; +@synthesize preferredRange = _preferredRange; + +#pragma mark - Property +- (CGFloat)scale { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + return self.uiformat.scale; + } else { + return _scale; + } +#else + return _scale; +#endif +} + +- (void)setScale:(CGFloat)scale { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + self.uiformat.scale = scale; + } else { + _scale = scale; + } +#else + _scale = scale; +#endif +} + +- (BOOL)opaque { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + return self.uiformat.opaque; + } else { + return _opaque; + } +#else + return _opaque; +#endif +} + +- (void)setOpaque:(BOOL)opaque { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + self.uiformat.opaque = opaque; + } else { + _opaque = opaque; + } +#else + _opaque = opaque; +#endif +} + +- (SDGraphicsImageRendererFormatRange)preferredRange { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + if (@available(iOS 12.0, tvOS 12.0, *)) { + return (SDGraphicsImageRendererFormatRange)self.uiformat.preferredRange; + } else { + BOOL prefersExtendedRange = self.uiformat.prefersExtendedRange; + if (prefersExtendedRange) { + return SDGraphicsImageRendererFormatRangeExtended; + } else { + return SDGraphicsImageRendererFormatRangeStandard; + } + } + } else { + return _preferredRange; + } +#else + return _preferredRange; +#endif +} + +- (void)setPreferredRange:(SDGraphicsImageRendererFormatRange)preferredRange { +#if SD_UIKIT + if (@available(iOS 10.0, tvOS 10.10, *)) { + if (@available(iOS 12.0, tvOS 12.0, *)) { + self.uiformat.preferredRange = (UIGraphicsImageRendererFormatRange)preferredRange; + } else { + switch (preferredRange) { + case SDGraphicsImageRendererFormatRangeExtended: + self.uiformat.prefersExtendedRange = YES; + break; + case SDGraphicsImageRendererFormatRangeStandard: + self.uiformat.prefersExtendedRange = NO; + default: + // Automatic means default + break; + } + } + } else { + _preferredRange = preferredRange; + } +#else + _preferredRange = preferredRange; +#endif +} - (instancetype)init { self = [super init]; @@ -24,17 +122,6 @@ if (@available(iOS 10.0, tvOS 10.10, *)) { UIGraphicsImageRendererFormat *uiformat = [[UIGraphicsImageRendererFormat alloc] init]; self.uiformat = uiformat; - self.scale = uiformat.scale; - self.opaque = uiformat.opaque; - if (@available(iOS 12.0, tvOS 12.0, *)) { - self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; - } else { - if (uiformat.prefersExtendedRange) { - self.preferredRange = SDGraphicsImageRendererFormatRangeExtended; - } else { - self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; - } - } } else { #endif self.scale = 1.0; @@ -62,17 +149,6 @@ uiformat = [UIGraphicsImageRendererFormat defaultFormat]; } self.uiformat = uiformat; - self.scale = uiformat.scale; - self.opaque = uiformat.opaque; - if (@available(iOS 12.0, tvOS 12.0, *)) { - self.preferredRange = (SDGraphicsImageRendererFormatRange)uiformat.preferredRange; - } else { - if (uiformat.prefersExtendedRange) { - self.preferredRange = SDGraphicsImageRendererFormatRangeExtended; - } else { - self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; - } - } } else { #endif #if SD_WATCH From c49bc5c925559b2ec4441654328eed8e3b5d1306 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 19:42:52 +0800 Subject: [PATCH 031/181] Change the implementation of `sd_rotatedImageWithAngle` using the UIGraphicsRenderer, avoid always using ARGB8888 --- SDWebImage/Core/UIImage+Transform.m | 44 +++++++++------------------ Tests/Tests/SDImageTransformerTests.m | 2 +- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 723d48be..45cd7fba 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -259,37 +259,23 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma if (!self.CGImage) return nil; size_t width = (size_t)CGImageGetWidth(self.CGImage); size_t height = (size_t)CGImageGetHeight(self.CGImage); - CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0., 0., width, height), + CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0, 0, width, height), fitSize ? CGAffineTransformMakeRotation(angle) : CGAffineTransformIdentity); - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(NULL, - (size_t)newRect.size.width, - (size_t)newRect.size.height, - 8, - (size_t)newRect.size.width * 4, - colorSpace, - kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); - CGColorSpaceRelease(colorSpace); - if (!context) return nil; - - CGContextSetShouldAntialias(context, true); - CGContextSetAllowsAntialiasing(context, true); - CGContextSetInterpolationQuality(context, kCGInterpolationHigh); - - CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); - CGContextRotateCTM(context, angle); - - CGContextDrawImage(context, CGRectMake(-(width * 0.5), -(height * 0.5), width, height), self.CGImage); - CGImageRef imgRef = CGBitmapContextCreateImage(context); -#if SD_UIKIT || SD_WATCH - UIImage *img = [UIImage imageWithCGImage:imgRef scale:self.scale orientation:self.imageOrientation]; -#else - UIImage *img = [[UIImage alloc] initWithCGImage:imgRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; -#endif - CGImageRelease(imgRef); - CGContextRelease(context); - return img; + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; + format.scale = self.scale; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:newRect.size format:format]; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + CGContextSetShouldAntialias(context, true); + CGContextSetAllowsAntialiasing(context, true); + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + // Use UIKit coordinate counterclockwise (⟲) + CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); + CGContextRotateCTM(context, -angle); + + [self drawInRect:CGRectMake(-(width * 0.5), -(height * 0.5), width, height)]; + }]; + return image; } - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index d0105f57..6ddcfc14 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -73,7 +73,7 @@ expect(CGSizeEqualToSize(rotatedImage.size, self.testImage.size)).beTruthy(); // Fit size, may change size rotatedImage = [self.testImage sd_rotatedImageWithAngle:angle fitSize:YES]; - CGSize rotatedSize = CGSizeMake(floor(300 * 1.414), floor(300 * 1.414)); // 45º, square length * sqrt(2) + CGSize rotatedSize = CGSizeMake(ceil(300 * 1.414), ceil(300 * 1.414)); // 45º, square length * sqrt(2) expect(CGSizeEqualToSize(rotatedImage.size, rotatedSize)).beTruthy(); // Check image not inversion UIColor *leftCenterColor = [rotatedImage sd_colorAtPoint:CGPointMake(60, 175)]; From 48a7b7f943a9a267eb58857fac88837db735ef38 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 19:49:41 +0800 Subject: [PATCH 032/181] Fix the issues during refactory, the UIGraphicsRenderer using the point size, not pixel size --- SDWebImage/Core/UIImage+Transform.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 45cd7fba..db5de997 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -257,8 +257,8 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma - (nullable UIImage *)sd_rotatedImageWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize { if (!self.CGImage) return nil; - size_t width = (size_t)CGImageGetWidth(self.CGImage); - size_t height = (size_t)CGImageGetHeight(self.CGImage); + size_t width = self.size.width; + size_t height = self.size.height; CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0, 0, width, height), fitSize ? CGAffineTransformMakeRotation(angle) : CGAffineTransformIdentity); From 1e778f0fe66461a76b24018efff36ff2a754aad4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 20:20:38 +0800 Subject: [PATCH 033/181] Refactory the `sd_flippedImageWithHorizontal` with the UIGraphicsRenderer, do not always need ARGB8888 --- SDWebImage/Core/UIImage+Transform.m | 48 +++++++++++------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index db5de997..ffcd9327 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -280,37 +280,25 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { if (!self.CGImage) return nil; - size_t width = (size_t)CGImageGetWidth(self.CGImage); - size_t height = (size_t)CGImageGetHeight(self.CGImage); - size_t bytesPerRow = width * 4; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); - CGColorSpaceRelease(colorSpace); - if (!context) return nil; + size_t width = self.size.width; + size_t height = self.size.height; - CGContextDrawImage(context, CGRectMake(0, 0, width, height), self.CGImage); - UInt8 *data = (UInt8 *)CGBitmapContextGetData(context); - if (!data) { - CGContextRelease(context); - return nil; - } - vImage_Buffer src = { data, height, width, bytesPerRow }; - vImage_Buffer dest = { data, height, width, bytesPerRow }; - if (vertical) { - vImageVerticalReflect_ARGB8888(&src, &dest, kvImageBackgroundColorFill); - } - if (horizontal) { - vImageHorizontalReflect_ARGB8888(&src, &dest, kvImageBackgroundColorFill); - } - CGImageRef imgRef = CGBitmapContextCreateImage(context); - CGContextRelease(context); -#if SD_UIKIT || SD_WATCH - UIImage *img = [UIImage imageWithCGImage:imgRef scale:self.scale orientation:self.imageOrientation]; -#else - UIImage *img = [[UIImage alloc] initWithCGImage:imgRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; -#endif - CGImageRelease(imgRef); - return img; + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; + format.scale = self.scale; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + // Use UIKit coordinate system + if (horizontal) { + CGAffineTransform flipHorizontal = CGAffineTransformMake(-1, 0, 0, 1, width, 0); + CGContextConcatCTM(context, flipHorizontal); + } + if (vertical) { + CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, height); + CGContextConcatCTM(context, flipVertical); + } + [self drawInRect:CGRectMake(0, 0, width, height)]; + }]; + return image; } #pragma mark - Image Blending From 3929f2cd2a354f2fc23b1be819c73451f9ed35e5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 15 Dec 2019 20:44:32 +0800 Subject: [PATCH 034/181] Replace the background decode core function CGImageCreateDecoded with UIGraphicsRenderer, this one should use the perferred format --- SDWebImage/Core/SDImageCoderHelper.m | 35 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 3cc0c7ea..1a3391e1 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -12,6 +12,7 @@ #import "NSData+ImageContentType.h" #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" +#import "SDGraphicsImageRenderer.h" #import "SDAssociatedObject.h" #if SD_UIKIT || SD_WATCH @@ -257,22 +258,26 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; - // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]` - // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage` - // But since our build-in coders use this bitmapInfo, this can have a little performance benefit - CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; - bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo); - if (!context) { - return NULL; - } - // Apply transform - CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); - CGContextConcatCTM(context, transform); - CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height - CGImageRef newImageRef = CGBitmapContextCreateImage(context); - CGContextRelease(context); + // Using UIGraphicsRenderer on supported platforms, and use the main screeen's perferred format (like Wide Color) + SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat]; + format.scale = 1; // use pixel instead of point + format.opaque = !hasAlpha; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:CGSizeMake(newWidth, newHeight) format:format]; + UIImage *newImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + // Apply transform + CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); + CGContextConcatCTM(context, transform); +#if SD_MAC + UIImage *drawImage = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp]; +#else + UIImage *drawImage = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp]; +#endif + [drawImage drawInRect:CGRectMake(0, 0, width, height)]; // The rect is bounding box of CGImage, don't swap width & height + }]; + CGImageRef newImageRef = newImage.CGImage; + // Retain the CGImage because this temp `newImage` will release after function return + CGImageRetain(newImageRef); return newImageRef; } From 92e3bfcc3e9ee89e0b0250cc48603c64406fbedf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 16 Dec 2019 11:44:52 +0800 Subject: [PATCH 035/181] Fix the sd_rotatedImageWithAngle on macOS, which should not apply the counterclockwise reverse --- SDWebImage/Core/UIImage+Transform.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index ffcd9327..54d3cef9 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -269,9 +269,13 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma CGContextSetShouldAntialias(context, true); CGContextSetAllowsAntialiasing(context, true); CGContextSetInterpolationQuality(context, kCGInterpolationHigh); - // Use UIKit coordinate counterclockwise (⟲) CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); +#if SD_UIKIT + // Use UIKit coordinate system counterclockwise (⟲) CGContextRotateCTM(context, -angle); +#else + CGContextRotateCTM(context, angle); +#endif [self drawInRect:CGRectMake(-(width * 0.5), -(height * 0.5), width, height)]; }]; From 1ee04f64b0a97698ad51e67fce85e220c50696db Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 16 Dec 2019 17:26:09 +0800 Subject: [PATCH 036/181] Update all the documentation of the SDGraphicsImageRenderer, fix small behavior to match Apple's documentation --- SDWebImage/Core/SDGraphicsImageRenderer.h | 40 +++++++++++++++++++---- SDWebImage/Core/SDGraphicsImageRenderer.m | 13 ++++++-- SDWebImage/Core/SDImageGraphics.h | 1 + 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.h b/SDWebImage/Core/SDGraphicsImageRenderer.h index 2899b3b4..e450d639 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.h +++ b/SDWebImage/Core/SDGraphicsImageRenderer.h @@ -8,8 +8,15 @@ #import "SDWebImageCompat.h" -typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context); +/** + These following class are provided to use `UIGraphicsImageRenderer` with polyfill, which allows write cross-platform(AppKit/UIKit) code and avoid runtime version check. + Compared to `UIGraphicsBeginImageContext`, `UIGraphicsImageRenderer` use dynamic bitmap info from your draw code to generate CGContext, not always use ARGB8888, which is more performant on RAM usage. + For usage, See more in Apple's documentation: https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer + For UIKit on iOS/tvOS 10+, these method just use the same `UIGraphicsImageRenderer` API. + For others (macOS/watchOS or iOS/tvOS 10-), these method use the `SDImageGraphics.h` to implements the same behavior. +*/ +typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context); typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) { SDGraphicsImageRendererFormatRangeUnspecified = -1, SDGraphicsImageRendererFormatRangeAutomatic = 0, @@ -17,28 +24,49 @@ typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) { SDGraphicsImageRendererFormatRangeStandard }; +/// A set of drawing attributes that represent the configuration of an image renderer context. @interface SDGraphicsImageRendererFormat : NSObject +/// The display scale of the image renderer context. +/// The default value is equal to the scale of the main screen. @property (nonatomic) CGFloat scale; + +/// A Boolean value indicating whether the underlying Core Graphics context has an alpha channel. +/// The default value is NO. @property (nonatomic) BOOL opaque; -/** - For iOS 12+, the value is from system API - For iOS 10-11, the value is from `prefersExtendedRange` property - For iOS 9, the value is `.unspecified` - */ +/// Specifying whether the bitmap context should use extended color. +/// For iOS 12+, the value is from system `preferredRange` property +/// For iOS 10-11, the value is from system `prefersExtendedRange` property +/// For iOS 9-, the value is `.standard` @property (nonatomic) SDGraphicsImageRendererFormatRange preferredRange; +/// Init the default format. See each properties's default value. - (nonnull instancetype)init; + +/// Returns a new format best suited for the main screen’s current configuration. + (nonnull instancetype)preferredFormat; @end +/// A graphics renderer for creating Core Graphics-backed images. @interface SDGraphicsImageRenderer : NSObject +/// Creates an image renderer for drawing images of a given size. +/// @param size The size of images output from the renderer, specified in points. +/// @return An initialized image renderer. - (nonnull instancetype)initWithSize:(CGSize)size; + +/// Creates a new image renderer with a given size and format. +/// @param size The size of images output from the renderer, specified in points. +/// @param format A SDGraphicsImageRendererFormat object that encapsulates the format used to create the renderer context. +/// @return An initialized image renderer. - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull SDGraphicsImageRendererFormat *)format; +/// Creates an image by following a set of drawing instructions. +/// @param actions A SDGraphicsImageDrawingActions block that, when invoked by the renderer, executes a set of drawing instructions to create the output image. +/// @note You should not retain or use the context outside the block, it's non-escaping. +/// @return A UIImage object created by the supplied drawing actions. - (nonnull UIImage *)imageWithActions:(nonnull NS_NOESCAPE SDGraphicsImageDrawingActions)actions; @end diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.m b/SDWebImage/Core/SDGraphicsImageRenderer.m index 141da2ff..869de2ca 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.m +++ b/SDWebImage/Core/SDGraphicsImageRenderer.m @@ -124,9 +124,16 @@ self.uiformat = uiformat; } else { #endif - self.scale = 1.0; +#if SD_WATCH + CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale; +#elif SD_UIKIT + CGFloat screenScale = [UIScreen mainScreen].scale; +#elif SD_MAC + CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor; +#endif + self.scale = screenScale; self.opaque = NO; - self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; + self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; #if SD_UIKIT } #endif @@ -160,7 +167,7 @@ #endif self.scale = screenScale; self.opaque = NO; - self.preferredRange = SDGraphicsImageRendererFormatRangeUnspecified; + self.preferredRange = SDGraphicsImageRendererFormatRangeStandard; #if SD_UIKIT } #endif diff --git a/SDWebImage/Core/SDImageGraphics.h b/SDWebImage/Core/SDImageGraphics.h index 67019c5b..131d6850 100644 --- a/SDWebImage/Core/SDImageGraphics.h +++ b/SDWebImage/Core/SDImageGraphics.h @@ -13,6 +13,7 @@ These following graphics context method are provided to easily write cross-platform(AppKit/UIKit) code. For UIKit, these methods just call the same method in `UIGraphics.h`. See the documentation for usage. For AppKit, these methods use `NSGraphicsContext` to create image context and match the behavior like UIKit. + @note If you don't care bitmap format (ARGB8888) and just draw image, use `SDGraphicsImageRenderer` instead. It's more performant on RAM usage.` */ /// Returns the current graphics context. From 5cb365ece9351c6d50931699837c8739e1aa65d6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 21 Dec 2019 17:20:19 +0800 Subject: [PATCH 037/181] Revert "Replace the background decode core function CGImageCreateDecoded with UIGraphicsRenderer, this one should use the perferred format" This reverts commit 3929f2cd2a354f2fc23b1be819c73451f9ed35e5. --- SDWebImage/Core/SDImageCoderHelper.m | 35 ++++++++++++---------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 1a3391e1..3cc0c7ea 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -12,7 +12,6 @@ #import "NSData+ImageContentType.h" #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" -#import "SDGraphicsImageRenderer.h" #import "SDAssociatedObject.h" #if SD_UIKIT || SD_WATCH @@ -258,26 +257,22 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; + // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]` + // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage` + // But since our build-in coders use this bitmapInfo, this can have a little performance benefit + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; + CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo); + if (!context) { + return NULL; + } - // Using UIGraphicsRenderer on supported platforms, and use the main screeen's perferred format (like Wide Color) - SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat]; - format.scale = 1; // use pixel instead of point - format.opaque = !hasAlpha; - SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:CGSizeMake(newWidth, newHeight) format:format]; - UIImage *newImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { - // Apply transform - CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); - CGContextConcatCTM(context, transform); -#if SD_MAC - UIImage *drawImage = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp]; -#else - UIImage *drawImage = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp]; -#endif - [drawImage drawInRect:CGRectMake(0, 0, width, height)]; // The rect is bounding box of CGImage, don't swap width & height - }]; - CGImageRef newImageRef = newImage.CGImage; - // Retain the CGImage because this temp `newImage` will release after function return - CGImageRetain(newImageRef); + // Apply transform + CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); + CGContextConcatCTM(context, transform); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height + CGImageRef newImageRef = CGBitmapContextCreateImage(context); + CGContextRelease(context); return newImageRef; } From a7682d58b45970bd41727acfd9a04919292f13ba Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 21 Dec 2019 20:02:13 +0800 Subject: [PATCH 038/181] Add the test case testSDGraphicsImageRenderer, update the documentation --- SDWebImage/Core/SDGraphicsImageRenderer.h | 5 +++-- Tests/Tests/SDUtilsTests.m | 27 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.h b/SDWebImage/Core/SDGraphicsImageRenderer.h index e450d639..900acd76 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.h +++ b/SDWebImage/Core/SDGraphicsImageRenderer.h @@ -10,10 +10,11 @@ /** These following class are provided to use `UIGraphicsImageRenderer` with polyfill, which allows write cross-platform(AppKit/UIKit) code and avoid runtime version check. - Compared to `UIGraphicsBeginImageContext`, `UIGraphicsImageRenderer` use dynamic bitmap info from your draw code to generate CGContext, not always use ARGB8888, which is more performant on RAM usage. + Compared to `UIGraphicsBeginImageContext`, `UIGraphicsImageRenderer` use dynamic bitmap from your draw code to generate CGContext, not always use ARGB8888, which is more performant on RAM usage. + Which means, if you draw CGImage/CIImage which contains grayscale only, the underlaying bitmap context use grayscale, it's managed by system and not a fixed type. (actually, the `kCGContextTypeAutomatic`) For usage, See more in Apple's documentation: https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer For UIKit on iOS/tvOS 10+, these method just use the same `UIGraphicsImageRenderer` API. - For others (macOS/watchOS or iOS/tvOS 10-), these method use the `SDImageGraphics.h` to implements the same behavior. + For others (macOS/watchOS or iOS/tvOS 10-), these method use the `SDImageGraphics.h` to implements the same behavior (but without dynamic bitmap support) */ typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context); diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index 89012e20..4d37f9c4 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -12,6 +12,7 @@ #import "SDDisplayLink.h" #import "SDInternalMacros.h" #import "SDFileAttributeHelper.h" +#import "UIColor+HexString.h" @interface SDUtilsTests : SDTestCase @@ -107,6 +108,32 @@ expect(hasAttr).beFalsy(); } +- (void)testSDGraphicsImageRenderer { + // Main Screen + SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat; +#if SD_UIKIT + CGFloat screenScale = [UIScreen mainScreen].scale; +#elif SD_MAC + CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor; +#endif + expect(format.scale).equal(screenScale); + expect(format.opaque).beFalsy(); +#if SD_UIKIT + expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeAutomatic); +#elif SD_MAC + expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeStandard); +#endif + CGSize size = CGSizeMake(100, 100); + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; + UIColor *color = UIColor.redColor; + UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + [color setFill]; + CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); + }]; + expect(image.scale).equal(format.scale); + expect([[image sd_colorAtPoint:CGPointMake(50, 50)].sd_hexString isEqualToString:color.sd_hexString]).beTruthy(); +} + - (void)testSDScaledImageForKey { // Test nil expect(SDScaledImageForKey(nil, nil)).beNil(); From f041dcabefb0a0730d453c897ffbad56aaff21a3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 26 Dec 2019 14:53:53 +0800 Subject: [PATCH 039/181] Support to use the creation date and the change date to determine the disk cache expire date compare --- SDWebImage/Core/SDDiskCache.m | 8 ++++++-- SDWebImage/Core/SDImageCacheConfig.h | 14 +++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/SDDiskCache.m b/SDWebImage/Core/SDDiskCache.m index 1dd9e10b..d7308dcc 100644 --- a/SDWebImage/Core/SDDiskCache.m +++ b/SDWebImage/Core/SDDiskCache.m @@ -146,11 +146,15 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis case SDImageCacheConfigExpireTypeAccessDate: cacheContentDateKey = NSURLContentAccessDateKey; break; - case SDImageCacheConfigExpireTypeModificationDate: cacheContentDateKey = NSURLContentModificationDateKey; break; - + case SDImageCacheConfigExpireTypeCreationDate: + cacheContentDateKey = NSURLCreationDateKey; + break; + case SDImageCacheConfigExpireTypeChangeDate: + cacheContentDateKey = NSURLAttributeModificationDateKey; + break; default: break; } diff --git a/SDWebImage/Core/SDImageCacheConfig.h b/SDWebImage/Core/SDImageCacheConfig.h index 460fd06b..e77e128c 100644 --- a/SDWebImage/Core/SDImageCacheConfig.h +++ b/SDWebImage/Core/SDImageCacheConfig.h @@ -12,13 +12,21 @@ /// Image Cache Expire Type typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) { /** - * When the image is accessed it will update this value + * When the image cache is accessed it will update this value */ SDImageCacheConfigExpireTypeAccessDate, /** - * The image was obtained from the disk cache (Default) + * When the image cache is created or modified it will update this value (Default) */ - SDImageCacheConfigExpireTypeModificationDate + SDImageCacheConfigExpireTypeModificationDate, + /** + * When the image cache is created it will update this value + */ + SDImageCacheConfigExpireTypeCreationDate, + /** + * When the image cache is created, modified, renamed, file attribute updated (like permission, xattr) it will update this value + */ + SDImageCacheConfigExpireTypeChangeDate, }; /** From e38388e13f1b2bd556454d9e4edb722d084d605a Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 26 Dec 2019 17:44:09 +0800 Subject: [PATCH 040/181] Fix the issue that "There may be no complete callback when download the picture of the local path" --- SDWebImage/Core/SDWebImageDownloader.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 5b931ea2..94bfa049 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -226,10 +226,11 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; SD_UNLOCK(self.operationsLock); }; self.URLOperations[url] = operation; + // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers. + downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; // Add operation to operation queue only after all configuration done according to Apple's doc. // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. [self.downloadQueue addOperation:operation]; - downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; } else { // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue) // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. From 0d44d70e6209aabd357d510e49b91ad0349ba1c6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 26 Dec 2019 17:44:09 +0800 Subject: [PATCH 041/181] Fix the issue that "There may be no complete callback when download the picture of the local path" --- SDWebImage/Core/SDWebImageDownloader.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 5b931ea2..94bfa049 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -226,10 +226,11 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; SD_UNLOCK(self.operationsLock); }; self.URLOperations[url] = operation; + // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers. + downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; // Add operation to operation queue only after all configuration done according to Apple's doc. // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. [self.downloadQueue addOperation:operation]; - downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; } else { // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue) // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. From d44a5a331e65489e6306eb45f98c542138b1657f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 13 Dec 2019 17:26:20 +0800 Subject: [PATCH 042/181] update the Readme with PDF/SVG/Link plugin repos from SDWebImage organization --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6199e699..f18658fb 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,13 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format - [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format +- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format image +- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format image - and more from community! #### Loaders - [SDWebImagePhotosPlugin](https://github.com/SDWebImage/SDWebImagePhotosPlugin) - plugin to support loading images from Photos (using `Photos.framework`) +- [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs From 0b3079d66b6f486376c9ce40f7f146b930d979df Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 16 Dec 2019 12:44:52 +0800 Subject: [PATCH 043/181] Fix the test case compile warning of `SDWebImageTestDiskCache` --- Tests/Tests/SDWebImageTestCache.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/Tests/SDWebImageTestCache.m b/Tests/Tests/SDWebImageTestCache.m index 7ef29cf7..68b2a154 100644 --- a/Tests/Tests/SDWebImageTestCache.m +++ b/Tests/Tests/SDWebImageTestCache.m @@ -9,6 +9,9 @@ #import "SDWebImageTestCache.h" #import +#import "SDFileAttributeHelper.h" + +static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hackemist.SDWebImageTestDiskCache"; @implementation SDWebImageTestMemoryCache @@ -104,4 +107,18 @@ return size; } +- (nullable NSData *)extendedDataForKey:(nonnull NSString *)key { + NSString *cachePathForKey = [self cachePathForKey:key]; + return [SDFileAttributeHelper extendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; +} + +- (void)setExtendedData:(nullable NSData *)extendedData forKey:(nonnull NSString *)key { + NSString *cachePathForKey = [self cachePathForKey:key]; + if (!extendedData) { + [SDFileAttributeHelper removeExtendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; + } else { + [SDFileAttributeHelper setExtendedAttribute:SDWebImageTestDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; + } +} + @end From 247f74a5d191e9c6bd601d292c99cf9fe1658930 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 27 Dec 2019 15:05:43 +0800 Subject: [PATCH 044/181] Bumped version to 5.4.1 Update the CHANGELOG --- CHANGELOG.md | 6 ++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed842941..04c166c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [5.4.1 - 5.4 Patch, on Dec 27th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.1) +See [all tickets marked for the 5.4.1 release](https://github.com/SDWebImage/SDWebImage/milestone/56) + +### Fixes +- Fix the issue that "There may be no complete callback when download the picture of the local path" #2917 + ## [5.4.0 Extended Cache Metadata, on Dec 6th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.0) See [all tickets marked for the 5.4.0 release](https://github.com/SDWebImage/SDWebImage/milestone/51) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 6e7c4229..1db2f2cf 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.4.0' + s.version = '5.4.1' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 7c51b93f..beb1d1ec 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.4.0 + 5.4.1 CFBundleSignature ???? CFBundleVersion - 5.4.0 + 5.4.1 NSPrincipalClass From 48fe948b8fdc3e8771f946a5f3e5d5cd616449e1 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 14:50:54 +0800 Subject: [PATCH 045/181] Fix the bug that watchOS sd_rotatedImageWithAngle does not works --- SDWebImage/Core/UIImage+Transform.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 54d3cef9..4565c2de 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -270,7 +270,7 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma CGContextSetAllowsAntialiasing(context, true); CGContextSetInterpolationQuality(context, kCGInterpolationHigh); CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); -#if SD_UIKIT +#if SD_UIKIT || SD_WATCH // Use UIKit coordinate system counterclockwise (⟲) CGContextRotateCTM(context, -angle); #else From cb7c4d59909ebb0a8e8d05a13d13d67450d195a5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 11:45:36 +0800 Subject: [PATCH 046/181] Add the compatible method for CIImage on macOS --- SDWebImage/Core/NSImage+Compatibility.h | 14 +++++++++++ SDWebImage/Core/NSImage+Compatibility.m | 31 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/SDWebImage/Core/NSImage+Compatibility.h b/SDWebImage/Core/NSImage+Compatibility.h index dccc1ffa..0a562cc4 100644 --- a/SDWebImage/Core/NSImage+Compatibility.h +++ b/SDWebImage/Core/NSImage+Compatibility.h @@ -19,6 +19,10 @@ The underlying Core Graphics image object. This will actually use `CGImageForProposedRect` with the image size. */ @property (nonatomic, readonly, nullable) CGImageRef CGImage; +/** + The underlying Core Image data. This will actually use `bestRepresentationForRect` with the image size to find the `NSCIImageRep`. + */ +@property (nonatomic, readonly, nullable) CIImage *CIImage; /** The scale factor of the image. This wil actually use `bestRepresentationForRect` with image size and pixel size to calculate the scale factor. If failed, use the default value 1.0. Should be greater than or equal to 1.0. */ @@ -38,6 +42,16 @@ The underlying Core Graphics image object. This will actually use `CGImageForPro */ - (nonnull instancetype)initWithCGImage:(nonnull CGImageRef)cgImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation; +/** + Initializes and returns an image object with the specified Core Image object. The representation is `NSCIImageRep`. + + @param ciImage A Core Image image object + @param scale The image scale factor + @param orientation The orientation of the image data + @return The image object + */ +- (nonnull instancetype)initWithCIImage:(nonnull CIImage *)ciImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation; + /** Returns an image object with the scale factor. The representation is created from the image data. @note The difference between these this and `initWithData:` is that `initWithData:` will always use `backingScaleFactor` as scale factor. diff --git a/SDWebImage/Core/NSImage+Compatibility.m b/SDWebImage/Core/NSImage+Compatibility.m index 83b80bc6..7de0c703 100644 --- a/SDWebImage/Core/NSImage+Compatibility.m +++ b/SDWebImage/Core/NSImage+Compatibility.m @@ -20,6 +20,15 @@ return cgImage; } +- (nullable CIImage *)CIImage { + NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); + NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; + if (![imageRep isKindOfClass:NSCIImageRep.class]) { + return nil; + } + return ((NSCIImageRep *)imageRep).CIImage; +} + - (CGFloat)scale { CGFloat scale = 1; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); @@ -65,6 +74,28 @@ return self; } +- (instancetype)initWithCIImage:(nonnull CIImage *)ciImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation { + NSCIImageRep *imageRep; + if (orientation != kCGImagePropertyOrientationUp) { + CIImage *rotatedCIImage = [ciImage imageByApplyingOrientation:orientation]; + imageRep = [[NSCIImageRep alloc] initWithCIImage:rotatedCIImage]; + } else { + imageRep = [[NSCIImageRep alloc] initWithCIImage:ciImage]; + } + if (scale < 1) { + scale = 1; + } + CGFloat pixelWidth = imageRep.pixelsWide; + CGFloat pixelHeight = imageRep.pixelsHigh; + NSSize size = NSMakeSize(pixelWidth / scale, pixelHeight / scale); + self = [self initWithSize:size]; + if (self) { + imageRep.size = size; + [self addRepresentation:imageRep]; + } + return self; +} + - (instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale { NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithData:data]; if (!imageRep) { From 4425c823ad4f285a2b7f581702f0ae8785b52bb7 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 14:55:45 +0800 Subject: [PATCH 047/181] Add CIImage support on transform methods --- SDWebImage/Core/UIImage+Transform.m | 56 ++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 4565c2de..1e776778 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -194,13 +194,30 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable UIImage *)sd_croppedImageWithRect:(CGRect)rect { - if (!self.CGImage) return nil; rect.origin.x *= self.scale; rect.origin.y *= self.scale; rect.size.width *= self.scale; rect.size.height *= self.scale; if (rect.size.width <= 0 || rect.size.height <= 0) return nil; - CGImageRef imageRef = CGImageCreateWithImageInRect(self.CGImage, rect); + +# SD_UIKIT || SD_MAC + if (self.CIImage) { + CIImage *ciImage = [self.CIImage imageByCroppingToRect:rect]; +#if SD_UIKIT + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } +#endif + + CGImageRef cgImage = self.CGImage; + if (!cgImage) { + return nil; + } + + CGImageRef imageRef = CGImageCreateWithImageInRect(cgImage, rect); if (!imageRef) { return nil; } @@ -214,7 +231,6 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable UIImage *)sd_roundedCornerImageWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor { - if (!self.CGImage) return nil; SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; @@ -256,12 +272,23 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable UIImage *)sd_rotatedImageWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize { - if (!self.CGImage) return nil; size_t width = self.size.width; size_t height = self.size.height; CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0, 0, width, height), fitSize ? CGAffineTransformMakeRotation(angle) : CGAffineTransformIdentity); + if (self.CIImage) { + CGAffineTransform transform = CGAffineTransformMakeRotation(angle); + transform = CGAffineTransformTranslate(transform, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); + CIImage *ciImage = [self.CIImage imageByApplyingTransform:transform]; +#if SD_UIKIT || SD_WATCH + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:newRect.size format:format]; @@ -283,10 +310,29 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { - if (!self.CGImage) return nil; size_t width = self.size.width; size_t height = self.size.height; + if (self.CIImage) { + CGAffineTransform transform = CGAffineTransformIdentity; + // Use UIKit coordinate system + if (horizontal) { + CGAffineTransform flipHorizontal = CGAffineTransformMake(-1, 0, 0, 1, width, 0); + transform = CGAffineTransformConcat(transform, flipHorizontal); + } + if (vertical) { + CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, height); + transform = CGAffineTransformConcat(transform, flipVertical); + } + CIImage *ciImage = [self.CIImage imageByApplyingTransform:transform]; +#if SD_UIKIT || SD_WATCH + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } + SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; From ac19d18d3c311d8bc9d111b85c356abdfd4a4975 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 15:37:43 +0800 Subject: [PATCH 048/181] Fix compile issue on watchOS, add `sd_tintedImageWithColor` support for CIImage --- SDWebImage/Core/UIImage+Transform.m | 61 +++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 1e776778..ca4c83d7 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -200,7 +200,8 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma rect.size.height *= self.scale; if (rect.size.width <= 0 || rect.size.height <= 0) return nil; -# SD_UIKIT || SD_MAC +#if SD_UIKIT || SD_MAC + // CIImage shortcut if (self.CIImage) { CIImage *ciImage = [self.CIImage imageByCroppingToRect:rect]; #if SD_UIKIT @@ -212,21 +213,21 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } #endif - CGImageRef cgImage = self.CGImage; - if (!cgImage) { - return nil; - } - - CGImageRef imageRef = CGImageCreateWithImageInRect(cgImage, rect); + CGImageRef imageRef = self.CGImage; if (!imageRef) { return nil; } + + CGImageRef croppedImageRef = CGImageCreateWithImageInRect(imageRef, rect); + if (!croppedImageRef) { + return nil; + } #if SD_UIKIT || SD_WATCH - UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; + UIImage *image = [UIImage imageWithCGImage:croppedImageRef scale:self.scale orientation:self.imageOrientation]; #else - UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; + UIImage *image = [[UIImage alloc] initWithCGImage:croppedImageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif - CGImageRelease(imageRef); + CGImageRelease(croppedImageRef); return image; } @@ -276,7 +277,9 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma size_t height = self.size.height; CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0, 0, width, height), fitSize ? CGAffineTransformMakeRotation(angle) : CGAffineTransformIdentity); - + +#if SD_UIKIT || SD_MAC + // CIImage shortcut if (self.CIImage) { CGAffineTransform transform = CGAffineTransformMakeRotation(angle); transform = CGAffineTransformTranslate(transform, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); @@ -288,6 +291,7 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #endif return image; } +#endif SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; @@ -312,7 +316,9 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { size_t width = self.size.width; size_t height = self.size.height; - + +#if SD_UIKIT || SD_MAC + // CIImage shortcut if (self.CIImage) { CGAffineTransform transform = CGAffineTransformIdentity; // Use UIKit coordinate system @@ -325,13 +331,14 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma transform = CGAffineTransformConcat(transform, flipVertical); } CIImage *ciImage = [self.CIImage imageByApplyingTransform:transform]; -#if SD_UIKIT || SD_WATCH +#if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } +#endif SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; @@ -354,15 +361,35 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #pragma mark - Image Blending - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor { - if (!self.CGImage) return nil; - if (!tintColor.CGColor) return nil; +#if SD_UIKIT || SD_MAC + // CIImage shortcut + if (self.CIImage) { + CIImage *colorImage = [CIImage imageWithColor:tintColor.CIColor]; + CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; + [filter setValue:colorImage forKey:kCIInputImageKey]; + [filter setValue:self.CIImage forKey:kCIInputBackgroundImageKey]; + CIImage *ciImage = filter.outputImage; +// CIImage *ciImage = [self.CIImage imageByCompositingOverImage:colorImage]; +#if SD_UIKIT + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } +#endif + + CGImageRef imageRef = self.CGImage; + if (!imageRef) { + return nil; + } BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; if (!hasTint) { #if SD_UIKIT || SD_WATCH - return [UIImage imageWithCGImage:self.CGImage scale:self.scale orientation:self.imageOrientation]; + return [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; #else - return [[UIImage alloc] initWithCGImage:self.CGImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; + return [[UIImage alloc] initWithCGImage:imageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif } From eda7422c82985c4997ca6504a288c02cf4170074 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 21:38:56 +0800 Subject: [PATCH 049/181] Fix the issue of sd_croppedImageWithRect (UIKit) and sd_rotatedImageWithAngle for CIImage --- SDWebImage/Core/UIImage+Transform.m | 111 +++++++++++++++----------- Tests/Tests/SDImageTransformerTests.m | 3 +- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index ca4c83d7..93e86356 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -203,7 +203,8 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #if SD_UIKIT || SD_MAC // CIImage shortcut if (self.CIImage) { - CIImage *ciImage = [self.CIImage imageByCroppingToRect:rect]; + CGRect croppingRect = CGRectMake(rect.origin.x, self.size.height - CGRectGetMaxY(rect), rect.size.width, rect.size.height); + CIImage *ciImage = [self.CIImage imageByCroppingToRect:croppingRect]; #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else @@ -281,9 +282,16 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #if SD_UIKIT || SD_MAC // CIImage shortcut if (self.CIImage) { - CGAffineTransform transform = CGAffineTransformMakeRotation(angle); - transform = CGAffineTransformTranslate(transform, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); - CIImage *ciImage = [self.CIImage imageByApplyingTransform:transform]; + CIImage *ciImage = self.CIImage; + if (fitSize) { + CGAffineTransform transform = CGAffineTransformMakeRotation(angle); + ciImage = [ciImage imageByApplyingTransform:transform]; + } else { + CIFilter *filter = [CIFilter filterWithName:@"CIStraightenFilter"]; + [filter setValue:ciImage forKey:kCIInputImageKey]; + [filter setValue:@(angle) forKey:kCIInputAngleKey]; + ciImage = filter.outputImage; + } #if SD_UIKIT || SD_WATCH UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else @@ -361,36 +369,9 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #pragma mark - Image Blending - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor { -#if SD_UIKIT || SD_MAC - // CIImage shortcut - if (self.CIImage) { - CIImage *colorImage = [CIImage imageWithColor:tintColor.CIColor]; - CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; - [filter setValue:colorImage forKey:kCIInputImageKey]; - [filter setValue:self.CIImage forKey:kCIInputBackgroundImageKey]; - CIImage *ciImage = filter.outputImage; -// CIImage *ciImage = [self.CIImage imageByCompositingOverImage:colorImage]; -#if SD_UIKIT - UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; -#else - UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; -#endif - return image; - } -#endif - - CGImageRef imageRef = self.CGImage; - if (!imageRef) { - return nil; - } - BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; if (!hasTint) { -#if SD_UIKIT || SD_WATCH - return [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; -#else - return [[UIImage alloc] initWithCGImage:imageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; -#endif + return self; } CGSize size = self.size; @@ -398,7 +379,7 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma CGFloat scale = self.scale; // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing - CGBlendMode blendMode = kCGBlendModeSourceAtop; + CGBlendMode blendMode = kCGBlendModeNormal; SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = scale; @@ -413,10 +394,19 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point { - if (!self) { - return nil; + CGImageRef imageRef; + // CIImage compatible + if (self.CIImage) { + CIImage *ciImage = self.CIImage; + imageRef = ciImage.CGImage; + if (!imageRef) { + CIContext *context = [CIContext context]; + imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; + } + } else { + imageRef = self.CGImage; } - CGImageRef imageRef = self.CGImage; + if (!imageRef) { return nil; } @@ -457,10 +447,19 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } - (nullable NSArray *)sd_colorsWithRect:(CGRect)rect { - if (!self) { - return nil; + CGImageRef imageRef; + // CIImage shortcut + if (self.CIImage) { + CIImage *ciImage = self.CIImage; + imageRef = ciImage.CGImage; + if (!imageRef) { + CIContext *context = [CIContext context]; + imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; + } + } else { + imageRef = self.CGImage; } - CGImageRef imageRef = self.CGImage; + if (!imageRef) { return nil; } @@ -524,15 +523,26 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma if (self.size.width < 1 || self.size.height < 1) { return nil; } - if (!self.CGImage) { - return nil; - } - BOOL hasBlur = blurRadius > __FLT_EPSILON__; if (!hasBlur) { return self; } +#if SD_UIKIT || SD_MAC + if (self.CIImage) { + CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [filter setValue:self.CIImage forKey:kCIInputImageKey]; + [filter setValue:@(blurRadius) forKey:kCIInputRadiusKey]; + CIImage *ciImage = filter.outputImage; +#if SD_UIKIT + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } +#endif + CGFloat scale = self.scale; CGImageRef imageRef = self.CGImage; @@ -615,12 +625,19 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #if SD_UIKIT || SD_MAC - (nullable UIImage *)sd_filteredImageWithFilter:(nonnull CIFilter *)filter { - if (!self.CGImage) return nil; - - CIContext *context = [CIContext context]; - CIImage *inputImage = [CIImage imageWithCGImage:self.CGImage]; + CIImage *inputImage; + if (self.CIImage) { + inputImage = self.CIImage; + } else { + CGImageRef imageRef = self.CGImage; + if (!imageRef) { + return nil; + } + inputImage = [CIImage imageWithCGImage:imageRef]; + } if (!inputImage) return nil; + CIContext *context = [CIContext context]; [filter setValue:inputImage forKey:kCIInputImageKey]; CIImage *outputImage = filter.outputImage; if (!outputImage) return nil; diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index 6ddcfc14..13d394f6 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -74,7 +74,8 @@ // Fit size, may change size rotatedImage = [self.testImage sd_rotatedImageWithAngle:angle fitSize:YES]; CGSize rotatedSize = CGSizeMake(ceil(300 * 1.414), ceil(300 * 1.414)); // 45º, square length * sqrt(2) - expect(CGSizeEqualToSize(rotatedImage.size, rotatedSize)).beTruthy(); + expect(rotatedImage.size.width - rotatedSize.width <= 1).beTruthy(); + expect(rotatedImage.size.height - rotatedSize.height <= 1).beTruthy(); // Check image not inversion UIColor *leftCenterColor = [rotatedImage sd_colorAtPoint:CGPointMake(60, 175)]; expect([leftCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); From a67ea9d371aa208fbd06e220f83ebcf0e2d5eb7a Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 29 Dec 2019 21:53:30 +0800 Subject: [PATCH 050/181] Fix the regression of sd_tintedImageWithColor with `CISourceAtopCompositing` filter --- SDWebImage/Core/UIImage+Transform.m | 43 ++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 93e86356..fa3841e2 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -164,6 +164,28 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma return [UIColor colorWithRed:r green:g blue:b alpha:a]; } +static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { + CGFloat red, green, blue, alpha; +#if SD_UIKIT + if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) { + [color getWhite:&red alpha:&alpha]; + green = red; + blue = red; + } +#else + @try { + [color getRed:&red green:&green blue:&blue alpha:&alpha]; + } + @catch (NSException *exception) { + [color getWhite:&red alpha:&alpha]; + green = red; + blue = red; + } +#endif + CIColor *ciColor = [CIColor colorWithRed:red green:green blue:blue alpha:alpha]; + return ciColor; +} + @implementation UIImage (Transform) - (void)sd_drawInRect:(CGRect)rect context:(CGContextRef)context scaleMode:(SDImageScaleMode)scaleMode clipsToBounds:(BOOL)clips { @@ -374,12 +396,31 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma return self; } +#if SD_UIKIT || SD_MAC + // CIImage shortcut + if (self.CIImage) { + CIImage *ciImage = self.CIImage; + CIImage *colorImage = [CIImage imageWithColor:SDCIColorConvertFromUIColor(tintColor)]; + colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; + CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; + [filter setValue:colorImage forKey:kCIInputImageKey]; + [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; + ciImage = filter.outputImage; +#if SD_UIKIT + UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; +#endif + return image; + } +#endif + CGSize size = self.size; CGRect rect = { CGPointZero, size }; CGFloat scale = self.scale; // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing - CGBlendMode blendMode = kCGBlendModeNormal; + CGBlendMode blendMode = kCGBlendModeSourceAtop; SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = scale; From 0c152231116e9d9aefb558ab82dc66eae8923eb6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 30 Dec 2019 17:17:16 +0800 Subject: [PATCH 051/181] Fix CIGaussianBlur cropping rect --- SDWebImage/Core/UIImage+Transform.m | 1 + 1 file changed, 1 insertion(+) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index fa3841e2..7a321c1f 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -575,6 +575,7 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { [filter setValue:self.CIImage forKey:kCIInputImageKey]; [filter setValue:@(blurRadius) forKey:kCIInputRadiusKey]; CIImage *ciImage = filter.outputImage; + ciImage = [ciImage imageByCroppingToRect:CGRectMake(0, 0, self.size.width, self.size.height)]; #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else From 40957da785f5e56357bdc34e85d2408ff7a4b224 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 30 Dec 2019 17:18:08 +0800 Subject: [PATCH 052/181] Fix `sd_blurredImageWithRadius` CGImage implementation bug that does not considerate bitmap other than BGRA --- SDWebImage/Core/UIImage+Transform.m | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 7a321c1f..a55eb130 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -588,6 +588,18 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { CGFloat scale = self.scale; CGImageRef imageRef = self.CGImage; + //convert to BGRA if it isn't + if (CGImageGetBitsPerPixel(imageRef) != 32 || + CGImageGetBitsPerComponent(imageRef) != 8 || + !((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) { + SDGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); + [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; + imageRef = SDGraphicsGetImageFromCurrentImageContext().CGImage; + SDGraphicsEndImageContext(); + } else { + CGImageRetain(imageRef); + } + vImage_Buffer effect = {}, scratch = {}; vImage_Buffer *input = NULL, *output = NULL; @@ -602,7 +614,7 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { }; vImage_Error err; - err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImagePrintDiagnosticsToConsole); + err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags); if (err != kvImageNoError) { NSLog(@"UIImage+Transform error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", err, self); return nil; From 2163691d1130e2b3295c2a58138078294549674b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 30 Dec 2019 17:41:08 +0800 Subject: [PATCH 053/181] Fix the compile issue on watchOS --- SDWebImage/Core/UIImage+Transform.m | 51 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index a55eb130..7ff7e98a 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -164,7 +164,9 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma return [UIColor colorWithRed:r green:g blue:b alpha:a]; } -static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { +#if SD_UIKIT || SD_MAC +// Core Image Support +static inline CIColor *SDCIColorFromUIColor(UIColor * _Nonnull color) { CGFloat red, green, blue, alpha; #if SD_UIKIT if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) { @@ -186,6 +188,19 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { return ciColor; } +static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciImage) { + CGImageRef imageRef = NULL; + if (@available(iOS 10, macOS 10.12, tvOS 10, *)) { + imageRef = ciImage.CGImage; + } + if (!imageRef) { + CIContext *context = [CIContext context]; + imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; + } + return imageRef; +} +#endif + @implementation UIImage (Transform) - (void)sd_drawInRect:(CGRect)rect context:(CGContextRef)context scaleMode:(SDImageScaleMode)scaleMode clipsToBounds:(BOOL)clips { @@ -400,7 +415,7 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { // CIImage shortcut if (self.CIImage) { CIImage *ciImage = self.CIImage; - CIImage *colorImage = [CIImage imageWithColor:SDCIColorConvertFromUIColor(tintColor)]; + CIImage *colorImage = [CIImage imageWithColor:SDCIColorFromUIColor(tintColor)]; colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; [filter setValue:colorImage forKey:kCIInputImageKey]; @@ -435,19 +450,16 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { } - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point { - CGImageRef imageRef; + CGImageRef imageRef = NULL; // CIImage compatible +#if SD_UIKIT || SD_MAC if (self.CIImage) { - CIImage *ciImage = self.CIImage; - imageRef = ciImage.CGImage; - if (!imageRef) { - CIContext *context = [CIContext context]; - imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; - } - } else { + imageRef = SDCGImageFromCIImage(self.CIImage); + } +#endif + if (!imageRef) { imageRef = self.CGImage; } - if (!imageRef) { return nil; } @@ -488,19 +500,16 @@ static inline CIColor *SDCIColorConvertFromUIColor(UIColor * _Nonnull color) { } - (nullable NSArray *)sd_colorsWithRect:(CGRect)rect { - CGImageRef imageRef; - // CIImage shortcut + CGImageRef imageRef = NULL; + // CIImage compatible +#if SD_UIKIT || SD_MAC if (self.CIImage) { - CIImage *ciImage = self.CIImage; - imageRef = ciImage.CGImage; - if (!imageRef) { - CIContext *context = [CIContext context]; - imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; - } - } else { + imageRef = SDCGImageFromCIImage(self.CIImage); + } +#endif + if (!imageRef) { imageRef = self.CGImage; } - if (!imageRef) { return nil; } From 3006ff299e32f73193f5c79c9c5c9008a839e4ee Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 30 Dec 2019 22:41:00 +0800 Subject: [PATCH 054/181] Fix the leak of CGImageRef from UIImage (getter only) --- SDWebImage/Core/UIImage+Transform.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 7ff7e98a..125d886e 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -605,8 +605,6 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; imageRef = SDGraphicsGetImageFromCurrentImageContext().CGImage; SDGraphicsEndImageContext(); - } else { - CGImageRetain(imageRef); } vImage_Buffer effect = {}, scratch = {}; From 1afadafc7887f01ce76203cc7b458ddec2f71416 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Jan 2020 11:58:12 +0800 Subject: [PATCH 055/181] Update all transformer test cases to support Core Image --- Tests/Tests/SDImageTransformerTests.m | 137 ++++++++++++++++++++------ 1 file changed, 108 insertions(+), 29 deletions(-) diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index 13d394f6..e7a44f5c 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -13,7 +13,8 @@ @interface SDImageTransformerTests : SDTestCase -@property (nonatomic, strong) UIImage *testImage; +@property (nonatomic, strong) UIImage *testImageCG; +@property (nonatomic, strong) UIImage *testImageCI; @end @@ -22,21 +23,37 @@ #pragma mark - UIImage+Transform // UIImage+Transform test is hard to write because it's more about visual effect. Current it's tied to the `TestImage.png`, please keep that image or write new test with new image -- (void)test01UIImageTransformResize { +- (void)test01UIImageTransformResizeCG { + [self test01UIImageTransformResizeWithImage:self.testImageCG]; +} + +- (void)test01UIImageTransformResizeCI { + [self test01UIImageTransformResizeWithImage:self.testImageCI]; +} + +- (void)test01UIImageTransformResizeWithImage:(UIImage *)testImage { CGSize scaleDownSize = CGSizeMake(200, 100); - UIImage *scaledDownImage = [self.testImage sd_resizedImageWithSize:scaleDownSize scaleMode:SDImageScaleModeFill]; + UIImage *scaledDownImage = [testImage sd_resizedImageWithSize:scaleDownSize scaleMode:SDImageScaleModeFill]; expect(CGSizeEqualToSize(scaledDownImage.size, scaleDownSize)).beTruthy(); CGSize scaleUpSize = CGSizeMake(2000, 1000); - UIImage *scaledUpImage = [self.testImage sd_resizedImageWithSize:scaleUpSize scaleMode:SDImageScaleModeAspectFit]; + UIImage *scaledUpImage = [testImage sd_resizedImageWithSize:scaleUpSize scaleMode:SDImageScaleModeAspectFit]; expect(CGSizeEqualToSize(scaledUpImage.size, scaleUpSize)).beTruthy(); // Check image not inversion UIColor *topCenterColor = [scaledUpImage sd_colorAtPoint:CGPointMake(1000, 50)]; expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test02UIImageTransformCrop { +- (void)test02UIImageTransformCropCG { + [self test02UIImageTransformCropWithImage:self.testImageCG]; +} + +- (void)test02UIImageTransformCropCI { + [self test02UIImageTransformCropWithImage:self.testImageCI]; +} + +- (void)test02UIImageTransformCropWithImage:(UIImage *)testImage { CGRect rect = CGRectMake(50, 10, 200, 200); - UIImage *croppedImage = [self.testImage sd_croppedImageWithRect:rect]; + UIImage *croppedImage = [testImage sd_croppedImageWithRect:rect]; expect(CGSizeEqualToSize(croppedImage.size, CGSizeMake(200, 200))).beTruthy(); UIColor *startColor = [croppedImage sd_colorAtPoint:CGPointZero]; expect([startColor.sd_hexString isEqualToString:[UIColor clearColor].sd_hexString]).beTruthy(); @@ -45,7 +62,15 @@ expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test03UIImageTransformRoundedCorner { +- (void)test03UIImageTransformRoundedCornerCG { + [self test03UIImageTransformRoundedCornerWithImage:self.testImageCG]; +} + +- (void)test03UIImageTransformRoundedCornerCI { + [self test03UIImageTransformRoundedCornerWithImage:self.testImageCI]; +} + +- (void)test03UIImageTransformRoundedCornerWithImage:(UIImage *)testImage { CGFloat radius = 50; #if SD_UIKIT SDRectCorner corners = UIRectCornerAllCorners; @@ -54,7 +79,7 @@ #endif CGFloat borderWidth = 1; UIColor *borderColor = [UIColor blackColor]; - UIImage *roundedCornerImage = [self.testImage sd_roundedCornerImageWithRadius:radius corners:corners borderWidth:borderWidth borderColor:borderColor]; + UIImage *roundedCornerImage = [testImage sd_roundedCornerImageWithRadius:radius corners:corners borderWidth:borderWidth borderColor:borderColor]; expect(CGSizeEqualToSize(roundedCornerImage.size, CGSizeMake(300, 300))).beTruthy(); UIColor *startColor = [roundedCornerImage sd_colorAtPoint:CGPointZero]; expect([startColor.sd_hexString isEqualToString:[UIColor clearColor].sd_hexString]).beTruthy(); @@ -66,13 +91,21 @@ expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test04UIImageTransformRotate { +- (void)test04UIImageTransformRotateCG { + [self test04UIImageTransformRotateWithImage:self.testImageCG]; +} + +- (void)test04UIImageTransformRotateCI { + [self test04UIImageTransformRotateWithImage:self.testImageCI]; +} + +- (void)test04UIImageTransformRotateWithImage:(UIImage *)testImage { CGFloat angle = M_PI_4; - UIImage *rotatedImage = [self.testImage sd_rotatedImageWithAngle:angle fitSize:NO]; + UIImage *rotatedImage = [testImage sd_rotatedImageWithAngle:angle fitSize:NO]; // Not fit size and no change - expect(CGSizeEqualToSize(rotatedImage.size, self.testImage.size)).beTruthy(); + expect(CGSizeEqualToSize(rotatedImage.size, testImage.size)).beTruthy(); // Fit size, may change size - rotatedImage = [self.testImage sd_rotatedImageWithAngle:angle fitSize:YES]; + rotatedImage = [testImage sd_rotatedImageWithAngle:angle fitSize:YES]; CGSize rotatedSize = CGSizeMake(ceil(300 * 1.414), ceil(300 * 1.414)); // 45º, square length * sqrt(2) expect(rotatedImage.size.width - rotatedSize.width <= 1).beTruthy(); expect(rotatedImage.size.height - rotatedSize.height <= 1).beTruthy(); @@ -81,11 +114,19 @@ expect([leftCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test05UIImageTransformFlip { +- (void)test05UIImageTransformFlipCG { + [self test05UIImageTransformFlipWithImage:self.testImageCG]; +} + +- (void)test05UIImageTransformFlipCI { + [self test05UIImageTransformFlipWithImage:self.testImageCI]; +} + +- (void)test05UIImageTransformFlipWithImage:(UIImage *)testImage { BOOL horizontal = YES; BOOL vertical = YES; - UIImage *flippedImage = [self.testImage sd_flippedImageWithHorizontal:horizontal vertical:vertical]; - expect(CGSizeEqualToSize(flippedImage.size, self.testImage.size)).beTruthy(); + UIImage *flippedImage = [testImage sd_flippedImageWithHorizontal:horizontal vertical:vertical]; + expect(CGSizeEqualToSize(flippedImage.size, testImage.size)).beTruthy(); // Test pixel colors method here UIColor *checkColor = [flippedImage sd_colorAtPoint:CGPointMake(75, 75)]; expect(checkColor); @@ -99,10 +140,18 @@ expect([bottomCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test06UIImageTransformTint { +- (void)test06UIImageTransformTintCG { + [self test06UIImageTransformTintWithImage:self.testImageCG]; +} + +- (void)test06UIImageTransformTintCI { + [self test06UIImageTransformTintWithImage:self.testImageCI]; +} + +- (void)test06UIImageTransformTintWithImage:(UIImage *)testImage { UIColor *tintColor = [UIColor blackColor]; - UIImage *tintedImage = [self.testImage sd_tintedImageWithColor:tintColor]; - expect(CGSizeEqualToSize(tintedImage.size, self.testImage.size)).beTruthy(); + UIImage *tintedImage = [testImage sd_tintedImageWithColor:tintColor]; + expect(CGSizeEqualToSize(tintedImage.size, testImage.size)).beTruthy(); // Check center color, should keep clear UIColor *centerColor = [tintedImage sd_colorAtPoint:CGPointMake(150, 150)]; expect([centerColor.sd_hexString isEqualToString:[UIColor clearColor].sd_hexString]); @@ -114,10 +163,18 @@ expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy(); } -- (void)test07UIImageTransformBlur { +- (void)test07UIImageTransformBlurCG { + [self test07UIImageTransformBlurWithImage:self.testImageCG]; +} + +- (void)test07UIImageTransformBlurCI { + [self test07UIImageTransformBlurWithImage:self.testImageCI]; +} + +- (void)test07UIImageTransformBlurWithImage:(UIImage *)testImage { CGFloat radius = 50; - UIImage *blurredImage = [self.testImage sd_blurredImageWithRadius:radius]; - expect(CGSizeEqualToSize(blurredImage.size, self.testImage.size)).beTruthy(); + UIImage *blurredImage = [testImage sd_blurredImageWithRadius:radius]; + expect(CGSizeEqualToSize(blurredImage.size, testImage.size)).beTruthy(); // Check left color, should be blurred UIColor *leftColor = [blurredImage sd_colorAtPoint:CGPointMake(80, 150)]; // Hard-code from the output @@ -128,11 +185,19 @@ expect([topCenterColor.sd_hexString isEqualToString:@"#9a430d06"]).beTruthy(); } -- (void)test08UIImageTransformFilter { +- (void)test08UIImageTransformFilterCG { + [self test08UIImageTransformFilterWithImage:self.testImageCG]; +} + +- (void)test08UIImageTransformFilterCI { + [self test08UIImageTransformFilterWithImage:self.testImageCI]; +} + +- (void)test08UIImageTransformFilterWithImage:(UIImage *)testImage { // Invert color filter CIFilter *filter = [CIFilter filterWithName:@"CIColorInvert"]; - UIImage *filteredImage = [self.testImage sd_filteredImageWithFilter:filter]; - expect(CGSizeEqualToSize(filteredImage.size, self.testImage.size)).beTruthy(); + UIImage *filteredImage = [testImage sd_filteredImageWithFilter:filter]; + expect(CGSizeEqualToSize(filteredImage.size, testImage.size)).beTruthy(); // Check left color, should be inverted UIColor *leftColor = [filteredImage sd_colorAtPoint:CGPointMake(80, 150)]; // Hard-code from the output @@ -199,7 +264,7 @@ NSString *transformerKey = [transformerKeys componentsJoinedByString:@"-"]; // SDImageTransformerKeySeparator expect([pipelineTransformer.transformerKey isEqualToString:transformerKey]).beTruthy(); - UIImage *transformedImage = [pipelineTransformer transformedImageWithImage:self.testImage forKey:@"Test"]; + UIImage *transformedImage = [pipelineTransformer transformedImageWithImage:self.testImageCG forKey:@"Test"]; expect(transformedImage).notTo.beNil(); expect(CGSizeEqualToSize(transformedImage.size, cropRect.size)).beTruthy(); } @@ -240,6 +305,8 @@ expect(SDTransformedKeyForKey(key, transformerKey)).equal(@"ftp://root:password@foo.com/image-SDImageFlippingTransformer(1,0).png"); } +#pragma mark - Coder Helper + - (void)test20CGImageCreateDecodedWithOrientation { // Test EXIF orientation tag, you can open this image with `Preview.app`, open inspector (Command+I) and rotate (Command+L/R) to check UIImage *image = [[UIImage alloc] initWithContentsOfFile:[self testPNGPathForName:@"TestEXIF"]]; @@ -332,11 +399,23 @@ #pragma mark - Helper -- (UIImage *)testImage { - if (!_testImage) { - _testImage = [[UIImage alloc] initWithContentsOfFile:[self testPNGPathForName:@"TestImage"]]; +- (UIImage *)testImageCG { + if (!_testImageCG) { + _testImageCG = [[UIImage alloc] initWithContentsOfFile:[self testPNGPathForName:@"TestImage"]]; } - return _testImage; + return _testImageCG; +} + +- (UIImage *)testImageCI { + if (!_testImageCI) { + CIImage *ciImage = [[CIImage alloc] initWithContentsOfURL:[NSURL fileURLWithPath:[self testPNGPathForName:@"TestImage"]]]; +#if SD_UIKIT + _testImageCI = [[UIImage alloc] initWithCIImage:ciImage scale:1 orientation:UIImageOrientationUp]; +#else + _testImageCI = [[UIImage alloc] initWithCIImage:ciImage scale:1 orientation:kCGImagePropertyOrientationUp]; +#endif + } + return _testImageCI; } - (NSString *)testPNGPathForName:(NSString *)name { From 86e3a164dc511a2a8089dfe25e4320fe1d90186b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Jan 2020 14:45:13 +0800 Subject: [PATCH 056/181] Fix the test case of blur radius calculation. CG and CI now match in visual --- SDWebImage/Core/UIImage+Transform.m | 3 ++- Tests/Tests/SDImageTransformerTests.m | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 125d886e..b069526c 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -582,7 +582,8 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma if (self.CIImage) { CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; [filter setValue:self.CIImage forKey:kCIInputImageKey]; - [filter setValue:@(blurRadius) forKey:kCIInputRadiusKey]; + // Blur Radius use pixel count + [filter setValue:@(blurRadius / 2) forKey:kCIInputRadiusKey]; CIImage *ciImage = filter.outputImage; ciImage = [ciImage imageByCroppingToRect:CGRectMake(0, 0, self.size.width, self.size.height)]; #if SD_UIKIT diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index e7a44f5c..9bc28ea5 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -182,7 +182,8 @@ expect([leftColor.sd_hexString isEqualToString:expectedColor.sd_hexString]); // Check rounded corner operation not inversion the image UIColor *topCenterColor = [blurredImage sd_colorAtPoint:CGPointMake(150, 20)]; - expect([topCenterColor.sd_hexString isEqualToString:@"#9a430d06"]).beTruthy(); + UIColor *bottomCenterColor = [blurredImage sd_colorAtPoint:CGPointMake(150, 280)]; + expect([topCenterColor.sd_hexString isEqualToString:bottomCenterColor.sd_hexString]).beFalsy(); } - (void)test08UIImageTransformFilterCG { From b62f48724dfa371b86395e0c0664a80bae9efa03 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Jan 2020 17:52:17 +0800 Subject: [PATCH 057/181] Use CIColor method instead of custom implementation --- SDWebImage/Core/UIImage+Transform.m | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index b069526c..091566bb 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -166,28 +166,6 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma #if SD_UIKIT || SD_MAC // Core Image Support -static inline CIColor *SDCIColorFromUIColor(UIColor * _Nonnull color) { - CGFloat red, green, blue, alpha; -#if SD_UIKIT - if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) { - [color getWhite:&red alpha:&alpha]; - green = red; - blue = red; - } -#else - @try { - [color getRed:&red green:&green blue:&blue alpha:&alpha]; - } - @catch (NSException *exception) { - [color getWhite:&red alpha:&alpha]; - green = red; - blue = red; - } -#endif - CIColor *ciColor = [CIColor colorWithRed:red green:green blue:blue alpha:alpha]; - return ciColor; -} - static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciImage) { CGImageRef imageRef = NULL; if (@available(iOS 10, macOS 10.12, tvOS 10, *)) { @@ -415,7 +393,7 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma // CIImage shortcut if (self.CIImage) { CIImage *ciImage = self.CIImage; - CIImage *colorImage = [CIImage imageWithColor:SDCIColorFromUIColor(tintColor)]; + CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]]; colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; [filter setValue:colorImage forKey:kCIInputImageKey]; From 375e94f31cd5f2de6bbcf28c121c32f515e18821 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Jan 2020 13:46:25 +0800 Subject: [PATCH 058/181] Fix the leak of CIImage based UIImage for `sd_colorAtPoint` --- SDWebImage/Core/UIImage+Transform.m | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 091566bb..53fc334c 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -165,8 +165,8 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma } #if SD_UIKIT || SD_MAC -// Core Image Support -static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciImage) { +// Create-Rule, caller should call CGImageRelease +static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull ciImage) { CGImageRef imageRef = NULL; if (@available(iOS 10, macOS 10.12, tvOS 10, *)) { imageRef = ciImage.CGImage; @@ -174,6 +174,8 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma if (!imageRef) { CIContext *context = [CIContext context]; imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; + } else { + CGImageRetain(imageRef); } return imageRef; } @@ -432,11 +434,12 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma // CIImage compatible #if SD_UIKIT || SD_MAC if (self.CIImage) { - imageRef = SDCGImageFromCIImage(self.CIImage); + imageRef = SDCreateCGImageFromCIImage(self.CIImage); } #endif if (!imageRef) { imageRef = self.CGImage; + CGImageRetain(imageRef); } if (!imageRef) { return nil; @@ -446,34 +449,38 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma CGFloat width = CGImageGetWidth(imageRef); CGFloat height = CGImageGetHeight(imageRef); if (point.x < 0 || point.y < 0 || point.x >= width || point.y >= height) { + CGImageRelease(imageRef); return nil; } // Get pixels CGDataProviderRef provider = CGImageGetDataProvider(imageRef); if (!provider) { + CGImageRelease(imageRef); return nil; } CFDataRef data = CGDataProviderCopyData(provider); if (!data) { + CGImageRelease(imageRef); return nil; } // Get pixel at point size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef); + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); CFRange range = CFRangeMake(bytesPerRow * point.y + components * point.x, 4); if (CFDataGetLength(data) < range.location + range.length) { CFRelease(data); + CGImageRelease(imageRef); return nil; } Pixel_8888 pixel = {0}; CFDataGetBytes(data, range, pixel); CFRelease(data); - + CGImageRelease(imageRef); // Convert to color - CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); return SDGetColorFromPixel(pixel, bitmapInfo); } @@ -482,11 +489,12 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma // CIImage compatible #if SD_UIKIT || SD_MAC if (self.CIImage) { - imageRef = SDCGImageFromCIImage(self.CIImage); + imageRef = SDCreateCGImageFromCIImage(self.CIImage); } #endif if (!imageRef) { imageRef = self.CGImage; + CGImageRetain(imageRef); } if (!imageRef) { return nil; @@ -496,16 +504,19 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma CGFloat width = CGImageGetWidth(imageRef); CGFloat height = CGImageGetHeight(imageRef); if (CGRectGetWidth(rect) <= 0 || CGRectGetHeight(rect) <= 0 || CGRectGetMinX(rect) < 0 || CGRectGetMinY(rect) < 0 || CGRectGetMaxX(rect) > width || CGRectGetMaxY(rect) > height) { + CGImageRelease(imageRef); return nil; } // Get pixels CGDataProviderRef provider = CGImageGetDataProvider(imageRef); if (!provider) { + CGImageRelease(imageRef); return nil; } CFDataRef data = CGDataProviderCopyData(provider); if (!data) { + CGImageRelease(imageRef); return nil; } @@ -517,6 +528,7 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma size_t end = bytesPerRow * (CGRectGetMaxY(rect) - 1) + components * CGRectGetMaxX(rect); if (CFDataGetLength(data) < (CFIndex)end) { CFRelease(data); + CGImageRelease(imageRef); return nil; } @@ -540,6 +552,7 @@ static inline CGImageRef _Nullable SDCGImageFromCIImage(CIImage * _Nonnull ciIma [colors addObject:color]; } CFRelease(data); + CGImageRelease(imageRef); return [colors copy]; } From f6d95e3fa02984181f8ceaae8a6b8041f07d1126 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 7 Jan 2020 12:15:17 +0800 Subject: [PATCH 059/181] SDAnimatedImage now only keep the animated coder when frame count >=1, else we wil lbehave like UIImage to save RAM usage --- SDWebImage/Core/SDAnimatedImage.h | 1 + SDWebImage/Core/SDAnimatedImage.m | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImage.h b/SDWebImage/Core/SDAnimatedImage.h index feb118e6..569eb88f 100644 --- a/SDWebImage/Core/SDAnimatedImage.h +++ b/SDWebImage/Core/SDAnimatedImage.h @@ -72,6 +72,7 @@ // This class override these methods from UIImage(NSImage), and it supports NSSecureCoding. // You should use these methods to create a new animated image. Use other methods just call super instead. +// Pay attention, when the animated image frame count <= 1, all the `SDAnimatedImage` protocol methods will return nil or 0 value, you'd better check the frame count before usage and keep fallback. + (nullable instancetype)imageNamed:(nonnull NSString *)name; // Cache in memory, no Asset Catalog support #if __has_include() + (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; // Cache in memory, no Asset Catalog support diff --git a/SDWebImage/Core/SDAnimatedImage.m b/SDWebImage/Core/SDAnimatedImage.m index ce86e331..422d6578 100644 --- a/SDWebImage/Core/SDAnimatedImage.m +++ b/SDWebImage/Core/SDAnimatedImage.m @@ -156,7 +156,10 @@ static CGFloat SDImageScaleFromPath(NSString *string) { self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; #endif if (self) { - _animatedCoder = animatedCoder; + // Only keep the animated coder if frame count > 1, save RAM usage for non-animated image format (APNG/WebP) + if (animatedCoder.animatedImageFrameCount > 1) { + _animatedCoder = animatedCoder; + } NSData *data = [animatedCoder animatedImageData]; SDImageFormat format = [NSData sd_imageFormatForImageData:data]; _animatedImageFormat = format; @@ -166,6 +169,9 @@ static CGFloat SDImageScaleFromPath(NSString *string) { #pragma mark - Preload - (void)preloadAllFrames { + if (!_animatedCoder) { + return; + } if (!self.isAllFramesLoaded) { NSMutableArray *frames = [NSMutableArray arrayWithCapacity:self.animatedImageFrameCount]; for (size_t i = 0; i < self.animatedImageFrameCount; i++) { @@ -180,6 +186,9 @@ static CGFloat SDImageScaleFromPath(NSString *string) { } - (void)unloadAllFrames { + if (!_animatedCoder) { + return; + } if (self.isAllFramesLoaded) { self.loadedAnimatedImageFrames = nil; self.allFramesLoaded = NO; @@ -190,11 +199,12 @@ static CGFloat SDImageScaleFromPath(NSString *string) { - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { + _animatedImageFormat = [aDecoder decodeIntegerForKey:NSStringFromSelector(@selector(animatedImageFormat))]; NSData *animatedImageData = [aDecoder decodeObjectOfClass:[NSData class] forKey:NSStringFromSelector(@selector(animatedImageData))]; - CGFloat scale = self.scale; if (!animatedImageData) { return self; } + CGFloat scale = self.scale; id animatedCoder = nil; for (idcoder in [SDImageCodersManager sharedManager].coders) { if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) { @@ -207,15 +217,16 @@ static CGFloat SDImageScaleFromPath(NSString *string) { if (!animatedCoder) { return self; } - _animatedCoder = animatedCoder; - SDImageFormat format = [NSData sd_imageFormatForImageData:animatedImageData]; - _animatedImageFormat = format; + if (animatedCoder.animatedImageFrameCount > 1) { + _animatedCoder = animatedCoder; + } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; + [aCoder encodeInteger:self.animatedImageFormat forKey:NSStringFromSelector(@selector(animatedImageFormat))]; NSData *animatedImageData = self.animatedImageData; if (animatedImageData) { [aCoder encodeObject:animatedImageData forKey:NSStringFromSelector(@selector(animatedImageData))]; From 5b3909308fdf6c06bd8bdf39389620c91b9abca5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 7 Jan 2020 21:45:24 +0800 Subject: [PATCH 060/181] Bumped version to 5.4.1 Update the CHANGELOG --- CHANGELOG.md | 6 ++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c166c2..7f82b05c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [5.4.2 - 5.4 Patch, on Jan 7th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.4.2) +See [all tickets marked for the 5.4.2 release](https://github.com/SDWebImage/SDWebImage/milestone/58) + +### Fixes +- SDAnimatedImage now only keep the animated coder when frame count >=1 , else we will behave like UIImage to save RAM usage #2924 + ## [5.4.1 - 5.4 Patch, on Dec 27th, 2019](https://github.com/rs/SDWebImage/releases/tag/5.4.1) See [all tickets marked for the 5.4.1 release](https://github.com/SDWebImage/SDWebImage/milestone/56) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 1db2f2cf..446b443a 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.4.1' + s.version = '5.4.2' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index beb1d1ec..d243db93 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.4.1 + 5.4.2 CFBundleSignature ???? CFBundleVersion - 5.4.1 + 5.4.2 NSPrincipalClass From e7d8341e8bf1e3041c03b22fd840df2055f8fad4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 8 Jan 2020 18:58:21 +0800 Subject: [PATCH 061/181] Fix the documenrtation about the SDAnimatedImage's behavior for static image --- SDWebImage/Core/SDAnimatedImage.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDAnimatedImage.h b/SDWebImage/Core/SDAnimatedImage.h index 569eb88f..a1e2fb19 100644 --- a/SDWebImage/Core/SDAnimatedImage.h +++ b/SDWebImage/Core/SDAnimatedImage.h @@ -72,7 +72,7 @@ // This class override these methods from UIImage(NSImage), and it supports NSSecureCoding. // You should use these methods to create a new animated image. Use other methods just call super instead. -// Pay attention, when the animated image frame count <= 1, all the `SDAnimatedImage` protocol methods will return nil or 0 value, you'd better check the frame count before usage and keep fallback. +// Pay attention, when the animated image frame count <= 1, all the `SDAnimatedImageProvider` protocol methods will return nil or 0 value, you'd better check the frame count before usage and keep fallback. + (nullable instancetype)imageNamed:(nonnull NSString *)name; // Cache in memory, no Asset Catalog support #if __has_include() + (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; // Cache in memory, no Asset Catalog support From a244962926f9e6fea3bcf2f77519b0252d88ea82 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 20:20:50 +0800 Subject: [PATCH 062/181] Add the thumbnail decoding API for protocols, update the code for ImageIO coders --- SDWebImage/Core/SDImageCoder.h | 15 ++++ SDWebImage/Core/SDImageCoder.m | 2 + SDWebImage/Core/SDImageIOAnimatedCoder.m | 75 ++++++++++++++++--- SDWebImage/Core/SDImageIOCoder.m | 29 ++++++- .../Private/SDImageIOAnimatedCoderInternal.h | 1 + 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 3b2049e5..817ce825 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -27,6 +27,21 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFirstFrame */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleFactor; +/** + A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). + Defaults to YES. + @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAspectRatio; + +/** + A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.generationRule`) the value size. + Defaults to CGSizeZero, which means no thumbnail generation at all. + @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize; + + // These options are for image encoding /** A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index c963376b..df5224ae 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -10,6 +10,8 @@ SDImageCoderOption const SDImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor"; +SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserveAspectRatio"; +SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize"; SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index b554fceb..1b71b54e 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -145,6 +145,40 @@ return frameDuration; } ++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize { + // Parse the image properties + NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL); + NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue]; + NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue]; + CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue]; + if (!exifOrientation) { + exifOrientation = kCGImagePropertyOrientationUp; + } + + CGImageRef imageRef; + if (CGSizeEqualToSize(thumbnailSize, CGSizeZero) || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { + imageRef = CGImageSourceCreateImageAtIndex(source, index, NULL); + } else { + NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; + thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); + thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = preserveAspectRatio ? @(MAX(thumbnailSize.width, thumbnailSize.height)) : @(MIN(thumbnailSize.width, thumbnailSize.height)); + thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); + imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); + } + if (!imageRef) { + return nil; + } + +#if SD_UIKIT || SD_WATCH + UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; +#endif + CGImageRelease(imageRef); + return image; +} + #pragma mark - Decode - (BOOL)canDecodeFromData:(nullable NSData *)data { return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat); @@ -160,14 +194,34 @@ scale = MAX([scaleFactor doubleValue], 1); } + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { #if SD_MAC - SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; - NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); - imageRep.size = size; - NSImage *animatedImage = [[NSImage alloc] initWithSize:size]; - [animatedImage addRepresentation:imageRep]; - return animatedImage; + thumbnailSize = thumbnailSizeValue.sizeValue; #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + } + + BOOL preserveAspectRatio = NO; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + +#if SD_MAC + // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG) + // Which decode frames in time and reduce memory usage + if (CGSizeEqualToSize(thumbnailSize, CGSizeZero)) { + SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; + NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); + imageRep.size = size; + NSImage *animatedImage = [[NSImage alloc] initWithSize:size]; + [animatedImage addRepresentation:imageRep]; + return animatedImage; + } +#endif CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); if (!source) { @@ -178,19 +232,17 @@ BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue]; if (decodeFirstFrame || count <= 1) { - animatedImage = [[UIImage alloc] initWithData:data scale:scale]; + animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; } else { NSMutableArray *frames = [NSMutableArray array]; for (size_t i = 0; i < count; i++) { - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL); - if (!imageRef) { + UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + if (!image) { continue; } NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source]; - UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - CGImageRelease(imageRef); SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; [frames addObject:frame]; @@ -205,7 +257,6 @@ CFRelease(source); return animatedImage; -#endif } #pragma mark - Progressive Decode diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index a6fa10af..c17c9f16 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -12,6 +12,7 @@ #import #import "UIImage+Metadata.h" #import "SDImageHEICCoderInternal.h" +#import "SDImageIOAnimatedCoderInternal.h" @implementation SDImageIOCoder { size_t _width, _height; @@ -74,7 +75,33 @@ scale = MAX([scaleFactor doubleValue], 1) ; } - UIImage *image = [[UIImage alloc] initWithData:data scale:scale]; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { +#if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; +#else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + } + + BOOL preserveAspectRatio = NO; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + if (!source) { + return nil; + } + + UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + CFRelease(source); + if (!image) { + return nil; + } + image.sd_imageFormat = [NSData sd_imageFormatForImageData:data]; return image; } diff --git a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h index 4b500131..f2976ea8 100644 --- a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h +++ b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h @@ -13,5 +13,6 @@ + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source; + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source; ++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize; @end From 80c64544952c434b447cafec0a0e7cad561c3454 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 20:31:17 +0800 Subject: [PATCH 063/181] Add the support for incremental decoding for thumbnail images --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 38 +++++++++++++++--------- SDWebImage/Core/SDImageIOCoder.m | 35 ++++++++++++---------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 1b71b54e..10856ddc 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -32,6 +32,8 @@ NSUInteger _frameCount; NSArray *_frames; BOOL _finished; + BOOL _preserveAspectRatio; + CGSize _thumbnailSize; } - (void)dealloc @@ -164,6 +166,10 @@ thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = preserveAspectRatio ? @(MAX(thumbnailSize.width, thumbnailSize.height)) : @(MIN(thumbnailSize.width, thumbnailSize.height)); thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); + if (preserveAspectRatio) { + // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice + exifOrientation = kCGImagePropertyOrientationUp; + } } if (!imageRef) { return nil; @@ -276,6 +282,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = NO; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -316,20 +338,8 @@ if (_width + _height > 0) { // Create the image - CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); - - if (partialImageRef) { - CGFloat scale = _scale; - NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; - if (scaleFactor != nil) { - scale = MAX([scaleFactor doubleValue], 1); - } -#if SD_UIKIT || SD_WATCH - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:UIImageOrientationUp]; -#else - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:kCGImagePropertyOrientationUp]; -#endif - CGImageRelease(partialImageRef); + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (image) { image.sd_imageFormat = self.class.imageFormat; } } diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index c17c9f16..6e80d1a4 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -20,6 +20,8 @@ CGImageSourceRef _imageSource; CGFloat _scale; BOOL _finished; + BOOL _preserveAspectRatio; + CGSize _thumbnailSize; } - (void)dealloc { @@ -122,6 +124,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = NO; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -167,21 +185,8 @@ if (_width + _height > 0) { // Create the image - CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); - - if (partialImageRef) { - CGFloat scale = _scale; - NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; - if (scaleFactor != nil) { - scale = MAX([scaleFactor doubleValue], 1); - } -#if SD_UIKIT || SD_WATCH - UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:_orientation]; - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:imageOrientation]; -#else - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:_orientation]; -#endif - CGImageRelease(partialImageRef); + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; } From 0b0c0d2840690b2e2b9baaef5a08a960dc855442 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 20:37:20 +0800 Subject: [PATCH 064/181] Add the support for animated coder for thumbnail images --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 37 ++++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 10856ddc..7451a712 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -431,6 +431,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = NO; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; _imageSource = imageSource; _imageData = data; #if SD_UIKIT @@ -482,24 +498,13 @@ } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); - if (!imageRef) { - return nil; - } + UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance) - CGImageRef newImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; - if (!newImageRef) { - newImageRef = imageRef; - } else { - CGImageRelease(imageRef); + UIImage *decodedImage = [SDImageCoderHelper decodedImageWithImage:image]; + if (!decodedImage) { + return image; } -#if SD_MAC - UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:kCGImagePropertyOrientationUp]; -#else - UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:UIImageOrientationUp]; -#endif - CGImageRelease(newImageRef); - return image; + return decodedImage; } @end From ae4aa3f848185be1ae6af9025aa1adbb79b46b20 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 20:39:44 +0800 Subject: [PATCH 065/181] Fix the animated image for sd_imageFormat --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 7451a712..eaa42688 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -499,12 +499,16 @@ - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (!image) { + return nil; + } // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance) UIImage *decodedImage = [SDImageCoderHelper decodedImageWithImage:image]; - if (!decodedImage) { - return image; + if (decodedImage) { + image = decodedImage; } - return decodedImage; + image.sd_imageFormat = self.class.imageFormat; + return image; } @end From 19af6b76e66ee63f7e060b6fb55ed3d1784a2339 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 21:04:19 +0800 Subject: [PATCH 066/181] Add the preserveAspectRatio and thumbnailPixelSize in the context options, update the usage --- SDWebImage/Core/SDImageCacheDefine.m | 16 ++++++++------ SDWebImage/Core/SDImageCoder.h | 2 +- SDWebImage/Core/SDImageLoader.m | 32 +++++++++++++++++----------- SDWebImage/Core/SDWebImageDefine.h | 12 +++++++++++ SDWebImage/Core/SDWebImageDefine.m | 2 ++ 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 99e57f1a..560a06eb 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -18,12 +18,16 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; - } + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; if (!decodeFirstFrame) { Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 817ce825..801ca938 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -35,7 +35,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleFacto FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAspectRatio; /** - A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.generationRule`) the value size. + A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.preserveAspectRatio`) the value size. Defaults to CGSizeZero, which means no thumbnail generation at all. @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 8cbbe4e0..aac7a93a 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -32,12 +32,16 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; - } + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; if (!decodeFirstFrame) { // check whether we should use `SDAnimatedImage` @@ -99,12 +103,16 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; - } + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; id progressiveCoder = objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey); if (!progressiveCoder) { diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index a9a34367..03dd210c 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -217,6 +217,18 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageT */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleFactor; +/** + A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). + Defaults to YES. (NSNumber) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImagePreserveAspectRatio; + +/** + A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size. + Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; + /** A SDImageCacheType raw value which specify the store cache type when the image has just been downloaded and will be stored to the cache. Specify `SDImageCacheTypeNone` to disable cache storage; `SDImageCacheTypeDisk` to store in disk cache only; `SDImageCacheTypeMemory` to store in memory only. And `SDImageCacheTypeAll` to store in both memory cache and disk cache. If you use image transformer feature, this actually apply for the transformed image, but not the original image itself. Use `SDWebImageContextOriginalStoreCacheType` if you want to control the original image's store cache type at the same time. diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 496392c4..921e878a 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -122,6 +122,8 @@ SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImage SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager"; SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer"; SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor"; +SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio"; +SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize"; SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType"; SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType"; SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass"; From 6bb8641783a40ba6373bf7c61e27d94b3dc8a410 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 22:20:37 +0800 Subject: [PATCH 067/181] Bridge the exist options `SDWebImageScaleDownLargeImages`, to use the thumbnail decoding instead. Defaults use 60MB limit and thumbnail size is (3966, 3966) --- SDWebImage/Core/SDImageCacheDefine.m | 11 ++++++- SDWebImage/Core/SDImageCoderHelper.h | 6 ++++ SDWebImage/Core/SDImageCoderHelper.m | 45 +++++++++++++--------------- SDWebImage/Core/SDImageLoader.m | 29 +++++++++++++----- SDWebImage/Core/SDWebImageDefine.h | 9 ++++-- 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 560a06eb..0a3afbe5 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -19,7 +19,16 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; - NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); + } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index dcf1da2b..9df53e0c 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -90,6 +90,12 @@ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; #if SD_UIKIT || SD_WATCH +/** + Control the default limit bytes to scale down larget images. + This value must be larger than or equal to 1MB. Defaults to 60MB. + */ +@property (class, readwrite) NSUInteger defaultScaleDownLimitBytes; + /** Convert an EXIF image orientation to an iOS one. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 3cc0c7ea..9e92dd60 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -18,26 +18,15 @@ static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; +static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; +static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; /* * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set * Suggested value for iPad1 and iPhone 3GS: 60. * Suggested value for iPad2 and iPhone 4: 120. * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. */ -static const CGFloat kDestImageSizeMB = 60.f; - -/* - * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set - * Suggested value for iPad1 and iPhone 3GS: 20. - * Suggested value for iPad2 and iPhone 4: 40. - * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10. - */ -static const CGFloat kSourceImageTileSizeMB = 20.f; - -static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; -static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; -static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; -static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; +static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB; static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. #endif @@ -311,13 +300,11 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over CGFloat destTotalPixels; CGFloat tileTotalPixels; - if (bytes > 0) { - destTotalPixels = bytes / kBytesPerPixel; - tileTotalPixels = destTotalPixels / 3; - } else { - destTotalPixels = kDestTotalPixels; - tileTotalPixels = kTileTotalPixels; + if (bytes == 0) { + bytes = kDestImageLimitBytes; } + destTotalPixels = bytes / kBytesPerPixel; + tileTotalPixels = destTotalPixels / 3; CGContextRef destContext; // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. @@ -433,6 +420,17 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } #if SD_UIKIT || SD_WATCH ++ (NSUInteger)defaultScaleDownLimitBytes { + return kDestImageLimitBytes; +} + ++ (void)setDefaultScaleDownLimitBytes:(NSUInteger)defaultScaleDownLimitBytes { + if (defaultScaleDownLimitBytes < kBytesPerMB) { + return; + } + kDestImageLimitBytes = defaultScaleDownLimitBytes; +} + // Convert an EXIF image orientation to an iOS one. + (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation { UIImageOrientation imageOrientation = UIImageOrientationUp; @@ -533,11 +531,10 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return NO; } CGFloat destTotalPixels; - if (bytes > 0) { - destTotalPixels = bytes / kBytesPerPixel; - } else { - destTotalPixels = kDestTotalPixels; + if (bytes == 0) { + bytes = kDestImageLimitBytes; } + destTotalPixels = bytes / kBytesPerPixel; if (destTotalPixels <= kPixelsPerMB) { // Too small to scale down return NO; diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index aac7a93a..4c831c59 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -33,7 +33,16 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; - NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); + } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); @@ -75,12 +84,7 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS } if (shouldDecode) { - BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); - if (shouldScaleDown) { - image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; - } else { - image = [SDImageCoderHelper decodedImageWithImage:image]; - } + image = [SDImageCoderHelper decodedImageWithImage:image]; } } @@ -104,7 +108,16 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; - NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); + } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 03dd210c..549c1da7 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -122,9 +122,12 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { SDWebImageAvoidAutoSetImage = 1 << 10, /** - * By default, images are decoded respecting their original size. On iOS, this flag will scale down the - * images to a size compatible with the constrained memory of devices. - * This flag take no effect if `SDWebImageAvoidDecodeImage` is set. And it will be ignored if `SDWebImageProgressiveLoad` is set. + * By default, images are decoded respecting their original size. + * On iOS/tvOS/watchOS, this flag will scale down the images to a size compatible with the constrained memory of devices. On macOS, this does nothing. + * To control the limit bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB) + * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966)). Previously does not. + * This flags effect the progressive and animated images as well from v5.5.0. Previously does not. + * @note If you need detail controls, it's better to use context option `imageThumbnailPixelSize` and `imagePreserveAspectRatio` instead. */ SDWebImageScaleDownLargeImages = 1 << 11, From c9dffc64dcd355a0ec7ae3bedb76e142a89a73f9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Jan 2020 22:40:51 +0800 Subject: [PATCH 068/181] Fix the compile issue on macOS. Fix the animated frame force decode issue on macOS --- SDWebImage/Core/SDImageCacheDefine.m | 2 ++ SDWebImage/Core/SDImageIOAnimatedCoder.m | 15 ++++++++++++--- SDWebImage/Core/SDImageLoader.m | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 0a3afbe5..d13c3c39 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -20,12 +20,14 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; +#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } +#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index eaa42688..ea9a7c25 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -12,6 +12,7 @@ #import "NSData+ImageContentType.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImageRep.h" +#import "UIImage+ForceDecode.h" @interface SDImageIOCoderFrame : NSObject @@ -502,11 +503,19 @@ if (!image) { return nil; } + image.sd_imageFormat = self.class.imageFormat; // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance) - UIImage *decodedImage = [SDImageCoderHelper decodedImageWithImage:image]; - if (decodedImage) { - image = decodedImage; + CGImageRef imageRef = [SDImageCoderHelper CGImageCreateDecoded:image.CGImage]; + if (!imageRef) { + return image; } +#if SD_MAC + image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp]; +#else + image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:image.imageOrientation]; +#endif + CGImageRelease(imageRef); + image.sd_isDecoded = YES; image.sd_imageFormat = self.class.imageFormat; return image; } diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 4c831c59..2bd48b15 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -34,12 +34,14 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; +#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } +#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } @@ -109,12 +111,14 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; +#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } +#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } From ec620438b9f44ef9b6e26c91413e2ea0438204d6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 6 Jan 2020 18:45:38 +0800 Subject: [PATCH 069/181] Fix the edge case when incrementalDecodedImageWithOptions provide custom scale factor and override the one from init method --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 7 ++++++- SDWebImage/Core/SDImageIOCoder.m | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index ea9a7c25..03f3acc2 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -339,7 +339,12 @@ if (_width + _height > 0) { // Create the image - image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGFloat scale = _scale; + NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; + if (scaleFactor != nil) { + scale = MAX([scaleFactor doubleValue], 1); + } + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (image) { image.sd_imageFormat = self.class.imageFormat; } diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 6e80d1a4..f5223887 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -185,7 +185,12 @@ if (_width + _height > 0) { // Create the image - image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGFloat scale = _scale; + NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; + if (scaleFactor != nil) { + scale = MAX([scaleFactor doubleValue], 1); + } + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; From 9e5ef8c0e9fef0c88513be257e511b45c7dc5dbf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 6 Jan 2020 18:54:01 +0800 Subject: [PATCH 070/181] Change all the force decode method to be compatible with macOS. Update the documentation as well --- SDWebImage/Core/SDImageCacheDefine.m | 9 +-------- SDWebImage/Core/SDImageCoderHelper.h | 2 +- SDWebImage/Core/SDImageCoderHelper.m | 25 +++++++++++-------------- SDWebImage/Core/SDImageLoader.m | 4 ---- SDWebImage/Core/SDWebImageDefine.h | 4 ++-- 5 files changed, 15 insertions(+), 29 deletions(-) diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index d13c3c39..75dfb4e6 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -20,14 +20,12 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; -#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } -#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } @@ -71,12 +69,7 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS shouldDecode = NO; } if (shouldDecode) { - BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); - if (shouldScaleDown) { - image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; - } else { - image = [SDImageCoderHelper decodedImageWithImage:image]; - } + image = [SDImageCoderHelper decodedImageWithImage:image]; } } diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index 9df53e0c..353a8729 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -89,13 +89,13 @@ */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; -#if SD_UIKIT || SD_WATCH /** Control the default limit bytes to scale down larget images. This value must be larger than or equal to 1MB. Defaults to 60MB. */ @property (class, readwrite) NSUInteger defaultScaleDownLimitBytes; +#if SD_UIKIT || SD_WATCH /** Convert an EXIF image orientation to an iOS one. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 9e92dd60..ac671366 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -13,8 +13,8 @@ #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" #import "SDAssociatedObject.h" +#import "UIImage+Metadata.h" -#if SD_UIKIT || SD_WATCH static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; @@ -29,7 +29,6 @@ static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB; static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. -#endif @implementation SDImageCoderHelper @@ -267,9 +266,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } + (UIImage *)decodedImageWithImage:(UIImage *)image { -#if SD_MAC - return image; -#else if (![self shouldDecodeImage:image]) { return image; } @@ -278,18 +274,18 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (!imageRef) { return image; } +#if SD_MAC + UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; +#else UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation]; +#endif CGImageRelease(imageRef); SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; -#endif } + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes { -#if SD_MAC - return image; -#else if (![self shouldDecodeImage:image]) { return image; } @@ -407,7 +403,11 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (destImageRef == NULL) { return image; } +#if SD_MAC + UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; +#else UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; +#endif CGImageRelease(destImageRef); if (destImage == nil) { return image; @@ -416,10 +416,8 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over destImage.sd_isDecoded = YES; return destImage; } -#endif } -#if SD_UIKIT || SD_WATCH + (NSUInteger)defaultScaleDownLimitBytes { return kDestImageLimitBytes; } @@ -431,6 +429,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over kDestImageLimitBytes = defaultScaleDownLimitBytes; } +#if SD_UIKIT || SD_WATCH // Convert an EXIF image orientation to an iOS one. + (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation { UIImageOrientation imageOrientation = UIImageOrientationUp; @@ -501,7 +500,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over #endif #pragma mark - Helper Fuction -#if SD_UIKIT || SD_WATCH + (BOOL)shouldDecodeImage:(nullable UIImage *)image { // Avoid extra decode if (image.sd_isDecoded) { @@ -512,7 +510,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return NO; } // do not decode animated images - if (image.images != nil) { + if (image.sd_isAnimated) { return NO; } @@ -548,7 +546,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return shouldScaleDown; } -#endif static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) { // Inspiration from @libfeihu diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 2bd48b15..4c831c59 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -34,14 +34,12 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; -#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } -#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } @@ -111,14 +109,12 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; -#if SD_UIKIT || SD_WATCH BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); if (shouldScaleDown) { CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; CGFloat dimension = ceil(sqrt(thumbnailPixels)); thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } -#endif if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 549c1da7..8c364844 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -123,8 +123,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /** * By default, images are decoded respecting their original size. - * On iOS/tvOS/watchOS, this flag will scale down the images to a size compatible with the constrained memory of devices. On macOS, this does nothing. - * To control the limit bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB) + * This flag will scale down the images to a size compatible with the constrained memory of devices. + * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB) * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966)). Previously does not. * This flags effect the progressive and animated images as well from v5.5.0. Previously does not. * @note If you need detail controls, it's better to use context option `imageThumbnailPixelSize` and `imagePreserveAspectRatio` instead. From 03e63ede2538b43bf76bf16441d496542dbea4bf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 6 Jan 2020 19:06:49 +0800 Subject: [PATCH 071/181] Change the default limit bytes 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS --- SDWebImage/Core/SDImageCoderHelper.h | 2 +- SDWebImage/Core/SDImageCoderHelper.m | 6 ++++++ SDWebImage/Core/SDWebImageDefine.h | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index 353a8729..38085b7e 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -91,7 +91,7 @@ /** Control the default limit bytes to scale down larget images. - This value must be larger than or equal to 1MB. Defaults to 60MB. + This value must be larger than or equal to 1MB. Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS. */ @property (class, readwrite) NSUInteger defaultScaleDownLimitBytes; diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index ac671366..b88f7068 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -26,7 +26,13 @@ static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; * Suggested value for iPad2 and iPhone 4: 120. * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. */ +#if SD_MAC +static CGFloat kDestImageLimitBytes = 90.f * kBytesPerMB; +#elif SD_UIKIT static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB; +#elif SD_WATCH +static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB; +#endif static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 8c364844..1e871ac0 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -124,8 +124,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /** * By default, images are decoded respecting their original size. * This flag will scale down the images to a size compatible with the constrained memory of devices. - * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB) - * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966)). Previously does not. + * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB on iOS) + * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966) on iOS). Previously does not. * This flags effect the progressive and animated images as well from v5.5.0. Previously does not. * @note If you need detail controls, it's better to use context option `imageThumbnailPixelSize` and `imagePreserveAspectRatio` instead. */ From fa124b4d11ea3a595f0bc9b17f67feecbedf01d4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 11:55:01 +0800 Subject: [PATCH 072/181] Enable the force decode test case for macOS as well --- Tests/Tests/SDImageCoderTests.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 0320c2c2..88a02fdf 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -20,7 +20,6 @@ expect([UIImage sd_decodedAndScaledDownImageWithImage:nil]).to.beNil(); } -#if SD_UIKIT - (void)test02ThatDecodedImageWithImageWorksWithARegularJPGImage { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; @@ -34,7 +33,11 @@ - (void)test03ThatDecodedImageWithImageDoesNotDecodeAnimatedImages { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"gif"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; +#if SD_MAC + UIImage *animatedImage = image; +#else UIImage *animatedImage = [UIImage animatedImageWithImages:@[image] duration:0]; +#endif UIImage *decodedImage = [UIImage sd_decodedImageWithImage:animatedImage]; expect(decodedImage).toNot.beNil(); expect(decodedImage).to.equal(animatedImage); @@ -61,7 +64,7 @@ - (void)test06ThatDecodeAndScaleDownImageWorks { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; - UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image]; + UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:(60 * 1024 * 1024)]; expect(decodedImage).toNot.beNil(); expect(decodedImage).toNot.equal(image); expect(decodedImage.size.width).toNot.equal(image.size.width); @@ -78,7 +81,6 @@ expect(decodedImage.size.width).to.equal(image.size.width); expect(decodedImage.size.height).to.equal(image.size.height); } -#endif - (void)test11ThatAPNGPCoderWorks { NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"]; From 77283f611688ec65992fa5d3a4ca2054d75d0a0f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 18:23:41 +0800 Subject: [PATCH 073/181] Update the test case, fix the behavior of thumbnail pixel size when aspect ratio is YES. --- SDWebImage/Core/SDImageCoder.h | 1 + SDWebImage/Core/SDImageCoderHelper.h | 10 +++++ SDWebImage/Core/SDImageCoderHelper.m | 53 ++++++++++++++++++++++++ SDWebImage/Core/SDImageIOAnimatedCoder.m | 19 ++++++++- SDWebImage/Core/SDWebImageDefine.h | 1 + Tests/Tests/SDImageCoderTests.m | 32 +++++++++++++- 6 files changed, 113 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 801ca938..221246ac 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -37,6 +37,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAs /** A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.preserveAspectRatio`) the value size. Defaults to CGSizeZero, which means no thumbnail generation at all. + @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize; diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index 38085b7e..5dbd523c 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -73,6 +73,16 @@ */ + (CGImageRef _Nullable)CGImageCreateDecoded:(_Nonnull CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation CF_RETURNS_RETAINED; +/** + Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage. + It will detect whether the image size matching the scale size, if not, stretch the image to the target size. + + @param cgImage The CGImage + @param size The scale size in pixel. + @return A new created scaled image + */ ++ (CGImageRef _Nullable)CGImageCreateScaled:(_Nonnull CGImageRef)cgImage size:(CGSize)size CF_RETURNS_RETAINED; + /** Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image @param image The image to be decoded diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index b88f7068..de3d0cfc 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -14,6 +14,12 @@ #import "UIImage+ForceDecode.h" #import "SDAssociatedObject.h" #import "UIImage+Metadata.h" +#import "SDInternalMacros.h" +#import + +static inline size_t SDByteAlign(size_t size, size_t alignment) { + return ((size + (alignment - 1)) / alignment) * alignment; +} static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; @@ -271,6 +277,53 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return newImageRef; } ++ (CGImageRef)CGImageCreateScaled:(CGImageRef)cgImage size:(CGSize)size { + if (!cgImage) { + return NULL; + } + size_t width = CGImageGetWidth(cgImage); + size_t height = CGImageGetHeight(cgImage); + if (width == size.width && height == size.height) { + CGImageRetain(cgImage); + return cgImage; + } + + __block vImage_Buffer input_buffer = {}, output_buffer = {}; + @onExit { + if (input_buffer.data) free(input_buffer.data); + if (output_buffer.data) free(output_buffer.data); + }; + + vImage_CGImageFormat format = (vImage_CGImageFormat) { + .bitsPerComponent = 8, + .bitsPerPixel = 32, + .colorSpace = NULL, + .bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrderDefault, + .version = 0, + .decode = NULL, + .renderingIntent = kCGRenderingIntentDefault, + }; + + vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags); + if (a_ret != kvImageNoError) return NULL; + output_buffer.width = MAX(size.width, 0); + output_buffer.height = MAX(size.height, 0); + output_buffer.rowBytes = SDByteAlign(output_buffer.width * 4, 64); + output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); + if (!output_buffer.data) return NULL; + + vImage_Error ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + if (ret != kvImageNoError) return NULL; + + CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &format, NULL, NULL, kvImageNoFlags, &ret); + if (ret != kvImageNoError) { + CGImageRelease(outputImage); + return NULL; + } + + return outputImage; +} + + (UIImage *)decodedImageWithImage:(UIImage *)image { if (![self shouldDecodeImage:image]) { return image; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 03f3acc2..2f771455 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -164,12 +164,29 @@ } else { NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); - thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = preserveAspectRatio ? @(MAX(thumbnailSize.width, thumbnailSize.height)) : @(MIN(thumbnailSize.width, thumbnailSize.height)); + CGFloat maxPixelSize; + if (preserveAspectRatio) { + if (pixelWidth > pixelHeight) { + maxPixelSize = thumbnailSize.width; + } else { + maxPixelSize = thumbnailSize.height; + } + } else { + maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); + } + thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); if (preserveAspectRatio) { // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice exifOrientation = kCGImagePropertyOrientationUp; + } else { + // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size + if (imageRef) { + CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize]; + CGImageRelease(imageRef); + imageRef = scaledImageRef; + } } } if (!imageRef) { diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 1e871ac0..bd4d4e68 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -228,6 +228,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP /** A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size. + @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 88a02fdf..e81f5373 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -199,14 +199,42 @@ withLocalImageURL:(NSURL *)imageUrl #endif } + // 3 - check thumbnail decoding + CGFloat pixelWidth = inputImage.size.width; + CGFloat pixelHeight = inputImage.size.height; + expect(pixelWidth).beGreaterThan(0); + expect(pixelHeight).beGreaterThan(0); + // check thumnail with scratch + UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(100, 100)), + SDImageCoderDecodePreserveAspectRatio : @(NO) + }]; + expect(thumbImage).toNot.beNil(); + expect(thumbImage.size).equal(CGSizeMake(100, 100)); + // check thumnail with aspect ratio limit + thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(100, 100)), + SDImageCoderDecodePreserveAspectRatio : @(YES) + }]; + expect(thumbImage).toNot.beNil(); + CGFloat ratio = pixelWidth / pixelHeight; + CGSize thumbnailPixelSize; + if (pixelWidth > pixelHeight) { + thumbnailPixelSize = CGSizeMake(100, floor(100 / ratio)); + } else { + thumbnailPixelSize = CGSizeMake(floor(100 * ratio), 100); + } + expect(thumbImage.size).equal(thumbnailPixelSize); + + if (supportsEncoding) { - // 3 - check if we can encode to the original format + // 4 - check if we can encode to the original format if (encodingFormat == SDImageFormatUndefined) { encodingFormat = inputImageFormat; } expect([coder canEncodeToFormat:encodingFormat]).to.beTruthy(); - // 4 - encode from UIImage to NSData using the inputImageFormat and check it + // 5 - encode from UIImage to NSData using the inputImageFormat and check it NSData *outputImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:nil]; expect(outputImageData).toNot.beNil(); UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil]; From cd1ae56f5f246cc851992a3119ced8e5f3d26ec1 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 19:28:39 +0800 Subject: [PATCH 074/181] Add one test case to ensure the `SDWebImageScaleDownLargeImages` is translated to thumbnail decoding --- Tests/Tests/SDWebImageManagerTests.m | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index 143dc504..c75fc950 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -247,6 +247,27 @@ [self waitForExpectationsWithCommonTimeout]; } +- (void)test13ThatScaleDownLargeImageUseThumbnailDecoding { + XCTestExpectation *expectation = [self expectationWithDescription:@"SDWebImageScaleDownLargeImages should translate to thumbnail decoding"]; + NSURL *originalImageURL = [NSURL URLWithString:@"http://via.placeholder.com/3999x3999.png"]; // Max size for this API + NSUInteger defaultLimitBytes = SDImageCoderHelper.defaultScaleDownLimitBytes; + SDImageCoderHelper.defaultScaleDownLimitBytes = 1000 * 1000 * 4; // Limit 1000x1000 pixel + // From v5.5.0, the `SDWebImageScaleDownLargeImages` translate to `SDWebImageContextImageThumbnailPixelSize`, and works for progressive loading + [SDWebImageManager.sharedManager loadImageWithURL:originalImageURL options:SDWebImageScaleDownLargeImages | SDWebImageProgressiveLoad progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + expect(image).notTo.beNil(); + expect(image.size).equal(CGSizeMake(1000, 1000)); + if (finished) { + [expectation fulfill]; + } else { + expect(image.sd_isIncremental).beTruthy(); + } + }]; + + [self waitForExpectationsWithCommonTimeoutUsingHandler:^(NSError * _Nullable error) { + SDImageCoderHelper.defaultScaleDownLimitBytes = defaultLimitBytes; + }]; +} + - (NSString *)testJPEGPath { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; return [testBundle pathForResource:@"TestImage" ofType:@"jpg"]; From d29dfda82a90981b630e1e30f862d21f6596998f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 19:30:17 +0800 Subject: [PATCH 075/181] Update the Example to use thumbnail decoding --- Examples/SDWebImage Demo/MasterViewController.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index 3a6c1780..d05b3c6d 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -115,7 +115,8 @@ cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0]; + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 + context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(100, 100))}]; return cell; } From 767ea255251a46ea9a6b01123531c9e40cfe1c3f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 19:44:35 +0800 Subject: [PATCH 076/181] Revert the example to use thumbnail --- Examples/SDWebImage Demo/MasterViewController.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index d05b3c6d..3a6c1780 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -115,8 +115,7 @@ cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 - context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(100, 100))}]; + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0]; return cell; } From 2e629f6c4637c93575e31877dfef3a76e3fc742e Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 19:46:37 +0800 Subject: [PATCH 077/181] Fix the issue that preserveAspectRatio default value, now it's YES --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 6 +++--- SDWebImage/Core/SDImageIOCoder.m | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 2f771455..5ba86f9c 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -228,7 +228,7 @@ #endif } - BOOL preserveAspectRatio = NO; + BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; @@ -310,7 +310,7 @@ #endif } _thumbnailSize = thumbnailSize; - BOOL preserveAspectRatio = NO; + BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; @@ -464,7 +464,7 @@ #endif } _thumbnailSize = thumbnailSize; - BOOL preserveAspectRatio = NO; + BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index f5223887..f617f437 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -87,7 +87,7 @@ #endif } - BOOL preserveAspectRatio = NO; + BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; @@ -134,7 +134,7 @@ #endif } _thumbnailSize = thumbnailSize; - BOOL preserveAspectRatio = NO; + BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; From 72250f218233b925787175bcc31b557046219817 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 22:33:05 +0800 Subject: [PATCH 078/181] Fix the test case again --- Tests/Tests/SDImageCoderTests.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index e81f5373..96411f79 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -206,23 +206,23 @@ withLocalImageURL:(NSURL *)imageUrl expect(pixelHeight).beGreaterThan(0); // check thumnail with scratch UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ - SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(100, 100)), + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(50, 50)), SDImageCoderDecodePreserveAspectRatio : @(NO) }]; expect(thumbImage).toNot.beNil(); - expect(thumbImage.size).equal(CGSizeMake(100, 100)); + expect(thumbImage.size).equal(CGSizeMake(50, 50)); // check thumnail with aspect ratio limit thumbImage = [coder decodedImageWithData:inputImageData options:@{ - SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(100, 100)), + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(50, 50)), SDImageCoderDecodePreserveAspectRatio : @(YES) }]; expect(thumbImage).toNot.beNil(); CGFloat ratio = pixelWidth / pixelHeight; CGSize thumbnailPixelSize; if (pixelWidth > pixelHeight) { - thumbnailPixelSize = CGSizeMake(100, floor(100 / ratio)); + thumbnailPixelSize = CGSizeMake(50, floor(50 / ratio)); } else { - thumbnailPixelSize = CGSizeMake(floor(100 * ratio), 100); + thumbnailPixelSize = CGSizeMake(floor(50 * ratio), 50); } expect(thumbImage.size).equal(thumbnailPixelSize); From f376b5da9a5a4d565c994582311aa65974a96fdb Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 23:34:55 +0800 Subject: [PATCH 079/181] Fix the behavior of limit width and height for thumbnail pixel size, should not be greater than the size --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 5ba86f9c..b72dc4e0 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -159,14 +159,16 @@ } CGImageRef imageRef; - if (CGSizeEqualToSize(thumbnailSize, CGSizeZero) || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { + if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { imageRef = CGImageSourceCreateImageAtIndex(source, index, NULL); } else { NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); CGFloat maxPixelSize; if (preserveAspectRatio) { - if (pixelWidth > pixelHeight) { + CGFloat pixelRatio = pixelWidth / pixelHeight; + CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; + if (pixelRatio > thumbnailRatio) { maxPixelSize = thumbnailSize.width; } else { maxPixelSize = thumbnailSize.height; @@ -237,7 +239,7 @@ #if SD_MAC // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG) // Which decode frames in time and reduce memory usage - if (CGSizeEqualToSize(thumbnailSize, CGSizeZero)) { + if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); imageRep.size = size; From 150ad1b104ee6e830489cfb22cead5ba5012e5b8 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Jan 2020 23:35:43 +0800 Subject: [PATCH 080/181] Apply a `Thumbnail-` prefix for the cache key for all the thumbnail images, this can avoid cache issue when you query the same URL with different thumbnail size --- .../SDWebImage Demo/MasterViewController.m | 3 +- SDWebImage/Core/SDWebImageManager.m | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index 3a6c1780..f131f55f 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -115,7 +115,8 @@ cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0]; + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 + context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}]; return cell; } diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 61c0a36c..97ffb488 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -95,19 +95,45 @@ static id _defaultImageLoader; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { - return [self cacheKeyForURL:url cacheKeyFilter:self.cacheKeyFilter]; + return [self cacheKeyForURL:url context:nil]; } -- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id)cacheKeyFilter { +- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context { if (!url) { return @""; } - - if (cacheKeyFilter) { - return [cacheKeyFilter cacheKeyForURL:url]; - } else { - return url.absoluteString; + + NSString *key; + // Cache Key Filter + id cacheKeyFilter = self.cacheKeyFilter; + if (context[SDWebImageContextCacheKeyFilter]) { + cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; } + if (cacheKeyFilter) { + key = [cacheKeyFilter cacheKeyForURL:url]; + } else { + key = url.absoluteString; + } + // Thumbnail Key Appending + NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + CGSize thumbnailSize = CGSizeZero; +#if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; +#else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + NSString *transformerKey = [NSString stringWithFormat:@"Thumbnail({%f,%f},%d)", thumbnailSize.width, thumbnailSize.height, preserveAspectRatio]; + key = SDTransformedKeyForKey(key, transformerKey); + } + + return key; } - (SDWebImageCombinedOperation *)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDInternalCompletionBlock)completedBlock { @@ -188,8 +214,7 @@ static id _defaultImageLoader; // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; @weakify(operation); operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); @@ -303,8 +328,7 @@ static id _defaultImageLoader; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; @@ -353,8 +377,7 @@ static id _defaultImageLoader; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; From 7a8407d0be077fad363970a9f0faee85bd00dc0d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 13 Jan 2020 22:02:52 +0800 Subject: [PATCH 081/181] Fix the test case again because of the behavior changes for aspect ratio rect limit --- Tests/Tests/SDImageCoderTests.m | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 96411f79..23dc6d56 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -205,26 +205,31 @@ withLocalImageURL:(NSURL *)imageUrl expect(pixelWidth).beGreaterThan(0); expect(pixelHeight).beGreaterThan(0); // check thumnail with scratch + CGFloat thumbnailWidth = 50; + CGFloat thumbnailHeight = 50; UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ - SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(50, 50)), + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), SDImageCoderDecodePreserveAspectRatio : @(NO) }]; expect(thumbImage).toNot.beNil(); - expect(thumbImage.size).equal(CGSizeMake(50, 50)); + expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight)); // check thumnail with aspect ratio limit thumbImage = [coder decodedImageWithData:inputImageData options:@{ - SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(50, 50)), + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), SDImageCoderDecodePreserveAspectRatio : @(YES) }]; expect(thumbImage).toNot.beNil(); CGFloat ratio = pixelWidth / pixelHeight; + CGFloat thumbnailRatio = thumbnailWidth / thumbnailHeight; CGSize thumbnailPixelSize; - if (pixelWidth > pixelHeight) { - thumbnailPixelSize = CGSizeMake(50, floor(50 / ratio)); + if (ratio > thumbnailRatio) { + thumbnailPixelSize = CGSizeMake(thumbnailWidth, round(thumbnailWidth / ratio)); } else { - thumbnailPixelSize = CGSizeMake(floor(50 * ratio), 50); + thumbnailPixelSize = CGSizeMake(round(thumbnailHeight * ratio), thumbnailHeight); } - expect(thumbImage.size).equal(thumbnailPixelSize); + // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1 + expect(ABS(thumbImage.size.width - thumbnailPixelSize.width) <= 1); + expect(ABS(thumbImage.size.height - thumbnailPixelSize.height) <= 1); if (supportsEncoding) { From 7e3482d4fc43e63116254b98f4f49dec34f94266 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 15 Jan 2020 11:56:19 +0800 Subject: [PATCH 082/181] Fix the issue of `CGImageCreateScaled`, which should use BGRX8888 on non-alpha image, BGRA8888 on alpha image --- SDWebImage/Core/SDImageCoderHelper.m | 7 +++++-- SDWebImage/Core/UIImage+Transform.m | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index de3d0cfc..c29685a9 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -293,12 +293,15 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (input_buffer.data) free(input_buffer.data); if (output_buffer.data) free(output_buffer.data); }; - + BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; + // iOS display alpha info (BGRA8888/BGRX8888) + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; vImage_CGImageFormat format = (vImage_CGImageFormat) { .bitsPerComponent = 8, .bitsPerPixel = 32, .colorSpace = NULL, - .bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrderDefault, + .bitmapInfo = bitmapInfo, .version = 0, .decode = NULL, .renderingIntent = kCGRenderingIntentDefault, diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 53fc334c..b1c3fc13 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -606,7 +606,7 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull .bitsPerComponent = 8, .bitsPerPixel = 32, .colorSpace = NULL, - .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, //requests a BGRA buffer. + .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host, //requests a BGRA buffer. .version = 0, .decode = NULL, .renderingIntent = kCGRenderingIntentDefault From e376dad5f61f6f44620bf6d98885323883a96735 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 15 Jan 2020 12:34:57 +0800 Subject: [PATCH 083/181] Update some wrong code comments --- SDWebImage/Core/SDAnimatedImage.m | 2 +- SDWebImage/Core/UIImage+Transform.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImage.m b/SDWebImage/Core/SDAnimatedImage.m index 422d6578..ce5d5d29 100644 --- a/SDWebImage/Core/SDAnimatedImage.m +++ b/SDWebImage/Core/SDAnimatedImage.m @@ -237,7 +237,7 @@ static CGFloat SDImageScaleFromPath(NSString *string) { return YES; } -#pragma mark - SDAnimatedImage +#pragma mark - SDAnimatedImageProvider - (NSData *)animatedImageData { return [self.animatedCoder animatedImageData]; diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index b1c3fc13..469e6949 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -559,7 +559,7 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull #pragma mark - Image Effect -// We use vImage to do box convolve for performance and support for watchOS. However, you can just use `CIFilter.CIBoxBlur`. For other blur effect, use any filter in `CICategoryBlur` +// We use vImage to do box convolve for performance and support for watchOS. However, you can just use `CIFilter.CIGaussianBlur`. For other blur effect, use any filter in `CICategoryBlur` - (nullable UIImage *)sd_blurredImageWithRadius:(CGFloat)blurRadius { if (self.size.width < 1 || self.size.height < 1) { return nil; From b8b7438ce97eb5983d92e75c4b4820a623601c12 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 15 Jan 2020 21:35:15 +0800 Subject: [PATCH 084/181] Fix the wrong behavior of current sd_blurredImageWithRadius, which calculate the wrong box size for consolve --- SDWebImage/Core/UIImage+Transform.m | 9 ++++----- Tests/Tests/SDImageTransformerTests.m | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 469e6949..a01cf12a 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -569,12 +569,13 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull return self; } + CGFloat scale = self.scale; + CGFloat inputRadius = blurRadius * scale; #if SD_UIKIT || SD_MAC if (self.CIImage) { CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; [filter setValue:self.CIImage forKey:kCIInputImageKey]; - // Blur Radius use pixel count - [filter setValue:@(blurRadius / 2) forKey:kCIInputRadiusKey]; + [filter setValue:@(inputRadius) forKey:kCIInputRadiusKey]; CIImage *ciImage = filter.outputImage; ciImage = [ciImage imageByCroppingToRect:CGRectMake(0, 0, self.size.width, self.size.height)]; #if SD_UIKIT @@ -586,7 +587,6 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull } #endif - CGFloat scale = self.scale; CGImageRef imageRef = self.CGImage; //convert to BGRA if it isn't @@ -640,9 +640,8 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull // // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. // - CGFloat inputRadius = blurRadius * scale; if (inputRadius - 2.0 < __FLT_EPSILON__) inputRadius = 2.0; - uint32_t radius = floor((inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5) / 2); + uint32_t radius = floor(inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5); radius |= 1; // force radius to be odd so that the three box-blur methodology works. int iterations; if (blurRadius * scale < 0.5) iterations = 1; diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index 9bc28ea5..18ab60ac 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -172,7 +172,7 @@ } - (void)test07UIImageTransformBlurWithImage:(UIImage *)testImage { - CGFloat radius = 50; + CGFloat radius = 25; UIImage *blurredImage = [testImage sd_blurredImageWithRadius:radius]; expect(CGSizeEqualToSize(blurredImage.size, testImage.size)).beTruthy(); // Check left color, should be blurred From 3ed7d74e09b8d1ad6c82e0d4527c4e772d0a42c5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 16 Jan 2020 19:03:04 +0800 Subject: [PATCH 085/181] Update the readme about thumbnail decoding --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f18658fb..99e9dc0a 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ This library provides an async image downloader with cache support. For convenie - [x] Categories for `UIImageView`, `UIButton`, `MKAnnotationView` adding web image and cache management - [x] An asynchronous image downloader - [x] An asynchronous memory + disk image caching with automatic cache expiration handling -- [x] A background image decompression +- [x] A background image decompression to avoid frame rate drop - [x] Progressive image loading (including animated image, like GIF showing in Web browser) +- [x] [Thumbnail image decoding](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#thumbnail-decoding-550) to save CPU && Memory for large images - [x] [Extendable image coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-coder-420) to support massive image format, like WebP - [x] [Full-stack solution for animated images](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) which keep a balance between CPU && Memory - [x] [Customizable and composable transformations](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#transformer-50) can be applied to the images right after download From 966e6c3ee4569227ce67434d890bb22073ead2d6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 16 Jan 2020 19:22:07 +0800 Subject: [PATCH 086/181] Bumped version to 5.5.0 Update the CHANGELOG --- CHANGELOG.md | 25 +++++++++++++++++++++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f82b05c..5d84a58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [5.5.0 - Thumbnail Decoding && Core Image, onJan, 16th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.0) +See [all tickets marked for the 5.5.0 release](https://github.com/SDWebImage/SDWebImage/milestone/55) + +### Features + +#### Thumbnail Decoding +- Supports to load the large web image with thumbnail, control the limit size and aspect ratio #2922 #2810 +- Better than resize transformer, which does not allocate full pixel RAM and faster on CPU. If you've already use transformer to generate thumbnail, you'd better have a try +- Works for both animated images and progressive images, each frame using the thumbnail decoding +- Applies for Vector Format like SVG/PDF as well, see more in [Coder Plugin List](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) + +#### Core Image +- Support all transformer method on CIImage based UIImage/NSImage #2918 +- For CIImage based UIImage/NSImage, using the CIFilter to take shortcut, which is faster and lazy (rasterize on demand) + +#### Cache +- Support to use the creation date and the change date to determine the disk cache expire date compare #2915 + +### Performances +- Using UIGraphicsImageRenderer on iOS 10+, save memory when image bitmap is RGB(-25%) or Grayscale(-75%) #2907 +- Provide the polyfill APIs for firmware iOS 10- and macOS. If you already use `SDGraphicsBeginImageContext` for drawing, you'd better replace that instead. + +### Fixes +- Fix Gaussian Blur's bug which take half of the blur radius compared to the standard, should match Core Image's behavior #2927 + ## [5.4.2 - 5.4 Patch, on Jan 7th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.4.2) See [all tickets marked for the 5.4.2 release](https://github.com/SDWebImage/SDWebImage/milestone/58) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 446b443a..990ab9ab 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.4.2' + s.version = '5.5.0' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index d243db93..e1f66d4e 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.4.2 + 5.5.0 CFBundleSignature ???? CFBundleVersion - 5.4.2 + 5.5.0 NSPrincipalClass From 649665e1b0f41c4bc7de7cf945d88763c3106ebf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 14:58:52 +0800 Subject: [PATCH 087/181] Fix the SDAnimatedImageView's progressive animation bug, which reset the frame index to 0 each time new frames available --- SDWebImage/Core/SDAnimatedImageView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index 78f1cb8b..efd2362d 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -169,7 +169,7 @@ self.currentLoopCount = loopCount; // Progressive image reach the current last frame index. Keep the state and pause animating. Wait for later restart if (self.isProgressive) { - NSUInteger lastFrameIndex = self.player.totalFrameCount; + NSUInteger lastFrameIndex = self.player.totalFrameCount - 1; [self.player seekToFrameAtIndex:lastFrameIndex loopCount:0]; [self.player pausePlaying]; } From bce101d112772a7124571e905fe03e1675c801d5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 15:38:02 +0800 Subject: [PATCH 088/181] The progressive animation should not update the loop count to 1 when automatically stopped at last index. --- SDWebImage/Core/SDAnimatedImageView.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index efd2362d..f0b811c4 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -166,12 +166,13 @@ }; self.player.animationLoopHandler = ^(NSUInteger loopCount) { @strongify(self); - self.currentLoopCount = loopCount; // Progressive image reach the current last frame index. Keep the state and pause animating. Wait for later restart if (self.isProgressive) { NSUInteger lastFrameIndex = self.player.totalFrameCount - 1; [self.player seekToFrameAtIndex:lastFrameIndex loopCount:0]; [self.player pausePlaying]; + } else { + self.currentLoopCount = loopCount; } }; From cde0e48a6d11a909fc3bd8fa536290f149069996 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 16:46:00 +0800 Subject: [PATCH 089/181] Add one progressive animation test case to avoid this regression bug in the future --- Tests/Tests/SDAnimatedImageTest.m | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Tests/Tests/SDAnimatedImageTest.m b/Tests/Tests/SDAnimatedImageTest.m index b3fb53f2..0640e3dc 100644 --- a/Tests/Tests/SDAnimatedImageTest.m +++ b/Tests/Tests/SDAnimatedImageTest.m @@ -8,6 +8,7 @@ */ #import "SDTestCase.h" +#import "SDInternalMacros.h" #import static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop count @@ -397,6 +398,76 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun [self waitForExpectationsWithCommonTimeout]; } +- (void)test27AnimatedImageProgressiveAnimation { + XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView progressive animation rendering"]; + + // Simulate progressive download + NSData *fullData = [self testAPNGPData]; + NSUInteger length = fullData.length; + + SDAnimatedImageView *imageView = [SDAnimatedImageView new]; +#if SD_UIKIT + [self.window addSubview:imageView]; +#else + [self.window.contentView addSubview:imageView]; +#endif + + __block NSUInteger previousFrameIndex = 0; + @weakify(imageView); + // Observe to check rendering behavior using frame index + [self.KVOController observe:imageView keyPath:NSStringFromSelector(@selector(currentFrameIndex)) options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary * _Nonnull change) { + @strongify(imageView); + NSUInteger currentFrameIndex = [change[NSKeyValueChangeNewKey] unsignedIntegerValue]; + printf("Animation Frame Index: %lu\n", (unsigned long)currentFrameIndex); + + // The last time should not be progressive + if (currentFrameIndex == 0 && !imageView.isProgressive) { + [self.KVOController unobserve:imageView]; + [expectation fulfill]; + } else { + // Each progressive rendering should render new frame index, no backward and should stop at last frame index + expect(currentFrameIndex - previousFrameIndex).beGreaterThanOrEqualTo(0); + previousFrameIndex = currentFrameIndex; + } + }]; + + SDImageAPNGCoder *coder = [[SDImageAPNGCoder alloc] initIncrementalWithOptions:nil]; + // Setup Data + NSData *setupData = [fullData subdataWithRange:NSMakeRange(0, length / 3.0)]; + [coder updateIncrementalData:setupData finished:NO]; + imageView.shouldIncrementalLoad = YES; + __block SDAnimatedImage *progressiveImage = [[SDAnimatedImage alloc] initWithAnimatedCoder:coder scale:1]; + progressiveImage.sd_isIncremental = YES; + imageView.image = progressiveImage; + expect(imageView.isProgressive).beTruthy(); + + __block NSUInteger partialFrameCount; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // Partial Data + NSData *partialData = [fullData subdataWithRange:NSMakeRange(0, length * 2.0 / 3.0)]; + [coder updateIncrementalData:partialData finished:NO]; + partialFrameCount = [coder animatedImageFrameCount]; + expect(partialFrameCount).beGreaterThan(1); + progressiveImage = [[SDAnimatedImage alloc] initWithAnimatedCoder:coder scale:1]; + progressiveImage.sd_isIncremental = YES; + imageView.image = progressiveImage; + expect(imageView.isProgressive).beTruthy(); + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // Full Data + [coder updateIncrementalData:fullData finished:YES]; + progressiveImage = [[SDAnimatedImage alloc] initWithAnimatedCoder:coder scale:1]; + progressiveImage.sd_isIncremental = NO; + imageView.image = progressiveImage; + NSUInteger fullFrameCount = [coder animatedImageFrameCount]; + expect(fullFrameCount).beGreaterThan(partialFrameCount); + expect(imageView.isProgressive).beFalsy(); + }); + + [self waitForExpectationsWithCommonTimeout]; +} + #pragma mark - Helper - (UIWindow *)window { if (!_window) { From bfa6314732e7fc7817605de44cec1092229bedc5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 20:01:45 +0800 Subject: [PATCH 090/181] Update the readme with progressive loading wiki link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99e9dc0a..58c85e8b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This library provides an async image downloader with cache support. For convenie - [x] An asynchronous image downloader - [x] An asynchronous memory + disk image caching with automatic cache expiration handling - [x] A background image decompression to avoid frame rate drop -- [x] Progressive image loading (including animated image, like GIF showing in Web browser) +- [x] [Progressive image loading](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#progressive-animation) (including animated image, like GIF showing in Web browser) - [x] [Thumbnail image decoding](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#thumbnail-decoding-550) to save CPU && Memory for large images - [x] [Extendable image coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-coder-420) to support massive image format, like WebP - [x] [Full-stack solution for animated images](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) which keep a balance between CPU && Memory From 443bf50b58c0161efe7a6da841524dde1815792d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 20:17:05 +0800 Subject: [PATCH 091/181] Bumped version to 5.5.1 Update the CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d84a58f..ea0ad1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [5.5.1 - 5.0 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.1) +See [all tickets marked for the 5.5.1 release](https://github.com/SDWebImage/SDWebImage/milestone/59) + +### Fixes +- Fix the SDAnimatedImageView's progressive animation bug, which reset the frame index to 0 each time new frames available #2931 + ## [5.5.0 - Thumbnail Decoding && Core Image, onJan, 16th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.0) See [all tickets marked for the 5.5.0 release](https://github.com/SDWebImage/SDWebImage/milestone/55) @@ -23,6 +29,12 @@ See [all tickets marked for the 5.5.0 release](https://github.com/SDWebImage/SDW ### Fixes - Fix Gaussian Blur's bug which take half of the blur radius compared to the standard, should match Core Image's behavior #2927 +## [5.4 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.4.3) +See [all tickets marked for the 5.4.3 release](https://github.com/SDWebImage/SDWebImage/milestone/61) + +### Fixes +- Fix the SDAnimatedImageView's progressive animation bug, which reset the frame index to 0 each time new frames available #2931 + ## [5.4.2 - 5.4 Patch, on Jan 7th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.4.2) See [all tickets marked for the 5.4.2 release](https://github.com/SDWebImage/SDWebImage/milestone/58) @@ -51,6 +63,12 @@ See [all tickets marked for the 5.4.0 release](https://github.com/SDWebImage/SDW - Using one global function to ensure we always sync all the UIImage category associated object status correctly inside our framework #2902 - Fix the thread safe issue with Downloader and DownloaderOperation during cancel #2903 +## [5.3 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.3.4) +See [all tickets marked for the 5.3.4 release](https://github.com/SDWebImage/SDWebImage/milestone/60) + +### Fixes +- Fix the SDAnimatedImageView's progressive animation bug, which reset the frame index to 0 each time new frames available #2931 + ## [5.3 Patch, on Dec 3rd, 2019](https://github.com/rs/SDWebImage/releases/tag/5.3.3) See [all tickets marked for the 5.3.3 release](https://github.com/SDWebImage/SDWebImage/milestone/54) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 990ab9ab..40b17178 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.5.0' + s.version = '5.5.1' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index e1f66d4e..80a6b573 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.5.0 + 5.5.1 CFBundleSignature ???? CFBundleVersion - 5.5.0 + 5.5.1 NSPrincipalClass From 25fe6e97f9ed3d2e578b2a0e1b428fdead522ec2 Mon Sep 17 00:00:00 2001 From: Ben Govero Date: Thu, 23 Jan 2020 17:53:14 -0600 Subject: [PATCH 092/181] Update docs to show correct arguments for SDInternalCompletionBlock --- SDWebImage/Core/SDWebImageManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageManager.h b/SDWebImage/Core/SDWebImageManager.h index d1ab013e..d940f742 100644 --- a/SDWebImage/Core/SDWebImageManager.h +++ b/SDWebImage/Core/SDWebImageManager.h @@ -87,7 +87,7 @@ SDWebImageManager *manager = [SDWebImageManager sharedManager]; [manager loadImageWithURL:imageURL options:0 progress:nil - completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { + completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { if (image) { // do something with image } From d565a3752930dcfc29a47e7b8daf666939d4f219 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 26 Jan 2020 18:05:39 +0800 Subject: [PATCH 093/181] Fix the issue that `maxBufferSize` property does not correctlly works for `SDAnimatedImageView`, should setup the player's property --- SDWebImage/Core/SDAnimatedImageView.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index f0b811c4..e9734383 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -19,6 +19,7 @@ @interface SDAnimatedImageView () { BOOL _initFinished; // Extra flag to mark the `commonInit` is called NSRunLoopMode _runLoopMode; + NSUInteger _maxBufferSize; double _playbackRate; } @@ -153,6 +154,9 @@ // RunLoop Mode self.player.runLoopMode = self.runLoopMode; + // Max Buffer Size + self.player.maxBufferSize = self.maxBufferSize; + // Play Rate self.player.playbackRate = self.playbackRate; @@ -207,6 +211,16 @@ return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; } +- (void)setMaxBufferSize:(NSUInteger)maxBufferSize +{ + _maxBufferSize = maxBufferSize; + self.player.maxBufferSize = maxBufferSize; +} + +- (NSUInteger)maxBufferSize { + return _maxBufferSize; // Defaults to 0 +} + - (void)setPlaybackRate:(double)playbackRate { _playbackRate = playbackRate; From aa7ff6f060d8f34d2c66a716aefff1a3bc9ef816 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 26 Jan 2020 20:20:32 +0800 Subject: [PATCH 094/181] Bumped version to 5.5.2 Update the CHANGELOG --- CHANGELOG.md | 8 +++++++- SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0ad1d5..33987189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [5.5.1 - 5.0 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.1) +## [5.5.2 - 5.5 Patch, on Jan 26th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.2) +See [all tickets marked for the 5.5.2 release](https://github.com/SDWebImage/SDWebImage/milestone/62) + +### Fixes +- Fix the issue that `maxBufferSize` property does not correctlly works for `SDAnimatedImageView` #2934 + +## [5.5.1 - 5.5 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.1) See [all tickets marked for the 5.5.1 release](https://github.com/SDWebImage/SDWebImage/milestone/59) ### Fixes diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 40b17178..0ae96891 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.5.1' + s.version = '5.5.2' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 80a6b573..7163374f 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.5.1 + 5.5.2 CFBundleSignature ???? CFBundleVersion - 5.5.1 + 5.5.2 NSPrincipalClass From cb84dbb273de89ba6e984079097339a474a01c08 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 13:31:57 +0800 Subject: [PATCH 095/181] Added the PDF/SVG image type define --- SDWebImage/Core/NSData+ImageContentType.h | 2 ++ SDWebImage/Core/NSData+ImageContentType.m | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/SDWebImage/Core/NSData+ImageContentType.h b/SDWebImage/Core/NSData+ImageContentType.h index 5bbb4ae6..8c2f97e8 100644 --- a/SDWebImage/Core/NSData+ImageContentType.h +++ b/SDWebImage/Core/NSData+ImageContentType.h @@ -23,6 +23,8 @@ static const SDImageFormat SDImageFormatTIFF = 3; static const SDImageFormat SDImageFormatWebP = 4; static const SDImageFormat SDImageFormatHEIC = 5; static const SDImageFormat SDImageFormatHEIF = 6; +static const SDImageFormat SDImageFormatPDF = 7; +static const SDImageFormat SDImageFormatSVG = 8; /** NSData category about the image content type and UTI. diff --git a/SDWebImage/Core/NSData+ImageContentType.m b/SDWebImage/Core/NSData+ImageContentType.m index 34dd4aa0..b3b8e22b 100644 --- a/SDWebImage/Core/NSData+ImageContentType.m +++ b/SDWebImage/Core/NSData+ImageContentType.m @@ -93,6 +93,12 @@ case SDImageFormatHEIF: UTType = kSDUTTypeHEIF; break; + case SDImageFormatPDF: + UTType = kUTTypePDF; + break; + case SDImageFormatSVG: + UTType = kUTTypeScalableVectorGraphics; + break; default: // default is kUTTypePNG UTType = kUTTypePNG; @@ -120,6 +126,10 @@ imageFormat = SDImageFormatHEIC; } else if (CFStringCompare(uttype, kSDUTTypeHEIF, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatHEIF; + } else if (CFStringCompare(uttype, kUTTypePDF, 0) == kCFCompareEqualTo) { + imageFormat = SDImageFormatPDF; + } else if (CFStringCompare(uttype, kUTTypeScalableVectorGraphics, 0) == kCFCompareEqualTo) { + imageFormat = SDImageFormatSVG; } else { imageFormat = SDImageFormatUndefined; } From 8ca455606655a9f57d9e315f91595eb9235107cf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 13:50:53 +0800 Subject: [PATCH 096/181] Added the `sd_isVector` API on UIImage+Metadata, useful for case when we want to filter the vector/bitmap images. Vector currently only sipports PDF/SVG --- SDWebImage/Core/UIImage+Metadata.h | 10 +++++++- SDWebImage/Core/UIImage+Metadata.m | 41 +++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/SDWebImage/Core/UIImage+Metadata.h b/SDWebImage/Core/UIImage+Metadata.h index e42ff697..8328c261 100644 --- a/SDWebImage/Core/UIImage+Metadata.h +++ b/SDWebImage/Core/UIImage+Metadata.h @@ -28,12 +28,20 @@ /** * UIKit: - * Check the `images` array property + * Check the `images` array property. * AppKit: * NSImage currently only support animated via GIF imageRep unlike UIImage. It will check the imageRep's frame count. */ @property (nonatomic, assign, readonly) BOOL sd_isAnimated; +/** + * UIKit: + * Check the `isSymbolImage` property. Also check the system PDF(iOS 11+) && SVG(iOS 13+) support. + * AppKit: + * NSImage supports PDF && SVG && EPS imageRep, check the imageRep class. + */ +@property (nonatomic, assign, readonly) BOOL sd_isVector; + /** * The image format represent the original compressed image data format. * If you don't manually specify a format, this information is retrieve from CGImage using `CGImageGetUTType`, which may return nil for non-CG based image. At this time it will return `SDImageFormatUndefined` as default value. diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 3c9bf929..ef63c41f 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -32,6 +32,26 @@ return (self.images != nil); } +- (BOOL)sd_isVector { + if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + if (self.isSymbolImage) { + return YES; + } + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + SEL SVGSelector = NSSelectorFromString(@"_CGSVGDocument"); + if ([self respondsToSelector:SVGSelector] && [self performSelector:SVGSelector] != nil) { + return YES; + } + SEL PDFSelector = NSSelectorFromString(@"_CGPDFPage"); + if ([self respondsToSelector:PDFSelector] && [self performSelector:PDFSelector] != nil) { + return YES; + } +#pragma clang diagnostic pop + return NO; +} + #else - (NSUInteger)sd_imageLoopCount { @@ -61,7 +81,7 @@ } - (BOOL)sd_isAnimated { - BOOL isGIF = NO; + BOOL isAnimated = NO; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; @@ -70,9 +90,24 @@ } if (bitmapImageRep) { NSUInteger frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; - isGIF = frameCount > 1 ? YES : NO; + isAnimated = frameCount > 1 ? YES : NO; } - return isGIF; + return isAnimated; +} + +- (BOOL)sd_isVector { + NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); + NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; + if ([imageRep isKindOfClass:[NSPDFImageRep class]]) { + return YES; + } + if ([imageRep isKindOfClass:[NSEPSImageRep class]]) { + return YES; + } + if ([NSStringFromClass(imageRep.class) hasSuffix:@"NSSVGImageRep"]) { + return YES; + } + return NO; } #endif From 08aab785db3ac135e3e3e243bfd81518d75d738e Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 14:04:12 +0800 Subject: [PATCH 097/181] Added the case to detect PDF format from file signature --- SDWebImage/Core/NSData+ImageContentType.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SDWebImage/Core/NSData+ImageContentType.m b/SDWebImage/Core/NSData+ImageContentType.m index b3b8e22b..6bce90b5 100644 --- a/SDWebImage/Core/NSData+ImageContentType.m +++ b/SDWebImage/Core/NSData+ImageContentType.m @@ -65,6 +65,15 @@ } break; } + case 0x25: { + if (data.length >= 4) { + //%PDF + NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 4)] encoding:NSASCIIStringEncoding]; + if ([testString isEqualToString:@"PDF"]) { + return SDImageFormatPDF; + } + } + } } return SDImageFormatUndefined; } From 96b0a2e0316111e0967ebe557fbb2e513f44cf45 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 15:11:42 +0800 Subject: [PATCH 098/181] Added the default ImageIO coder with PDF support, use the screen size if user does not provide any explict pixel size --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 50 ++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index b72dc4e0..1db7a495 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -14,6 +14,9 @@ #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" +// Specify DPI for vector format in CGImageSource, like PDF +static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI"; + @interface SDImageIOCoderFrame : NSObject @property (nonatomic, assign) NSUInteger index; // Frame index (zero based) @@ -158,9 +161,33 @@ exifOrientation = kCGImagePropertyOrientationUp; } + CFStringRef uttype = CGImageSourceGetType(source); + // Check vector format + BOOL isVector = NO; + if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) { + isVector = YES; + } + CGImageRef imageRef; - if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { - imageRef = CGImageSourceCreateImageAtIndex(source, index, NULL); + if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { + NSDictionary *options; + if (isVector) { + if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { + // Provide the default pixel count for vector images, simply just use the screen size +#if SD_WATCH + thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size; +#elif SD_UIKIT + thumbnailSize = UIScreen.mainScreen.bounds.size; +#elif SD_MAC + thumbnailSize = NSScreen.mainScreen.frame.size; +#endif + } + CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); + NSUInteger DPIPerPixel = 2; + NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel; + options = @{kSDCGImageSourceRasterizationDPI : @(rasterizationDPI)}; + } + imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)options); } else { NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); @@ -179,21 +206,22 @@ thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); + } + if (!imageRef) { + return nil; + } + + if (thumbnailSize.width > 0 && thumbnailSize.height > 0) { if (preserveAspectRatio) { // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice exifOrientation = kCGImagePropertyOrientationUp; } else { // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size - if (imageRef) { - CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize]; - CGImageRelease(imageRef); - imageRef = scaledImageRef; - } + CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize]; + CGImageRelease(imageRef); + imageRef = scaledImageRef; } } - if (!imageRef) { - return nil; - } #if SD_UIKIT || SD_WATCH UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; @@ -363,7 +391,7 @@ if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } - image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (image) { image.sd_imageFormat = self.class.imageFormat; } From 8c6556e835386199af0cbf80b55a891bda92dbab Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 15:29:15 +0800 Subject: [PATCH 099/181] Fix the PDF data detection --- SDWebImage/Core/NSData+ImageContentType.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/NSData+ImageContentType.m b/SDWebImage/Core/NSData+ImageContentType.m index 6bce90b5..4db1ff5b 100644 --- a/SDWebImage/Core/NSData+ImageContentType.m +++ b/SDWebImage/Core/NSData+ImageContentType.m @@ -68,7 +68,7 @@ case 0x25: { if (data.length >= 4) { //%PDF - NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 4)] encoding:NSASCIIStringEncoding]; + NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding]; if ([testString isEqualToString:@"PDF"]) { return SDImageFormatPDF; } From eeec6de69840767bdff8fac6299d10b6fc469836 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 15:29:57 +0800 Subject: [PATCH 100/181] Update the PDF demo and test cases --- .../SDWebImage Demo/MasterViewController.m | 1 + .../project.pbxproj | 8 +++++ Tests/Tests/Images/TestImage.pdf | Bin 0 -> 1018 bytes Tests/Tests/SDImageCoderTests.m | 33 +++++++++++++++--- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 Tests/Tests/Images/TestImage.pdf diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index f131f55f..b82b17c0 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -75,6 +75,7 @@ @"https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic", @"https://nokiatech.github.io/heif/content/image_sequences/starfield_animation.heic", @"https://s2.ax1x.com/2019/11/01/KHYIgJ.gif", + @"https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf", @"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png", @"http://via.placeholder.com/200x200.jpg", nil]; diff --git a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj index c98bbde1..ac80009d 100644 --- a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj +++ b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 322241802272F808002429DB /* SDUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3222417E2272F808002429DB /* SDUtilsTests.m */; }; 3226ECBB20754F7700FAFACF /* SDWebImageTestDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3226ECBA20754F7700FAFACF /* SDWebImageTestDownloadOperation.m */; }; 3226ECBC20754F7700FAFACF /* SDWebImageTestDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3226ECBA20754F7700FAFACF /* SDWebImageTestDownloadOperation.m */; }; + 3234306223E2BAC800C290C8 /* TestImage.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3234306123E2BAC800C290C8 /* TestImage.pdf */; }; + 3234306323E2BAC800C290C8 /* TestImage.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3234306123E2BAC800C290C8 /* TestImage.pdf */; }; + 3234306423E2BAC800C290C8 /* TestImage.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3234306123E2BAC800C290C8 /* TestImage.pdf */; }; 323B8E1F20862322008952BE /* SDWebImageTestLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 323B8E1E20862322008952BE /* SDWebImageTestLoader.m */; }; 323B8E2020862322008952BE /* SDWebImageTestLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 323B8E1E20862322008952BE /* SDWebImageTestLoader.m */; }; 324047442271956F007C53E1 /* TestEXIF.png in Resources */ = {isa = PBXBuildFile; fileRef = 324047432271956F007C53E1 /* TestEXIF.png */; }; @@ -107,6 +110,7 @@ 3222417E2272F808002429DB /* SDUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDUtilsTests.m; sourceTree = ""; }; 3226ECB920754F7700FAFACF /* SDWebImageTestDownloadOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestDownloadOperation.h; sourceTree = ""; }; 3226ECBA20754F7700FAFACF /* SDWebImageTestDownloadOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDWebImageTestDownloadOperation.m; sourceTree = ""; }; + 3234306123E2BAC800C290C8 /* TestImage.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = TestImage.pdf; sourceTree = ""; }; 323B8E1D20862322008952BE /* SDWebImageTestLoader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestLoader.h; sourceTree = ""; }; 323B8E1E20862322008952BE /* SDWebImageTestLoader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDWebImageTestLoader.m; sourceTree = ""; }; 324047432271956F007C53E1 /* TestEXIF.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TestEXIF.png; sourceTree = ""; }; @@ -238,6 +242,7 @@ 433BBBB81D7EF8260086B6E9 /* TestImage.png */, 327A418B211D660600495442 /* TestImage.heic */, 32905E63211D786E00460FCF /* TestImage.heif */, + 3234306123E2BAC800C290C8 /* TestImage.pdf */, 327054E1206CEFF3006EA328 /* TestImageAnimated.apng */, 3297A09E23374D1600814590 /* TestImageAnimated.heic */, ); @@ -443,6 +448,7 @@ 3299228B2365DC6C00EAFD97 /* TestImage.heic in Resources */, 329922872365DC6C00EAFD97 /* TestLoopCount.gif in Resources */, 3299228C2365DC6C00EAFD97 /* TestImage.heif in Resources */, + 3234306423E2BAC800C290C8 /* TestImage.pdf in Resources */, 329922892365DC6C00EAFD97 /* TestImageLarge.jpg in Resources */, 3299228A2365DC6C00EAFD97 /* TestImage.png in Resources */, 329922842365DC6C00EAFD97 /* MonochromeTestImage.jpg in Resources */, @@ -461,6 +467,7 @@ 32B99EA3203B31360017FD66 /* TestImage.gif in Resources */, 324047452271956F007C53E1 /* TestEXIF.png in Resources */, 32B99EA4203B31360017FD66 /* TestImage.jpg in Resources */, + 3234306323E2BAC800C290C8 /* TestImage.pdf in Resources */, 32B99EA6203B31360017FD66 /* TestImage.png in Resources */, 3297A0A023374D1700814590 /* TestImageAnimated.heic in Resources */, 32B99EA2203B31360017FD66 /* MonochromeTestImage.jpg in Resources */, @@ -479,6 +486,7 @@ 5F7F38AD1AE2A77A00B0E330 /* TestImage.jpg in Resources */, 32905E64211D786E00460FCF /* TestImage.heif in Resources */, 43828A451DA67F9900000E62 /* TestImageLarge.jpg in Resources */, + 3234306223E2BAC800C290C8 /* TestImage.pdf in Resources */, 433BBBB71D7EF8200086B6E9 /* TestImage.gif in Resources */, 433BBBB91D7EF8260086B6E9 /* TestImage.png in Resources */, 3297A09F23374D1700814590 /* TestImageAnimated.heic in Resources */, diff --git a/Tests/Tests/Images/TestImage.pdf b/Tests/Tests/Images/TestImage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..132681b067d4be6b5415f062924037811b926d9a GIT binary patch literal 1018 zcma)5O=uHA6c!I6>kmb-t$KMvqb+E5lkFyH357OIg1-$LMZ^{zH`8P#-HEf4k}6(2 zhzhnK>Y>!1s#HAGqTofL2t9~+5PA_24;H)#MGt}pf8T6=8rqA)FthW`oAitZz#TJi;sUQI%vR7omQezP!CWiD{|0|kf{KGny~YNa@_Mo&|Ye>3{|?i6jyLc zhLqC8k+JTLt9PuA|Cm^~b9a8eF>*1fwTy0W`to}-Y2NvK;l`iYshNgH<4t{+TKva` zR=LA-=jJ{)J~@A(dFz8~&9@sSGyHo0>!&@zk>Nu37@y9a+_mMzt(TYk-;7^*I5YX; z>QU=g-`wb##2P)lY4M)_{ry)T4(;o-*1h|_d&-EL(`((2ZwEGv{nfvI+BSRhTR5pu zCsW~vUs9QrBX<-OXz!5s@)WG;_LRm;BAFJb5~5KF;82N%2T)_(5CGL8xT1Evz!hi3 zk5Tgj>fwK=eZHt=89toCtt0r|>kz3RSsotzpAvspNU`d7R zQQL24Tx2YTnl-jXDXh*xsLnwQyF)MztroF4gMdGv`Ge4!6QaaDa#G0Img{Fc z+t%iVNpY4bTG(y>|7@uW;kxi;{xKp*o@L9fu_A7z4oh!JoDM1)L=>$yAY>T20fSJ* z3?1ts2RJn>7+Q&G0Zh+wyLQa#GE7JB%Q2%PvH}YS>+A$4cAj$Y7fst|DN>g0X)-22 e3NN?IUT2VjEQ429R}d~dh2d~m@%iF?3FRM0)gsma literal 0 HcmV?d00001 diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 23dc6d56..95b8b5a2 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -156,22 +156,34 @@ withLocalImageURL:heicURL supportsEncoding:supportsEncoding encodingFormat:SDImageFormatHEIC - isAnimatedImage:isAnimatedImage]; + isAnimatedImage:isAnimatedImage + isVectorImage:NO]; } } +- (void)test17ThatPDFWorks { + NSURL *pdfURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"pdf"]; + [self verifyCoder:[SDImageIOCoder sharedCoder] + withLocalImageURL:pdfURL + supportsEncoding:NO + encodingFormat:SDImageFormatUndefined + isAnimatedImage:NO + isVectorImage:YES]; +} + - (void)verifyCoder:(id)coder withLocalImageURL:(NSURL *)imageUrl supportsEncoding:(BOOL)supportsEncoding isAnimatedImage:(BOOL)isAnimated { - [self verifyCoder:coder withLocalImageURL:imageUrl supportsEncoding:supportsEncoding encodingFormat:SDImageFormatUndefined isAnimatedImage:isAnimated]; + [self verifyCoder:coder withLocalImageURL:imageUrl supportsEncoding:supportsEncoding encodingFormat:SDImageFormatUndefined isAnimatedImage:isAnimated isVectorImage:NO]; } - (void)verifyCoder:(id)coder withLocalImageURL:(NSURL *)imageUrl supportsEncoding:(BOOL)supportsEncoding encodingFormat:(SDImageFormat)encodingFormat - isAnimatedImage:(BOOL)isAnimated { + isAnimatedImage:(BOOL)isAnimated + isVectorImage:(BOOL)isVector { NSData *inputImageData = [NSData dataWithContentsOfURL:imageUrl]; expect(inputImageData).toNot.beNil(); SDImageFormat inputImageFormat = [NSData sd_imageFormatForImageData:inputImageData]; @@ -204,7 +216,18 @@ withLocalImageURL:(NSURL *)imageUrl CGFloat pixelHeight = inputImage.size.height; expect(pixelWidth).beGreaterThan(0); expect(pixelHeight).beGreaterThan(0); - // check thumnail with scratch + // check vector format supports thumbnail with screen size + if (isVector) { +#if SD_UIKIT + CGFloat maxScreenSize = MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height); +#else + CGFloat maxScreenSize = MAX(NSScreen.mainScreen.frame.size.width, NSScreen.mainScreen.frame.size.height); +#endif + expect(pixelWidth).equal(maxScreenSize); + expect(pixelHeight).equal(maxScreenSize); + } + + // check thumbnail with scratch CGFloat thumbnailWidth = 50; CGFloat thumbnailHeight = 50; UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ @@ -213,7 +236,7 @@ withLocalImageURL:(NSURL *)imageUrl }]; expect(thumbImage).toNot.beNil(); expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight)); - // check thumnail with aspect ratio limit + // check thumbnail with aspect ratio limit thumbImage = [coder decodedImageWithData:inputImageData options:@{ SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), SDImageCoderDecodePreserveAspectRatio : @(YES) From ef2373668e1db302502aa23c9d43291d38b41fe6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 15:56:25 +0800 Subject: [PATCH 101/181] Fix the Xcode 10 support using runtime selector, the force decode feature does not process on vector image format --- SDWebImage/Core/SDImageCoderHelper.m | 4 ++++ SDWebImage/Core/UIImage+Metadata.m | 28 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index c29685a9..7bba1025 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -575,6 +575,10 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (image.sd_isAnimated) { return NO; } + // do not decode vector images + if (image.sd_isVector) { + return NO; + } return YES; } diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index ef63c41f..2d24c4e2 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -32,25 +32,31 @@ return (self.images != nil); } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - (BOOL)sd_isVector { if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { - if (self.isSymbolImage) { + // Xcode 11 supports symbol image, keep Xcode 10 compatible currently + SEL SymbolSelector = NSSelectorFromString(@"isSymbolImage"); + if ([self respondsToSelector:SymbolSelector] && [self performSelector:SymbolSelector]) { + return YES; + } + // SVG + SEL SVGSelector = NSSelectorFromString(@"_CGSVGDocument"); + if ([self respondsToSelector:SVGSelector] && [self performSelector:SVGSelector]) { return YES; } } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - SEL SVGSelector = NSSelectorFromString(@"_CGSVGDocument"); - if ([self respondsToSelector:SVGSelector] && [self performSelector:SVGSelector] != nil) { - return YES; + if (@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)) { + // PDF + SEL PDFSelector = NSSelectorFromString(@"_CGPDFPage"); + if ([self respondsToSelector:PDFSelector] && [self performSelector:PDFSelector]) { + return YES; + } } - SEL PDFSelector = NSSelectorFromString(@"_CGPDFPage"); - if ([self respondsToSelector:PDFSelector] && [self performSelector:PDFSelector] != nil) { - return YES; - } -#pragma clang diagnostic pop return NO; } +#pragma clang diagnostic pop #else From 5629af83303e6d4c219a4a97a1ec4cfae111df91 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 16:09:46 +0800 Subject: [PATCH 102/181] Added `SDWebImageTransformVectorImage`, which can allows the transformer to transform the vector image format, although most coders works for vector format (if you don't grab CGImage), some are not --- SDWebImage/Core/SDWebImageDefine.h | 6 ++++++ SDWebImage/Core/SDWebImageManager.m | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index bd4d4e68..568de147 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -195,6 +195,12 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { * Note if you use this when using the custom cache serializer, or using the transformer, we will also wait until the output image data written is finished. */ SDWebImageWaitStoreCache = 1 << 22, + + /** + * We usually don't apply transform on vector images, because vector images supports dynamically changing to any size, rasterize to a fixed size will loss details. To modify vector images, you can process the vector data at runtime (such as modifying PDF tag / SVG element). + * Use this flag to transform them anyway. + */ + SDWebImageTransformVectorImage = 1 << 23, }; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 97ffb488..4fcb0ab1 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -332,7 +332,9 @@ static id _defaultImageLoader; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; - BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; + BOOL shouldTransformImage = downloadedImage && transformer; + shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); + shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage)); BOOL shouldCacheOriginal = downloadedImage && finished; BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); @@ -380,7 +382,9 @@ static id _defaultImageLoader; NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; - BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; + BOOL shouldTransformImage = originalImage && transformer; + shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); + shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage)); BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store transformed image to cache if (shouldTransformImage) { From bb424d44fdd8a4d41c43c86387de3b32e1941cf8 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 16:52:55 +0800 Subject: [PATCH 103/181] Added the URLSessionTaskMetrics support for downloader && operation, which can be used for network metrics --- SDWebImage/Core/SDWebImageDownloader.m | 9 +++++++++ SDWebImage/Core/SDWebImageDownloaderOperation.h | 7 +++++++ SDWebImage/Core/SDWebImageDownloaderOperation.m | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 94bfa049..c836cc59 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -498,6 +498,15 @@ didReceiveResponse:(NSURLResponse *)response } } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + + // Identify the operation that runs this task and pass it the delegate method + NSOperation *dataOperation = [self operationWithTask:task]; + if ([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) { + [dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics]; + } +} + @end @implementation SDWebImageDownloadToken diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.h b/SDWebImage/Core/SDWebImageDownloaderOperation.h index e987ba42..3b93aa71 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.h +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.h @@ -36,6 +36,7 @@ @optional @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; +@property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (strong, nonatomic, nullable) NSURLCredential *credential; @property (assign, nonatomic) double minimumProgressInterval; @@ -62,6 +63,12 @@ */ @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; +/** + * The collected metrics from `-URLSession:task:didFinishCollectingMetrics:`. + * This can be used to collect the network metrics like download duration, DNS lookup duration, SSL handshake dureation, etc. See Apple's documentation: https://developer.apple.com/documentation/foundation/urlsessiontaskmetrics + */ +@property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + /** * The credential used for authentication challenges in `-URLSession:task:didReceiveChallenge:completionHandler:`. * diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 6527eddd..355e207e 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -52,6 +52,8 @@ typedef NSMutableDictionary SDCallbacksDictionary; @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; +@property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + @property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding #if SD_UIKIT @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; @@ -512,6 +514,10 @@ didReceiveResponse:(NSURLResponse *)response } } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + self.metrics = metrics; +} + #pragma mark Helper methods + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions { SDWebImageOptions options = 0; From ed894ecff59aa5acb3a11dfb21490f974a76066f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 17:32:46 +0800 Subject: [PATCH 104/181] Added the metrics in the download token, make it easy to grab the information right in completion block (is this useful ?) --- SDWebImage/Core/SDWebImageDownloader.h | 5 +++++ SDWebImage/Core/SDWebImageDownloader.m | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDownloader.h b/SDWebImage/Core/SDWebImageDownloader.h index 571b72a2..a365395c 100644 --- a/SDWebImage/Core/SDWebImageDownloader.h +++ b/SDWebImage/Core/SDWebImageDownloader.h @@ -128,6 +128,11 @@ typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock; */ @property (nonatomic, strong, nullable, readonly) NSURLResponse *response; +/** + The download's metrics. This will be nil if download operation does not support metrics. + */ +@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + @end diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index c836cc59..d7deee95 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -24,6 +24,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; @property (nonatomic, strong, nullable, readwrite) NSURL *url; @property (nonatomic, strong, nullable, readwrite) NSURLRequest *request; @property (nonatomic, strong, nullable, readwrite) NSURLResponse *response; +@property (nonatomic, strong, nullable, readwrite) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (nonatomic, weak, nullable, readwrite) id downloadOperationCancelToken; @property (nonatomic, weak, nullable) NSOperation *downloadOperation; @property (nonatomic, assign, getter=isCancelled) BOOL cancelled; @@ -519,18 +520,30 @@ didReceiveResponse:(NSURLResponse *)response self = [super init]; if (self) { _downloadOperation = downloadOperation; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:downloadOperation]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:downloadOperation]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidStop:) name:SDWebImageDownloadStopNotification object:downloadOperation]; } return self; } -- (void)downloadReceiveResponse:(NSNotification *)notification { +- (void)downloadDidReceiveResponse:(NSNotification *)notification { NSOperation *downloadOperation = notification.object; if (downloadOperation && downloadOperation == self.downloadOperation) { self.response = downloadOperation.response; } } +- (void)downloadDidStop:(NSNotification *)notification { + NSOperation *downloadOperation = notification.object; + if (downloadOperation && downloadOperation == self.downloadOperation) { + if ([downloadOperation respondsToSelector:@selector(metrics)]) { + if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) { + self.metrics = downloadOperation.metrics; + } + } + } +} + - (void)cancel { @synchronized (self) { if (self.isCancelled) { From d56636e15b2d106f275ae9ea0887a0da04b7024d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 18:33:16 +0800 Subject: [PATCH 105/181] Update the Example and Test case about URLSessionMetrics, expose the API in UIVIew+WebCache to make it easy to write code (or user have to write NSStringFromClass) --- .../SDWebImage Demo/MasterViewController.m | 20 +++++++++--- SDWebImage/Core/UIView+WebCache.h | 7 +++++ Tests/Tests/SDWebImageDownloaderTests.m | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index f131f55f..f3aecbea 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -113,10 +113,22 @@ } cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; - [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] - placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 - context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}]; + __weak SDAnimatedImageView *imageView = cell.customImageView; + [imageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] + placeholderImage:placeholderImage + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 + context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))} + progress:nil + completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { + SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey]; + SDWebImageDownloadToken *token = operation.loaderOperation; + if (@available(iOS 10.0, *)) { + NSURLSessionTaskMetrics *metrics = token.metrics; + if (metrics) { + printf("Metrics: %s download in (%f) seconds\n", [imageURL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], metrics.taskInterval.duration); + } + } + }]; return cell; } diff --git a/SDWebImage/Core/UIView+WebCache.h b/SDWebImage/Core/UIView+WebCache.h index d0a7966f..c7e12a47 100644 --- a/SDWebImage/Core/UIView+WebCache.h +++ b/SDWebImage/Core/UIView+WebCache.h @@ -31,6 +31,13 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL; +/** + * Get the current image operation key. Operation key is used to identify the different queries for one view instance (like UIButton). + * See more about this in `SDWebImageContextSetImageOperationKey`. + * @note You can use method `UIView+WebCacheOperation` to invesigate different queries' operation. + */ +@property (nonatomic, strong, readonly, nullable) NSString *sd_latestOperationKey; + /** * The current image loading progress associated to the view. The unit count is the received size and excepted size of download. * The `totalUnitCount` and `completedUnitCount` will be reset to 0 after a new image loading start (change from current queue). And they will be set to `SDWebImageProgressUnitCountUnknown` if the progressBlock not been called but the image loading success to mark the progress finished (change from main queue). diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m index e5f0a2b5..467623ac 100644 --- a/Tests/Tests/SDWebImageDownloaderTests.m +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -642,6 +642,37 @@ }]; } +- (void)test26DownloadURLSessionMetrics { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download URLSessionMetrics works"]; + + SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] init]; + + __block SDWebImageDownloadToken *token; + token = [downloader downloadImageWithURL:[NSURL URLWithString:kTestJPEGURL] completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) { + expect(error).beNil(); + if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, *)) { + NSURLSessionTaskMetrics *metrics = token.metrics; + expect(metrics).notTo.beNil(); + expect(metrics.redirectCount).equal(0); + expect(metrics.transactionMetrics.count).equal(1); + NSURLSessionTaskTransactionMetrics *metric = metrics.transactionMetrics.firstObject; + // Metrcis Test + expect(metric.fetchStartDate).notTo.beNil(); + expect(metric.connectStartDate).notTo.beNil(); + expect(metric.connectEndDate).notTo.beNil(); + expect(metric.networkProtocolName).equal(@"http/1.1"); + expect(metric.resourceFetchType).equal(NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad); + expect(metric.isProxyConnection).beFalsy(); + expect(metric.isReusedConnection).beFalsy(); + } + [expectation1 fulfill]; + }]; + + [self waitForExpectationsWithCommonTimeoutUsingHandler:^(NSError * _Nullable error) { + [downloader invalidateSessionAndCancel:YES]; + }]; +} + #pragma mark - SDWebImageLoader - (void)test30CustomImageLoaderWorks { XCTestExpectation *expectation = [self expectationWithDescription:@"Custom image not works"]; From 4d354c4acdc0ba8e3db1af8fc687a8bf450a46ee Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 12 Feb 2020 12:13:04 +0800 Subject: [PATCH 106/181] Make the SDAniamtedImage response to the UIImage+Metadata category method, which should return the status matching the behavior --- SDWebImage/Core/SDAnimatedImage.m | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/SDWebImage/Core/SDAnimatedImage.m b/SDWebImage/Core/SDAnimatedImage.m index ce5d5d29..d27e2c4b 100644 --- a/SDWebImage/Core/SDAnimatedImage.m +++ b/SDWebImage/Core/SDAnimatedImage.m @@ -12,6 +12,7 @@ #import "SDImageCodersManager.h" #import "SDImageFrame.h" #import "UIImage+MemoryCacheCost.h" +#import "UIImage+Metadata.h" #import "SDImageAssetManager.h" #import "objc/runtime.h" @@ -298,3 +299,31 @@ static CGFloat SDImageScaleFromPath(NSString *string) { } @end + +@implementation SDAnimatedImage (Metadata) + +- (BOOL)sd_isAnimated { + return YES; +} + +- (NSUInteger)sd_imageLoopCount { + return self.animatedImageLoopCount; +} + +- (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { + return; +} + +- (SDImageFormat)sd_imageFormat { + return self.animatedImageFormat; +} + +- (void)setSd_imageFormat:(SDImageFormat)sd_imageFormat { + return; +} + +- (BOOL)sd_isVector { + return NO; +} + +@end From 4acd81177bc5d906ab9fad6e48f194ca6ec02b57 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 12 Feb 2020 12:35:48 +0800 Subject: [PATCH 107/181] Added macros to expand SPI symbol to Selector, which can make it easy to distinguish and maintain in the future --- SDWebImage/Core/SDAnimatedImageView.m | 4 ++-- SDWebImage/Core/UIImage+Metadata.m | 5 +++-- SDWebImage/Private/SDInternalMacros.h | 12 ++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index e9734383..71ee7e34 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -470,10 +470,10 @@ // NSImageView use a subview. We need this subview's layer for actual rendering. // Why using this design may because of properties like `imageAlignment` and `imageScaling`, which it's not available for UIImageView.contentMode (it's impossible to align left and keep aspect ratio at the same time) - (NSView *)imageView { - NSImageView *imageView = imageView = objc_getAssociatedObject(self, NSSelectorFromString(@"_imageView")); + NSImageView *imageView = imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView)); if (!imageView) { // macOS 10.14 - imageView = objc_getAssociatedObject(self, NSSelectorFromString(@"_imageSubview")); + imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageSubview)); } return imageView; } diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 2d24c4e2..09724236 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -8,6 +8,7 @@ #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" +#import "SDInternalMacros.h" #import "objc/runtime.h" @implementation UIImage (Metadata) @@ -42,14 +43,14 @@ return YES; } // SVG - SEL SVGSelector = NSSelectorFromString(@"_CGSVGDocument"); + SEL SVGSelector = SD_SEL_SPI(CGSVGDocument); if ([self respondsToSelector:SVGSelector] && [self performSelector:SVGSelector]) { return YES; } } if (@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)) { // PDF - SEL PDFSelector = NSSelectorFromString(@"_CGPDFPage"); + SEL PDFSelector = SD_SEL_SPI(CGPDFPage); if ([self respondsToSelector:PDFSelector] && [self performSelector:PDFSelector]) { return YES; } diff --git a/SDWebImage/Private/SDInternalMacros.h b/SDWebImage/Private/SDInternalMacros.h index 837d77b0..aad700f8 100644 --- a/SDWebImage/Private/SDInternalMacros.h +++ b/SDWebImage/Private/SDInternalMacros.h @@ -21,6 +21,18 @@ #define SD_OPTIONS_CONTAINS(options, value) (((options) & (value)) == (value)) #endif +#ifndef SD_CSTRING +#define SD_CSTRING(str) #str +#endif + +#ifndef SD_NSSTRING +#define SD_NSSTRING(str) @(SD_CSTRING(str)) +#endif + +#ifndef SD_SEL_SPI +#define SD_SEL_SPI(name) NSSelectorFromString([NSString stringWithFormat:@"_%@", SD_NSSTRING(name)]) +#endif + #ifndef weakify #define weakify(...) \ sd_keywordify \ From ac4dcbe316a91d1cc9f06d2687f88790eaa4bbb4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 26 Feb 2020 12:14:50 +0800 Subject: [PATCH 108/181] Copy the SVG detection from SVGCoder to the utils --- SDWebImage/Core/NSData+ImageContentType.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SDWebImage/Core/NSData+ImageContentType.m b/SDWebImage/Core/NSData+ImageContentType.m index 4db1ff5b..f9014480 100644 --- a/SDWebImage/Core/NSData+ImageContentType.m +++ b/SDWebImage/Core/NSData+ImageContentType.m @@ -17,6 +17,7 @@ // Currently Image/IO does not support WebP #define kSDUTTypeWebP ((__bridge CFStringRef)@"public.webp") +#define kSVGTagEnd @"" @implementation NSData (ImageContentType) @@ -74,6 +75,15 @@ } } } + case 0x3C: { + if (data.length > 100) { + // Check end with SVG tag + NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(data.length - 100, 100)] encoding:NSASCIIStringEncoding]; + if ([testString containsString:kSVGTagEnd]) { + return SDImageFormatSVG; + } + } + } } return SDImageFormatUndefined; } From c0f796aa779e5e016be9a81bf96f8baca699c552 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 27 Feb 2020 11:20:52 +0800 Subject: [PATCH 109/181] Try to fix the issue caused by Swift PM 5.2, the `sources` DSL only matches the individual source files, but not folder --- Package.swift | 18 ++++-------------- SDWebImage/MapKit/MKAnnotationView+WebCache.m | 8 ++------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Package.swift b/Package.swift index 55da4f0b..50390b74 100644 --- a/Package.swift +++ b/Package.swift @@ -30,24 +30,14 @@ let package = Package( .target( name: "SDWebImage", dependencies: [], - path: ".", - sources: ["SDWebImage/Core", "SDWebImage/Private"], - publicHeadersPath: "SDWebImage/Core", - cSettings: [ - .headerSearchPath("SDWebImage/Core"), - .headerSearchPath("SDWebImage/Private") - ] + path: "SDWebImage", + exclude: ["MapKit"], + publicHeadersPath: "Core" ), .target( name: "SDWebImageMapKit", dependencies: ["SDWebImage"], - path: ".", - sources: ["SDWebImage/MapKit"], - publicHeadersPath: "SDWebImage/MapKit", - cSettings: [ - .headerSearchPath("SDWebImage/Core"), - .headerSearchPath("SDWebImage/Private") - ] + path: "SDWebImage/MapKit" ) ] ) diff --git a/SDWebImage/MapKit/MKAnnotationView+WebCache.m b/SDWebImage/MapKit/MKAnnotationView+WebCache.m index adc02a92..11b91b19 100644 --- a/SDWebImage/MapKit/MKAnnotationView+WebCache.m +++ b/SDWebImage/MapKit/MKAnnotationView+WebCache.m @@ -10,10 +10,7 @@ #if SD_UIKIT || SD_MAC -#import "objc/runtime.h" -#import "UIView+WebCacheOperation.h" #import "UIView+WebCache.h" -#import "SDInternalMacros.h" @implementation MKAnnotationView (WebCache) @@ -55,14 +52,13 @@ context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { - @weakify(self); + __weak typeof(self) wself = self; [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:context setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { - @strongify(self); - self.image = image; + wself.image = image; } progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { From faf82c1e1a1d987f6beb6c93fe3011d1534bc138 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 27 Feb 2020 15:39:37 +0800 Subject: [PATCH 110/181] Complete all the SDWebImage error code with the localized description, make it easy for debugging --- SDWebImage/Core/SDWebImageDownloaderOperation.m | 16 ++++++++-------- SDWebImage/Core/SDWebImageError.h | 2 +- SDWebImage/Core/SDWebImageManager.m | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 355e207e..9411dcac 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -140,7 +140,7 @@ typedef NSMutableDictionary SDCallbacksDictionary; SDWebImageDownloaderCompletedBlock completedBlock = [token valueForKey:kCompletedCallbackKey]; dispatch_main_async_safe(^{ if (completedBlock) { - completedBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil], YES); + completedBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}], YES); } }); } @@ -152,7 +152,7 @@ typedef NSMutableDictionary SDCallbacksDictionary; if (self.isCancelled) { self.finished = YES; // Operation cancelled by user before sending the request - [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil]]; + [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]]; [self reset]; return; } @@ -246,8 +246,8 @@ typedef NSMutableDictionary SDCallbacksDictionary; if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; } - // Operation cancelled by user before sending the request - [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil]]; + // Operation cancelled by user during sending the request + [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]]; [self reset]; } @@ -309,7 +309,7 @@ didReceiveResponse:(NSURLResponse *)response response = [self.responseModifier modifiedResponseWithResponse:response]; if (!response) { valid = NO; - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:nil]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}]; } } @@ -323,13 +323,13 @@ didReceiveResponse:(NSURLResponse *)response BOOL statusCodeValid = statusCode >= 200 && statusCode < 400; if (!statusCodeValid) { valid = NO; - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}]; } //'304 Not Modified' is an exceptional one //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check if (statusCode == 304 && !self.cachedData) { valid = NO; - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Download response status code is 304 not modified and ignored"}]; } if (valid) { @@ -455,7 +455,7 @@ didReceiveResponse:(NSURLResponse *)response * then we should check if the cached data is equal to image data */ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) { - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}]; // call completion block with not modified error [self callCompletionBlocksWithError:self.responseError]; [self done]; diff --git a/SDWebImage/Core/SDWebImageError.h b/SDWebImage/Core/SDWebImageError.h index 3c08cc9d..e935ac97 100644 --- a/SDWebImage/Core/SDWebImageError.h +++ b/SDWebImage/Core/SDWebImageError.h @@ -22,5 +22,5 @@ typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) { SDWebImageErrorInvalidDownloadOperation = 2000, // The image download operation is invalid, such as nil operation or unexpected error occur when operation initialized SDWebImageErrorInvalidDownloadStatusCode = 2001, // The image download response a invalid status code. You can check the status code in error's userInfo under `SDWebImageErrorDownloadStatusCodeKey` SDWebImageErrorCancelled = 2002, // The image loading operation is cancelled before finished, during either async disk cache query, or waiting before actual network request. For actual network request error, check `NSURLErrorDomain` error domain and code. - SDWebImageErrorInvalidDownloadResponse = 2003, // When using response modifier, the modified download response is nil and marked as cancelled. + SDWebImageErrorInvalidDownloadResponse = 2003, // When using response modifier, the modified download response is nil and marked as failed. }; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 4fcb0ab1..c5502cc3 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -220,7 +220,7 @@ static id _defaultImageLoader; @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user - [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url]; + [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url]; [self safelyRemoveOperationFromRunning:operation]; return; } @@ -269,7 +269,7 @@ static id _defaultImageLoader; @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user - [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url]; + [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url]; } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) { // Image refresh hit the NSURLCache cache, do not call the completion block } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) { From b395243d378c5193088d38fd6333dba490085585 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 27 Feb 2020 15:55:53 +0800 Subject: [PATCH 111/181] Garden all the private headers with description and null-ability annotation --- SDWebImage/Private/SDAsyncBlockOperation.h | 1 + SDWebImage/Private/SDDeviceHelper.h | 1 + SDWebImage/Private/SDDisplayLink.h | 5 ++--- SDWebImage/Private/SDFileAttributeHelper.h | 11 ++++++----- SDWebImage/Private/SDImageAssetManager.h | 4 ++-- SDWebImage/Private/SDImageCachesManagerOperation.h | 2 +- SDWebImage/Private/SDWeakProxy.h | 1 + 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Private/SDAsyncBlockOperation.h b/SDWebImage/Private/SDAsyncBlockOperation.h index ecc68be8..a3480deb 100644 --- a/SDWebImage/Private/SDAsyncBlockOperation.h +++ b/SDWebImage/Private/SDAsyncBlockOperation.h @@ -11,6 +11,7 @@ @class SDAsyncBlockOperation; typedef void (^SDAsyncBlock)(SDAsyncBlockOperation * __nonnull asyncOperation); +/// A async block operation, success after you call `completer` (not like `NSBlockOperation` which is for sync block, success on return) @interface SDAsyncBlockOperation : NSOperation - (nonnull instancetype)initWithBlock:(nonnull SDAsyncBlock)block; diff --git a/SDWebImage/Private/SDDeviceHelper.h b/SDWebImage/Private/SDDeviceHelper.h index 740fb2e3..5d5676b1 100644 --- a/SDWebImage/Private/SDDeviceHelper.h +++ b/SDWebImage/Private/SDDeviceHelper.h @@ -9,6 +9,7 @@ #import #import "SDWebImageCompat.h" +/// Device information helper methods @interface SDDeviceHelper : NSObject + (NSUInteger)totalMemory; diff --git a/SDWebImage/Private/SDDisplayLink.h b/SDWebImage/Private/SDDisplayLink.h index 60d4e80e..3ee8c6fd 100644 --- a/SDWebImage/Private/SDDisplayLink.h +++ b/SDWebImage/Private/SDDisplayLink.h @@ -9,9 +9,8 @@ #import #import "SDWebImageCompat.h" -// Cross-platform display link wrapper. Do not retain the target -// Use `CADisplayLink` on iOS/tvOS, `CVDisplayLink` on macOS, `NSTimer` on watchOS - +/// Cross-platform display link wrapper. Do not retain the target +/// Use `CADisplayLink` on iOS/tvOS, `CVDisplayLink` on macOS, `NSTimer` on watchOS @interface SDDisplayLink : NSObject @property (readonly, nonatomic, weak, nullable) id target; diff --git a/SDWebImage/Private/SDFileAttributeHelper.h b/SDWebImage/Private/SDFileAttributeHelper.h index 1e66ded7..b5594e95 100644 --- a/SDWebImage/Private/SDFileAttributeHelper.h +++ b/SDWebImage/Private/SDFileAttributeHelper.h @@ -7,12 +7,13 @@ #import +/// File Extended Attribute (xattr) helper methods @interface SDFileAttributeHelper : NSObject -+ (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -+ (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -+ (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; -+ (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err; -+ (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err; ++ (nullable NSArray *)extendedAttributeNamesAtPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)hasExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (nullable NSData *)extendedAttribute:(nonnull NSString*)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)setExtendedAttribute:(nonnull NSString*)name value:(nonnull NSData *)value atPath:(nonnull NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)removeExtendedAttribute:(nonnull NSString*)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; @end diff --git a/SDWebImage/Private/SDImageAssetManager.h b/SDWebImage/Private/SDImageAssetManager.h index 68184187..88dee489 100644 --- a/SDWebImage/Private/SDImageAssetManager.h +++ b/SDWebImage/Private/SDImageAssetManager.h @@ -9,8 +9,8 @@ #import #import "SDWebImageCompat.h" -// Apple parse the Asset Catalog compiled file(`Assets.car`) by CoreUI.framework, however it's a private framework and there are no other ways to directly get the data. So we just process the normal bundle files :) - +/// A Image-Asset manager to work like UIKit/AppKit's image cache behavior +/// Apple parse the Asset Catalog compiled file(`Assets.car`) by CoreUI.framework, however it's a private framework and there are no other ways to directly get the data. So we just process the normal bundle files :) @interface SDImageAssetManager : NSObject @property (nonatomic, strong, nonnull) NSMapTable *imageTable; diff --git a/SDWebImage/Private/SDImageCachesManagerOperation.h b/SDWebImage/Private/SDImageCachesManagerOperation.h index fddf78c1..0debe6ca 100644 --- a/SDWebImage/Private/SDImageCachesManagerOperation.h +++ b/SDWebImage/Private/SDImageCachesManagerOperation.h @@ -9,7 +9,7 @@ #import #import "SDWebImageCompat.h" -// This is used for operation management, but not for operation queue execute +/// This is used for operation management, but not for operation queue execute @interface SDImageCachesManagerOperation : NSOperation @property (nonatomic, assign, readonly) NSUInteger pendingCount; diff --git a/SDWebImage/Private/SDWeakProxy.h b/SDWebImage/Private/SDWeakProxy.h index 4fd16228..d92c682b 100644 --- a/SDWebImage/Private/SDWeakProxy.h +++ b/SDWebImage/Private/SDWeakProxy.h @@ -9,6 +9,7 @@ #import #import "SDWebImageCompat.h" +/// A weak proxy which forward all the message to the target @interface SDWeakProxy : NSProxy @property (nonatomic, weak, readonly, nullable) id target; From f607e909bce79708cb7b5606c7f11e44574f4bfd Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 27 Feb 2020 16:31:54 +0800 Subject: [PATCH 112/181] Fix the SDWebImageDownloadStopNotification does not get removed on dealloc --- SDWebImage/Core/SDWebImageDownloader.m | 1 + 1 file changed, 1 insertion(+) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index d7deee95..3d354add 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -514,6 +514,7 @@ didReceiveResponse:(NSURLResponse *)response - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:SDWebImageDownloadReceiveResponseNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:SDWebImageDownloadStopNotification object:nil]; } - (instancetype)initWithDownloadOperation:(NSOperation *)downloadOperation { From eea85faaa5bb78b5b2e1ae8e92138790e92bba12 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 1 Mar 2020 15:44:06 +0800 Subject: [PATCH 113/181] Update the readme with SDWebImageLottiePlugin --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 58c85e8b..2ab5371a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries +- [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie](https://github.com/airbnb/lottie-ios) animation with remote JSON files - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching From 692f01b84b2f4e528514c3b1513c88585c8a1afe Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 2 Mar 2020 13:06:09 +0800 Subject: [PATCH 114/181] Fix the rare case when call `SDWebImageDownloaderOperation.cancel`, the completion block will callback twice --- SDWebImage/Core/SDWebImageDownloaderOperation.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 9411dcac..67d1a8e2 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -245,9 +245,10 @@ typedef NSMutableDictionary SDCallbacksDictionary; // maintain the isFinished and isExecuting flags. if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; + } else { + // Operation cancelled by user during sending the request + [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]]; } - // Operation cancelled by user during sending the request - [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]]; [self reset]; } From 3df399508e38698cc115583ebfc03c353908921a Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 1 Mar 2020 18:38:40 +0800 Subject: [PATCH 115/181] Add a better check to handle the cases when call `storeImage` without imageData. Firstly check SDAnimatedImage, then check sd_imageFormat. --- SDWebImage/Core/SDImageCache.m | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 362a299b..06bc49a5 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -187,13 +187,20 @@ dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = imageData; + if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) { + // If image is custom animated image class, prefer its original animated data + data = [((id)image) animatedImageData]; + } if (!data && image) { - // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format - SDImageFormat format; - if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) { - format = SDImageFormatPNG; - } else { - format = SDImageFormatJPEG; + // Check image's associated image format, may return .undefined + SDImageFormat format = image.sd_imageFormat; + if (format == SDImageFormatUndefined) { + // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format + if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) { + format = SDImageFormatPNG; + } else { + format = SDImageFormatJPEG; + } } data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil]; } From 12bdd57f31cb041434705340997aa5d2557934a9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 1 Mar 2020 18:51:48 +0800 Subject: [PATCH 116/181] Added the test case `test42StoreCacheWithImageAndFormatWithoutImageData` to ensure this behavior --- Tests/Tests/SDImageCacheTests.m | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index 516a7f36..f35d0c97 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -391,6 +391,36 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; [self waitForExpectationsWithCommonTimeout]; } +- (void)test42StoreCacheWithImageAndFormatWithoutImageData { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"StoreImage with sd_imageFormat should use that format"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"StoreImage with SDAnimatedImage should use animatedImageData"]; + NSData *gifData = [NSData dataWithContentsOfFile:[self testGIFPath]]; + + UIImage *gifImage = [UIImage sd_imageWithData:gifData]; // UIAnimatedImage + expect(gifImage.sd_isAnimated).beTruthy(); + expect(gifImage.sd_imageFormat).equal(SDImageFormatGIF); + + NSString *kAnimatedImageKey1 = @"kAnimatedImageKey1"; + NSString *kAnimatedImageKey2 = @"kAnimatedImageKey2"; + [SDImageCache.sharedImageCache storeImage:gifImage forKey:kAnimatedImageKey1 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey1]; + // Should save to GIF + expect(diskImage.sd_isAnimated).beTruthy(); + expect(diskImage.sd_imageFormat).equal(SDImageFormatGIF); + [expectation1 fulfill]; + }]; + + SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData]; + expect(animatedImage.animatedImageData).notTo.beNil(); + [SDImageCache.sharedImageCache storeImage:animatedImage forKey:kAnimatedImageKey2 toDisk:YES completion:^{ + NSData *data = [SDImageCache.sharedImageCache diskImageDataForKey:kAnimatedImageKey2]; + // Should save with animatedImageData + expect(data).equal(animatedImage.animatedImageData); + [expectation2 fulfill]; + }]; + [self waitForExpectationsWithCommonTimeout]; +} + #pragma mark - SDMemoryCache & SDDiskCache - (void)test42CustomMemoryCache { SDImageCacheConfig *config = [[SDImageCacheConfig alloc] init]; From 6f8d83b2f29884fa88c7c85b783033155ee13951 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 2 Mar 2020 11:21:26 +0800 Subject: [PATCH 117/181] Add another logic. to check UIAnimatedImage when there are no image format to detect, this should use GIF to encode --- SDWebImage/Core/SDImageCache.h | 2 ++ SDWebImage/Core/SDImageCache.m | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 688d3fc2..1b1afd47 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -162,6 +162,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { * @param key The unique image cache key, usually it's image absolute URL * @param toDisk Store the image to disk cache if YES. If NO, the completion block is called synchronously * @param completionBlock A block executed after the operation is finished + * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG. */ - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key @@ -178,6 +179,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { * @param key The unique image cache key, usually it's image absolute URL * @param toDisk Store the image to disk cache if YES. If NO, the completion block is called synchronously * @param completionBlock A block executed after the operation is finished + * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG. */ - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 06bc49a5..17403997 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -195,11 +195,16 @@ // Check image's associated image format, may return .undefined SDImageFormat format = image.sd_imageFormat; if (format == SDImageFormatUndefined) { - // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format - if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) { - format = SDImageFormatPNG; + // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14) + if (image.sd_isAnimated) { + format = SDImageFormatGIF; } else { - format = SDImageFormatJPEG; + // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format + if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) { + format = SDImageFormatPNG; + } else { + format = SDImageFormatJPEG; + } } } data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil]; From d88b7d81db9ab5ffb976972cfd85a9a59df779f2 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 2 Mar 2020 11:28:07 +0800 Subject: [PATCH 118/181] Update the test case with Case 3: UIAnimatedImage without sd_imageFormat should use GIF not APNG --- Tests/Tests/SDImageCacheTests.m | 38 ++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index f35d0c97..701dd411 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -394,14 +394,18 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; - (void)test42StoreCacheWithImageAndFormatWithoutImageData { XCTestExpectation *expectation1 = [self expectationWithDescription:@"StoreImage with sd_imageFormat should use that format"]; XCTestExpectation *expectation2 = [self expectationWithDescription:@"StoreImage with SDAnimatedImage should use animatedImageData"]; - NSData *gifData = [NSData dataWithContentsOfFile:[self testGIFPath]]; + XCTestExpectation *expectation3 = [self expectationWithDescription:@"StoreImage with UIAnimatedImage without sd_imageFormat should use GIF"]; + NSData *gifData = [NSData dataWithContentsOfFile:[self testGIFPath]]; UIImage *gifImage = [UIImage sd_imageWithData:gifData]; // UIAnimatedImage expect(gifImage.sd_isAnimated).beTruthy(); expect(gifImage.sd_imageFormat).equal(SDImageFormatGIF); NSString *kAnimatedImageKey1 = @"kAnimatedImageKey1"; NSString *kAnimatedImageKey2 = @"kAnimatedImageKey2"; + NSString *kAnimatedImageKey3 = @"kAnimatedImageKey3"; + + // Case 1: UIImage with `sd_imageFormat` should use that format [SDImageCache.sharedImageCache storeImage:gifImage forKey:kAnimatedImageKey1 toDisk:YES completion:^{ UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey1]; // Should save to GIF @@ -410,6 +414,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; [expectation1 fulfill]; }]; + // Case 2: SDAnimatedImage should use `animatedImageData` SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData]; expect(animatedImage.animatedImageData).notTo.beNil(); [SDImageCache.sharedImageCache storeImage:animatedImage forKey:kAnimatedImageKey2 toDisk:YES completion:^{ @@ -418,6 +423,22 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; expect(data).equal(animatedImage.animatedImageData); [expectation2 fulfill]; }]; + + // Case 3: UIAnimatedImage without sd_imageFormat should use GIF not APNG + NSData *apngData = [NSData dataWithContentsOfFile:[self testAPNGPath]]; + UIImage *apngImage = [UIImage sd_imageWithData:apngData]; + expect(apngImage.sd_isAnimated).beTruthy(); + expect(apngImage.sd_imageFormat).equal(SDImageFormatPNG); + // Remove sd_imageFormat + apngImage.sd_imageFormat = SDImageFormatUndefined; + [SDImageCache.sharedImageCache storeImage:apngImage forKey:kAnimatedImageKey3 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey3]; + // Should save to GIF + expect(diskImage.sd_isAnimated).beTruthy(); + expect(diskImage.sd_imageFormat).equal(SDImageFormatGIF); + [expectation3 fulfill]; + }]; + [self waitForExpectationsWithCommonTimeout]; } @@ -757,6 +778,15 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; return reusableImage; } +- (UIImage *)testAPNGImage { + static UIImage *reusableImage = nil; + if (!reusableImage) { + NSData *data = [NSData dataWithContentsOfFile:[self testAPNGPath]]; + reusableImage = [UIImage sd_imageWithData:data]; + } + return reusableImage; +} + - (NSString *)testJPEGPath { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; return [testBundle pathForResource:@"TestImage" ofType:@"jpg"]; @@ -773,6 +803,12 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; return testPath; } +- (NSString *)testAPNGPath { + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSString *testPath = [testBundle pathForResource:@"TestImageAnimated" ofType:@"apng"]; + return testPath; +} + - (nullable NSString *)userCacheDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); return paths.firstObject; From cc8e80ff84305122b6182ec1d7359670319782e0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 2 Mar 2020 12:02:19 +0800 Subject: [PATCH 119/181] Try to fix the test case `test11ThatCancelAllDownloadWorks` to make it stable --- Tests/Tests/SDWebImageDownloaderTests.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m index 467623ac..da0ee89b 100644 --- a/Tests/Tests/SDWebImageDownloaderTests.m +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -187,10 +187,13 @@ - (void)test11ThatCancelAllDownloadWorks { XCTestExpectation *expectation = [self expectationWithDescription:@"CancelAllDownloads"]; + // Previous test case download may not finished, so we just check the download count should + 1 after new request + NSUInteger currentDownloadCount = [SDWebImageDownloader sharedDownloader].currentDownloadCount; - NSURL *imageURL = [NSURL URLWithString:@"http://via.placeholder.com/1100x1100.png"]; + // Choose a large image to avoid download too fast + NSURL *imageURL = [NSURL URLWithString:@"https://www.sample-videos.com/img/Sample-png-image-1mb.png"]; [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL completed:nil]; - expect([SDWebImageDownloader sharedDownloader].currentDownloadCount).to.equal(1); + expect([SDWebImageDownloader sharedDownloader].currentDownloadCount).to.equal(currentDownloadCount + 1); [[SDWebImageDownloader sharedDownloader] cancelAllDownloads]; From f415e51508bfaef46e515bfdab6bdf41c953d389 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 2 Mar 2020 12:21:15 +0800 Subject: [PATCH 120/181] Update the test case, because previouslly we have no test case about the storeImage behavior when imageData is nil --- Tests/Tests/SDImageCacheTests.m | 80 +++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index 701dd411..93adcd47 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -392,51 +392,93 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; } - (void)test42StoreCacheWithImageAndFormatWithoutImageData { - XCTestExpectation *expectation1 = [self expectationWithDescription:@"StoreImage with sd_imageFormat should use that format"]; - XCTestExpectation *expectation2 = [self expectationWithDescription:@"StoreImage with SDAnimatedImage should use animatedImageData"]; - XCTestExpectation *expectation3 = [self expectationWithDescription:@"StoreImage with UIAnimatedImage without sd_imageFormat should use GIF"]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"StoreImage UIImage without sd_imageFormat should use PNG for alpha channel"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"StoreImage UIImage without sd_imageFormat should use JPEG for non-alpha channel"]; + XCTestExpectation *expectation3 = [self expectationWithDescription:@"StoreImage UIImage/UIAnimatedImage with sd_imageFormat should use that format"]; + XCTestExpectation *expectation4 = [self expectationWithDescription:@"StoreImage SDAnimatedImage should use animatedImageData"]; + XCTestExpectation *expectation5 = [self expectationWithDescription:@"StoreImage UIAnimatedImage without sd_imageFormat should use GIF"]; + + NSString *kAnimatedImageKey1 = @"kAnimatedImageKey1"; + NSString *kAnimatedImageKey2 = @"kAnimatedImageKey2"; + NSString *kAnimatedImageKey3 = @"kAnimatedImageKey3"; + NSString *kAnimatedImageKey4 = @"kAnimatedImageKey4"; + NSString *kAnimatedImageKey5 = @"kAnimatedImageKey5"; + + // Case 1: UIImage without `sd_imageFormat` should use PNG for alpha channel + NSData *pngData = [NSData dataWithContentsOfFile:[self testPNGPath]]; + UIImage *pngImage = [UIImage sd_imageWithData:pngData]; + expect(pngImage.sd_isAnimated).beFalsy(); + expect(pngImage.sd_imageFormat).equal(SDImageFormatPNG); + // Remove sd_imageFormat + pngImage.sd_imageFormat = SDImageFormatUndefined; + // Check alpha channel + expect([SDImageCoderHelper CGImageContainsAlpha:pngImage.CGImage]).beTruthy(); + + [SDImageCache.sharedImageCache storeImage:pngImage forKey:kAnimatedImageKey1 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey1]; + // Should save to PNG + expect(diskImage.sd_isAnimated).beFalsy(); + expect(diskImage.sd_imageFormat).equal(SDImageFormatPNG); + [expectation1 fulfill]; + }]; + + // Case 2: UIImage without `sd_imageFormat` should use JPEG for non-alpha channel + SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat]; + format.opaque = YES; + SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:pngImage.size format:format]; + // Non-alpha image, also test `SDGraphicsImageRenderer` behavior here :) + UIImage *nonAlphaImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + [pngImage drawInRect:CGRectMake(0, 0, pngImage.size.width, pngImage.size.height)]; + }]; + expect(nonAlphaImage).notTo.beNil(); + expect([SDImageCoderHelper CGImageContainsAlpha:nonAlphaImage.CGImage]).beFalsy(); + + [SDImageCache.sharedImageCache storeImage:nonAlphaImage forKey:kAnimatedImageKey2 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey2]; + // Should save to JPEG + expect(diskImage.sd_isAnimated).beFalsy(); + expect(diskImage.sd_imageFormat).equal(SDImageFormatJPEG); + [expectation2 fulfill]; + }]; NSData *gifData = [NSData dataWithContentsOfFile:[self testGIFPath]]; UIImage *gifImage = [UIImage sd_imageWithData:gifData]; // UIAnimatedImage expect(gifImage.sd_isAnimated).beTruthy(); expect(gifImage.sd_imageFormat).equal(SDImageFormatGIF); - NSString *kAnimatedImageKey1 = @"kAnimatedImageKey1"; - NSString *kAnimatedImageKey2 = @"kAnimatedImageKey2"; - NSString *kAnimatedImageKey3 = @"kAnimatedImageKey3"; - - // Case 1: UIImage with `sd_imageFormat` should use that format - [SDImageCache.sharedImageCache storeImage:gifImage forKey:kAnimatedImageKey1 toDisk:YES completion:^{ - UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey1]; + // Case 3: UIImage with `sd_imageFormat` should use that format + [SDImageCache.sharedImageCache storeImage:gifImage forKey:kAnimatedImageKey3 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey3]; // Should save to GIF expect(diskImage.sd_isAnimated).beTruthy(); expect(diskImage.sd_imageFormat).equal(SDImageFormatGIF); - [expectation1 fulfill]; + [expectation3 fulfill]; }]; - // Case 2: SDAnimatedImage should use `animatedImageData` + // Case 4: SDAnimatedImage should use `animatedImageData` SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData]; expect(animatedImage.animatedImageData).notTo.beNil(); - [SDImageCache.sharedImageCache storeImage:animatedImage forKey:kAnimatedImageKey2 toDisk:YES completion:^{ - NSData *data = [SDImageCache.sharedImageCache diskImageDataForKey:kAnimatedImageKey2]; + [SDImageCache.sharedImageCache storeImage:animatedImage forKey:kAnimatedImageKey4 toDisk:YES completion:^{ + NSData *data = [SDImageCache.sharedImageCache diskImageDataForKey:kAnimatedImageKey4]; // Should save with animatedImageData expect(data).equal(animatedImage.animatedImageData); - [expectation2 fulfill]; + [expectation4 fulfill]; }]; - // Case 3: UIAnimatedImage without sd_imageFormat should use GIF not APNG + // Case 5: UIAnimatedImage without sd_imageFormat should use GIF not APNG NSData *apngData = [NSData dataWithContentsOfFile:[self testAPNGPath]]; UIImage *apngImage = [UIImage sd_imageWithData:apngData]; expect(apngImage.sd_isAnimated).beTruthy(); expect(apngImage.sd_imageFormat).equal(SDImageFormatPNG); // Remove sd_imageFormat apngImage.sd_imageFormat = SDImageFormatUndefined; - [SDImageCache.sharedImageCache storeImage:apngImage forKey:kAnimatedImageKey3 toDisk:YES completion:^{ - UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey3]; + + [SDImageCache.sharedImageCache storeImage:apngImage forKey:kAnimatedImageKey5 toDisk:YES completion:^{ + UIImage *diskImage = [SDImageCache.sharedImageCache imageFromDiskCacheForKey:kAnimatedImageKey5]; // Should save to GIF expect(diskImage.sd_isAnimated).beTruthy(); expect(diskImage.sd_imageFormat).equal(SDImageFormatGIF); - [expectation3 fulfill]; + [expectation5 fulfill]; }]; [self waitForExpectationsWithCommonTimeout]; From 412269368ea7f80c63cad28f4f896afc0de3b6e0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 4 Mar 2020 12:16:34 +0800 Subject: [PATCH 121/181] Fix the SwiftPM Package.swift declaration for Xcode 11.4-Beta 3 and old versions --- Package.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 50390b74..7dd16986 100644 --- a/Package.swift +++ b/Package.swift @@ -32,12 +32,16 @@ let package = Package( dependencies: [], path: "SDWebImage", exclude: ["MapKit"], - publicHeadersPath: "Core" + publicHeadersPath: "Core", + cSettings: [ + .headerSearchPath("Private") + ] ), .target( name: "SDWebImageMapKit", dependencies: ["SDWebImage"], - path: "SDWebImage/MapKit" + path: "SDWebImage/MapKit", + publicHeadersPath: "." ) ] ) From f0388739b6421f653d5adc20bb8f9818821c0633 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 4 Mar 2020 11:37:43 +0800 Subject: [PATCH 122/181] Add the new context option, including the cache, loader and coder. They can be used to use custom cache/loader in a more convenient way, instead of creating dummy SDWebImageManager --- SDWebImage/Core/SDImageCacheDefine.m | 10 ++++- SDWebImage/Core/SDImageLoader.m | 29 ++++++++++---- SDWebImage/Core/SDWebImageDefine.h | 20 ++++++++++ SDWebImage/Core/SDWebImageDefine.m | 3 ++ SDWebImage/Core/SDWebImageManager.m | 57 +++++++++++++++++++++------- 5 files changed, 98 insertions(+), 21 deletions(-) diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 75dfb4e6..19db161a 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -38,6 +38,14 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS mutableCoderOptions[SDImageCoderWebImageContext] = context; SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; + // Grab the image coder + id imageCoder; + if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) { + imageCoder = context[SDWebImageContextImageCoder]; + } else { + imageCoder = [SDImageCodersManager sharedManager]; + } + if (!decodeFirstFrame) { Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; // check whether we should use `SDAnimatedImage` @@ -57,7 +65,7 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS } } if (!image) { - image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions]; + image = [imageCoder decodedImageWithData:imageData options:coderOptions]; } if (image) { BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage); diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 4c831c59..c529954e 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -52,6 +52,14 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS mutableCoderOptions[SDImageCoderWebImageContext] = context; SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; + // Grab the image coder + id imageCoder; + if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) { + imageCoder = context[SDWebImageContextImageCoder]; + } else { + imageCoder = [SDImageCodersManager sharedManager]; + } + if (!decodeFirstFrame) { // check whether we should use `SDAnimatedImage` Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; @@ -71,7 +79,7 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS } } if (!image) { - image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions]; + image = [imageCoder decodedImageWithData:imageData options:coderOptions]; } if (image) { BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage); @@ -127,14 +135,21 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im mutableCoderOptions[SDImageCoderWebImageContext] = context; SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; + // Grab the progressive image coder id progressiveCoder = objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey); if (!progressiveCoder) { - // We need to create a new instance for progressive decoding to avoid conflicts - for (idcoder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) { - if ([coder conformsToProtocol:@protocol(SDProgressiveImageCoder)] && - [((id)coder) canIncrementalDecodeFromData:imageData]) { - progressiveCoder = [[[coder class] alloc] initIncrementalWithOptions:coderOptions]; - break; + id imageCoder = context[SDWebImageContextImageCoder]; + // Check the progressive coder if provided + if ([imageCoder conformsToProtocol:@protocol(SDProgressiveImageCoder)]) { + progressiveCoder = [[[imageCoder class] alloc] initIncrementalWithOptions:coderOptions]; + } else { + // We need to create a new instance for progressive decoding to avoid conflicts + for (id coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) { + if ([coder conformsToProtocol:@protocol(SDProgressiveImageCoder)] && + [((id)coder) canIncrementalDecodeFromData:imageData]) { + progressiveCoder = [[[coder class] alloc] initIncrementalWithOptions:coderOptions]; + break; + } } } objc_setAssociatedObject(operation, SDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC); diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 568de147..4ad3aca9 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -213,9 +213,29 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetIma /** A SDWebImageManager instance to control the image download and cache process using in UIImageView+WebCache category and likes. If not provided, use the shared manager (SDWebImageManager *) + @note Consider deprecated. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransofmer` (See below), which already matches all the properties in SDWebImageManager. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager; +/** + A id instance which conforms to `SDImageCache` protocol. It's used to override the image mananger's cache during the image loading pipeline. + In other word, if you just want to specify a custom cache during image loading, you don't need to re-create a dummy SDWebImageManager instance with the cache. If not provided, use the image manager's cache (id) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageCache; + +/** + A id instance which conforms to `SDImageLoader` protocol. It's used to override the image mananger's loader during the image loading pipeline. + In other word, if you just want to specify a custom loader during image loading, you don't need to re-create a dummy SDWebImageManager instance with the loader. If not provided, use the image manager's cache (id) +*/ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageLoader; + +/** + A id instance which conforms to `SDImageCoder` protocol. It's used to override the default image codre for image decoding(including progressive) and encoding during the image loading process. + If you use this context option, we will not always use `SDImageCodersManager.shared` to loop through all registered coders and find the suitable one. Instead, we will arbitrarily use the exact provided coder without extra checking (We may not call `canDecodeFromData:`). + @note This is only useful for cases which you can ensure the loading url matches your coder, or you find it's too hard to write a common coder which can used for generic usage. This will bind the loading url with the coder logic, which is not always a good design, but possible. (id) +*/ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageCoder; + /** A id instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. (id) */ diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 921e878a..866b164d 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -120,6 +120,9 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage * SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey"; SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager"; +SDWebImageContextOption const SDWebImageContextImageCache = @"imageCache"; +SDWebImageContextOption const SDWebImageContextImageLoader = @"imageLoader"; +SDWebImageContextOption const SDWebImageContextImageCoder = @"imageCoder"; SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer"; SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor"; SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio"; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index c5502cc3..c4aff6ac 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -211,12 +211,19 @@ static id _defaultImageLoader; context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { + // Grab the image cache to use + id imageCache; + if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) { + imageCache = context[SDWebImageContextImageCache]; + } else { + imageCache = self.imageCache; + } // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { NSString *key = [self cacheKeyForURL:url context:context]; @weakify(operation); - operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { + operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user @@ -243,11 +250,18 @@ static id _defaultImageLoader; cacheType:(SDImageCacheType)cacheType progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { + // Grab the image loader to use + id imageLoader; + if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) { + imageLoader = context[SDWebImageContextImageLoader]; + } else { + imageLoader = self.imageLoader; + } // Check whether we should download image from network BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached); shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); - shouldDownload &= [self.imageLoader canRequestImageForURL:url]; + shouldDownload &= [imageLoader canRequestImageForURL:url]; if (shouldDownload) { if (cachedImage && options & SDWebImageRefreshCached) { // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image @@ -265,7 +279,7 @@ static id _defaultImageLoader; } @weakify(operation); - operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { + operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user @@ -277,7 +291,7 @@ static id _defaultImageLoader; [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url]; } else if (error) { [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url]; - BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error]; + BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context]; if (shouldBlockFailedURL) { SD_LOCK(self.failedURLsLock); @@ -336,7 +350,6 @@ static id _defaultImageLoader; shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage)); BOOL shouldCacheOriginal = downloadedImage && finished; - BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store original image to cache if (shouldCacheOriginal) { @@ -346,14 +359,14 @@ static id _defaultImageLoader; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url]; - [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ + [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{ // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; }]; } }); } else { - [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ + [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{ // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; }]; @@ -385,7 +398,6 @@ static id _defaultImageLoader; BOOL shouldTransformImage = originalImage && transformer; shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage)); - BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store transformed image to cache if (shouldTransformImage) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @@ -404,7 +416,7 @@ static id _defaultImageLoader; } // keep the original image format and extended data SDImageCopyAssociatedObject(originalImage, transformedImage); - [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType waitStoreCache:waitStoreCache completion:^{ + [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; }]; } else { @@ -432,10 +444,18 @@ static id _defaultImageLoader; imageData:(nullable NSData *)data forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType - waitStoreCache:(BOOL)waitStoreCache + options:(SDWebImageOptions)options + context:(nullable SDWebImageContext *)context completion:(nullable SDWebImageNoParamsBlock)completion { + id imageCache; + if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) { + imageCache = context[SDWebImageContextImageCache]; + } else { + imageCache = self.imageCache; + } + BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // Check whether we should wait the store cache finished. If not, callback immediately - [self.imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{ + [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{ if (waitStoreCache) { if (completion) { completion(); @@ -472,13 +492,24 @@ static id _defaultImageLoader; } - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url - error:(nonnull NSError *)error { + error:(nonnull NSError *)error + options:(SDWebImageOptions)options + context:(nullable SDWebImageContext *)context { + if ((options & SDWebImageRetryFailed)) { + return NO; + } + id imageLoader; + if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) { + imageLoader = context[SDWebImageContextImageLoader]; + } else { + imageLoader = self.imageLoader; + } // Check whether we should block failed url BOOL shouldBlockFailedURL; if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) { shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error]; } else { - shouldBlockFailedURL = [self.imageLoader shouldBlockFailedURLWithURL:url error:error]; + shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error]; } return shouldBlockFailedURL; From 2abb8d06281ed22df747d9f8d24110850dfcf4e8 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 4 Mar 2020 12:25:59 +0800 Subject: [PATCH 123/181] Formal deprecate the SDWebImageContextCustomManager context option --- SDWebImage/Core/SDWebImageDefine.h | 4 ++-- SDWebImage/Core/UIView+WebCache.m | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 4ad3aca9..52d79674 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -213,9 +213,9 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetIma /** A SDWebImageManager instance to control the image download and cache process using in UIImageView+WebCache category and likes. If not provided, use the shared manager (SDWebImageManager *) - @note Consider deprecated. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransofmer` (See below), which already matches all the properties in SDWebImageManager. + @deprecated Deprecated. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransofmer` (See below), which already matches all the properties in SDWebImageManager. */ -FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager; +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager __deprecated_msg("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead"); /** A id instance which conforms to `SDImageCache` protocol. It's used to override the image mananger's cache during the image loading pipeline. diff --git a/SDWebImage/Core/UIView+WebCache.m b/SDWebImage/Core/UIView+WebCache.m index 311dd1ba..7a3ffa6d 100644 --- a/SDWebImage/Core/UIView+WebCache.m +++ b/SDWebImage/Core/UIView+WebCache.m @@ -80,11 +80,13 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL; [self sd_startImageIndicator]; id imageIndicator = self.sd_imageIndicator; #endif - +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" SDWebImageManager *manager = context[SDWebImageContextCustomManager]; if (!manager) { manager = [SDWebImageManager sharedManager]; } +#pragma clang diagnostic pop SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { if (imageProgress) { From 8736d98f850ab1cceb9629d8270a48546ee8e382 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 4 Mar 2020 12:52:02 +0800 Subject: [PATCH 124/181] Fix the compatible with Xcode 11.0~Xcode 11.3 on SwiftPM, the `exclude` arg does not treat the source code in the publicHeadersPath. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7dd16986..b8f49233 100644 --- a/Package.swift +++ b/Package.swift @@ -31,7 +31,7 @@ let package = Package( name: "SDWebImage", dependencies: [], path: "SDWebImage", - exclude: ["MapKit"], + sources: ["Core", "Private"], publicHeadersPath: "Core", cSettings: [ .headerSearchPath("Private") From cf79d1c3aefb0dff7f5a8c806da9ff787a65ea9a Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Mar 2020 17:00:04 +0800 Subject: [PATCH 125/181] Revert the changes to check `SDWebImageRetryFailed` before adding the black list. This is the previous version behavior. --- SDWebImage/Core/SDWebImageManager.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index c4aff6ac..4d33e6d7 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -495,9 +495,6 @@ static id _defaultImageLoader; error:(nonnull NSError *)error options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { - if ((options & SDWebImageRetryFailed)) { - return NO; - } id imageLoader; if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) { imageLoader = context[SDWebImageContextImageLoader]; From b54cdcc4bbbd47342634566b3df0682a90a40336 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Mar 2020 17:55:39 +0800 Subject: [PATCH 126/181] Change the deprecatation into the soft deprecation, which still works but will be removed in SDWebImage 6.0.0 --- SDWebImage/Core/SDWebImageDefine.h | 4 ++-- SDWebImage/Core/UIView+WebCache.m | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 52d79674..1be59810 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -213,9 +213,9 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetIma /** A SDWebImageManager instance to control the image download and cache process using in UIImageView+WebCache category and likes. If not provided, use the shared manager (SDWebImageManager *) - @deprecated Deprecated. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransofmer` (See below), which already matches all the properties in SDWebImageManager. + @deprecated Deprecated in the future. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransofmer` (See below), which already matches all the properties in SDWebImageManager. */ -FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager __deprecated_msg("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead"); +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager API_DEPRECATED("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** A id instance which conforms to `SDImageCache` protocol. It's used to override the image mananger's cache during the image loading pipeline. diff --git a/SDWebImage/Core/UIView+WebCache.m b/SDWebImage/Core/UIView+WebCache.m index 7a3ffa6d..5e9f030a 100644 --- a/SDWebImage/Core/UIView+WebCache.m +++ b/SDWebImage/Core/UIView+WebCache.m @@ -80,13 +80,10 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL; [self sd_startImageIndicator]; id imageIndicator = self.sd_imageIndicator; #endif -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" SDWebImageManager *manager = context[SDWebImageContextCustomManager]; if (!manager) { manager = [SDWebImageManager sharedManager]; } -#pragma clang diagnostic pop SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { if (imageProgress) { From bd33f4179ddc9fff5ad8f6de3a94623f3446b8c9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Mar 2020 18:44:50 +0800 Subject: [PATCH 127/181] Supresss the deprecation warning when min deployment target version set to iOS 13+ or macCatalyst --- SDWebImage/Core/SDGraphicsImageRenderer.m | 3 +++ SDWebImage/Core/SDImageCache.m | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/SDWebImage/Core/SDGraphicsImageRenderer.m b/SDWebImage/Core/SDGraphicsImageRenderer.m index 869de2ca..03aef3a5 100644 --- a/SDWebImage/Core/SDGraphicsImageRenderer.m +++ b/SDWebImage/Core/SDGraphicsImageRenderer.m @@ -69,6 +69,8 @@ #endif } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" - (SDGraphicsImageRendererFormatRange)preferredRange { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { @@ -114,6 +116,7 @@ _preferredRange = preferredRange; #endif } +#pragma clang diagnostic pop - (instancetype)init { self = [super init]; diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 17403997..f7585895 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -223,7 +223,10 @@ } } else { @try { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; +#pragma clang diagnostic pop } @catch (NSException *exception) { NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception); } @@ -397,7 +400,10 @@ } } else { @try { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData]; +#pragma clang diagnostic pop } @catch (NSException *exception) { NSLog(@"NSKeyedUnarchiver unarchive failed with exception: %@", exception); } From aa7cc070ccf01ce6ea85dd0b3176f712ac23ea1a Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 5 Mar 2020 18:48:43 +0800 Subject: [PATCH 128/181] Bumped version to 5.6.0 Update the CHANGELOG --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++-- SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33987189..c56f967b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,38 @@ +## [5.6.0 - URLSession Metrics && Vector Format, on Mar 5th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.6.0) +See [all tickets marked for the 5.6.0 release](https://github.com/SDWebImage/SDWebImage/milestone/63) + +### Features + +#### URLSession Metrics +- Added the URLSessionTaskMetrics support for downloader && operation, which can be used for network metrics #2937 +- Typically you use custom operation class to collect all metrics in your app. You can also collect metrics for single url request level. Check the #2937 example code to grab the download token and check metrics. + +#### Vector Image +- Feature - better support for vector format detection, now PDF rasterized bitmap is built-in #2936 +- Pass `.thumbnailPixelSize` to control the PDF bitmap size. If you want vector PDF rendering, you still need to use [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder). +- Vector image like SVG (via [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder)) and PDF (via [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder)), or system symbol images, can be detected by new API `sd_isVector`. +- Vector image does not pass to transformer by default, because they support dynamic size changing. Pass `.transformVectorImage` option to allow transformation. + +#### Cache +- Add a better check to handle the cases when call `storeImage` without imageData #2953 +- Which means, if you store image to disk without data, we will use extra information via `sd_imageFormat` or custom image class, to choose the the image format (including GIF and PDF) for encoding. Previously we only encode it into PNG or JPEG. + +#### Context Option +- Feature add context option for cache, loader and coder, deprecated SDWebImageContextCustomManager #2955 +- This makes it easy to use custom loader, cache, and decoder, without need to create a dummy SDWebImageManager instance. + +### Fixes +- Fix the rare case when call `SDWebImageDownloaderOperation.cancel`, the completion block may callback twice #2954 + +### Warnings +- Suppress the deprecation warning when min deployment target version set to iOS 13+ or macCatalyst +- Complete all the SDWebImage error code with the localized description, make it easy for debugging #2948 + ## [5.5.2 - 5.5 Patch, on Jan 26th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.2) See [all tickets marked for the 5.5.2 release](https://github.com/SDWebImage/SDWebImage/milestone/62) ### Fixes -- Fix the issue that `maxBufferSize` property does not correctlly works for `SDAnimatedImageView` #2934 +- Fix the issue that `maxBufferSize` property does not correctly works for `SDAnimatedImageView` #2934 ## [5.5.1 - 5.5 Patch, on Jan 18th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.1) See [all tickets marked for the 5.5.1 release](https://github.com/SDWebImage/SDWebImage/milestone/59) @@ -10,7 +40,7 @@ See [all tickets marked for the 5.5.1 release](https://github.com/SDWebImage/SDW ### Fixes - Fix the SDAnimatedImageView's progressive animation bug, which reset the frame index to 0 each time new frames available #2931 -## [5.5.0 - Thumbnail Decoding && Core Image, onJan, 16th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.0) +## [5.5.0 - Thumbnail Decoding && Core Image, on Jan 16th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.5.0) See [all tickets marked for the 5.5.0 release](https://github.com/SDWebImage/SDWebImage/milestone/55) ### Features diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 0ae96891..919e806f 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.5.2' + s.version = '5.6.0' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 7163374f..94bc5516 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.5.2 + 5.6.0 CFBundleSignature ???? CFBundleVersion - 5.5.2 + 5.6.0 NSPrincipalClass From 4b0c4c7d8ca1038b0c72a957f53a016af0856e8b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 9 Mar 2020 20:05:33 +0800 Subject: [PATCH 129/181] Update the readme about Lottie animation coder, describe the coder's base codec as well. --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ab5371a..7f9c5df9 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,14 @@ As such, we have moved/built new modules to [SDWebImage org](https://github.com/ We support SwiftUI by building with the functions (caching, loading and animation) powered by SDWebImage. You can have a try with [SDWebImageSwiftUI](https://github.com/SDWebImage/SDWebImageSwiftUI) #### Coders for additional image formats -- [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) - coder for WebP image format. Based on [libwebp](https://chromium.googlesource.com/webm/libwebp) -- [SDWebImageHEIFCoder](https://github.com/SDWebImage/SDWebImageHEIFCoder) - coder to support HEIF image without Apple's `Image/IO framework`, iOS 8+/macOS 10.10+ support. -- [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format -- [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format -- [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format -- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format image -- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format image +- [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) - coder for WebP format. Based on [libwebp](https://chromium.googlesource.com/webm/libwebp) +- [SDWebImageHEIFCoder](https://github.com/SDWebImage/SDWebImageHEIFCoder) - coder for HEIF format, iOS 8+/macOS 10.10+ support. Based on [libheif](https://github.com/strukturag/libheif) +- [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format. Based on [libbpg](https://github.com/mirrorer/libbpg) +- [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format. Based on [libflif](https://github.com/FLIF-hub/FLIF) +- [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format. Based on [libavif](https://github.com/AOMediaCodec/libavif) +- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. +- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. +- [SDWebImageLottieCoder](https://github.com/SDWebImage/SDWebImageLottieCoder) - coder for Lottie animation format. Based on [rlottie](https://github.com/Samsung/rlottie) - and more from community! #### Loaders @@ -66,7 +67,8 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries -- [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie](https://github.com/airbnb/lottie-ios) animation with remote JSON files +- [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with JSON files +- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SDWebImage/SDWebImageSVGKitPlugin), SVG using Core Animation rendering - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching From b080a4c5a240654c0537d92176bbb3f6226a3a11 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 9 Mar 2020 20:11:23 +0800 Subject: [PATCH 130/181] Update the readme again about the coder plugins's framework and Animated Image --- README.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7f9c5df9..0b65b8ef 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format. Based on [libbpg](https://github.com/mirrorer/libbpg) - [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format. Based on [libflif](https://github.com/FLIF-hub/FLIF) - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format. Based on [libavif](https://github.com/AOMediaCodec/libavif) -- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. -- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. +- [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. Using built-in frameworks +- [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. Using built-in frameworks - [SDWebImageLottieCoder](https://github.com/SDWebImage/SDWebImageLottieCoder) - coder for Lottie animation format. Based on [rlottie](https://github.com/Samsung/rlottie) - and more from community! @@ -67,8 +67,8 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries -- [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with JSON files -- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SDWebImage/SDWebImageSVGKitPlugin), SVG using Core Animation rendering +- [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with remote JSON files +- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), iOS 8+/macOS 10.10+ support SVG rendering using Core Animation - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching @@ -147,6 +147,22 @@ imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg In 5.0, we introduced a brand new mechanism for supporting animated images. This includes animated image loading, rendering, decoding, and also supports customizations (for advanced users). This animated image solution is available for `iOS`/`tvOS`/`macOS`. The `SDAnimatedImage` is subclass of `UIImage/NSImage`, and `SDAnimatedImageView` is subclass of `UIImageView/NSImageView`, to make them compatible with the common frameworks APIs. See [Animated Image](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for more detailed information. +* Objective-C + +```objective-c +SDAnimatedImageView *imageView = [SDAnimatedImageView new]; +SDAnimatedImage *animatedImage = [SDAnimatedImage imageNamed:@"image.gif"]; +imageView.image = animatedImage; +``` + +* Swift + +```swift +let imageView = SDAnimatedImageView() +let animatedImage = SDAnimatedImage(name: "image.gif") +imageView.image = animatedImage +``` + #### FLAnimatedImage integration has its own dedicated repo In order to clean up things and make our core project do less things, we decided that the `FLAnimatedImage` integration does not belong here. From 5.0, this will still be available, but under a dedicated repo [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin). From c975288eb45b859bbb08dcb44290e334ddfe3195 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 9 Mar 2020 20:22:53 +0800 Subject: [PATCH 131/181] Update the readme again with the SwiftPM support on codecs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b65b8ef..1d24fb83 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,15 @@ We support SwiftUI by building with the functions (caching, loading and animatio #### Integration with 3rd party libraries - [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with remote JSON files -- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), iOS 8+/macOS 10.10+ support SVG rendering using Core Animation +- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), SVG rendering using Core Animation, iOS 8+/macOS 10.10+ support - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching #### Make our lives easier - [libwebp-Xcode](https://github.com/SDWebImage/libwebp-Xcode) - A wrapper for [libwebp](https://chromium.googlesource.com/webm/libwebp) + an Xcode project. - [libheif-Xcode](https://github.com/SDWebImage/libheif-Xcode) - A wrapper for [libheif](https://github.com/strukturag/libheif) + an Xcode project. -- and more third-party C/C++ image codec libraries with CocoaPods/Carthage support. +- [libavif-Xcode](https://github.com/SDWebImage/libavif-Xcode) - A wrapper for [libavif](https://github.com/AOMediaCodec/libavif) + an Xcode project. +- and more third-party C/C++ image codec libraries with CocoaPods/Carthage/SwiftPM support. You can use those directly, or create similar components of your own. From 3d1280315b6500e51b1f06103980d0d52d17dde3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 11 Mar 2020 11:45:19 +0800 Subject: [PATCH 132/181] Keep the progressive decoding process only exist one per image download. Cancel the unused progressive decoding when full pixel data is available. --- .../Core/SDWebImageDownloaderOperation.m | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 67d1a8e2..1de8212a 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -54,7 +54,7 @@ typedef NSMutableDictionary SDCallbacksDictionary; @property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); -@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding +@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding #if SD_UIKIT @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; #endif @@ -89,7 +89,8 @@ typedef NSMutableDictionary SDCallbacksDictionary; _finished = NO; _expectedSize = 0; _unownedSession = session; - _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL); + _coderQueue = [NSOperationQueue new]; + _coderQueue.maxConcurrentOperationCount = 1; #if SD_UIKIT _backgroundTaskId = UIBackgroundTaskInvalid; #endif @@ -384,17 +385,18 @@ didReceiveResponse:(NSURLResponse *)response // Get the image data NSData *imageData = [self.imageData copy]; - // progressive decode the image in coder queue - dispatch_async(self.coderQueue, ^{ - @autoreleasepool { + // keep maxmium one progressive decode process during download + if (self.coderQueue.operationCount == 0) { + // NSOperation have autoreleasepool, don't need to create extra one + [self.coderQueue addOperationWithBlock:^{ UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); if (image) { // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; } - } - }); + }]; + } } for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { @@ -461,19 +463,18 @@ didReceiveResponse:(NSURLResponse *)response [self callCompletionBlocksWithError:self.responseError]; [self done]; } else { - // decode the image in coder queue - dispatch_async(self.coderQueue, ^{ - @autoreleasepool { - UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); - CGSize imageSize = image.size; - if (imageSize.width == 0 || imageSize.height == 0) { - [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; - } else { - [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES]; - } - [self done]; + // decode the image in coder queue, cancel all previous decoding process + [self.coderQueue cancelAllOperations]; + [self.coderQueue addOperationWithBlock:^{ + UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); + CGSize imageSize = image.size; + if (imageSize.width == 0 || imageSize.height == 0) { + [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; + } else { + [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES]; } - }); + [self done]; + }]; } } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; From 13d0e739fbb36f9e99e3cdb8ca29256894f9e359 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 11 Mar 2020 11:54:10 +0800 Subject: [PATCH 133/181] Update the coderQueue QoS based on SDWebImageDownloaderLowPriority && SDWebImageDownloaderHighPriority --- SDWebImage/Core/SDWebImageDownloaderOperation.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 1de8212a..00d803f2 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -208,8 +208,13 @@ typedef NSMutableDictionary SDCallbacksDictionary; if (self.dataTask) { if (self.options & SDWebImageDownloaderHighPriority) { self.dataTask.priority = NSURLSessionTaskPriorityHigh; + self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive; } else if (self.options & SDWebImageDownloaderLowPriority) { self.dataTask.priority = NSURLSessionTaskPriorityLow; + self.coderQueue.qualityOfService = NSQualityOfServiceBackground; + } else { + self.dataTask.priority = NSURLSessionTaskPriorityDefault; + self.coderQueue.qualityOfService = NSQualityOfServiceDefault; } [self.dataTask resume]; for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { From 8f9174a952caa3b17e8a4fd1b0621bdd7fb4b253 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 11 Mar 2020 12:19:10 +0800 Subject: [PATCH 134/181] Coding Style fix --- SDWebImage/Private/SDFileAttributeHelper.h | 10 +++++----- SDWebImage/Private/SDFileAttributeHelper.m | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Private/SDFileAttributeHelper.h b/SDWebImage/Private/SDFileAttributeHelper.h index b5594e95..3ce6bade 100644 --- a/SDWebImage/Private/SDFileAttributeHelper.h +++ b/SDWebImage/Private/SDFileAttributeHelper.h @@ -10,10 +10,10 @@ /// File Extended Attribute (xattr) helper methods @interface SDFileAttributeHelper : NSObject -+ (nullable NSArray *)extendedAttributeNamesAtPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; -+ (BOOL)hasExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; -+ (nullable NSData *)extendedAttribute:(nonnull NSString*)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; -+ (BOOL)setExtendedAttribute:(nonnull NSString*)name value:(nonnull NSData *)value atPath:(nonnull NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError * _Nullable * _Nullable)err; -+ (BOOL)removeExtendedAttribute:(nonnull NSString*)name atPath:(nonnull NSString*)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (nullable NSArray *)extendedAttributeNamesAtPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)hasExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (nullable NSData *)extendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)setExtendedAttribute:(nonnull NSString *)name value:(nonnull NSData *)value atPath:(nonnull NSString *)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError * _Nullable * _Nullable)err; ++ (BOOL)removeExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; @end diff --git a/SDWebImage/Private/SDFileAttributeHelper.m b/SDWebImage/Private/SDFileAttributeHelper.m index fcb8ad47..45c015e0 100644 --- a/SDWebImage/Private/SDFileAttributeHelper.m +++ b/SDWebImage/Private/SDFileAttributeHelper.m @@ -10,7 +10,7 @@ @implementation SDFileAttributeHelper -+ (NSArray*)extendedAttributeNamesAtPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (NSArray*)extendedAttributeNamesAtPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list @@ -39,7 +39,7 @@ return [NSArray arrayWithArray:names]; } -+ (BOOL)hasExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (BOOL)hasExtendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list @@ -67,7 +67,7 @@ return NO; } -+ (NSData*)extendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (NSData *)extendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get length ssize_t attrLen = getxattr([path fileSystemRepresentation], [name UTF8String], NULL, 0, 0, flags); @@ -85,12 +85,12 @@ } // get attribute data - NSMutableData * attrData = [NSMutableData dataWithLength:attrLen]; + NSMutableData *attrData = [NSMutableData dataWithLength:attrLen]; getxattr([path fileSystemRepresentation], [name UTF8String], [attrData mutableBytes], attrLen, 0, flags); return attrData; } -+ (BOOL)setExtendedAttribute:(NSString*)name value:(NSData*)value atPath:(NSString*)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError**)err { ++ (BOOL)setExtendedAttribute:(NSString *)name value:(NSData *)value atPath:(NSString *)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError **)err { int flags = (follow? 0 : XATTR_NOFOLLOW) | (overwrite? 0 : XATTR_CREATE); if (0 == setxattr([path fileSystemRepresentation], [name UTF8String], [value bytes], [value length], 0, flags)) return YES; // error @@ -108,7 +108,7 @@ return NO; } -+ (BOOL)removeExtendedAttribute:(NSString*)name atPath:(NSString*)path traverseLink:(BOOL)follow error:(NSError**)err { ++ (BOOL)removeExtendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = (follow? 0 : XATTR_NOFOLLOW); if (0 == removexattr([path fileSystemRepresentation], [name UTF8String], flags)) return YES; // error From 2dcf1b65994d92ef8806f52dc41aa6fdd677489c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 13 Mar 2020 20:49:15 +0800 Subject: [PATCH 135/181] Bumped version to 5.6.1 Update the CHANGELOG --- CHANGELOG.md | 9 +++++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56f967b..a4c32f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [5.6.1 - 5.6 Patch, on Mar 13th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.6.1) +See [all tickets marked for the 5.6.1 release](https://github.com/SDWebImage/SDWebImage/milestone/65) + +### Performances +- Keep the progressive decoding process only exist one per image download. Cancel the unused progressive decoding when full pixel data is available. #2483 + +### Fixes +- Fix the NotificationCenter does not remove the observer and little private header garden #2959 + ## [5.6.0 - URLSession Metrics && Vector Format, on Mar 5th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.6.0) See [all tickets marked for the 5.6.0 release](https://github.com/SDWebImage/SDWebImage/milestone/63) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 919e806f..8f0f1ae1 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.6.0' + s.version = '5.6.1' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 94bc5516..10938b0c 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.6.0 + 5.6.1 CFBundleSignature ???? CFBundleVersion - 5.6.0 + 5.6.1 NSPrincipalClass From ef6aebf9ae09db18918e8a922268eb9ef7789be9 Mon Sep 17 00:00:00 2001 From: rain2540 Date: Wed, 25 Mar 2020 14:16:19 +0800 Subject: [PATCH 136/181] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d24fb83..4c371c21 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ imageView.image = animatedImage; ```swift let imageView = SDAnimatedImageView() -let animatedImage = SDAnimatedImage(name: "image.gif") +let animatedImage = SDAnimatedImage(named: "image.gif") imageView.image = animatedImage ``` From a7606eb5e289de68ea8caa485a2563df0529830f Mon Sep 17 00:00:00 2001 From: huangboju Date: Wed, 1 Apr 2020 14:38:22 +0800 Subject: [PATCH 137/181] simplify code --- SDWebImage/Private/SDFileAttributeHelper.m | 78 +++++++++++----------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/SDWebImage/Private/SDFileAttributeHelper.m b/SDWebImage/Private/SDFileAttributeHelper.m index 45c015e0..65ecacd7 100644 --- a/SDWebImage/Private/SDFileAttributeHelper.m +++ b/SDWebImage/Private/SDFileAttributeHelper.m @@ -17,15 +17,15 @@ ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); if (nameBuffLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSString stringWithUTF8String:strerror(errno)], @"error", - @"listxattr", @"function", - path, @":path", - [NSNumber numberWithBool:follow], @":traverseLink", - nil] - ]; + @{ + @"error": [NSString stringWithUTF8String:strerror(errno)], + @"function": @"listxattr", + @":path": path, + @":traverseLink": @(follow) + } + ]; return nil; - } else if (nameBuffLen == 0) return [NSArray array]; + } else if (nameBuffLen == 0) return @[]; // get name list NSMutableData *nameBuff = [NSMutableData dataWithLength:nameBuffLen]; @@ -36,7 +36,7 @@ char *nextName, *endOfNames = [nameBuff mutableBytes] + nameBuffLen; for(nextName = [nameBuff mutableBytes]; nextName < endOfNames; nextName += 1+strlen(nextName)) [names addObject:[NSString stringWithUTF8String:nextName]]; - return [NSArray arrayWithArray:names]; + return names.copy; } + (BOOL)hasExtendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { @@ -46,13 +46,13 @@ ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); if (nameBuffLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSString stringWithUTF8String:strerror(errno)], @"error", - @"listxattr", @"function", - path, @":path", - [NSNumber numberWithBool:follow], @":traverseLink", - nil] - ]; + @{ + @"error": [NSString stringWithUTF8String:strerror(errno)], + @"function": @"listxattr", + @":path": path, + @":traverseLink": @(follow) + } + ]; return NO; } else if (nameBuffLen == 0) return NO; @@ -73,13 +73,13 @@ ssize_t attrLen = getxattr([path fileSystemRepresentation], [name UTF8String], NULL, 0, 0, flags); if (attrLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSString stringWithUTF8String:strerror(errno)], @"error", - @"getxattr", @"function", - name, @":name", - path, @":path", - [NSNumber numberWithBool:follow], @":traverseLink", - nil] + @{ + @"error": [NSString stringWithUTF8String:strerror(errno)], + @"function": @"getxattr", + @":name": name, + @":path": path, + @":traverseLink": @(follow) + } ]; return nil; } @@ -95,15 +95,15 @@ if (0 == setxattr([path fileSystemRepresentation], [name UTF8String], [value bytes], [value length], 0, flags)) return YES; // error if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSString stringWithUTF8String:strerror(errno)], @"error", - @"setxattr", @"function", - name, @":name", - [NSNumber numberWithUnsignedInteger:[value length]], @":value.length", - path, @":path", - [NSNumber numberWithBool:follow], @":traverseLink", - [NSNumber numberWithBool:overwrite], @":overwrite", - nil] + @{ + @"error": [NSString stringWithUTF8String:strerror(errno)], + @"function": @"setxattr", + @":name": name, + @":value.length": [NSNumber numberWithUnsignedInteger:[value length]], + @":path": path, + @":traverseLink": @(follow), + @":overwrite": @(overwrite) + } ]; return NO; } @@ -113,13 +113,13 @@ if (0 == removexattr([path fileSystemRepresentation], [name UTF8String], flags)) return YES; // error if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSString stringWithUTF8String:strerror(errno)], @"error", - @"removexattr", @"function", - name, @":name", - path, @":path", - [NSNumber numberWithBool:follow], @":traverseLink", - nil] + @{ + @"error": [NSString stringWithUTF8String:strerror(errno)], + @"function": @"removexattr", + @":name": name, + @":path": path, + @":traverseLink": @(follow) + } ]; return NO; } From fbe76bc43666aaa27b510aeef0ecf2e87e27c3b1 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 17:01:51 +0800 Subject: [PATCH 138/181] Added new query cache type support, including the SDImageCache API and context option --- SDWebImage/Core/SDImageCache.h | 13 +++++++++++++ SDWebImage/Core/SDImageCache.m | 22 ++++++++++++++++++---- SDWebImage/Core/SDImageCacheDefine.h | 2 ++ SDWebImage/Core/SDImageCachesManager.m | 22 +++++++++++----------- SDWebImage/Core/SDWebImageDefine.h | 6 ++++++ SDWebImage/Core/SDWebImageDefine.m | 1 + SDWebImage/Core/SDWebImageManager.m | 7 ++++++- 7 files changed, 57 insertions(+), 16 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 1b1afd47..d3d97c17 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -268,6 +268,19 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { */ - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; +/** + * Asynchronously queries the cache with operation and call the completion when done. + * + * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. + * @param options A mask to specify options to use for this cache query + * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. + * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediatelly. + * @param doneBlock The completion block. Will not get called if the operation is cancelled + * + * @return a NSOperation instance containing the cache op + */ +- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; + /** * Synchronously query the memory cache. * diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index f7585895..28db40f7 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -426,12 +426,23 @@ } - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { + return [self queryCacheOperationForKey:key options:options context:context cacheType:SDImageCacheTypeAll done:doneBlock]; +} + +- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { if (!key) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } + // Invalid cache type + if (queryCacheType == SDImageCacheTypeNone) { + if (doneBlock) { + doneBlock(nil, nil, SDImageCacheTypeNone); + } + return nil; + } id transformer = context[SDWebImageContextImageTransformer]; if (transformer) { @@ -441,7 +452,10 @@ } // First check the in-memory cache... - UIImage *image = [self imageFromMemoryCacheForKey:key]; + UIImage *image; + if (queryCacheType != SDImageCacheTypeDisk) { + image = [self imageFromMemoryCacheForKey:key]; + } if (image) { if (options & SDImageCacheDecodeFirstFrameOnly) { @@ -464,7 +478,7 @@ } } - BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData)); + BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil, SDImageCacheTypeMemory); @@ -698,7 +712,7 @@ #pragma mark - SDImageCache -- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { +- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { SDImageCacheOptions cacheOptions = 0; if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData; if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync; @@ -709,7 +723,7 @@ if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames; if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass; - return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock]; + return [self queryCacheOperationForKey:key options:cacheOptions context:context cacheType:cacheType done:completionBlock]; } - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { diff --git a/SDWebImage/Core/SDImageCacheDefine.h b/SDWebImage/Core/SDImageCacheDefine.h index be4e0211..3ca07ec9 100644 --- a/SDWebImage/Core/SDImageCacheDefine.h +++ b/SDWebImage/Core/SDImageCacheDefine.h @@ -68,12 +68,14 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn @param key The image cache key @param options A mask to specify options to use for this query @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. + @param cacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediatelly. @param completionBlock The completion block. Will not get called if the operation is cancelled @return The operation for this query */ - (nullable id)queryImageForKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context + cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock; /** diff --git a/SDWebImage/Core/SDImageCachesManager.m b/SDWebImage/Core/SDImageCachesManager.m index 6b6f7d8a..de03ed96 100644 --- a/SDWebImage/Core/SDImageCachesManager.m +++ b/SDWebImage/Core/SDImageCachesManager.m @@ -84,7 +84,7 @@ #pragma mark - SDImageCache -- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context completion:(SDImageCacheQueryCompletionBlock)completionBlock { +- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock { if (!key) { return nil; } @@ -93,30 +93,30 @@ if (count == 0) { return nil; } else if (count == 1) { - return [caches.firstObject queryImageForKey:key options:options context:context completion:completionBlock]; + return [caches.firstObject queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } switch (self.queryOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; - return [cache queryImageForKey:key options:options context:context completion:completionBlock]; + return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; - return [cache queryImageForKey:key options:options context:context completion:completionBlock]; + return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; - [self concurrentQueryImageForKey:key options:options context:context completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; + [self concurrentQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; case SDImageCachesManagerOperationPolicySerial: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; - [self serialQueryImageForKey:key options:options context:context completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; + [self serialQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; @@ -279,11 +279,11 @@ #pragma mark - Concurrent Operation -- (void)concurrentQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { +- (void)concurrentQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { - [cache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { if (operation.isCancelled) { // Cancelled return; @@ -422,7 +422,7 @@ #pragma mark - Serial Operation -- (void)serialQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { +- (void)serialQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); id cache = enumerator.nextObject; @@ -435,7 +435,7 @@ return; } @weakify(self); - [cache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { @strongify(self); if (operation.isCancelled) { // Cancelled @@ -455,7 +455,7 @@ return; } // Next - [self serialQueryImageForKey:key options:options context:context completion:completionBlock enumerator:enumerator operation:operation]; + [self serialQueryImageForKey:key options:options context:context cacheType:queryCacheType completion:completionBlock enumerator:enumerator operation:operation]; }]; } diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 1be59810..1e8afef8 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -259,6 +259,12 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; +/** + A SDImageCacheType raw value which specify the source of cache to query. For example, you can query memory only, query disk only, or query from both memory and disk cache. + If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextQueryCacheType; + /** A SDImageCacheType raw value which specify the store cache type when the image has just been downloaded and will be stored to the cache. Specify `SDImageCacheTypeNone` to disable cache storage; `SDImageCacheTypeDisk` to store in disk cache only; `SDImageCacheTypeMemory` to store in memory only. And `SDImageCacheTypeAll` to store in both memory cache and disk cache. If you use image transformer feature, this actually apply for the transformed image, but not the original image itself. Use `SDWebImageContextOriginalStoreCacheType` if you want to control the original image's store cache type at the same time. diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 866b164d..2809af69 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -127,6 +127,7 @@ SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransfo SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor"; SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio"; SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize"; +SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType"; SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType"; SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType"; SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass"; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 4d33e6d7..a58831aa 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -220,10 +220,15 @@ static id _defaultImageLoader; } // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); + // Get the query cache type + SDImageCacheType queryCacheType = SDImageCacheTypeAll; + if (context[SDWebImageContextQueryCacheType]) { + queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue]; + } if (shouldQueryCache) { NSString *key = [self cacheKeyForURL:url context:context]; @weakify(operation); - operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { + operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user From 7f540a6296ec6d8fbf6abf7457816411c5d39519 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 17:03:56 +0800 Subject: [PATCH 139/181] Fix the test cases for cacheType arg --- Tests/Tests/SDImageCacheTests.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index 93adcd47..f6961ef4 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -610,7 +610,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; - (void)test50SDImageCacheQueryOp { XCTestExpectation *expectation = [self expectationWithDescription:@"SDImageCache query op works"]; [[SDImageCache sharedImageCache] storeImage:[self testJPEGImage] forKey:kTestImageKeyJPEG toDisk:NO completion:nil]; - [[SDImageCachesManager sharedManager] queryImageForKey:kTestImageKeyJPEG options:0 context:nil completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + [[SDImageCachesManager sharedManager] queryImageForKey:kTestImageKeyJPEG options:0 context:nil cacheType:SDImageCacheTypeAll completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { expect(image).notTo.beNil(); [expectation fulfill]; }]; @@ -680,7 +680,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; cachesManager.removeOperationPolicy = SDImageCachesManagerOperationPolicyLowestOnly; cachesManager.containsOperationPolicy = SDImageCachesManagerOperationPolicyLowestOnly; cachesManager.clearOperationPolicy = SDImageCachesManagerOperationPolicyLowestOnly; - [cachesManager queryImageForKey:kTestImageKeyJPEG options:0 context:nil completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + [cachesManager queryImageForKey:kTestImageKeyJPEG options:0 context:nil cacheType:SDImageCacheTypeAll completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { expect(image).to.beNil(); }]; [cachesManager storeImage:[self testJPEGImage] imageData:nil forKey:kTestImageKeyJPEG cacheType:SDImageCacheTypeMemory completion:nil]; @@ -699,7 +699,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; cachesManager.removeOperationPolicy = SDImageCachesManagerOperationPolicyHighestOnly; cachesManager.containsOperationPolicy = SDImageCachesManagerOperationPolicyHighestOnly; cachesManager.clearOperationPolicy = SDImageCachesManagerOperationPolicyHighestOnly; - [cachesManager queryImageForKey:kTestImageKeyPNG options:0 context:nil completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + [cachesManager queryImageForKey:kTestImageKeyPNG options:0 context:nil cacheType:SDImageCacheTypeAll completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { expect(image).to.beNil(); }]; [cachesManager storeImage:[self testPNGImage] imageData:nil forKey:kTestImageKeyPNG cacheType:SDImageCacheTypeMemory completion:nil]; @@ -732,7 +732,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; cachesManager.removeOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent; cachesManager.containsOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent; cachesManager.clearOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent; - [cachesManager queryImageForKey:kConcurrentTestImageKey options:0 context:nil completion:nil]; + [cachesManager queryImageForKey:kConcurrentTestImageKey options:0 context:nil cacheType:SDImageCacheTypeAll completion:nil]; [cachesManager storeImage:[self testJPEGImage] imageData:nil forKey:kConcurrentTestImageKey cacheType:SDImageCacheTypeMemory completion:nil]; [cachesManager removeImageForKey:kConcurrentTestImageKey cacheType:SDImageCacheTypeMemory completion:nil]; [cachesManager clearWithCacheType:SDImageCacheTypeMemory completion:nil]; @@ -772,7 +772,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; cachesManager.removeOperationPolicy = SDImageCachesManagerOperationPolicySerial; cachesManager.containsOperationPolicy = SDImageCachesManagerOperationPolicySerial; cachesManager.clearOperationPolicy = SDImageCachesManagerOperationPolicySerial; - [cachesManager queryImageForKey:kSerialTestImageKey options:0 context:nil completion:nil]; + [cachesManager queryImageForKey:kSerialTestImageKey options:0 context:nil cacheType:SDImageCacheTypeAll completion:nil]; [cachesManager storeImage:[self testJPEGImage] imageData:nil forKey:kSerialTestImageKey cacheType:SDImageCacheTypeMemory completion:nil]; [cachesManager removeImageForKey:kSerialTestImageKey cacheType:SDImageCacheTypeMemory completion:nil]; [cachesManager clearWithCacheType:SDImageCacheTypeMemory completion:nil]; From de153b0a32a98c81b03ca086a10968346f7f1b98 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 17:12:11 +0800 Subject: [PATCH 140/181] Added the new Async API for disk data query, to avoid user to dispatch their own global queu (not IO queue), solve the IO safe issue --- SDWebImage/Core/SDImageCache.h | 9 +++++++++ SDWebImage/Core/SDImageCache.m | 11 +++++++++++ SDWebImage/Core/SDImageCacheDefine.h | 1 + 3 files changed, 21 insertions(+) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index d3d97c17..ec90efb0 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -235,6 +235,15 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { */ - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key; +/** + * Asynchronously load the image data in disk cache. You can decode the image data to image after loaded. + * + * @param key The unique key used to store the wanted image + * @param completionBlock the block to be executed when the check is done. + * @note the completion block will be always executed on the main queue + */ +- (void)diskImageDataQueryForKey:(nullable NSString *)key completion:(nullable SDImageCacheQueryDataCompletionBlock)completionBlock; + /** * Operation that queries the cache asynchronously and call the completion when done. * diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 28db40f7..6d9a8cd3 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -314,6 +314,17 @@ return [self.diskCache containsDataForKey:key]; } +- (void)diskImageDataQueryForKey:(NSString *)key completion:(SDImageCacheQueryDataCompletionBlock)completionBlock { + dispatch_async(self.ioQueue, ^{ + NSData *imageData = [self diskImageDataBySearchingAllPathsForKey:key]; + if (completionBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionBlock(imageData); + }); + } + }); +} + - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key { if (!key) { return nil; diff --git a/SDWebImage/Core/SDImageCacheDefine.h b/SDWebImage/Core/SDImageCacheDefine.h index 3ca07ec9..479d0dd9 100644 --- a/SDWebImage/Core/SDImageCacheDefine.h +++ b/SDWebImage/Core/SDImageCacheDefine.h @@ -36,6 +36,7 @@ typedef NS_ENUM(NSInteger, SDImageCacheType) { }; typedef void(^SDImageCacheCheckCompletionBlock)(BOOL isInCache); +typedef void(^SDImageCacheQueryDataCompletionBlock)(NSData * _Nullable data); typedef void(^SDImageCacheCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize); typedef NSString * _Nullable (^SDImageCacheAdditionalCachePathBlock)(NSString * _Nonnull key); typedef void(^SDImageCacheQueryCompletionBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType); From 45be39f4a1dcb5869014a757879e0df3a64e6837 Mon Sep 17 00:00:00 2001 From: huangboju Date: Thu, 2 Apr 2020 10:15:12 +0800 Subject: [PATCH 141/181] Update SDFileAttributeHelper.m --- SDWebImage/Private/SDFileAttributeHelper.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Private/SDFileAttributeHelper.m b/SDWebImage/Private/SDFileAttributeHelper.m index 65ecacd7..5122089d 100644 --- a/SDWebImage/Private/SDFileAttributeHelper.m +++ b/SDWebImage/Private/SDFileAttributeHelper.m @@ -99,7 +99,7 @@ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"setxattr", @":name": name, - @":value.length": [NSNumber numberWithUnsignedInteger:[value length]], + @":value.length": @(value.length), @":path": path, @":traverseLink": @(follow), @":overwrite": @(overwrite) From ae1f6b9b8c0ae1f8bc2c114bad99e7cfc69bd8b3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 2 Apr 2020 11:41:17 +0800 Subject: [PATCH 142/181] Revert the removal to the old SDImageCache protocol API, should keep API with to use .all cache type, until next major version --- SDWebImage/Core/SDImageCache.m | 4 ++++ SDWebImage/Core/SDImageCacheDefine.h | 15 +++++++++++++++ SDWebImage/Core/SDImageCachesManager.m | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 6d9a8cd3..af201386 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -723,6 +723,10 @@ #pragma mark - SDImageCache +- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { + return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock]; +} + - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { SDImageCacheOptions cacheOptions = 0; if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData; diff --git a/SDWebImage/Core/SDImageCacheDefine.h b/SDWebImage/Core/SDImageCacheDefine.h index 479d0dd9..e2449bfd 100644 --- a/SDWebImage/Core/SDImageCacheDefine.h +++ b/SDWebImage/Core/SDImageCacheDefine.h @@ -62,6 +62,21 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn @protocol SDImageCache @required +/** + Query the cached image from image cache for given key. The operation can be used to cancel the query. + If image is cached in memory, completion is called synchronously, else aynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`) + + @param key The image cache key + @param options A mask to specify options to use for this query + @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. + @param completionBlock The completion block. Will not get called if the operation is cancelled + @return The operation for this query + */ +- (nullable id)queryImageForKey:(nullable NSString *)key + options:(SDWebImageOptions)options + context:(nullable SDWebImageContext *)context + completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock; + /** Query the cached image from image cache for given key. The operation can be used to cancel the query. If image is cached in memory, completion is called synchronously, else aynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`) diff --git a/SDWebImage/Core/SDImageCachesManager.m b/SDWebImage/Core/SDImageCachesManager.m index de03ed96..b6b13c12 100644 --- a/SDWebImage/Core/SDImageCachesManager.m +++ b/SDWebImage/Core/SDImageCachesManager.m @@ -84,6 +84,10 @@ #pragma mark - SDImageCache +- (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context completion:(SDImageCacheQueryCompletionBlock)completionBlock { + return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock]; +} + - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock { if (!key) { return nil; From d4da82e9c3ce632e4f48217db57edcfd7c6996c5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 2 Apr 2020 12:18:12 +0800 Subject: [PATCH 143/181] Update the test cases about the custom ImageCache protocol --- Tests/Tests/SDImageCacheTests.m | 32 +++++ Tests/Tests/SDWebImageDownloaderTests.m | 6 +- Tests/Tests/SDWebImageTestCache.h | 13 +- Tests/Tests/SDWebImageTestCache.m | 152 +++++++++++++++++++++++- 4 files changed, 197 insertions(+), 6 deletions(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index f6961ef4..0b195f89 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -793,6 +793,38 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; [self waitForExpectationsWithCommonTimeout]; } +- (void)test58CustomImageCache { + NSString *cachePath = [[self userCacheDirectory] stringByAppendingPathComponent:@"custom"]; + SDImageCacheConfig *config = [[SDImageCacheConfig alloc] init]; + SDWebImageTestCache *cache = [[SDWebImageTestCache alloc] initWithCachePath:cachePath config:config]; + expect(cache.memoryCache).notTo.beNil(); + expect(cache.diskCache).notTo.beNil(); + + // Store + UIImage *image1 = self.testJPEGImage; + NSString *key1 = @"testJPEGImage"; + [cache storeImage:image1 imageData:nil forKey:key1 cacheType:SDImageCacheTypeAll completion:nil]; + // Contain + [cache containsImageForKey:key1 cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType containsCacheType) { + expect(containsCacheType).equal(SDImageCacheTypeMemory); + }]; + // Query + [cache queryImageForKey:key1 options:0 context:nil cacheType:SDImageCacheTypeAll completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { + expect(image).equal(image1); + expect(data).beNil(); + expect(cacheType).equal(SDImageCacheTypeMemory); + }]; + // Remove + [cache removeImageForKey:key1 cacheType:SDImageCacheTypeAll completion:nil]; + [cache containsImageForKey:key1 cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType containsCacheType) { + expect(containsCacheType).equal(SDImageCacheTypeNone); + }]; + // Clear + [cache clearWithCacheType:SDImageCacheTypeAll completion:nil]; + NSArray *cacheFiles = [cache.diskCache.fileManager contentsOfDirectoryAtPath:cachePath error:nil]; + expect(cacheFiles.count).equal(0); +} + #pragma mark Helper methods - (UIImage *)testJPEGImage { diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m index da0ee89b..4cd877c1 100644 --- a/Tests/Tests/SDWebImageDownloaderTests.m +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -474,7 +474,6 @@ [self waitForExpectationsWithCommonTimeout]; } -#if SD_UIKIT - (void)test22ThatCustomDecoderWorksForImageDownload { XCTestExpectation *expectation = [self expectationWithDescription:@"Custom decoder for SDWebImageDownloader not works"]; SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] init]; @@ -487,8 +486,8 @@ UIImage *testJPEGImage = [[UIImage alloc] initWithContentsOfFile:testJPEGImagePath]; [downloader downloadImageWithURL:testImageURL options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) { - NSData *data1 = UIImagePNGRepresentation(testJPEGImage); - NSData *data2 = UIImagePNGRepresentation(image); + NSData *data1 = [testJPEGImage sd_imageDataAsFormat:SDImageFormatPNG]; + NSData *data2 = [image sd_imageDataAsFormat:SDImageFormatPNG]; if (![data1 isEqualToData:data2]) { XCTFail(@"The image data is not equal to cutom decoder, check -[SDWebImageTestDecoder decodedImageWithData:]"); } @@ -499,7 +498,6 @@ [self waitForExpectationsWithCommonTimeout]; [downloader invalidateSessionAndCancel:YES]; } -#endif - (void)test23ThatDownloadRequestModifierWorks { XCTestExpectation *expectation = [self expectationWithDescription:@"Download request modifier not works"]; diff --git a/Tests/Tests/SDWebImageTestCache.h b/Tests/Tests/SDWebImageTestCache.h index 0736c698..8029ff4d 100644 --- a/Tests/Tests/SDWebImageTestCache.h +++ b/Tests/Tests/SDWebImageTestCache.h @@ -9,9 +9,9 @@ #import #import +#import // A really naive implementation of custom memory cache and disk cache - @interface SDWebImageTestMemoryCache : NSObject @property (nonatomic, strong, nonnull) SDImageCacheConfig *config; @@ -26,3 +26,14 @@ @property (nonatomic, strong, nonnull) NSFileManager *fileManager; @end + +// A really naive implementation of custom image cache using memory cache and disk cache +@interface SDWebImageTestCache : NSObject + +@property (nonatomic, strong, nonnull) SDImageCacheConfig *config; +@property (nonatomic, strong, nonnull) SDWebImageTestMemoryCache *memoryCache; +@property (nonatomic, strong, nonnull) SDWebImageTestDiskCache *diskCache; + +- (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config; + +@end diff --git a/Tests/Tests/SDWebImageTestCache.m b/Tests/Tests/SDWebImageTestCache.m index 68b2a154..143f2c80 100644 --- a/Tests/Tests/SDWebImageTestCache.m +++ b/Tests/Tests/SDWebImageTestCache.m @@ -8,7 +8,7 @@ */ #import "SDWebImageTestCache.h" -#import +#import #import "SDFileAttributeHelper.h" static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hackemist.SDWebImageTestDiskCache"; @@ -122,3 +122,153 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac } @end + +@implementation SDWebImageTestCache + +- (instancetype)initWithCachePath:(NSString *)cachePath config:(SDImageCacheConfig *)config { + self = [super init]; + if (self) { + self.config = config; + self.memoryCache = [[SDWebImageTestMemoryCache alloc] initWithConfig:config]; + self.diskCache = [[SDWebImageTestDiskCache alloc] initWithCachePath:cachePath config:config]; + } + return self; +} + +- (void)clearWithCacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { + switch (cacheType) { + case SDImageCacheTypeNone: + break; + case SDImageCacheTypeMemory: + [self.memoryCache removeAllObjects]; + break; + case SDImageCacheTypeDisk: + [self.diskCache removeAllData]; + break; + case SDImageCacheTypeAll: + [self.memoryCache removeAllObjects]; + [self.diskCache removeAllData]; + break; + default: + break; + } + if (completionBlock) { + completionBlock(); + } +} + +- (void)containsImageForKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock { + SDImageCacheType containsCacheType = SDImageCacheTypeNone; + switch (cacheType) { + case SDImageCacheTypeNone: + break; + case SDImageCacheTypeMemory: + containsCacheType = [self.memoryCache objectForKey:key] ? SDImageCacheTypeMemory : SDImageCacheTypeNone; + break; + case SDImageCacheTypeDisk: + containsCacheType = [self.diskCache containsDataForKey:key] ? SDImageCacheTypeDisk : SDImageCacheTypeNone; + break; + case SDImageCacheTypeAll: + if ([self.memoryCache objectForKey:key]) { + containsCacheType = SDImageCacheTypeMemory; + } else if ([self.diskCache containsDataForKey:key]) { + containsCacheType = SDImageCacheTypeDisk; + } + default: + break; + } + if (completionBlock) { + completionBlock(containsCacheType); + } +} + +- (nullable id)queryImageForKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { + return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock]; +} + +- (nullable id)queryImageForKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { + UIImage *image; + NSData *data; + SDImageCacheType resultCacheType = SDImageCacheTypeNone; + switch (cacheType) { + case SDImageCacheTypeNone: + break; + case SDImageCacheTypeMemory: + image = [self.memoryCache objectForKey:key]; + if (image) { + resultCacheType = SDImageCacheTypeMemory; + } + break; + case SDImageCacheTypeDisk: + data = [self.diskCache dataForKey:key]; + image = [UIImage sd_imageWithData:data]; + if (data) { + resultCacheType = SDImageCacheTypeDisk; + } + break; + case SDImageCacheTypeAll: + image = [self.memoryCache objectForKey:key]; + if (image) { + resultCacheType = SDImageCacheTypeMemory; + } else { + data = [self.diskCache dataForKey:key]; + image = [UIImage sd_imageWithData:data]; + if (data) { + resultCacheType = SDImageCacheTypeDisk; + } + } + break; + default: + break; + } + if (completionBlock) { + completionBlock(image, data, resultCacheType); + } + return nil; +} + +- (void)removeImageForKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { + switch (cacheType) { + case SDImageCacheTypeNone: + break; + case SDImageCacheTypeMemory: + [self.memoryCache removeObjectForKey:key]; + break; + case SDImageCacheTypeDisk: + [self.diskCache removeDataForKey:key]; + break; + case SDImageCacheTypeAll: + [self.memoryCache removeObjectForKey:key]; + [self.diskCache removeDataForKey:key]; + break; + default: + break; + } + if (completionBlock) { + completionBlock(); + } +} + +- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { + switch (cacheType) { + case SDImageCacheTypeNone: + break; + case SDImageCacheTypeMemory: + [self.memoryCache setObject:image forKey:key]; + break; + case SDImageCacheTypeDisk: + [self.diskCache setData:imageData forKey:key]; + break; + case SDImageCacheTypeAll: + [self.memoryCache setObject:image forKey:key]; + [self.diskCache setData:imageData forKey:key]; + break; + default: + break; + } + if (completionBlock) { + completionBlock(); + } +} + +@end From 067174b1fdceb0d14db0a645da581395c3f91855 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 2 Apr 2020 12:49:15 +0800 Subject: [PATCH 144/181] Added the test case about using the custom cache and loader with context option to manager, full pipeline testing --- Tests/Tests/SDImageCacheTests.m | 3 +++ Tests/Tests/SDWebImageManagerTests.m | 26 ++++++++++++++++++++++++++ Tests/Tests/SDWebImageTestCache.h | 2 ++ Tests/Tests/SDWebImageTestCache.m | 23 +++++++++++++++++++++-- Tests/Tests/SDWebImageTestLoader.h | 2 ++ Tests/Tests/SDWebImageTestLoader.m | 9 +++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Tests/Tests/SDImageCacheTests.m b/Tests/Tests/SDImageCacheTests.m index 0b195f89..96fb0f48 100644 --- a/Tests/Tests/SDImageCacheTests.m +++ b/Tests/Tests/SDImageCacheTests.m @@ -800,6 +800,8 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; expect(cache.memoryCache).notTo.beNil(); expect(cache.diskCache).notTo.beNil(); + // Clear + [cache clearWithCacheType:SDImageCacheTypeAll completion:nil]; // Store UIImage *image1 = self.testJPEGImage; NSString *key1 = @"testJPEGImage"; @@ -816,6 +818,7 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png"; }]; // Remove [cache removeImageForKey:key1 cacheType:SDImageCacheTypeAll completion:nil]; + // Contain [cache containsImageForKey:key1 cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType containsCacheType) { expect(containsCacheType).equal(SDImageCacheTypeNone); }]; diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index c75fc950..ba61529f 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -8,6 +8,8 @@ #import "SDTestCase.h" #import "SDWebImageTestTransformer.h" +#import "SDWebImageTestCache.h" +#import "SDWebImageTestLoader.h" @interface SDWebImageManagerTests : SDTestCase @@ -268,6 +270,30 @@ }]; } +- (void)test14ThatCustomCacheAndLoaderWorks { + XCTestExpectation *expectation = [self expectationWithDescription:@"Custom Cache and Loader during manger query"]; + NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/100x100.png"]; + SDWebImageContext *context = @{ + SDWebImageContextImageCache : SDWebImageTestCache.sharedCache, + SDWebImageContextImageLoader : SDWebImageTestLoader.sharedLoader + }; + [SDWebImageTestCache.sharedCache clearWithCacheType:SDImageCacheTypeAll completion:nil]; + [SDWebImageManager.sharedManager loadImageWithURL:url options:SDWebImageWaitStoreCache context:context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + expect(image).notTo.beNil(); + expect(image.size.width).equal(100); + expect(image.size.height).equal(100); + expect(data).notTo.beNil(); + NSString *cacheKey = [SDWebImageManager.sharedManager cacheKeyForURL:imageURL]; + // Check Disk Cache (SDWebImageWaitStoreCache behavior) + [SDWebImageTestCache.sharedCache containsImageForKey:cacheKey cacheType:SDImageCacheTypeDisk completion:^(SDImageCacheType containsCacheType) { + expect(containsCacheType).equal(SDImageCacheTypeDisk); + [expectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithCommonTimeout]; +} + - (NSString *)testJPEGPath { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; return [testBundle pathForResource:@"TestImage" ofType:@"jpg"]; diff --git a/Tests/Tests/SDWebImageTestCache.h b/Tests/Tests/SDWebImageTestCache.h index 8029ff4d..6c900c6f 100644 --- a/Tests/Tests/SDWebImageTestCache.h +++ b/Tests/Tests/SDWebImageTestCache.h @@ -36,4 +36,6 @@ - (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config; +@property (nonatomic, class, readonly, nonnull) SDWebImageTestCache *sharedCache; + @end diff --git a/Tests/Tests/SDWebImageTestCache.m b/Tests/Tests/SDWebImageTestCache.m index 143f2c80..d9e34e3d 100644 --- a/Tests/Tests/SDWebImageTestCache.m +++ b/Tests/Tests/SDWebImageTestCache.m @@ -49,7 +49,7 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac @implementation SDWebImageTestDiskCache - (nullable NSString *)cachePathForKey:(nonnull NSString *)key { - return [self.cachePath stringByAppendingPathComponent:key]; + return [self.cachePath stringByAppendingPathComponent:key.lastPathComponent]; } - (BOOL)containsDataForKey:(nonnull NSString *)key { @@ -72,7 +72,10 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac } - (void)removeAllData { - [self.fileManager removeItemAtPath:self.cachePath error:nil]; + for (NSString *path in [self.fileManager subpathsAtPath:self.cachePath]) { + NSString *filePath = [self.cachePath stringByAppendingPathComponent:path]; + [self.fileManager removeItemAtPath:filePath error:nil]; + } } - (void)removeDataForKey:(nonnull NSString *)key { @@ -125,6 +128,17 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac @implementation SDWebImageTestCache ++ (SDWebImageTestCache *)sharedCache { + static dispatch_once_t onceToken; + static SDWebImageTestCache *cache; + dispatch_once(&onceToken, ^{ + NSString *cachePath = [[self userCacheDirectory] stringByAppendingPathComponent:@"SDWebImageTestCache"]; + SDImageCacheConfig *config = SDImageCacheConfig.defaultCacheConfig; + cache = [[SDWebImageTestCache alloc] initWithCachePath:cachePath config:config]; + }); + return cache; +} + - (instancetype)initWithCachePath:(NSString *)cachePath config:(SDImageCacheConfig *)config { self = [super init]; if (self) { @@ -271,4 +285,9 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac } } ++ (nullable NSString *)userCacheDirectory { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + return paths.firstObject; +} + @end diff --git a/Tests/Tests/SDWebImageTestLoader.h b/Tests/Tests/SDWebImageTestLoader.h index d6a3f5f9..bd343cd8 100644 --- a/Tests/Tests/SDWebImageTestLoader.h +++ b/Tests/Tests/SDWebImageTestLoader.h @@ -13,4 +13,6 @@ // A really naive implementation of custom image loader using `NSURLSession` @interface SDWebImageTestLoader : NSObject +@property (nonatomic, class, readonly, nonnull) SDWebImageTestLoader *sharedLoader; + @end diff --git a/Tests/Tests/SDWebImageTestLoader.m b/Tests/Tests/SDWebImageTestLoader.m index 22978edb..14f6f7e5 100644 --- a/Tests/Tests/SDWebImageTestLoader.m +++ b/Tests/Tests/SDWebImageTestLoader.m @@ -16,6 +16,15 @@ @implementation SDWebImageTestLoader ++ (SDWebImageTestLoader *)sharedLoader { + static dispatch_once_t onceToken; + static SDWebImageTestLoader *loader; + dispatch_once(&onceToken, ^{ + loader = [[SDWebImageTestLoader alloc] init]; + }); + return loader; +} + - (BOOL)canRequestImageForURL:(NSURL *)url { return YES; } From ce4eced4d45d9985a8abc4d50d085138519e5f15 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 2 Apr 2020 16:15:10 +0800 Subject: [PATCH 145/181] Added the query cache type cases as well, update some documentation --- SDWebImage/Core/SDWebImageDefine.h | 2 +- Tests/Tests/SDWebImageManagerTests.m | 23 +++++++++++++++++++++++ Tests/Tests/SDWebImageTestCache.m | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 1e8afef8..6f02f279 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -260,7 +260,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; /** - A SDImageCacheType raw value which specify the source of cache to query. For example, you can query memory only, query disk only, or query from both memory and disk cache. + A SDImageCacheType raw value which specify the source of cache to query. Specify `SDImageCacheTypeDisk` to query from disk cache only; `SDImageCacheTypeMemory` to query from memory only. And `SDImageCacheTypeAll` to query from both memory cache and disk cache. Specify `SDImageCacheTypeNone` is invalid and totally ignore the cache query. If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextQueryCacheType; diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index ba61529f..305caf3c 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -294,6 +294,29 @@ [self waitForExpectationsWithCommonTimeout]; } +- (void)test15ThatQueryCacheTypeWork { + XCTestExpectation *expectation = [self expectationWithDescription:@"Image query cache type works"]; + NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/101x101.png"]; + NSString *key = [SDWebImageManager.sharedManager cacheKeyForURL:url]; + NSData *testImageData = [NSData dataWithContentsOfFile:[self testJPEGPath]]; + [SDImageCache.sharedImageCache storeImageDataToDisk:testImageData forKey:key]; + + // Query memory first + [SDWebImageManager.sharedManager loadImageWithURL:url options:SDWebImageFromCacheOnly context:@{SDWebImageContextQueryCacheType : @(SDImageCacheTypeMemory)} progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + expect(image).beNil(); + expect(cacheType).equal(SDImageCacheTypeNone); + // Query disk secondly + [SDWebImageManager.sharedManager loadImageWithURL:url options:SDWebImageFromCacheOnly context:@{SDWebImageContextQueryCacheType : @(SDImageCacheTypeDisk)} progress:nil completed:^(UIImage * _Nullable image2, NSData * _Nullable data2, NSError * _Nullable error2, SDImageCacheType cacheType2, BOOL finished2, NSURL * _Nullable imageURL2) { + expect(image2).notTo.beNil(); + expect(cacheType2).equal(SDImageCacheTypeDisk); + [SDImageCache.sharedImageCache removeImageFromDiskForKey:key]; + [expectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithCommonTimeout]; +} + - (NSString *)testJPEGPath { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; return [testBundle pathForResource:@"TestImage" ofType:@"jpg"]; diff --git a/Tests/Tests/SDWebImageTestCache.m b/Tests/Tests/SDWebImageTestCache.m index d9e34e3d..762a2d9a 100644 --- a/Tests/Tests/SDWebImageTestCache.m +++ b/Tests/Tests/SDWebImageTestCache.m @@ -188,6 +188,7 @@ static NSString * const SDWebImageTestDiskCacheExtendedAttributeName = @"com.hac } else if ([self.diskCache containsDataForKey:key]) { containsCacheType = SDImageCacheTypeDisk; } + break; default: break; } From 1dc70b84302e77f62f4a6cfa4d7f3ba0ba816cf0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 31 Mar 2020 18:47:51 +0800 Subject: [PATCH 146/181] Refactory the current thumbnail && transformer about cache key. Developer should have the API to calcualte the cache key from thumbnail or transformer, not hard-coded. --- SDWebImage/Core/SDImageCache.h | 6 +++--- SDWebImage/Core/SDImageCache.m | 8 -------- SDWebImage/Core/SDImageCoder.h | 2 +- SDWebImage/Core/SDImageTransformer.h | 13 +++++++++++-- SDWebImage/Core/SDImageTransformer.m | 5 +++++ SDWebImage/Core/SDWebImageManager.h | 6 ++++++ SDWebImage/Core/SDWebImageManager.m | 18 ++++++++++++------ 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index ec90efb0..57aedcb2 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -247,7 +247,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Operation that queries the cache asynchronously and call the completion when done. * - * @param key The unique key used to store the wanted image + * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a NSOperation instance containing the cache op @@ -257,7 +257,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Asynchronously queries the cache with operation and call the completion when done. * - * @param key The unique key used to store the wanted image + * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param doneBlock The completion block. Will not get called if the operation is cancelled * @@ -268,7 +268,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Asynchronously queries the cache with operation and call the completion when done. * - * @param key The unique key used to store the wanted image + * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param doneBlock The completion block. Will not get called if the operation is cancelled diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index af201386..197b75c2 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -9,7 +9,6 @@ #import "SDImageCache.h" #import "NSImage+Compatibility.h" #import "SDImageCodersManager.h" -#import "SDImageTransformer.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" @@ -455,13 +454,6 @@ return nil; } - id transformer = context[SDWebImageContextImageTransformer]; - if (transformer) { - // grab the transformed disk image if transformer provided - NSString *transformerKey = [transformer transformerKey]; - key = SDTransformedKeyForKey(key, transformerKey); - } - // First check the in-memory cache... UIImage *image; if (queryCacheType != SDImageCacheTypeDisk) { diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 221246ac..038b41f3 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -61,7 +61,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressio But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data inforamtion only. See `SDWebImageContext` for more detailed information. */ -FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext; +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("The coder component will be seperated from Core subspec in the future. Update your code to not rely on this context option.", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));; #pragma mark - Coder /** diff --git a/SDWebImage/Core/SDImageTransformer.h b/SDWebImage/Core/SDImageTransformer.h index f165cce5..a2a0850c 100644 --- a/SDWebImage/Core/SDImageTransformer.h +++ b/SDWebImage/Core/SDImageTransformer.h @@ -18,6 +18,15 @@ */ FOUNDATION_EXPORT NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullable key, NSString * _Nonnull transformerKey); +/** + Return the thumbnailed cache key which applied with specify thumbnailSize and preserveAspectRatio control. + @param key The original cache key + @param thumbnailPixelSize The thumbnail pixel size + @param preserveAspectRatio The preserve aspect ratio option + @return The thumbnailed cache key + */ +FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thumbnailPixelSize, BOOL preserveAspectRatio); + /** A transformer protocol to transform the image load from cache or from download. You can provide transformer to cache and manager (Through the `transformer` property or context option `SDWebImageContextImageTransformer`). @@ -38,10 +47,10 @@ FOUNDATION_EXPORT NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullab Transform the image to another image. @param image The image to be transformed - @param key The cache key associated to the image + @param key The cache key associated to the image. This arg is a hint for image source, not always useful and should be nullable. In the future we will remove this arg. @return The transformed image, or nil if transform failed */ -- (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key; +- (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key API_DEPRECATED("The key arg will be removed in the future. Update your code and don't rely on that.", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @end diff --git a/SDWebImage/Core/SDImageTransformer.m b/SDWebImage/Core/SDImageTransformer.m index 26ee45cd..9397ec73 100644 --- a/SDWebImage/Core/SDImageTransformer.m +++ b/SDWebImage/Core/SDImageTransformer.m @@ -38,6 +38,11 @@ NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullable key, NSString * } } +NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thumbnailPixelSize, BOOL preserveAspectRatio) { + NSString *thumbnailKey = [NSString stringWithFormat:@"Thumbnail({%f,%f},%d)", thumbnailPixelSize.width, thumbnailPixelSize.height, preserveAspectRatio]; + return SDTransformedKeyForKey(key, thumbnailKey); +} + @interface SDImagePipelineTransformer () @property (nonatomic, copy, readwrite, nonnull) NSArray> *transformers; diff --git a/SDWebImage/Core/SDWebImageManager.h b/SDWebImage/Core/SDWebImageManager.h index d940f742..4c7cd558 100644 --- a/SDWebImage/Core/SDWebImageManager.h +++ b/SDWebImage/Core/SDWebImageManager.h @@ -266,4 +266,10 @@ SDWebImageManager *manager = [SDWebImageManager sharedManager]; */ - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url; +/** + * Return the cache key for a given URL and context option. + * Some option like `.thumbnailPixelSize` and `imageTransformer` will effect the generated cache key, using this if you have those context associated. +*/ +- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context; + @end diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index a58831aa..87d5c085 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -114,6 +114,7 @@ static id _defaultImageLoader; } else { key = url.absoluteString; } + // Thumbnail Key Appending NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; if (thumbnailSizeValue != nil) { @@ -123,14 +124,21 @@ static id _defaultImageLoader; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif - BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } - NSString *transformerKey = [NSString stringWithFormat:@"Thumbnail({%f,%f},%d)", thumbnailSize.width, thumbnailSize.height, preserveAspectRatio]; - key = SDTransformedKeyForKey(key, transformerKey); + key = SDThumbnailedKeyForKey(key, thumbnailSize, preserveAspectRatio); + } + + // Transformer Key Appending + id transformer = self.transformer; + if (context[SDWebImageContextImageTransformer]) { + transformer = context[SDWebImageContextImageTransformer]; + } + if (transformer) { + key = SDTransformedKeyForKey(key, transformer.transformerKey); } return key; @@ -409,8 +417,6 @@ static id _defaultImageLoader; @autoreleasepool { UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key]; if (transformedImage && finished) { - NSString *transformerKey = [transformer transformerKey]; - NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey); BOOL imageWasTransformed = ![transformedImage isEqual:originalImage]; NSData *cacheData; // pass nil if the image was transformed, so we can recalculate the data from the image @@ -421,7 +427,7 @@ static id _defaultImageLoader; } // keep the original image format and extended data SDImageCopyAssociatedObject(originalImage, transformedImage); - [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{ + [self storeImage:transformedImage imageData:cacheData forKey:key cacheType:storeCacheType options:options context:context completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; }]; } else { From 543b5c95d42d6b69d2dbbcbd775e9f2c4411856f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 12:04:56 +0800 Subject: [PATCH 147/181] Fix the test case about the original cache key calculation rule --- SDWebImage/Core/SDImageCache.h | 6 +++--- SDWebImage/Core/SDImageTransformer.h | 1 + SDWebImage/Core/SDWebImageManager.h | 5 +++-- SDWebImage/Core/SDWebImageManager.m | 23 +++++++++++++++++++++-- Tests/Tests/SDWebImageManagerTests.m | 2 +- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 57aedcb2..1ebeef12 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -247,7 +247,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Operation that queries the cache asynchronously and call the completion when done. * - * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. + * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a NSOperation instance containing the cache op @@ -257,7 +257,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Asynchronously queries the cache with operation and call the completion when done. * - * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. + * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param doneBlock The completion block. Will not get called if the operation is cancelled * @@ -268,7 +268,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * Asynchronously queries the cache with operation and call the completion when done. * - * @param key The unique key used to store the wanted image. If you need transformer's image, calculate the key with `SDTransformedKeyForKey` or generate the cache key from url with `cacheKeyForURL:context:`. + * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param doneBlock The completion block. Will not get called if the operation is cancelled diff --git a/SDWebImage/Core/SDImageTransformer.h b/SDWebImage/Core/SDImageTransformer.h index a2a0850c..5b6d535a 100644 --- a/SDWebImage/Core/SDImageTransformer.h +++ b/SDWebImage/Core/SDImageTransformer.h @@ -24,6 +24,7 @@ FOUNDATION_EXPORT NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullab @param thumbnailPixelSize The thumbnail pixel size @param preserveAspectRatio The preserve aspect ratio option @return The thumbnailed cache key + @note If you have both transformer and thumbnail applied for image, call `SDThumbnailedKeyForKey` firstly and then with `SDTransformedKeyForKey`.` */ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thumbnailPixelSize, BOOL preserveAspectRatio); diff --git a/SDWebImage/Core/SDWebImageManager.h b/SDWebImage/Core/SDWebImageManager.h index 4c7cd558..c7e52ca9 100644 --- a/SDWebImage/Core/SDWebImageManager.h +++ b/SDWebImage/Core/SDWebImageManager.h @@ -262,13 +262,14 @@ SDWebImageManager *manager = [SDWebImageManager sharedManager]; - (void)cancelAll; /** - * Return the cache key for a given URL + * Return the cache key for a given URL, does not considerate transformer or thumbnail. + * @note This method does not have context option, only use the url and manager level cacheKeyFilter to generate the cache key. */ - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url; /** * Return the cache key for a given URL and context option. - * Some option like `.thumbnailPixelSize` and `imageTransformer` will effect the generated cache key, using this if you have those context associated. + * @note The context option like `.thumbnailPixelSize` and `.imageTransformer` will effect the generated cache key, using this if you have those context associated. */ - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 87d5c085..f02f23f1 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -95,7 +95,23 @@ static id _defaultImageLoader; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { - return [self cacheKeyForURL:url context:nil]; + return [self cacheKeyForURL:url cacheKeyFilter:self.cacheKeyFilter]; +} + +- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(nullable id)cacheKeyFilter { + if (!url) { + return @""; + } + + NSString *key; + // Cache Key Filter + if (cacheKeyFilter) { + key = [cacheKeyFilter cacheKeyForURL:url]; + } else { + key = url.absoluteString; + } + + return key; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context { @@ -355,7 +371,9 @@ static id _defaultImageLoader; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } - NSString *key = [self cacheKeyForURL:url context:context]; + // origin cache key + id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter] ?: self.cacheKeyFilter; + NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; @@ -405,6 +423,7 @@ static id _defaultImageLoader; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } + // transformed cache key NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index 305caf3c..fbed31ed 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -229,7 +229,7 @@ SDWebImageContext *context = @{SDWebImageContextOriginalStoreCacheType : @(SDImageCacheTypeDisk), SDWebImageContextStoreCacheType : @(SDImageCacheTypeMemory)}; NSURL *url = [NSURL URLWithString:kTestJPEGURL]; NSString *originalKey = [manager cacheKeyForURL:url]; - NSString *transformedKey = SDTransformedKeyForKey(originalKey, transformer.transformerKey); + NSString *transformedKey = [manager cacheKeyForURL:url context:context]; [manager loadImageWithURL:url options:SDWebImageTransformAnimatedImage context:context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { expect(image).equal(transformer.testImage); From dfc8fe27d94b80e4dc711f50f499d48e2abd0d9d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 12:47:07 +0800 Subject: [PATCH 148/181] Refactory to simplify the code to calculate the original cache key, pass null to disable this. --- SDWebImage/Core/SDWebImageDefine.h | 2 +- SDWebImage/Core/SDWebImageManager.m | 21 +++++++++++++++------ Tests/Tests/SDWebImageManagerTests.m | 1 + 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 6f02f279..96f71d68 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -237,7 +237,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageL FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageCoder; /** - A id instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. (id) + A id instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. If you pass NSNull, the transformer feature will be disabled. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTransformer; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index f02f23f1..22c0e06c 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -95,16 +95,13 @@ static id _defaultImageLoader; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { - return [self cacheKeyForURL:url cacheKeyFilter:self.cacheKeyFilter]; -} - -- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(nullable id)cacheKeyFilter { if (!url) { return @""; } NSString *key; // Cache Key Filter + id cacheKeyFilter = self.cacheKeyFilter; if (cacheKeyFilter) { key = [cacheKeyFilter cacheKeyForURL:url]; } else { @@ -152,6 +149,9 @@ static id _defaultImageLoader; id transformer = self.transformer; if (context[SDWebImageContextImageTransformer]) { transformer = context[SDWebImageContextImageTransformer]; + if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) { + transformer = nil; + } } if (transformer) { key = SDTransformedKeyForKey(key, transformer.transformerKey); @@ -372,9 +372,14 @@ static id _defaultImageLoader; originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } // origin cache key - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter] ?: self.cacheKeyFilter; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + SDWebImageMutableContext *originContext = [context mutableCopy]; + // disable transformer for cache key generation + originContext[SDWebImageContextImageTransformer] = [NSNull null]; + NSString *key = [self cacheKeyForURL:url context:originContext]; id transformer = context[SDWebImageContextImageTransformer]; + if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) { + transformer = nil; + } id cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = downloadedImage && transformer; @@ -426,7 +431,11 @@ static id _defaultImageLoader; // transformed cache key NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; + if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) { + transformer = nil; + } id cacheSerializer = context[SDWebImageContextCacheSerializer]; + BOOL shouldTransformImage = originalImage && transformer; shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage)); diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index fbed31ed..327c16e7 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -220,6 +220,7 @@ // Use a fresh manager && cache to avoid get effected by other test cases SDImageCache *cache = [[SDImageCache alloc] initWithNamespace:@"SDWebImageStoreCacheType"]; + [cache clearDiskOnCompletion:nil]; SDWebImageManager *manager = [[SDWebImageManager alloc] initWithCache:cache loader:SDWebImageDownloader.sharedDownloader]; SDWebImageTestTransformer *transformer = [[SDWebImageTestTransformer alloc] init]; transformer.testImage = [[UIImage alloc] initWithContentsOfFile:[self testJPEGPath]]; From 8c2141ecda447cbd34e74cab680a8aff41f833da Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 16:30:21 +0800 Subject: [PATCH 149/181] Added the API to query disk image with options and context, this is needed if you have animated image/transformer/thumbnail usage --- SDWebImage/Core/SDImageCache.h | 29 ++++++++++++++++++++++++----- SDWebImage/Core/SDImageCache.m | 13 +++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 1ebeef12..f2735a36 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -227,8 +227,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { #pragma mark - Query and Retrieve Ops /** - * Asynchronously queries the cache with operation and call the completion when done. - * Query the image data for the given key synchronously. + * Synchronously query the image data for the given key in disk cache. You can decode the image data to image after loaded. * * @param key The unique key used to store the wanted image * @return The image data for the given key, or nil if not found. @@ -236,16 +235,16 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key; /** - * Asynchronously load the image data in disk cache. You can decode the image data to image after loaded. + * Asynchronously query the image data for the given key in disk cache. You can decode the image data to image after loaded. * * @param key The unique key used to store the wanted image - * @param completionBlock the block to be executed when the check is done. + * @param completionBlock the block to be executed when the query is done. * @note the completion block will be always executed on the main queue */ - (void)diskImageDataQueryForKey:(nullable NSString *)key completion:(nullable SDImageCacheQueryDataCompletionBlock)completionBlock; /** - * Operation that queries the cache asynchronously and call the completion when done. + * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param doneBlock The completion block. Will not get called if the operation is cancelled @@ -306,6 +305,16 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { */ - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key; +/** + * Synchronously query the disk cache. With the options and context which may effect the image generation. (Such as transformer, animated image, thumbnail, etc) + * + * @param key The unique key used to store the image + * @param options A mask to specify options to use for this cache query + * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. + * @return The image for the given key, or nil if not found. + */ +- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context; + /** * Synchronously query the cache (memory and or disk) after checking the memory cache. * @@ -314,6 +323,16 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { */ - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; +/** + * Synchronously query the cache (memory and or disk) after checking the memory cache. With the options and context which may effect the image generation. (Such as transformer, animated image, thumbnail, etc) + * + * @param key The unique key used to store the image + * @param options A mask to specify options to use for this cache query + * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. + * @return The image for the given key, or nil if not found. + */ +- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context;; + #pragma mark - Remove Ops /** diff --git a/SDWebImage/Core/SDImageCache.m b/SDWebImage/Core/SDImageCache.m index 197b75c2..076fa43c 100644 --- a/SDWebImage/Core/SDImageCache.m +++ b/SDWebImage/Core/SDImageCache.m @@ -341,7 +341,12 @@ } - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key { - UIImage *diskImage = [self diskImageForKey:key]; + return [self imageFromDiskCacheForKey:key options:0 context:nil]; +} + +- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context { + NSData *data = [self diskImageDataForKey:key]; + UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = diskImage.sd_memoryCost; [self.memoryCache setObject:diskImage forKey:key cost:cost]; @@ -351,6 +356,10 @@ } - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { + return [self imageFromCacheForKey:key options:0 context:nil]; +} + +- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context { // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { @@ -358,7 +367,7 @@ } // Second check the disk cache... - image = [self imageFromDiskCacheForKey:key]; + image = [self imageFromDiskCacheForKey:key options:options context:context]; return image; } From d4782871e08aaeae94d9febde0dd773503393bf3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Apr 2020 20:05:28 +0800 Subject: [PATCH 150/181] Fix the issue for Carthage/SwiftPM framework version symbols, this should match the framework name SDWebImage, or will get a link error when used --- WebImage/SDWebImage.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/WebImage/SDWebImage.h b/WebImage/SDWebImage.h index f219978e..eeadf43f 100644 --- a/WebImage/SDWebImage.h +++ b/WebImage/SDWebImage.h @@ -9,15 +9,11 @@ #import -#if SD_UIKIT -#import -#endif +//! Project version number for SDWebImage. +FOUNDATION_EXPORT double SDWebImageVersionNumber; -//! Project version number for WebImage. -FOUNDATION_EXPORT double WebImageVersionNumber; - -//! Project version string for WebImage. -FOUNDATION_EXPORT const unsigned char WebImageVersionString[]; +//! Project version string for SDWebImage. +FOUNDATION_EXPORT const unsigned char SDWebImageVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import From 2ca731c2e8ba2e4f2237aaeb1890ca7e6a8ca766 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Apr 2020 22:52:43 +0800 Subject: [PATCH 151/181] Support to provide the background color when you encode a alpha UIImage into non-alpha format like JPEG --- SDWebImage/Core/SDImageCoder.h | 6 ++++++ SDWebImage/Core/SDImageCoder.m | 1 + SDWebImage/Core/SDImageIOAnimatedCoder.m | 5 +++++ SDWebImage/Core/SDImageIOCoder.m | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 038b41f3..3e0f3b72 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -55,6 +55,12 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeFirstFrame */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality; +/** + A UIColor(NSColor) value to used for non-alpha image encoding when the input image has alpha channel, the background color will be used to compose the alpha one. If not provide, use white color. + @note works for `SDImageEncoder` + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackgroundColor; + /** A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext) This option is ignored for all built-in coders and take no effect. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index df5224ae..14893ff2 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -15,5 +15,6 @@ SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnai SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; +SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor"; SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext"; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 1db7a495..f5d518eb 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -426,11 +426,16 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati return nil; } NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + // Encoding Options double compressionQuality = 1; if (options[SDImageCoderEncodeCompressionQuality]) { compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue]; } properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality); + CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor]; + if (backgroundColor) { + properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); + } BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue]; if (encodeFirstFrame || frames.count == 0) { diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index f617f437..5ceefc4b 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -248,11 +248,16 @@ CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; #endif properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation); + // Encoding Options double compressionQuality = 1; if (options[SDImageCoderEncodeCompressionQuality]) { compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue]; } properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality); + CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor]; + if (backgroundColor) { + properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); + } // Add your image to the destination. CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties); From e71bbf239c9e827a70a6ee06fa55dd729062a8b9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Apr 2020 23:28:08 +0800 Subject: [PATCH 152/181] Supports the encoding max pixel size options as well, which let the codec to do thumbnail rescale encoding, better performance than transformer to scale and then encode --- SDWebImage/Core/SDImageCoder.h | 9 ++++++- SDWebImage/Core/SDImageCoder.m | 1 + SDWebImage/Core/SDImageIOAnimatedCoder.m | 29 ++++++++++++++++++++- SDWebImage/Core/SDImageIOCoder.m | 32 ++++++++++++++++++++++-- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 3e0f3b72..5c03b2d4 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -57,10 +57,17 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressio /** A UIColor(NSColor) value to used for non-alpha image encoding when the input image has alpha channel, the background color will be used to compose the alpha one. If not provide, use white color. - @note works for `SDImageEncoder` + @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackgroundColor; +/** + A CGSize value indicating the max image resolution in pixels during encoding. For vector image, this also effect the output vector data information about width and height. The encoder will not generate the encoded image larger than this limit. Note it always use the aspect ratio of input image. + Defaults to CGSizeZero, which means no max size limit at all. + @note works for `SDImageCoder` + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSize; + /** A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext) This option is ignored for all built-in coders and take no effect. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index 14893ff2..58176547 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -16,5 +16,6 @@ SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnai SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor"; +SDImageCoderOption const SDImageCoderEncodeMaxPixelSize = @"encodeMaxPixelSize"; SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext"; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index f5d518eb..253034ac 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -409,6 +409,11 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati if (!image) { return nil; } + CGImageRef imageRef = image.CGImage; + if (!imageRef) { + // Earily return, supports CGImage only + return nil; + } if (format != self.class.imageFormat) { return nil; @@ -436,11 +441,33 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati if (backgroundColor) { properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); } + CGSize maxPixelSize = CGSizeZero; + NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize]; + if (maxPixelSizeValue != nil) { +#if SD_MAC + maxPixelSize = maxPixelSizeValue.sizeValue; +#else + maxPixelSize = maxPixelSizeValue.CGSizeValue; +#endif + } + NSUInteger pixelWidth = CGImageGetWidth(imageRef); + NSUInteger pixelHeight = CGImageGetHeight(imageRef); + if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) { + CGFloat pixelRatio = pixelWidth / pixelHeight; + CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height; + CGFloat finalPixelSize; + if (pixelRatio > maxPixelSizeRatio) { + finalPixelSize = maxPixelSize.width; + } else { + finalPixelSize = maxPixelSize.height; + } + properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); + } BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue]; if (encodeFirstFrame || frames.count == 0) { // for static single images - CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties); + CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); } else { // for animated images NSUInteger loopCount = image.sd_imageLoopCount; diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 5ceefc4b..981d3593 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -221,9 +221,14 @@ if (!image) { return nil; } + CGImageRef imageRef = image.CGImage; + if (!imageRef) { + // Earily return, supports CGImage only + return nil; + } if (format == SDImageFormatUndefined) { - BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage]; + BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef]; if (hasAlpha) { format = SDImageFormatPNG; } else { @@ -258,9 +263,32 @@ if (backgroundColor) { properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); } + CGSize maxPixelSize = CGSizeZero; + NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize]; + if (maxPixelSizeValue != nil) { +#if SD_MAC + maxPixelSize = maxPixelSizeValue.sizeValue; +#else + maxPixelSize = maxPixelSizeValue.CGSizeValue; +#endif + } + NSUInteger pixelWidth = CGImageGetWidth(imageRef); + NSUInteger pixelHeight = CGImageGetHeight(imageRef); + if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) { + CGFloat pixelRatio = pixelWidth / pixelHeight; + CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height; + CGFloat finalPixelSize; + if (pixelRatio > maxPixelSizeRatio) { + finalPixelSize = maxPixelSize.width; + } else { + finalPixelSize = maxPixelSize.height; + } + properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); + } + // Add your image to the destination. - CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties); + CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); // Finalize the destination. if (CGImageDestinationFinalize(imageDestination) == NO) { From f798b89fc27f55bcecd1554889f0a9ed81210597 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 3 Apr 2020 23:59:17 +0800 Subject: [PATCH 153/181] Fix the maxPixelSize for animated images, update the readme --- SDWebImage/Core/SDImageCoder.h | 5 ++++- SDWebImage/Core/SDImageIOAnimatedCoder.m | 12 +++++++++--- Tests/Tests/SDImageCoderTests.m | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 5c03b2d4..f1e9c340 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -37,6 +37,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAs /** A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.preserveAspectRatio`) the value size. Defaults to CGSizeZero, which means no thumbnail generation at all. + @note Supports for animated image as well. @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ @@ -62,8 +63,10 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressio FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackgroundColor; /** - A CGSize value indicating the max image resolution in pixels during encoding. For vector image, this also effect the output vector data information about width and height. The encoder will not generate the encoded image larger than this limit. Note it always use the aspect ratio of input image. + A CGSize value indicating the max image resolution in pixels during encoding. For vector image, this also effect the output vector data information about width and height. The encoder will not generate the encoded image larger than this limit. Note it always use the aspect ratio of input image.. Defaults to CGSizeZero, which means no max size limit at all. + @note Supports for animated image as well. + @note The ouput image's width is limited to pixel size's width, the output image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSize; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 253034ac..9050cd6e 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -452,21 +452,23 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati } NSUInteger pixelWidth = CGImageGetWidth(imageRef); NSUInteger pixelHeight = CGImageGetHeight(imageRef); + CGFloat finalPixelSize = 0; if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) { CGFloat pixelRatio = pixelWidth / pixelHeight; CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height; - CGFloat finalPixelSize; if (pixelRatio > maxPixelSizeRatio) { finalPixelSize = maxPixelSize.width; } else { finalPixelSize = maxPixelSize.height; } - properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); } BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue]; if (encodeFirstFrame || frames.count == 0) { // for static single images + if (finalPixelSize > 0) { + properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); + } CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); } else { // for animated images @@ -479,7 +481,11 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati SDImageFrame *frame = frames[i]; NSTimeInterval frameDuration = frame.duration; CGImageRef frameImageRef = frame.image.CGImage; - NSDictionary *frameProperties = @{self.class.dictionaryProperty : @{self.class.delayTimeProperty : @(frameDuration)}}; + NSMutableDictionary *frameProperties = [NSMutableDictionary dictionary]; + frameProperties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)}; + if (finalPixelSize > 0) { + frameProperties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); + } CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); } } diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 95b8b5a2..dbdfc6c0 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -271,6 +271,25 @@ withLocalImageURL:(NSURL *)imageUrl #if SD_UIKIT expect(outputImage.images.count).to.equal(inputImage.images.count); #endif + + // check max pixel size encoding with scratch + CGFloat maxWidth = 50; + CGFloat maxHeight = 50; + CGFloat maxRatio = maxWidth / maxHeight; + CGSize maxPixelSize; + if (ratio > maxRatio) { + maxPixelSize = CGSizeMake(maxWidth, round(maxWidth / ratio)); + } else { + maxPixelSize = CGSizeMake(round(maxHeight * ratio), maxHeight); + } + NSData *outputMaxImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(maxWidth, maxHeight))}]; + UIImage *outputMaxImage = [coder decodedImageWithData:outputMaxImageData options:nil]; + // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1 + expect(ABS(outputMaxImage.size.width - maxPixelSize.width) <= 1); + expect(ABS(outputMaxImage.size.height - maxPixelSize.height) <= 1); +#if SD_UIKIT + expect(outputMaxImage.images.count).to.equal(inputImage.images.count); +#endif } } From db610363f4142848757312288afd46df0610e906 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 13:07:20 +0800 Subject: [PATCH 154/181] Added the test case to ensure the background color encoding options on JPEG works --- Tests/Tests/SDImageCoderTests.m | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index dbdfc6c0..25ab0632 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -8,6 +8,7 @@ */ #import "SDTestCase.h" +#import "UIColor+HexString.h" @interface SDWebImageDecoderTests : SDTestCase @@ -82,6 +83,21 @@ expect(decodedImage.size.height).to.equal(image.size.height); } +- (void)test08ThatEncodeAlphaImageToJPGWithBackgroundColor { + NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"]; + UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; + UIColor *backgroundColor = [UIColor blackColor]; + NSData *encodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeBackgroundColor : backgroundColor}]; + expect(encodedData).notTo.beNil(); + UIImage *decodedImage = [SDImageCodersManager.sharedManager decodedImageWithData:encodedData options:nil]; + expect(decodedImage).notTo.beNil(); + expect(decodedImage.size.width).to.equal(image.size.width); + expect(decodedImage.size.height).to.equal(image.size.height); + // Check background color, should not be white but the black color + UIColor *testColor = [decodedImage sd_colorAtPoint:CGPointMake(1, 1)]; + expect(testColor.sd_hexString).equal(backgroundColor.sd_hexString); +} + - (void)test11ThatAPNGPCoderWorks { NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"]; [self verifyCoder:[SDImageAPNGCoder sharedCoder] From 1ce44a12b0558dc1df5111fc1835a1be1c9d6759 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 14:19:08 +0800 Subject: [PATCH 155/181] Change the behavior to return the abstract for unknown UTI type, this can solve the accident issue for custom coder who provide a new format --- SDWebImage/Core/NSData+ImageContentType.h | 2 ++ SDWebImage/Core/NSData+ImageContentType.m | 4 ++-- Tests/Tests/SDCategoriesTests.m | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Core/NSData+ImageContentType.h b/SDWebImage/Core/NSData+ImageContentType.h index 8c2f97e8..de2a6bf3 100644 --- a/SDWebImage/Core/NSData+ImageContentType.h +++ b/SDWebImage/Core/NSData+ImageContentType.h @@ -45,6 +45,7 @@ static const SDImageFormat SDImageFormatSVG = 8; * * @param format Format as SDImageFormat * @return The UTType as CFStringRef + * @note For unknown format, `kUTTypeImage` abstract type will return */ + (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format CF_RETURNS_NOT_RETAINED NS_SWIFT_NAME(sd_UTType(from:)); @@ -53,6 +54,7 @@ static const SDImageFormat SDImageFormatSVG = 8; * * @param uttype The UTType as CFStringRef * @return The Format as SDImageFormat + * @note For unknown type, `SDImageFormatUndefined` will return */ + (SDImageFormat)sd_imageFormatFromUTType:(nonnull CFStringRef)uttype; diff --git a/SDWebImage/Core/NSData+ImageContentType.m b/SDWebImage/Core/NSData+ImageContentType.m index f9014480..87d041ed 100644 --- a/SDWebImage/Core/NSData+ImageContentType.m +++ b/SDWebImage/Core/NSData+ImageContentType.m @@ -119,8 +119,8 @@ UTType = kUTTypeScalableVectorGraphics; break; default: - // default is kUTTypePNG - UTType = kUTTypePNG; + // default is kUTTypeImage abstract type + UTType = kUTTypeImage; break; } return UTType; diff --git a/Tests/Tests/SDCategoriesTests.m b/Tests/Tests/SDCategoriesTests.m index f38fe1bc..ee5aaf56 100644 --- a/Tests/Tests/SDCategoriesTests.m +++ b/Tests/Tests/SDCategoriesTests.m @@ -25,7 +25,7 @@ // Test invalid format CFStringRef type = [NSData sd_UTTypeFromImageFormat:SDImageFormatUndefined]; - expect(CFStringCompare(kUTTypePNG, type, 0)).equal(kCFCompareEqualTo); + expect(CFStringCompare(kUTTypeImage, type, 0)).equal(kCFCompareEqualTo); expect([NSData sd_imageFormatFromUTType:kUTTypeImage]).equal(SDImageFormatUndefined); } From b427ad5f3f036cbf46d915bd14abc53c244d5cf3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 15:31:34 +0800 Subject: [PATCH 156/181] Added the support for max file size for lossy encoding --- SDWebImage/Core/SDImageCoder.h | 7 +++++++ SDWebImage/Core/SDImageCoder.m | 1 + SDWebImage/Core/SDImageIOAnimatedCoder.m | 6 ++++++ SDWebImage/Core/SDImageIOCoder.m | 8 +++++++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index f1e9c340..1676ef94 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -71,6 +71,13 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackground */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSize; +/** + A NSUInteger value specify the max ouput data bytes size after encoding. Some lossy format like JPEG/HEIF supports the hint for codec to automatically reduce the quality and match the file size you want. Note this option will override the `SDImageCoderEncodeCompressionQuality`, because now the quality is decided by the encoder. (NSNumber) + @note Not all format supports this feature. And this options does not works for vector images. + @note works for `SDImageCoder` + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxFileSize; + /** A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext) This option is ignored for all built-in coders and take no effect. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index 58176547..37dfdc41 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -17,5 +17,6 @@ SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOn SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor"; SDImageCoderOption const SDImageCoderEncodeMaxPixelSize = @"encodeMaxPixelSize"; +SDImageCoderOption const SDImageCoderEncodeMaxFileSize = @"encodeMaxFileSize"; SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext"; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 9050cd6e..25103513 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -16,6 +16,8 @@ // Specify DPI for vector format in CGImageSource, like PDF static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI"; +// Specify File Size for lossy format encoding, like JPEG +static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; @interface SDImageIOCoderFrame : NSObject @@ -462,6 +464,10 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati finalPixelSize = maxPixelSize.height; } } + NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; + if (maxFileSize > 0) { + properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); + } BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue]; if (encodeFirstFrame || frames.count == 0) { diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 981d3593..70781ba7 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -14,6 +14,9 @@ #import "SDImageHEICCoderInternal.h" #import "SDImageIOAnimatedCoderInternal.h" +// Specify File Size for lossy format encoding, like JPEG +static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; + @implementation SDImageIOCoder { size_t _width, _height; CGImagePropertyOrientation _orientation; @@ -285,7 +288,10 @@ } properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); } - + NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; + if (maxFileSize > 0) { + properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); + } // Add your image to the destination. CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); From 6316f08bb863c9e5ab97d552b3fc22167f4587e9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 15:52:26 +0800 Subject: [PATCH 157/181] Remove the compression quality when have max file size limit, and update the test cases for JPEG --- SDWebImage/Core/SDImageCoder.h | 2 +- SDWebImage/Core/SDImageIOAnimatedCoder.m | 2 ++ SDWebImage/Core/SDImageIOCoder.m | 2 ++ Tests/Tests/SDImageCoderTests.m | 20 ++++++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 1676ef94..590fbba0 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -73,7 +73,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSi /** A NSUInteger value specify the max ouput data bytes size after encoding. Some lossy format like JPEG/HEIF supports the hint for codec to automatically reduce the quality and match the file size you want. Note this option will override the `SDImageCoderEncodeCompressionQuality`, because now the quality is decided by the encoder. (NSNumber) - @note Not all format supports this feature. And this options does not works for vector images. + @note This is a hint, no gurantee for output size because of compression algorithm limit. And this options does not works for vector images. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxFileSize; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 25103513..fb3dea6e 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -467,6 +467,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; if (maxFileSize > 0) { properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); + // Remove the quality if we have file size limit + properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil; } BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue]; diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 70781ba7..482aea8d 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -291,6 +291,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; if (maxFileSize > 0) { properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); + // Remove the quality if we have file size limit + properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil; } // Add your image to the destination. diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 25ab0632..3c7b5793 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -98,6 +98,26 @@ expect(testColor.sd_hexString).equal(backgroundColor.sd_hexString); } +- (void)test09ThatJPGImageEncodeWithMaxFileSize { + NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"]; + UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; + // This large JPEG encoding size between (770KB ~ 2.23MB) + NSUInteger limitFileSize = 1 * 1024 * 1024; // 1MB + // 100 quality (biggest) + NSData *maxEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:nil]; + expect(maxEncodedData).notTo.beNil(); + expect(maxEncodedData.length).beGreaterThan(limitFileSize); + // 0 quality (smallest) + NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0)}]; + expect(minEncodedData).notTo.beNil(); + expect(minEncodedData.length).beLessThan(limitFileSize); + NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}]; + expect(limitEncodedData).notTo.beNil(); + // So, if we limit the file size, the output data should in (770KB ~ 2.23MB) + expect(limitEncodedData.length).beLessThan(maxEncodedData.length); + expect(limitEncodedData.length).beGreaterThan(minEncodedData.length); +} + - (void)test11ThatAPNGPCoderWorks { NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"]; [self verifyCoder:[SDImageAPNGCoder sharedCoder] From 82249e82d8aea80b23c7c8b554cc6d6a3bf45381 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 17:48:31 +0800 Subject: [PATCH 158/181] Bumped version to 5.7.0 Update the CHANGELOG --- CHANGELOG.md | 25 +++++++++++++++++++++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c32f75..9f7c1a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [5.7.0 - Query Cache Type and Encoding Options, on Apr 4th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.7.0) +See [all tickets marked for the 5.7.0 release](https://github.com/SDWebImage/SDWebImage/milestone/66) + +### Features + +#### Cache +- Added the async version API to query disk image data only +- Added the sync API to query disk image with context and options, which matches the async version + +#### Coder +- Feature supports encoding options like max file size, max pixel size, as well as background color when using JPEG for alpha image #2972 +- You can use `.encodeMaxFileSize` to limit the desired lossy file size, better than compression quality +- You can use `.encodeMaxPixelSize` to limit the pixel size, like thumbnail encoding + +#### Transformer +- Refactory the current thumbnail && transformer about cache key. Developer should have the API to calculate the cache key from thumbnail or transformer, not hard-coded. #2966 + +#### Context Option +- Added new query cache type support, including the SDImageCache API and context option #2968 +- You use `.queryCacheType` to query image from memory/disk/both cache during image pipeline loading + +### Fixes +- Fix the issue for Carthage/SwiftPM framework version symbols, this should match the framework name SDWebImage, or will get a link error when used #2971 #2969 +- Simplify the xattr helper method's code with modern Objective-C syntax #2967. Thanks @huangboju + ## [5.6.1 - 5.6 Patch, on Mar 13th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.6.1) See [all tickets marked for the 5.6.1 release](https://github.com/SDWebImage/SDWebImage/milestone/65) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index 8f0f1ae1..b96039d6 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.6.1' + s.version = '5.7.0' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 10938b0c..93161971 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.6.1 + 5.7.0 CFBundleSignature ???? CFBundleVersion - 5.6.1 + 5.7.0 NSPrincipalClass From e2285181a62daf4d1d3caf66d6d776b667092303 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 17:55:06 +0800 Subject: [PATCH 159/181] Update the CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f7c1a9c..8c294039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ See [all tickets marked for the 5.7.0 release](https://github.com/SDWebImage/SDW - Fix the issue for Carthage/SwiftPM framework version symbols, this should match the framework name SDWebImage, or will get a link error when used #2971 #2969 - Simplify the xattr helper method's code with modern Objective-C syntax #2967. Thanks @huangboju +### Changes +- Change the behavior to return the abstract type for unknown image format, this can solve the accident issue for custom coder who provide a new format #2973 + ## [5.6.1 - 5.6 Patch, on Mar 13th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.6.1) See [all tickets marked for the 5.6.1 release](https://github.com/SDWebImage/SDWebImage/milestone/65) From bbc65ed0cca55eb759547b3ac5989ec54b20d55c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 4 Apr 2020 18:30:30 +0800 Subject: [PATCH 160/181] Fix the wrong value assignment for SDAnimatedImageView code on macOS, warning --- SDWebImage/Core/SDAnimatedImageView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index 71ee7e34..e5cad14d 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -470,7 +470,7 @@ // NSImageView use a subview. We need this subview's layer for actual rendering. // Why using this design may because of properties like `imageAlignment` and `imageScaling`, which it's not available for UIImageView.contentMode (it's impossible to align left and keep aspect ratio at the same time) - (NSView *)imageView { - NSImageView *imageView = imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView)); + NSImageView *imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView)); if (!imageView) { // macOS 10.14 imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageSubview)); From bddc130914e0bc36103b80b4c4f0657887fa29c2 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sun, 5 Apr 2020 11:51:39 +0800 Subject: [PATCH 161/181] Update the README about the latest SwiftUI support and Ecosystem --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4c371c21..055c63d9 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ This library provides an async image downloader with cache support. For convenie ## Supported Image Formats -- Image formats supported by UIImage (JPEG, PNG, HEIC, ...), including GIF/APNG/HEIC animation +- Image formats supported by Apple system (JPEG, PNG, TIFF, HEIC, ...), including GIF/APNG/HEIC animation - WebP format, including animated WebP (use the [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) project) - Support extendable coder plugins for new image formats like BPG, AVIF. And vector format like PDF, SVG. See all the list in [Image coder plugin List](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) -## Additional modules +## Additional modules and Ecosystem In order to keep SDWebImage focused and limited to the core features, but also allow extensibility and custom behaviors, during the 5.0 refactoring we focused on modularizing the library. As such, we have moved/built new modules to [SDWebImage org](https://github.com/SDWebImage). @@ -49,7 +49,9 @@ As such, we have moved/built new modules to [SDWebImage org](https://github.com/ #### SwiftUI [SwiftUI](https://developer.apple.com/xcode/swiftui/) is an innovative UI framework written in Swift to build user interfaces across all Apple platforms. -We support SwiftUI by building with the functions (caching, loading and animation) powered by SDWebImage. You can have a try with [SDWebImageSwiftUI](https://github.com/SDWebImage/SDWebImageSwiftUI) +We support SwiftUI by building a brand new framework called [SDWebImageSwiftUI](https://github.com/SDWebImage/SDWebImageSwiftUI), which is built on top of SDWebImage core functions (caching, loading and animation). + +The new framework introduce two View structs `WebImage` and `AnimatedImage` for SwiftUI world, `ImageIndicator` modifier for any View, `ImageManager` observable object for data source. Supports iOS 13+/macOS 10.15+/tvOS 13+/watchOS 6+ and Swift 5.1. Have a nice try and provide feedback! #### Coders for additional image formats - [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) - coder for WebP format. Based on [libwebp](https://chromium.googlesource.com/webm/libwebp) @@ -72,13 +74,18 @@ We support SwiftUI by building with the functions (caching, loading and animatio - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching +#### Community driven popular libraries +- [FirebaseUI](https://github.com/firebase/FirebaseUI-iOS) - Firebase Storage binding for query images, based on SDWebImage loader system +- [react-native-fast-image](https://github.com/DylanVann/react-native-fast-image) - React Native fast image component, based on SDWebImage Animated Image solution +- [flutter_image_compress](https://github.com/OpenFlutter/flutter_image_compress) - Flutter compresses image plugin, based on SDWebImageWebPCoder coder plugin + #### Make our lives easier - [libwebp-Xcode](https://github.com/SDWebImage/libwebp-Xcode) - A wrapper for [libwebp](https://chromium.googlesource.com/webm/libwebp) + an Xcode project. - [libheif-Xcode](https://github.com/SDWebImage/libheif-Xcode) - A wrapper for [libheif](https://github.com/strukturag/libheif) + an Xcode project. - [libavif-Xcode](https://github.com/SDWebImage/libavif-Xcode) - A wrapper for [libavif](https://github.com/AOMediaCodec/libavif) + an Xcode project. - and more third-party C/C++ image codec libraries with CocoaPods/Carthage/SwiftPM support. -You can use those directly, or create similar components of your own. +You can use those directly, or create similar components of your own, by using the customizable architecture of SDWebImage. ## Requirements @@ -116,6 +123,7 @@ You can use those directly, or create similar components of your own. - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/sdwebimage). - If you **found a bug**, open an issue. - If you **have a feature request**, open an issue. +- If you **need IRC channel**, use [Gitter](https://gitter.im/SDWebImage/community). ## Contribution @@ -146,7 +154,14 @@ imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg ## Animated Images (GIF) support In 5.0, we introduced a brand new mechanism for supporting animated images. This includes animated image loading, rendering, decoding, and also supports customizations (for advanced users). -This animated image solution is available for `iOS`/`tvOS`/`macOS`. The `SDAnimatedImage` is subclass of `UIImage/NSImage`, and `SDAnimatedImageView` is subclass of `UIImageView/NSImageView`, to make them compatible with the common frameworks APIs. See [Animated Image](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for more detailed information. + +This animated image solution is available for `iOS`/`tvOS`/`macOS`. The `SDAnimatedImage` is subclass of `UIImage/NSImage`, and `SDAnimatedImageView` is subclass of `UIImageView/NSImageView`, to make them compatible with the common frameworks APIs. + +The `SDAnimatedImageView` supports the familiar image loading category methods, works like drop-in replacement for `UIImageView/NSImageView`. + +Don't have UIView (like WatchKit or CALayer)? you can still use `SDAnimatedPlayer` the player engine for advanced playback and rendering. + +See [Animated Image](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for more detailed information. * Objective-C From ed8887db35e58cbfd14e7bf86bdda150db53a051 Mon Sep 17 00:00:00 2001 From: Brian Amerige Date: Tue, 7 Apr 2020 13:58:11 -0700 Subject: [PATCH 162/181] [SDWebImageManager] Don't copy attributes from originalImage to transformedImage. This fixes https://github.com/SDWebImage/SDWebImage/issues/2975. --- SDWebImage/Core/SDWebImageManager.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 22c0e06c..2adf2b64 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -453,8 +453,6 @@ static id _defaultImageLoader; } else { cacheData = (imageWasTransformed ? nil : originalData); } - // keep the original image format and extended data - SDImageCopyAssociatedObject(originalImage, transformedImage); [self storeImage:transformedImage imageData:cacheData forKey:key cacheType:storeCacheType options:options context:context completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; }]; From a8177c1327ce94a148cb87beb2de724fbd451d00 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 8 Apr 2020 11:18:03 +0800 Subject: [PATCH 163/181] Update the test case `test12ThatStoreCacheTypeWork` to ensure the transformed image does not inherit the image format or any attributes from original one --- Tests/Tests/SDWebImageManagerTests.m | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index 327c16e7..fc1a194a 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -121,7 +121,7 @@ SDWebImageManager *manager = [[SDWebImageManager alloc] initWithCache:[SDImageCache sharedImageCache] loader:[SDWebImageDownloader sharedDownloader]]; manager.transformer = transformer; [[SDImageCache sharedImageCache] removeImageForKey:kTestJPEGURL withCompletion:^{ - [manager loadImageWithURL:url options:SDWebImageTransformAnimatedImage progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + [manager loadImageWithURL:url options:SDWebImageTransformAnimatedImage | SDWebImageTransformVectorImage progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { expect(image).equal(transformer.testImage); [expectation fulfill]; }]; @@ -228,21 +228,30 @@ // test: original image -> disk only, transformed image -> memory only SDWebImageContext *context = @{SDWebImageContextOriginalStoreCacheType : @(SDImageCacheTypeDisk), SDWebImageContextStoreCacheType : @(SDImageCacheTypeMemory)}; - NSURL *url = [NSURL URLWithString:kTestJPEGURL]; + NSURL *url = [NSURL URLWithString:kTestAPNGPURL]; NSString *originalKey = [manager cacheKeyForURL:url]; NSString *transformedKey = [manager cacheKeyForURL:url context:context]; [manager loadImageWithURL:url options:SDWebImageTransformAnimatedImage context:context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { expect(image).equal(transformer.testImage); + // the transformed image should not inherite any attribute from original one + expect(image.sd_imageFormat).equal(SDImageFormatJPEG); + expect(image.sd_isAnimated).beFalsy(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*kMinDelayNanosecond), dispatch_get_main_queue(), ^{ // original -> disk only - [manager.imageCache containsImageForKey:originalKey cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType originalCacheType) { - expect(originalCacheType).equal(SDImageCacheTypeDisk); - // transformed -> memory only - [manager.imageCache containsImageForKey:transformedKey cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType transformedCacheType) { - expect(transformedCacheType).equal(SDImageCacheTypeMemory); - [expectation fulfill]; - }]; + UIImage *originalImage = [cache imageFromMemoryCacheForKey:originalKey]; + expect(originalImage).beNil(); + NSData *originalData = [cache diskImageDataForKey:originalKey]; + expect(originalData).notTo.beNil(); + originalImage = [UIImage sd_imageWithData:originalData]; + expect(originalImage).notTo.beNil(); + expect(originalImage.sd_imageFormat).equal(SDImageFormatPNG); + expect(originalImage.sd_isAnimated).beTruthy(); + // transformed -> memory only + [manager.imageCache containsImageForKey:transformedKey cacheType:SDImageCacheTypeAll completion:^(SDImageCacheType transformedCacheType) { + expect(transformedCacheType).equal(SDImageCacheTypeMemory); + [cache clearDiskOnCompletion:nil]; + [expectation fulfill]; }]; }); }]; From f52dc6b4bd5bf29c7da5f1969b0f42e0392079b0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 8 Apr 2020 11:26:58 +0800 Subject: [PATCH 164/181] Bumped version to 5.7.1 Update the CHANGELOG --- CHANGELOG.md | 7 +++++++ SDWebImage.podspec | 2 +- WebImage/Info.plist | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c294039..cc6d2f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [5.7.1 - 5.7 Patch, on Apr 8th, 2020](https://github.com/SDWebImage/SDWebImage/releases/tag/5.7.1) +See [all tickets marked for the 5.7.1 release](https://github.com/SDWebImage/SDWebImage/milestone/67) + +### Fixes +- Don't copy attributes from originalImage to transformedImage when caching transformedImage #2976. Thanks @bdaz +- Fix the wrong value assignment for SDAnimatedImageView code on macOS, warning #2974 + ## [5.7.0 - Query Cache Type and Encoding Options, on Apr 4th, 2020](https://github.com/rs/SDWebImage/releases/tag/5.7.0) See [all tickets marked for the 5.7.0 release](https://github.com/SDWebImage/SDWebImage/milestone/66) diff --git a/SDWebImage.podspec b/SDWebImage.podspec index b96039d6..46f2061a 100644 --- a/SDWebImage.podspec +++ b/SDWebImage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SDWebImage' - s.version = '5.7.0' + s.version = '5.7.1' s.osx.deployment_target = '10.10' s.ios.deployment_target = '8.0' diff --git a/WebImage/Info.plist b/WebImage/Info.plist index 93161971..3f491f95 100644 --- a/WebImage/Info.plist +++ b/WebImage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.7.0 + 5.7.1 CFBundleSignature ???? CFBundleVersion - 5.7.0 + 5.7.1 NSPrincipalClass From 13bae85e3dbda81b5ce44a7034e30735dd44d187 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Apr 2020 23:34:23 +0800 Subject: [PATCH 165/181] Fix that when first play animated image and use maxBufferSize to 0, the calculation does not works (The CGImage is nil) --- SDWebImage/Core/SDAnimatedImagePlayer.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index 39658b43..54135094 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -179,12 +179,12 @@ #pragma mark - Animation Control - (void)startPlaying { [self.displayLink start]; - // Calculate max buffer size - [self calculateMaxBufferCount]; // Setup frame if (self.currentFrameIndex == 0 && !self.currentFrame) { [self setupCurrentFrame]; } + // Calculate max buffer size + [self calculateMaxBufferCount]; } - (void)stopPlaying { From a5e129dac7b6e18c2f51551428d258df3d247cf0 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 11 Apr 2020 14:44:38 +0800 Subject: [PATCH 166/181] SDAnimatedImageView animation rendering should not use CGContext force decoding, use `kCGImageSourceShouldCacheImmediately` instead which can avoid OOM for large number of GIFs #2977 --- SDWebImage/Core/SDImageCoder.h | 1 + SDWebImage/Core/SDImageCoderHelper.m | 8 +- SDWebImage/Core/SDImageIOAnimatedCoder.m | 69 ++++++++++-------- SDWebImage/Core/SDImageIOCoder.m | 4 +- .../Private/SDImageIOAnimatedCoderInternal.h | 2 +- .../project.pbxproj | 8 ++ Tests/Tests/Images/TestImageLarge.png | Bin 0 -> 313855 bytes Tests/Tests/SDImageCoderTests.m | 60 ++++++++++++--- 8 files changed, 106 insertions(+), 46 deletions(-) create mode 100644 Tests/Tests/Images/TestImageLarge.png diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 590fbba0..fe5ca0cc 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -21,6 +21,7 @@ typedef NSMutableDictionary SDImageCoderMutableOptions; @note works for `SDImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFirstFrameOnly; + /** A CGFloat value which is greater than or equal to 1.0. This value specify the image scale factor for decoding. If not provide, use 1.0. (NSNumber) @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 7bba1025..c0547dc6 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -563,14 +563,14 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over #pragma mark - Helper Fuction + (BOOL)shouldDecodeImage:(nullable UIImage *)image { - // Avoid extra decode - if (image.sd_isDecoded) { - return NO; - } // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error if (image == nil) { return NO; } + // Avoid extra decode + if (image.sd_isDecoded) { + return NO; + } // do not decode animated images if (image.sd_isAnimated) { return NO; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index fb3dea6e..89220c88 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -122,8 +122,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { + NSDictionary *options = @{ + (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES), + (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage + }; NSTimeInterval frameDuration = 0.1; - CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); + CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options); if (!cfFrameProperties) { return frameDuration; } @@ -153,9 +157,10 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination return frameDuration; } -+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize { ++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options { + // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :) // Parse the image properties - NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL); + NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options); NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue]; NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue]; CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue]; @@ -169,10 +174,15 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) { isVector = YES; } - + + NSMutableDictionary *decodingOptions; + if (options) { + decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options]; + } else { + decodingOptions = [NSMutableDictionary dictionary]; + } CGImageRef imageRef; if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { - NSDictionary *options; if (isVector) { if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { // Provide the default pixel count for vector images, simply just use the screen size @@ -187,12 +197,11 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); NSUInteger DPIPerPixel = 2; NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel; - options = @{kSDCGImageSourceRasterizationDPI : @(rasterizationDPI)}; + decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI); } - imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)options); + imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)decodingOptions); } else { - NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; - thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); + decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); CGFloat maxPixelSize; if (preserveAspectRatio) { CGFloat pixelRatio = pixelWidth / pixelHeight; @@ -205,9 +214,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } else { maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); } - thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); - thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); - imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); + decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); + decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); + imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)decodingOptions); } if (!imageRef) { return nil; @@ -288,12 +297,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue]; if (decodeFirstFrame || count <= 1) { - animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil]; } else { NSMutableArray *frames = [NSMutableArray array]; for (size_t i = 0; i < count; i++) { - UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil]; if (!image) { continue; } @@ -369,7 +378,11 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished); if (_width + _height == 0) { - CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL); + NSDictionary *options = @{ + (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES), + (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage + }; + CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options); if (properties) { CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); @@ -393,7 +406,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } - image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil]; if (image) { image.sd_imageFormat = self.class.imageFormat; } @@ -597,24 +610,20 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { - UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (index >= _frameCount) { + return nil; + } + // Animated Image should not use the CGContext solution to force decode. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961 + NSDictionary *options = @{ + (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES), + (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage + }; + UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:options]; if (!image) { return nil; } image.sd_imageFormat = self.class.imageFormat; - // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance) - CGImageRef imageRef = [SDImageCoderHelper CGImageCreateDecoded:image.CGImage]; - if (!imageRef) { - return image; - } -#if SD_MAC - image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp]; -#else - image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:image.imageOrientation]; -#endif - CGImageRelease(imageRef); - image.sd_isDecoded = YES; - image.sd_imageFormat = self.class.imageFormat; + image.sd_isDecoded = YES;; return image; } diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 482aea8d..df93e14e 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -101,7 +101,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination return nil; } - UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil]; CFRelease(source); if (!image) { return nil; @@ -193,7 +193,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } - image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil]; if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; diff --git a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h index f2976ea8..cb4ee2c4 100644 --- a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h +++ b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h @@ -13,6 +13,6 @@ + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source; + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source; -+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize; ++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(nullable NSDictionary *)options; @end diff --git a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj index ac80009d..12e43aec 100644 --- a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj +++ b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 1E3C51E919B46E370092B5E6 /* SDWebImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */; }; 2D7AF0601F329763000083C2 /* SDTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D7AF05F1F329763000083C2 /* SDTestCase.m */; }; + 320224F72440C39B00E5B29D /* TestImageLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 320224F62440C39B00E5B29D /* TestImageLarge.png */; }; + 320224F82440C39B00E5B29D /* TestImageLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 320224F62440C39B00E5B29D /* TestImageLarge.png */; }; + 320224F92440C39B00E5B29D /* TestImageLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 320224F62440C39B00E5B29D /* TestImageLarge.png */; }; 320630412085A37C006E0FA4 /* SDAnimatedImageTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32A571552037DB2D002EDAAE /* SDAnimatedImageTest.m */; }; 3222417F2272F808002429DB /* SDUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3222417E2272F808002429DB /* SDUtilsTests.m */; }; 322241802272F808002429DB /* SDUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3222417E2272F808002429DB /* SDUtilsTests.m */; }; @@ -107,6 +110,7 @@ 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageDownloaderTests.m; sourceTree = ""; }; 2D7AF05E1F329763000083C2 /* SDTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDTestCase.h; sourceTree = ""; }; 2D7AF05F1F329763000083C2 /* SDTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDTestCase.m; sourceTree = ""; }; + 320224F62440C39B00E5B29D /* TestImageLarge.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TestImageLarge.png; sourceTree = ""; }; 3222417E2272F808002429DB /* SDUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDUtilsTests.m; sourceTree = ""; }; 3226ECB920754F7700FAFACF /* SDWebImageTestDownloadOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestDownloadOperation.h; sourceTree = ""; }; 3226ECBA20754F7700FAFACF /* SDWebImageTestDownloadOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDWebImageTestDownloadOperation.m; sourceTree = ""; }; @@ -239,6 +243,7 @@ 326E69462334C0C200B7252C /* TestLoopCount.gif */, 5F7F38AC1AE2A77A00B0E330 /* TestImage.jpg */, 43828A441DA67F9900000E62 /* TestImageLarge.jpg */, + 320224F62440C39B00E5B29D /* TestImageLarge.png */, 433BBBB81D7EF8260086B6E9 /* TestImage.png */, 327A418B211D660600495442 /* TestImage.heic */, 32905E63211D786E00460FCF /* TestImage.heif */, @@ -449,6 +454,7 @@ 329922872365DC6C00EAFD97 /* TestLoopCount.gif in Resources */, 3299228C2365DC6C00EAFD97 /* TestImage.heif in Resources */, 3234306423E2BAC800C290C8 /* TestImage.pdf in Resources */, + 320224F92440C39B00E5B29D /* TestImageLarge.png in Resources */, 329922892365DC6C00EAFD97 /* TestImageLarge.jpg in Resources */, 3299228A2365DC6C00EAFD97 /* TestImage.png in Resources */, 329922842365DC6C00EAFD97 /* MonochromeTestImage.jpg in Resources */, @@ -468,6 +474,7 @@ 324047452271956F007C53E1 /* TestEXIF.png in Resources */, 32B99EA4203B31360017FD66 /* TestImage.jpg in Resources */, 3234306323E2BAC800C290C8 /* TestImage.pdf in Resources */, + 320224F82440C39B00E5B29D /* TestImageLarge.png in Resources */, 32B99EA6203B31360017FD66 /* TestImage.png in Resources */, 3297A0A023374D1700814590 /* TestImageAnimated.heic in Resources */, 32B99EA2203B31360017FD66 /* MonochromeTestImage.jpg in Resources */, @@ -487,6 +494,7 @@ 32905E64211D786E00460FCF /* TestImage.heif in Resources */, 43828A451DA67F9900000E62 /* TestImageLarge.jpg in Resources */, 3234306223E2BAC800C290C8 /* TestImage.pdf in Resources */, + 320224F72440C39B00E5B29D /* TestImageLarge.png in Resources */, 433BBBB71D7EF8200086B6E9 /* TestImage.gif in Resources */, 433BBBB91D7EF8260086B6E9 /* TestImage.png in Resources */, 3297A09F23374D1700814590 /* TestImageAnimated.heic in Resources */, diff --git a/Tests/Tests/Images/TestImageLarge.png b/Tests/Tests/Images/TestImageLarge.png new file mode 100644 index 0000000000000000000000000000000000000000..4573dd8fa4ad3e6f2741e6fa647d156097a0e578 GIT binary patch literal 313855 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≫V z7gO|byGFLT@tie1IPJoShHtouqeIU}>N=ig@@ehmk4s^sA2|EvL)@bSTx8rH{O;u8 zcE(TFFX*WZcGgR)JEitnGaHV*e*B;keVtcZ5uL5nKQ}f~%pD}r`N0O_Ows>1Z47an zag9KE*GM|(LMroU1tVYE`zt2JqC)h7CoyeO-2SlPEHrm?xoA7qv;e-xHV|s z!o?Ye#Pl3-IF9!{9p%hEQyl1<<|O4^CGyZ-J784S;+v@tJeOj0)cp1M&X9)+cf z)uv)H{>=w%)54DEU8j`Kh%{8+sihokJ^SiRdiq|0ww?#K6%h`&vbo=)FU+wcEk`7n z5%l~nERIVxwKCF_w0|^lK&rB$K(wO7Tc^DaGSbn6ks@DYm;LFIdZM@~RWfO5?vN=i zIW74ahwsc`CHlm$h;;g^q**T(MLTO$+RO7YXOH)B`q^Op(xg_qFKAk?T1exZZ%y>x zvDf_N=)t9x3M@L@VVIWgcs9u+GZeSZccD6Whqj0!&7eXqOx(*tV8{-t`Yc;m?3H4y zo@n3bb9h}uvQ5C&AUEYKXhQOt;_h!We>5z*OmFYcdg@|Kv@I15kMN996bR!-M$Kx% zW1sYu2AB0G7B5+%BJA2L7F`71z#=TQXt>kRBIJ^N6~7g8Rh-OGR9=6EJS6L1)5&BBjLG@dEJf6N)^?H=0#cCkViS8mZw^Q(8Otsv$ruW~j@zY<_m&r`5RO03iLy^j(QKH#A&o?3-HZ>Ufky&j6v4NlT-aCN)aP{Bdh1uq7suVX%nRx zN*Soh435}T@G}GXuHRSe<4#L^0D{+2S`?CDWyynoRz1LdFRSXLCmUO9)g1~UH=@ak z+W7_Cn`;TeoHB{+Q|BeWu0qS!9$@qH0($Fbf2brNsfwR9zVR8l|1}Nw`~{2JfF$O% zG{!>Yh+Eh3#XFa36J$X(q=Roy4SuXLewpp#sTjgw51jQK6a{J!sFumDfg8&Dr@=M$ zsn5(FP<#Q_q4QLH@2TTr)~PTWa((_&dkRx+_^Fyjoa@VMRkR0 IxesCg4