mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
8.3 KiB
210 lines
8.3 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
using SixLabors.ImageSharp.Formats.Jpeg.Components;
|
|
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
|
|
using SixLabors.ImageSharp.IO;
|
|
using SixLabors.ImageSharp.Memory;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
|
|
using Xunit.Abstractions;
|
|
|
|
// ReSharper disable InconsistentNaming
|
|
namespace SixLabors.ImageSharp.Tests.Formats.Jpg;
|
|
|
|
[Trait("Format", "Jpg")]
|
|
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.Testorig420,
|
|
TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Baseline.Bad.BadEOF,
|
|
TestImages.Jpeg.Baseline.MultiScanBaselineCMYK
|
|
};
|
|
|
|
public static readonly string[] ProgressiveTestJpegs =
|
|
{
|
|
TestImages.Jpeg.Progressive.Fb, TestImages.Jpeg.Progressive.Progress,
|
|
TestImages.Jpeg.Progressive.Festzug, TestImages.Jpeg.Progressive.Bad.BadEOF,
|
|
TestImages.Jpeg.Progressive.Bad.ExifUndefType,
|
|
};
|
|
|
|
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
|
|
|
|
[Theory(Skip = "Debug only, enable manually!")]
|
|
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
|
|
public void Decoder_ParseStream_SaveSpectralResult<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
// Calculating data from ImageSharp
|
|
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
|
|
JpegDecoderOptions option = new();
|
|
|
|
using var decoder = new JpegDecoderCore(option);
|
|
using var ms = new MemoryStream(sourceBytes);
|
|
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
|
|
|
|
// internal scan decoder which we substitute to assert spectral correctness
|
|
var debugConverter = new DebugSpectralConverter<TPixel>();
|
|
var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default);
|
|
|
|
// This would parse entire image
|
|
decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default);
|
|
VerifyJpeg.SaveSpectralImage(provider, debugConverter.SpectralData);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
|
|
public void VerifySpectralCorrectness<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
if (!TestEnvironment.IsWindows)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Expected data from libjpeg
|
|
LibJpegTools.SpectralData libJpegData = LibJpegTools.ExtractSpectralData(provider.SourceFileOrDescription);
|
|
|
|
// Calculating data from ImageSharp
|
|
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes;
|
|
JpegDecoderOptions options = new();
|
|
|
|
using var decoder = new JpegDecoderCore(options);
|
|
using var ms = new MemoryStream(sourceBytes);
|
|
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
|
|
|
|
// internal scan decoder which we substitute to assert spectral correctness
|
|
var debugConverter = new DebugSpectralConverter<TPixel>();
|
|
|
|
// This would parse entire image
|
|
decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default);
|
|
|
|
// Actual verification
|
|
this.VerifySpectralCorrectnessImpl(libJpegData, debugConverter.SpectralData);
|
|
}
|
|
|
|
private void VerifySpectralCorrectnessImpl(
|
|
LibJpegTools.SpectralData libJpegData,
|
|
LibJpegTools.SpectralData imageSharpData)
|
|
{
|
|
bool equality = libJpegData.Equals(imageSharpData);
|
|
this.Output.WriteLine("Spectral data equality: " + equality);
|
|
|
|
int componentCount = imageSharpData.ComponentCount;
|
|
if (libJpegData.ComponentCount != componentCount)
|
|
{
|
|
throw new Exception("libJpegData.ComponentCount != componentCount");
|
|
}
|
|
|
|
double averageDifference = 0;
|
|
double totalDifference = 0;
|
|
double tolerance = 0;
|
|
|
|
this.Output.WriteLine("*** Differences ***");
|
|
for (int i = 0; i < componentCount; i++)
|
|
{
|
|
LibJpegTools.ComponentData libJpegComponent = libJpegData.Components[i];
|
|
LibJpegTools.ComponentData imageSharpComponent = imageSharpData.Components[i];
|
|
|
|
(double total, double average) = LibJpegTools.CalculateDifference(libJpegComponent, imageSharpComponent);
|
|
|
|
this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]");
|
|
averageDifference += average;
|
|
totalDifference += total;
|
|
Size s = libJpegComponent.SpectralBlocks.Size();
|
|
tolerance += s.Width * s.Height;
|
|
}
|
|
|
|
averageDifference /= componentCount;
|
|
|
|
tolerance /= 64; // fair enough?
|
|
|
|
this.Output.WriteLine($"AVERAGE: {averageDifference}");
|
|
this.Output.WriteLine($"TOTAL: {totalDifference}");
|
|
this.Output.WriteLine($"TOLERANCE = totalNumOfBlocks / 64 = {tolerance}");
|
|
|
|
Assert.True(totalDifference < tolerance);
|
|
}
|
|
|
|
private class DebugSpectralConverter<TPixel> : SpectralConverter
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
private JpegFrame frame;
|
|
|
|
private IRawJpegData jpegData;
|
|
|
|
private LibJpegTools.SpectralData spectralData;
|
|
|
|
private int baselineScanRowCounter;
|
|
|
|
public LibJpegTools.SpectralData SpectralData
|
|
{
|
|
get
|
|
{
|
|
// 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.Interleaved)
|
|
{
|
|
this.PrepareForDecoding();
|
|
LibJpegTools.ComponentData[] components = this.spectralData.Components;
|
|
for (int i = 0; i < components.Length; i++)
|
|
{
|
|
components[i].LoadSpectral(this.frame.Components[i]);
|
|
}
|
|
}
|
|
|
|
return this.spectralData;
|
|
}
|
|
}
|
|
|
|
public override void ConvertStrideBaseline()
|
|
{
|
|
// This would be called only for baseline non-interleaved images
|
|
// We must copy spectral strides here
|
|
LibJpegTools.ComponentData[] components = this.spectralData.Components;
|
|
for (int i = 0; i < components.Length; i++)
|
|
{
|
|
components[i].LoadSpectralStride(this.frame.Components[i].SpectralBlocks, this.baselineScanRowCounter);
|
|
}
|
|
|
|
this.baselineScanRowCounter++;
|
|
|
|
// As spectral buffers are reused for each stride decoding - we need to manually clear it like it's done in SpectralConverter<TPixel>
|
|
foreach (JpegComponent component in this.frame.Components)
|
|
{
|
|
Buffer2D<Block8x8> spectralBlocks = component.SpectralBlocks;
|
|
for (int i = 0; i < spectralBlocks.Height; i++)
|
|
{
|
|
spectralBlocks.DangerousGetRowSpan(i).Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool HasPixelBuffer() => throw new NotImplementedException();
|
|
|
|
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
|
|
{
|
|
this.frame = frame;
|
|
this.jpegData = jpegData;
|
|
}
|
|
|
|
public override void PrepareForDecoding()
|
|
{
|
|
var spectralComponents = new LibJpegTools.ComponentData[this.frame.ComponentCount];
|
|
for (int i = 0; i < spectralComponents.Length; i++)
|
|
{
|
|
JpegComponent component = this.frame.Components[i];
|
|
spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index);
|
|
}
|
|
|
|
this.spectralData = new LibJpegTools.SpectralData(spectralComponents);
|
|
}
|
|
}
|
|
}
|
|
|