Browse Source

Merge branch 'main' into heic-support

pull/2633/head
Ynse Hoornenborg 2 years ago
committed by GitHub
parent
commit
8f227bfa4f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  2. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  3. 201
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  4. 60
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  5. 96
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  6. 7
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  7. 11
      src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
  8. 7
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  9. 19
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  10. 29
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  11. 86
      src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs
  12. 25
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  13. 159
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  14. 40
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  15. 66
      tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs
  16. 58
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  17. 15
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  18. 36
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  19. 1
      tests/ImageSharp.Tests/TestImages.cs
  20. 3
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png
  21. 3
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png
  22. 3
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png
  23. 3
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png
  24. 3
      tests/Images/Input/Webp/issues/Issue2801.webp

12
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -575,7 +575,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 16 MaxColors = 16,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@ -623,7 +625,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 4 MaxColors = 4,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@ -680,7 +684,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 2 MaxColors = 2,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -119,6 +119,8 @@ internal class HuffmanScanDecoder : IJpegScanDecoder
this.frame.AllocateComponents(); this.frame.AllocateComponents();
this.todo = this.restartInterval;
if (!this.frame.Progressive) if (!this.frame.Progressive)
{ {
this.ParseBaselineData(); this.ParseBaselineData();

201
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
/// </remarks> /// </remarks>
private readonly byte[] streamWriteBuffer; private readonly byte[] streamWriteBuffer;
private readonly int restartInterval;
/// <summary> /// <summary>
/// Number of jagged bits stored in <see cref="accumulatedBits"/> /// Number of jagged bits stored in <see cref="accumulatedBits"/>
/// </summary> /// </summary>
@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class. /// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class.
/// </summary> /// </summary>
/// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param> /// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="outputStream">Output stream for saving encoded data.</param> /// <param name="outputStream">Output stream for saving encoded data.</param>
public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream) public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
{ {
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit; int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)]; this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
this.emitWriteIndex = this.emitBuffer.Length; this.emitWriteIndex = this.emitBuffer.Length;
this.restartInterval = restartInterval;
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier]; this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
this.target = outputStream; this.target = outputStream;
@ -211,6 +216,9 @@ internal class HuffmanScanEncoder
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++) for (int i = 0; i < h; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -221,6 +229,13 @@ internal class HuffmanScanEncoder
for (nuint k = 0; k < (uint)w; k++) for (nuint k = 0; k < (uint)w; k++)
{ {
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteBlock( this.WriteBlock(
component, component,
ref Unsafe.Add(ref blockRef, k), ref Unsafe.Add(ref blockRef, k),
@ -231,6 +246,133 @@ internal class HuffmanScanEncoder
{ {
this.FlushToStream(); this.FlushToStream();
} }
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the DC coefficients for a given component's blocks in a scan.
/// </summary>
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeDcScan(Component component, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteDc(
component,
ref Unsafe.Add(ref blockRef, k),
ref dcHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the AC coefficients for a specified range of blocks in a component's scan.
/// </summary>
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
int restarts = 0;
int restartsToGo = this.restartInterval;
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
}
this.WriteAcBlock(
ref Unsafe.Add(ref blockRef, k),
start,
end,
ref acHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
} }
} }
@ -250,6 +392,9 @@ internal class HuffmanScanEncoder
int mcusPerColumn = frame.McusPerColumn; int mcusPerColumn = frame.McusPerColumn;
int mcusPerLine = frame.McusPerLine; int mcusPerLine = frame.McusPerLine;
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int j = 0; j < mcusPerColumn; j++) for (int j = 0; j < mcusPerColumn; j++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -260,6 +405,16 @@ internal class HuffmanScanEncoder
// Encode spectral to binary // Encode spectral to binary
for (int i = 0; i < mcusPerLine; i++) for (int i = 0; i < mcusPerLine; i++)
{ {
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
foreach (var component in frame.Components)
{
component.DcPredictor = 0;
}
}
// Scan an interleaved mcu... process components in order // Scan an interleaved mcu... process components in order
int mcuCol = mcu % mcusPerLine; int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < frame.Components.Length; k++) for (int k = 0; k < frame.Components.Length; k++)
@ -300,6 +455,17 @@ internal class HuffmanScanEncoder
{ {
this.FlushToStream(); this.FlushToStream();
} }
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
} }
} }
@ -371,25 +537,29 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes(); this.FlushRemainingBytes();
} }
private void WriteBlock( private void WriteDc(
Component component, Component component,
ref Block8x8 block, ref Block8x8 block,
ref HuffmanLut dcTable, ref HuffmanLut dcTable)
ref HuffmanLut acTable)
{ {
// Emit the DC delta. // Emit the DC delta.
int dc = block[0]; int dc = block[0];
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor); this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc; component.DcPredictor = dc;
}
private void WriteAcBlock(
ref Block8x8 block,
nint start,
nint end,
ref HuffmanLut acTable)
{
// Emit the AC components. // Emit the AC components.
int[] acHuffTable = acTable.Values; int[] acHuffTable = acTable.Values;
nint lastValuableIndex = block.GetLastNonZeroIndex();
int runLength = 0; int runLength = 0;
ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block); ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block);
for (nint zig = 1; zig <= lastValuableIndex; zig++) for (nint zig = start; zig < end; zig++)
{ {
const int zeroRun1 = 1 << 4; const int zeroRun1 = 1 << 4;
const int zeroRun16 = 16 << 4; const int zeroRun16 = 16 << 4;
@ -413,14 +583,25 @@ internal class HuffmanScanEncoder
} }
// if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over
// this can be done for any number of trailing zeros, even when all 63 ac values are zero if (runLength > 0)
// (Block8x8F.Size - 1) == 63 - last index of the mcu elements
if (lastValuableIndex != Block8x8F.Size - 1)
{ {
this.EmitHuff(acHuffTable, 0x00); this.EmitHuff(acHuffTable, 0x00);
} }
} }
private void WriteBlock(
Component component,
ref Block8x8 block,
ref HuffmanLut dcTable,
ref HuffmanLut acTable)
{
this.WriteDc(component, ref block, ref dcTable);
this.WriteAcBlock(ref block, 1, 64, ref acTable);
}
private void WriteRestart(int restart) =>
this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
/// <summary> /// <summary>
/// Emits the most significant count of bits to the buffer. /// Emits the most significant count of bits to the buffer.
/// </summary> /// </summary>

60
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
/// </summary> /// </summary>
private int? quality; private int? quality;
/// <summary>
/// Backing field for <see cref="ProgressiveScans"/>
/// </summary>
private int progressiveScans = 4;
/// <summary>
/// Backing field for <see cref="RestartInterval"/>
/// </summary>
private int restartInterval;
/// <summary> /// <summary>
/// Gets the quality, that will be used to encode the image. Quality /// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min). /// index must be between 1 and 100 (compression from max to min).
@ -33,6 +43,56 @@ public sealed class JpegEncoder : ImageEncoder
} }
} }
/// <summary>
/// Gets a value indicating whether progressive encoding is used.
/// </summary>
public bool Progressive { get; init; }
/// <summary>
/// Gets number of scans per component for progressive encoding.
/// Defaults to <value>4</value>.
/// </summary>
/// <remarks>
/// Number of scans must be between 2 and 64.
/// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
/// </remarks>
/// <exception cref="ArgumentException">Progressive scans must be in [2..64] range.</exception>
public int ProgressiveScans
{
get => this.progressiveScans;
init
{
if (value is < 2 or > 64)
{
throw new ArgumentException("Progressive scans must be in [2..64] range.");
}
this.progressiveScans = value;
}
}
/// <summary>
/// Gets numbers of MCUs between restart markers.
/// Defaults to <value>0</value>.
/// </summary>
/// <remarks>
/// Currently supported in progressive encoding only.
/// </remarks>
/// <exception cref="ArgumentException">Restart interval must be in [0..65535] range.</exception>
public int RestartInterval
{
get => this.restartInterval;
init
{
if (value is < 0 or > 65535)
{
throw new ArgumentException("Restart interval must be in [0..65535] range.");
}
this.restartInterval = value;
}
}
/// <summary> /// <summary>
/// Gets the component encoding mode. /// Gets the component encoding mode.
/// </summary> /// </summary>

