Browse Source

Merge branch 'js/gif-fixes' into js/png-pallete

pull/2485/head
James Jackson-South 3 years ago
parent
commit
3325380449
  1. 2
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  2. 78
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 4
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  4. 26
      src/ImageSharp/IO/IFileSystem.cs
  5. 20
      src/ImageSharp/IO/LocalFileSystem.cs
  6. 8
      src/ImageSharp/Image.FromFile.cs
  7. 2
      src/ImageSharp/ImageExtensions.cs
  8. 109
      tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs
  9. 8
      tests/ImageSharp.Tests/Image/ImageSaveTests.cs
  10. 6
      tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs
  11. 44
      tests/ImageSharp.Tests/TestFileSystem.cs
  12. 4
      tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

2
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -640,7 +640,7 @@ internal static partial class SimdUtils
/// <param name="mask">The mask vector.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> BlendVariable(in Vector128<byte> left, in Vector128<byte> right, in Vector128<byte> mask)
public static Vector128<byte> BlendVariable(Vector128<byte> left, Vector128<byte> right, Vector128<byte> mask)
{
if (Sse41.IsSupported)
{

78
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -291,20 +291,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
// TODO: Consider an optimization that trims down the buffer to the minimum size required.
// We would use a process similar to entropy crop where we trim the buffer from the edges
// until we hit a non-transparent pixel.
this.WriteImageDescriptor(frame, useLocal, stream);
// Assign the correct buffer to compress.
// If we are using a local palette or it's the first run then we want to use the quantized frame.
Buffer2D<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
// Trim down the buffer to the minimum size required.
Buffer2DRegion<byte> region = TrimTransparentPixels(buffer, transparencyIndex);
this.WriteImageDescriptor(region.Rectangle, useLocal, stream);
if (useLocal)
{
this.WriteColorTable(quantized, stream);
}
// Assign the correct buffer to compress.
// If we are using a local palette or it's the first run then we want to use the quantized frame.
Buffer2D<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
this.WriteImageData(buffer, stream);
this.WriteImageData(region, stream);
// Swap the buffers.
(quantized, previousQuantized) = (previousQuantized, quantized);
@ -386,6 +386,44 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
private static Buffer2DRegion<byte> TrimTransparentPixels(Buffer2D<byte> buffer, int transparencyIndex)
{
if (transparencyIndex < 0)
{
return buffer.GetRegion();
}
byte trimmableIndex = unchecked((byte)transparencyIndex);
int top = int.MaxValue;
int bottom = int.MinValue;
int left = int.MaxValue;
int right = int.MinValue;
for (int y = 0; y < buffer.Height; y++)
{
Span<byte> rowSpan = buffer.DangerousGetRowSpan(y);
for (int x = 0; x < rowSpan.Length; x++)
{
if (rowSpan[x] != trimmableIndex)
{
top = Math.Min(top, y);
bottom = Math.Max(bottom, y);
left = Math.Min(left, x);
right = Math.Max(right, x);
}
}
}
if (top == int.MaxValue || bottom == int.MinValue)
{
// No valid rectangle found
return buffer.GetRegion();
}
return buffer.GetRegion(Rectangle.FromLTRB(left, top, right, bottom));
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
@ -583,7 +621,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (metadata is null)
{
metadata = new();
hasTransparency = transparencyIndex > -1;
hasTransparency = transparencyIndex >= 0;
}
else
{
@ -619,7 +657,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
if (extensionSize > 128)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
@ -642,14 +680,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
/// <summary>
/// Writes the image descriptor to the stream.
/// Writes the image frame descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="rectangle">The frame location and size.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream)
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
@ -658,10 +694,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
localColorTableSize: this.bitDepth - 1);
GifImageDescriptor descriptor = new(
left: 0,
top: 0,
width: (ushort)image.Width,
height: (ushort)image.Height,
left: (ushort)rectangle.X,
top: (ushort)rectangle.Y,
width: (ushort)rectangle.Width,
height: (ushort)rectangle.Height,
packed: packedValue);
Span<byte> buffer = stackalloc byte[20];
@ -697,9 +733,9 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <param name="indices">The <see cref="Buffer2D{Byte}"/> containing indexed pixels.</param>
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData(Buffer2D<byte> indices, Stream stream)
private void WriteImageData(Buffer2DRegion<byte> indices, Stream stream)
{
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(indices, stream);

4
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
/// </summary>
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
public void Encode(Buffer2D<byte> indexedPixels, Stream stream)
public void Encode(Buffer2DRegion<byte> indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="initialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</param>
private void Compress(Buffer2D<byte> indexedPixels, int initialBits, Stream stream)
private void Compress(Buffer2DRegion<byte> indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;

26
src/ImageSharp/IO/IFileSystem.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
internal interface IFileSystem
{
/// <summary>
/// Returns a readable stream as defined by the path.
/// Opens a file as defined by the path and returns it as a readable stream.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the file to open.</returns>
/// <returns>A stream representing the opened file.</returns>
Stream OpenRead(string path);
/// <summary>
/// Creates or opens a file and returns it as a writable stream as defined by the path.
/// Opens a file as defined by the path and returns it as a readable stream
/// that can be used for asynchronous reading.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the file to open.</returns>
/// <returns>A stream representing the opened file.</returns>
Stream OpenReadAsynchronous(string path);
/// <summary>
/// Creates or opens a file as defined by the path and returns it as a writable stream.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the opened file.</returns>
Stream Create(string path);
/// <summary>
/// Creates or opens a file as defined by the path and returns it as a writable stream
/// that can be used for asynchronous reading and writing.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the opened file.</returns>
Stream CreateAsynchronous(string path);
}

20
src/ImageSharp/IO/LocalFileSystem.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
/// <inheritdoc/>
public Stream OpenRead(string path) => File.OpenRead(path);
/// <inheritdoc/>
public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous,
});
/// <inheritdoc/>
public Stream Create(string path) => File.Create(path);
/// <inheritdoc/>
public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
Options = FileOptions.Asynchronous,
});
}

8
src/ImageSharp/Image.FromFile.cs

@ -72,7 +72,7 @@ public abstract partial class Image
{
Guard.NotNull(options, nameof(options));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -144,7 +144,7 @@ public abstract partial class Image
CancellationToken cancellationToken = default)
{
Guard.NotNull(options, nameof(options));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -214,7 +214,7 @@ public abstract partial class Image
string path,
CancellationToken cancellationToken = default)
{
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -291,7 +291,7 @@ public abstract partial class Image
Guard.NotNull(options, nameof(options));
Guard.NotNull(path, nameof(path));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync<TPixel>(options, stream, cancellationToken).ConfigureAwait(false);
}
}

