2016-06-17 23:54:08 +03:00
|
|
|
|
using System;
|
Re-work the managed-native types (#900)
Changes:
- Added `GCHandleProxy` to debug builds
- this is used to track all `GCHandle` `Alloc` and `Free` calls to ensure that all allocations are freed.
- added some unit tests to make sure this is actually enforced
- as a result, several object are now freed correctly
- Added `ISKReferenceCounted` and `ISKNonVirtualReferenceCounted` interfaces to represent the reference counting types used in the native library
- this helps with automatically de-referencing objects
- `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` have been re-written to use better delegates
- instead of passing each of the delegates as parameters, they are now a struct that is passed as a single object
- better for extensions (which there shouldn't be) and only a single static field on the type
- removed the usage of `Marshal.GetFunctionPointerForDelegate`, which should help out with WASM (see #876)
- the objects now only keep weak references, meaning that they can now be garbage collected
- instead of trying to resolve the instances with a dictionary, a delegate is used and passed as "user context"
- Moved some of the repetitive logic from the types into the base `SKObject` and `SKNativeObject`
- some logic is automatically executed if the concrete type is `ISKReferenceCounted` or `ISKNonVirtualReferenceCounted`
- with the more centralized logic and stricter patterns, better tests can be written to make sure all memory is freed correctly and timely
- `SKData`, `SKFontManager` and `SKTypeface` now correctly prevent disposal of the "static" instances
- `SKPaint` now references the `Shader`, `MaskFilter`, `ColorFilter`, `ImageFilter`, `Typeface` and `PathEffect` properties
- this prevents accidental collection, or non-collection when the object goes out of scope
- the `SKPath` iterators (`Iterator` and `RawIterator`) and op builder (`OpBuilder`) now correctly own and dispose their native objects
- `SKRegion` objects are now disposed on the native side
- `SKTypeface` construction from a `SKManagedStream` (via both `SKTypeface` and `SKFontManager`) now copy the contents of the .NET `Stream` into a native memory
- typeface construction requires multiple seeks (previously, the stream was copied only if it was non-seekable)
- it also requires "duplicating" the stream, which is not supported on .NET streams
- duplicates or forks of a stream means that each of the streams need to be read concurrently from different locations
- .NET streams can only have a single position
- Updated the NuGets used for the tests
- using the `Xunit.AssemblyFixture` and `Xunit.SkippableFact` NuGets instead of using the code directly
- removed the `Xunit.Categories` NuGet as it was preventing tests from running
This PR has a big set of changes that may be breaking due to bug fixes:
- The `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` no longer prevent the GC from collecting them. This means that if code no longer references them, they will be disposed.
- As far as I can tell, this should not be a problem for the streams as they are never kept around - they are just used for reading and writing and typically only need to live for as long as a single method, and then need to be disposed by the caller. The `SKTypeface` and `SKDocument` do keep it around for a bit, but then they also take ownership of the stream and keep a hard reference to the streams themselves. They will dispose the streams when they are disposed.
- `SKDrawable` is never kept around and is entirely a user-controlled object. If it goes out of scope, skia doesn't have a reference anyway.
- The `SKFontManager` and `SKTypeface` no longer use the managed streams (`SKManagedStream` or `Stream`) directly
- they make a copy.
- This is simply because skia streams can do things that are not possible for .NET - they can be read concurrently from different positions. If a `SKFileStream` or `SKMemoryStream` are passed, then the streams are not copied.
- Further optimizations can be made in the case of a `MemoryStream` or `byte[]` to not actually copy but use GC pinning to get a handle to the managed data and work with pointers. But this can be done later so that this PR can be merged and tested.
2019-07-30 04:26:21 +03:00
|
|
|
|
using System.Collections.Generic;
|
2016-06-17 23:54:08 +03:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2017-12-18 02:33:26 +03:00
|
|
|
|
using System.Net.Http;
|
Re-work the managed-native types (#900)
Changes:
- Added `GCHandleProxy` to debug builds
- this is used to track all `GCHandle` `Alloc` and `Free` calls to ensure that all allocations are freed.
- added some unit tests to make sure this is actually enforced
- as a result, several object are now freed correctly
- Added `ISKReferenceCounted` and `ISKNonVirtualReferenceCounted` interfaces to represent the reference counting types used in the native library
- this helps with automatically de-referencing objects
- `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` have been re-written to use better delegates
- instead of passing each of the delegates as parameters, they are now a struct that is passed as a single object
- better for extensions (which there shouldn't be) and only a single static field on the type
- removed the usage of `Marshal.GetFunctionPointerForDelegate`, which should help out with WASM (see #876)
- the objects now only keep weak references, meaning that they can now be garbage collected
- instead of trying to resolve the instances with a dictionary, a delegate is used and passed as "user context"
- Moved some of the repetitive logic from the types into the base `SKObject` and `SKNativeObject`
- some logic is automatically executed if the concrete type is `ISKReferenceCounted` or `ISKNonVirtualReferenceCounted`
- with the more centralized logic and stricter patterns, better tests can be written to make sure all memory is freed correctly and timely
- `SKData`, `SKFontManager` and `SKTypeface` now correctly prevent disposal of the "static" instances
- `SKPaint` now references the `Shader`, `MaskFilter`, `ColorFilter`, `ImageFilter`, `Typeface` and `PathEffect` properties
- this prevents accidental collection, or non-collection when the object goes out of scope
- the `SKPath` iterators (`Iterator` and `RawIterator`) and op builder (`OpBuilder`) now correctly own and dispose their native objects
- `SKRegion` objects are now disposed on the native side
- `SKTypeface` construction from a `SKManagedStream` (via both `SKTypeface` and `SKFontManager`) now copy the contents of the .NET `Stream` into a native memory
- typeface construction requires multiple seeks (previously, the stream was copied only if it was non-seekable)
- it also requires "duplicating" the stream, which is not supported on .NET streams
- duplicates or forks of a stream means that each of the streams need to be read concurrently from different locations
- .NET streams can only have a single position
- Updated the NuGets used for the tests
- using the `Xunit.AssemblyFixture` and `Xunit.SkippableFact` NuGets instead of using the code directly
- removed the `Xunit.Categories` NuGet as it was preventing tests from running
This PR has a big set of changes that may be breaking due to bug fixes:
- The `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` no longer prevent the GC from collecting them. This means that if code no longer references them, they will be disposed.
- As far as I can tell, this should not be a problem for the streams as they are never kept around - they are just used for reading and writing and typically only need to live for as long as a single method, and then need to be disposed by the caller. The `SKTypeface` and `SKDocument` do keep it around for a bit, but then they also take ownership of the stream and keep a hard reference to the streams themselves. They will dispose the streams when they are disposed.
- `SKDrawable` is never kept around and is entirely a user-controlled object. If it goes out of scope, skia doesn't have a reference anyway.
- The `SKFontManager` and `SKTypeface` no longer use the managed streams (`SKManagedStream` or `Stream`) directly
- they make a copy.
- This is simply because skia streams can do things that are not possible for .NET - they can be read concurrently from different positions. If a `SKFileStream` or `SKMemoryStream` are passed, then the streams are not copied.
- Further optimizations can be made in the case of a `MemoryStream` or `byte[]` to not actually copy but use GC pinning to get a handle to the managed data and work with pointers. But this can be done later so that this PR can be merged and tested.
2019-07-30 04:26:21 +03:00
|
|
|
|
using System.Runtime.InteropServices;
|
2018-05-09 02:18:28 +03:00
|
|
|
|
using System.Threading.Tasks;
|
2017-12-14 02:51:18 +03:00
|
|
|
|
using Xunit;
|
2016-06-17 23:54:08 +03:00
|
|
|
|
|
|
|
|
|
namespace SkiaSharp.Tests
|
|
|
|
|
{
|
|
|
|
|
public class SKCodecTest : SKTest
|
|
|
|
|
{
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-06-17 23:54:08 +03:00
|
|
|
|
public void MinBufferedBytesNeededHasAValue ()
|
|
|
|
|
{
|
2017-01-21 03:38:48 +03:00
|
|
|
|
Assert.True (SKCodec.MinBufferedBytesNeeded > 0);
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 23:20:20 +03:00
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void StreamLosesOwnershipTddoCodecButIsNotForgotten()
|
|
|
|
|
{
|
|
|
|
|
var codec = SKCodec.Create(Path.Combine(PathToImages, "color-wheel.png"));
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < 1000; ++i)
|
|
|
|
|
{
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Re-work the managed-native types (#900)
Changes:
- Added `GCHandleProxy` to debug builds
- this is used to track all `GCHandle` `Alloc` and `Free` calls to ensure that all allocations are freed.
- added some unit tests to make sure this is actually enforced
- as a result, several object are now freed correctly
- Added `ISKReferenceCounted` and `ISKNonVirtualReferenceCounted` interfaces to represent the reference counting types used in the native library
- this helps with automatically de-referencing objects
- `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` have been re-written to use better delegates
- instead of passing each of the delegates as parameters, they are now a struct that is passed as a single object
- better for extensions (which there shouldn't be) and only a single static field on the type
- removed the usage of `Marshal.GetFunctionPointerForDelegate`, which should help out with WASM (see #876)
- the objects now only keep weak references, meaning that they can now be garbage collected
- instead of trying to resolve the instances with a dictionary, a delegate is used and passed as "user context"
- Moved some of the repetitive logic from the types into the base `SKObject` and `SKNativeObject`
- some logic is automatically executed if the concrete type is `ISKReferenceCounted` or `ISKNonVirtualReferenceCounted`
- with the more centralized logic and stricter patterns, better tests can be written to make sure all memory is freed correctly and timely
- `SKData`, `SKFontManager` and `SKTypeface` now correctly prevent disposal of the "static" instances
- `SKPaint` now references the `Shader`, `MaskFilter`, `ColorFilter`, `ImageFilter`, `Typeface` and `PathEffect` properties
- this prevents accidental collection, or non-collection when the object goes out of scope
- the `SKPath` iterators (`Iterator` and `RawIterator`) and op builder (`OpBuilder`) now correctly own and dispose their native objects
- `SKRegion` objects are now disposed on the native side
- `SKTypeface` construction from a `SKManagedStream` (via both `SKTypeface` and `SKFontManager`) now copy the contents of the .NET `Stream` into a native memory
- typeface construction requires multiple seeks (previously, the stream was copied only if it was non-seekable)
- it also requires "duplicating" the stream, which is not supported on .NET streams
- duplicates or forks of a stream means that each of the streams need to be read concurrently from different locations
- .NET streams can only have a single position
- Updated the NuGets used for the tests
- using the `Xunit.AssemblyFixture` and `Xunit.SkippableFact` NuGets instead of using the code directly
- removed the `Xunit.Categories` NuGet as it was preventing tests from running
This PR has a big set of changes that may be breaking due to bug fixes:
- The `SKAbstractManagedStream`, `SKAbstractManagedWStream` and `SKDrawable` no longer prevent the GC from collecting them. This means that if code no longer references them, they will be disposed.
- As far as I can tell, this should not be a problem for the streams as they are never kept around - they are just used for reading and writing and typically only need to live for as long as a single method, and then need to be disposed by the caller. The `SKTypeface` and `SKDocument` do keep it around for a bit, but then they also take ownership of the stream and keep a hard reference to the streams themselves. They will dispose the streams when they are disposed.
- `SKDrawable` is never kept around and is entirely a user-controlled object. If it goes out of scope, skia doesn't have a reference anyway.
- The `SKFontManager` and `SKTypeface` no longer use the managed streams (`SKManagedStream` or `Stream`) directly
- they make a copy.
- This is simply because skia streams can do things that are not possible for .NET - they can be read concurrently from different positions. If a `SKFileStream` or `SKMemoryStream` are passed, then the streams are not copied.
- Further optimizations can be made in the case of a `MemoryStream` or `byte[]` to not actually copy but use GC pinning to get a handle to the managed data and work with pointers. But this can be done later so that this PR can be merged and tested.
2019-07-30 04:26:21 +03:00
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void ReleaseDataWasInvokedOnlyAfterTheCodecWasFinished()
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine(PathToImages, "color-wheel.png");
|
|
|
|
|
var bytes = File.ReadAllBytes(path);
|
|
|
|
|
|
|
|
|
|
var released = false;
|
|
|
|
|
|
|
|
|
|
fixed (byte* b = bytes)
|
|
|
|
|
{
|
|
|
|
|
var data = SKData.Create((IntPtr)b, bytes.Length, (addr, ctx) => released = true);
|
|
|
|
|
|
|
|
|
|
var codec = SKCodec.Create(data);
|
|
|
|
|
Assert.NotEqual(SKImageInfo.Empty, codec.Info);
|
|
|
|
|
|
|
|
|
|
data.Dispose();
|
|
|
|
|
Assert.False(released, "The SKDataReleaseDelegate was called too soon.");
|
|
|
|
|
|
|
|
|
|
codec.Dispose();
|
|
|
|
|
Assert.True(released, "The SKDataReleaseDelegate was not called at all.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void StreamLosesOwnershipToCodecButIsNotForgotten()
|
|
|
|
|
{
|
|
|
|
|
var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png"));
|
|
|
|
|
var stream = new SKMemoryStream(bytes);
|
|
|
|
|
var handle = stream.Handle;
|
|
|
|
|
|
|
|
|
|
Assert.True(stream.OwnsHandle);
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(handle, out _));
|
|
|
|
|
|
|
|
|
|
var codec = SKCodec.Create(stream);
|
|
|
|
|
Assert.False(stream.OwnsHandle);
|
|
|
|
|
|
|
|
|
|
stream.Dispose();
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(handle, out _));
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels));
|
|
|
|
|
Assert.NotEmpty(pixels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually()
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine(PathToImages, "color-wheel.png");
|
|
|
|
|
var stream = new SKMemoryStream(File.ReadAllBytes(path));
|
|
|
|
|
var handle = stream.Handle;
|
|
|
|
|
|
|
|
|
|
Assert.True(stream.OwnsHandle);
|
|
|
|
|
Assert.False(stream.IgnorePublicDispose);
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(handle, out _));
|
|
|
|
|
|
|
|
|
|
var codec = SKCodec.Create(stream);
|
|
|
|
|
Assert.False(stream.OwnsHandle);
|
|
|
|
|
Assert.True(stream.IgnorePublicDispose);
|
|
|
|
|
|
|
|
|
|
stream.Dispose();
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(handle, out var inst));
|
|
|
|
|
Assert.Same(stream, inst);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels));
|
|
|
|
|
Assert.NotEmpty(pixels);
|
|
|
|
|
|
|
|
|
|
codec.Dispose();
|
|
|
|
|
Assert.False(SKObject.GetInstance<SKMemoryStream>(handle, out _));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void InvalidStreamIsDisposedImmediately()
|
|
|
|
|
{
|
|
|
|
|
var stream = CreateTestSKStream();
|
|
|
|
|
var handle = stream.Handle;
|
|
|
|
|
|
|
|
|
|
Assert.True(stream.OwnsHandle);
|
|
|
|
|
Assert.False(stream.IgnorePublicDispose);
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKStream>(handle, out _));
|
|
|
|
|
|
|
|
|
|
Assert.Null(SKCodec.Create(stream));
|
|
|
|
|
|
|
|
|
|
Assert.False(stream.OwnsHandle);
|
|
|
|
|
Assert.True(stream.IgnorePublicDispose);
|
|
|
|
|
Assert.False(SKObject.GetInstance<SKStream>(handle, out _));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableFact]
|
|
|
|
|
public unsafe void StreamLosesOwnershipAndCanBeGarbageCollected()
|
|
|
|
|
{
|
|
|
|
|
VerifyImmediateFinalizers();
|
|
|
|
|
|
|
|
|
|
var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png"));
|
|
|
|
|
|
|
|
|
|
DoWork(out var codecH, out var streamH);
|
|
|
|
|
|
|
|
|
|
CollectGarbage();
|
|
|
|
|
|
|
|
|
|
Assert.False(SKObject.GetInstance<SKMemoryStream>(streamH, out _));
|
|
|
|
|
Assert.False(SKObject.GetInstance<SKCodec>(codecH, out _));
|
|
|
|
|
|
|
|
|
|
void DoWork(out IntPtr codecHandle, out IntPtr streamHandle)
|
|
|
|
|
{
|
|
|
|
|
var codec = CreateCodec(out streamHandle);
|
|
|
|
|
codecHandle = codec.Handle;
|
|
|
|
|
|
|
|
|
|
CollectGarbage();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels));
|
|
|
|
|
Assert.NotEmpty(pixels);
|
|
|
|
|
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(streamHandle, out var stream));
|
|
|
|
|
Assert.False(stream.OwnsHandle);
|
|
|
|
|
Assert.True(stream.IgnorePublicDispose);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SKCodec CreateCodec(out IntPtr streamHandle)
|
|
|
|
|
{
|
|
|
|
|
var stream = new SKMemoryStream(bytes);
|
|
|
|
|
streamHandle = stream.Handle;
|
|
|
|
|
|
|
|
|
|
Assert.True(stream.OwnsHandle);
|
|
|
|
|
Assert.False(stream.IgnorePublicDispose);
|
|
|
|
|
Assert.True(SKObject.GetInstance<SKMemoryStream>(streamHandle, out _));
|
|
|
|
|
|
|
|
|
|
return SKCodec.Create(stream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2020-06-20 20:18:52 +03:00
|
|
|
|
public void CanCreateStreamCodec()
|
2016-06-17 23:54:08 +03:00
|
|
|
|
{
|
2020-06-20 20:25:52 +03:00
|
|
|
|
var stream = File.OpenRead(Path.Combine(PathToImages, "color-wheel.png"));
|
2020-06-20 20:18:52 +03:00
|
|
|
|
using var codec = SKCodec.Create(stream);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKEncodedImageFormat.Png, codec.EncodedFormat);
|
|
|
|
|
Assert.Equal(128, codec.Info.Width);
|
|
|
|
|
Assert.Equal(128, codec.Info.Height);
|
|
|
|
|
Assert.Equal(SKAlphaType.Unpremul, codec.Info.AlphaType);
|
|
|
|
|
Assert.Equal(SKImageInfo.PlatformColorType, codec.Info.ColorType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableFact]
|
|
|
|
|
public void CanCreateStreamCodecWithResult()
|
|
|
|
|
{
|
2020-06-20 20:25:52 +03:00
|
|
|
|
var stream = File.OpenRead(Path.Combine(PathToImages, "color-wheel.png"));
|
2020-06-20 20:18:52 +03:00
|
|
|
|
using var codec = SKCodec.Create(stream, out var result);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, result);
|
|
|
|
|
Assert.Equal(SKEncodedImageFormat.Png, codec.EncodedFormat);
|
|
|
|
|
Assert.Equal(128, codec.Info.Width);
|
|
|
|
|
Assert.Equal(128, codec.Info.Height);
|
|
|
|
|
Assert.Equal(SKAlphaType.Unpremul, codec.Info.AlphaType);
|
|
|
|
|
Assert.Equal(SKImageInfo.PlatformColorType, codec.Info.ColorType);
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
2019-12-22 19:50:47 +03:00
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-11-26 09:24:25 +03:00
|
|
|
|
public void GetGifFrames ()
|
|
|
|
|
{
|
|
|
|
|
const int FrameCount = 16;
|
|
|
|
|
|
|
|
|
|
var stream = new SKFileStream (Path.Combine (PathToImages, "animated-heart.gif"));
|
|
|
|
|
using (var codec = SKCodec.Create (stream)) {
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (-1, codec.RepetitionCount);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
|
|
|
|
|
var frameInfos = codec.FrameInfo;
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (FrameCount, frameInfos.Length);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (-1, frameInfos [0].RequiredFrame);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
|
|
|
|
|
var cachedFrames = new SKBitmap [FrameCount];
|
|
|
|
|
var info = new SKImageInfo (codec.Info.Width, codec.Info.Height);
|
|
|
|
|
|
2018-07-12 14:32:02 +03:00
|
|
|
|
var decode = new Action<SKBitmap, int, int> ((bm, cachedIndex, index) => {
|
|
|
|
|
var decodeInfo = info;
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
decodeInfo = info.WithAlphaType (frameInfos [index].AlphaType);
|
|
|
|
|
}
|
2019-09-24 02:34:44 +03:00
|
|
|
|
Assert.True (bm.TryAllocPixels (decodeInfo));
|
2018-07-12 14:32:02 +03:00
|
|
|
|
if (cachedIndex != -1) {
|
|
|
|
|
Assert.True (cachedFrames [cachedIndex].CopyTo (bm));
|
|
|
|
|
}
|
|
|
|
|
var opts = new SKCodecOptions (index, cachedIndex);
|
|
|
|
|
var result = codec.GetPixels (decodeInfo, bm.GetPixels (), opts);
|
|
|
|
|
if (cachedIndex != -1 && frameInfos [cachedIndex].DisposalMethod == SKCodecAnimationDisposalMethod.RestorePrevious) {
|
|
|
|
|
Assert.Equal (SKCodecResult.InvalidParameters, result);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
}
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (SKCodecResult.Success, result);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < FrameCount; i++) {
|
2018-07-12 14:32:02 +03:00
|
|
|
|
var cachedFrame = cachedFrames [i] = new SKBitmap ();
|
|
|
|
|
decode (cachedFrame, -1, i);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
|
2018-07-12 14:32:02 +03:00
|
|
|
|
var uncachedFrame = new SKBitmap ();
|
|
|
|
|
decode (uncachedFrame, frameInfos [i].RequiredFrame, i);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
|
2019-09-24 02:34:44 +03:00
|
|
|
|
var cachedBytes = cachedFrame.Bytes;
|
|
|
|
|
var uncachedBytes = uncachedFrame.Bytes;
|
|
|
|
|
Assert.Equal (cachedBytes, uncachedBytes);
|
2016-11-26 09:24:25 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-06-17 23:54:08 +03:00
|
|
|
|
|
2018-07-12 14:32:02 +03:00
|
|
|
|
[SkippableFact]
|
|
|
|
|
public void GetSingleGifFrame()
|
|
|
|
|
{
|
|
|
|
|
var stream = new SKFileStream(Path.Combine(PathToImages, "animated-heart.gif"));
|
|
|
|
|
using (var codec = SKCodec.Create(stream))
|
|
|
|
|
{
|
|
|
|
|
var frameInfos = codec.FrameInfo;
|
|
|
|
|
for (var i = 0; i < frameInfos.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
Assert.True(codec.GetFrameInfo(i, out var info));
|
|
|
|
|
Assert.Equal(frameInfos[i], info);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-11-25 11:55:59 +03:00
|
|
|
|
public void GetEncodedInfo ()
|
|
|
|
|
{
|
|
|
|
|
var stream = new SKFileStream (Path.Combine (PathToImages, "color-wheel.png"));
|
|
|
|
|
using (var codec = SKCodec.Create (stream)) {
|
2018-07-12 14:32:02 +03:00
|
|
|
|
Assert.Equal (SKImageInfo.PlatformColorType, codec.Info.ColorType);
|
|
|
|
|
Assert.Equal (SKAlphaType.Unpremul, codec.Info.AlphaType);
|
|
|
|
|
Assert.Equal (32, codec.Info.BitsPerPixel);
|
2016-11-25 11:55:59 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-06-17 23:54:08 +03:00
|
|
|
|
public void CanGetPixels ()
|
|
|
|
|
{
|
|
|
|
|
var stream = new SKFileStream (Path.Combine (PathToImages, "baboon.png"));
|
|
|
|
|
using (var codec = SKCodec.Create (stream)) {
|
|
|
|
|
var pixels = codec.Pixels;
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (codec.Info.BytesSize, pixels.Length);
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2017-02-06 14:17:57 +03:00
|
|
|
|
public void DecodeImageScanlines ()
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine (PathToImages, "CMYK.jpg");
|
|
|
|
|
var imageHeight = 516;
|
|
|
|
|
|
|
|
|
|
var fileData = File.ReadAllBytes (path);
|
|
|
|
|
var correctBitmap = SKBitmap.Decode (path);
|
|
|
|
|
|
|
|
|
|
var stream = new SKFileStream (path);
|
|
|
|
|
using (var codec = SKCodec.Create (stream)) {
|
|
|
|
|
var info = new SKImageInfo (codec.Info.Width, codec.Info.Height);
|
|
|
|
|
using (var scanlineBitmap = new SKBitmap (info)) {
|
|
|
|
|
scanlineBitmap.Erase (SKColors.Fuchsia);
|
|
|
|
|
|
|
|
|
|
var result = codec.StartScanlineDecode (info);
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (SKCodecResult.Success, result);
|
2017-02-06 14:17:57 +03:00
|
|
|
|
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (SKCodecScanlineOrder.TopDown, codec.ScanlineOrder);
|
|
|
|
|
Assert.Equal (0, codec.NextScanline);
|
2017-02-06 14:17:57 +03:00
|
|
|
|
|
|
|
|
|
// only decode every second line
|
|
|
|
|
for (int y = 0; y < info.Height; y += 2) {
|
2020-05-17 23:11:59 +03:00
|
|
|
|
Assert.Equal (1, codec.GetScanlines (scanlineBitmap.GetAddress (0, y), 1, info.RowBytes));
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (y + 1, codec.NextScanline);
|
2017-02-06 14:17:57 +03:00
|
|
|
|
if (codec.SkipScanlines (1))
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (y + 2, codec.NextScanline);
|
2017-02-06 14:17:57 +03:00
|
|
|
|
else
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (imageHeight, codec.NextScanline); // reached the end
|
2017-02-06 14:17:57 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Assert.False (codec.SkipScanlines (1));
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (imageHeight, codec.NextScanline);
|
2017-02-06 14:17:57 +03:00
|
|
|
|
|
|
|
|
|
for (var x = 0; x < info.Width; x++) {
|
|
|
|
|
for (var y = 0; y < info.Height; y++) {
|
|
|
|
|
if (y % 2 == 0)
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (correctBitmap.GetPixel (x, y), scanlineBitmap.GetPixel (x, y));
|
2017-02-06 14:17:57 +03:00
|
|
|
|
else
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (SKColors.Fuchsia, scanlineBitmap.GetPixel (x, y));
|
2017-02-06 14:17:57 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-11-17 05:35:51 +03:00
|
|
|
|
public void DecodePartialImage ()
|
|
|
|
|
{
|
|
|
|
|
// read the data here, so we can fake a throttle/download
|
|
|
|
|
var path = Path.Combine (PathToImages, "baboon.png");
|
|
|
|
|
var fileData = File.ReadAllBytes (path);
|
|
|
|
|
SKColor[] correctBytes;
|
|
|
|
|
using (var bitmap = SKBitmap.Decode (path)) {
|
|
|
|
|
correctBytes = bitmap.Pixels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
int maxCount = 1024 * 4;
|
|
|
|
|
|
|
|
|
|
// the "download" stream needs some data
|
|
|
|
|
var downloadStream = new MemoryStream ();
|
|
|
|
|
downloadStream.Write (fileData, offset, maxCount);
|
|
|
|
|
downloadStream.Position -= maxCount;
|
|
|
|
|
offset += maxCount;
|
|
|
|
|
|
|
|
|
|
using (var codec = SKCodec.Create (new SKManagedStream (downloadStream))) {
|
|
|
|
|
var info = new SKImageInfo (codec.Info.Width, codec.Info.Height);
|
|
|
|
|
|
|
|
|
|
// the bitmap to be decoded
|
|
|
|
|
using (var incremental = new SKBitmap (info)) {
|
|
|
|
|
|
|
|
|
|
// start decoding
|
|
|
|
|
IntPtr length;
|
|
|
|
|
var pixels = incremental.GetPixels (out length);
|
|
|
|
|
var result = codec.StartIncrementalDecode (info, pixels, info.RowBytes);
|
|
|
|
|
|
|
|
|
|
// make sure the start was successful
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (SKCodecResult.Success, result);
|
2016-11-17 05:35:51 +03:00
|
|
|
|
result = SKCodecResult.IncompleteInput;
|
|
|
|
|
|
|
|
|
|
while (result == SKCodecResult.IncompleteInput) {
|
|
|
|
|
// decode the rest
|
|
|
|
|
int rowsDecoded = 0;
|
|
|
|
|
result = codec.IncrementalDecode (out rowsDecoded);
|
|
|
|
|
|
|
|
|
|
// write some more data to the stream
|
|
|
|
|
maxCount = Math.Min (maxCount, fileData.Length - offset);
|
|
|
|
|
downloadStream.Write (fileData, offset, maxCount);
|
|
|
|
|
downloadStream.Position -= maxCount;
|
|
|
|
|
offset += maxCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// compare to original
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (correctBytes, incremental.Pixels);
|
2016-11-17 05:35:51 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-06-17 23:54:08 +03:00
|
|
|
|
public void BitmapDecodesCorrectly ()
|
|
|
|
|
{
|
|
|
|
|
byte[] codecPixels;
|
|
|
|
|
byte[] bitmapPixels;
|
|
|
|
|
|
|
|
|
|
using (var codec = SKCodec.Create (new SKFileStream (Path.Combine (PathToImages, "baboon.png")))) {
|
|
|
|
|
codecPixels = codec.Pixels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var bitmap = SKBitmap.Decode (Path.Combine (PathToImages, "baboon.png"))) {
|
|
|
|
|
bitmapPixels = bitmap.Bytes;
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (codecPixels, bitmapPixels);
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 04:19:59 +03:00
|
|
|
|
[SkippableFact]
|
2016-06-17 23:54:08 +03:00
|
|
|
|
public void BitmapDecodesCorrectlyWithManagedStream ()
|
|
|
|
|
{
|
|
|
|
|
byte[] codecPixels;
|
|
|
|
|
byte[] bitmapPixels;
|
|
|
|
|
|
|
|
|
|
var stream = File.OpenRead (Path.Combine(PathToImages, "baboon.png"));
|
|
|
|
|
using (var codec = SKCodec.Create (new SKManagedStream (stream))) {
|
|
|
|
|
codecPixels = codec.Pixels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var bitmap = SKBitmap.Decode (Path.Combine (PathToImages, "baboon.png"))) {
|
|
|
|
|
bitmapPixels = bitmap.Bytes;
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-14 02:51:18 +03:00
|
|
|
|
Assert.Equal (codecPixels, bitmapPixels);
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
2019-12-22 19:50:47 +03:00
|
|
|
|
|
2020-04-28 23:20:20 +03:00
|
|
|
|
[SkippableFact]
|
|
|
|
|
public void CanReadManagedStream()
|
|
|
|
|
{
|
|
|
|
|
using (var stream = File.OpenRead(Path.Combine(PathToImages, "baboon.png")))
|
|
|
|
|
using (var codec = SKCodec.Create(stream))
|
|
|
|
|
Assert.NotNull(codec);
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-09 05:43:26 +03:00
|
|
|
|
[SkippableFact (Skip = "This keeps breaking CI for some reason.")]
|
2018-05-09 02:18:28 +03:00
|
|
|
|
public async Task DownloadedStream ()
|
2017-12-18 02:33:26 +03:00
|
|
|
|
{
|
|
|
|
|
var httpClient = new HttpClient ();
|
2018-05-09 02:18:28 +03:00
|
|
|
|
using (var stream = await httpClient.GetStreamAsync (new Uri ("http://www.gstatic.com/webp/gallery/2.webp")))
|
|
|
|
|
using (var bitmap = SKBitmap.Decode (stream))
|
2017-12-18 02:33:26 +03:00
|
|
|
|
Assert.NotNull (bitmap);
|
|
|
|
|
}
|
2019-12-22 19:50:47 +03:00
|
|
|
|
|
2017-12-18 02:33:26 +03:00
|
|
|
|
[SkippableFact]
|
|
|
|
|
public void ReadOnlyStream ()
|
|
|
|
|
{
|
|
|
|
|
using (var stream = File.OpenRead (Path.Combine (PathToImages, "baboon.png")))
|
|
|
|
|
using (var nonSeekable = new NonSeekableReadOnlyStream (stream))
|
|
|
|
|
using (var bitmap = SKBitmap.Decode (nonSeekable))
|
|
|
|
|
Assert.NotNull (bitmap);
|
|
|
|
|
}
|
2020-04-03 14:25:20 +03:00
|
|
|
|
|
|
|
|
|
[SkippableTheory]
|
|
|
|
|
[InlineData("CMYK.jpg")]
|
|
|
|
|
[InlineData("baboon.png")]
|
|
|
|
|
[InlineData("color-wheel.png")]
|
|
|
|
|
public void CanDecodePath(string image)
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine(PathToImages, image);
|
|
|
|
|
|
|
|
|
|
using var codec = SKCodec.Create(path);
|
|
|
|
|
Assert.NotNull(codec);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels));
|
|
|
|
|
Assert.NotEmpty(pixels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SkippableTheory]
|
|
|
|
|
[InlineData("CMYK.jpg")]
|
|
|
|
|
[InlineData("baboon.png")]
|
|
|
|
|
[InlineData("color-wheel.png")]
|
|
|
|
|
public void CanDecodeData(string image)
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine(PathToImages, image);
|
|
|
|
|
|
|
|
|
|
using var data = SKData.Create(path);
|
|
|
|
|
Assert.NotNull(data);
|
|
|
|
|
|
|
|
|
|
using var codec = SKCodec.Create(data);
|
|
|
|
|
Assert.NotNull(codec);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels));
|
|
|
|
|
Assert.NotEmpty(pixels);
|
|
|
|
|
}
|
2016-06-17 23:54:08 +03:00
|
|
|
|
}
|
|
|
|
|
}
|