96
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -100,12 +100,15 @@ internal sealed unsafe partial class JpegEncoderCore
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables. // Write the Huffman tables.
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables. // Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer);
// Write define restart interval
this.WriteDri(this.encoder.RestartInterval, buffer);
// Write scans with actual pixel data // Write scans with actual pixel data
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables); using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
@ -426,6 +429,25 @@ internal sealed unsafe partial class JpegEncoderCore
} }
} }
/// <summary>
/// Writes the DRI marker
/// </summary>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteDri(int restartInterval, Span<byte> buffer)
{
if (restartInterval <= 0)
{
return;
}
this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);
buffer[1] = (byte)(restartInterval & 0xff);
buffer[0] = (byte)(restartInterval >> 8);
this.outputStream.Write(buffer, 0, 2);
}
/// <summary> /// <summary>
/// Writes the App1 header. /// Writes the App1 header.
/// </summary> /// </summary>
@ -563,7 +585,8 @@ internal sealed unsafe partial class JpegEncoderCore
// Length (high byte, low byte), 8 + components * 3. // Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length); int markerlen = 8 + (3 * components.Length);
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer); byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
this.WriteMarkerHeader(marker, markerlen, buffer);
buffer[5] = (byte)components.Length; buffer[5] = (byte)components.Length;
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
buffer[1] = (byte)(height >> 8); buffer[1] = (byte)(height >> 8);
@ -597,7 +620,17 @@ internal sealed unsafe partial class JpegEncoderCore
/// </summary> /// </summary>
/// <param name="components">The collecction of component configuration items.</param> /// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param> /// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) =>
this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="spectralStart">Start of spectral selection</param>
/// <param name="spectralEnd">End of spectral selection</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer, byte spectralStart, byte spectralEnd)
{ {
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - the marker length "\x00\x0c",
@ -630,8 +663,8 @@ internal sealed unsafe partial class JpegEncoderCore
buffer[i2 + 6] = (byte)tableSelectors; buffer[i2 + 6] = (byte)tableSelectors;
} }
buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection. buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
buffer[sosSize] = 0x3f; // Se - End of spectral selection. buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(buffer, 0, sosSize + 2); this.outputStream.Write(buffer, 0, sosSize + 2);
} }
@ -666,7 +699,14 @@ internal sealed unsafe partial class JpegEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (frame.Components.Length == 1) if (this.encoder.Progressive)
{
frame.AllocateComponents(fullScan: true);
spectralConverter.ConvertFull();
this.WriteProgressiveScans<TPixel>(frame, frameConfig, encoder, buffer, cancellationToken);
}
else if (frame.Components.Length == 1)
{ {
frame.AllocateComponents(fullScan: false); frame.AllocateComponents(fullScan: false);
@ -694,6 +734,50 @@ internal sealed unsafe partial class JpegEncoderCore
} }
} }
/// <summary>
/// Writes the progressive scans
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="frame">The current frame.</param>
/// <param name="frameConfig">The frame configuration.</param>
/// <param name="encoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void WriteProgressiveScans<TPixel>(
JpegFrame frame,
JpegFrameConfig frameConfig,
HuffmanScanEncoder encoder,
Span<byte> buffer,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Span<JpegComponentConfig> components = frameConfig.Components;
// Phase 1: DC scan
for (int i = 0; i < frame.Components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
encoder.EncodeDcScan(frame.Components[i], cancellationToken);
}
// Phase 2: AC scans
int acScans = this.encoder.ProgressiveScans - 1;
int valuesPerScan = 64 / acScans;
for (int scan = 0; scan < acScans; scan++)
{
int start = Math.Max(1, scan * valuesPerScan);
int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
for (int i = 0; i < components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
}
}
}
/// <summary> /// <summary>
/// Writes the header for a marker with the given length. /// Writes the header for a marker with the given length.
/// </summary> /// </summary>

7
src/ImageSharp/Formats/Webp/AlphaDecoder.cs

@ -183,7 +183,7 @@ internal class AlphaDecoder : IDisposable
else else
{ {
this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span);
this.ExtractAlphaRows(this.Vp8LDec); this.ExtractAlphaRows(this.Vp8LDec, this.Width);
} }
} }
@ -257,14 +257,15 @@ internal class AlphaDecoder : IDisposable
/// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet. /// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet.
/// </summary> /// </summary>
/// <param name="dec">The VP8L decoder.</param> /// <param name="dec">The VP8L decoder.</param>
private void ExtractAlphaRows(Vp8LDecoder dec) /// <param name="width">The image width.</param>
private void ExtractAlphaRows(Vp8LDecoder dec, int width)
{ {
int numRowsToProcess = dec.Height; int numRowsToProcess = dec.Height;
int width = dec.Width;
Span<uint> input = dec.Pixels.Memory.Span; Span<uint> input = dec.Pixels.Memory.Span;
Span<byte> output = this.Alpha.Memory.Span; Span<byte> output = this.Alpha.Memory.Span;
// Extract alpha (which is stored in the green plane). // Extract alpha (which is stored in the green plane).
// the final width (!= dec->width_)
int pixelCount = width * numRowsToProcess; int pixelCount = width * numRowsToProcess;
WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator);
ExtractGreen(input, output, pixelCount); ExtractGreen(input, output, pixelCount);

11
src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs

@ -269,7 +269,11 @@ internal static unsafe class LosslessUtils
/// </summary> /// </summary>
/// <param name="transform">The transform data contains color table size and the entries in the color table.</param> /// <param name="transform">The transform data contains color table size and the entries in the color table.</param>
/// <param name="pixelData">The pixel data to apply the reverse transform on.</param> /// <param name="pixelData">The pixel data to apply the reverse transform on.</param>
public static void ColorIndexInverseTransform(Vp8LTransform transform, Span<uint> pixelData) /// <param name="outputSpan">The resulting pixel data with the reversed transformation data.</param>
public static void ColorIndexInverseTransform(
Vp8LTransform transform,
Span<uint> pixelData,
Span<uint> outputSpan)
{ {
int bitsPerPixel = 8 >> transform.Bits; int bitsPerPixel = 8 >> transform.Bits;
int width = transform.XSize; int width = transform.XSize;
@ -282,7 +286,6 @@ internal static unsafe class LosslessUtils
int countMask = pixelsPerByte - 1; int countMask = pixelsPerByte - 1;
int bitMask = (1 << bitsPerPixel) - 1; int bitMask = (1 << bitsPerPixel) - 1;
uint[] decodedPixelData = new uint[width * height];
int pixelDataPos = 0; int pixelDataPos = 0;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
@ -298,12 +301,12 @@ internal static unsafe class LosslessUtils
packedPixels = GetArgbIndex(pixelData[pixelDataPos++]); packedPixels = GetArgbIndex(pixelData[pixelDataPos++]);
} }
decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; outputSpan[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)];
packedPixels >>= bitsPerPixel; packedPixels >>= bitsPerPixel;
} }
} }
decodedPixelData.AsSpan().CopyTo(pixelData); outputSpan.CopyTo(pixelData);
} }
else else
{ {

7
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -684,6 +684,7 @@ internal sealed class WebpLosslessDecoder
List<Vp8LTransform> transforms = decoder.Transforms; List<Vp8LTransform> transforms = decoder.Transforms;
for (int i = transforms.Count - 1; i >= 0; i--) for (int i = transforms.Count - 1; i >= 0; i--)
{ {
// TODO: Review these 1D allocations. They could conceivably exceed limits.
Vp8LTransform transform = transforms[i]; Vp8LTransform transform = transforms[i];
switch (transform.TransformType) switch (transform.TransformType)
{ {
@ -701,7 +702,11 @@ internal sealed class WebpLosslessDecoder
LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); LosslessUtils.ColorSpaceInverseTransform(transform, pixelData);
break; break;
case Vp8LTransformType.ColorIndexingTransform: case Vp8LTransformType.ColorIndexingTransform:
LosslessUtils.ColorIndexInverseTransform(transform, pixelData); using (IMemoryOwner<uint> output = memoryAllocator.Allocate<uint>(transform.XSize * transform.YSize, AllocationOptions.Clean))
{
LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan());
}
break; break;
} }
} }

