diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 70a4465121..b5a51c5a4a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -38,10 +38,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private int restartInterval; - // How many mcu's are left to do. + /// + /// How many mcu's are left to do. + /// private int todo; - // The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. + /// + /// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. + /// private int eobrun; /// @@ -54,14 +58,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private readonly HuffmanTable[] acHuffmanTables; - // The unzig data. + /// + /// The unzig data. + /// private ZigZag dctZigZag; private HuffmanScanBuffer scanBuffer; private readonly SpectralConverter spectralConverter; - private CancellationToken cancellationToken; + private readonly CancellationToken cancellationToken; /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs index e84d13ff16..23bb01409c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { /// @@ -30,5 +32,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Actual stride height depends on the subsampling factor of the given component. /// public abstract void ConvertStrideBaseline(); + + /// + /// Gets the color converter. + /// + /// The jpeg frame with the color space to convert to. + /// The raw JPEG data. + /// The color converter. + public virtual JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 50cfa0188a..313a132b81 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -11,12 +11,12 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { - internal sealed class SpectralConverter : SpectralConverter, IDisposable + internal class SpectralConverter : SpectralConverter, IDisposable where TPixel : unmanaged, IPixel { private readonly Configuration configuration; - private CancellationToken cancellationToken; + private readonly CancellationToken cancellationToken; private JpegComponentPostProcessor[] componentProcessors; @@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } + /// public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { MemoryAllocator allocator = this.configuration.MemoryAllocator; @@ -85,9 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.rgbaBuffer = allocator.Allocate(frame.PixelWidth); // color converter from Rgba32 to TPixel - this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision); + this.colorConverter = this.GetColorConverter(frame, jpegData); } + /// public override void ConvertStrideBaseline() { // Convert next pixel stride using single spectral `stride' diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 331da275cc..4b74400cac 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder private int emitLen = 0; /// - /// Emmited bits 'micro buffer' before being transfered to the . + /// Emitted bits 'micro buffer' before being transferred to the . /// private int accumulatedBits; @@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// private readonly Stream target; - public HuffmanScanEncoder(Stream outputStream) - { - this.target = outputStream; - } + public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream; /// /// Encodes the image with no subsampling. /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee - /// Chrominance quantization table provided by the callee + /// Luminance quantization table provided by the callee. + /// Chrominance quantization table provided by the callee. /// The token to monitor for cancellation. public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee - /// Chrominance quantization table provided by the callee + /// Luminance quantization table provided by the callee. + /// Chrominance quantization table provided by the callee. /// The token to monitor for cancellation. public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee + /// Luminance quantization table provided by the callee. /// The token to monitor for cancellation. public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -234,6 +231,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.FlushInternalBuffer(); } + /// + /// Encodes the image with no subsampling and keeps the pixel data as Rgb24. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// Luminance quantization table provided by the callee. + /// The token to monitor for cancellation. + public void EncodeRgb(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + this.huffmanTables = HuffmanLut.TheHuffmanLut; + + var unzig = ZigZag.CreateUnzigTable(); + + // ReSharper disable once InconsistentNaming + int prevDCR = 0, prevDCG = 0, prevDCB = 0; + + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet currentRows = default; + + var pixelConverter = new RgbForwardConverter(frame); + + for (int y = 0; y < pixels.Height; y += 8) + { + cancellationToken.ThrowIfCancellationRequested(); + currentRows.Update(pixelBuffer, y); + + for (int x = 0; x < pixels.Width; x += 8) + { + pixelConverter.Convert(x, y, ref currentRows); + + prevDCR = this.WriteBlock( + QuantIndex.Luminance, + prevDCR, + ref pixelConverter.R, + ref luminanceQuantTable, + ref unzig); + + prevDCG = this.WriteBlock( + QuantIndex.Luminance, + prevDCG, + ref pixelConverter.G, + ref luminanceQuantTable, + ref unzig); + + prevDCB = this.WriteBlock( + QuantIndex.Luminance, + prevDCB, + ref pixelConverter.B, + ref luminanceQuantTable, + ref unzig); + } + } + + this.FlushInternalBuffer(); + } + /// /// Writes a block of pixel data using the given quantization table, /// returning the post-quantized DC value of the DCT-transformed block. @@ -437,7 +492,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max"); #if SUPPORTS_BITOPERATIONS // This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation - // But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value)) + // But internal log2 is implemented like this: (31 - (int)Lzcnt.LeadingZeroCount(value)) // BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0 // Lzcnt would return 32 for input value of 0 - no need to check that with branching @@ -449,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder // if 0 - return 0 in this case // else - return log2(value) + 1 // - // Hack based on input value constaint: + // Hack based on input value constraint: // We know that input values are guaranteed to be maximum 16 bit large for huffman encoding // We can safely shift input value for one bit -> log2(value << 1) // Because of the 16 bit value constraint it won't overflow diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs index 5eee5dfde4..f9d0fba57f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs @@ -1,21 +1,21 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { /// - /// Enumerates the quantization tables + /// Enumerates the quantization tables. /// internal enum QuantIndex { /// - /// The luminance quantization table index + /// The luminance quantization table index. /// Luminance = 0, /// - /// The chrominance quantization table index + /// The chrominance quantization table index. /// Chrominance = 1, } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs new file mode 100644 index 0000000000..7fad63e117 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + /// + /// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks. + /// + /// The pixel type to work on. + internal ref struct RgbForwardConverter + where TPixel : unmanaged, IPixel + { + /// + /// Number of pixels processed per single call + /// + private const int PixelsPerSample = 8 * 8; + + /// + /// Total byte size of processed pixels converted from TPixel to + /// + private const int RgbSpanByteSize = PixelsPerSample * 3; + + /// + /// of sampling area from given frame pixel buffer. + /// + private static readonly Size SampleSize = new Size(8, 8); + + /// + /// The Red component. + /// + public Block8x8F R; + + /// + /// The Green component. + /// + public Block8x8F G; + + /// + /// The Blue component. + /// + public Block8x8F B; + + /// + /// Temporal 64-byte span to hold unconverted TPixel data. + /// + private readonly Span pixelSpan; + + /// + /// Temporal 64-byte span to hold converted Rgb24 data. + /// + private readonly Span rgbSpan; + + /// + /// Sampled pixel buffer size. + /// + private readonly Size samplingAreaSize; + + /// + /// for internal operations. + /// + private readonly Configuration config; + + public RgbForwardConverter(ImageFrame frame) + { + this.R = default; + this.G = default; + this.B = default; + + // temporal pixel buffers + this.pixelSpan = new TPixel[PixelsPerSample].AsSpan(); + this.rgbSpan = MemoryMarshal.Cast(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan()); + + // frame data + this.samplingAreaSize = new Size(frame.Width, frame.Height); + this.config = frame.GetConfiguration(); + } + + /// + /// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24. + /// + public void Convert(int x, int y, ref RowOctet currentRows) + { + YCbCrForwardConverter.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize); + + PixelOperations.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan); + + ref Block8x8F redBlock = ref this.R; + ref Block8x8F greenBlock = ref this.G; + ref Block8x8F blueBlock = ref this.B; + + CopyToBlock(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock); + } + + private static void CopyToBlock(Span rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock) + { + ref Rgb24 rgbStart = ref MemoryMarshal.GetReference(rgbSpan); + + for (int i = 0; i < Block8x8F.Size; i++) + { + Rgb24 c = Unsafe.Add(ref rgbStart, i); + + redBlock[i] = c.R; + greenBlock[i] = c.G; + blueBlock[i] = c.B; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs index a4abd532b3..bfeafcbb3d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs @@ -58,22 +58,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Temporal 16x8 block to hold TPixel data /// - private Span pixelSpan; + private readonly Span pixelSpan; /// /// Temporal RGB block /// - private Span rgbSpan; + private readonly Span rgbSpan; /// /// Sampled pixel buffer size /// - private Size samplingAreaSize; + private readonly Size samplingAreaSize; /// /// for internal operations /// - private Configuration config; + private readonly Configuration config; public YCbCrForwardConverter420(ImageFrame frame) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs index ef589272bd..2dbd1a2dc5 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs @@ -53,22 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Temporal 64-byte span to hold unconverted TPixel data /// - private Span pixelSpan; + private readonly Span pixelSpan; /// /// Temporal 64-byte span to hold converted Rgb24 data /// - private Span rgbSpan; + private readonly Span rgbSpan; /// /// Sampled pixel buffer size /// - private Size samplingAreaSize; + private readonly Size samplingAreaSize; /// /// for internal operations /// - private Configuration config; + private readonly Configuration config; public YCbCrForwardConverter444(ImageFrame frame) { diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index a9f564b450..70cfd18e94 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -16,13 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public int? Quality { get; set; } /// - /// Gets the subsample ration, that will be used to encode the image. - /// - /// The subsample ratio of the jpg image. - JpegSubsample? Subsample { get; } - - /// - /// Gets the color type. + /// Gets the color type, that will be used to encode the image. /// JpegColorType? ColorType { get; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegColorType.cs b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs index 73b3215d62..c15038c23b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegColorType.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs @@ -10,12 +10,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { /// /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only + /// sampled on each alternate line. /// - YCbCr = 0, + YCbCrRatio420 = 0, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// High Quality - Each of the three Y'CbCr components have the same sample rate, + /// thus there is no chroma subsampling. + /// + YCbCrRatio444 = 1, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// The two chroma components are sampled at half the horizontal sample rate of luma while vertically it has full resolution. + /// + /// Note: Not supported by the encoder. + /// + YCbCrRatio422 = 2, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// In 4:1:1 chroma subsampling, the horizontal color resolution is quartered. + /// + /// Note: Not supported by the encoder. + /// + YCbCrRatio411 = 3, + + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// This ratio uses half of the vertical and one-fourth the horizontal color resolutions. + /// + /// Note: Not supported by the encoder. + /// + YCbCrRatio410 = 4, /// /// Single channel, luminance. /// - Luminance = 1 + Luminance = 5, + + /// + /// The pixel data will be preserved as RGB without any sub sampling. + /// + Rgb = 6, + + /// + /// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing. + /// + /// Note: Not supported by the encoder. + /// + Cmyk = 7, } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 96520c535c..dfef139ab0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -136,8 +137,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Finds the next file marker within the byte stream. /// - /// The buffer to read file markers to - /// The input stream + /// The buffer to read file markers to. + /// The input stream. /// The public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream) { @@ -200,6 +201,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); } + /// + /// Load quantization and/or Huffman tables for subsequent use for jpeg's embedded in tiff's, + /// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips). + /// + /// The table bytes. + /// The scan decoder. + public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder) + { + this.Metadata = new ImageMetadata(); + this.QuantizationTables = new Block8x8F[4]; + this.scanDecoder = huffmanScanDecoder; + using var ms = new MemoryStream(tableBytes); + using var stream = new BufferedReadStream(this.Configuration, ms); + + // Check for the Start Of Image marker. + stream.Read(this.markerBuffer, 0, 2); + var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); + } + + // Read next marker. + stream.Read(this.markerBuffer, 0, 2); + byte marker = this.markerBuffer[1]; + fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); + + while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) + { + if (!fileMarker.Invalid) + { + // Get the marker length. + int remaining = this.ReadUint16(stream) - 2; + + switch (fileMarker.Marker) + { + case JpegConstants.Markers.SOI: + break; + case JpegConstants.Markers.RST0: + case JpegConstants.Markers.RST7: + break; + case JpegConstants.Markers.DHT: + this.ProcessDefineHuffmanTablesMarker(stream, remaining); + break; + case JpegConstants.Markers.DQT: + this.ProcessDefineQuantizationTablesMarker(stream, remaining); + break; + case JpegConstants.Markers.DRI: + this.ProcessDefineRestartIntervalMarker(stream, remaining); + break; + case JpegConstants.Markers.EOI: + return; + } + } + + // Read next marker. + stream.Read(this.markerBuffer, 0, 2); + fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); + } + } + /// /// Parses the input stream for file markers. /// @@ -225,7 +287,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Read(this.markerBuffer, 0, 2); byte marker = this.markerBuffer[1]; fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); - this.QuantizationTables = new Block8x8F[4]; + this.QuantizationTables ??= new Block8x8F[4]; // Break only when we discover a valid EOI marker. // https://github.com/SixLabors/ImageSharp/issues/695 @@ -236,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { - // Get the marker length + // Get the marker length. int remaining = this.ReadUint16(stream) - 2; switch (fileMarker.Marker) @@ -272,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream, remaining, cancellationToken); + this.ProcessStartOfScanMarker(stream, remaining); break; } else @@ -371,10 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Returns the correct colorspace based on the image component count and the jpeg frame components. + /// Returns the correct colorspace based on the image component count and the jpeg frame component id's. /// + /// The number of components. /// The - private JpegColorSpace DeduceJpegColorSpace(byte componentCount, JpegComponent[] components) + private JpegColorSpace DeduceJpegColorSpace(byte componentCount) { if (componentCount == 1) { @@ -389,7 +452,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr. - if (components[2].Id == 66 && components[1].Id == 71 && components[0].Id == 82) + if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82) { return JpegColorSpace.RGB; } @@ -410,6 +473,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return default; } + /// + /// Returns the jpeg color type based on the colorspace and subsampling used. + /// + /// Jpeg color type. + private JpegColorType DeduceJpegColorType() + { + switch (this.ColorSpace) + { + case JpegColorSpace.Grayscale: + return JpegColorType.Luminance; + + case JpegColorSpace.RGB: + return JpegColorType.Rgb; + + case JpegColorSpace.YCbCr: + if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio444; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio420; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 2 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 2) + { + return JpegColorType.YCbCrRatio422; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio411; + } + else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 && + this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && + this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) + { + return JpegColorType.YCbCrRatio410; + } + else + { + return JpegColorType.YCbCrRatio420; + } + + case JpegColorSpace.Cmyk: + return JpegColorType.Cmyk; + + default: + return JpegColorType.YCbCrRatio420; + } + } + /// /// Initializes the EXIF profile. /// @@ -608,7 +729,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. - /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. + /// The tableBytes of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. /// /// The input stream. /// The remaining bytes in the segment block. @@ -823,7 +944,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Processes the Start of Frame marker. Specified in section B.2.2. + /// Processes the Start of Frame marker. Specified in section B.2.2. /// /// The input stream. /// The remaining bytes in the segment block. @@ -915,9 +1036,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg index += componentBytes; } - this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components); - - this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + this.ColorSpace = this.DeduceJpegColorSpace(componentCount); + this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType(); if (!metadataOnly) { @@ -1015,7 +1135,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken) + private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) { if (this.Frame is null) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 5e199b4204..6f116f4fb3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -16,14 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int? Quality { get; set; } - /// - /// Gets or sets the subsample ration, that will be used to encode the image. - /// - public JpegSubsample? Subsample { get; set; } - - /// - /// Gets or sets the color type, that will be used to encode the image. - /// + /// public JpegColorType? ColorType { get; set; } /// @@ -36,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - this.InitializeColorType(image); + this.InitializeColorType(image); encoder.Encode(image, stream); } @@ -52,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - this.InitializeColorType(image); + this.InitializeColorType(image); return encoder.EncodeAsync(image, stream, cancellationToken); } @@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg bool isGrayscale = typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) || typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32); - this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; + this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCrRatio420; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 88d96f554d..14e7cc6778 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -33,20 +33,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private readonly byte[] buffer = new byte[20]; - /// - /// Gets or sets the subsampling method to use. - /// - private JpegSubsample? subsample; - /// /// The quality, that will be used to encode the image. /// private readonly int? quality; /// - /// Gets or sets the subsampling method to use. + /// Gets or sets the colorspace to use. /// - private readonly JpegColorType? colorType; + private JpegColorType? colorType; /// /// The output stream. All attempted writes after the first error become no-ops. @@ -56,12 +51,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Initializes a new instance of the class. /// - /// The options + /// The options. public JpegEncoderCore(IJpegEncoderOptions options) { this.quality = options.Quality; - this.subsample = options.Subsample; - this.colorType = options.ColorType; + + if (IsSupportedColorType(options.ColorType)) + { + this.colorType = options.ColorType; + } } /// @@ -88,49 +86,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg ImageMetadata metadata = image.Metadata; JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); + // If the color type was not specified by the user, preserve the color type of the input image, if it's a supported color type. + if (!this.colorType.HasValue && IsSupportedColorType(jpegMetadata.ColorType)) + { + this.colorType = jpegMetadata.ColorType; + } + // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; + ReadOnlySpan componentIds = this.GetComponentIds(); // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that // Initialize the quantization tables. this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); // Write the Start Of Image marker. - this.WriteApplicationHeader(metadata); + this.WriteStartOfImage(); + + // Do not write APP0 marker for RGB colorspace. + if (this.colorType != JpegColorType.Rgb) + { + this.WriteJfifApplicationHeader(metadata); + } // Write Exif, ICC and IPTC profiles this.WriteProfiles(metadata); + if (this.colorType == JpegColorType.Rgb) + { + // Write App14 marker to indicate RGB color space. + this.WriteApp14Marker(); + } + // Write the quantization tables. this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); // Write the image dimensions. - this.WriteStartOfFrame(image.Width, image.Height, componentCount); + this.WriteStartOfFrame(image.Width, image.Height, componentCount, componentIds); // Write the Huffman tables. this.WriteDefineHuffmanTables(componentCount); // Write the scan header. - this.WriteStartOfScan(image, componentCount, cancellationToken); + this.WriteStartOfScan(componentCount, componentIds); // Write the scan compressed data. var scanEncoder = new HuffmanScanEncoder(stream); if (this.colorType == JpegColorType.Luminance) { - // luminance quantization table only + // luminance quantization table only. scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); } else { - // luminance and chrominance quantization tables - switch (this.subsample) + // luminance and chrominance quantization tables. + switch (this.colorType) { - case JpegSubsample.Ratio444: + case JpegColorType.YCbCrRatio444: + case JpegColorType.Luminance: scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); break; - case JpegSubsample.Ratio420: + case JpegColorType.YCbCrRatio420: scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); break; + case JpegColorType.Rgb: + scanEncoder.EncodeRgb(image, ref luminanceQuantTable, cancellationToken); + break; } } @@ -141,12 +162,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Writes data to "Define Quantization Tables" block for QuantIndex + /// Returns true, if the color type is supported by the encoder. /// - /// The "Define Quantization Tables" block - /// Offset in "Define Quantization Tables" block - /// The quantization index - /// The quantization table to copy data from + /// The color type. + /// true, if color type is supported. + private static bool IsSupportedColorType(JpegColorType? colorType) + => colorType == JpegColorType.YCbCrRatio444 + || colorType == JpegColorType.YCbCrRatio420 + || colorType == JpegColorType.Luminance + || colorType == JpegColorType.Rgb; + + /// + /// Gets the component ids. + /// For color space RGB this will be RGB as ASCII, otherwise 1, 2, 3. + /// + /// The component Ids. + private ReadOnlySpan GetComponentIds() => this.colorType == JpegColorType.Rgb + ? new ReadOnlySpan(new byte[] { 82, 71, 66 }) + : new ReadOnlySpan(new byte[] { 1, 2, 3 }); + + /// + /// Writes data to "Define Quantization Tables" block for QuantIndex. + /// + /// The "Define Quantization Tables" block. + /// Offset in "Define Quantization Tables" block. + /// The quantization index. + /// The quantization table to copy data from. private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant) { dqt[offset++] = (byte)i; @@ -157,52 +198,60 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Writes the application header containing the JFIF identifier plus extra data. + /// Write the start of image marker. /// - /// The image metadata. - private void WriteApplicationHeader(ImageMetadata meta) + private void WriteStartOfImage() { - // Write the start of image marker. Markers are always prefixed with 0xff. + // Markers are always prefixed with 0xff. this.buffer[0] = JpegConstants.Markers.XFF; this.buffer[1] = JpegConstants.Markers.SOI; + this.outputStream.Write(this.buffer, 0, 2); + } + + /// + /// Writes the application header containing the JFIF identifier plus extra data. + /// + /// The image metadata. + private void WriteJfifApplicationHeader(ImageMetadata meta) + { // Write the JFIF headers - this.buffer[2] = JpegConstants.Markers.XFF; - this.buffer[3] = JpegConstants.Markers.APP0; // Application Marker - this.buffer[4] = 0x00; - this.buffer[5] = 0x10; - this.buffer[6] = 0x4a; // J + this.buffer[0] = JpegConstants.Markers.XFF; + this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker + this.buffer[2] = 0x00; + this.buffer[3] = 0x10; + this.buffer[4] = 0x4a; // J + this.buffer[5] = 0x46; // F + this.buffer[6] = 0x49; // I this.buffer[7] = 0x46; // F - this.buffer[8] = 0x49; // I - this.buffer[9] = 0x46; // F - this.buffer[10] = 0x00; // = "JFIF",'\0' - this.buffer[11] = 0x01; // versionhi - this.buffer[12] = 0x01; // versionlo + this.buffer[8] = 0x00; // = "JFIF",'\0' + this.buffer[9] = 0x01; // versionhi + this.buffer[10] = 0x01; // versionlo // Resolution. Big Endian - Span hResolution = this.buffer.AsSpan(14, 2); - Span vResolution = this.buffer.AsSpan(16, 2); + Span hResolution = this.buffer.AsSpan(12, 2); + Span vResolution = this.buffer.AsSpan(14, 2); if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) { // Scale down to PPI - this.buffer[13] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits + this.buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution))); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); } else { // We can simply pass the value. - this.buffer[13] = (byte)meta.ResolutionUnits; // xyunits + this.buffer[11] = (byte)meta.ResolutionUnits; // xyunits BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(meta.HorizontalResolution)); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); } // No thumbnail - this.buffer[18] = 0x00; // Thumbnail width - this.buffer[19] = 0x00; // Thumbnail height + this.buffer[16] = 0x00; // Thumbnail width + this.buffer[17] = 0x00; // Thumbnail height - this.outputStream.Write(this.buffer, 0, 20); + this.outputStream.Write(this.buffer, 0, 18); } /// @@ -212,7 +261,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void WriteDefineHuffmanTables(int componentCount) { // Table identifiers. - Span headers = stackalloc byte[] + ReadOnlySpan headers = stackalloc byte[] { 0x00, 0x10, @@ -249,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable) { - // Marker + quantization table lengths + // Marker + quantization table lengths. int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size)); this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); @@ -265,6 +314,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.outputStream.Write(dqt, 0, dqtCount); } + /// + /// Writes the APP14 marker to indicate the image is in RGB color space. + /// + private void WriteApp14Marker() + { + this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + AdobeMarker.Length); + + // Identifier: ASCII "Adobe". + this.buffer[0] = 0x41; + this.buffer[1] = 0x64; + this.buffer[2] = 0x6F; + this.buffer[3] = 0x62; + this.buffer[4] = 0x65; + + // Version, currently 100. + BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(5, 2), 100); + + // Flags0 + BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(7, 2), 0); + + // Flags1 + BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0); + + // Transform byte, 0 in combination with three components means the image is in RGB colorspace. + this.buffer[11] = 0; + + this.outputStream.Write(this.buffer.AsSpan(0, 12)); + } + /// /// Writes the EXIF profile. /// @@ -343,7 +421,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes"); } - var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + + int app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + ProfileResolver.AdobeImageResourceBlockMarker.Length + ProfileResolver.AdobeIptcMarker.Length + 2 + 4 + data.Length; @@ -385,7 +463,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The ICC profile to write. /// - /// Thrown if any of the ICC profiles size exceeds the limit + /// Thrown if any of the ICC profiles size exceeds the limit. /// private void WriteIccProfile(IccProfile iccProfile) { @@ -405,7 +483,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return; } - // Calculate the number of markers we'll need, rounding up of course + // Calculate the number of markers we'll need, rounding up of course. int dataLength = data.Length; int count = dataLength / MaxData; @@ -478,22 +556,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Writes the Start Of Frame (Baseline) marker + /// Writes the Start Of Frame (Baseline) marker. /// - /// The width of the image - /// The height of the image - /// The number of components in a pixel - private void WriteStartOfFrame(int width, int height, int componentCount) + /// The width of the image. + /// The height of the image. + /// The number of components in a pixel. + /// The component Id's. + private void WriteStartOfFrame(int width, int height, int componentCount, ReadOnlySpan componentIds) { + // This uses a C#'s compiler optimization that refers to the static data segment of the assembly, + // and doesn't incur any allocation at all. // "default" to 4:2:0 - Span subsamples = stackalloc byte[] + ReadOnlySpan subsamples = stackalloc byte[] { 0x22, 0x11, 0x11 }; - Span chroma = stackalloc byte[] + ReadOnlySpan chroma = stackalloc byte[] { 0x00, 0x01, @@ -511,17 +592,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } else { - switch (this.subsample) + switch (this.colorType) { - case JpegSubsample.Ratio444: + case JpegColorType.YCbCrRatio444: + case JpegColorType.Rgb: subsamples = stackalloc byte[] { 0x11, 0x11, 0x11 }; + + if (this.colorType == JpegColorType.Rgb) + { + chroma = stackalloc byte[] + { + 0x00, + 0x00, + 0x00 + }; + } + break; - case JpegSubsample.Ratio420: + case JpegColorType.YCbCrRatio420: subsamples = stackalloc byte[] { 0x22, @@ -545,10 +638,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int i = 0; i < componentCount; i++) { int i3 = 3 * i; - this.buffer[i3 + 6] = (byte)(i + 1); - this.buffer[i3 + 7] = subsamples[i]; - this.buffer[i3 + 8] = chroma[i]; + // Component ID. + Span bufferSpan = this.buffer.AsSpan(i3 + 6, 3); + bufferSpan[2] = chroma[i]; + bufferSpan[1] = subsamples[i]; + bufferSpan[0] = componentIds[i]; } this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9); @@ -557,26 +652,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Writes the StartOfScan marker. /// - /// The pixel format. - /// The pixel accessor providing access to the image pixels. /// The number of components in a pixel. - /// The token to monitor for cancellation. - private void WriteStartOfScan(Image image, int componentCount, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// The componentId's. + private void WriteStartOfScan(int componentCount, ReadOnlySpan componentIds) { - Span componentId = stackalloc byte[] - { - 0x01, - 0x02, - 0x03 - }; - Span huffmanId = stackalloc byte[] + // This uses a C#'s compiler optimization that refers to the static data segment of the assembly, + // and doesn't incur any allocation at all. + ReadOnlySpan huffmanId = stackalloc byte[] { 0x00, 0x11, 0x11 }; + // Use the same DC/AC tables for all channels for RGB. + if (this.colorType == JpegColorType.Rgb) + { + huffmanId = stackalloc byte[] + { + 0x00, + 0x00, + 0x00 + }; + } + // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // - the marker length "\x00\x0c", // - the number of components "\x03", @@ -597,7 +696,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int i = 0; i < componentCount; i++) { int i2 = 2 * i; - this.buffer[i2 + 5] = componentId[i]; // Component Id + this.buffer[i2 + 5] = componentIds[i]; // Component Id this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table } @@ -633,7 +732,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Initializes quntization tables. + /// Initializes quantization tables. /// /// /// We take quality values in a hierarchical order: @@ -672,9 +771,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); - if (!this.subsample.HasValue) + if (!this.colorType.HasValue) { - this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + this.colorType = chromaQuality >= 91 ? JpegColorType.YCbCrRatio444 : JpegColorType.YCbCrRatio420; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs b/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs deleted file mode 100644 index 16488f6d21..0000000000 --- a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Formats.Jpeg -{ - /// - /// Enumerates the chroma subsampling method applied to the image. - /// - public enum JpegSubsample - { - /// - /// High Quality - Each of the three Y'CbCr components have the same sample rate, - /// thus there is no chroma subsampling. - /// - Ratio444, - - /// - /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only - /// sampled on each alternate line. - /// - Ratio420 - } -} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs index 30d21e54ce..f456324e53 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors private static bool IsReplicateRun(ReadOnlySpan rowSpan, int startPos) { // We consider run which has at least 3 same consecutive bytes a candidate for a run. - var startByte = rowSpan[startPos]; + byte startByte = rowSpan[startPos]; int count = 0; for (int i = startPos + 1; i < rowSpan.Length; i++) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs new file mode 100644 index 0000000000..0ae8fd37bd --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + internal class TiffJpegCompressor : TiffBaseCompressor + { + public TiffJpegCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(output, memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + public override TiffCompression Method => TiffCompression.Jpeg; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) + { + int pixelCount = rows.Length / 3; + int width = pixelCount / height; + + using var memoryStream = new MemoryStream(); + var image = Image.LoadPixelData(rows, width, height); + image.Save(memoryStream, new JpegEncoder() + { + ColorType = JpegColorType.Rgb + }); + memoryStream.Position = 0; + memoryStream.WriteTo(this.Output); + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs new file mode 100644 index 0000000000..bd1c496b49 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed as a jpeg stream. + /// + internal sealed class JpegTiffCompression : TiffBaseDecompressor + { + private readonly Configuration configuration; + + private readonly byte[] jpegTables; + + private readonly TiffPhotometricInterpretation photometricInterpretation; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits per pixel. + /// The JPEG tables containing the quantization and/or Huffman tables. + /// The photometric interpretation. + public JpegTiffCompression( + Configuration configuration, + MemoryAllocator memoryAllocator, + int width, + int bitsPerPixel, + byte[] jpegTables, + TiffPhotometricInterpretation photometricInterpretation) + : base(memoryAllocator, width, bitsPerPixel) + { + this.configuration = configuration; + this.jpegTables = jpegTables; + this.photometricInterpretation = photometricInterpretation; + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) + { + if (this.jpegTables != null) + { + using var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder()); + + // TODO: Should we pass through the CancellationToken from the tiff decoder? + // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space. + // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB). + using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? + new RgbJpegSpectralConverter(this.configuration, CancellationToken.None) : new SpectralConverter(this.configuration, CancellationToken.None); + var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + scanDecoder.ResetInterval = 0; + jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); + + using var image = new Image(this.configuration, spectralConverter.PixelBuffer, new ImageMetadata()); + CopyImageBytesToBuffer(buffer, image); + } + else + { + using var image = Image.Load(stream); + CopyImageBytesToBuffer(buffer, image); + } + } + + private static void CopyImageBytesToBuffer(Span buffer, Image image) + { + int offset = 0; + for (int y = 0; y < image.Height; y++) + { + Span pixelRowSpan = image.GetPixelRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs new file mode 100644 index 0000000000..45be3dd038 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Threading; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Spectral converter for YCbCr TIFF's which use the JPEG compression. + /// The jpeg data should be always treated as RGB color space. + /// + /// The type of the pixel. + internal sealed class RgbJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// This Spectral converter will always convert the pixel data to RGB color. + /// + /// The configuration. + /// The cancellation token. + public RgbJpegSpectralConverter(Configuration configuration, CancellationToken cancellationToken) + : base(configuration, cancellationToken) + { + } + + /// + public override JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(JpegColorSpace.RGB, frame.Precision); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs index 14a0c6e9d7..db2b935b74 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs @@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression // The following compression types are not implemented in the encoder and will default to no compression instead. case TiffCompression.ItuTRecT43: case TiffCompression.ItuTRecT82: - case TiffCompression.Jpeg: case TiffCompression.OldJpeg: case TiffCompression.OldDeflate: case TiffCompression.None: @@ -34,6 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression return new NoCompressor(output, allocator, width, bitsPerPixel); + case TiffCompression.Jpeg: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new TiffJpegCompressor(output, allocator, width, bitsPerPixel); + case TiffCompression.PackBits: DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index 61b691eb84..d8843c1078 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -36,11 +36,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// Image data is compressed using CCITT T.6 fax compression. /// - T6 = 6, + T6 = 5, /// /// Image data is compressed using modified huffman compression. /// - HuffmanRle = 5, + HuffmanRle = 6, + + /// + /// The image data is compressed as a JPEG stream. + /// + Jpeg = 7, } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index 735ea1aa2a..a7e2e276bd 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression internal static class TiffDecompressorsFactory { public static TiffBaseDecompressor Create( + Configuration configuration, TiffDecoderCompressionType method, MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, @@ -19,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression TiffColorType colorType, TiffPredictor predictor, FaxCompressionOptions faxOptions, + byte[] jpegTables, TiffFillOrder fillOrder, ByteOrder byteOrder) { @@ -54,6 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + case TiffDecoderCompressionType.Jpeg: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation); + default: throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index beac42db7c..ff4aa52b0c 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -48,7 +48,7 @@ |CcittGroup4Fax | | Y | | |Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | |Old Jpeg | | | We should not even try to support this | -|Jpeg (Technote 2) | | | | +|Jpeg (Technote 2) | Y | Y | | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | |Old Deflate (Technote 2) | | Y | | diff --git a/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf b/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf new file mode 100644 index 0000000000..e4822d4093 Binary files /dev/null and b/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf differ diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index ff5f8923e3..55af87005a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -105,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffFillOrder FillOrder { get; set; } + /// + /// Gets or sets the JPEG tables when jpeg compression is used. + /// + public byte[] JpegTables { get; set; } + /// /// Gets or sets the planar configuration type to use when decoding the image. /// @@ -144,7 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff var frames = new List>(); foreach (ExifProfile ifd in directories) { - ImageFrame frame = this.DecodeFrame(ifd); + cancellationToken.ThrowIfCancellationRequested(); + ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); frames.Add(frame); } @@ -186,10 +192,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// The pixel format. /// The IFD tags. - /// - /// The tiff frame. - /// - private ImageFrame DecodeFrame(ExifProfile tags) + /// The token to monitor cancellation. + /// The tiff frame. + private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? @@ -211,11 +216,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts); + this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); } else { - this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts); + this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); } return frame; @@ -263,14 +268,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff } /// - /// Decodes the image data for strip encoded data. + /// Decodes the image data for planar encoded pixel data. /// /// The pixel format. /// The image frame to decode data into. /// The number of rows per strip of data. /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) + /// The token to monitor cancellation. + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Channels; @@ -290,6 +296,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.Configuration, this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, @@ -298,6 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorType, this.Predictor, this.FaxCompressionOptions, + this.JpegTables, this.FillOrder, this.byteOrder); @@ -312,6 +320,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff for (int i = 0; i < stripsPerPlane; i++) { + cancellationToken.ThrowIfCancellationRequested(); + int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; int stripIndex = i; @@ -338,7 +348,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) + /// + /// Decodes the image data for chunky encoded pixel data. + /// + /// The pixel format. + /// The image frame to decode data into. + /// The rows per strip. + /// The strip offsets. + /// The strip byte counts. + /// The token to monitor cancellation. + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. @@ -355,6 +374,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Buffer2D pixels = frame.PixelBuffer; using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.Configuration, this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, @@ -363,6 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorType, this.Predictor, this.FaxCompressionOptions, + this.JpegTables, this.FillOrder, this.byteOrder); @@ -379,6 +400,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { + cancellationToken.ThrowIfCancellationRequested(); + int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index d8357d945f..a8420fabba 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -87,6 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.FillOrder = fillOrder; + options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); @@ -432,6 +433,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.Jpeg: + { + options.CompressionType = TiffDecoderCompressionType.Jpeg; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d7c9848a44..1e4254a4ef 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -155,6 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff Image metadataImage = image; foreach (ImageFrame frame in image.Frames) { + cancellationToken.ThrowIfCancellationRequested(); + var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); @@ -223,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff entriesCollector, (int)this.BitsPerPixel); - int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow); + int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); colorWriter.Write(compressor, rowsPerStrip); @@ -245,13 +247,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// The height of the image. /// The number of bytes per row. + /// The compression used. /// Number of rows per strip. - private int CalcRowsPerStrip(int height, int bytesPerRow) + private int CalcRowsPerStrip(int height, int bytesPerRow, TiffCompression? compression) { DebugGuard.MustBeGreaterThan(height, 0, nameof(height)); DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow)); - int rowsPerStrip = TiffConstants.DefaultStripSize / bytesPerRow; + // Jpeg compressed images should be written in one strip. + if (compression is TiffCompression.Jpeg) + { + return height; + } + + // If compression is used, change stripSizeInBytes heuristically to a larger value to not write to many strips. + int stripSizeInBytes = compression is TiffCompression.Deflate || compression is TiffCompression.Lzw ? TiffConstants.DefaultStripSize * 2 : TiffConstants.DefaultStripSize; + int rowsPerStrip = stripSizeInBytes / bytesPerRow; if (rowsPerStrip > 0) { diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 15694978fc..55dd7d3973 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -396,6 +396,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffCompression.Ccitt1D: return (ushort)TiffCompression.Ccitt1D; + + case TiffCompression.Jpeg: + return (ushort)TiffCompression.Jpeg; } return (ushort)TiffCompression.None; diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs index 87170e8d24..508b4b3b09 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs @@ -40,8 +40,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg this.bmpCore = Image.Load(this.bmpStream); this.bmpCore.Metadata.ExifProfile = null; - this.encoder420 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio420 }; - this.encoder444 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio444 }; + this.encoder420 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 }; + this.encoder444 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio444 }; this.bmpStream.Position = 0; this.bmpDrawing = SDImage.FromStream(this.bmpStream); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 403eeaf908..e5f8989c5c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -67,16 +67,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg string imagePath, int expectedPixelSize, bool exifProfilePresent, - bool iccProfilePresent) - { - TestMetadataImpl( + bool iccProfilePresent) => TestMetadataImpl( useIdentify, JpegDecoder, imagePath, expectedPixelSize, exifProfilePresent, iccProfilePresent); - } [Theory] [MemberData(nameof(RatioFiles))] @@ -133,8 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - var decoder = new JpegDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = JpegDecoder.Decode(Configuration.Default, stream)) { JpegMetadata meta = image.Metadata.GetJpegMetadata(); Assert.Equal(quality, meta.Quality); @@ -142,6 +138,42 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } } + [Theory] + [InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegColorType.Luminance)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegColorType.YCbCrRatio420)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegColorType.YCbCrRatio444)] + [InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegColorType.Rgb)] + [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorType.Cmyk)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegColorType.YCbCrRatio410)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg422, JpegColorType.YCbCrRatio422)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegColorType.YCbCrRatio411)] + public void Identify_DetectsCorrectColorType(string imagePath, JpegColorType expectedColorType) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo image = JpegDecoder.Identify(Configuration.Default, stream); + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + Assert.Equal(expectedColorType, meta.ColorType); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] + [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)] + [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32, JpegColorType.Cmyk)] + public void Decode_DetectsCorrectColorType(TestImageProvider provider, JpegColorType expectedColorType) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(JpegDecoder)) + { + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + Assert.Equal(expectedColorType, meta.ColorType); + } + } + private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action test) { var testFile = TestFile.Create(imagePath); @@ -161,9 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg string imagePath, int expectedPixelSize, bool exifProfilePresent, - bool iccProfilePresent) - { - TestImageInfo( + bool iccProfilePresent) => TestImageInfo( imagePath, decoder, useIdentify, @@ -207,7 +237,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Null(iccProfile); } }); - } [Theory] [InlineData(false)] @@ -237,9 +266,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [InlineData(false)] [InlineData(true)] - public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) - { - TestImageInfo( + public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) => TestImageInfo( TestImages.Jpeg.Baseline.Floorplan, JpegDecoder, useIdentify, @@ -248,14 +275,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(300, imageInfo.Metadata.HorizontalResolution); Assert.Equal(300, imageInfo.Metadata.VerticalResolution); }); - } [Theory] [InlineData(false)] [InlineData(true)] - public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) - { - TestImageInfo( + public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) => TestImageInfo( TestImages.Jpeg.Baseline.Jpeg420Exif, JpegDecoder, useIdentify, @@ -264,6 +288,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(72, imageInfo.Metadata.HorizontalResolution); Assert.Equal(72, imageInfo.Metadata.VerticalResolution); }); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 6b1ce19e49..2a18a2c103 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -193,7 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void Issue1732_DecodesWithRgbColorSpace(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage(new JpegDecoder())) + using (Image image = provider.GetImage(JpegDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 8e12b04be9..2bd2961de3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -24,6 +24,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public class JpegEncoderTests { + private static JpegEncoder JpegEncoder => new JpegEncoder(); + + private static JpegDecoder JpegDecoder => new JpegDecoder(); + public static readonly TheoryData QualityFiles = new TheoryData { @@ -31,15 +35,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { TestImages.Jpeg.Progressive.Fb, 75 } }; - public static readonly TheoryData BitsPerPixel_Quality = - new TheoryData + public static readonly TheoryData BitsPerPixel_Quality = + new TheoryData { - { JpegSubsample.Ratio420, 40 }, - { JpegSubsample.Ratio420, 60 }, - { JpegSubsample.Ratio420, 100 }, - { JpegSubsample.Ratio444, 40 }, - { JpegSubsample.Ratio444, 60 }, - { JpegSubsample.Ratio444, 100 }, + { JpegColorType.YCbCrRatio420, 40 }, + { JpegColorType.YCbCrRatio420, 60 }, + { JpegColorType.YCbCrRatio420, 100 }, + { JpegColorType.YCbCrRatio444, 40 }, + { JpegColorType.YCbCrRatio444, 60 }, + { JpegColorType.YCbCrRatio444, 100 }, + { JpegColorType.Rgb, 40 }, + { JpegColorType.Rgb, 60 }, + { JpegColorType.Rgb, 100 } }; public static readonly TheoryData Grayscale_Quality = @@ -59,17 +66,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg }; [Theory] - [MemberData(nameof(QualityFiles))] - public void Encode_PreserveQuality(string imagePath, int quality) + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)] + [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)] + public void Encode_PreservesColorType(TestImageProvider provider, JpegColorType expectedColorType) + where TPixel : unmanaged, IPixel + { + // arrange + using Image input = provider.GetImage(JpegDecoder); + using var memoryStream = new MemoryStream(); + + // act + input.Save(memoryStream, JpegEncoder); + + // assert + memoryStream.Position = 0; + using var output = Image.Load(memoryStream); + JpegMetadata meta = output.Metadata.GetJpegMetadata(); + Assert.Equal(expectedColorType, meta.ColorType); + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg410, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg411, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Baseline.Jpeg422, PixelTypes.Rgba32)] + public void Encode_WithUnsupportedColorType_FromInputImage_DefaultsToYCbCr420(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // arrange + using Image input = provider.GetImage(JpegDecoder); + using var memoryStream = new MemoryStream(); + + // act + input.Save(memoryStream, new JpegEncoder() + { + Quality = 75 + }); + + // assert + memoryStream.Position = 0; + using var output = Image.Load(memoryStream); + JpegMetadata meta = output.Metadata.GetJpegMetadata(); + Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType); + } + + [Theory] + [InlineData(JpegColorType.Cmyk)] + [InlineData(JpegColorType.YCbCrRatio410)] + [InlineData(JpegColorType.YCbCrRatio411)] + [InlineData(JpegColorType.YCbCrRatio422)] + public void Encode_WithUnsupportedColorType_DefaultsToYCbCr420(JpegColorType colorType) { - var options = new JpegEncoder(); + // arrange + var jpegEncoder = new JpegEncoder() { ColorType = colorType }; + using var input = new Image(10, 10); + using var memoryStream = new MemoryStream(); + + // act + input.Save(memoryStream, jpegEncoder); + // assert + memoryStream.Position = 0; + using var output = Image.Load(memoryStream); + JpegMetadata meta = output.Metadata.GetJpegMetadata(); + Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType); + } + + [Theory] + [MemberData(nameof(QualityFiles))] + public void Encode_PreservesQuality(string imagePath, int quality) + { var testFile = TestFile.Create(imagePath); using (Image input = testFile.CreateRgba32Image()) { using (var memStream = new MemoryStream()) { - input.Save(memStream, options); + input.Save(memStream, JpegEncoder); memStream.Position = 0; using (var output = Image.Load(memStream)) @@ -83,16 +157,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] + public void EncodeBaseline_CalliphoraPartial(TestImageProvider provider, JpegColorType colorType, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality); + + [Theory] + [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 158, 24, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 153, 21, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 138, 24, PixelTypes.Rgba32)] + public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality); + + [Theory] + [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)] + [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 143, 81, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 48, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 46, 8, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)] - [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] - [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] - [WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)] - [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)] - public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegSubsample subsample, int quality) - where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, subsample, quality); + public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.12f)); [Theory] [WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)] @@ -102,46 +190,51 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)] public void EncodeBaseline_Grayscale(TestImageProvider provider, int quality) - where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, null, quality, JpegColorType.Luminance); + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, JpegColorType.Luminance, quality); + + [Theory] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 96, PixelTypes.Rgba32 | PixelTypes.Bgra32)] + public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality); [Theory] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] - public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegSubsample subsample, int quality) - where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, subsample, quality); + public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f)); [Theory] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegSubsample.Ratio444)] - [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegSubsample.Ratio444)] - [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegSubsample.Ratio420)] - [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegSubsample.Ratio420)] - public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegSubsample subsample) + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] + [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] + [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegColorType.YCbCrRatio420)] + [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegColorType.YCbCrRatio420)] + public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegColorType colorType) where TPixel : unmanaged, IPixel { - ImageComparer comparer = subsample == JpegSubsample.Ratio444 + ImageComparer comparer = colorType == JpegColorType.YCbCrRatio444 ? ImageComparer.TolerantPercentage(0.1f) : ImageComparer.TolerantPercentage(5f); provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); - TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer); + TestJpegEncoderCore(provider, colorType, 100, comparer); } /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// - private static ImageComparer GetComparer(int quality, JpegSubsample? subsample) + private static ImageComparer GetComparer(int quality, JpegColorType? colorType) { float tolerance = 0.015f; // ~1.5% if (quality < 50) { - tolerance *= 10f; + tolerance *= 4.5f; } - else if (quality < 75 || subsample == JpegSubsample.Ratio420) + else if (quality < 75 || colorType == JpegColorType.YCbCrRatio420) { - tolerance *= 5f; - if (subsample == JpegSubsample.Ratio420) + tolerance *= 2.0f; + if (colorType == JpegColorType.YCbCrRatio420) { - tolerance *= 2f; + tolerance *= 2.0f; } } @@ -150,9 +243,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private static void TestJpegEncoderCore( TestImageProvider provider, - JpegSubsample? subsample, + JpegColorType colorType = JpegColorType.YCbCrRatio420, int quality = 100, - JpegColorType colorType = JpegColorType.YCbCr, ImageComparer comparer = null) where TPixel : unmanaged, IPixel { @@ -163,13 +255,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var encoder = new JpegEncoder { - Subsample = subsample, Quality = quality, ColorType = colorType }; - string info = $"{subsample}-Q{quality}"; + string info = $"{colorType}-Q{quality}"; - comparer ??= GetComparer(quality, subsample); + comparer ??= GetComparer(quality, colorType); // Does DebugSave & load reference CompareToReferenceInput(): image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png"); @@ -225,14 +316,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) { - var options = new JpegEncoder(); - var testFile = TestFile.Create(imagePath); using (Image input = testFile.CreateRgba32Image()) { using (var memStream = new MemoryStream()) { - input.Save(memStream, options); + input.Save(memStream, JpegEncoder); memStream.Position = 0; using (var output = Image.Load(memStream)) @@ -253,11 +342,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using var input = new Image(1, 1); input.Metadata.IptcProfile = new IptcProfile(); input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test"); - var encoder = new JpegEncoder(); // act using var memStream = new MemoryStream(); - input.Save(memStream, encoder); + input.Save(memStream, JpegEncoder); // assert memStream.Position = 0; @@ -275,11 +363,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using var input = new Image(1, 1); input.Metadata.ExifProfile = new ExifProfile(); input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test"); - var encoder = new JpegEncoder(); // act using var memStream = new MemoryStream(); - input.Save(memStream, encoder); + input.Save(memStream, JpegEncoder); // assert memStream.Position = 0; @@ -296,11 +383,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // arrange using var input = new Image(1, 1); input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array); - var encoder = new JpegEncoder(); // act using var memStream = new MemoryStream(); - input.Save(memStream, encoder); + input.Save(memStream, JpegEncoder); // assert memStream.Position = 0; @@ -312,9 +398,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } [Theory] - [InlineData(JpegSubsample.Ratio420)] - [InlineData(JpegSubsample.Ratio444)] - public async Task Encode_IsCancellable(JpegSubsample subsample) + [InlineData(JpegColorType.YCbCrRatio420)] + [InlineData(JpegColorType.YCbCrRatio444)] + public async Task Encode_IsCancellable(JpegColorType colorType) { var cts = new CancellationTokenSource(); using var pausedStream = new PausedStream(new MemoryStream()); @@ -336,7 +422,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using var image = new Image(5000, 5000); await Assert.ThrowsAsync(async () => { - var encoder = new JpegEncoder() { Subsample = subsample }; + var encoder = new JpegEncoder() { ColorType = colorType }; await image.SaveAsync(pausedStream, encoder, cts.Token); }); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs index 56bf207b97..3f045dd1a0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var clone = (JpegMetadata)meta.DeepClone(); clone.Quality = 99; - clone.ColorType = JpegColorType.YCbCr; + clone.ColorType = JpegColorType.YCbCrRatio420; Assert.False(meta.Quality.Equals(clone.Quality)); Assert.False(meta.ColorType.Equals(clone.ColorType)); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 40b9e68677..1785f3dec4 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -21,10 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public class SpectralJpegTests { - public SpectralJpegTests(ITestOutputHelper output) - { - this.Output = output; - } + public SpectralJpegTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } @@ -46,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray(); [Theory(Skip = "Debug only, enable manually!")] - //[Theory] [WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)] public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider provider) where TPixel : unmanaged, IPixel diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 290b992c25..b64d22991f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -28,8 +28,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); [Theory] - [WithFile(Calliphora_RgbJpeg, PixelTypes.Rgba32)] - [WithFile(RgbJpeg, PixelTypes.Rgba32)] [WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] @@ -39,7 +37,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] [InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] - [InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Calliphora_GrayscaleUncompressed, 8, 200, 298, 96, 96, PixelResolutionUnit.PixelsPerInch)] [InlineData(Flower4BitPalette, 4, 73, 43, 72, 72, PixelResolutionUnit.PixelsPerInch)] public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit) { @@ -377,6 +375,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] + [WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)] + [WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)] + [WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + [Theory] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] public void DecodeMultiframe(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 712c8502ab..d7333c0b5a 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)] - [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.Jpeg)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works( @@ -288,6 +288,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits); + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithJpegCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, useExactComparer: false, compareTolerance: 0.012f); + [Theory] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeGray_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs index 9de3fc8dfe..4a47ac2365 100644 --- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks img.Dispose(); }, #pragma warning disable SA1515 // Single-line comment should be preceded by blank line - // ReSharper disable once ExplicitCallerInfoArgument + // ReSharper disable once ExplicitCallerInfoArgument $"Decode {fileName}"); #pragma warning restore SA1515 // Single-line comment should be preceded by blank line } @@ -92,11 +92,11 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks // Benchmark, enable manually! [Theory(Skip = ProfilingSetup.SkipProfilingTests)] - [InlineData(1, 75, JpegSubsample.Ratio420)] - [InlineData(30, 75, JpegSubsample.Ratio420)] - [InlineData(30, 75, JpegSubsample.Ratio444)] - [InlineData(30, 100, JpegSubsample.Ratio444)] - public void EncodeJpeg(int executionCount, int quality, JpegSubsample subsample) + [InlineData(1, 75, JpegColorType.YCbCrRatio420)] + [InlineData(30, 75, JpegColorType.YCbCrRatio420)] + [InlineData(30, 75, JpegColorType.YCbCrRatio444)] + [InlineData(30, 100, JpegColorType.YCbCrRatio444)] + public void EncodeJpeg(int executionCount, int quality, JpegColorType colorType) { // do not run this on CI even by accident if (TestEnvironment.RunsOnCI) @@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { foreach (Image img in testImages) { - var options = new JpegEncoder { Quality = quality, Subsample = subsample }; + var options = new JpegEncoder { Quality = quality, ColorType = colorType }; img.Save(ms, options); ms.Seek(0, SeekOrigin.Begin); } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index de2f2b64ee..d1a6624af2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -190,6 +190,10 @@ namespace SixLabors.ImageSharp.Tests public const string Jpeg420Exif = "Jpg/baseline/jpeg420exif.jpg"; public const string Jpeg444 = "Jpg/baseline/jpeg444.jpg"; public const string Jpeg420Small = "Jpg/baseline/jpeg420small.jpg"; + public const string JpegRgb = "Jpg/baseline/jpeg-rgb.jpg"; + public const string Jpeg410 = "Jpg/baseline/jpeg410.jpg"; + public const string Jpeg411 = "Jpg/baseline/jpeg411.jpg"; + public const string Jpeg422 = "Jpg/baseline/jpeg422.jpg"; public const string Testorig420 = "Jpg/baseline/testorig.jpg"; public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg"; @@ -558,7 +562,9 @@ namespace SixLabors.ImageSharp.Tests public const string RgbDeflate = "Tiff/rgb_deflate.tiff"; public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff"; public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff"; - public const string RgbJpeg = "Tiff/rgb_jpeg.tiff"; + public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff"; + public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff"; + public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff"; public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff"; public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff"; public const string RgbLzwNoPredictorMultistrip = "Tiff/rgb_lzw_noPredictor_multistrip.tiff"; @@ -606,6 +612,7 @@ namespace SixLabors.ImageSharp.Tests public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff"; public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff"; public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff"; + public const string YCbCrJpegCompressed = "Tiff/ycbcr_jpegcompressed.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; diff --git a/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg b/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg new file mode 100644 index 0000000000..2f2be0fa1a --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4630c33d722a89de5cb1834bffa43c729f204c0f8c95d4ec2127ddfcd433f60 +size 10100 diff --git a/tests/Images/Input/Jpg/baseline/jpeg410.jpg b/tests/Images/Input/Jpg/baseline/jpeg410.jpg new file mode 100644 index 0000000000..3bc41af8d8 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/jpeg410.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:318338c1a541227632a99cc47b04fc9f6d19c3e8300a76e71136edec2d17a5f5 +size 9073 diff --git a/tests/Images/Input/Jpg/baseline/jpeg411.jpg b/tests/Images/Input/Jpg/baseline/jpeg411.jpg new file mode 100644 index 0000000000..43a2ba49d8 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/jpeg411.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70315936e36bb1edf38fc950b7b321f20be5a2592db59bfd28d79dbc391a5aaf +size 4465 diff --git a/tests/Images/Input/Jpg/baseline/jpeg422.jpg b/tests/Images/Input/Jpg/baseline/jpeg422.jpg new file mode 100644 index 0000000000..4782b53b33 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/jpeg422.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be21207ecb96bcb0925706649678c522c68bf08ee26e6e8878456f4f7772dd31 +size 3951 diff --git a/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff index b0dbdde549..5b668ac513 100644 --- a/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff +++ b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed3b08730e5c34eb8d268f58d1e09efe2605398899bfd726cc3b35de21baa6ff -size 121196 +oid sha256:c35902ca485fba441230efa88f794ee5aafa9f75ad5e4a14cb3d592a0a98c538 +size 7760 diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff index d2761d2919..3592206bc5 100644 --- a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba35f1e43c8425f3bcfab682efae4d2c00c62f0d8a4b411e646d32047469526 -size 125802 +oid sha256:da7d98823c284d92982a88c4a51434bbc140dceac245a8a054c6e41a489d0cc7 +size 5986 diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff index 8bdbdcddc6..e0b4fa35e3 100644 --- a/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8c7f712f9e7d1feeeb55e7743f6ce7d66bc5292f4786ea8526d95057d73145e -size 4534 +oid sha256:118e55fe0f0dbb5d2712337ec9456a27407f11c0eb9f7e7e81700d4d84b7db09 +size 4378 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff index 621ef158aa..3909521203 100644 --- a/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff +++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2314b31ca9938fa8b11cbabda0b118a90025a45d2931fca9afa131c0d6919aca -size 557717 +oid sha256:d4d3541db6b7751d3225599aa822c3376fb27bb9dc2dd28674ef4f78bf426191 +size 83356 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff index f44a6e9343..e7fdef14bb 100644 --- a/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff +++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9576b3a49b84e26938a7e9ded5f43a1a3c3390bf4824803f5aaab8e00c1afb4 -size 630947 +oid sha256:d007429701cc20154e84909af6988414001e6e60fba5c995f67b2a04cad6e57b +size 41135 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff index b14eeba8d5..de85622968 100644 --- a/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff +++ b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f24fd8f36a4847fcb84a317de4fd2eacd5eb0c58ef4436d33919f0a6658d0d9 -size 698309 +oid sha256:649f0b8ad50a9465fdb447c411e33f20a8b367600e7c3af83c8f686f3ab3e6dc +size 47143 diff --git a/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff index 5db7ef564e..e6ff007d4a 100644 --- a/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff +++ b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0283f2be39a151ca3ed19be97ebe4a6b17978ed251dd4d0d568895865fec24c7 -size 964588 +oid sha256:5925388b374b75161ef49273e8c85c4f99713e5e3be380ea13a03895f47809ba +size 60001 diff --git a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff index e0a39d2483..1998b371cb 100644 --- a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff +++ b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a4f687de9925863b1c9f32f53b6c05fb121f9d7b02ff5869113c4745433f10d -size 124644 +oid sha256:f27e758bb72d5e03fdcf0b1c58c417e4a7928cbdf8d432dc5b9a7d8d7ee4d06b +size 5668 diff --git a/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff index 1592645c8e..1d3c9a7899 100644 --- a/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff +++ b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b70500348b1af7828c15e7782eaca105ff749136d7c45eb4cab8c5cd5269c3f6 -size 966134 +oid sha256:29d4b30265158a7cc651d75130843a7f5a7ebf8b2f0f4bb0cf86c82cbec7f6ec +size 61549 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff index c2ebed3649..0ded461400 100644 --- a/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff +++ b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da6e6a35a0bb0f5f2d49e3c5f0eb2deb7118718dd08844f66a6cb72f48b5c489 -size 1476294 +oid sha256:392e1269220e7e3feb9e2b256e82cce6a2394a5cabef042fdb10def6b24ff165 +size 111819 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff index c9f5fadee8..f45aacce44 100644 --- a/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff +++ b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba79ffac35e16208406e073492d770d3a77e51a47112aa02ab8fd98b5a8487b2 -size 198564 +oid sha256:4baf0f4c462e5bef71ab36f505dfff87a31bd1d25deabccd822a359c1075e08e +size 65748 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif deleted file mode 100644 index 7450522679..0000000000 --- a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:53006876fcdc655a794462de57eb6b56f4d0cdd3cb8b752c63328db0eb4aa3c1 -size 725085 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff new file mode 100644 index 0000000000..5e4cf8be9b --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d83d8a81ebb7337f00b319a8c37cfdef07423d6a61006411130e386238dd00dd +size 121907 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff index 99642af524..2fa884f36b 100644 --- a/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff +++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecb529e5e3e0eca6f5e407b034fa8ba67bb4b9068af9e9b30425b08d30a249c0 -size 1756355 +oid sha256:993207c34358165af5fac0d0b955b56cd79e9707c1c46344863e01cbc9c7707d +size 126695 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff index 862db0b39f..6fc2c9b213 100644 --- a/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff +++ b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59dbb48f10c40cbbd4f5617a9f57536790ce0b9a4cc241dc8d6257095598cb76 -size 2891292 +oid sha256:81e7456578510c85e5bebe8bc7c5796da6e2cd61f5bbe0a1f6bb46b8aee3d695 +size 179949 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff index 7ebd74d9d4..7fc5923153 100644 --- a/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff +++ b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acb46c990af78fcb0e63849f0f26acffe26833d5cfd2fc77a728baf92166eea3 -size 2893218 +oid sha256:bb25349a7f803aeafc47d8a05deca1a8afdc4bdc5a53e2916f68d5c3e7d8cad3 +size 179207 diff --git a/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff b/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff new file mode 100644 index 0000000000..b97ab8830d --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b81013d7b0a29ed1ac9c33e175e0c0e69494b93b2b65b692f16d9ea042b9d5d +size 7759 diff --git a/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff b/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff new file mode 100644 index 0000000000..2d43f97789 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f027d8c2ab0b244f04e51b9bf724eac0123e104a2446324496a08bdf5881922 +size 10550 diff --git a/tests/Images/Input/Tiff/rgb_jpeg.tiff b/tests/Images/Input/Tiff/rgb_jpegcompression.tiff similarity index 100% rename from tests/Images/Input/Tiff/rgb_jpeg.tiff rename to tests/Images/Input/Tiff/rgb_jpegcompression.tiff diff --git a/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff b/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff new file mode 100644 index 0000000000..18334be2af --- /dev/null +++ b/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27fa1d37cd62a9cf105a5e3e015e6a16a13ca7fa82f927a73d32847046c66073 +size 6136