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
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:
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:
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
Version with issue: 2.88.3
Last known good version: unknown
IDE: Microsoft Visual Studio Professional 2022 (64-bit) - Version 17.6.4
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:
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:
Code Read from FileStream:
Read from non-seekable stream wrapper:
NonSeekableStreamWrapper
code: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