mirror of https://github.com/SixLabors/ImageSharp
46 changed files with 1293 additions and 689 deletions
@ -0,0 +1,67 @@ |
|||||
|
// <copyright file="Image.Decode.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using Formats; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha
|
||||
|
/// packed into a single unsigned integer value.
|
||||
|
/// </summary>
|
||||
|
public sealed partial class Image |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Decodes the image stream to the current image.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">the configuration.</param>
|
||||
|
/// <param name="img">The decoded image</param>
|
||||
|
/// <returns>
|
||||
|
/// [true] if can successfull decode the image otherwise [false].
|
||||
|
/// </returns>
|
||||
|
private static bool Decode<TColor>(Stream stream, IDecoderOptions options, Configuration config, out Image<TColor> img) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
img = null; |
||||
|
int maxHeaderSize = config.MaxHeaderSize; |
||||
|
if (maxHeaderSize <= 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
IImageFormat format; |
||||
|
byte[] header = ArrayPool<byte>.Shared.Rent(maxHeaderSize); |
||||
|
try |
||||
|
{ |
||||
|
long startPosition = stream.Position; |
||||
|
stream.Read(header, 0, maxHeaderSize); |
||||
|
stream.Position = startPosition; |
||||
|
format = config.ImageFormats.FirstOrDefault(x => x.IsSupportedFileFormat(header)); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
ArrayPool<byte>.Shared.Return(header); |
||||
|
} |
||||
|
|
||||
|
if (format == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
img = format.Decoder.Decode<TColor>(stream, options); |
||||
|
img.CurrentImageFormat = format; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
// <copyright file="Image.FromStream.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using Formats; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha
|
||||
|
/// packed into a single unsigned integer value.
|
||||
|
/// </summary>
|
||||
|
public sealed partial class Image |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(byte[] stream) |
||||
|
{ |
||||
|
return Load(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(byte[] stream, IDecoderOptions options) |
||||
|
{ |
||||
|
return Load(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(byte[] stream, Configuration config) |
||||
|
{ |
||||
|
return Load(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(byte[] stream, IDecoderOptions options, Configuration config) |
||||
|
{ |
||||
|
using (MemoryStream ms = new MemoryStream(stream)) |
||||
|
{ |
||||
|
return Load(ms, options, config); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(byte[] stream) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(byte[] stream, IDecoderOptions options) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(byte[] stream, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(byte[] stream, IDecoderOptions options, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
using (MemoryStream ms = new MemoryStream(stream)) |
||||
|
{ |
||||
|
return Load<TColor>(ms, options, config); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
// <copyright file="Image.FromStream.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using Formats; |
||||
|
|
||||
|
#if !NETSTANDARD1_1
|
||||
|
/// <summary>
|
||||
|
/// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha
|
||||
|
/// packed into a single unsigned integer value.
|
||||
|
/// </summary>
|
||||
|
public sealed partial class Image |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(string stream) |
||||
|
{ |
||||
|
return Load(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(string stream, IDecoderOptions options) |
||||
|
{ |
||||
|
return Load(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(string stream, Configuration config) |
||||
|
{ |
||||
|
return Load(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(string stream, IDecoderOptions options, Configuration config) |
||||
|
{ |
||||
|
return new Image(Image.Load<Color>(stream, options, config)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(string stream) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(string stream, IDecoderOptions options) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(string stream, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(string stream, IDecoderOptions options, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
config = config ?? Configuration.Default; |
||||
|
using (Stream s = config.FileSystem.OpenRead(stream)) |
||||
|
{ |
||||
|
return Load<TColor>(s, options, config); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
#endif
|
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
// <copyright file="Image.FromStream.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using Formats; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha
|
||||
|
/// packed into a single unsigned integer value.
|
||||
|
/// </summary>
|
||||
|
public sealed partial class Image |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(Stream stream) |
||||
|
{ |
||||
|
return Load(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(Stream stream, IDecoderOptions options) |
||||
|
{ |
||||
|
return Load(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(Stream stream, Configuration config) |
||||
|
{ |
||||
|
return Load(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image Load(Stream stream, IDecoderOptions options, Configuration config) |
||||
|
{ |
||||
|
return new Image(Load<Color>(stream, options, config)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(Stream stream) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(Stream stream, IDecoderOptions options) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, options, null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="config">The config for the decoder.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(Stream stream, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
return Load<TColor>(stream, null, config); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads the image from the given stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TColor">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream containing image information.</param>
|
||||
|
/// <param name="options">The options for the decoder.</param>
|
||||
|
/// <param name="config">The configuration options.</param>
|
||||
|
/// <exception cref="NotSupportedException">
|
||||
|
/// Thrown if the stream is not readable nor seekable.
|
||||
|
/// </exception>
|
||||
|
/// <returns>The image</returns>
|
||||
|
public static Image<TColor> Load<TColor>(Stream stream, IDecoderOptions options, Configuration config) |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
config = config ?? Configuration.Default; |
||||
|
|
||||
|
if (!config.ImageFormats.Any()) |
||||
|
{ |
||||
|
throw new InvalidOperationException("No image formats have been configured."); |
||||
|
} |
||||
|
|
||||
|
if (!stream.CanRead) |
||||
|
{ |
||||
|
throw new NotSupportedException("Cannot read from the stream."); |
||||
|
} |
||||
|
|
||||
|
if (stream.CanSeek) |
||||
|
{ |
||||
|
if (Decode(stream, options, config, out Image<TColor> img)) |
||||
|
{ |
||||
|
return img; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// We want to be able to load images from things like HttpContext.Request.Body
|
||||
|
using (MemoryStream ms = new MemoryStream()) |
||||
|
{ |
||||
|
stream.CopyTo(ms); |
||||
|
ms.Position = 0; |
||||
|
|
||||
|
if (Decode(ms, options, config, out Image<TColor> img)) |
||||
|
{ |
||||
|
return img; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
StringBuilder stringBuilder = new StringBuilder(); |
||||
|
stringBuilder.AppendLine("Image cannot be loaded. Available formats:"); |
||||
|
|
||||
|
foreach (IImageFormat format in config.ImageFormats) |
||||
|
{ |
||||
|
stringBuilder.AppendLine("-" + format); |
||||
|
} |
||||
|
|
||||
|
throw new NotSupportedException(stringBuilder.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,341 @@ |
|||||
|
// <copyright file="PixelAccessorTests.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp.Tests |
||||
|
{ |
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using ImageSharp.Formats; |
||||
|
using ImageSharp.IO; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tests the <see cref="Image"/> class.
|
||||
|
/// </summary>
|
||||
|
public class ImageLoadTests : IDisposable |
||||
|
{ |
||||
|
private readonly Mock<IFileSystem> fileSystem; |
||||
|
private readonly IDecoderOptions decoderOptions; |
||||
|
private Image<Color> returnImage; |
||||
|
private Mock<IImageDecoder> localDecoder; |
||||
|
private Mock<IImageFormat> localFormat; |
||||
|
private readonly string FilePath; |
||||
|
|
||||
|
public Configuration LocalConfiguration { get; private set; } |
||||
|
public byte[] Marker { get; private set; } |
||||
|
public MemoryStream DataStream { get; private set; } |
||||
|
public byte[] DecodedData { get; private set; } |
||||
|
|
||||
|
public ImageLoadTests() |
||||
|
{ |
||||
|
this.returnImage = new Image<Color>(1, 1); |
||||
|
|
||||
|
this.localDecoder = new Mock<IImageDecoder>(); |
||||
|
this.localFormat = new Mock<IImageFormat>(); |
||||
|
this.localFormat.Setup(x => x.Decoder).Returns(this.localDecoder.Object); |
||||
|
this.localFormat.Setup(x => x.Encoder).Returns(new Mock<IImageEncoder>().Object); |
||||
|
this.localFormat.Setup(x => x.MimeType).Returns("img/test"); |
||||
|
this.localFormat.Setup(x => x.Extension).Returns("png"); |
||||
|
this.localFormat.Setup(x => x.HeaderSize).Returns(1); |
||||
|
this.localFormat.Setup(x => x.IsSupportedFileFormat(It.IsAny<byte[]>())).Returns(true); |
||||
|
this.localFormat.Setup(x => x.SupportedExtensions).Returns(new string[] { "png", "jpg" }); |
||||
|
this.localDecoder.Setup(x => x.Decode<Color>(It.IsAny<Stream>(), It.IsAny<IDecoderOptions>())) |
||||
|
.Callback<Stream, IDecoderOptions>((s, o) => { |
||||
|
using (var ms = new MemoryStream()) |
||||
|
{ |
||||
|
s.CopyTo(ms); |
||||
|
this.DecodedData = ms.ToArray(); |
||||
|
} |
||||
|
}) |
||||
|
.Returns(this.returnImage); |
||||
|
|
||||
|
this.fileSystem = new Mock<IFileSystem>(); |
||||
|
|
||||
|
this.LocalConfiguration = new Configuration(this.localFormat.Object) |
||||
|
{ |
||||
|
FileSystem = this.fileSystem.Object |
||||
|
}; |
||||
|
TestFormat.RegisterGloablTestFormat(); |
||||
|
this.Marker = Guid.NewGuid().ToByteArray(); |
||||
|
this.DataStream = TestFormat.GlobalTestFormat.CreateStream(this.Marker); |
||||
|
this.decoderOptions = new Mock<IDecoderOptions>().Object; |
||||
|
|
||||
|
this.FilePath = Guid.NewGuid().ToString(); |
||||
|
this.fileSystem.Setup(x => x.OpenRead(this.FilePath)).Returns(this.DataStream); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStream() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
|
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithType() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithOptions() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream, this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithTypeAndOptions() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream, this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithConfig() |
||||
|
{ |
||||
|
Stream stream = new MemoryStream(); |
||||
|
Image img = Image.Load(stream, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(stream, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithTypeAndConfig() |
||||
|
{ |
||||
|
Stream stream = new MemoryStream(); |
||||
|
Image<Color> img = Image.Load<Color>(stream, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(stream, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithConfigAndOptions() |
||||
|
{ |
||||
|
Stream stream = new MemoryStream(); |
||||
|
Image img = Image.Load(stream, this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(stream, this.decoderOptions)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromStreamWithTypeAndConfigAndOptions() |
||||
|
{ |
||||
|
Stream stream = new MemoryStream(); |
||||
|
Image<Color> img = Image.Load<Color>(stream, this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(stream, this.decoderOptions)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytes() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream.ToArray()); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
|
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithType() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream.ToArray()); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithOptions() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream.ToArray(), this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithTypeAndOptions() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream.ToArray(), this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithConfig() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream.ToArray(), this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(It.IsAny<Stream>(), null)); |
||||
|
Assert.Equal(this.DataStream.ToArray(), this.DecodedData); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithTypeAndConfig() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream.ToArray(), this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
|
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(It.IsAny<Stream>(), null)); |
||||
|
Assert.Equal(this.DataStream.ToArray(), this.DecodedData); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithConfigAndOptions() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream.ToArray(), this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(It.IsAny<Stream>(), this.decoderOptions)); |
||||
|
Assert.Equal(this.DataStream.ToArray(), this.DecodedData); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromBytesWithTypeAndConfigAndOptions() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream.ToArray(), this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(It.IsAny<Stream>(), this.decoderOptions)); |
||||
|
Assert.Equal(this.DataStream.ToArray(), this.DecodedData); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFile() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
|
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithType() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithOptions() |
||||
|
{ |
||||
|
Image img = Image.Load(this.DataStream, this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithTypeAndOptions() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.DataStream, this.decoderOptions); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat.Sample<Color>(), img); |
||||
|
Assert.Equal(TestFormat.GlobalTestFormat, img.CurrentImageFormat); |
||||
|
TestFormat.GlobalTestFormat.VerifyDecodeCall(this.Marker, this.decoderOptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithConfig() |
||||
|
{ |
||||
|
Image img = Image.Load(this.FilePath, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(this.DataStream, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithTypeAndConfig() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(this.FilePath, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(this.DataStream, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithConfigAndOptions() |
||||
|
{ |
||||
|
Image img = Image.Load(this.FilePath, this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(this.DataStream, this.decoderOptions)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void LoadFromFileWithTypeAndConfigAndOptions() |
||||
|
{ |
||||
|
Image<Color> img = Image.Load<Color>(FilePath, this.decoderOptions, this.LocalConfiguration); |
||||
|
|
||||
|
Assert.NotNull(img); |
||||
|
Assert.Equal(this.returnImage, img); |
||||
|
Assert.Equal(this.localFormat.Object, img.CurrentImageFormat); |
||||
|
this.localDecoder.Verify(x => x.Decode<Color>(this.DataStream, this.decoderOptions)); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
// clean up the global object;
|
||||
|
this.returnImage?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,174 @@ |
|||||
|
// <copyright file="TestImage.cs" company="James Jackson-South">
|
||||
|
// Copyright (c) James Jackson-South and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
|
||||
|
namespace ImageSharp.Tests |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using ImageSharp.Formats; |
||||
|
using Xunit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A test image file.
|
||||
|
/// </summary>
|
||||
|
public class TestFormat : ImageSharp.Formats.IImageFormat |
||||
|
{ |
||||
|
public static TestFormat GlobalTestFormat { get; } = new TestFormat(); |
||||
|
|
||||
|
public static void RegisterGloablTestFormat() |
||||
|
{ |
||||
|
Configuration.Default.AddImageFormat(GlobalTestFormat); |
||||
|
} |
||||
|
|
||||
|
public TestFormat() |
||||
|
{ |
||||
|
this.Encoder = new TestEncoder(this); ; |
||||
|
this.Decoder = new TestDecoder(this); ; |
||||
|
} |
||||
|
|
||||
|
public List<DecodeOperation> DecodeCalls { get; } = new List<DecodeOperation>(); |
||||
|
|
||||
|
public IImageEncoder Encoder { get; } |
||||
|
|
||||
|
public IImageDecoder Decoder { get; } |
||||
|
|
||||
|
private byte[] header = Guid.NewGuid().ToByteArray(); |
||||
|
|
||||
|
public MemoryStream CreateStream(byte[] marker = null) |
||||
|
{ |
||||
|
MemoryStream ms = new MemoryStream(); |
||||
|
byte[] data = this.header; |
||||
|
ms.Write(data, 0, data.Length); |
||||
|
if (marker != null) |
||||
|
{ |
||||
|
ms.Write(marker, 0, marker.Length); |
||||
|
} |
||||
|
ms.Position = 0; |
||||
|
return ms; |
||||
|
} |
||||
|
|
||||
|
Dictionary<Type, object> _sampleImages = new Dictionary<Type, object>(); |
||||
|
|
||||
|
public void VerifyDecodeCall(byte[] marker, IDecoderOptions options) |
||||
|
{ |
||||
|
DecodeOperation[] discovered = this.DecodeCalls.Where(x => x.IsMatch(marker, options)).ToArray(); |
||||
|
|
||||
|
Assert.True(discovered.Any(), "No calls to decode on this formate with the proveded options happend"); |
||||
|
|
||||
|
foreach (DecodeOperation d in discovered) { |
||||
|
this.DecodeCalls.Remove(d); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Image<TColor> Sample<TColor>() |
||||
|
where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
lock (this._sampleImages) |
||||
|
{ |
||||
|
if (!this._sampleImages.ContainsKey(typeof(TColor))) |
||||
|
{ |
||||
|
this._sampleImages.Add(typeof(TColor), new Image<TColor>(1, 1)); |
||||
|
} |
||||
|
|
||||
|
return (Image<TColor>)this._sampleImages[typeof(TColor)]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string MimeType => "img/test"; |
||||
|
|
||||
|
public string Extension => "test_ext"; |
||||
|
|
||||
|
public IEnumerable<string> SupportedExtensions => new[] { "test_ext" }; |
||||
|
|
||||
|
public int HeaderSize => this.header.Length; |
||||
|
|
||||
|
public bool IsSupportedFileFormat(byte[] header) |
||||
|
{ |
||||
|
if (header.Length < this.header.Length) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
for (int i = 0; i < this.header.Length; i++) |
||||
|
{ |
||||
|
if (header[i] != this.header[i]) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
public struct DecodeOperation |
||||
|
{ |
||||
|
public byte[] marker; |
||||
|
public IDecoderOptions options; |
||||
|
|
||||
|
public bool IsMatch(byte[] testMarker, IDecoderOptions testOptions) |
||||
|
{ |
||||
|
if(this.options != testOptions) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (testMarker.Length != this.marker.Length) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < this.marker.Length; i++) |
||||
|
{ |
||||
|
if (testMarker[i] != this.marker[i]) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class TestDecoder : ImageSharp.Formats.IImageDecoder |
||||
|
{ |
||||
|
private TestFormat testFormat; |
||||
|
|
||||
|
public TestDecoder(TestFormat testFormat) |
||||
|
{ |
||||
|
this.testFormat = testFormat; |
||||
|
} |
||||
|
|
||||
|
public Image<TColor> Decode<TColor>(Stream stream, IDecoderOptions options) where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
var ms = new MemoryStream(); |
||||
|
stream.CopyTo(ms); |
||||
|
var marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray(); |
||||
|
this.testFormat.DecodeCalls.Add(new DecodeOperation |
||||
|
{ |
||||
|
marker = marker, |
||||
|
options = options |
||||
|
}); |
||||
|
|
||||
|
// TODO record this happend so we an verify it.
|
||||
|
return this.testFormat.Sample<TColor>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class TestEncoder : ImageSharp.Formats.IImageEncoder |
||||
|
{ |
||||
|
private TestFormat testFormat; |
||||
|
|
||||
|
public TestEncoder(TestFormat testFormat) |
||||
|
{ |
||||
|
this.testFormat = testFormat; |
||||
|
} |
||||
|
|
||||
|
public void Encode<TColor>(Image<TColor> image, Stream stream, IEncoderOptions options) where TColor : struct, IPixel<TColor> |
||||
|
{ |
||||
|
// TODO record this happend so we an verify it.
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue