Browse Source

Refactor decoder and add notes

pull/2579/head
James Jackson-South 2 years ago
committed by 舰队的偶像-岛风酱!
parent
commit
ffde9a9380
No known key found for this signature in database GPG Key ID: 71F5B3A2B181950C
  1. 4
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 12
      src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs
  3. 2
      src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs
  4. 2
      src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs
  5. 2
      src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs
  6. 8
      src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs
  7. 8
      src/ImageSharp/Formats/Icon/IconAssert.cs
  8. 107
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  9. 5
      src/ImageSharp/Formats/Icon/IconFrameCompression.cs
  10. 1
      src/ImageSharp/Formats/Icon/IconFrameMetadata.cs
  11. 6
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  12. 3
      src/ImageSharp/Metadata/ImageMetadata.cs
  13. 7
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  14. 5
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

4
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -105,7 +105,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/> /// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
private readonly bool skipFileHeader; private readonly bool skipFileHeader;
/// <inheritdoc cref="BmpDecoderOptions.IsDoubleHeight"/> /// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
private readonly bool isDoubleHeight; private readonly bool isDoubleHeight;
/// <summary> /// <summary>
@ -120,7 +120,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
this.processedAlphaMask = options.ProcessedAlphaMask; this.processedAlphaMask = options.ProcessedAlphaMask;
this.skipFileHeader = options.SkipFileHeader; this.skipFileHeader = options.SkipFileHeader;
this.isDoubleHeight = options.IsDoubleHeight; this.isDoubleHeight = options.UseDoubleHeight;
} }
/// <inheritdoc /> /// <inheritdoc />

12
src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs

@ -18,10 +18,10 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions
public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; } public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; }
/// <summary> /// <summary>
/// Gets a value indicating whether the additional AlphaMask is processed at decoding time. /// Gets a value indicating whether the additional alpha mask is processed at decoding time.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It will be used at IcoDecoder. /// Used by the icon decoder.
/// </remarks> /// </remarks>
internal bool ProcessedAlphaMask { get; init; } internal bool ProcessedAlphaMask { get; init; }
@ -29,15 +29,15 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions
/// Gets a value indicating whether to skip loading the BMP file header. /// Gets a value indicating whether to skip loading the BMP file header.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It will be used at IcoDecoder. /// Used by the icon decoder.
/// </remarks> /// </remarks>
internal bool SkipFileHeader { get; init; } internal bool SkipFileHeader { get; init; }
/// <summary> /// <summary>
/// Gets a value indicating whether the height is double of true height. /// Gets a value indicating whether to treat the height as double of true height.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It will be used at IcoDecoder. /// Used by the icon decoder.
/// </remarks> /// </remarks>
internal bool IsDoubleHeight { get; init; } internal bool UseDoubleHeight { get; init; }
} }

2
src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs

@ -3,6 +3,8 @@
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
// TODO: flatten namespace.
// namespace SixLabors.ImageSharp.Formats.Cur;
namespace SixLabors.ImageSharp.Formats.Icon.Cur; namespace SixLabors.ImageSharp.Formats.Icon.Cur;
internal sealed class CurDecoderCore : IconDecoderCore internal sealed class CurDecoderCore : IconDecoderCore

2
src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs

@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Icon.Cur; namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <summary> /// <summary>
/// IcoFrameMetadata /// IcoFrameMetadata. TODO: Remove base class and merge into this class.
/// </summary> /// </summary>
public class CurFrameMetadata : IconFrameMetadata, IDeepCloneable<CurFrameMetadata>, IDeepCloneable public class CurFrameMetadata : IconFrameMetadata, IDeepCloneable<CurFrameMetadata>, IDeepCloneable
{ {

2
src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs

@ -3,6 +3,8 @@
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
// TODO: flatten namespace.
// namespace SixLabors.ImageSharp.Formats.Ico;
namespace SixLabors.ImageSharp.Formats.Icon.Ico; namespace SixLabors.ImageSharp.Formats.Icon.Ico;
internal sealed class IcoDecoderCore : IconDecoderCore internal sealed class IcoDecoderCore : IconDecoderCore

8
src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs

@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Icon.Ico; namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <summary> /// <summary>
/// IcoFrameMetadata /// IcoFrameMetadata. TODO: Remove base class and merge into this class.
/// </summary> /// </summary>
public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable<IcoFrameMetadata>, IDeepCloneable public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
{ {
@ -38,11 +38,9 @@ public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable<IcoFrameMetada
} }
/// <summary> /// <summary>
/// Gets or sets Specifies bits per pixel. /// Gets or sets the bits per pixel.
/// TODO: This needs to be constrained and calculated using the metadata returned from the png/bmp.
/// </summary> /// </summary>
/// <remarks>
/// It may used by Encoder.
/// </remarks>
public ushort BitCount { get => this.Field2; set => this.Field2 = value; } public ushort BitCount { get => this.Field2; set => this.Field2 = value; }
/// <inheritdoc/> /// <inheritdoc/>

8
src/ImageSharp/Formats/Icon/IconAssert.cs

@ -32,12 +32,4 @@ internal class IconAssert
return v; return v;
} }
internal static void NotSquare(in Size size)
{
if (size.Width != size.Height)
{
throw new FormatException("This image is not square.");
}
}
} }