2
src/ImageSharp/ImageExtensions.cs

@ -70,7 +70,7 @@ public static partial class ImageExtensions
Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder));
using Stream fs = source.GetConfiguration().FileSystem.Create(path);
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
}

109
tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
@ -11,36 +11,113 @@ public class LocalFileSystemTests
public void OpenRead()
{
string path = Path.GetTempFileName();
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
try
{
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
var fs = new LocalFileSystem();
LocalFileSystem fs = new();
using (var r = new StreamReader(fs.OpenRead(path)))
{
string data = r.ReadToEnd();
using (FileStream stream = (FileStream)fs.OpenRead(path))
using (StreamReader reader = new(stream))
{
Assert.False(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);
Assert.Equal(testData, data);
string data = reader.ReadToEnd();
Assert.Equal(testData, data);
}
}
finally
{
File.Delete(path);
}
}
File.Delete(path);
[Fact]
public async Task OpenReadAsynchronous()
{
string path = Path.GetTempFileName();
try
{
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
LocalFileSystem fs = new();
await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path))
using (StreamReader reader = new(stream))
{
Assert.True(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);
string data = await reader.ReadToEndAsync();
Assert.Equal(testData, data);
}
}
finally
{
File.Delete(path);
}
}
[Fact]
public void Create()
{
string path = Path.GetTempFileName();
string testData = Guid.NewGuid().ToString();
var fs = new LocalFileSystem();
try
{
string testData = Guid.NewGuid().ToString();
LocalFileSystem fs = new();
using (FileStream stream = (FileStream)fs.Create(path))
using (StreamWriter writer = new(stream))
{
Assert.False(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.True(stream.CanWrite);
using (var r = new StreamWriter(fs.Create(path)))
writer.Write(testData);
}
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
}
finally
{
r.Write(testData);
File.Delete(path);
}
}
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
[Fact]
public async Task CreateAsynchronous()
{
string path = Path.GetTempFileName();
try
{
string testData = Guid.NewGuid().ToString();
LocalFileSystem fs = new();
await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path))
await using (StreamWriter writer = new(stream))
{
Assert.True(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.True(stream.CanWrite);
await writer.WriteAsync(testData);
}
File.Delete(path);
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
}
finally
{
File.Delete(path);
}
}
}

