mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
162 changed files with 2722 additions and 58 deletions
@ -0,0 +1,20 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
|
|||
/// </summary>
|
|||
public sealed class CurConfigurationModule : IImageFormatConfigurationModule |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public void Configure(Configuration configuration) |
|||
{ |
|||
configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder()); |
|||
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance); |
|||
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Defines constants relating to ICOs
|
|||
/// </summary>
|
|||
internal static class CurConstants |
|||
{ |
|||
/// <summary>
|
|||
/// The list of mime types that equate to a cur.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
|
|||
/// </remarks>
|
|||
public static readonly IEnumerable<string> MimeTypes = |
|||
[ |
|||
|
|||
// IANA-registered
|
|||
"image/vnd.microsoft.icon", |
|||
|
|||
// ICO & CUR types used by Windows
|
|||
"image/x-icon", |
|||
|
|||
// Erroneous types but have been used
|
|||
"image/ico", |
|||
"image/icon", |
|||
"text/ico", |
|||
"application/ico", |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// The list of file extensions that equate to a cur.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> FileExtensions = ["cur"]; |
|||
|
|||
public const uint FileHeader = 0x00_02_00_00; |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Decoder for generating an image out of a ico encoded stream.
|
|||
/// </summary>
|
|||
public sealed class CurDecoder : ImageDecoder |
|||
{ |
|||
private CurDecoder() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static CurDecoder Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
Image<TPixel> image = new CurDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken); |
|||
|
|||
ScaleToTargetSize(options, image); |
|||
|
|||
return image; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
=> this.Decode<Rgba32>(options, stream, cancellationToken); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
return new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
internal sealed class CurDecoderCore : IconDecoderCore |
|||
{ |
|||
public CurDecoderCore(DecoderOptions options) |
|||
: base(options) |
|||
{ |
|||
} |
|||
|
|||
protected override void SetFrameMetadata( |
|||
ImageFrameMetadata metadata, |
|||
in IconDirEntry entry, |
|||
IconFrameCompression compression, |
|||
BmpBitsPerPixel bitsPerPixel, |
|||
ReadOnlyMemory<Color>? colorTable) |
|||
{ |
|||
CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); |
|||
curFrameMetadata.FromIconDirEntry(entry); |
|||
curFrameMetadata.Compression = compression; |
|||
curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; |
|||
curFrameMetadata.ColorTable = colorTable; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Image encoder for writing an image to a stream as a Windows Cursor.
|
|||
/// </summary>
|
|||
public sealed class CurEncoder : QuantizingImageEncoder |
|||
{ |
|||
/// <inheritdoc/>
|
|||
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
CurEncoderCore encoderCore = new(this); |
|||
encoderCore.Encode(image, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
internal sealed class CurEncoderCore : IconEncoderCore |
|||
{ |
|||
public CurEncoderCore(QuantizingImageEncoder encoder) |
|||
: base(encoder, IconFileType.CUR) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the ICO format.
|
|||
/// </summary>
|
|||
public sealed class CurFormat : IImageFormat<CurMetadata, CurFrameMetadata> |
|||
{ |
|||
private CurFormat() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static CurFormat Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public string Name => "ICO"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public string DefaultMimeType => CurConstants.MimeTypes.First(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> MimeTypes => CurConstants.MimeTypes; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> FileExtensions => CurConstants.FileExtensions; |
|||
|
|||
/// <inheritdoc/>
|
|||
public CurMetadata CreateDefaultFormatMetadata() => new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public CurFrameMetadata CreateDefaultFormatFrameMetadata() => new(); |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// IcoFrameMetadata.
|
|||
/// </summary>
|
|||
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
|
|||
/// </summary>
|
|||
public CurFrameMetadata() |
|||
{ |
|||
} |
|||
|
|||
private CurFrameMetadata(CurFrameMetadata other) |
|||
{ |
|||
this.Compression = other.Compression; |
|||
this.HotspotX = other.HotspotX; |
|||
this.HotspotY = other.HotspotY; |
|||
this.EncodingWidth = other.EncodingWidth; |
|||
this.EncodingHeight = other.EncodingHeight; |
|||
this.BmpBitsPerPixel = other.BmpBitsPerPixel; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame compressions format.
|
|||
/// </summary>
|
|||
public IconFrameCompression Compression { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left.
|
|||
/// </summary>
|
|||
public ushort HotspotX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top.
|
|||
/// </summary>
|
|||
public ushort HotspotY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoding width. <br />
|
|||
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
|
|||
/// </summary>
|
|||
public byte EncodingWidth { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoding height. <br />
|
|||
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
|
|||
/// </summary>
|
|||
public byte EncodingHeight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of bits per pixel.<br/>
|
|||
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
|
|||
/// </summary>
|
|||
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the color table, if any.
|
|||
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<Color>? ColorTable { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public CurFrameMetadata DeepClone() => new(this); |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
|
|||
internal void FromIconDirEntry(IconDirEntry entry) |
|||
{ |
|||
this.EncodingWidth = entry.Width; |
|||
this.EncodingHeight = entry.Height; |
|||
this.HotspotX = entry.Planes; |
|||
this.HotspotY = entry.BitCount; |
|||
} |
|||
|
|||
internal IconDirEntry ToIconDirEntry() |
|||
{ |
|||
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 |
|||
? (byte)0 |
|||
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); |
|||
|
|||
return new() |
|||
{ |
|||
Width = this.EncodingWidth, |
|||
Height = this.EncodingHeight, |
|||
Planes = this.HotspotX, |
|||
BitCount = this.HotspotY, |
|||
ColorCount = colorCount |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Cur; |
|||
|
|||
/// <summary>
|
|||
/// Provides Ico specific metadata information for the image.
|
|||
/// </summary>
|
|||
public class CurMetadata : IDeepCloneable<CurMetadata>, IDeepCloneable |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public CurMetadata DeepClone() => new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Extension methods for the <see cref="ImageMetadata"/> type.
|
|||
/// </summary>
|
|||
public static partial class MetadataExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the Icon format specific metadata for the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="CurMetadata"/>.</returns>
|
|||
public static CurMetadata GetCurMetadata(this ImageMetadata source) |
|||
=> source.GetFormatMetadata(CurFormat.Instance); |
|||
|
|||
/// <summary>
|
|||
/// Gets the Icon format specific metadata for the image frame.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="CurFrameMetadata"/>.</returns>
|
|||
public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) |
|||
=> source.GetFormatMetadata(CurFormat.Instance); |
|||
|
|||
/// <summary>
|
|||
/// Gets the Icon format specific metadata for the image frame.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <param name="metadata">
|
|||
/// When this method returns, contains the metadata associated with the specified frame,
|
|||
/// if found; otherwise, the default value for the type of the metadata parameter.
|
|||
/// This parameter is passed uninitialized.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// <see langword="true"/> if the Icon frame metadata exists; otherwise, <see langword="false"/>.
|
|||
/// </returns>
|
|||
public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata) |
|||
=> source.TryGetFormatMetadata(CurFormat.Instance, out metadata); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
|
|||
/// </summary>
|
|||
public sealed class IcoConfigurationModule : IImageFormatConfigurationModule |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public void Configure(Configuration configuration) |
|||
{ |
|||
configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder()); |
|||
configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance); |
|||
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Defines constants relating to ICOs
|
|||
/// </summary>
|
|||
internal static class IcoConstants |
|||
{ |
|||
/// <summary>
|
|||
/// The list of mime types that equate to a ico.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
|
|||
/// </remarks>
|
|||
public static readonly IEnumerable<string> MimeTypes = |
|||
[ |
|||
|
|||
// IANA-registered
|
|||
"image/vnd.microsoft.icon", |
|||
|
|||
// ICO & CUR types used by Windows
|
|||
"image/x-icon", |
|||
|
|||
// Erroneous types but have been used
|
|||
"image/ico", |
|||
"image/icon", |
|||
"text/ico", |
|||
"application/ico", |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// The list of file extensions that equate to a ico.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> FileExtensions = ["ico"]; |
|||
|
|||
public const uint FileHeader = 0x00_01_00_00; |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Decoder for generating an image out of a ico encoded stream.
|
|||
/// </summary>
|
|||
public sealed class IcoDecoder : ImageDecoder |
|||
{ |
|||
private IcoDecoder() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static IcoDecoder Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
Image<TPixel> image = new IcoDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken); |
|||
|
|||
ScaleToTargetSize(options, image); |
|||
|
|||
return image; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
=> this.Decode<Rgba32>(options, stream, cancellationToken); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
return new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
internal sealed class IcoDecoderCore : IconDecoderCore |
|||
{ |
|||
public IcoDecoderCore(DecoderOptions options) |
|||
: base(options) |
|||
{ |
|||
} |
|||
|
|||
protected override void SetFrameMetadata( |
|||
ImageFrameMetadata metadata, |
|||
in IconDirEntry entry, |
|||
IconFrameCompression compression, |
|||
BmpBitsPerPixel bitsPerPixel, |
|||
ReadOnlyMemory<Color>? colorTable) |
|||
{ |
|||
IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); |
|||
icoFrameMetadata.FromIconDirEntry(entry); |
|||
icoFrameMetadata.Compression = compression; |
|||
icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; |
|||
icoFrameMetadata.ColorTable = colorTable; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Image encoder for writing an image to a stream as a Windows Icon.
|
|||
/// </summary>
|
|||
public sealed class IcoEncoder : QuantizingImageEncoder |
|||
{ |
|||
/// <inheritdoc/>
|
|||
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
IcoEncoderCore encoderCore = new(this); |
|||
encoderCore.Encode(image, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
internal sealed class IcoEncoderCore : IconEncoderCore |
|||
{ |
|||
public IcoEncoderCore(QuantizingImageEncoder encoder) |
|||
: base(encoder, IconFileType.ICO) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Registers the image encoders, decoders and mime type detectors for the ICO format.
|
|||
/// </summary>
|
|||
public sealed class IcoFormat : IImageFormat<IcoMetadata, IcoFrameMetadata> |
|||
{ |
|||
private IcoFormat() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static IcoFormat Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public string Name => "ICO"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public string DefaultMimeType => IcoConstants.MimeTypes.First(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> MimeTypes => IcoConstants.MimeTypes; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> FileExtensions => IcoConstants.FileExtensions; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IcoMetadata CreateDefaultFormatMetadata() => new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public IcoFrameMetadata CreateDefaultFormatFrameMetadata() => new(); |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Provides Ico specific metadata information for the image frame.
|
|||
/// </summary>
|
|||
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
|
|||
/// </summary>
|
|||
public IcoFrameMetadata() |
|||
{ |
|||
} |
|||
|
|||
private IcoFrameMetadata(IcoFrameMetadata other) |
|||
{ |
|||
this.Compression = other.Compression; |
|||
this.EncodingWidth = other.EncodingWidth; |
|||
this.EncodingHeight = other.EncodingHeight; |
|||
this.BmpBitsPerPixel = other.BmpBitsPerPixel; |
|||
|
|||
if (other.ColorTable?.Length > 0) |
|||
{ |
|||
this.ColorTable = other.ColorTable.Value.ToArray(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame compressions format.
|
|||
/// </summary>
|
|||
public IconFrameCompression Compression { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoding width. <br />
|
|||
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
|
|||
/// </summary>
|
|||
public byte EncodingWidth { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoding height. <br />
|
|||
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
|
|||
/// </summary>
|
|||
public byte EncodingHeight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of bits per pixel.<br/>
|
|||
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
|
|||
/// </summary>
|
|||
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the color table, if any.
|
|||
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<Color>? ColorTable { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public IcoFrameMetadata DeepClone() => new(this); |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
|
|||
internal void FromIconDirEntry(IconDirEntry entry) |
|||
{ |
|||
this.EncodingWidth = entry.Width; |
|||
this.EncodingHeight = entry.Height; |
|||
} |
|||
|
|||
internal IconDirEntry ToIconDirEntry() |
|||
{ |
|||
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 |
|||
? (byte)0 |
|||
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); |
|||
|
|||
return new() |
|||
{ |
|||
Width = this.EncodingWidth, |
|||
Height = this.EncodingHeight, |
|||
Planes = 1, |
|||
ColorCount = colorCount, |
|||
BitCount = this.Compression switch |
|||
{ |
|||
IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel, |
|||
IconFrameCompression.Png or _ => 32, |
|||
}, |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ico; |
|||
|
|||
/// <summary>
|
|||
/// Provides Ico specific metadata information for the image.
|
|||
/// </summary>
|
|||
public class IcoMetadata : IDeepCloneable<IcoMetadata>, IDeepCloneable |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public IcoMetadata DeepClone() => new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Extension methods for the <see cref="ImageMetadata"/> type.
|
|||
/// </summary>
|
|||
public static partial class MetadataExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the Ico format specific metadata for the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="IcoMetadata"/>.</returns>
|
|||
public static IcoMetadata GetIcoMetadata(this ImageMetadata source) |
|||
=> source.GetFormatMetadata(IcoFormat.Instance); |
|||
|
|||
/// <summary>
|
|||
/// Gets the Ico format specific metadata for the image frame.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="IcoFrameMetadata"/>.</returns>
|
|||
public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) |
|||
=> source.GetFormatMetadata(IcoFormat.Instance); |
|||
|
|||
/// <summary>
|
|||
/// Gets the Ico format specific metadata for the image frame.
|
|||
/// </summary>
|
|||
/// <param name="source">The metadata this method extends.</param>
|
|||
/// <param name="metadata">
|
|||
/// When this method returns, contains the metadata associated with the specified frame,
|
|||
/// if found; otherwise, the default value for the type of the metadata parameter.
|
|||
/// This parameter is passed uninitialized.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// <see langword="true"/> if the Ico frame metadata exists; otherwise, <see langword="false"/>.
|
|||
/// </returns>
|
|||
public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata) |
|||
=> source.TryGetFormatMetadata(IcoFormat.Instance, out metadata); |
|||
} |
|||
@ -0,0 +1,306 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
internal abstract class IconDecoderCore : IImageDecoderInternals |
|||
{ |
|||
private IconDir fileHeader; |
|||
private IconDirEntry[]? entries; |
|||
|
|||
protected IconDecoderCore(DecoderOptions options) |
|||
=> this.Options = options; |
|||
|
|||
public DecoderOptions Options { get; } |
|||
|
|||
public Size Dimensions { get; private set; } |
|||
|
|||
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
// Stream may not at 0.
|
|||
long basePosition = stream.Position; |
|||
this.ReadHeader(stream); |
|||
|
|||
Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length]; |
|||
|
|||
List<(Image<TPixel> Image, IconFrameCompression Compression, int Index)> decodedEntries |
|||
= new((int)Math.Min(this.entries.Length, this.Options.MaxFrames)); |
|||
|
|||
for (int i = 0; i < this.entries.Length; i++) |
|||
{ |
|||
if (i == this.Options.MaxFrames) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
ref IconDirEntry entry = ref this.entries[i]; |
|||
|
|||
// If we hit the end of the stream we should break.
|
|||
if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// There should always be enough bytes for this regardless of the entry type.
|
|||
if (stream.Read(flag) != PngConstants.HeaderBytes.Length) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// Reset the stream position.
|
|||
_ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); |
|||
|
|||
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); |
|||
|
|||
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
|
|||
Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(stream, cancellationToken); |
|||
decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i)); |
|||
|
|||
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
|
|||
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
|
|||
this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); |
|||
} |
|||
|
|||
ImageMetadata metadata = new(); |
|||
BmpMetadata? bmpMetadata = null; |
|||
PngMetadata? pngMetadata = null; |
|||
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => |
|||
{ |
|||
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; |
|||
ReadOnlyMemory<Color>? colorTable = null; |
|||
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions); |
|||
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe; |
|||
for (int y = 0; y < source.Height; y++) |
|||
{ |
|||
source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); |
|||
} |
|||
|
|||
// Copy the format specific frame metadata to the image.
|
|||
if (x.Compression is IconFrameCompression.Png) |
|||
{ |
|||
if (x.Index == 0) |
|||
{ |
|||
pngMetadata = x.Image.Metadata.GetPngMetadata(); |
|||
} |
|||
|
|||
target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngMetadata()); |
|||
} |
|||
else |
|||
{ |
|||
BmpMetadata meta = x.Image.Metadata.GetBmpMetadata(); |
|||
bitsPerPixel = meta.BitsPerPixel; |
|||
colorTable = meta.ColorTable; |
|||
|
|||
if (x.Index == 0) |
|||
{ |
|||
bmpMetadata = meta; |
|||
} |
|||
} |
|||
|
|||
this.SetFrameMetadata( |
|||
target.Metadata, |
|||
this.entries[x.Index], |
|||
x.Compression, |
|||
bitsPerPixel, |
|||
colorTable); |
|||
|
|||
x.Image.Dispose(); |
|||
|
|||
return target; |
|||
}).ToArray()); |
|||
|
|||
// Copy the format specific metadata to the image.
|
|||
if (bmpMetadata != null) |
|||
{ |
|||
result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); |
|||
} |
|||
|
|||
if (pngMetadata != null) |
|||
{ |
|||
result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) |
|||
{ |
|||
// Stream may not at 0.
|
|||
long basePosition = stream.Position; |
|||
this.ReadHeader(stream); |
|||
|
|||
Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length]; |
|||
|
|||
ImageMetadata metadata = new(); |
|||
BmpMetadata? bmpMetadata = null; |
|||
PngMetadata? pngMetadata = null; |
|||
ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)]; |
|||
int bpp = 0; |
|||
for (int i = 0; i < frames.Length; i++) |
|||
{ |
|||
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; |
|||
ReadOnlyMemory<Color>? colorTable = null; |
|||
ref IconDirEntry entry = ref this.entries[i]; |
|||
|
|||
// If we hit the end of the stream we should break.
|
|||
if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// There should always be enough bytes for this regardless of the entry type.
|
|||
if (stream.Read(flag) != PngConstants.HeaderBytes.Length) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// Reset the stream position.
|
|||
_ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); |
|||
|
|||
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); |
|||
|
|||
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
|
|||
ImageInfo temp = this.GetDecoder(isPng).Identify(stream, cancellationToken); |
|||
|
|||
ImageFrameMetadata frameMetadata = new(); |
|||
|
|||
if (isPng) |
|||
{ |
|||
if (i == 0) |
|||
{ |
|||
pngMetadata = temp.Metadata.GetPngMetadata(); |
|||
} |
|||
|
|||
frameMetadata.SetFormatMetadata(PngFormat.Instance, temp.FrameMetadataCollection[0].GetPngMetadata()); |
|||
} |
|||
else |
|||
{ |
|||
BmpMetadata meta = temp.Metadata.GetBmpMetadata(); |
|||
bitsPerPixel = meta.BitsPerPixel; |
|||
colorTable = meta.ColorTable; |
|||
|
|||
if (i == 0) |
|||
{ |
|||
bmpMetadata = meta; |
|||
} |
|||
} |
|||
|
|||
bpp = Math.Max(bpp, (int)bitsPerPixel); |
|||
|
|||
frames[i] = frameMetadata; |
|||
|
|||
this.SetFrameMetadata( |
|||
frames[i], |
|||
this.entries[i], |
|||
isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, |
|||
bitsPerPixel, |
|||
colorTable); |
|||
|
|||
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
|
|||
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
|
|||
this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); |
|||
} |
|||
|
|||
// Copy the format specific metadata to the image.
|
|||
if (bmpMetadata != null) |
|||
{ |
|||
metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); |
|||
} |
|||
|
|||
if (pngMetadata != null) |
|||
{ |
|||
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); |
|||
} |
|||
|
|||
return new(new(bpp), this.Dimensions, metadata, frames); |
|||
} |
|||
|
|||
protected abstract void SetFrameMetadata( |
|||
ImageFrameMetadata metadata, |
|||
in IconDirEntry entry, |
|||
IconFrameCompression compression, |
|||
BmpBitsPerPixel bitsPerPixel, |
|||
ReadOnlyMemory<Color>? colorTable); |
|||
|
|||
[MemberNotNull(nameof(entries))] |
|||
protected void ReadHeader(Stream stream) |
|||
{ |
|||
Span<byte> buffer = stackalloc byte[IconDirEntry.Size]; |
|||
|
|||
// ICONDIR
|
|||
_ = CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size); |
|||
this.fileHeader = IconDir.Parse(buffer); |
|||
|
|||
// ICONDIRENTRY
|
|||
this.entries = new IconDirEntry[this.fileHeader.Count]; |
|||
for (int i = 0; i < this.entries.Length; i++) |
|||
{ |
|||
_ = CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size); |
|||
this.entries[i] = IconDirEntry.Parse(buffer); |
|||
} |
|||
|
|||
int width = 0; |
|||
int height = 0; |
|||
foreach (IconDirEntry entry in this.entries) |
|||
{ |
|||
// Since Windows 95 size of an image in the ICONDIRENTRY structure might
|
|||
// be set to zero, which means 256 pixels.
|
|||
if (entry.Width == 0) |
|||
{ |
|||
width = 256; |
|||
} |
|||
|
|||
if (entry.Height == 0) |
|||
{ |
|||
height = 256; |
|||
} |
|||
|
|||
if (width == 256 && height == 256) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
width = Math.Max(width, entry.Width); |
|||
height = Math.Max(height, entry.Height); |
|||
} |
|||
|
|||
this.Dimensions = new(width, height); |
|||
} |
|||
|
|||
private IImageDecoderInternals GetDecoder(bool isPng) |
|||
{ |
|||
if (isPng) |
|||
{ |
|||
return new PngDecoderCore(new() |
|||
{ |
|||
GeneralOptions = this.Options, |
|||
}); |
|||
} |
|||
|
|||
return new BmpDecoderCore(new() |
|||
{ |
|||
GeneralOptions = this.Options, |
|||
ProcessedAlphaMask = true, |
|||
SkipFileHeader = true, |
|||
UseDoubleHeight = true, |
|||
}); |
|||
} |
|||
|
|||
private static int CheckEndOfStream(int v, int length) |
|||
{ |
|||
if (v != length) |
|||
{ |
|||
throw new InvalidImageContentException("Not enough bytes to read icon header."); |
|||
} |
|||
|
|||
return v; |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] |
|||
internal struct IconDir(ushort reserved, IconFileType type, ushort count) |
|||
{ |
|||
public const int Size = 3 * sizeof(ushort); |
|||
|
|||
/// <summary>
|
|||
/// Reserved. Must always be 0.
|
|||
/// </summary>
|
|||
public ushort Reserved = reserved; |
|||
|
|||
/// <summary>
|
|||
/// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid.
|
|||
/// </summary>
|
|||
public IconFileType Type = type; |
|||
|
|||
/// <summary>
|
|||
/// Specifies number of images in the file.
|
|||
/// </summary>
|
|||
public ushort Count = count; |
|||
|
|||
public IconDir(IconFileType type) |
|||
: this(type, 0) |
|||
{ |
|||
} |
|||
|
|||
public IconDir(IconFileType type, ushort count) |
|||
: this(0, type, count) |
|||
{ |
|||
} |
|||
|
|||
public static IconDir Parse(ReadOnlySpan<byte> data) |
|||
=> MemoryMarshal.Cast<byte, IconDir>(data)[0]; |
|||
|
|||
public readonly unsafe void WriteTo(Stream stream) |
|||
=> stream.Write(MemoryMarshal.Cast<IconDir, byte>([this])); |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] |
|||
internal struct IconDirEntry |
|||
{ |
|||
public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint)); |
|||
|
|||
/// <summary>
|
|||
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
|
|||
/// </summary>
|
|||
public byte Width; |
|||
|
|||
/// <summary>
|
|||
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[
|
|||
/// </summary>
|
|||
public byte Height; |
|||
|
|||
/// <summary>
|
|||
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
|
|||
/// </summary>
|
|||
public byte ColorCount; |
|||
|
|||
/// <summary>
|
|||
/// Reserved. Should be 0.
|
|||
/// </summary>
|
|||
public byte Reserved; |
|||
|
|||
/// <summary>
|
|||
/// In ICO format: Specifies color planes. Should be 0 or 1.<br/>
|
|||
/// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
|
|||
/// </summary>
|
|||
public ushort Planes; |
|||
|
|||
/// <summary>
|
|||
/// In ICO format: Specifies bits per pixel.<br/>
|
|||
/// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
|
|||
/// </summary>
|
|||
public ushort BitCount; |
|||
|
|||
/// <summary>
|
|||
/// Specifies the size of the image's data in bytes
|
|||
/// </summary>
|
|||
public uint BytesInRes; |
|||
|
|||
/// <summary>
|
|||
/// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file.
|
|||
/// </summary>
|
|||
public uint ImageOffset; |
|||
|
|||
public static IconDirEntry Parse(in ReadOnlySpan<byte> data) |
|||
=> MemoryMarshal.Cast<byte, IconDirEntry>(data)[0]; |
|||
|
|||
public readonly unsafe void WriteTo(in Stream stream) |
|||
=> stream.Write(MemoryMarshal.Cast<IconDirEntry, byte>([this])); |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
internal abstract class IconEncoderCore : IImageEncoderInternals |
|||
{ |
|||
private readonly QuantizingImageEncoder encoder; |
|||
private readonly IconFileType iconFileType; |
|||
private IconDir fileHeader; |
|||
private EncodingFrameMetadata[]? entries; |
|||
|
|||
protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType) |
|||
{ |
|||
this.encoder = encoder; |
|||
this.iconFileType = iconFileType; |
|||
} |
|||
|
|||
public void Encode<TPixel>( |
|||
Image<TPixel> image, |
|||
Stream stream, |
|||
CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(image, nameof(image)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
// Stream may not at 0.
|
|||
long basePosition = stream.Position; |
|||
this.InitHeader(image); |
|||
|
|||
// We don't write the header and entries yet as we need to write the image data first.
|
|||
int dataOffset = IconDir.Size + (IconDirEntry.Size * this.entries.Length); |
|||
_ = stream.Seek(dataOffset, SeekOrigin.Current); |
|||
|
|||
for (int i = 0; i < image.Frames.Count; i++) |
|||
{ |
|||
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
|
|||
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
|
|||
ImageFrame<TPixel> frame = image.Frames[i]; |
|||
int width = this.entries[i].Entry.Width; |
|||
if (width is 0) |
|||
{ |
|||
width = frame.Width; |
|||
} |
|||
|
|||
int height = this.entries[i].Entry.Height; |
|||
if (height is 0) |
|||
{ |
|||
height = frame.Height; |
|||
} |
|||
|
|||
this.entries[i].Entry.ImageOffset = (uint)stream.Position; |
|||
|
|||
// We crop the frame to the size specified in the metadata.
|
|||
// TODO: we can optimize this by cropping the frame only if the new size is both required and different.
|
|||
using Image<TPixel> encodingFrame = new(width, height); |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
frame.PixelBuffer.DangerousGetRowSpan(y)[..width] |
|||
.CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); |
|||
} |
|||
|
|||
ref EncodingFrameMetadata encodingMetadata = ref this.entries[i]; |
|||
|
|||
QuantizingImageEncoder encoder = encodingMetadata.Compression switch |
|||
{ |
|||
IconFrameCompression.Bmp => new BmpEncoder() |
|||
{ |
|||
Quantizer = this.GetQuantizer(encodingMetadata), |
|||
ProcessedAlphaMask = true, |
|||
UseDoubleHeight = true, |
|||
SkipFileHeader = true, |
|||
SupportTransparency = false, |
|||
BitsPerPixel = encodingMetadata.BmpBitsPerPixel |
|||
}, |
|||
IconFrameCompression.Png => new PngEncoder() |
|||
{ |
|||
// Only 32bit Png supported.
|
|||
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
|
|||
BitDepth = PngBitDepth.Bit8, |
|||
ColorType = PngColorType.RgbWithAlpha, |
|||
CompressionLevel = PngCompressionLevel.BestCompression |
|||
}, |
|||
_ => throw new NotSupportedException(), |
|||
}; |
|||
|
|||
encoder.Encode(encodingFrame, stream); |
|||
encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset; |
|||
} |
|||
|
|||
// We now need to rewind the stream and write the header and the entries.
|
|||
long endPosition = stream.Position; |
|||
_ = stream.Seek(basePosition, SeekOrigin.Begin); |
|||
this.fileHeader.WriteTo(stream); |
|||
foreach (EncodingFrameMetadata frame in this.entries) |
|||
{ |
|||
frame.Entry.WriteTo(stream); |
|||
} |
|||
|
|||
_ = stream.Seek(endPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
[MemberNotNull(nameof(entries))] |
|||
private void InitHeader(Image image) |
|||
{ |
|||
this.fileHeader = new(this.iconFileType, (ushort)image.Frames.Count); |
|||
this.entries = this.iconFileType switch |
|||
{ |
|||
IconFileType.ICO => |
|||
image.Frames.Select(i => |
|||
{ |
|||
IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); |
|||
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); |
|||
}).ToArray(), |
|||
IconFileType.CUR => |
|||
image.Frames.Select(i => |
|||
{ |
|||
CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); |
|||
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); |
|||
}).ToArray(), |
|||
_ => throw new NotSupportedException(), |
|||
}; |
|||
} |
|||
|
|||
private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata) |
|||
{ |
|||
if (metadata.Entry.BitCount > 8) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (this.encoder.Quantizer is not null) |
|||
{ |
|||
return this.encoder.Quantizer; |
|||
} |
|||
|
|||
if (metadata.ColorTable is null) |
|||
{ |
|||
return new WuQuantizer(new() |
|||
{ |
|||
MaxColors = metadata.Entry.ColorCount |
|||
}); |
|||
} |
|||
|
|||
// Don't dither if we have a palette. We want to preserve as much information as possible.
|
|||
return new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }); |
|||
} |
|||
|
|||
internal sealed class EncodingFrameMetadata |
|||
{ |
|||
private IconDirEntry iconDirEntry; |
|||
|
|||
public EncodingFrameMetadata( |
|||
IconFrameCompression compression, |
|||
BmpBitsPerPixel bmpBitsPerPixel, |
|||
ReadOnlyMemory<Color>? colorTable, |
|||
IconDirEntry iconDirEntry) |
|||
{ |
|||
this.Compression = compression; |
|||
this.BmpBitsPerPixel = compression == IconFrameCompression.Png |
|||
? BmpBitsPerPixel.Pixel32 |
|||
: bmpBitsPerPixel; |
|||
this.ColorTable = colorTable; |
|||
this.iconDirEntry = iconDirEntry; |
|||
} |
|||
|
|||
public IconFrameCompression Compression { get; } |
|||
|
|||
public BmpBitsPerPixel BmpBitsPerPixel { get; } |
|||
|
|||
public ReadOnlyMemory<Color>? ColorTable { get; set; } |
|||
|
|||
public ref IconDirEntry Entry => ref this.iconDirEntry; |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
/// <summary>
|
|||
/// Ico file type
|
|||
/// </summary>
|
|||
internal enum IconFileType : ushort |
|||
{ |
|||
/// <summary>
|
|||
/// ICO file
|
|||
/// </summary>
|
|||
ICO = 1, |
|||
|
|||
/// <summary>
|
|||
/// CUR file
|
|||
/// </summary>
|
|||
CUR = 2, |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
/// <summary>
|
|||
/// IconFrameCompression
|
|||
/// </summary>
|
|||
public enum IconFrameCompression |
|||
{ |
|||
/// <summary>
|
|||
/// Bmp
|
|||
/// </summary>
|
|||
Bmp, |
|||
|
|||
/// <summary>
|
|||
/// Png
|
|||
/// </summary>
|
|||
Png |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Icon; |
|||
|
|||
/// <summary>
|
|||
/// Detects ico file headers.
|
|||
/// </summary>
|
|||
public class IconImageFormatDetector : IImageFormatDetector |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
|||
{ |
|||
format = this.IsSupportedFileFormat(header) switch |
|||
{ |
|||
true => Ico.IcoFormat.Instance, |
|||
false => Cur.CurFormat.Instance, |
|||
null => default |
|||
}; |
|||
|
|||
return format is not null; |
|||
} |
|||
|
|||
private bool? IsSupportedFileFormat(ReadOnlySpan<byte> header) |
|||
{ |
|||
// There are no magic bytes in the first few bytes of a tga file,
|
|||
// so we try to figure out if its a valid tga by checking for valid tga header bytes.
|
|||
if (header.Length < this.HeaderSize) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
IconDir dir = IconDir.Parse(header); |
|||
if (dir is not { Reserved: 0 } // Should be 0.
|
|||
or not { Type: IconFileType.ICO or IconFileType.CUR } // Unknown Type.
|
|||
or { Count: 0 }) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); |
|||
if (entry is not { Reserved: 0 } // Should be 0.
|
|||
or { BytesInRes: 0 } // Should not be 0.
|
|||
|| entry.ImageOffset < IconDir.Size + (dir.Count * IconDirEntry.Size)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (dir.Type is IconFileType.ICO) |
|||
{ |
|||
if (entry is not { BitCount: 1 or 4 or 8 or 16 or 24 or 32 } or not { Planes: 0 or 1 }) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using static SixLabors.ImageSharp.Tests.TestImages.Cur; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; |
|||
|
|||
[Trait("Format", "Cur")] |
|||
[ValidateDisposedMemoryAllocations] |
|||
public class CurDecoderTests |
|||
{ |
|||
[Theory] |
|||
[WithFile(WindowsMouse, PixelTypes.Rgba32)] |
|||
public void CurDecoder_Decode(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(CurDecoder.Instance); |
|||
|
|||
CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); |
|||
Assert.Equal(image.Width, meta.EncodingWidth); |
|||
Assert.Equal(image.Height, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(CurFake, PixelTypes.Rgba32)] |
|||
[WithFile(CurReal, PixelTypes.Rgba32)] |
|||
public void CurDecoder_Decode2(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(CurDecoder.Instance); |
|||
CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); |
|||
Assert.Equal(image.Width, meta.EncodingWidth); |
|||
Assert.Equal(image.Height, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using static SixLabors.ImageSharp.Tests.TestImages.Cur; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; |
|||
|
|||
[Trait("Format", "Cur")] |
|||
public class CurEncoderTests |
|||
{ |
|||
private static CurEncoder Encoder => new(); |
|||
|
|||
[Theory] |
|||
[WithFile(CurReal, PixelTypes.Rgba32)] |
|||
[WithFile(WindowsMouse, PixelTypes.Rgba32)] |
|||
public void CanRoundTripEncoder<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using Image<TPixel> image = provider.GetImage(CurDecoder.Instance); |
|||
using MemoryStream memStream = new(); |
|||
image.DebugSaveMultiFrame(provider); |
|||
|
|||
image.Save(memStream, Encoder); |
|||
memStream.Seek(0, SeekOrigin.Begin); |
|||
|
|||
using Image<TPixel> encoded = Image.Load<TPixel>(memStream); |
|||
encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); |
|||
|
|||
encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); |
|||
} |
|||
} |
|||
@ -0,0 +1,332 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using static SixLabors.ImageSharp.Tests.TestImages.Ico; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; |
|||
|
|||
[Trait("Format", "Icon")] |
|||
[ValidateDisposedMemoryAllocations] |
|||
public class IcoDecoderTests |
|||
{ |
|||
[Theory] |
|||
[WithFile(Flutter, PixelTypes.Rgba32)] |
|||
public void IcoDecoder_Decode(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSaveMultiFrame(provider); |
|||
|
|||
Assert.Equal(10, image.Frames.Count); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(Bpp1Size15x15, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size16x16, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size17x17, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size1x1, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size256x256, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size2x2, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size31x31, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size32x32, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size33x33, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size3x3, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size4x4, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size5x5, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size6x6, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size7x7, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size8x8, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1Size9x9, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1TranspNotSquare, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp1TranspPartial, PixelTypes.Rgba32)] |
|||
public void Bpp1Test(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(Bpp24Size15x15, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size16x16, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size17x17, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size1x1, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size256x256, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size2x2, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size31x31, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size32x32, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size33x33, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size3x3, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size4x4, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size5x5, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size6x6, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size7x7, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size8x8, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Size9x9, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24TranspNotSquare, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24TranspPartial, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp24Transp, PixelTypes.Rgba32)] |
|||
public void Bpp24Test(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(Bpp32Size15x15, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size16x16, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size17x17, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size1x1, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size256x256, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size2x2, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size31x31, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size32x32, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size33x33, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size3x3, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size4x4, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size5x5, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size6x6, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size7x7, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size8x8, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Size9x9, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32TranspNotSquare, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32TranspPartial, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp32Transp, PixelTypes.Rgba32)] |
|||
public void Bpp32Test(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(Bpp4Size15x15, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size16x16, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size17x17, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size1x1, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size256x256, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size2x2, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size31x31, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size32x32, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size33x33, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size3x3, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size4x4, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size5x5, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size6x6, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size7x7, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size8x8, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4Size9x9, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4TranspNotSquare, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp4TranspPartial, PixelTypes.Rgba32)] |
|||
public void Bpp4Test(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(Bpp8Size15x15, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size16x16, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size17x17, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size1x1, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size256x256, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size2x2, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size31x31, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size32x32, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size33x33, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size3x3, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size4x4, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size5x5, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size6x6, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8Size7x7, PixelTypes.Rgba32)] |
|||
|
|||
// [WithFile(Bpp8Size8x8, PixelTypes.Rgba32)] This is actually 24 bit.
|
|||
[WithFile(Bpp8Size9x9, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8TranspNotSquare, PixelTypes.Rgba32)] |
|||
[WithFile(Bpp8TranspPartial, PixelTypes.Rgba32)] |
|||
public void Bpp8Test(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(InvalidAll, PixelTypes.Rgba32)] |
|||
[WithFile(InvalidBpp, PixelTypes.Rgba32)] |
|||
[WithFile(InvalidCompression, PixelTypes.Rgba32)] |
|||
[WithFile(InvalidRLE4, PixelTypes.Rgba32)] |
|||
[WithFile(InvalidRLE8, PixelTypes.Rgba32)] |
|||
public void InvalidTest(TestImageProvider<Rgba32> provider) |
|||
=> Assert.Throws<NotSupportedException>(() => |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
}); |
|||
|
|||
[Theory] |
|||
[WithFile(InvalidPng, PixelTypes.Rgba32)] |
|||
public void InvalidPngTest(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Png, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(MixedBmpPngA, PixelTypes.Rgba32)] |
|||
[WithFile(MixedBmpPngB, PixelTypes.Rgba32)] |
|||
[WithFile(MixedBmpPngC, PixelTypes.Rgba32)] |
|||
public void MixedBmpPngTest(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
Assert.True(image.Frames.Count > 1); |
|||
|
|||
image.DebugSaveMultiFrame(provider); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(MultiSizeA, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeB, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeC, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeD, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeE, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeF, PixelTypes.Rgba32)] |
|||
public void MultiSizeTest(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
Assert.True(image.Frames.Count > 1); |
|||
|
|||
for (int i = 0; i < image.Frames.Count; i++) |
|||
{ |
|||
ImageFrame<Rgba32> frame = image.Frames[i]; |
|||
IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata(); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
|
|||
image.DebugSaveMultiFrame(provider); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(MultiSizeA, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeB, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeC, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeD, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeE, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeF, PixelTypes.Rgba32)] |
|||
public void MultiSize_CanDecodeSingleFrame(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance, new() { MaxFrames = 1 }); |
|||
Assert.Single(image.Frames); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(MultiSizeA)] |
|||
[InlineData(MultiSizeB)] |
|||
[InlineData(MultiSizeC)] |
|||
[InlineData(MultiSizeD)] |
|||
[InlineData(MultiSizeE)] |
|||
[InlineData(MultiSizeF)] |
|||
public void MultiSize_CanIdentifySingleFrame(string imagePath) |
|||
{ |
|||
TestFile testFile = TestFile.Create(imagePath); |
|||
using MemoryStream stream = new(testFile.Bytes, false); |
|||
|
|||
ImageInfo imageInfo = Image.Identify(new() { MaxFrames = 1 }, stream); |
|||
|
|||
Assert.Single(imageInfo.FrameMetadataCollection); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(MultiSizeMultiBitsA, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeMultiBitsB, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeMultiBitsC, PixelTypes.Rgba32)] |
|||
[WithFile(MultiSizeMultiBitsD, PixelTypes.Rgba32)] |
|||
public void MultiSizeMultiBitsTest(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
Assert.True(image.Frames.Count > 1); |
|||
|
|||
image.DebugSaveMultiFrame(provider); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(IcoFake, PixelTypes.Rgba32)] |
|||
public void IcoFakeTest(TestImageProvider<Rgba32> provider) |
|||
{ |
|||
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata(); |
|||
int expectedWidth = image.Width >= 256 ? 0 : image.Width; |
|||
int expectedHeight = image.Height >= 256 ? 0 : image.Height; |
|||
|
|||
Assert.Equal(expectedWidth, meta.EncodingWidth); |
|||
Assert.Equal(expectedHeight, meta.EncodingHeight); |
|||
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); |
|||
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using static SixLabors.ImageSharp.Tests.TestImages.Ico; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; |
|||
|
|||
[Trait("Format", "Icon")] |
|||
public class IcoEncoderTests |
|||
{ |
|||
private static IcoEncoder Encoder => new(); |
|||
|
|||
[Theory] |
|||
[WithFile(Flutter, PixelTypes.Rgba32)] |
|||
public void CanRoundTripEncoder<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using Image<TPixel> image = provider.GetImage(IcoDecoder.Instance); |
|||
using MemoryStream memStream = new(); |
|||
image.DebugSaveMultiFrame(provider); |
|||
|
|||
image.Save(memStream, Encoder); |
|||
memStream.Seek(0, SeekOrigin.Begin); |
|||
|
|||
using Image<TPixel> encoded = Image.Load<TPixel>(memStream); |
|||
encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); |
|||
|
|||
// Despite preservation of the palette. The process can still be lossy
|
|||
encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:846dda605ee23bb641534b272fa57300eacd85038feea5dd1a3f6d4b543a935e |
|||
size 190 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:029d0438eda83d4d9e087cf79abe2d0234728c37f570de808355c0e79c71be17 |
|||
size 198 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8db024a49fdd91c9d5d1fc0c1d6f60991518a5776031f58cdafbdd3ed9e4f26b |
|||
size 206 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:f13d1bbfa5b29bc386270cb492b705eded0825a9eb3a6341f4ea2b3dbe085cd1 |
|||
size 78 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:c1aa37daf2fd65b424c5e13e94bed165328e624584cc5664d73bb4030a1e1f12 |
|||
size 16454 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:18be2a5384812de1bec70733fb0b283a159edc5d0bc03981de8fb3ccddb8911e |
|||
size 86 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a3a4e7964e3ed5ca2a98929e2f903a6b969961410aab6a935a0c54fbe716d0c3 |
|||
size 318 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:06a55f8219234e43beec6776fe15a7d6d4ad314deaf64df115ea45f2100e5283 |
|||
size 326 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:82fa82f6b954515eace3cdd6d4681abd149b37354a7dee4f0d1966f516f27850 |
|||
size 598 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:5ff0bf1c415925642d99691a9a9a6b92d9995447bc62ed80b9b0c6d4efdcd19b |
|||
size 94 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:5853ba73b06e0f2a0c71850011526419db3bd7c76e5b7b2f6b22f748ce919bf2 |
|||
size 102 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:ecd253a6862ec9a9870c8a8a370528b28c22d23247dfc29e09fab65d95b9416d |
|||
size 110 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a80c7f8d37dc3997bcd674a5af835cae802cacbf5d65020f0aaae67f70cfc31e |
|||
size 118 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:00a26274e8563e6378f8bfa4d1aa9695030593a38791ca366cecd3949b0f52af |
|||
size 126 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:78150aee2f5d5ccd2a172abfe11f1127efb950cb9173e22e380809afb2a94d3c |
|||
size 134 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:d8cecf833b31208710c1dde1bed3322a95453468a3f4b39afdad66ac9bc5f86b |
|||
size 142 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:bd9f41711b53d4e1e915ddb992522b97a981fbe3f4536826f0c66e2d6a3677fb |
|||
size 182 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:cedcd221abf4b95115f9f8f34f15da456b09e7e56972cced24a99fa56bf8aca9 |
|||
size 326 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b506403852d936d14975ed9aba1c50ab3873cbbd81afcf38381f8e5e841fafd0 |
|||
size 842 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8bfa9de0f613e9e82e9037193c4124f87d05ac1390459e2f018da45c16200de6 |
|||
size 894 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a4cd2ad22e55000a706d365e0a3395f3e4d9a3933c00880d5f12903ac0aed60e |
|||
size 1014 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3b20e9a6479c3831b7af75d30bc909ce3046e45384bfd62d7a10ac6816c1c947 |
|||
size 70 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1175641f39e68bd6cd38b8f64f02510b22c5a642afc7503d357b064163b3d37b |
|||
size 204862 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:72a1380a306b51ae37eabd3edeb0b134b0f8e8e120e6c64b6b08bd833a9c70a4 |
|||
size 86 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:574b0971908b44334f1f3be79e5b0ff336e74a8b9947b43a295d3a2b86733965 |
|||
size 3162 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fa45880e5611436fc06de0603481d0a61044e52a1a053961fdda1139bb5660a6 |
|||
size 3262 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8704a77d6264f6f104e23950099e3d3a4a577787e67c7c39d7925f9d0a347572 |
|||
size 3626 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:81229c27fbc684c0da99b1b04189046d5b9f531ee4111ccc43bde6404dce4f12 |
|||
size 110 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:f7cb620eb83acbf20d308f5c6cb8b0246b8b156cdcc43e39f5809754fd4d5ddb |
|||
size 126 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:bdf1718e3d9695cdf2552e42219d1e3166ad5a94f53cbb44db4a7222e2a32f9a |
|||
size 162 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1e5f3a8f59e297cf67251a89558d4406c4c515c3e3ce7555df4a53ef5420fc38 |
|||
size 206 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0ee1b5836c9f8c89e9b8c8873f1ad92f7555ed9706f4dc76345a727bf3e9f334 |
|||
size 258 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b46ca32ddb84074d9140224738480eaa0a6c0dce2dbf2074625add1901c27117 |
|||
size 286 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:7454d6332f4bdba929d610e4a8a232b1443d5a64119de4e69c00e0f03e55e237 |
|||
size 350 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:eb8ea41822350e5f40bac2aef19ec7a4c40561ce6637948b3fa6db7835c1fded |
|||
size 3262 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:44b4c79ff497df0e99f55d68d82f59c0d7c2f4d5e9bb63bcc1b5910f4a2853db |
|||
size 1126 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:f8d109413d7f699b92e9d6a5e2c52d2b5c747f2ca9ff31d326f8d4ec2fd5840f |
|||
size 3262 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:02b5abd8e15ef2fba1a26cdbdf6e8f66abbbf9aa188404ef911a1d2d02b7b050 |
|||
size 1022 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:50bb07bd12f9b388ba6a3abbb815aaf1c800438e35ec48201269fa23342e5622 |
|||
size 1150 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6bf4c701d38fc988186e3fcfee6e426ed5a84807b54b91e4ab8c1b12e0794746 |
|||
size 1286 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:f62be892b36609a57c34eb4784dfb6dc82698ecd15709898a6579ee4f21f668e |
|||
size 70 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b15f75adc54e70c4751cf9973854f225ef15b12bdaddb5ab00cb9edfd8b386b1 |
|||
size 270398 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fa859752136cdf055874ae71589f9585aaaeaef804b13ce192991793d9e57e57 |
|||
size 86 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:d5ecf1dc1fdd3dc6cb2fd90b49632b19260e73ad3a6624372d1e9eefc470ed6b |
|||
size 4030 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:395ccdc596487ed63db4d893d03b6aa24743b37279d896196d8d38e7028415ea |
|||
size 4286 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:7e8b89c061abcf959b90149585cc34237aad19e1f67c162d62e5adbad4826970 |
|||
size 4682 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:52b7772a9c8fae189abc3cf37d5d24bcdfe94077e6b1cf7be8cc7e0bc6f6bddf |
|||
size 110 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:2bb0831fab10fb0ff9a0b2b2ea137609a45a6ec3982d594878996eafc32836ad |
|||
size 142 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:10e2ed5cbacc761f2d467c22a8d72dcc4086b9423c5487ba05826804c642730a |
|||
size 182 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1ab79a308d24592557b368f00566999d777e38de385eb6ebabc90d53ac723ae9 |
|||
size 230 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3799164811b0284535fa90ca60f13e9c0754ca4e8f00d96a83951e91e748d760 |
|||
size 286 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:2a3187750c9313024f983fdfca270f5db2c5d730831adfce0fb19ddac25deecc |
|||
size 350 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1ab8bd47cc2d2ce0e9c1a5810b5ccbe3f46e35001114c18f34cbf51ff0566bf6 |
|||
size 422 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b4f94718304fa41041b8cebad7b76762d410b366b46d620f5599b03a2fa7ba00 |
|||
size 4286 |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue