mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.5k stars 538 forks source link

[BUG] Some images are only read partially from non-seekable streams #2514

Open msvprogs opened 1 year ago

msvprogs commented 1 year ago

Description

Some images, especially small ones (up to 20-30 Kb), are not read correctly if the source stream is non-seekable (CanSeek == false). I've encountered problems with WEBP and PNG source formats, it looks like the problem is in stream reading logic, not decoding.

This file is processed incorrectly on Windows 11. There were problems with other small PNG files on Linux too, but I couldn't reproduce the bug with them on Windows. test_image.zip

If I try to resize a bitmap loaded directly from FileStream (seekable), it is converted correctly. Conversion result looks like this: target_seekable

But if I read the file from non-seekable stream, for example, the one that was returned by Amazon S3 client library, or by implementing stream wrapper that sets CanSeek to false for any underlying stream, conversion result is incorrect. It looks like the source data has only been read partially, for example, only first block has been read: target_non_seekable

Code Read from FileStream:

using var fileStream = new FileStream("test_image.webp", FileMode.Open, FileAccess.Read);

using var codec = SKCodec.Create(fileStream, out var codecResult);
if (codec == null)
    throw new Exception("codec == null, " + codecResult);

using var sourceBitmap = SKBitmap.Decode(codec);
if (sourceBitmap == null)
    throw new Exception("sourceBitmap == null");

using var targetBitmap = new SKBitmap(new SKImageInfo(200, 150));
var shrinkSucceeded = sourceBitmap.ScalePixels(targetBitmap, SKFilterQuality.High);
if (!shrinkSucceeded)
    throw new Exception("!shrinkSucceeded");

using var targetStream = new FileStream("target.jpg", FileMode.Create, FileAccess.Write);

var encodeSucceeded = targetBitmap.Encode(targetStream, SKEncodedImageFormat.Jpeg, 90);
if (!encodeSucceeded)
    throw new Exception("!encodeSucceeded");

targetStream.Flush();
targetStream.Close();

Read from non-seekable stream wrapper:

using var fileStream = new FileStream("test_image.webp", FileMode.Open, FileAccess.Read);

using var codec = SKCodec.Create(new NonSeekableStreamWrapper(fileStream), out var codecResult);
if (codec == null)
    throw new Exception("codec == null, " + codecResult);

using var sourceBitmap = SKBitmap.Decode(codec);
if (sourceBitmap == null)
    throw new Exception("sourceBitmap == null");

using var targetBitmap = new SKBitmap(new SKImageInfo(200, 150));
var shrinkSucceeded = sourceBitmap.ScalePixels(targetBitmap, SKFilterQuality.High);
if (!shrinkSucceeded)
    throw new Exception("!shrinkSucceeded");

using var targetStream = new FileStream("target.jpg", FileMode.Create, FileAccess.Write);

var encodeSucceeded = targetBitmap.Encode(targetStream, SKEncodedImageFormat.Jpeg, 90);
if (!encodeSucceeded)
    throw new Exception("!encodeSucceeded");

targetStream.Flush();
targetStream.Close();

NonSeekableStreamWrapper code:

private sealed class NonSeekableStreamWrapper : Stream
{
    public override bool CanRead
        => _underlyingStream.CanRead;

    public override bool CanSeek
        => false;

    public override bool CanWrite
        => _underlyingStream.CanWrite;

    public override long Length
        => _underlyingStream.Length;

    public override long Position
    {
        get => _underlyingStream.Position;
        set => _underlyingStream.Position = value;
    }

    private readonly Stream _underlyingStream;

    public NonSeekableStreamWrapper(Stream underlyingStream)
        => _underlyingStream = underlyingStream ?? throw new ArgumentNullException(nameof(underlyingStream));

    public override void Flush()
        => _underlyingStream.Flush();

    public override int Read(byte[] buffer, int offset, int count)
        => _underlyingStream.Read(buffer, offset, count);

    public override long Seek(long offset, SeekOrigin origin)
        => _underlyingStream.Seek(offset, origin);

    public override void SetLength(long value)
        => _underlyingStream.SetLength(value);

    public override void Write(byte[] buffer, int offset, int count)
        => _underlyingStream.Write(buffer, offset, count);

    public override Task FlushAsync(CancellationToken cancellationToken)
        => _underlyingStream.FlushAsync(cancellationToken);

    public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
        => _underlyingStream.CopyToAsync(destination, bufferSize, cancellationToken);

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        => _underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
        => _underlyingStream.ReadAsync(buffer, cancellationToken);

    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        => _underlyingStream.WriteAsync(buffer, offset, count, cancellationToken);

    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
        => _underlyingStream.WriteAsync(buffer, cancellationToken);

    public override ValueTask DisposeAsync()
        => _underlyingStream.DisposeAsync();

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            _underlyingStream.Dispose();
    }
}

Expected Behavior

SKBitmap is read correctly from both the seekable (FileStream) and non-seekable streams.

Actual Behavior

SKBitmap is read correctly from seekable stream, but only read partially when source is non-seekable stream, which results in empty background instead of image data.

Basic Information

tgranie commented 1 year ago

I face the same issue, any news please. As a fix I use the MemoryStream as an intermediate but takes time to copy huge images.