diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index b9cdad39d..c209cde3f 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -408,7 +408,24 @@ namespace ImageSharp.Formats { if (this.componentCount == 4) { - this.ConvertFromCmyk(this.imageWidth, this.imageHeight, image); + if (!this.adobeTransformValid) + { + throw new ImageFormatException("Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + } + + // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe + // See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + // TODO: YCbCrA? + if (this.adobeTransform == JpegConstants.Adobe.ColorTransformYcck) + { + this.ConvertFromYcck(this.imageWidth, this.imageHeight, image); + } + else if (this.adobeTransform == JpegConstants.Adobe.ColorTransformUnknown) + { + // Assume CMYK + this.ConvertFromCmyk(this.imageWidth, this.imageHeight, image); + } + return; } @@ -1167,60 +1184,87 @@ namespace ImageSharp.Formats } /// - /// Converts the image from the original CMYK image pixels. + /// Converts the image from the original YCCK image pixels. /// /// The pixel format. /// The packed format. uint, long, float. /// The image width. /// The image height. /// The image. - private void ConvertFromCmyk(int width, int height, Image image) + private void ConvertFromYcck(int width, int height, Image image) where TColor : struct, IPackedPixel where TPacked : struct, IEquatable { - if (!this.adobeTransformValid) + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + + image.InitPixels(width, height); + + using (PixelAccessor pixels = image.Lock()) { - throw new ImageFormatException( - "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + Parallel.For( + 0, + height, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YChannel[yo + x]; + byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + + TColor packed = default(TColor); + this.PackYcck(ref packed, yy, cb, cr, x, y); + pixels[x, y] = packed; + } + }); } - // If the 4-component JPEG image isn't explicitly marked as "Unknown (RGB - // or CMYK)" as per http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe - if (this.adobeTransform != JpegConstants.Adobe.ColorTransformUnknown) - { - int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + this.AssignResolution(image); + } - image.InitPixels(width, height); + /// + /// Converts the image from the original CMYK image pixels. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromCmyk(int width, int height, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct, IEquatable + { + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; - using (PixelAccessor pixels = image.Lock()) - { - // Convert the YCbCr part of the YCbCrK to RGB, invert the RGB to get - // CMY, and patch in the original K. The RGB to CMY inversion cancels - // out the 'Adobe inversion' described in the applyBlack doc comment - // above, so in practice, only the fourth channel (black) is inverted. - Parallel.For( - 0, - height, - y => - { - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); + image.InitPixels(width, height); - for (int x = 0; x < width; x++) - { - byte yy = this.ycbcrImage.YChannel[yo + x]; - byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + using (PixelAccessor pixels = image.Lock()) + { + Parallel.For( + 0, + height, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); - TColor packed = default(TColor); - this.PackCmyk(ref packed, yy, cb, cr, x, y); - pixels[x, y] = packed; - } - }); - } + for (int x = 0; x < width; x++) + { + byte cyan = this.ycbcrImage.YChannel[yo + x]; + byte magenta = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte yellow = this.ycbcrImage.CrChannel[co + (x / scale)]; - this.AssignResolution(image); + TColor packed = default(TColor); + this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); + pixels[x, y] = packed; + } + }); } + + this.AssignResolution(image); } /// @@ -2101,7 +2145,7 @@ namespace ImageSharp.Formats } /// - /// Optimized method to pack bytes to the image from the CMYK color space. + /// Optimized method to pack bytes to the image from the YCCK color space. /// This is faster than implicit casting as it avoids double packing. /// /// The pixel format. @@ -2112,10 +2156,14 @@ namespace ImageSharp.Formats /// The cr chroma component. /// The x-position within the image. /// The y-position within the image. - private void PackCmyk(ref TColor packed, byte y, byte cb, byte cr, int xx, int yy) + private void PackYcck(ref TColor packed, byte y, byte cb, byte cr, int xx, int yy) where TColor : struct, IPackedPixel where TPacked : struct, IEquatable { + // Convert the YCbCr part of the YCbCrK to RGB, invert the RGB to get + // CMY, and patch in the original K. The RGB to CMY inversion cancels + // out the 'Adobe inversion' described in the applyBlack doc comment + // above, so in practice, only the fourth channel (black) is inverted. // TODO: We can speed this up further with Vector4 int ccb = cb - 128; int ccr = cr - 128; @@ -2136,6 +2184,33 @@ namespace ImageSharp.Formats packed.PackFromBytes(r, g, b, 255); } + /// + /// Optimized method to pack bytes to the image from the CMYK color space. + /// This is faster than implicit casting as it avoids double packing. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The packed pixel. + /// The cyan component. + /// The magenta component. + /// The yellow component. + /// The x-position within the image. + /// The y-position within the image. + private void PackCmyk(ref TColor packed, byte c, byte m, byte y, int xx, int yy) + where TColor : struct, IPackedPixel + where TPacked : struct, IEquatable + { + // Get keyline + float keyline = (255 - this.blackPixels[(yy * this.blackStride) + xx]) / 255F; + + // Convert back to RGB. CMY are not inverted + byte r = (byte)(((c / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + byte g = (byte)(((m / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + byte b = (byte)(((y / 255F) * (1F - keyline)).Clamp(0, 1) * 255); + + packed.PackFromBytes(r, g, b, 255); + } + /// /// Represents a component scan /// diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 18b3863cb..80db710cd 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/tests/ImageSharp.Tests/FileTestBase.cs @@ -22,6 +22,7 @@ namespace ImageSharp.Tests // new TestFile(TestImages.Png.Pd), // Perf: Enable for local testing only // new TestFile(TestImages.Jpeg.Floorplan), // Perf: Enable for local testing only new TestFile(TestImages.Jpeg.Calliphora), + // new TestFile(TestImages.Jpeg.Ycck), // Perf: Enable for local testing only // new TestFile(TestImages.Jpeg.Cmyk), // Perf: Enable for local testing only new TestFile(TestImages.Jpeg.Turtle), // new TestFile(TestImages.Jpeg.Fb), // Perf: Enable for local testing only diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9c0a9d66c..941aa376e 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -41,6 +41,7 @@ namespace ImageSharp.Tests public static string Exif => folder + "exif.jpg"; public static string Floorplan => folder + "Floorplan.jpg"; public static string Calliphora => folder + "Calliphora.jpg"; + public static string Ycck => folder + "ycck.jpg"; public static string Turtle => folder + "turtle.jpg"; public static string Fb => folder + "fb.jpg"; public static string Progress => folder + "progress.jpg"; diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/cmyk.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/cmyk.jpg index 30e88773e..8837cfed3 100644 Binary files a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/cmyk.jpg and b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/cmyk.jpg differ diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/ycck.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/ycck.jpg new file mode 100644 index 000000000..30e88773e Binary files /dev/null and b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/ycck.jpg differ