107
src/ImageSharp/Formats/Icon/IconDecoderCore.cs

@ -2,7 +2,8 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -30,50 +31,63 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
long basePosition = stream.Position; long basePosition = stream.Position;
this.ReadHeader(stream); this.ReadHeader(stream);
Span<byte> flag = stackalloc byte[Png.PngConstants.HeaderBytes.Length]; Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length];
Image<TPixel> result = new(this.Dimensions.Width, this.Dimensions.Height);
List<(Image<TPixel> Image, bool IsPng, int Index)> frames = new(this.Entries.Length);
for (int i = 0; i < this.Entries.Length; i++) for (int i = 0; i < this.Entries.Length; i++)
{ {
_ = IconAssert.EndOfStream(stream.Seek(basePosition + this.Entries[i].ImageOffset, SeekOrigin.Begin), basePosition + this.Entries[i].ImageOffset); ref IconDirEntry entry = ref this.Entries[i];
_ = IconAssert.EndOfStream(stream.Read(flag), Png.PngConstants.HeaderBytes.Length);
_ = stream.Seek(-Png.PngConstants.HeaderBytes.Length, SeekOrigin.Current);
bool isPng = flag.SequenceEqual(Png.PngConstants.HeaderBytes); // If we hit the end of the stream we should break.
if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length)
{
break;
}
Image<TPixel> img = this.GetDecoder(isPng).Decode<TPixel>(stream, cancellationToken); // There should always be enough bytes for this regardless of the entry type.
IconAssert.NotSquare(img.Size); if (stream.Read(flag) != PngConstants.HeaderBytes.Length)
frames.Add((img, isPng, i));
if (isPng && img.Size.Width > this.Dimensions.Width)
{ {
this.Dimensions = img.Size; break;
} }
}
ImageMetadata metadata = new(); // Reset the stream position.
return new(this.Options.Configuration, metadata, frames.Select(i => stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current);
{
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions); bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
ImageFrame<TPixel> source = i.Image.Frames.RootFrameUnsafe; using Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(stream, cancellationToken);
ImageFrame<TPixel> source = temp.Frames.RootFrameUnsafe;
ImageFrame<TPixel> target = i == 0 ? result.Frames.RootFrameUnsafe : result.Frames.CreateFrame();
// Draw the new frame at position 0,0. We capture the dimensions for cropping during encoding
// via the icon entry.
for (int h = 0; h < source.Height; h++) for (int h = 0; h < source.Height; h++)
{ {
source.PixelBuffer.DangerousGetRowSpan(h).CopyTo(target.PixelBuffer.DangerousGetRowSpan(h)); source.PixelBuffer.DangerousGetRowSpan(h).CopyTo(target.PixelBuffer.DangerousGetRowSpan(h));
} }
if (i.IsPng) // Copy the format specific metadata to the image.
if (isPng)
{ {
target.Metadata.UnsafeSetFormatMetadata(Png.PngFormat.Instance, i.Image.Metadata.GetPngMetadata()); if (i == 0)
{
result.Metadata.SetFormatMetadata(PngFormat.Instance, temp.Metadata.GetPngMetadata());
}
target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngFrameMetadata());
} }
else else if (i == 0)
{ {
target.Metadata.UnsafeSetFormatMetadata(Bmp.BmpFormat.Instance, i.Image.Metadata.GetBmpMetadata()); // Bmp does not contain frame specific metadata.
result.Metadata.SetFormatMetadata(BmpFormat.Instance, temp.Metadata.GetBmpMetadata());
} }
this.GetFrameMetadata(target.Metadata).FromIconDirEntry(this.Entries[i.Index]); // TODO: The inheriting decoder should be responsible for setting the actual data (FromIconDirEntry)
// so we can avoid the protected Field1 and Field2 properties and use strong typing.
this.GetFrameMetadata(target.Metadata).FromIconDirEntry(entry);
}
i.Image.Dispose(); return result;
return target;
}).ToArray());
} }
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
@ -84,11 +98,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
ImageFrameMetadata[] frames = new ImageFrameMetadata[this.FileHeader.Count]; ImageFrameMetadata[] frames = new ImageFrameMetadata[this.FileHeader.Count];
for (int i = 0; i < frames.Length; i++) for (int i = 0; i < frames.Length; i++)
{ {
// TODO: Use the Identify methods in each decoder to return the
// format specific metadata for the image and frame.
frames[i] = new(); frames[i] = new();
IconFrameMetadata icoFrameMetadata = this.GetFrameMetadata(frames[i]); IconFrameMetadata icoFrameMetadata = this.GetFrameMetadata(frames[i]);
icoFrameMetadata.FromIconDirEntry(this.Entries[i]); icoFrameMetadata.FromIconDirEntry(this.Entries[i]);
} }
// TODO: Use real values from the metadata.
return new(new(32), new(0), metadata, frames); return new(new(32), new(0), metadata, frames);
} }
@ -96,6 +113,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
protected void ReadHeader(Stream stream) protected void ReadHeader(Stream stream)
{ {
// TODO: Check length and throw if the header cannot be read.
_ = Read(stream, out this.fileHeader, IconDir.Size); _ = Read(stream, out this.fileHeader, IconDir.Size);
this.Entries = new IconDirEntry[this.FileHeader.Count]; this.Entries = new IconDirEntry[this.FileHeader.Count];
for (int i = 0; i < this.Entries.Length; i++) for (int i = 0; i < this.Entries.Length; i++)
@ -103,15 +121,39 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
_ = Read(stream, out this.Entries[i], IconDirEntry.Size); _ = Read(stream, out this.Entries[i], IconDirEntry.Size);
} }
this.Dimensions = new( int width = 0;
this.Entries.Max(i => i.Width), int height = 0;
this.Entries.Max(i => i.Height)); foreach (IconDirEntry entry in this.Entries)
{
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 static int Read<T>(Stream stream, out T data, int size) private static int Read<T>(Stream stream, out T data, int size)
where T : unmanaged where T : unmanaged
{ {
// TODO: Use explicit parsing methods for each T type.
// See PngHeader.Parse() for example.
Span<byte> buffer = stackalloc byte[size]; Span<byte> buffer = stackalloc byte[size];
_ = IconAssert.EndOfStream(stream.Read(buffer), size); _ = IconAssert.EndOfStream(stream.Read(buffer), size);
data = MemoryMarshal.Cast<byte, T>(buffer)[0]; data = MemoryMarshal.Cast<byte, T>(buffer)[0];
return size; return size;
@ -121,15 +163,16 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
{ {
if (isPng) if (isPng)
{ {
return new Png.PngDecoderCore(this.Options); return new PngDecoderCore(this.Options);
} }
else else
{ {
return new Bmp.BmpDecoderCore(new() return new BmpDecoderCore(new()
{ {
GeneralOptions = this.Options,
ProcessedAlphaMask = true, ProcessedAlphaMask = true,
SkipFileHeader = true, SkipFileHeader = true,
IsDoubleHeight = true, UseDoubleHeight = true,
}); });
} }
} }

5
src/ImageSharp/Formats/Icon/IconFrameCompression.cs

@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Icon;
/// </summary> /// </summary>
public enum IconFrameCompression public enum IconFrameCompression
{ {
/// <summary>
/// Unknown
/// </summary>
Unknown,
/// <summary> /// <summary>
/// Bmp /// Bmp
/// </summary> /// </summary>

1
src/ImageSharp/Formats/Icon/IconFrameMetadata.cs

@ -5,6 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary> /// <summary>
/// IconFrameMetadata /// IconFrameMetadata
/// TODO: Delete this. Treat the individual metadata types as separate types so we can avoid Field1, Field2 and use strong typing with constaints.
/// </summary> /// </summary>
public abstract class IconFrameMetadata : IDeepCloneable public abstract class IconFrameMetadata : IDeepCloneable
{ {

6
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -99,9 +99,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
return newMeta; return newMeta;
} }
internal void UnsafeSetFormatMetadata( internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
IImageFormat key, where TFormatMetadata : class
IDeepCloneable value) where TFormatFrameMetadata : class, IDeepCloneable
=> this.formatMetadata[key] = value; => this.formatMetadata[key] = value;
/// <summary> /// <summary>

3
src/ImageSharp/Metadata/ImageMetadata.cs

@ -215,6 +215,9 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
metadata = default; metadata = default;
return false; return false;
} }
internal void SetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> key, TFormatMetadata value)
where TFormatMetadata : class, IDeepCloneable
=> this.formatMetadata[key] = value;
/// <inheritdoc/> /// <inheritdoc/>
public ImageMetadata DeepClone() => new(this); public ImageMetadata DeepClone() => new(this);

7
tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon.Cur; using SixLabors.ImageSharp.Formats.Icon.Cur;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Tests.TestImages.Cur; using static SixLabors.ImageSharp.Tests.TestImages.Cur;
@ -18,6 +17,10 @@ public class CurDecoderTests
{ {
using Image<Rgba32> image = provider.GetImage(CurDecoder.Instance); using Image<Rgba32> image = provider.GetImage(CurDecoder.Instance);
image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder()); image.DebugSaveMultiFrame(provider, extension: "png");
image.DebugSaveMultiFrame(provider, extension: "png");
// TODO: Assert metadata, frame count, etc
} }
} }

5
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon.Ico; using SixLabors.ImageSharp.Formats.Icon.Ico;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Tests.TestImages.Ico; using static SixLabors.ImageSharp.Tests.TestImages.Ico;
@ -18,6 +17,8 @@ public class IcoDecoderTests
{ {
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder()); image.DebugSaveMultiFrame(provider, extension: "png");
// TODO: Assert metadata, frame count, etc
} }
} }

Loading…
Cancel
Save