Skip to content

Ensure seekable streams can be read multiple times. #2267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 7 additions & 21 deletions src/ImageSharp/IO/BufferedReadStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public BufferedReadStream(Configuration configuration, Stream stream)
}

// This triggers a full read on first attempt.
this.readBufferIndex = this.BufferSize;
this.readBufferIndex = int.MinValue;
}

/// <summary>
Expand Down Expand Up @@ -96,8 +96,9 @@ public override long Position
else
{
// Base stream seek will throw for us if invalid.
this.BaseStream.Seek(value, SeekOrigin.Begin);
this.readerPosition = value;
this.FillReadBuffer();
this.readBufferIndex = int.MinValue;
}
}
}
Expand Down Expand Up @@ -140,7 +141,7 @@ public override int ReadByte()

// Our buffer has been read.
// We need to refill and start again.
if (this.readBufferIndex > this.maxBufferIndex)
if ((uint)this.readBufferIndex > (uint)this.maxBufferIndex)
{
this.FillReadBuffer();
}
Expand All @@ -156,22 +157,7 @@ public override int ReadByte()
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count)
{
// Too big for our buffer. Read directly from the stream.
if (count > this.BufferSize)
{
return this.ReadToBufferDirectSlow(buffer, offset, count);
}

// Too big for remaining buffer but less than entire buffer length
// Copy to buffer then read from there.
if (count + this.readBufferIndex > this.BufferSize)
{
return this.ReadToBufferViaCopySlow(buffer, offset, count);
}

return this.ReadToBufferViaCopyFast(buffer, offset, count);
}
=> this.Read(buffer.AsSpan(offset, count));

/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -186,7 +172,7 @@ public override int Read(Span<byte> buffer)

// Too big for remaining buffer but less than entire buffer length
// Copy to buffer then read from there.
if (count + this.readBufferIndex > this.BufferSize)
if ((uint)this.readBufferIndex > (uint)(this.BufferSize - count))
{
return this.ReadToBufferViaCopySlow(buffer);
}
Expand All @@ -206,7 +192,7 @@ public override void Flush()
}

// Reset to trigger full read on next attempt.
this.readBufferIndex = this.BufferSize;
this.readBufferIndex = int.MinValue;
}

/// <inheritdoc/>
Expand Down
16 changes: 15 additions & 1 deletion src/ImageSharp/Image.FromStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ public static Image Load(DecoderOptions options, Stream stream, out IImageFormat
/// <param name="stream">The input stream.</param>
/// <param name="action">The action to perform.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="NotSupportedException">Cannot read from the stream.</exception>
internal static T WithSeekableStream<T>(
DecoderOptions options,
Stream stream,
Expand Down Expand Up @@ -587,7 +588,20 @@ internal static async Task<T> WithSeekableStreamAsync<T>(
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;

return action(memoryStream, cancellationToken);
T Action(Stream ms, CancellationToken ct)
{
// Reset the position of the seekable stream if we did not read to the end
// to allow additional reads.
T result = action(ms, ct);
if (stream.CanSeek && ms.Position != ms.Length)
{
stream.Position = ms.Position;
}

return result;
}

return Action(memoryStream, cancellationToken);
}

[DoesNotReturn]
Expand Down
42 changes: 36 additions & 6 deletions tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ public void ResolutionShouldChange<TPixel>(TestImageProvider<TPixel> provider)
image.DebugSave(provider);
}

[Fact]
public void ReadOriginIsRespectedOnLoad()
{
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using Image<Rgb24> i = Image.Load<Rgb24>(stream);
long position1 = stream.Position;
Assert.NotEqual(0, position1);

using Image<Rgb24> j = Image.Load<Rgb24>(stream);
long position2 = stream.Position;
Assert.True(position2 > position1);

Assert.NotEqual(i[5, 5], j[5, 5]);
}

[Fact]
public async Task ReadOriginIsRespectedOnLoadAsync()
{
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using Image<Rgb24> i = await Image.LoadAsync<Rgb24>(stream);
long position1 = stream.Position;
Assert.NotEqual(0, position1);

using Image<Rgb24> j = await Image.LoadAsync<Rgb24>(stream);
long position2 = stream.Position;
Assert.True(position2 > position1);

Assert.NotEqual(i[5, 5], j[5, 5]);
}

[Fact]
public void ImageCanEncodeToString()
{
Expand Down Expand Up @@ -155,15 +185,15 @@ public void ImageShouldPreservePixelByteOrderWhenSerialized()
foreach (TestFile file in Files)
{
byte[] serialized;
using (var image = Image.Load(file.Bytes, out IImageFormat mimeType))
using (var memoryStream = new MemoryStream())
using (Image image = Image.Load(file.Bytes, out IImageFormat mimeType))
using (MemoryStream memoryStream = new())
{
image.Save(memoryStream, mimeType);
memoryStream.Flush();
serialized = memoryStream.ToArray();
}

using var image2 = Image.Load<Rgba32>(serialized);
using Image<Rgba32> image2 = Image.Load<Rgba32>(serialized);
image2.Save($"{path}{Path.DirectorySeparatorChar}{file.FileName}");
}
}
Expand Down Expand Up @@ -198,8 +228,8 @@ public void ImageShouldPreservePixelByteOrderWhenSerialized()

public void CanIdentifyImageLoadedFromBytes(int width, int height, string extension)
{
using var image = Image.LoadPixelData<Rgba32>(new Rgba32[width * height], width, height);
using var memoryStream = new MemoryStream();
using Image<Rgba32> image = Image.LoadPixelData<Rgba32>(new Rgba32[width * height], width, height);
using MemoryStream memoryStream = new();
IImageFormat format = GetFormat(extension);
image.Save(memoryStream, format);
memoryStream.Position = 0;
Expand All @@ -220,7 +250,7 @@ public void IdentifyReturnsNullWithInvalidStream()
{
byte[] invalid = new byte[10];

using var memoryStream = new MemoryStream(invalid);
using MemoryStream memoryStream = new(invalid);
IImageInfo imageInfo = Image.Identify(memoryStream, out IImageFormat format);

Assert.Null(imageInfo);
Expand Down
3 changes: 3 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ public static class Png
// Issue 2209: https://github.com/SixLabors/ImageSharp/issues/2209
public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png";

// Issue 2259: https://github.com/SixLabors/ImageSharp/issues/2259
public const string Issue2259 = "Png/issues/Issue_2259.png";

public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/issues/Issue_2259.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.