diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index 29386a9..8e51a9e 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -818,7 +818,7 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef maxFileSize:(NSUInteger)maxFileSize options:(nullable SDImageCoderOptions *)options { - NSData *webpData; + NSData *webpData = nil; if (!imageRef) { return nil; } @@ -950,16 +950,55 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef result = WebPEncode(&config, &picture); WebPPictureFree(&picture); free(dest.data); - + if (result) { - // success - webpData = [NSData dataWithBytes:writer.mem length:writer.size]; + // Add ICC profile if present + // See: https://developers.google.com/speed/webp/docs/riff_container#color_profile + // Skip ICC profile when maxFileSize is set, as meeting the size limit takes priority + CFDataRef iccData = NULL; + if (colorSpace && maxFileSize == 0) { + if (@available(iOS 10, tvOS 10, macOS 10.12, watchOS 3, *)) { + iccData = CGColorSpaceCopyICCData(colorSpace); + } + } + + if (iccData && CFDataGetLength(iccData) > 0) { + // Use WebPMux to add ICCP chunk + // This automatically converts Simple Format to Extended Format (VP8X) + WebPMux *mux = WebPMuxNew(); + if (mux) { + WebPData webp_input = { + .bytes = writer.mem, + .size = writer.size + }; + + if (WebPMuxSetImage(mux, &webp_input, 0) == WEBP_MUX_OK) { + WebPData icc_chunk = { + .bytes = CFDataGetBytePtr(iccData), + .size = CFDataGetLength(iccData) + }; + + if (WebPMuxSetChunk(mux, "ICCP", &icc_chunk, 0) == WEBP_MUX_OK) { + WebPData output; + if (WebPMuxAssemble(mux, &output) == WEBP_MUX_OK) { + webpData = [NSData dataWithBytes:output.bytes length:output.size]; + WebPDataClear(&output); + } + } + } + WebPMuxDelete(mux); + } + CFRelease(iccData); + } + + if (!webpData) { + webpData = [NSData dataWithBytes:writer.mem length:writer.size]; + } } else { - // failed webpData = nil; } WebPMemoryWriterClear(&writer); - + return webpData; } diff --git a/Tests/Images/TestDisplayP3.png b/Tests/Images/TestDisplayP3.png new file mode 100644 index 0000000..55bad7f Binary files /dev/null and b/Tests/Images/TestDisplayP3.png differ diff --git a/Tests/SDWebImageWebPCoderTests.m b/Tests/SDWebImageWebPCoderTests.m index 89a89a8..f89bcfa 100644 --- a/Tests/SDWebImageWebPCoderTests.m +++ b/Tests/SDWebImageWebPCoderTests.m @@ -381,7 +381,7 @@ - (void)testWebPEncodingWithICCProfile { NSString *jpegPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestColorspaceBefore" ofType:@"jpeg"]; NSData *jpegData = [NSData dataWithContentsOfFile:jpegPath]; UIImage *jpegImage = [[UIImage alloc] initWithData:jpegData]; - + NSData *webpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:jpegImage format:SDImageFormatWebP options:nil]; // Re-decode to pick color UIImage *webpImage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:nil]; @@ -404,6 +404,74 @@ - (void)testWebPEncodingWithICCProfile { #endif } +- (void)testWebPEncodingEmbedICCProfile { + // Test that ICC profile is embedded in WebP + NSString *jpegPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestColorspaceBefore" ofType:@"jpeg"]; + NSData *jpegData = [NSData dataWithContentsOfFile:jpegPath]; + UIImage *jpegImage = [[UIImage alloc] initWithData:jpegData]; + expect(jpegImage).notTo.beNil(); + + NSData *webpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:jpegImage format:SDImageFormatWebP options:nil]; + expect(webpData).notTo.beNil(); + + // Check for ICCP chunk + WebPData webp_data; + WebPDataInit(&webp_data); + webp_data.bytes = webpData.bytes; + webp_data.size = webpData.length; + + WebPDemuxer *demuxer = WebPDemux(&webp_data); + expect(demuxer).notTo.beNil(); + + uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); + expect(flags & ICCP_FLAG).notTo.equal(0); + + WebPChunkIterator chunk_iter; + int result = WebPDemuxGetChunk(demuxer, "ICCP", 1, &chunk_iter); + expect(result).notTo.equal(0); + expect(chunk_iter.chunk.size).to.beGreaterThan(0); + + WebPDemuxReleaseChunkIterator(&chunk_iter); + WebPDemuxDelete(demuxer); +} + +- (void)testWebPEncodingDisplayP3 { + // Test Display P3 wide color gamut encoding with ICC profile + NSString *pngPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestDisplayP3" ofType:@"png"]; + if (!pngPath) { + return; + } + + NSData *pngData = [NSData dataWithContentsOfFile:pngPath]; + UIImage *p3Image = [[UIImage alloc] initWithData:pngData]; + expect(p3Image).notTo.beNil(); + + NSData *webpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:p3Image + format:SDImageFormatWebP + options:@{SDImageCoderEncodeCompressionQuality: @0.95}]; + expect(webpData).notTo.beNil(); + + // Check for ICCP chunk + WebPData webp_data; + WebPDataInit(&webp_data); + webp_data.bytes = webpData.bytes; + webp_data.size = webpData.length; + + WebPDemuxer *demuxer = WebPDemux(&webp_data); + expect(demuxer).notTo.beNil(); + + uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); + expect(flags & ICCP_FLAG).notTo.equal(0); + + WebPChunkIterator chunk_iter; + int result = WebPDemuxGetChunk(demuxer, "ICCP", 1, &chunk_iter); + expect(result).notTo.equal(0); + expect(chunk_iter.chunk.size).to.beGreaterThan(0); + + WebPDemuxReleaseChunkIterator(&chunk_iter); + WebPDemuxDelete(demuxer); +} + @end @implementation SDWebImageWebPCoderTests (Helpers) diff --git a/Tests/SDWebImageWebPCoderTests.xcodeproj/project.pbxproj b/Tests/SDWebImageWebPCoderTests.xcodeproj/project.pbxproj index 5626066..e09834c 100644 --- a/Tests/SDWebImageWebPCoderTests.xcodeproj/project.pbxproj +++ b/Tests/SDWebImageWebPCoderTests.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -23,6 +23,8 @@ 808C918E213FD131004B0F7C /* SDWebImageWebPCoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 808C918D213FD131004B0F7C /* SDWebImageWebPCoderTests.m */; }; 808C919C213FD2B2004B0F7C /* TestImageStatic.webp in Resources */ = {isa = PBXBuildFile; fileRef = 808C919A213FD2B2004B0F7C /* TestImageStatic.webp */; }; 808C919D213FD2B2004B0F7C /* TestImageAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 808C919B213FD2B2004B0F7C /* TestImageAnimated.webp */; }; + B359E6032EB3ACBE0064933A /* TestDisplayP3.png in Resources */ = {isa = PBXBuildFile; fileRef = B359E6022EB3ACBE0064933A /* TestDisplayP3.png */; }; + B359E6042EB3ACBE0064933A /* TestDisplayP3.png in Resources */ = {isa = PBXBuildFile; fileRef = B359E6022EB3ACBE0064933A /* TestDisplayP3.png */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -40,6 +42,7 @@ 808C918F213FD131004B0F7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 808C919A213FD2B2004B0F7C /* TestImageStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageStatic.webp; sourceTree = ""; }; 808C919B213FD2B2004B0F7C /* TestImageAnimated.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageAnimated.webp; sourceTree = ""; }; + B359E6022EB3ACBE0064933A /* TestDisplayP3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TestDisplayP3.png; sourceTree = ""; }; D92E6791BF088D1A101E670E /* Pods-SDWebImageWebPCoderTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDWebImageWebPCoderTests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-SDWebImageWebPCoderTests/Pods-SDWebImageWebPCoderTests.release.xcconfig"; sourceTree = ""; }; F121CFAEBEFA209D335C5C6D /* Pods-SDWebImageWebPCoderTests-macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDWebImageWebPCoderTests-macOS.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-SDWebImageWebPCoderTests-macOS/Pods-SDWebImageWebPCoderTests-macOS.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -107,6 +110,7 @@ 808C9199213FD2B2004B0F7C /* Images */ = { isa = PBXGroup; children = ( + B359E6022EB3ACBE0064933A /* TestDisplayP3.png */, 32B4C7912AFB959E003A4BC7 /* TestColorspaceBefore.jpeg */, 326420302A5D53E300EE3E46 /* TestColorspaceStatic.webp */, 325E268D25C82BE1000B807B /* TestImageGrayscale.jpg */, @@ -211,6 +215,7 @@ 32B4C78C2AFB954C003A4BC7 /* TestColorspaceStatic.webp in Resources */, 32B4C78D2AFB954C003A4BC7 /* TestImageBlendAnimated.webp in Resources */, 32B4C7932AFB959E003A4BC7 /* TestColorspaceBefore.jpeg in Resources */, + B359E6042EB3ACBE0064933A /* TestDisplayP3.png in Resources */, 32B4C78B2AFB954C003A4BC7 /* TestImageGrayscale.jpg in Resources */, 32B4C78F2AFB954C003A4BC7 /* TestImageStatic.webp in Resources */, ); @@ -224,6 +229,7 @@ 808C919D213FD2B2004B0F7C /* TestImageAnimated.webp in Resources */, 808C919C213FD2B2004B0F7C /* TestImageStatic.webp in Resources */, 32B4C7922AFB959E003A4BC7 /* TestColorspaceBefore.jpeg in Resources */, + B359E6032EB3ACBE0064933A /* TestDisplayP3.png in Resources */, 326420312A5D53E300EE3E46 /* TestColorspaceStatic.webp in Resources */, 325E268E25C82BE1000B807B /* TestImageGrayscale.jpg in Resources */, ); @@ -262,10 +268,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests/Pods-SDWebImageWebPCoderTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests/Pods-SDWebImageWebPCoderTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests/Pods-SDWebImageWebPCoderTests-frameworks.sh\"\n"; @@ -297,10 +307,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests-macOS/Pods-SDWebImageWebPCoderTests-macOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests-macOS/Pods-SDWebImageWebPCoderTests-macOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SDWebImageWebPCoderTests-macOS/Pods-SDWebImageWebPCoderTests-macOS-frameworks.sh\"\n";