19
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary> /// </summary>
public class AffineTransformBuilder public class AffineTransformBuilder
{ {
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new(); private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class. /// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
@ -301,7 +301,8 @@ public class AffineTransformBuilder
/// </summary> /// </summary>
/// <param name="sourceSize">The source image size.</param> /// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); public Matrix3x2 BuildMatrix(Size sourceSize)
=> this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
/// <summary> /// <summary>
/// Returns the combined transform matrix for a given source rectangle. /// Returns the combined transform matrix for a given source rectangle.
@ -345,18 +346,8 @@ public class AffineTransformBuilder
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle) public Size GetTransformedSize(Rectangle sourceRectangle)
{ {
Size size = sourceRectangle.Size; Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
// Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Func<Size, Matrix3x2> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace);
} }
private static void CheckDegenerate(Matrix3x2 matrix) private static void CheckDegenerate(Matrix3x2 matrix)

29
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -61,12 +61,12 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
if (matrix.Equals(Matrix3x2.Identity)) if (matrix.Equals(Matrix3x2.Identity))
{ {
// The clone will be blank here copy all the pixel data over // The clone will be blank here copy all the pixel data over
var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++) for (int y = 0; y < sourceBuffer.Height; y++)
{ {
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y));
} }
return; return;
@ -77,7 +77,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
if (sampler is NearestNeighborResampler) if (sampler is NearestNeighborResampler)
{ {
var nnOperation = new NNAffineOperation( NNAffineOperation nnOperation = new(
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
destination.PixelBuffer, destination.PixelBuffer,
@ -91,7 +91,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
return; return;
} }
var operation = new AffineOperation<TResampler>( AffineOperation<TResampler> operation = new(
configuration, configuration,
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
@ -128,17 +128,17 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y) public void Invoke(int y)
{ {
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y); Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destRow.Length; x++) for (int x = 0; x < destinationRowSpan.Length; x++)
{ {
var point = Vector2.Transform(new Vector2(x, y), this.matrix); Vector2 point = Vector2.Transform(new Vector2(x, y), this.matrix);
int px = (int)MathF.Round(point.X); int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y); int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py)) if (this.bounds.Contains(px, py))
{ {
destRow[x] = this.source.GetElementUnsafe(px, py); destinationRowSpan[x] = this.source.GetElementUnsafe(px, py);
} }
} }
} }
@ -195,16 +195,16 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y); Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4( PixelOperations<TPixel>.Instance.ToVector4(
this.configuration, this.configuration,
rowSpan, destinationRowSpan,
span, span,
PixelConversionModifiers.Scale); PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++) for (int x = 0; x < span.Length; x++)
{ {
var point = Vector2.Transform(new Vector2(x, y), matrix); Vector2 point = Vector2.Transform(new Vector2(x, y), matrix);
float pY = point.Y; float pY = point.Y;
float pX = point.X; float pX = point.X;
@ -221,13 +221,14 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
Vector4 sum = Vector4.Zero; Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++) for (int yK = top; yK <= bottom; yK++)
{ {
Span<TPixel> sourceRowSpan = this.source.DangerousGetRowSpan(yK);
float yWeight = sampler.GetValue(yK - pY); float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++) for (int xK = left; xK <= right; xK++)
{ {
float xWeight = sampler.GetValue(xK - pX); float xWeight = sampler.GetValue(xK - pX);
Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Vector4 current = sourceRowSpan[xK].ToScaledVector4();
Numerics.Premultiply(ref current); Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight; sum += current * xWeight * yWeight;
} }
@ -240,7 +241,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
PixelOperations<TPixel>.Instance.FromVector4Destructive( PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration, this.configuration,
span, span,
rowSpan, destinationRowSpan,
PixelConversionModifiers.Scale); PixelConversionModifiers.Scale);
} }
} }

