mirror of https://github.com/SixLabors/ImageSharp
Browse Source
* Attempt to use same weight generation algorithm as resize.
* tests pass
* Identical output
* Update LinearTransformKernelFactory{TResampler}.cs
* Use new low allocation iterator
* Migrate projective transforms.
* Optimizations
* Smaller kernel
* Fix sampling accuracy
* Finalize and update refs
* Revert unnecessary changes
* Remove enumerator
* Actually save output for debugging.
* Use custom test png encoder for reduced memory environments
* Convolution should use scaled vectors
* Update TestEnvironmentTests.cs
* Try using doubles
* Moar double precision
* Fix radius calculation
* Test if issue is SIMD related.
* Detect runtime to determine pipeline.
* Fix stack overflow
* fix condition
* Try simplified scalar run
* Simplify unpremultiply scalar
* Update Numerics.cs
* Fix runtime environment
* Update ImageSharp.csproj
* Duplicate the caller with scalar versions
* Update method name, exclude from coverage.
* Don't save output during coverage tests for perf.
* Update src/ImageSharp/Common/Helpers/RuntimeEnvironment.cs
Co-authored-by: Anton Firszov <antonfir@gmail.com>
Co-authored-by: Anton Firszov <antonfir@gmail.com>
pull/1600/head
committed by
GitHub
32 changed files with 604 additions and 351 deletions
@ -0,0 +1,32 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides information about the .NET runtime installation.
|
||||
|
/// Many methods defer to <see cref="RuntimeInformation"/> when available.
|
||||
|
/// </summary>
|
||||
|
internal static class RuntimeEnvironment |
||||
|
{ |
||||
|
private static readonly Lazy<bool> IsNetCoreLazy = new Lazy<bool>(() => FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the .NET installation is .NET Core 3.1 or lower.
|
||||
|
/// </summary>
|
||||
|
public static bool IsNetCore => IsNetCoreLazy.Value; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the name of the .NET installation on which an app is running.
|
||||
|
/// </summary>
|
||||
|
public static string FrameworkDescription => RuntimeInformation.FrameworkDescription; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates whether the current application is running on the specified platform.
|
||||
|
/// </summary>
|
||||
|
public static bool IsOSPlatform(OSPlatform osPlatform) => RuntimeInformation.IsOSPlatform(osPlatform); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Utility methods for linear transforms.
|
||||
|
/// </summary>
|
||||
|
internal static class LinearTransformUtility |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Returns the sampling radius for the given sampler and dimensions.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResampler">The type of resampler.</typeparam>
|
||||
|
/// <param name="sampler">The resampler sampler.</param>
|
||||
|
/// <param name="sourceSize">The source size.</param>
|
||||
|
/// <param name="destinationSize">The destination size.</param>
|
||||
|
/// <returns>The <see cref="float"/>.</returns>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public static float GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize) |
||||
|
where TResampler : struct, IResampler |
||||
|
{ |
||||
|
float scale = (float)sourceSize / destinationSize; |
||||
|
|
||||
|
if (scale < 1F) |
||||
|
{ |
||||
|
scale = 1F; |
||||
|
} |
||||
|
|
||||
|
return MathF.Ceiling(sampler.Radius * scale); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the start position (inclusive) for a sampling range given
|
||||
|
/// the radius, center position and max constraint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="radius">The radius.</param>
|
||||
|
/// <param name="center">The center position.</param>
|
||||
|
/// <param name="max">The max allowed amouunt.</param>
|
||||
|
/// <returns>The <see cref="int"/>.</returns>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public static int GetRangeStart(float radius, float center, int max) |
||||
|
=> Numerics.Clamp((int)MathF.Ceiling(center - radius), 0, max); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the end position (inclusive) for a sampling range given
|
||||
|
/// the radius, center position and max constraint.
|
||||
|
/// </summary>
|
||||
|
/// <param name="radius">The radius.</param>
|
||||
|
/// <param name="center">The center position.</param>
|
||||
|
/// <param name="max">The max allowed amouunt.</param>
|
||||
|
/// <returns>The <see cref="int"/>.</returns>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public static int GetRangeEnd(float radius, float center, int max) |
||||
|
=> Numerics.Clamp((int)MathF.Floor(center + radius), 0, max); |
||||
|
} |
||||
|
} |
||||
@ -1,104 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Numerics; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Utility methods for affine and projective transforms.
|
|
||||
/// </summary>
|
|
||||
internal static class LinearTransformUtils |
|
||||
{ |
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
internal static int GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize) |
|
||||
where TResampler : struct, IResampler |
|
||||
{ |
|
||||
double scale = sourceSize / destinationSize; |
|
||||
if (scale < 1) |
|
||||
{ |
|
||||
scale = 1; |
|
||||
} |
|
||||
|
|
||||
return (int)Math.Ceiling(scale * sampler.Radius); |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
internal static void Convolve<TResampler, TPixel>( |
|
||||
in TResampler sampler, |
|
||||
Vector2 transformedPoint, |
|
||||
Buffer2D<TPixel> sourcePixels, |
|
||||
Span<Vector4> targetRow, |
|
||||
int column, |
|
||||
ref float yKernelSpanRef, |
|
||||
ref float xKernelSpanRef, |
|
||||
Vector2 radialExtents, |
|
||||
Vector4 maxSourceExtents) |
|
||||
where TResampler : struct, IResampler |
|
||||
where TPixel : unmanaged, IPixel<TPixel> |
|
||||
{ |
|
||||
// Clamp sampling pixel radial extents to the source image edges
|
|
||||
Vector2 minXY = transformedPoint - radialExtents; |
|
||||
Vector2 maxXY = transformedPoint + radialExtents; |
|
||||
|
|
||||
// left, top, right, bottom
|
|
||||
var sourceExtents = new Vector4( |
|
||||
MathF.Ceiling(minXY.X), |
|
||||
MathF.Ceiling(minXY.Y), |
|
||||
MathF.Floor(maxXY.X), |
|
||||
MathF.Floor(maxXY.Y)); |
|
||||
|
|
||||
sourceExtents = Numerics.Clamp(sourceExtents, Vector4.Zero, maxSourceExtents); |
|
||||
|
|
||||
int left = (int)sourceExtents.X; |
|
||||
int top = (int)sourceExtents.Y; |
|
||||
int right = (int)sourceExtents.Z; |
|
||||
int bottom = (int)sourceExtents.W; |
|
||||
|
|
||||
if (left == right || top == bottom) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
CalculateWeights(in sampler, top, bottom, transformedPoint.Y, ref yKernelSpanRef); |
|
||||
CalculateWeights(in sampler, left, right, transformedPoint.X, ref xKernelSpanRef); |
|
||||
|
|
||||
Vector4 sum = Vector4.Zero; |
|
||||
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++) |
|
||||
{ |
|
||||
float yWeight = Unsafe.Add(ref yKernelSpanRef, kernelY); |
|
||||
|
|
||||
for (int kernelX = 0, x = left; x <= right; x++, kernelX++) |
|
||||
{ |
|
||||
float xWeight = Unsafe.Add(ref xKernelSpanRef, kernelX); |
|
||||
|
|
||||
// Values are first premultiplied to prevent darkening of edge pixels.
|
|
||||
var current = sourcePixels[x, y].ToVector4(); |
|
||||
Numerics.Premultiply(ref current); |
|
||||
sum += current * xWeight * yWeight; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Reverse the premultiplication
|
|
||||
Numerics.UnPremultiply(ref sum); |
|
||||
targetRow[column] = sum; |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
private static void CalculateWeights<TResampler>(in TResampler sampler, int min, int max, float point, ref float weightsRef) |
|
||||
where TResampler : struct, IResampler |
|
||||
{ |
|
||||
float sum = 0; |
|
||||
for (int x = 0, i = min; i <= max; i++, x++) |
|
||||
{ |
|
||||
float weight = sampler.GetValue(i - point); |
|
||||
sum += weight; |
|
||||
Unsafe.Add(ref weightsRef, x) = weight; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,41 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System.Runtime.InteropServices; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable IDE0022 // Use expression body for methods
|
||||
|
namespace SixLabors.ImageSharp.Tests.Helpers |
||||
|
{ |
||||
|
public class RuntimeEnvironmentTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void CanDetectNetCore() |
||||
|
{ |
||||
|
#if NET5_0_OR_GREATER
|
||||
|
Assert.False(RuntimeEnvironment.IsNetCore); |
||||
|
#elif NETCOREAPP
|
||||
|
Assert.True(RuntimeEnvironment.IsNetCore); |
||||
|
#else
|
||||
|
Assert.False(RuntimeEnvironment.IsNetCore); |
||||
|
#endif
|
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanDetectOSPlatform() |
||||
|
{ |
||||
|
if (TestEnvironment.IsLinux) |
||||
|
{ |
||||
|
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.Linux)); |
||||
|
} |
||||
|
else if (TestEnvironment.IsOSX) |
||||
|
{ |
||||
|
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.OSX)); |
||||
|
} |
||||
|
else if (TestEnvironment.IsWindows) |
||||
|
{ |
||||
|
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.Windows)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using SixLabors.ImageSharp.Formats; |
||||
|
using SixLabors.ImageSharp.Formats.Png; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Png encoder that uses the ImageSharp core encoder but the default configuration.
|
||||
|
/// This allows encoding under environments with restricted memory.
|
||||
|
/// </summary>
|
||||
|
public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder, IPngEncoderOptions |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public PngBitDepth? BitDepth { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngColorType? ColorType { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngFilterMethod? FilterMethod { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public int TextCompressionThreshold { get; set; } = 1024; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public float? Gamma { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IQuantizer Quantizer { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public byte Threshold { get; set; } = byte.MaxValue; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngInterlaceMode? InterlaceMethod { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngChunkFilter? ChunkFilter { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool IgnoreMetadata { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public PngTransparentColorMode TransparentColorMode { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
|
||||
|
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
|
||||
|
public void Encode<TPixel>(Image<TPixel> image, Stream stream) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Configuration configuration = Configuration.Default; |
||||
|
MemoryAllocator allocator = configuration.MemoryAllocator; |
||||
|
|
||||
|
using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this)); |
||||
|
encoder.Encode(image, stream); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
|
||||
|
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
|
||||
|
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
|
public async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Configuration configuration = Configuration.Default; |
||||
|
MemoryAllocator allocator = configuration.MemoryAllocator; |
||||
|
|
||||
|
// The introduction of a local variable that refers to an object the implements
|
||||
|
// IDisposable means you must use async/await, where the compiler generates the
|
||||
|
// state machine and a continuation.
|
||||
|
using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this)); |
||||
|
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:57376aa4446fc2d034d2a0cb627163ac416d1b6768b063b2c11ccf8517443bda |
oid sha256:2ef489dc0837b382ad7c7ead6b7c7042dfbfba39902d4cc81b5f3805d5b03967 |
||||
size 10135 |
size 9175 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:9c83b59471a50df9e1d7b8f0e35c50aa417cd3be1730d6369f47f5cc99b87cef |
oid sha256:99d6c1d6b092a2feba2aebe2e09c521c3cc9682f3d748927cdc3cbaa38448b28 |
||||
size 6405 |
size 710 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:f9f149093c5d9d487f3e16cb59e6807ea6ce581b5c307e57aeb4edaf921de9ca |
oid sha256:04a3b84c668758b586f4d998f080ef96b09f726b0298b28f5dd3d739b0e90744 |
||||
size 15138 |
size 13138 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:f9f149093c5d9d487f3e16cb59e6807ea6ce581b5c307e57aeb4edaf921de9ca |
oid sha256:04a3b84c668758b586f4d998f080ef96b09f726b0298b28f5dd3d739b0e90744 |
||||
size 15138 |
size 13138 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:0c7467702a784807a3a5813a75d5f643655ed5ad427e978d6ee079da67b05961 |
oid sha256:6ea7ca66c31474c0bb9673a0d85c1c7465e387ebabf4e2d1e8f9daebfc7c8f34 |
||||
size 15363 |
size 13956 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:a836c63c0912f69683bc9c8f59687b11e6b84f257816a01ff979ad0b6f4ab656 |
oid sha256:6dd98ac441f3c20ea999f058c7b21601d5981d46e9b77709c25f2930a64edb93 |
||||
size 19059 |
size 17148 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:05de30dc282a0b8a096a476f689a1d2b6bb298098692a2f665c59c3d14902aa6 |
oid sha256:d5c4772d9b9dc57c4b5d47450ec9d02d96e40656cf2015f171b5425945f8a598 |
||||
size 20426 |
size 18726 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:8892179d8edf96583900048bdd895eff24e57be0105e91efafe1de971414db0e |
oid sha256:bb025c4470cec1b0d6544924e46b84cbdb90d75da5d0f879f2c7d7ec9875dee2 |
||||
size 22457 |
size 20574 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:2f351ec6ae6df56537e383494984c2fbcf35a66c44a6ce53d6fd8d6d74a330f3 |
oid sha256:c4abaa06827cb779026f8fbb655692bdd8adab37ae5b00c3ae18ebea456eb8d9 |
||||
size 15342 |
size 13459 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:b0fb38dbdded32a1518d62ac79f4aa88133aaddad947f23c1066dc33d6938e0b |
oid sha256:3435ade8f7988779280820342e16881b049f717735d2218ac5a971a1bd807db1 |
||||
size 15372 |
size 13448 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:0be42a5fd4ff10680af74a99ed1e0561ae38181f4efe4641bd63891222dcdf3c |
oid sha256:f0098aa45e820544dd16a58396fa70860886b7d79900916ed97376a8328a5ff2 |
||||
size 15283 |
size 13367 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:1eabaf35e0dda3eccd5e8866ddd65e0c45557c8d5cc29423a99e2f377ee1bfa9 |
oid sha256:7729495277b80a42e24dd8f40cdd8a280443575710fb4e199e4871c28b558271 |
||||
size 16271 |
size 14253 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:5ab9bea8f45e7d29d7a8c5e3d0182c0f3f3aa7014aa358883dee53db6dfeb3f7 |
oid sha256:a2304c234b93bdabaa018263dec70e62090ad1bbb7005ef62643b88600a863fb |
||||
size 14076 |
size 12157 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:ea452bc46c508f6990870a34d908224a58aa350c4ccebabd4fa6ba138e8034a0 |
oid sha256:54b0da9646b7f4cf83df784d69dfbec48e0bdc1788d70a9872817543f72f57c1 |
||||
size 18383 |
size 16829 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:2c83df7a2f70aec8f150799055ce42db09568b47b95216c91a79233ce69381d5 |
oid sha256:233d8c6c9e101dddf5d210d47c7a20807f8f956738289068ea03b774258ef8c6 |
||||
size 191563 |
size 182754 |
||||
|
|||||
Loading…
Reference in new issue