diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index d3a5ea15b0..15fdb3dc4b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -247,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; + bool fullScan = this.frame.Progressive || !this.frame.Interleaved; this.frame.AllocateComponents(fullScan); if (this.frame.Progressive) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index da2d5da65a..308c52dbac 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; + bool fullScan = this.frame.Progressive || !this.frame.Interleaved; this.frame.AllocateComponents(fullScan); if (!this.frame.Progressive) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index db1febd399..200096e193 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount) { - this.Extended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9; + this.IsExtended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9; this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10; this.Precision = precision; @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Gets a value indicating whether the frame uses the extended specification. /// - public bool Extended { get; private set; } + public bool IsExtended { get; private set; } /// /// Gets a value indicating whether the frame uses the progressive specification. @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// This is true for progressive and baseline non-interleaved images. /// - public bool MultiScan { get; set; } + public bool Interleaved { get; set; } /// /// Gets the precision. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index d271287bca..583b7225de 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -135,7 +135,65 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder tables[tableConfig.DestinationIndex] = new HuffmanLut(tableConfig.Table); } - public void EncodeScanBaselineInterleaved(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + public void EncodeScanBaselineInterleaved(JpegEncodingColor color, JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + switch (color) + { + case JpegEncodingColor.YCbCrRatio444: + case JpegEncodingColor.Rgb: + this.EncodeScanBaselineInterleaved444(frame, converter, cancellationToken); + break; + case JpegEncodingColor.YCbCrRatio420: + this.EncodeScanBaselineInterleaved420(frame, converter, cancellationToken); + break; + default: + this.EncodeScanBaselineInterleavedArbitrarySampling(frame, converter, cancellationToken); + break; + } + + this.FlushRemainingBytes(); + } + + public void EncodeScanBaselineSingleComponent(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + JpegComponent component = frame.Components[0]; + + int mcuLines = frame.McusPerColumn; + int w = component.WidthInBlocks; + + ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + for (int i = 0; i < mcuLines; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Convert from pixels to spectral via given converter + converter.ConvertStrideBaseline(); + + // Encode spectral to binary + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: 0); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int k = 0; k < w; k++) + { + this.WriteBlock( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable, + ref acHuffmanTable); + + if (this.IsStreamFlushNeeded) + { + this.FlushToStream(); + } + } + } + } + + private void EncodeScanBaselineInterleavedArbitrarySampling(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int mcu = 0; @@ -154,7 +212,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { // Scan an interleaved mcu... process components in order int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < frame.ComponentCount; k++) + for (int k = 0; k < frame.Components.Length; k++) { JpegComponent component = frame.Components[k]; @@ -192,24 +250,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder } } } - - this.FlushRemainingBytes(); } - public void EncodeScanBaselineSingleComponent(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + private void EncodeScanBaselineInterleaved444(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - // DEBUG INITIALIZATION SETUP - frame.AllocateComponents(fullScan: false); + int mcusPerColumn = frame.McusPerColumn; + int mcusPerLine = frame.McusPerLine; - JpegComponent component = frame.Components[0]; - int mcuLines = frame.McusPerColumn; - int w = component.WidthInBlocks; - int h = component.SamplingFactors.Height; - ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; - ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + JpegComponent c2 = frame.Components[2]; + JpegComponent c1 = frame.Components[1]; + JpegComponent c0 = frame.Components[0]; - for (int i = 0; i < mcuLines; i++) + ref HuffmanLut c0dcHuffmanTable = ref this.dcHuffmanTables[c0.DcTableId]; + ref HuffmanLut c0acHuffmanTable = ref this.acHuffmanTables[c0.AcTableId]; + ref HuffmanLut c1dcHuffmanTable = ref this.dcHuffmanTables[c1.DcTableId]; + ref HuffmanLut c1acHuffmanTable = ref this.acHuffmanTables[c1.AcTableId]; + ref HuffmanLut c2dcHuffmanTable = ref this.dcHuffmanTables[c2.DcTableId]; + ref HuffmanLut c2acHuffmanTable = ref this.acHuffmanTables[c2.AcTableId]; + + ref Block8x8 c0BlockRef = ref MemoryMarshal.GetReference(c0.SpectralBlocks.DangerousGetRowSpan(y: 0)); + ref Block8x8 c1BlockRef = ref MemoryMarshal.GetReference(c1.SpectralBlocks.DangerousGetRowSpan(y: 0)); + ref Block8x8 c2BlockRef = ref MemoryMarshal.GetReference(c2.SpectralBlocks.DangerousGetRowSpan(y: 0)); + + for (int j = 0; j < mcusPerColumn; j++) { cancellationToken.ThrowIfCancellationRequested(); @@ -217,28 +281,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder converter.ConvertStrideBaseline(); // Encode spectral to binary - for (int j = 0; j < h; j++) + for (int i = 0; i < mcusPerLine; i++) { - Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); - ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + this.WriteBlock( + c0, + ref Unsafe.Add(ref c0BlockRef, i), + ref c0dcHuffmanTable, + ref c0acHuffmanTable); + + this.WriteBlock( + c1, + ref Unsafe.Add(ref c1BlockRef, i), + ref c1dcHuffmanTable, + ref c1acHuffmanTable); + + this.WriteBlock( + c2, + ref Unsafe.Add(ref c2BlockRef, i), + ref c2dcHuffmanTable, + ref c2acHuffmanTable); - for (int k = 0; k < w; k++) + if (this.IsStreamFlushNeeded) { - this.WriteBlock( - component, - ref Unsafe.Add(ref blockRef, k), - ref dcHuffmanTable, - ref acHuffmanTable); - - if (this.IsStreamFlushNeeded) - { - this.FlushToStream(); - } + this.FlushToStream(); } } } } + private void EncodeScanBaselineInterleaved420(JpegFrame frame, SpectralConverter converter, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + throw new NotImplementedException(); + } + private void WriteBlock( JpegComponent component, ref Block8x8 block, diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs index c20e3d8635..a6a9bcb0a9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.McusPerLine = (int)Numerics.DivideCeil((uint)image.Width, (uint)maxSubFactorH * 8); this.McusPerColumn = (int)Numerics.DivideCeil((uint)image.Height, (uint)maxSubFactorV * 8); - for (int i = 0; i < this.ComponentCount; i++) + for (int i = 0; i < this.Components.Length; i++) { JpegComponent component = this.Components[i]; component.Init(this, maxSubFactorH, maxSubFactorV); @@ -50,8 +50,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public int PixelWidth { get; private set; } - public int ComponentCount => this.Components.Length; - public JpegComponent[] Components { get; } public int McusPerLine { get; } @@ -70,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void AllocateComponents(bool fullScan) { - for (int i = 0; i < this.ComponentCount; i++) + for (int i = 0; i < this.Components.Length; i++) { JpegComponent component = this.Components[i]; component.AllocateSpectral(fullScan); diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index ff87d88eb5..de55904212 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -14,5 +14,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Defaults to 75. /// public int? Quality { get; set; } + + /// + /// Gets or sets the component encoding mode. + /// + /// + /// Interleaved encoding mode encodes all color components in a single scan. + /// Non-interleaved encoding mode encodes each color component in a separate scan. + /// + public bool? Interleaved { get; set; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 10e52066d1..25e31a2362 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -488,6 +488,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Read on. fileMarker = FindNextFileMarker(this.markerBuffer, stream); } + + this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved; } /// @@ -1178,6 +1180,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); + this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; remaining -= length; @@ -1386,7 +1389,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // selectorsCount*2 bytes: component index + huffman tables indices stream.Read(this.temp, 0, selectorsBytes); - this.Frame.MultiScan = this.Frame.ComponentCount != selectorsCount; + this.Frame.Interleaved = this.Frame.ComponentCount == selectorsCount; for (int i = 0; i < selectorsBytes; i += 2) { // 1 byte: Component id diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 5d0e964a1e..c2a50b37e2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -24,6 +24,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int? Quality { get; set; } + /// + public bool? Interleaved { get; set; } + /// /// Sets jpeg color for encoding. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 4318f9e09c..fe3fbb4d94 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -38,9 +38,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private readonly int? quality; - /// - /// Gets or sets the colorspace to use. - /// + private readonly bool? interleaved; + private JpegEncodingColor? colorType; private JpegFrameConfig frameConfig; @@ -60,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public JpegEncoderCore(IJpegEncoderOptions options, JpegFrameConfig frameConfig) { this.quality = options.Quality; + this.interleaved = options.Interleaved; this.frameConfig = frameConfig; this.colorType = frameConfig.EncodingColor; @@ -121,20 +121,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Write the quantization tables. this.WriteDefineQuantizationTables(this.frameConfig.QuantizationTables, jpegMetadata); - // Write the scan header. - this.WriteStartOfScan(this.frameConfig.Components.Length, this.frameConfig.Components); - var spectralConverter = new SpectralConverter(frame, image, this.QuantizationTables, Configuration.Default); - // TODO: change this for non-interleaved scans - frame.AllocateComponents(fullScan: false); - if (frame.ComponentCount > 1) + if (frame.Components.Length == 1) + { + frame.AllocateComponents(fullScan: false); + + this.WriteStartOfScan(this.frameConfig.Components); + this.scanEncoder.EncodeScanBaselineSingleComponent(frame, spectralConverter, cancellationToken); + } + else if (this.interleaved ?? jpegMetadata.Interleaved ?? true) { - this.scanEncoder.EncodeScanBaselineInterleaved(frame, spectralConverter, cancellationToken); + frame.AllocateComponents(fullScan: false); + + this.WriteStartOfScan(this.frameConfig.Components); + this.scanEncoder.EncodeScanBaselineInterleaved(this.frameConfig.EncodingColor, frame, spectralConverter, cancellationToken); } else { - this.scanEncoder.EncodeScanBaselineSingleComponent(frame, spectralConverter, cancellationToken); + throw new NotImplementedException(); } // Write the End Of Image marker. @@ -143,50 +148,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Flush(); } - /// - /// If color type was not set, set it based on the given image. - /// Note, if there is no metadata and the image has multiple components this method - /// returns defering the field assignment - /// to . - /// - private static JpegEncodingColor? GetFallbackColorType(Image image) - where TPixel : unmanaged, IPixel - { - // First inspect the image metadata. - JpegEncodingColor? colorType = null; - JpegMetadata metadata = image.Metadata.GetJpegMetadata(); - if (IsSupportedColorType(metadata.ColorType)) - { - return metadata.ColorType; - } - - // Secondly, inspect the pixel type. - // TODO: PixelTypeInfo should contain a component count! - bool isGrayscale = - typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) || - typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32); - - // We don't set multi-component color types here since we can set it based upon - // the quality in InitQuantizationTables. - if (isGrayscale) - { - colorType = JpegEncodingColor.Luminance; - } - - return colorType; - } - - /// - /// Returns true, if the color type is supported by the encoder. - /// - /// The color type. - /// true, if color type is supported. - private static bool IsSupportedColorType(JpegEncodingColor? colorType) - => colorType == JpegEncodingColor.YCbCrRatio444 - || colorType == JpegEncodingColor.YCbCrRatio420 - || colorType == JpegEncodingColor.Luminance - || colorType == JpegEncodingColor.Rgb; - /// /// Write the start of image marker. /// @@ -611,7 +572,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Writes the StartOfScan marker. /// - private void WriteStartOfScan(int componentCount, JpegComponentConfig[] components) + private void WriteStartOfScan(Span components) { // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // - the marker length "\x00\x0c", @@ -626,13 +587,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[1] = JpegConstants.Markers.SOS; // Length (high byte, low byte), must be 6 + 2 * (number of components in scan) - int sosSize = 6 + (2 * componentCount); + int sosSize = 6 + (2 * components.Length); this.buffer[2] = 0x00; this.buffer[3] = (byte)sosSize; - this.buffer[4] = (byte)componentCount; // Number of components in a scan + this.buffer[4] = (byte)components.Length; // Number of components in a scan // Components data - for (int i = 0; i < componentCount; i++) + for (int i = 0; i < components.Length; i++) { int i2 = 2 * i; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 6c34036793..b878d26fb8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -99,9 +99,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Gets or sets the color type. + /// Gets the color type. /// - public JpegEncodingColor? ColorType { get; set; } + public JpegEncodingColor? ColorType { get; internal set; } + + /// + /// Gets the component encoding mode. + /// + /// + /// Interleaved encoding mode encodes all color components in a single scan. + /// Non-interleaved encoding mode encodes each color component in a separate scan. + /// + public bool? Interleaved { get; internal set; } + + /// + /// Gets the scan encoding mode. + /// + /// + /// Progressive jpeg images encode component data across multiple scans. + /// + public bool? Progressive { get; internal set; } /// public IDeepCloneable DeepClone() => new JpegMetadata(this); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index c4a448ff8e..c5b3b3da50 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { // Due to underlying architecture, baseline interleaved jpegs would inject spectral data during parsing // Progressive and multi-scan images must be loaded manually - if (this.frame.Progressive || this.frame.MultiScan) + if (this.frame.Progressive || !this.frame.Interleaved) { LibJpegTools.ComponentData[] components = this.spectralData.Components; for (int i = 0; i < components.Length; i++)