From 590042601e41507d5e1ec3ac3bbb33a98c01fdb4 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 20 Aug 2017 04:51:56 +0200 Subject: [PATCH] introduced LibJpegTools, made FrameComponent a class again --- .../PdfJsPort/Components/FrameComponent.cs | 3 +- .../Formats/Jpeg/PdfJsPort/JpegDecoderCore.cs | 100 ++++---- .../Formats/Jpg/LibJpegTools.cs | 238 ++++++++++++++++++ .../Formats/Jpg/SpectralJpegTests.cs | 89 +++++++ .../ImageSharp.Tests/ImageSharp.Tests.csproj | 1 + 5 files changed, 384 insertions(+), 47 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/LibJpegTools.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/FrameComponent.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/FrameComponent.cs index 1bb78a84db..4bc66406bb 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/FrameComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/Components/FrameComponent.cs @@ -9,8 +9,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components /// /// Represents a single frame component /// - internal struct FrameComponent : IDisposable + internal class FrameComponent : IDisposable { + #pragma warning disable SA1401 // Fields should be private /// /// Gets or sets the component Id /// diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/JpegDecoderCore.cs index dc8d477f01..3df47f5245 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/JpegDecoderCore.cs @@ -16,6 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// internal sealed class JpegDecoderCore : IDisposable { +#pragma warning disable SA1401 // Fields should be private /// /// The global configuration /// @@ -34,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort private HuffmanTables acHuffmanTables; - private Frame frame; + internal Frame Frame; private ComponentBlocks components; @@ -42,11 +43,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort private ushort resetInterval; - private int imageWidth; + internal int ImageWidth { get; private set; } - private int imageHeight; + internal int ImageHeight { get; private set; } - private int numberOfComponents; + internal int NumberOfComponents { get; private set; } /// /// Whether the image has a EXIF header @@ -137,12 +138,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort public Image Decode(Stream stream) where TPixel : struct, IPixel { - this.InputStream = stream; - - var metadata = new ImageMetaData(); - this.ParseStream(metadata, false); + ImageMetaData metadata = this.ParseStream(stream); - var image = new Image(this.configuration, this.imageWidth, this.imageHeight, metadata); + var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, metadata); this.FillPixelData(image); this.AssignResolution(image); return image; @@ -151,7 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// public void Dispose() { - this.frame?.Dispose(); + this.Frame?.Dispose(); this.components?.Dispose(); this.quantizationTables?.Dispose(); this.dcHuffmanTables?.Dispose(); @@ -159,13 +157,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort this.pixelArea.Dispose(); // Set large fields to null. - this.frame = null; + this.Frame = null; this.components = null; this.quantizationTables = null; this.dcHuffmanTables = null; this.acHuffmanTables = null; } + internal ImageMetaData ParseStream(Stream stream) + { + this.InputStream = stream; + + var metadata = new ImageMetaData(); + this.ParseStream(metadata, false); + return metadata; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetBlockBufferOffset(ref Component component, int row, int col) { @@ -262,18 +269,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); } - this.imageWidth = this.frame.SamplesPerLine; - this.imageHeight = this.frame.Scanlines; - this.components = new ComponentBlocks { Components = new Component[this.frame.ComponentCount] }; + this.ImageWidth = this.Frame.SamplesPerLine; + this.ImageHeight = this.Frame.Scanlines; + this.components = new ComponentBlocks { Components = new Component[this.Frame.ComponentCount] }; for (int i = 0; i < this.components.Components.Length; i++) { - ref var frameComponent = ref this.frame.Components[i]; + ref var frameComponent = ref this.Frame.Components[i]; var component = new Component { Scale = new System.Numerics.Vector2( - frameComponent.HorizontalFactor / (float)this.frame.MaxHorizontalFactor, - frameComponent.VerticalFactor / (float)this.frame.MaxVerticalFactor), + frameComponent.HorizontalFactor / (float)this.Frame.MaxHorizontalFactor, + frameComponent.VerticalFactor / (float)this.Frame.MaxVerticalFactor), BlocksPerLine = frameComponent.BlocksPerLine, BlocksPerColumn = frameComponent.BlocksPerColumn }; @@ -282,7 +289,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort this.components.Components[i] = component; } - this.numberOfComponents = this.components.Components.Length; + this.NumberOfComponents = this.components.Components.Length; } /// @@ -293,21 +300,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort private void FillPixelData(Image image) where TPixel : struct, IPixel { - if (this.numberOfComponents > 4) + if (this.NumberOfComponents > 4) { - throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numberOfComponents}"); + throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.NumberOfComponents}"); } - this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numberOfComponents); + this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.NumberOfComponents); this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height); - if (this.numberOfComponents == 1) + if (this.NumberOfComponents == 1) { this.FillGrayScaleImage(image); return; } - if (this.numberOfComponents == 3) + if (this.NumberOfComponents == 3) { if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr) { @@ -319,7 +326,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort } } - if (this.numberOfComponents == 4) + if (this.NumberOfComponents == 4) { if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck) { @@ -601,14 +608,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// The current frame marker. private void ProcessStartOfFrameMarker(int remaining, FileMarker frameMarker) { - if (this.frame != null) + if (this.Frame != null) { throw new ImageFormatException("Multiple SOF markers. Only single frame jpegs supported."); } this.InputStream.Read(this.temp, 0, remaining); - this.frame = new Frame + this.Frame = new Frame { Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, @@ -623,10 +630,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort int index = 6; // No need to pool this. They max out at 4 - this.frame.ComponentIds = new byte[this.frame.ComponentCount]; - this.frame.Components = new FrameComponent[this.frame.ComponentCount]; + this.Frame.ComponentIds = new byte[this.Frame.ComponentCount]; + this.Frame.Components = new FrameComponent[this.Frame.ComponentCount]; - for (int i = 0; i < this.frame.Components.Length; i++) + for (int i = 0; i < this.Frame.Components.Length; i++) { int h = this.temp[index + 1] >> 4; int v = this.temp[index + 1] & 15; @@ -641,19 +648,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort maxV = v; } - ref var component = ref this.frame.Components[i]; + var component = new FrameComponent(); + this.Frame.Components[i] = component; component.Id = this.temp[index]; component.HorizontalFactor = h; component.VerticalFactor = v; component.QuantizationIdentifier = this.temp[index + 2]; - this.frame.ComponentIds[i] = component.Id; + this.Frame.ComponentIds[i] = component.Id; index += 3; } - this.frame.MaxHorizontalFactor = maxH; - this.frame.MaxVerticalFactor = maxV; + this.Frame.MaxHorizontalFactor = maxH; + this.Frame.MaxVerticalFactor = maxV; this.PrepareComponents(); } @@ -729,9 +737,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort componentIndex = -1; int selector = this.InputStream.ReadByte(); - for (int j = 0; j < this.frame.ComponentIds.Length; j++) + for (int j = 0; j < this.Frame.ComponentIds.Length; j++) { - byte id = this.frame.ComponentIds[j]; + byte id = this.Frame.ComponentIds[j]; if (selector == id) { componentIndex = j; @@ -743,7 +751,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort throw new ImageFormatException("Unknown component selector"); } - ref FrameComponent component = ref this.frame.Components[componentIndex]; + ref FrameComponent component = ref this.Frame.Components[componentIndex]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; @@ -757,11 +765,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort var scanDecoder = default(ScanDecoder); scanDecoder.DecodeScan( - this.frame, + this.Frame, this.InputStream, this.dcHuffmanTables, this.acHuffmanTables, - this.frame.Components, + this.Frame.Components, componentIndex, selectorsCount, this.resetInterval, @@ -827,14 +835,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort /// private void PrepareComponents() { - int mcusPerLine = (int)MathF.Ceiling(this.frame.SamplesPerLine / 8F / this.frame.MaxHorizontalFactor); - int mcusPerColumn = (int)MathF.Ceiling(this.frame.Scanlines / 8F / this.frame.MaxVerticalFactor); + int mcusPerLine = (int)MathF.Ceiling(this.Frame.SamplesPerLine / 8F / this.Frame.MaxHorizontalFactor); + int mcusPerColumn = (int)MathF.Ceiling(this.Frame.Scanlines / 8F / this.Frame.MaxVerticalFactor); - for (int i = 0; i < this.frame.ComponentCount; i++) + for (int i = 0; i < this.Frame.ComponentCount; i++) { - ref var component = ref this.frame.Components[i]; - int blocksPerLine = (int)MathF.Ceiling(MathF.Ceiling(this.frame.SamplesPerLine / 8F) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); - int blocksPerColumn = (int)MathF.Ceiling(MathF.Ceiling(this.frame.Scanlines / 8F) * component.VerticalFactor / this.frame.MaxVerticalFactor); + ref var component = ref this.Frame.Components[i]; + int blocksPerLine = (int)MathF.Ceiling(MathF.Ceiling(this.Frame.SamplesPerLine / 8F) * component.HorizontalFactor / this.Frame.MaxHorizontalFactor); + int blocksPerColumn = (int)MathF.Ceiling(MathF.Ceiling(this.Frame.Scanlines / 8F) * component.VerticalFactor / this.Frame.MaxVerticalFactor); int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; @@ -846,8 +854,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort component.BlocksPerColumn = blocksPerColumn; } - this.frame.McusPerLine = mcusPerLine; - this.frame.McusPerColumn = mcusPerColumn; + this.Frame.McusPerLine = mcusPerLine; + this.Frame.McusPerColumn = mcusPerColumn; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/LibJpegTools.cs b/tests/ImageSharp.Tests/Formats/Jpg/LibJpegTools.cs new file mode 100644 index 0000000000..a6efa3f2fb --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/LibJpegTools.cs @@ -0,0 +1,238 @@ +namespace SixLabors.ImageSharp.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Numerics; + using System.Reflection; + + using BitMiracle.LibJpeg.Classic; + + using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; + using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort.Components; + using SixLabors.ImageSharp.PixelFormats; + + using Xunit; + + internal static class LibJpegTools + { + public unsafe struct Block + { + public Block(short[] data) + { + this.Data = data; + } + + public short[] Data { get; } + + //public fixed short Data[64]; + + //public Block8x8(short[] data) + //{ + // fixed (short* p = Data) + // { + // for (int i = 0; i < 64; i++) + // { + // p[i] = data[i]; + // } + // } + //} + + public short this[int x, int y] + { + get => this.Data[y * 8 + x]; + set => this.Data[y * 8 + x] = value; + } + } + + public class SpectralData + { + public int ComponentCount { get; private set; } + + public ComponentData[] Components { get; private set; } + + private SpectralData(Array wholeImage) + { + this.ComponentCount = 0; + + for (int i = 0; i < wholeImage.Length && wholeImage.GetValue(i) != null; i++) + { + this.ComponentCount++; + } + + this.Components = new ComponentData[this.ComponentCount]; + + for (int i = 0; i < this.ComponentCount; i++) + { + object jVirtArray = wholeImage.GetValue(i); + Array bloxSource = (Array)GetNonPublicMember(jVirtArray, "m_buffer"); + + this.Components[i] = ComponentData.Load(bloxSource, i); + } + } + + private SpectralData(ComponentData[] components) + { + this.ComponentCount = components.Length; + this.Components = components; + } + + public static SpectralData Load(jpeg_decompress_struct cinfo) + { + //short[][][] result = new short[cinfo.Image_height][][]; + //int blockPerMcu = (int)GetNonPublicMember(cinfo, "m_blocks_in_MCU"); + //int mcuPerRow = (int)GetNonPublicMember(cinfo, "m_MCUs_per_row"); + //int mcuRows = (int)GetNonPublicMember(cinfo, "m_MCU_rows_in_scan"); + + object coefController = GetNonPublicMember(cinfo, "m_coef"); + Array wholeImage = (Array)GetNonPublicMember(coefController, "m_whole_image"); + + var result = new SpectralData(wholeImage); + + return result; + } + + public static SpectralData Load(Stream fileStream) + { + jpeg_error_mgr err = new jpeg_error_mgr(); + jpeg_decompress_struct cinfo = new jpeg_decompress_struct(err); + + cinfo.jpeg_stdio_src(fileStream); + cinfo.jpeg_read_header(true); + cinfo.Buffered_image = true; + cinfo.Do_block_smoothing = false; + + cinfo.jpeg_start_decompress(); + + var output = CreateOutputArray(cinfo); + for (int scan = 0; scan < cinfo.Input_scan_number; scan++) + { + cinfo.jpeg_start_output(scan); + for (int i = 0; i < cinfo.Image_height; i++) + { + int numScanlines = cinfo.jpeg_read_scanlines(output, 1); + if (numScanlines != 1) throw new Exception("?"); + } + } + + var result = SpectralData.Load(cinfo); + return result; + } + + private static byte[][] CreateOutputArray(jpeg_decompress_struct cinfo) + { + byte[][] output = new byte[cinfo.Image_height][]; + for (int i = 0; i < cinfo.Image_height; i++) + { + output[i] = new byte[cinfo.Image_width * cinfo.Num_components]; + } + return output; + } + + public static SpectralData LoadFromImageSharpDecoder(JpegDecoderCore decoder) + { + FrameComponent[] srcComponents = decoder.Frame.Components; + + ComponentData[] destComponents = new ComponentData[srcComponents.Length]; + throw new NotImplementedException(); + } + } + + public class ComponentData + { + public ComponentData(int yCount, int xCount, int index) + { + this.YCount = yCount; + this.XCount = xCount; + this.Index = index; + this.Blocks = new Block[this.YCount, this.XCount]; + } + + public int Index { get; } + + public int YCount { get; } + + public int XCount { get; } + + public Block[,] Blocks { get; private set; } + + public short MinVal { get; private set; } = short.MaxValue; + + public short MaxVal { get; private set; } = short.MinValue; + + public static ComponentData Load(Array bloxSource, int index) + { + int yCount = bloxSource.Length; + Array row0 = (Array)bloxSource.GetValue(0); + int xCount = row0.Length; + ComponentData result = new ComponentData(yCount, xCount, index); + result.Init(bloxSource); + return result; + } + + private void Init(Array bloxSource) + { + for (int i = 0; i < bloxSource.Length; i++) + { + Array row = (Array)bloxSource.GetValue(i); + for (int j = 0; j < row.Length; j++) + { + object jBlock = row.GetValue(j); + short[] data = (short[])GetNonPublicMember(jBlock, "data"); + this.MinVal = Math.Min(this.MinVal, data.Min()); + this.MaxVal = Math.Max(this.MaxVal, data.Max()); + this.Blocks[i, j] = new Block(data); + } + } + } + + public Image CreateGrayScaleImage() + { + Image result = new Image(this.XCount * 8, this.YCount * 8); + + for (int by = 0; by < this.YCount; by++) + { + for (int bx = 0; bx < this.XCount; bx++) + { + WriteToImage(this.Blocks[by, bx], bx, by, result); + } + } + return result; + } + + private void WriteToImage(Block block, int bx, int by, Image image) + { + float d = (this.MaxVal - this.MinVal); + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < 8; x++) + { + int yy = by * 8 + y; + int xx = bx * 8 + x; + float val = block[x, y]; + val -= this.MinVal; + val /= d; + + Vector4 v = new Vector4(val, val, val, 1); + Rgba32 color = default(Rgba32); + color.PackFromVector4(v); + + image[xx, yy] = color; + } + } + } + } + + internal static FieldInfo GetNonPublicField(object obj, string fieldName) + { + Type type = obj.GetType(); + return type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + } + + internal static object GetNonPublicMember(object obj, string fieldName) + { + FieldInfo fi = GetNonPublicField(obj, fieldName); + return fi.GetValue(obj); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs new file mode 100644 index 0000000000..eeb879b8cc --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -0,0 +1,89 @@ +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests +{ + using System.IO; + + using SixLabors.ImageSharp.Formats; + using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; + using SixLabors.ImageSharp.PixelFormats; + + using Xunit; + using Xunit.Abstractions; + + public class SpectralJpegTests + { + public SpectralJpegTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + public static readonly string[] BaselineTestJpegs = + { + TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk, + TestImages.Jpeg.Baseline.Jpeg400, TestImages.Jpeg.Baseline.Jpeg444, + TestImages.Jpeg.Baseline.Testimgorig, + TestImages.Jpeg.Baseline.Bad.BadEOF, + TestImages.Jpeg.Baseline.Bad.ExifUndefType, + }; + + public static readonly string[] ProgressiveTestJpegs = + { + TestImages.Jpeg.Progressive.Fb, TestImages.Jpeg.Progressive.Progress, + TestImages.Jpeg.Progressive.Festzug, TestImages.Jpeg.Progressive.Bad.BadEOF + }; + + [Theory] + [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] + public void BuildLibJpegSpectralResult(TestImageProvider provider) + where TPixel : struct, IPixel + { + byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; + + using (var ms = new MemoryStream(sourceBytes)) + { + LibJpegTools.SpectralData data = LibJpegTools.SpectralData.Load(ms); + Assert.True(data.ComponentCount > 0); + this.Output.WriteLine($"ComponentCount: {data.ComponentCount}"); + + this.SaveSpectralImage(provider, data); + } + } + + [Theory] + [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] + public void JpegDecoderCore_ParseStream_SaveSpectralResult(TestImageProvider provider) + where TPixel : struct, IPixel + { + JpegDecoderCore decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + + byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; + + using (var ms = new MemoryStream(sourceBytes)) + { + decoder.ParseStream(ms); + + var data = LibJpegTools.SpectralData.LoadFromImageSharpDecoder(decoder); + this.SaveSpectralImage(provider, data); + } + } + + private void SaveSpectralImage(TestImageProvider provider, LibJpegTools.SpectralData data) + where TPixel : struct, IPixel + { + foreach (LibJpegTools.ComponentData comp in data.Components) + { + this.Output.WriteLine("Min: " + comp.MinVal); + this.Output.WriteLine("MAx: " + comp.MaxVal); + + using (Image image = comp.CreateGrayScaleImage()) + { + string details = $"C{comp.Index}"; + image.DebugSave(provider, details, appendPixelTypeToFileName: false); + } + } + } + + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index e8a6e8c596..f936fa9841 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -16,6 +16,7 @@ +