86
src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs

@ -0,0 +1,86 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;
/// <summary>
/// Represents a solver for systems of linear equations using the Gaussian Elimination method.
/// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector.
/// This implementation is based on: <see href="https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html"/>
/// </summary>
internal static class GaussianEliminationSolver
{
/// <summary>
/// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination.
/// </summary>
/// <param name="matrix">The square matrix representing the coefficients of the linear equations.</param>
/// <param name="result">The vector representing the constants on the right-hand side of the linear equations.</param>
/// <exception cref="Exception">Thrown if the matrix is singular and cannot be solved.</exception>
/// <remarks>
/// The matrix passed to this method must be a square matrix.
/// If the matrix is singular (i.e., has no unique solution), an <see cref="NotSupportedException"/> will be thrown.
/// </remarks>
public static void Solve(double[][] matrix, double[] result)
{
TransformToRowEchelonForm(matrix, result);
ApplyBackSubstitution(matrix, result);
}
private static void TransformToRowEchelonForm(double[][] matrix, double[] result)
{
int colCount = matrix.Length;
int rowCount = matrix[0].Length;
int pivotRow = 0;
for (int pivotCol = 0; pivotCol < colCount; pivotCol++)
{
double maxValue = double.Abs(matrix[pivotRow][pivotCol]);
int maxIndex = pivotRow;
for (int r = pivotRow + 1; r < rowCount; r++)
{
double value = double.Abs(matrix[r][pivotCol]);
if (value > maxValue)
{
maxIndex = r;
maxValue = value;
}
}
if (matrix[maxIndex][pivotCol] == 0)
{
throw new NotSupportedException("Matrix is singular and cannot be solve");
}
(matrix[pivotRow], matrix[maxIndex]) = (matrix[maxIndex], matrix[pivotRow]);
(result[pivotRow], result[maxIndex]) = (result[maxIndex], result[pivotRow]);
for (int r = pivotRow + 1; r < rowCount; r++)
{
double fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol];
for (int c = pivotCol + 1; c < colCount; c++)
{
matrix[r][c] -= matrix[pivotRow][c] * fraction;
}
result[r] -= result[pivotRow] * fraction;
matrix[r][pivotCol] = 0;
}
pivotRow++;
}
}
private static void ApplyBackSubstitution(double[][] matrix, double[] result)
{
int rowCount = matrix[0].Length;
for (int row = rowCount - 1; row >= 0; row--)
{
result[row] /= matrix[row][row];
for (int r = 0; r < row; r++)
{
result[r] -= result[row] * matrix[r][row];
}
}
}
}

25
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -61,12 +61,12 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (matrix.Equals(Matrix4x4.Identity)) if (matrix.Equals(Matrix4x4.Identity))
{ {
// The clone will be blank here copy all the pixel data over // The clone will be blank here copy all the pixel data over
var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++) for (int y = 0; y < sourceBuffer.Height; y++)
{ {
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y));
} }
return; return;
@ -77,7 +77,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (sampler is NearestNeighborResampler) if (sampler is NearestNeighborResampler)
{ {
var nnOperation = new NNProjectiveOperation( NNProjectiveOperation nnOperation = new(
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
destination.PixelBuffer, destination.PixelBuffer,
@ -91,7 +91,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
return; return;
} }
var operation = new ProjectiveOperation<TResampler>( ProjectiveOperation<TResampler> operation = new(
configuration, configuration,
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
@ -128,9 +128,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y) public void Invoke(int y)
{ {
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y); Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destRow.Length; x++) for (int x = 0; x < destinationRowSpan.Length; x++)
{ {
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix); Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X); int px = (int)MathF.Round(point.X);
@ -138,7 +138,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (this.bounds.Contains(px, py)) if (this.bounds.Contains(px, py))
{ {
destRow[x] = this.source.GetElementUnsafe(px, py); destinationRowSpan[x] = this.source.GetElementUnsafe(px, py);
} }
} }
} }
@ -195,10 +195,10 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y); Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4( PixelOperations<TPixel>.Instance.ToVector4(
this.configuration, this.configuration,
rowSpan, destinationRowSpan,
span, span,
PixelConversionModifiers.Scale); PixelConversionModifiers.Scale);
@ -221,13 +221,14 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
Vector4 sum = Vector4.Zero; Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++) for (int yK = top; yK <= bottom; yK++)
{ {
Span<TPixel> sourceRowSpan = this.source.DangerousGetRowSpan(yK);
float yWeight = sampler.GetValue(yK - pY); float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++) for (int xK = left; xK <= right; xK++)
{ {
float xWeight = sampler.GetValue(xK - pX); float xWeight = sampler.GetValue(xK - pX);
Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Vector4 current = sourceRowSpan[xK].ToScaledVector4();
Numerics.Premultiply(ref current); Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight; sum += current * xWeight * yWeight;
} }
@ -240,7 +241,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive( PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration, this.configuration,
span, span,
rowSpan, destinationRowSpan,
PixelConversionModifiers.Scale); PixelConversionModifiers.Scale);
} }
} }

