Skip to content

Commit 3325380

Browse files
Merge branch 'js/gif-fixes' into js/png-pallete
2 parents d7709ee + f33f67d commit 3325380

12 files changed

Lines changed: 237 additions & 74 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ public static Vector256<float> MultiplyAddNegated(
640640
/// <param name="mask">The mask vector.</param>
641641
/// <returns>The <see cref="Vector256{T}"/>.</returns>
642642
[MethodImpl(MethodImplOptions.AggressiveInlining)]
643-
public static Vector128<byte> BlendVariable(in Vector128<byte> left, in Vector128<byte> right, in Vector128<byte> mask)
643+
public static Vector128<byte> BlendVariable(Vector128<byte> left, Vector128<byte> right, Vector128<byte> mask)
644644
{
645645
if (Sse41.IsSupported)
646646
{

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -291,20 +291,20 @@ private void EncodeFrame<TPixel>(
291291

292292
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
293293

294-
// TODO: Consider an optimization that trims down the buffer to the minimum size required.
295-
// We would use a process similar to entropy crop where we trim the buffer from the edges
296-
// until we hit a non-transparent pixel.
297-
this.WriteImageDescriptor(frame, useLocal, stream);
294+
// Assign the correct buffer to compress.
295+
// If we are using a local palette or it's the first run then we want to use the quantized frame.
296+
Buffer2D<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
297+
298+
// Trim down the buffer to the minimum size required.
299+
Buffer2DRegion<byte> region = TrimTransparentPixels(buffer, transparencyIndex);
300+
this.WriteImageDescriptor(region.Rectangle, useLocal, stream);
298301

299302
if (useLocal)
300303
{
301304
this.WriteColorTable(quantized, stream);
302305
}
303306

304-
// Assign the correct buffer to compress.
305-
// If we are using a local palette or it's the first run then we want to use the quantized frame.
306-
Buffer2D<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
307-
this.WriteImageData(buffer, stream);
307+
this.WriteImageData(region, stream);
308308

309309
// Swap the buffers.
310310
(quantized, previousQuantized) = (previousQuantized, quantized);
@@ -386,6 +386,44 @@ private static void DeDuplicatePixels<TPixel>(
386386
}
387387
}
388388

389+
private static Buffer2DRegion<byte> TrimTransparentPixels(Buffer2D<byte> buffer, int transparencyIndex)
390+
{
391+
if (transparencyIndex < 0)
392+
{
393+
return buffer.GetRegion();
394+
}
395+
396+
byte trimmableIndex = unchecked((byte)transparencyIndex);
397+
398+
int top = int.MaxValue;
399+
int bottom = int.MinValue;
400+
int left = int.MaxValue;
401+
int right = int.MinValue;
402+
403+
for (int y = 0; y < buffer.Height; y++)
404+
{
405+
Span<byte> rowSpan = buffer.DangerousGetRowSpan(y);
406+
for (int x = 0; x < rowSpan.Length; x++)
407+
{
408+
if (rowSpan[x] != trimmableIndex)
409+
{
410+
top = Math.Min(top, y);
411+
bottom = Math.Max(bottom, y);
412+
left = Math.Min(left, x);
413+
right = Math.Max(right, x);
414+
}
415+
}
416+
}
417+
418+
if (top == int.MaxValue || bottom == int.MinValue)
419+
{
420+
// No valid rectangle found
421+
return buffer.GetRegion();
422+
}
423+
424+
return buffer.GetRegion(Rectangle.FromLTRB(left, top, right, bottom));
425+
}
426+
389427
/// <summary>
390428
/// Returns the index of the most transparent color in the palette.
391429
/// </summary>
@@ -583,7 +621,7 @@ private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int tran
583621
if (metadata is null)
584622
{
585623
metadata = new();
586-
hasTransparency = transparencyIndex > -1;
624+
hasTransparency = transparencyIndex >= 0;
587625
}
588626
else
589627
{
@@ -619,7 +657,7 @@ private void WriteExtension<TGifExtension>(TGifExtension extension, Stream strea
619657
}
620658

621659
IMemoryOwner<byte>? owner = null;
622-
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
660+
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
623661
if (extensionSize > 128)
624662
{
625663
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
@@ -642,14 +680,12 @@ private void WriteExtension<TGifExtension>(TGifExtension extension, Stream strea
642680
}
643681

644682
/// <summary>
645-
/// Writes the image descriptor to the stream.
683+
/// Writes the image frame descriptor to the stream.
646684
/// </summary>
647-
/// <typeparam name="TPixel">The pixel format.</typeparam>
648-
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
685+
/// <param name="rectangle">The frame location and size.</param>
649686
/// <param name="hasColorTable">Whether to use the global color table.</param>
650687
/// <param name="stream">The stream to write to.</param>
651-
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
652-
where TPixel : unmanaged, IPixel<TPixel>
688+
private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream)
653689
{
654690
byte packedValue = GifImageDescriptor.GetPackedValue(
655691
localColorTableFlag: hasColorTable,
@@ -658,10 +694,10 @@ private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColo
658694
localColorTableSize: this.bitDepth - 1);
659695

660696
GifImageDescriptor descriptor = new(
661-
left: 0,
662-
top: 0,
663-
width: (ushort)image.Width,
664-
height: (ushort)image.Height,
697+
left: (ushort)rectangle.X,
698+
top: (ushort)rectangle.Y,
699+
width: (ushort)rectangle.Width,
700+
height: (ushort)rectangle.Height,
665701
packed: packedValue);
666702

667703
Span<byte> buffer = stackalloc byte[20];
@@ -697,9 +733,9 @@ private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, Stream str
697733
/// <summary>
698734
/// Writes the image pixel data to the stream.
699735
/// </summary>
700-
/// <param name="indices">The <see cref="Buffer2D{Byte}"/> containing indexed pixels.</param>
736+
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
701737
/// <param name="stream">The stream to write to.</param>
702-
private void WriteImageData(Buffer2D<byte> indices, Stream stream)
738+
private void WriteImageData(Buffer2DRegion<byte> indices, Stream stream)
703739
{
704740
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
705741
encoder.Encode(indices, stream);

src/ImageSharp/Formats/Gif/LzwEncoder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public LzwEncoder(MemoryAllocator memoryAllocator, int colorDepth)
186186
/// </summary>
187187
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
188188
/// <param name="stream">The stream to write to.</param>
189-
public void Encode(Buffer2D<byte> indexedPixels, Stream stream)
189+
public void Encode(Buffer2DRegion<byte> indexedPixels, Stream stream)
190190
{
191191
// Write "initial code size" byte
192192
stream.WriteByte((byte)this.initialCodeSize);
@@ -249,7 +249,7 @@ private void ClearBlock(Stream stream)
249249
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
250250
/// <param name="initialBits">The initial bits.</param>
251251
/// <param name="stream">The stream to write to.</param>
252-
private void Compress(Buffer2D<byte> indexedPixels, int initialBits, Stream stream)
252+
private void Compress(Buffer2DRegion<byte> indexedPixels, int initialBits, Stream stream)
253253
{
254254
// Set up the globals: globalInitialBits - initial number of bits
255255
this.globalInitialBits = initialBits;

src/ImageSharp/IO/IFileSystem.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
99
internal interface IFileSystem
1010
{
1111
/// <summary>
12-
/// Returns a readable stream as defined by the path.
12+
/// Opens a file as defined by the path and returns it as a readable stream.
1313
/// </summary>
1414
/// <param name="path">Path to the file to open.</param>
15-
/// <returns>A stream representing the file to open.</returns>
15+
/// <returns>A stream representing the opened file.</returns>
1616
Stream OpenRead(string path);
1717

1818
/// <summary>
19-
/// Creates or opens a file and returns it as a writable stream as defined by the path.
19+
/// Opens a file as defined by the path and returns it as a readable stream
20+
/// that can be used for asynchronous reading.
2021
/// </summary>
2122
/// <param name="path">Path to the file to open.</param>
22-
/// <returns>A stream representing the file to open.</returns>
23+
/// <returns>A stream representing the opened file.</returns>
24+
Stream OpenReadAsynchronous(string path);
25+
26+
/// <summary>
27+
/// Creates or opens a file as defined by the path and returns it as a writable stream.
28+
/// </summary>
29+
/// <param name="path">Path to the file to open.</param>
30+
/// <returns>A stream representing the opened file.</returns>
2331
Stream Create(string path);
32+
33+
/// <summary>
34+
/// Creates or opens a file as defined by the path and returns it as a writable stream
35+
/// that can be used for asynchronous reading and writing.
36+
/// </summary>
37+
/// <param name="path">Path to the file to open.</param>
38+
/// <returns>A stream representing the opened file.</returns>
39+
Stream CreateAsynchronous(string path);
2440
}

src/ImageSharp/IO/LocalFileSystem.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
1111
/// <inheritdoc/>
1212
public Stream OpenRead(string path) => File.OpenRead(path);
1313

14+
/// <inheritdoc/>
15+
public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
16+
{
17+
Mode = FileMode.Open,
18+
Access = FileAccess.Read,
19+
Share = FileShare.Read,
20+
Options = FileOptions.Asynchronous,
21+
});
22+
1423
/// <inheritdoc/>
1524
public Stream Create(string path) => File.Create(path);
25+
26+
/// <inheritdoc/>
27+
public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
28+
{
29+
Mode = FileMode.Create,
30+
Access = FileAccess.ReadWrite,
31+
Share = FileShare.None,
32+
Options = FileOptions.Asynchronous,
33+
});
1634
}

src/ImageSharp/Image.FromFile.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static async Task<IImageFormat> DetectFormatAsync(
7272
{
7373
Guard.NotNull(options, nameof(options));
7474

75-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
75+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
7676
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
7777
}
7878

@@ -144,7 +144,7 @@ public static async Task<ImageInfo> IdentifyAsync(
144144
CancellationToken cancellationToken = default)
145145
{
146146
Guard.NotNull(options, nameof(options));
147-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
147+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
148148
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
149149
}
150150

@@ -214,7 +214,7 @@ public static async Task<Image> LoadAsync(
214214
string path,
215215
CancellationToken cancellationToken = default)
216216
{
217-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
217+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
218218
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
219219
}
220220

@@ -291,7 +291,7 @@ public static async Task<Image<TPixel>> LoadAsync<TPixel>(
291291
Guard.NotNull(options, nameof(options));
292292
Guard.NotNull(path, nameof(path));
293293

294-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
294+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
295295
return await LoadAsync<TPixel>(options, stream, cancellationToken).ConfigureAwait(false);
296296
}
297297
}

src/ImageSharp/ImageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static async Task SaveAsync(
7070
Guard.NotNull(path, nameof(path));
7171
Guard.NotNull(encoder, nameof(encoder));
7272

73-
using Stream fs = source.GetConfiguration().FileSystem.Create(path);
73+
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
7474
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
7575
}
7676

0 commit comments

Comments
 (0)