Uno.SkiaSharp/tests/Tests/SKCodecTest.cs

496 строки
14 KiB
C#
Исходник Обычный вид История

2016-06-17 23:54:08 +03:00
using System;
using System.IO;
2017-12-18 02:33:26 +03:00
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
2016-06-17 23:54:08 +03:00
namespace SkiaSharp.Tests
{
public class SKCodecTest : SKTest
{
[SkippableFact]
public void MinBufferedBytesNeededHasAValue()
2016-06-17 23:54:08 +03:00
{
Assert.True(SKCodec.MinBufferedBytesNeeded > 0);
2016-06-17 23:54:08 +03:00
}
[SkippableFact]
public unsafe void ImageCanBeDecodedManyTimes()
{
var codec = SKCodec.Create(Path.Combine(PathToImages, "color-wheel.png"));
for (var i = 0; i < 1000; ++i)
{
Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _));
}
}
[SkippableTheory]
[InlineData("P8211052.JPG", SKEncodedOrigin.LeftBottom)]
[InlineData("PA010741.JPG", SKEncodedOrigin.LeftBottom)]
public void CodecCanLoadCorrectOrigin(string image, SKEncodedOrigin origin)
{
var codec = SKCodec.Create(Path.Combine(PathToImages, image));
Assert.Equal(origin, codec.EncodedOrigin);
}
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);
}
}
[SkippableFact]
2020-06-20 20:18:52 +03:00
public void CanCreateStreamCodec()
2016-06-17 23:54:08 +03:00
{
2020-06-20 20:36:57 +03:00
var stream = new SKFileStream(Path.Combine(PathToImages, "color-wheel.png"));
Assert.True(stream.IsValid);
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:31:31 +03:00
var stream = new SKFileStream(Path.Combine(PathToImages, "color-wheel.png"));
Assert.True(stream.IsValid);
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
}
[SkippableFact]
public void GetGifFrames()
{
const int FrameCount = 16;
var stream = new SKFileStream(Path.Combine(PathToImages, "animated-heart.gif"));
using (var codec = SKCodec.Create(stream))
{
Assert.Equal(-1, codec.RepetitionCount);
var frameInfos = codec.FrameInfo;
Assert.Equal(FrameCount, frameInfos.Length);
Assert.Equal(-1, frameInfos[0].RequiredFrame);
var cachedFrames = new SKBitmap[FrameCount];
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decode = new Action<SKBitmap, int, int>((bm, cachedIndex, index) =>
{
2018-07-12 14:32:02 +03:00
var decodeInfo = info;
if (index > 0)
{
decodeInfo = info.WithAlphaType(frameInfos[index].AlphaType);
2018-07-12 14:32:02 +03:00
}
Assert.True(bm.TryAllocPixels(decodeInfo));
if (cachedIndex != -1)
{
Assert.True(cachedFrames[cachedIndex].CopyTo(bm));
2018-07-12 14:32:02 +03:00
}
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);
}
Assert.Equal(SKCodecResult.Success, result);
});
for (var i = 0; i < FrameCount; i++)
{
var cachedFrame = cachedFrames[i] = new SKBitmap();
decode(cachedFrame, -1, i);
var uncachedFrame = new SKBitmap();
decode(uncachedFrame, frameInfos[i].RequiredFrame, i);
var cachedBytes = cachedFrame.Bytes;
var uncachedBytes = uncachedFrame.Bytes;
Assert.Equal(cachedBytes, uncachedBytes);
}
}
}
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);
}
}
}
[SkippableFact]
public void GetEncodedInfo()
2016-11-25 11:55:59 +03:00
{
var stream = new SKFileStream(Path.Combine(PathToImages, "color-wheel.png"));
using (var codec = SKCodec.Create(stream))
{
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
}
}
[SkippableFact]
public void CanGetPixels()
2016-06-17 23:54:08 +03:00
{
var stream = new SKFileStream(Path.Combine(PathToImages, "baboon.png"));
using (var codec = SKCodec.Create(stream))
{
2016-06-17 23:54:08 +03:00
var pixels = codec.Pixels;
Assert.Equal(codec.Info.BytesSize, pixels.Length);
2016-06-17 23:54:08 +03:00
}
}
[SkippableFact]
public void DecodeImageScanlines()
2017-02-06 14:17:57 +03:00
{
var path = Path.Combine(PathToImages, "CMYK.jpg");
2017-02-06 14:17:57 +03:00
var imageHeight = 516;
var fileData = File.ReadAllBytes(path);
var correctBitmap = SKBitmap.Decode(path);
2017-02-06 14:17:57 +03:00
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);
2017-02-06 14:17:57 +03:00
var result = codec.StartScanlineDecode(info);
Assert.Equal(SKCodecResult.Success, result);
2017-02-06 14:17:57 +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)
{
Assert.Equal(1, codec.GetScanlines(scanlineBitmap.GetAddress(0, y), 1, info.RowBytes));
Assert.Equal(y + 1, codec.NextScanline);
if (codec.SkipScanlines(1))
Assert.Equal(y + 2, codec.NextScanline);
2017-02-06 14:17:57 +03:00
else
Assert.Equal(imageHeight, codec.NextScanline); // reached the end
2017-02-06 14:17:57 +03:00
}
Assert.False(codec.SkipScanlines(1));
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++)
{
2017-02-06 14:17:57 +03:00
if (y % 2 == 0)
Assert.Equal(correctBitmap.GetPixel(x, y), scanlineBitmap.GetPixel(x, y));
2017-02-06 14:17:57 +03:00
else
Assert.Equal(SKColors.Fuchsia, scanlineBitmap.GetPixel(x, y));
2017-02-06 14:17:57 +03:00
}
}
}
}
}
[SkippableFact]
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
Assert.Equal(SKCodecResult.Success, result);
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
Assert.Equal(correctBytes, incremental.Pixels);
}
}
}
[SkippableFact]
public void BitmapDecodesCorrectly()
2016-06-17 23:54:08 +03:00
{
byte[] codecPixels;
byte[] bitmapPixels;
using (var codec = SKCodec.Create(new SKFileStream(Path.Combine(PathToImages, "baboon.png"))))
{
2016-06-17 23:54:08 +03:00
codecPixels = codec.Pixels;
}
using (var bitmap = SKBitmap.Decode(Path.Combine(PathToImages, "baboon.png")))
{
2016-06-17 23:54:08 +03:00
bitmapPixels = bitmap.Bytes;
}
Assert.Equal(codecPixels, bitmapPixels);
2016-06-17 23:54:08 +03:00
}
[SkippableFact]
public void BitmapDecodesCorrectlyWithManagedStream()
2016-06-17 23:54:08 +03:00
{
byte[] codecPixels;
byte[] bitmapPixels;
var stream = File.OpenRead(Path.Combine(PathToImages, "baboon.png"));
using (var codec = SKCodec.Create(new SKManagedStream(stream)))
{
2016-06-17 23:54:08 +03:00
codecPixels = codec.Pixels;
}
using (var bitmap = SKBitmap.Decode(Path.Combine(PathToImages, "baboon.png")))
{
2016-06-17 23:54:08 +03:00
bitmapPixels = bitmap.Bytes;
}
Assert.Equal(codecPixels, bitmapPixels);
2016-06-17 23:54:08 +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);
}
[SkippableFact(Skip = "This keeps breaking CI for some reason.")]
public async Task DownloadedStream()
2017-12-18 02:33:26 +03:00
{
var httpClient = new HttpClient();
using (var stream = await httpClient.GetStreamAsync(new Uri("http://www.gstatic.com/webp/gallery/2.webp")))
using (var bitmap = SKBitmap.Decode(stream))
Assert.NotNull(bitmap);
2017-12-18 02:33:26 +03:00
}
2017-12-18 02:33:26 +03:00
[SkippableFact]
public void ReadOnlyStream()
2017-12-18 02:33:26 +03:00
{
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);
2017-12-18 02:33:26 +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
}
}