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] SKBitmap.Decode always returns null for a specific image #2429

Open olegbaslak opened 1 year ago

olegbaslak commented 1 year ago

Description

Cannot create Bitmap or Codec of a specific jpg image (attached).

The image is not corrupted, it can be opened in Windows Photos and other image viewers, it can be displayed by browser.

Looks similar to https://github.com/mono/SkiaSharp/issues/1621 and https://github.com/mono/SkiaSharp/issues/1551. However, this workaround does not work:

Stream? stream = /*......*/;
using var inputStream = new SKManagedStream(stream);
using var inputData = SKData.Create(inputStream);
var bitmap = SKBitmap.Decode(inputData);
var image = SKImage.FromBitmap(bitmap);
var outputData = image.Encode(SKEncodedImageFormat.Png, 100);

Code

using var bitmap = SKBitmap.Decode(imageContentStream); // Returns null
using var codec = SKCodec.Create(imageContentStream); // Returns null

Expected Behavior

Bitmap and codec are created from a stream.

Actual Behavior

Bitmap and codec creation returns null.

Basic Information

Screenshots

Reproduction Link

image.zip

olegbaslak commented 1 year ago

It does not work for Java bindings too. Looks like the problem may be in the Skia code.

JanKotschenreuther commented 1 year ago

I think i ran into the same problem.

I am using InputFile to offer the user the option to pick a file from his local client-site filesystem. After has been picked, the OnChange-Event is fired and after all conditions are met, stream is read by the event handler PickFileAsync. Stream lengths have been checked to make sure the streams contain all bytes of the file. SKBitmap.Decode returns null, when passing the memorystream.

I have been using simple .png files with QR-Codes as content, like this one:

qr5

@using SkiaSharp.Views.Blazor
@using SkiaSharp;

<div>
  @*InputFile is of type Microsoft.AspNetCore.Components.Forms.IBrowserFile.InputFile*@
  <InputFile OnChange="PickFileAsync"></InputFile>
</div>
<div>
  <SKCanvasView @ref="_canvasView" OnPaintSurface="PaintSurface" width="@(_bitmap?.Width ?? 0)" height="@(_bitmap?.Height ?? 0)"></SKCanvasView>
</div>

@code {
    private SKCanvasView? _canvasView;
    private SKBitmap? _bitmap;

    private async Task PickFileAsync(InputFileChangeEventArgs e)
    {
        if (e.FileCount > 1)
        {
            Console.WriteLine("To many files selected!");
            return;
        }

        var allowedContentType = "image/png";
        if (e.File.ContentType != allowedContentType)
        {
            Console.WriteLine($"File ContentType '{e.File.ContentType}' not supported! Pick an image of ContentType {allowedContentType}");
            return;
        }

        if (_bitmap != null)
        {
            _bitmap.Dispose();
            _bitmap = null;
        }

        //File is of type Microsoft.AspNetCore.Components.Forms.IBrowserFile
        using var stream = e.File.OpenReadStream();
        Console.WriteLine(stream.Length); //returns correct byte size of the file.
        using var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream);
        Console.WriteLine(memoryStream.Length); //returns correct byte size of the file.

        //_bitmap is null after SKBitmap.Decode returns.
        this._bitmap = SKBitmap.Decode(memoryStream);
        this._canvasView?.Invalidate();
    }

    public void PaintSurface(SKPaintSurfaceEventArgs e)
    {
        if (this._bitmap == null)
        {
            return;
        }

        var canvas = e.Surface.Canvas;
        canvas.Clear();
        canvas.DrawBitmap(this._bitmap, new SKPoint(0, 0));
    }
}

The position of the pointer of the stream has already been at 0. I tried setting the position to 0 anyway, just to make sure that this is not the same problem as in Issue https://github.com/mono/SkiaSharp/issues/640#issuecomment-711481285.

//Did not help.
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.Position = 0;

I checked, if SKData.Create may be the point of failure, but it returned an object.

//skData is not null, but I am absolutley not sure, what the content should look like.
var skData = SKData.Create(memoryStream);

Workaround

In my case, I am able to workaround this problem using byte-arrays as shown in the following example.

using var stream = e.File.OpenReadStream();        
using var memoryStream = new MemoryStream();        
await stream.CopyToAsync(memoryStream);
var byteArray = memoryStream.ToArray();

this._bitmap = SKBitmap.Decode(byteArray);
this._canvasView?.Invalidate();

It is somehow weird that SKBitmap.Decode behaves differently for byte-arrays and streams.


Error Handling

It is kind of weird to return null to indicate errors, as the API-Description hints The decoded bitmap, or null on error.. This makes it very hard to find the true problem.


Basic Information

themcoo commented 1 year ago

@JanKotschenreuther SKBitmap.Decode(memoryStream); overload for streams simply wraps it if it's seekable. So you create the SKBitmap this._bitmap but before using it you dispose the stream(it's wrapped in using statement) making SKBitmap unusable. This explains why "the workaround" with byte arrays works

JanKotschenreuther commented 1 year ago

As far as I know, the using statement I am using there, is scoped to the scope of the method.

Using statements without braces and termination by ; are disposed at the end of the scope in which they are declared.

themcoo commented 1 year ago

I know how 'using' is scoped. Are you sure though thatthis._canvasView?.Invalidate(); is causing synchronous access to the this._bitmap field before the nethod ends? Because I'm pretty sure it does not.

JanKotschenreuther commented 1 year ago

I do not know, if the method SKCanvasView.Invalidate is doing anything asynchronous.

The name, parameters or return-types are not telling me it is using any of the well known asynchronous patterns. I also were not able to find any docs telling me that it could be an asynchronous operation.

umartechboy commented 6 months ago

This is strange, either something wrong with Bitmap Data creation, OnPaint event scope or something that isn't properly documented. I was getting the same issue in a slightly different scenario.

Platform: WebAssembly Problem statement: SKBitmap.Decode on simple byte[] within an OnPaint of the blazor view, bitmap is returned non-null. Image info looks right too (Height, Width, DPI etc). It just doesn't render rightaway in the same OnPaint call. What didn't work. Switching from SKGLView to SKCanvasView. The only difference was the error shown in console. SKGLView showed a warning of failed image drawing, no info about memory access. SKCanvasView displayed a error with memory access exception. try/catch didn't work either and the OnPaint event returned rightaway. What partially worked: Cacheing the bitmap in global scope and rendering in any coming OnPaint was always successful. Calling the Bitmap Cache routine in mouse events instead of OnPaint Wrong Suspect My initial suspect was some thread sync issue in WebGL, but there something must be wrong with OnPaint Root Cause: Unknown Resolve: Double Buffering. Keep a buffer of SKBitmap, the same size as the the canvas. Render on the buffer outside OnPaint. Draw on OnPaint request only if the buffer has been prepared. Works like a charm. I only have to keep in mind one extra refresh/invalidate request due to an added layer of buffer.