159
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -3,6 +3,7 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -278,6 +279,91 @@ internal static class TransformUtils
return matrix; return matrix;
} }
/// <summary>
/// Computes the projection matrix for a quad distortion transformation.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="topLeft">The top-left point of the distorted quad.</param>
/// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the matrix.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks>
/// This method is based on the algorithm described in the following article:
/// <see href="https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/"/>
/// </remarks>
public static Matrix4x4 CreateQuadDistortionMatrix(
Rectangle rectangle,
PointF topLeft,
PointF topRight,
PointF bottomRight,
PointF bottomLeft,
TransformSpace transformSpace)
{
PointF p1 = new(rectangle.X, rectangle.Y);
PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y);
PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height);
PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height);
PointF q1 = topLeft;
PointF q2 = topRight;
PointF q3 = bottomRight;
PointF q4 = bottomLeft;
double[][] matrixData =
[
[p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X],
[0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y],
[p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X],
[0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y],
[p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X],
[0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y],
[p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X],
[0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y],
];
double[] b =
[
q1.X,
q1.Y,
q2.X,
q2.Y,
q3.X,
q3.Y,
q4.X,
q4.Y,
];
GaussianEliminationSolver.Solve(matrixData, b);
#pragma warning disable SA1117
Matrix4x4 projectionMatrix = new(
(float)b[0], (float)b[3], 0, (float)b[6],
(float)b[1], (float)b[4], 0, (float)b[7],
0, 0, 1, 0,
(float)b[2], (float)b[5], 0, 1);
#pragma warning restore SA1117
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
if (transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(projectionMatrix))
{
if (projectionMatrix.M41 != 0)
{
projectionMatrix.M41--;
}
if (projectionMatrix.M42 != 0)
{
projectionMatrix.M42--;
}
}
return projectionMatrix;
}
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Returns the size relative to the source for the given transformation matrix.
/// </summary> /// </summary>
@ -293,15 +379,16 @@ internal static class TransformUtils
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <returns> /// <returns>
/// The <see cref="Size"/>. /// The <see cref="Size"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Matrix4x4 matrix, Size size) public static Size GetTransformedSize(Matrix4x4 matrix, Size size, TransformSpace transformSpace)
{ {
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) if (matrix.IsIdentity || matrix.Equals(default))
{ {
return size; return size;
} }
@ -309,27 +396,7 @@ internal static class TransformUtils
// Check if the matrix involves only affine transformations by inspecting the relevant components. // Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include // We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image. // any perspective effects, non-standard scaling, or unusual translations that could distort the image.
// The conditions are as follows: bool usePixelSpace = transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(matrix);
bool usePixelSpace =
// 1. Ensure there's no perspective distortion:
// M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0.
(matrix.M34 == 0) &&
// 2. Ensure standard affine transformation without any unusual depth or perspective scaling:
// M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth
// scaling or perspective, which suggests a more complex transformation.
(matrix.M44 == 1) &&
// 3. Ensure no unusual translation in the x-direction:
// M14 represents translation in the x-direction that might be part of a more complex transformation.
// For standard affine transformations, M14 should be 0.
(matrix.M14 == 0) &&
// 4. Ensure no unusual translation in the y-direction:
// M24 represents translation in the y-direction that might be part of a more complex transformation.
// For standard affine transformations, M24 should be 0.
(matrix.M24 == 0);
// Define an offset size to translate between pixel space and coordinate space. // Define an offset size to translate between pixel space and coordinate space.
// When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates. // When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
@ -376,7 +443,7 @@ internal static class TransformUtils
{ {
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) if (matrix.IsIdentity || matrix.Equals(default))
{ {
return size; return size;
} }
@ -412,7 +479,7 @@ internal static class TransformUtils
/// </returns> /// </returns>
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
{ {
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) if (matrix.IsIdentity || rectangle.Equals(default))
{ {
bounds = default; bounds = default;
return false; return false;
@ -439,7 +506,7 @@ internal static class TransformUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
{ {
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) if (matrix.IsIdentity || rectangle.Equals(default))
{ {
bounds = default; bounds = default;
return false; return false;
@ -492,4 +559,44 @@ internal static class TransformUtils
(int)Math.Ceiling(right), (int)Math.Ceiling(right),
(int)Math.Ceiling(bottom)); (int)Math.Ceiling(bottom));
} }
private static bool IsAffineRotationOrSkew(Matrix4x4 matrix)
{
const float epsilon = 1e-6f;
// Check if the matrix is affine (last column should be [0, 0, 0, 1])
if (Math.Abs(matrix.M14) > epsilon ||
Math.Abs(matrix.M24) > epsilon ||
Math.Abs(matrix.M34) > epsilon ||
Math.Abs(matrix.M44 - 1f) > epsilon)
{
return false;
}
// Translation component (M41, m42) are allowed, others are not.
if (Math.Abs(matrix.M43) > epsilon)
{
return false;
}
// Extract the linear (rotation and skew) part of the matrix
// Upper-left 3x3 matrix
float m11 = matrix.M11, m12 = matrix.M12, m13 = matrix.M13;
float m21 = matrix.M21, m22 = matrix.M22, m23 = matrix.M23;
float m31 = matrix.M31, m32 = matrix.M32, m33 = matrix.M33;
// Compute the determinant of the linear part
float determinant = (m11 * ((m22 * m33) - (m23 * m32))) -
(m12 * ((m21 * m33) - (m23 * m31))) +
(m13 * ((m21 * m32) - (m22 * m31)));
// Check if the determinant is approximately ±1 (no scaling)
if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon)
{
return false;
}
// All checks passed; the matrix represents rotation and/or skew (with possible translation)
return true;
}
} }

40
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary> /// </summary>
public class ProjectiveTransformBuilder public class ProjectiveTransformBuilder
{ {
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new(); private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class. /// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
@ -279,6 +279,30 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder AppendTranslation(Vector2 position) public ProjectiveTransformBuilder AppendTranslation(Vector2 position)
=> this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0)));
/// <summary>
/// Prepends a quad distortion matrix using the specified corner points.
/// </summary>
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
/// <summary>
/// Appends a quad distortion matrix using the specified corner points.
/// </summary>
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Append(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
/// <summary> /// <summary>
/// Prepends a raw matrix. /// Prepends a raw matrix.
/// </summary> /// </summary>
@ -361,18 +385,8 @@ public class ProjectiveTransformBuilder
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle) public Size GetTransformedSize(Rectangle sourceRectangle)
{ {
Size size = sourceRectangle.Size; Matrix4x4 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
// Translate the origin matrix to cater for source rectangle offsets.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
foreach (Func<Size, Matrix4x4> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(matrix, size);
} }
private static void CheckDegenerate(Matrix4x4 matrix) private static void CheckDegenerate(Matrix4x4 matrix)

66
tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs

@ -0,0 +1,66 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;
namespace SixLabors.ImageSharp.Tests.Common;
public class GaussianEliminationSolverTest
{
[Theory]
[MemberData(nameof(MatrixTestData))]
public void CanSolve(double[][] matrix, double[] result, double[] expected)
{
GaussianEliminationSolver.Solve(matrix, result);
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(result[i], expected[i], 4);
}
}
public static TheoryData<double[][], double[], double[]> MatrixTestData
{
get
{
TheoryData<double[][], double[], double[]> data = [];
{
double[][] matrix =
[
[2, 3, 4],
[1, 2, 3],
[3, -4, 0],
];
double[] result = [6, 4, 10];
double[] expected = [18 / 11f, -14 / 11f, 18 / 11f];
data.Add(matrix, result, expected);
}
{
double[][] matrix =
[
[1, 4, -1],
[2, 5, 8],
[1, 3, -3],
];
double[] result = [4, 15, 1];
double[] expected = [1, 1, 1];
data.Add(matrix, result, expected);
}
{
double[][] matrix =
[
[-1, 0, 0],
[0, 1, 0],
[0, 0, 1],
];
double[] result = [1, 2, 3];
double[] expected = [-1, 2, 3];
data.Add(matrix, result, expected);
}
return data;
}
}
}