8
tests/ImageSharp.Tests/Image/ImageSaveTests.cs

@ -44,7 +44,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SavePath()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream);
this.image.Save("path.png");
@ -54,7 +54,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SavePathWithEncoder()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream);
this.image.Save("path.jpg", this.encoderNotInFormat.Object);
@ -73,7 +73,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SaveStreamWithMime()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.image.Save(stream, this.localImageFormat.Object);
this.encoder.Verify(x => x.Encode(this.image, stream));
@ -82,7 +82,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SaveStreamWithEncoder()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.image.Save(stream, this.encoderNotInFormat.Object);

6
tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs

@ -122,6 +122,7 @@ public partial class ImageTests
Stream StreamFactory() => this.DataStream;
this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory);
this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory);
this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory);
this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object;
this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem;
@ -132,6 +133,11 @@ public partial class ImageTests
// Clean up the global object;
this.localStreamReturnImageRgba32?.Dispose();
this.localStreamReturnImageAgnostic?.Dispose();
if (this.dataStreamLazy.IsValueCreated)
{
this.dataStreamLazy.Value.Dispose();
}
}
protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker);

44
tests/ImageSharp.Tests/TestFileSystem.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable enable
namespace SixLabors.ImageSharp.Tests;
/// <summary>
@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests;
/// </summary>
public class TestFileSystem : ImageSharp.IO.IFileSystem
{
private readonly Dictionary<string, Func<Stream>> fileSystem = new Dictionary<string, Func<Stream>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Func<Stream>> fileSystem = new(StringComparer.OrdinalIgnoreCase);
public void AddFile(string path, Func<Stream> data)
{
@ -18,35 +20,39 @@ public class TestFileSystem : ImageSharp.IO.IFileSystem
}
}
public Stream Create(string path)
public Stream Create(string path) => this.GetStream(path) ?? File.Create(path);
public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
{
// if we have injected a fake file use it instead
lock (this.fileSystem)
{
if (this.fileSystem.ContainsKey(path))
{
Stream stream = this.fileSystem[path]();
stream.Position = 0;
return stream;
}
}
Mode = FileMode.Create,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
Options = FileOptions.Asynchronous,
});
return File.Create(path);
}
public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path);
public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous,
});
public Stream OpenRead(string path)
private Stream? GetStream(string path)
{
// if we have injected a fake file use it instead
lock (this.fileSystem)
{
if (this.fileSystem.ContainsKey(path))
if (this.fileSystem.TryGetValue(path, out Func<Stream>? streamFactory))
{
Stream stream = this.fileSystem[path]();
Stream stream = streamFactory();
stream.Position = 0;
return stream;
}
}
return File.OpenRead(path);
return null;
}
}

4
tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem
Stream IFileSystem.Create(string path) => this.stream;
Stream IFileSystem.CreateAsynchronous(string path) => this.stream;
Stream IFileSystem.OpenRead(string path) => this.stream;
Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream;
}

Loading…
Cancel
Save