58
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -160,6 +160,64 @@ public partial class JpegEncoderTests
TestJpegEncoderCore(provider, colorType, 100, comparer); TestJpegEncoderCore(provider, colorType, 100, comparer);
} }
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_DefaultNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
JpegEncoder encoder = new()
{
Quality = quality,
ColorType = colorType,
Progressive = true
};
string info = $"{colorType}-Q{quality}";
ImageComparer comparer = new TolerantImageComparer(tolerance);
// Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_CustomNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
JpegEncoder encoder = new()
{
Quality = quality,
ColorType = colorType,
Progressive = true,
ProgressiveScans = 4,
RestartInterval = 7
};
string info = $"{colorType}-Q{quality}";
using MemoryStream ms = new();
image.SaveAsJpeg(ms, encoder);
ms.Position = 0;
// TEMP: Save decoded output as PNG so we can do a pixel compare.
using Image<TPixel> image2 = Image.Load<TPixel>(ms);
image2.DebugSave(provider, testOutputDetails: info, extension: "png");
ImageComparer comparer = new TolerantImageComparer(tolerance);
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}
[Theory] [Theory]
[InlineData(JpegColorType.YCbCrRatio420)] [InlineData(JpegColorType.YCbCrRatio420)]
[InlineData(JpegColorType.YCbCrRatio444)] [InlineData(JpegColorType.YCbCrRatio444)]

15
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -516,6 +516,21 @@ public class WebpEncoderTests
image.VerifyEncoder(provider, "webp", string.Empty, encoder); image.VerifyEncoder(provider, "webp", string.Empty, encoder);
} }
// https://github.com/SixLabors/ImageSharp/issues/2801
[Theory]
[WithFile(Lossy.Issue2801, PixelTypes.Rgba32)]
public void WebpDecoder_CanDecode_Issue2801<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
WebpEncoder encoder = new()
{
Quality = 100
};
using Image<TPixel> image = provider.GetImage();
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.TolerantPercentage(0.0994F));
}
public static void RunEncodeLossy_WithPeakImage() public static void RunEncodeLossy_WithPeakImage()
{ {
TestImageProvider<Rgba32> provider = TestImageProvider<Rgba32>.File(TestImageLossyFullPath); TestImageProvider<Rgba32> provider = TestImageProvider<Rgba32>.File(TestImageLossyFullPath);

36
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -55,6 +55,14 @@ public class ProjectiveTransformTests
{ TaperSide.Right, TaperCorner.RightOrBottom }, { TaperSide.Right, TaperCorner.RightOrBottom },
}; };
public static readonly TheoryData<PointF, PointF, PointF, PointF> QuadDistortionData = new()
{
{ new PointF(0, 0), new PointF(150, 0), new PointF(150, 150), new PointF(0, 150) }, // source == destination
{ new PointF(25, 50), new PointF(210, 25), new PointF(140, 210), new PointF(15, 125) }, // Distortion
{ new PointF(-50, -50), new PointF(200, -50), new PointF(200, 200), new PointF(-50, 200) }, // Scaling
{ new PointF(150, 0), new PointF(150, 150), new PointF(0, 150), new PointF(0, 0) }, // Rotation
};
public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output; public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output;
[Theory] [Theory]
@ -93,6 +101,24 @@ public class ProjectiveTransformTests
} }
} }
[Theory]
[WithTestPatternImages(nameof(QuadDistortionData), 150, 150, PixelTypes.Rgba32)]
public void Transform_WithQuadDistortion<TPixel>(TestImageProvider<TPixel> provider, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft);
image.Mutate(i => i.Transform(builder));
FormattableString testOutputDetails = $"{topLeft}-{topRight}-{bottomRight}-{bottomLeft}";
image.DebugSave(provider, testOutputDetails);
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
}
}
[Theory] [Theory]
[WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)] [WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)]
public void RawTransformMatchesDocumentedExample<TPixel>(TestImageProvider<TPixel> provider) public void RawTransformMatchesDocumentedExample<TPixel>(TestImageProvider<TPixel> provider)
@ -128,11 +154,11 @@ public class ProjectiveTransformTests
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
#pragma warning disable SA1117 // Parameters should be on same line or separate lines #pragma warning disable SA1117 // Parameters should be on same line or separate lines
Matrix4x4 matrix = new( Matrix4x4 matrix = new(
0.260987f, -0.434909f, 0, -0.0022184f, 0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f, 0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0, 0, 0, 1, 0,
52, 165, 0, 1); 52, 165, 0, 1);
#pragma warning restore SA1117 // Parameters should be on same line or separate lines #pragma warning restore SA1117 // Parameters should be on same line or separate lines
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()

1
tests/ImageSharp.Tests/TestImages.cs

@ -827,6 +827,7 @@ public static class TestImages
public const string Issue2257 = "Webp/issues/Issue2257.webp"; public const string Issue2257 = "Webp/issues/Issue2257.webp";
public const string Issue2670 = "Webp/issues/Issue2670.webp"; public const string Issue2670 = "Webp/issues/Issue2670.webp";
public const string Issue2763 = "Webp/issues/Issue2763.png"; public const string Issue2763 = "Webp/issues/Issue2763.png";
public const string Issue2801 = "Webp/issues/Issue2801.webp";
} }
} }

3
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abce6af307a81a8ebac8e502142b00b2615403b5570c8dbe7b6895cfdd1a6d60
size 66879

3
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4cda265a50aa26711efafdbcd947c9a01eff872611df5298920583f9a3d4224
size 26458

3
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:278a488a858b8eda141493fe00c617eb1f664196853da8341d7e5b7f231ddce4
size 24645

3
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e03e79e6fab3a9e43041e54640a04c7cc3677709e7d879f9f410cf8afc7547a7
size 42691

3
tests/Images/Input/Webp/issues/Issue2801.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e90a0d853ddf70d823d8da44eb6c57081e955b1fb7f436a1fd88ca5e5c75a003
size 261212
Loading…
Cancel
Save