Browse Source

Merge pull request #2751 from SixLabors/js/format-conversion

Add API for metadata conversion between formats.
pull/2786/head
James Jackson-South 2 years ago
committed by GitHub
parent
commit
8da27c94fa
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 39
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 62
      src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
  3. 14
      src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
  4. 19
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  5. 1
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  6. 26
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  7. 113
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  8. 20
      src/ImageSharp/Formats/Bmp/MetadataExtensions.cs
  9. 18
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  10. 133
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  11. 173
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  12. 45
      src/ImageSharp/Formats/Cur/MetadataExtensions.cs
  13. 20
      src/ImageSharp/Formats/EncodingType.cs
  14. 54
      src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
  15. 70
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  16. 23
      src/ImageSharp/Formats/FrameBlendMode.cs
  17. 20
      src/ImageSharp/Formats/FrameColorTableMode.cs
  18. 38
      src/ImageSharp/Formats/FrameDisposalMode.cs
  19. 20
      src/ImageSharp/Formats/Gif/GifColorTableMode.cs
  20. 32
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  21. 37
      src/ImageSharp/Formats/Gif/GifDisposalMethod.cs
  22. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  23. 75
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  24. 76
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  25. 58
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  26. 105
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  27. 6
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  28. 33
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  29. 41
      src/ImageSharp/Formats/IFormatMetadata.cs
  30. 50
      src/ImageSharp/Formats/IImageDecoderInternals.cs
  31. 22
      src/ImageSharp/Formats/IImageEncoderInternals.cs
  32. 6
      src/ImageSharp/Formats/IImageFormat.cs
  33. 16
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  34. 133
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  35. 159
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  36. 45
      src/ImageSharp/Formats/Ico/MetadataExtensions.cs
  37. 46
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  38. 6
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  39. 26
      src/ImageSharp/Formats/ImageDecoder.cs
  40. 127
      src/ImageSharp/Formats/ImageDecoderCore.cs
  41. 81
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  42. 7
      src/ImageSharp/Formats/ImageEncoder.cs
  43. 4
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs
  44. 6
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  45. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs
  46. 2
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  47. 42
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  48. 2
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  49. 18
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs
  50. 12
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  51. 138
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  52. 21
      src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
  53. 25
      src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs
  54. 20
      src/ImageSharp/Formats/Pbm/MetadataExtensions.cs
  55. 26
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  56. 2
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  57. 3
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  58. 96
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  59. 20
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  60. 84
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  61. 22
      src/ImageSharp/Formats/Png/PngBlendMethod.cs
  62. 6
      src/ImageSharp/Formats/Png/PngDecoder.cs
  63. 87
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  64. 25
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  65. 149
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  66. 56
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  67. 165
      src/ImageSharp/Formats/Png/PngMetadata.cs
  68. 20
      src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
  69. 23
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  70. 10
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  71. 60
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  72. 20
      src/ImageSharp/Formats/Tga/MetadataExtensions.cs
  73. 8
      src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs
  74. 20
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  75. 110
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  76. 71
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  77. 2
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
  78. 4
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  79. 45
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  80. 2
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  81. 27
      src/ImageSharp/Formats/Tiff/MetadataExtensions.cs
  82. 8
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  83. 17
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  84. 16
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  85. 78
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  86. 2
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  87. 245
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  88. 32
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  89. 152
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  90. 20
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  91. 4
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  92. 8
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  93. 75
      src/ImageSharp/Formats/Webp/MetadataExtensions.cs
  94. 4
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  95. 4
      src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs
  96. 22
      src/ImageSharp/Formats/Webp/WebpBlendMethod.cs
  97. 33
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  98. 25
      src/ImageSharp/Formats/Webp/WebpColorType.cs
  99. 50
      src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
  100. 37
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

39
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -10,10 +10,13 @@ using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -129,6 +132,7 @@ internal static class AotCompilerTools
AotCompileImageDecoderInternals<TPixel>(); AotCompileImageDecoderInternals<TPixel>();
AotCompileImageEncoders<TPixel>(); AotCompileImageEncoders<TPixel>();
AotCompileImageDecoders<TPixel>(); AotCompileImageDecoders<TPixel>();
AotCompileSpectralConverter<TPixel>();
AotCompileImageProcessors<TPixel>(); AotCompileImageProcessors<TPixel>();
AotCompileGenericImageProcessors<TPixel>(); AotCompileGenericImageProcessors<TPixel>();
AotCompileResamplers<TPixel>(); AotCompileResamplers<TPixel>();
@ -195,39 +199,41 @@ internal static class AotCompilerTools
=> default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext<TPixel>(default, default, default); => default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext<TPixel>(default, default, default);
/// <summary> /// <summary>
/// This method pre-seeds the all <see cref="IImageEncoderInternals"/> in the AoT compiler. /// This method pre-seeds the all core encoders in the AoT compiler.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve] [Preserve]
private static void AotCompileImageEncoderInternals<TPixel>() private static void AotCompileImageEncoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
default(BmpEncoderCore).Encode<TPixel>(default, default, default); default(BmpEncoderCore).Encode<TPixel>(default, default, default);
default(GifEncoderCore).Encode<TPixel>(default, default, default); default(GifEncoderCore).Encode<TPixel>(default, default, default);
default(JpegEncoderCore).Encode<TPixel>(default, default, default); default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PbmEncoderCore).Encode<TPixel>(default, default, default); default(PbmEncoderCore).Encode<TPixel>(default, default, default);
default(PngEncoderCore).Encode<TPixel>(default, default, default); default(PngEncoderCore).Encode<TPixel>(default, default, default);
default(QoiEncoderCore).Encode<TPixel>(default, default, default);
default(TgaEncoderCore).Encode<TPixel>(default, default, default); default(TgaEncoderCore).Encode<TPixel>(default, default, default);
default(TiffEncoderCore).Encode<TPixel>(default, default, default); default(TiffEncoderCore).Encode<TPixel>(default, default, default);
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
} }
/// <summary> /// <summary>
/// This method pre-seeds the all <see cref="IImageDecoderInternals"/> in the AoT compiler. /// This method pre-seeds the all <see cref="ImageDecoderCore"/> in the AoT compiler.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve] [Preserve]
private static void AotCompileImageDecoderInternals<TPixel>() private static void AotCompileImageDecoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
default(WebpDecoderCore).Decode<TPixel>(default, default); default(BmpDecoderCore).Decode<TPixel>(default, default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default); default(GifDecoderCore).Decode<TPixel>(default, default, default);
default(GifDecoderCore).Decode<TPixel>(default, default); default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default); default(PbmDecoderCore).Decode<TPixel>(default, default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default); default(PngDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default); default(QoiDecoderCore).Decode<TPixel>(default, default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default); default(TgaDecoderCore).Decode<TPixel>(default, default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default); default(TiffDecoderCore).Decode<TPixel>(default, default, default);
default(WebpDecoderCore).Decode<TPixel>(default, default, default);
} }
/// <summary> /// <summary>
@ -266,6 +272,17 @@ internal static class AotCompilerTools
AotCompileImageDecoder<TPixel, TiffDecoder>(); AotCompileImageDecoder<TPixel, TiffDecoder>();
} }
[Preserve]
private static void AotCompileSpectralConverter<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(SpectralConverter<TPixel>).GetPixelBuffer(default);
default(GrayJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(RgbJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
default(TiffOldJpegSpectralConverter<TPixel>).GetPixelBuffer(default);
}
/// <summary> /// <summary>
/// This method pre-seeds the <see cref="IImageEncoder"/> in the AoT compiler. /// This method pre-seeds the <see cref="IImageEncoder"/> in the AoT compiler.
/// </summary> /// </summary>

62
src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs

@ -30,65 +30,3 @@ internal class AnimatedImageFrameMetadata
/// </summary> /// </summary>
public FrameDisposalMode DisposalMode { get; set; } public FrameDisposalMode DisposalMode { get; set; }
} }
#pragma warning disable SA1201 // Elements should appear in the correct order
internal enum FrameBlendMode
#pragma warning restore SA1201 // Elements should appear in the correct order
{
/// <summary>
/// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame.
/// </summary>
Source = 0,
/// <summary>
/// Blend the current frame with the previous frame in the animation sequence within the rectangle covered
/// by the current frame.
/// If the current has any transparent areas, the corresponding areas of the previous frame will be visible
/// through these transparent regions.
/// </summary>
Over = 1
}
internal enum FrameDisposalMode
{
/// <summary>
/// No disposal specified.
/// The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to
/// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains
/// transparency, the previous frame will be visible through these transparent areas.
/// </summary>
DoNotDispose = 1,
/// <summary>
/// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is
/// filled with the background color specified in the image metadata.
/// This effectively erases the current frame by replacing it with the background color before the next frame is displayed.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// Restore to previous. This method restores the area affected by the current frame to what it was before the
/// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image
/// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small
/// part of the image changes from frame to frame.
/// </summary>
RestoreToPrevious = 3
}
internal enum FrameColorTableMode
{
/// <summary>
/// The frame uses the shared color table specified by the image metadata.
/// </summary>
Global,
/// <summary>
/// The frame uses a color table specified by the frame metadata.
/// </summary>
Local
}

14
src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs

@ -11,35 +11,35 @@ public enum BmpBitsPerPixel : short
/// <summary> /// <summary>
/// 1 bit per pixel. /// 1 bit per pixel.
/// </summary> /// </summary>
Pixel1 = 1, Bit1 = 1,
/// <summary> /// <summary>
/// 2 bits per pixel. /// 2 bits per pixel.
/// </summary> /// </summary>
Pixel2 = 2, Bit2 = 2,
/// <summary> /// <summary>
/// 4 bits per pixel. /// 4 bits per pixel.
/// </summary> /// </summary>
Pixel4 = 4, Bit4 = 4,
/// <summary> /// <summary>
/// 8 bits per pixel. Each pixel consists of 1 byte. /// 8 bits per pixel. Each pixel consists of 1 byte.
/// </summary> /// </summary>
Pixel8 = 8, Bit8 = 8,
/// <summary> /// <summary>
/// 16 bits per pixel. Each pixel consists of 2 bytes. /// 16 bits per pixel. Each pixel consists of 2 bytes.
/// </summary> /// </summary>
Pixel16 = 16, Bit16 = 16,
/// <summary> /// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes. /// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary> /// </summary>
Pixel24 = 24, Bit24 = 24,
/// <summary> /// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes. /// 32 bits per pixel. Each pixel consists of 4 bytes.
/// </summary> /// </summary>
Pixel32 = 32 Bit32 = 32
} }

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

@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp;
/// <remarks> /// <remarks>
/// A useful decoding source example can be found at <see href="https://dxr.mozilla.org/mozilla-central/source/image/decoders/nsBMPDecoder.cpp"/> /// A useful decoding source example can be found at <see href="https://dxr.mozilla.org/mozilla-central/source/image/decoders/nsBMPDecoder.cpp"/>
/// </remarks> /// </remarks>
internal sealed class BmpDecoderCore : IImageDecoderInternals internal sealed class BmpDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// The default mask for the red part of the color for 16 bit rgb bitmaps. /// The default mask for the red part of the color for 16 bit rgb bitmaps.
@ -114,8 +114,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
public BmpDecoderCore(BmpDecoderOptions options) public BmpDecoderCore(BmpDecoderOptions options)
: base(options.GeneralOptions)
{ {
this.Options = options.GeneralOptions;
this.rleSkippedPixelHandling = options.RleSkippedPixelHandling; this.rleSkippedPixelHandling = options.RleSkippedPixelHandling;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
@ -125,14 +125,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc />
public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel>? image = null; Image<TPixel>? image = null;
try try
@ -224,10 +217,10 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ReadImageHeaders(stream, out _, out _); this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); return new ImageInfo(new(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
} }
/// <summary> /// <summary>
@ -1463,6 +1456,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata = this.metadata.GetBmpMetadata();
this.bmpMetadata.InfoHeaderType = infoHeaderType; this.bmpMetadata.InfoHeaderType = infoHeaderType;
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
this.Dimensions = new(this.infoHeader.Width, this.infoHeader.Height);
} }
/// <summary> /// <summary>

1
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;

26
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a Windows bitmap. /// Image encoder for writing an image to a stream as a Windows bitmap.
/// </summary> /// </summary>
internal sealed class BmpEncoderCore : IImageEncoderInternals internal sealed class BmpEncoderCore
{ {
/// <summary> /// <summary>
/// The amount to pad each row by. /// The amount to pad each row by.
@ -146,10 +146,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
int colorPaletteSize = this.bitsPerPixel switch int colorPaletteSize = this.bitsPerPixel switch
{ {
BmpBitsPerPixel.Pixel8 => ColorPaletteSize8Bit, BmpBitsPerPixel.Bit8 => ColorPaletteSize8Bit,
BmpBitsPerPixel.Pixel4 => ColorPaletteSize4Bit, BmpBitsPerPixel.Bit4 => ColorPaletteSize4Bit,
BmpBitsPerPixel.Pixel2 => ColorPaletteSize2Bit, BmpBitsPerPixel.Bit2 => ColorPaletteSize2Bit,
BmpBitsPerPixel.Pixel1 => ColorPaletteSize1Bit, BmpBitsPerPixel.Bit1 => ColorPaletteSize1Bit,
_ => 0 _ => 0
}; };
@ -248,7 +248,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
clrUsed: 0, clrUsed: 0,
clrImportant: 0); clrImportant: 0);
if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Bit32)
{ {
infoHeader.AlphaMask = Rgba32AlphaMask; infoHeader.AlphaMask = Rgba32AlphaMask;
infoHeader.RedMask = Rgba32RedMask; infoHeader.RedMask = Rgba32RedMask;
@ -351,31 +351,31 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case BmpBitsPerPixel.Pixel32: case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels); this.Write32BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel24: case BmpBitsPerPixel.Bit24:
this.Write24BitPixelData(configuration, stream, pixels); this.Write24BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel16: case BmpBitsPerPixel.Bit16:
this.Write16BitPixelData(configuration, stream, pixels); this.Write16BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel8: case BmpBitsPerPixel.Bit8:
this.Write8BitPixelData(configuration, stream, image); this.Write8BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel4: case BmpBitsPerPixel.Bit4:
this.Write4BitPixelData(configuration, stream, image); this.Write4BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel2: case BmpBitsPerPixel.Bit2:
this.Write2BitPixelData(configuration, stream, image); this.Write2BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel1: case BmpBitsPerPixel.Bit1:
this.Write1BitPixelData(configuration, stream, image); this.Write1BitPixelData(configuration, stream, image);
break; break;
} }

113
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -1,12 +1,15 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
// TODO: Add color table information.
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary> /// <summary>
/// Provides Bmp specific metadata information for the image. /// Provides Bmp specific metadata information for the image.
/// </summary> /// </summary>
public class BmpMetadata : IDeepCloneable public class BmpMetadata : IFormatMetadata<BmpMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BmpMetadata"/> class. /// Initializes a new instance of the <see cref="BmpMetadata"/> class.
@ -38,7 +41,7 @@ public class BmpMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel. /// Gets or sets the number of bits per pixel.
/// </summary> /// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Bit24;
/// <summary> /// <summary>
/// Gets or sets the color table, if any. /// Gets or sets the color table, if any.
@ -46,5 +49,109 @@ public class BmpMetadata : IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new BmpMetadata(this); public static BmpMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
return bpp switch
{
1 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit1 },
2 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit2 },
<= 4 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit4 },
<= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Bit8 },
<= 16 => new BmpMetadata
{
BitsPerPixel = BmpBitsPerPixel.Bit16,
InfoHeaderType = BmpInfoHeaderType.WinVersion3
},
<= 24 => new BmpMetadata
{
BitsPerPixel = BmpBitsPerPixel.Bit24,
InfoHeaderType = BmpInfoHeaderType.WinVersion4
},
_ => new BmpMetadata
{
BitsPerPixel = BmpBitsPerPixel.Bit32,
InfoHeaderType = BmpInfoHeaderType.WinVersion5
}
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BitsPerPixel;
PixelAlphaRepresentation alpha = this.InfoHeaderType switch
{
BmpInfoHeaderType.WinVersion2 or
BmpInfoHeaderType.Os2Version2Short or
BmpInfoHeaderType.WinVersion3 or
BmpInfoHeaderType.AdobeVersion3 or
BmpInfoHeaderType.Os2Version2 => PixelAlphaRepresentation.None,
BmpInfoHeaderType.AdobeVersion3WithAlpha or
BmpInfoHeaderType.WinVersion4 or
BmpInfoHeaderType.WinVersion5 or
_ => bpp < 32 ? PixelAlphaRepresentation.None : PixelAlphaRepresentation.Unassociated
};
PixelComponentInfo info;
PixelColorType color;
switch (this.BitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
info = PixelComponentInfo.Create(1, bpp, 1);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit2:
info = PixelComponentInfo.Create(1, bpp, 2);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
info = PixelComponentInfo.Create(1, bpp, 4);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Indexed;
break;
// Could be 555 with padding but 565 is more common in newer bitmaps and offers
// greater accuracy due to extra green precision.
case BmpBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(3, bpp, 5, 6, 5);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public BmpMetadata DeepClone() => new(this);
} }

20
src/ImageSharp/Formats/Bmp/MetadataExtensions.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
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 bmp format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="BmpMetadata"/>.</returns>
public static BmpMetadata GetBmpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(BmpFormat.Instance);
}

18
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -15,16 +15,30 @@ internal sealed class CurDecoderCore : IconDecoderCore
} }
protected override void SetFrameMetadata( protected override void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? colorTable) ReadOnlyMemory<Color>? colorTable)
{ {
CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata();
curFrameMetadata.FromIconDirEntry(entry); curFrameMetadata.FromIconDirEntry(entry);
curFrameMetadata.Compression = compression; curFrameMetadata.Compression = compression;
curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; curFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
curFrameMetadata.ColorTable = colorTable; curFrameMetadata.ColorTable = colorTable;
if (index == 0)
{
CurMetadata curMetadata = imageMetadata.GetCurMetadata();
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
curMetadata.HotspotX = curFrameMetadata.HotspotX;
curMetadata.HotspotY = curFrameMetadata.HotspotY;
}
} }
} }

133
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary> /// <summary>
/// IcoFrameMetadata. /// IcoFrameMetadata.
/// </summary> /// </summary>
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class. /// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
@ -60,7 +60,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary> /// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary> /// <summary>
/// Gets or sets the color table, if any. /// Gets or sets the color table, if any.
@ -69,11 +69,75 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this); public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
if (!metadata.PixelTypeInfo.HasValue)
{
return new CurFrameMetadata
{
BmpBitsPerPixel = BmpBitsPerPixel.Bit32,
Compression = IconFrameCompression.Png
};
}
byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};
byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
IconFrameCompression compression = IconFrameCompression.Bmp;
if (bbpp is BmpBitsPerPixel.Bit32)
{
compression = IconFrameCompression.Png;
}
return new CurFrameMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry) internal void FromIconDirEntry(IconDirEntry entry)
{ {
this.EncodingWidth = entry.Width; this.EncodingWidth = entry.Width;
@ -84,7 +148,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
internal IconDirEntry ToIconDirEntry() internal IconDirEntry ToIconDirEntry()
{ {
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0 ? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -97,4 +161,65 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
ColorCount = colorCount ColorCount = colorCount
}; };
} }
private PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
PixelComponentInfo info;
PixelColorType color;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bpp = 32;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
info = PixelComponentInfo.Create(1, bpp, 1);
color = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
info = PixelComponentInfo.Create(1, bpp, 2);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
info = PixelComponentInfo.Create(1, bpp, 4);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Indexed;
break;
// Could be 555 with padding but 565 is more common in newer bitmaps and offers
// greater accuracy due to extra green precision.
case BmpBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(3, bpp, 5, 6, 5);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
} }

173
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -1,16 +1,183 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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; namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image. /// Provides Cur specific metadata information for the image.
/// </summary> /// </summary>
public class CurMetadata : IDeepCloneable<CurMetadata>, IDeepCloneable public class CurMetadata : IFormatMetadata<CurMetadata>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="CurMetadata"/> class.
/// </summary>
public CurMetadata()
{
}
private CurMetadata(CurMetadata 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;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
}
/// <summary>
/// Gets or sets the frame compressions format. Derived from the root frame.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
/// </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. Derived from the root frame.
/// </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. Derived from the root frame.
/// </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.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public CurMetadata DeepClone() => new(); public static CurMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
IconFrameCompression compression = IconFrameCompression.Bmp;
if (bbpp is BmpBitsPerPixel.Bit32)
{
compression = IconFrameCompression.Png;
}
return new CurMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
PixelComponentInfo info;
PixelColorType color;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bpp = 32;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
info = PixelComponentInfo.Create(1, bpp, 1);
color = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
info = PixelComponentInfo.Create(1, bpp, 2);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
info = PixelComponentInfo.Create(1, bpp, 4);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Indexed;
break;
// Could be 555 with padding but 565 is more common in newer bitmaps and offers
// greater accuracy due to extra green precision.
case BmpBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(3, bpp, 5, 6, 5);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurMetadata DeepClone() => new(this);
} }

45
src/ImageSharp/Formats/Cur/MetadataExtensions.cs

@ -1,45 +0,0 @@
// 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);
}

20
src/ImageSharp/Formats/EncodingType.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides a way to specify the type of encoding to be used.
/// </summary>
public enum EncodingType
{
/// <summary>
/// Lossless encoding, which compresses data without any loss of information.
/// </summary>
Lossless,
/// <summary>
/// Lossy encoding, which compresses data by discarding some of it.
/// </summary>
Lossy
}

54
src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs

@ -0,0 +1,54 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// A metadata format designed to allow conversion between different image format frames.
/// </summary>
public class FormatConnectingFrameMetadata
{
/// <summary>
/// Gets information about the encoded pixel type if any.
/// </summary>
public PixelTypeInfo? PixelTypeInfo { get; init; }
/// <summary>
/// Gets the frame color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }
/// <summary>
/// Gets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; init; }
/// <summary>
/// Gets the duration of the frame.
/// </summary>
public TimeSpan Duration { get; init; }
/// <summary>
/// Gets the frame alpha blending mode.
/// </summary>
public FrameBlendMode BlendMode { get; init; }
/// <summary>
/// Gets the frame disposal mode.
/// </summary>
public FrameDisposalMode DisposalMode { get; init; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Used for formats that require a specific frame size.
/// </summary>
public int? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Used for formats that require a specific frame size.
/// </summary>
public int? EncodingHeight { get; set; }
}

70
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -0,0 +1,70 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// A metadata format designed to allow conversion between different image formats.
/// </summary>
public class FormatConnectingMetadata
{
/// <summary>
/// Gets the encoding type.
/// </summary>
public EncodingType EncodingType { get; init; }
/// <summary>
/// Gets the quality to use when <see cref="EncodingType"/> is <see cref="EncodingType.Lossy"/>.
/// </summary>
/// <remarks>
/// The value is usually between 1 and 100. Defaults to 100.
/// </remarks>
public int Quality { get; init; } = 100;
/// <summary>
/// Gets information about the encoded pixel type.
/// </summary>
public PixelTypeInfo PixelTypeInfo { get; init; }
/// <summary>
/// Gets the shared color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }
/// <summary>
/// Gets the shared color table mode.
/// </summary>
/// <remarks>
/// Defaults to <see cref="FrameColorTableMode.Global"/>.
/// </remarks>
public FrameColorTableMode ColorTableMode { get; init; } = FrameColorTableMode.Global;
/// <summary>
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.
/// </remarks>
public Color BackgroundColor { get; init; } = Color.Transparent;
/// <summary>
/// Gets the number of times any animation is repeated.
/// </summary>
/// <remarks>
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
/// </remarks>
public ushort RepeatCount { get; init; } = 1;
/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence.
/// </summary>
/// <remarks>
/// Defaults to <see langword="true"/>.
/// </remarks>
public bool AnimateRootFrame { get; init; } = true;
}

23
src/ImageSharp/Formats/FrameBlendMode.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides a way to specify how the current frame should be blended with the previous frame in the animation sequence.
/// </summary>
public enum FrameBlendMode
{
/// <summary>
/// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame.
/// </summary>
Source = 0,
/// <summary>
/// Blend the current frame with the previous frame in the animation sequence within the rectangle covered
/// by the current frame.
/// If the current has any transparent areas, the corresponding areas of the previous frame will be visible
/// through these transparent regions.
/// </summary>
Over = 1
}

20
src/ImageSharp/Formats/FrameColorTableMode.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides a way to specify how the color table is used by the frame.
/// </summary>
public enum FrameColorTableMode
{
/// <summary>
/// The frame uses the shared color table specified by the image metadata.
/// </summary>
Global,
/// <summary>
/// The frame uses a color table specified by the frame metadata.
/// </summary>
Local
}

38
src/ImageSharp/Formats/FrameDisposalMode.cs

@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides a way to specify how the current frame should be disposed of before rendering the next frame.
/// </summary>
public enum FrameDisposalMode
{
/// <summary>
/// No disposal specified.
/// The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to
/// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains
/// transparency, the previous frame will be visible through these transparent areas.
/// </summary>
DoNotDispose = 1,
/// <summary>
/// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is
/// filled with the background color specified in the image metadata.
/// This effectively erases the current frame by replacing it with the background color before the next frame is displayed.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// Restore to previous. This method restores the area affected by the current frame to what it was before the
/// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image
/// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small
/// part of the image changes from frame to frame.
/// </summary>
RestoreToPrevious = 3
}

20
src/ImageSharp/Formats/Gif/GifColorTableMode.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides enumeration for the available color table modes.
/// </summary>
public enum GifColorTableMode
{
/// <summary>
/// A single color table is calculated from the first frame and reused for subsequent frames.
/// </summary>
Global,
/// <summary>
/// A unique color table is calculated for each frame.
/// </summary>
Local
}

32
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
/// Performs the gif decoding operation. /// Performs the gif decoding operation.
/// </summary> /// </summary>
internal sealed class GifDecoderCore : IImageDecoderInternals internal sealed class GifDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// The temp buffer used to reduce allocations. /// The temp buffer used to reduce allocations.
@ -94,8 +94,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public GifDecoderCore(DecoderOptions options) public GifDecoderCore(DecoderOptions options)
: base(options)
{ {
this.Options = options;
this.configuration = options.Configuration; this.configuration = options.Configuration;
this.skipMetadata = options.SkipMetadata; this.skipMetadata = options.SkipMetadata;
this.maxFrames = options.MaxFrames; this.maxFrames = options.MaxFrames;
@ -103,14 +103,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc />
public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
uint frameCount = 0; uint frameCount = 0;
Image<TPixel>? image = null; Image<TPixel>? image = null;
@ -181,7 +174,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
uint frameCount = 0; uint frameCount = 0;
ImageFrameMetadata? previousFrame = null; ImageFrameMetadata? previousFrame = null;
@ -249,7 +242,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
return new ImageInfo( return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata, this.metadata,
framesMetadata); framesMetadata);
@ -287,6 +279,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
} }
this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height);
} }
/// <summary> /// <summary>
@ -498,7 +492,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
else else
{ {
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{ {
prevFrame = previousFrame; prevFrame = previousFrame;
} }
@ -615,7 +609,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
previousFrame = currentFrame ?? image.Frames.RootFrame; previousFrame = currentFrame ?? image.Frames.RootFrame;
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground) if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToBackground)
{ {
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
} }
@ -690,14 +684,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0) && this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global; gifMeta.ColorTableMode = FrameColorTableMode.Global;
} }
if (this.imageDescriptor.LocalColorTableFlag if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0) && this.imageDescriptor.LocalColorTableSize > 0)
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local; gifMeta.ColorTableMode = FrameColorTableMode.Local;
Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize]; Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]); ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]);
@ -713,7 +707,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag; gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex; gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; gifMeta.DisposalMode = this.graphicsControlExtension.DisposalMethod;
} }
} }
@ -760,8 +754,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.metadata = meta; this.metadata = meta;
this.gifMetadata = meta.GetGifMetadata(); this.gifMetadata = meta.GetGifMetadata();
this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? GifColorTableMode.Global ? FrameColorTableMode.Global
: GifColorTableMode.Local; : FrameColorTableMode.Local;
if (this.logicalScreenDescriptor.GlobalColorTableFlag) if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{ {

37
src/ImageSharp/Formats/Gif/GifDisposalMethod.cs

@ -1,37 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides enumeration for instructing the decoder what to do with the last image
/// in an animation sequence.
/// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23
/// </summary>
public enum GifDisposalMethod
{
/// <summary>
/// No disposal specified.
/// The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// Do not dispose.
/// The graphic is to be left in place.
/// </summary>
NotDispose = 1,
/// <summary>
/// Restore to background color.
/// The area used by the graphic must be restored to the background color.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// Restore to previous.
/// The decoder is required to restore the area overwritten by the
/// graphic with what was there prior to rendering the graphic.
/// </summary>
RestoreToPrevious = 3
}

2
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -11,7 +11,7 @@ public sealed class GifEncoder : QuantizingImageEncoder
/// <summary> /// <summary>
/// Gets the color table mode: Global or local. /// Gets the color table mode: Global or local.
/// </summary> /// </summary>
public GifColorTableMode? ColorTableMode { get; init; } public FrameColorTableMode? ColorTableMode { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)

75
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -5,8 +5,6 @@ using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@ -19,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
/// Implements the GIF encoding protocol. /// Implements the GIF encoding protocol.
/// </summary> /// </summary>
internal sealed class GifEncoderCore : IImageEncoderInternals internal sealed class GifEncoderCore
{ {
/// <summary> /// <summary>
/// Used for allocating memory during processing operations. /// Used for allocating memory during processing operations.
@ -49,7 +47,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// The color table mode: Global or local. /// The color table mode: Global or local.
/// </summary> /// </summary>
private GifColorTableMode? colorTableMode; private FrameColorTableMode? colorTableMode;
/// <summary> /// <summary>
/// The pixel sampling strategy for global quantization. /// The pixel sampling strategy for global quantization.
@ -85,9 +83,9 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
GifMetadata gifMetadata = GetGifMetadata(image); GifMetadata gifMetadata = image.Metadata.CloneGifMetadata();
this.colorTableMode ??= gifMetadata.ColorTableMode; this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global;
// Quantize the first image frame returning a palette. // Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null; IndexedImageFrame<TPixel>? quantized = null;
@ -99,7 +97,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (this.quantizer is null) if (this.quantizer is null)
{ {
// Is this a gif with color information. If so use that, otherwise use octree. // Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{ {
// We avoid dithering by default to preserve the original colors. // We avoid dithering by default to preserve the original colors.
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
@ -171,63 +169,24 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMethod); this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
stream.WriteByte(GifConstants.EndIntroducer); stream.WriteByte(GifConstants.EndIntroducer);
quantized?.Dispose(); quantized?.Dispose();
} }
private static GifMetadata GetGifMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
return (GifMetadata)gif.DeepClone();
}
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
return GifMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
return GifMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex) private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
GifFrameMetadata? metadata = null; GifFrameMetadata metadata = frame.Metadata.CloneGifMetadata();
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) if (metadata.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1)
{
metadata = (GifFrameMetadata)gif.DeepClone();
}
else if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
}
else if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
}
if (metadata?.ColorTableMode == GifColorTableMode.Global && transparencyIndex > -1)
{ {
metadata.HasTransparency = true; metadata.HasTransparency = true;
metadata.TransparencyIndex = ClampIndex(transparencyIndex); metadata.TransparencyIndex = ClampIndex(transparencyIndex);
} }
return metadata ?? new(); return metadata;
} }
private void EncodeAdditionalFrames<TPixel>( private void EncodeAdditionalFrames<TPixel>(
@ -235,7 +194,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Image<TPixel> image, Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette, ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex, int globalTransparencyIndex,
GifDisposalMethod previousDisposalMethod) FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (image.Frames.Count == 1) if (image.Frames.Count == 1)
@ -258,7 +217,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> currentFrame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local); bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0) if (!useLocal && !hasPaletteQuantizer && i > 0)
{ {
@ -279,10 +238,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
useLocal, useLocal,
gifMetadata, gifMetadata,
paletteQuantizer, paletteQuantizer,
previousDisposalMethod); previousDisposalMode);
previousFrame = currentFrame; previousFrame = currentFrame;
previousDisposalMethod = gifMetadata.DisposalMethod; previousDisposalMode = gifMetadata.DisposalMode;
} }
if (hasPaletteQuantizer) if (hasPaletteQuantizer)
@ -301,7 +260,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer; Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle(); Rectangle interest = indices.FullRectangle();
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata.ColorTableMode == GifColorTableMode.Local); bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream); this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
@ -323,14 +282,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool useLocal, bool useLocal,
GifFrameMetadata metadata, GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer, PaletteQuantizer<TPixel> globalPaletteQuantizer,
GifDisposalMethod previousDisposal) FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Capture any explicit transparency index from the metadata. // Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels. // We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1; int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
// Deduplicate and quantize the frame capturing only required parts. // Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) = (bool difference, Rectangle bounds) =
@ -664,7 +623,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool hasTransparency = metadata.HasTransparency; bool hasTransparency = metadata.HasTransparency;
byte packedValue = GifGraphicControlExtension.GetPackedValue( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod, disposalMode: metadata.DisposalMode,
transparencyFlag: hasTransparency); transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new( GifGraphicControlExtension extension = new(

76
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
/// Provides Gif specific metadata information for the image frame. /// Provides Gif specific metadata information for the image frame.
/// </summary> /// </summary>
public class GifFrameMetadata : IDeepCloneable public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifFrameMetadata"/> class. /// Initializes a new instance of the <see cref="GifFrameMetadata"/> class.
@ -26,7 +26,7 @@ public class GifFrameMetadata : IDeepCloneable
{ {
this.ColorTableMode = other.ColorTableMode; this.ColorTableMode = other.ColorTableMode;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMode = other.DisposalMode;
if (other.LocalColorTable?.Length > 0) if (other.LocalColorTable?.Length > 0)
{ {
@ -40,7 +40,7 @@ public class GifFrameMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the color table mode. /// Gets or sets the color table mode.
/// </summary> /// </summary>
public GifColorTableMode ColorTableMode { get; set; } public FrameColorTableMode ColorTableMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the local color table, if any. /// Gets or sets the local color table, if any.
@ -73,10 +73,64 @@ public class GifFrameMetadata : IDeepCloneable
/// Primarily used in Gif animation, this field indicates the way in which the graphic is to /// Primarily used in Gif animation, this field indicates the way in which the graphic is to
/// be treated after being displayed. /// be treated after being displayed.
/// </summary> /// </summary>
public GifDisposalMethod DisposalMethod { get; set; } public FrameDisposalMode DisposalMode { get; set; }
/// <inheritdoc />
public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
int index = -1;
const float background = 1f;
if (metadata.ColorTable.HasValue)
{
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
Vector4 vector = colorTable[i].ToScaledVector4();
if (vector.W < background)
{
index = i;
}
}
}
bool hasTransparency = index >= 0;
return new()
{
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMode = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
/// <inheritdoc />
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
{
// For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or
// has a local palette with 256 colors and is not transparent we should use 'Source'.
bool blendSource = this.DisposalMode == FrameDisposalMode.RestoreToBackground || (this.LocalColorTable?.Length == 256 && !this.HasTransparency);
// If the color table is global and frame has no transparency. Consider it 'Source' also.
blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency;
return new()
{
ColorTable = this.LocalColorTable,
ColorTableMode = this.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10),
DisposalMode = this.DisposalMode,
BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifFrameMetadata(this); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public GifFrameMetadata DeepClone() => new(this);
internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
{ {
@ -101,19 +155,11 @@ public class GifFrameMetadata : IDeepCloneable
return new() return new()
{ {
LocalColorTable = metadata.ColorTable, LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMethod = GetMode(metadata.DisposalMode), DisposalMode = metadata.DisposalMode,
HasTransparency = hasTransparency, HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
}; };
} }
private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
{
FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose,
FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious,
_ => GifDisposalMethod.Unspecified,
};
} }

58
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
/// Provides Gif specific metadata information for the image. /// Provides Gif specific metadata information for the image.
/// </summary> /// </summary>
public class GifMetadata : IDeepCloneable public class GifMetadata : IFormatMetadata<GifMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifMetadata"/> class. /// Initializes a new instance of the <see cref="GifMetadata"/> class.
@ -49,7 +49,7 @@ public class GifMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the color table mode. /// Gets or sets the color table mode.
/// </summary> /// </summary>
public GifColorTableMode ColorTableMode { get; set; } public FrameColorTableMode ColorTableMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the global color table, if any. /// Gets or sets the global color table, if any.
@ -67,12 +67,10 @@ public class GifMetadata : IDeepCloneable
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any /// Gets or sets the collection of comments about the graphics, credits, descriptions or any
/// other type of non-control and non-graphic data. /// other type of non-control and non-graphic data.
/// </summary> /// </summary>
public IList<string> Comments { get; set; } = new List<string>(); public IList<string> Comments { get; set; } = [];
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifMetadata(this); public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
{ {
int index = 0; int index = 0;
Color background = metadata.BackgroundColor; Color background = metadata.BackgroundColor;
@ -81,20 +79,60 @@ public class GifMetadata : IDeepCloneable
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span; ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++) for (int i = 0; i < colorTable.Length; i++)
{ {
if (background == colorTable[i]) if (background != colorTable[i])
{ {
index = i; continue;
break;
} }
index = i;
break;
} }
} }
return new() return new()
{ {
GlobalColorTable = metadata.ColorTable, GlobalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, ColorTableMode = metadata.ColorTableMode,
RepeatCount = metadata.RepeatCount, RepeatCount = metadata.RepeatCount,
BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255), BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255),
}; };
} }
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = this.GlobalColorTable.HasValue
? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8)
: 8;
return new PixelTypeInfo(bpp)
{
ColorType = PixelColorType.Indexed,
ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp),
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
{
Color color = this.GlobalColorTable.HasValue && this.GlobalColorTable.Value.Span.Length > this.BackgroundColorIndex
? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex]
: Color.Transparent;
return new()
{
AnimateRootFrame = true,
BackgroundColor = color,
ColorTable = this.GlobalColorTable,
ColorTableMode = this.ColorTableMode,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = this.RepeatCount,
};
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public GifMetadata DeepClone() => new(this);
} }

105
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -1,105 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
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 gif format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns>
public static GifMetadata GetGifMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">
/// When this method returns, contains the metadata associated with the specified image,
/// 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 gif metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageMetadata source, [NotNullWhen(true)] out GifMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns>
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif 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 gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
{
Color background = Color.Transparent;
if (source.GlobalColorTable != null)
{
background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex];
}
return new()
{
ColorTable = source.GlobalColorTable,
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
RepeatCount = source.RepeatCount,
BackgroundColor = background,
};
}
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source)
{
// For most scenarios we would consider the blend method to be 'Over' however if a frame has a disposal method of 'RestoreToBackground' or
// has a local palette with 256 colors and is not transparent we should use 'Source'.
bool blendSource = source.DisposalMethod == GifDisposalMethod.RestoreToBackground || (source.LocalColorTable?.Length == 256 && !source.HasTransparency);
// If the color table is global and frame has no transparency. Consider it 'Source' also.
blendSource |= source.ColorTableMode == GifColorTableMode.Global && !source.HasTransparency;
return new()
{
ColorTable = source.LocalColorTable,
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10),
DisposalMode = GetMode(source.DisposalMethod),
BlendMode = blendSource ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch
{
GifDisposalMethod.NotDispose => FrameDisposalMode.DoNotDispose,
GifDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
GifDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
_ => FrameDisposalMode.Unspecified,
};
}

6
src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

@ -52,7 +52,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
/// Gets the disposal method which indicates the way in which the /// Gets the disposal method which indicates the way in which the
/// graphic is to be treated after being displayed. /// graphic is to be treated after being displayed.
/// </summary> /// </summary>
public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2); public FrameDisposalMode DisposalMethod => (FrameDisposalMode)((this.Packed & 0x1C) >> 2);
/// <summary> /// <summary>
/// Gets a value indicating whether transparency flag is to be set. /// Gets a value indicating whether transparency flag is to be set.
@ -80,7 +80,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
public static GifGraphicControlExtension Parse(ReadOnlySpan<byte> buffer) public static GifGraphicControlExtension Parse(ReadOnlySpan<byte> buffer)
=> MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0]; => MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) public static byte GetPackedValue(FrameDisposalMode disposalMode, bool userInputFlag = false, bool transparencyFlag = false)
{ {
/* /*
Reserved | 3 Bits Reserved | 3 Bits
@ -91,7 +91,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
byte value = 0; byte value = 0;
value |= (byte)((int)disposalMethod << 2); value |= (byte)((int)disposalMode << 2);
if (userInputFlag) if (userInputFlag)
{ {

33
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// An interface that provides metadata for a specific image format frames.
/// </summary>
public interface IFormatFrameMetadata : IDeepCloneable
{
/// <summary>
/// Converts the metadata to a <see cref="FormatConnectingFrameMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns>
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
}
/// <summary>
/// An interface that provides metadata for a specific image format frames.
/// </summary>
/// <typeparam name="TSelf">The metadata type implementing this interface.</typeparam>
public interface IFormatFrameMetadata<TSelf> : IFormatFrameMetadata, IDeepCloneable<TSelf>
where TSelf : class, IFormatFrameMetadata
{
/// <summary>
/// Creates a new instance of the <typeparamref name="TSelf"/> class from the given <see cref="FormatConnectingFrameMetadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="FormatConnectingFrameMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

41
src/ImageSharp/Formats/IFormatMetadata.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// An interface that provides metadata for a specific image format.
/// </summary>
public interface IFormatMetadata : IDeepCloneable
{
/// <summary>
/// Converts the metadata to a <see cref="PixelTypeInfo"/> instance.
/// </summary>
/// <returns>The pixel type info.</returns>
PixelTypeInfo GetPixelTypeInfo();
/// <summary>
/// Converts the metadata to a <see cref="FormatConnectingMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingMetadata"/>.</returns>
FormatConnectingMetadata ToFormatConnectingMetadata();
}
/// <summary>
/// An interface that provides metadata for a specific image format.
/// </summary>
/// <typeparam name="TSelf">The metadata type implementing this interface.</typeparam>
public interface IFormatMetadata<TSelf> : IFormatMetadata, IDeepCloneable<TSelf>
where TSelf : class, IFormatMetadata
{
/// <summary>
/// Creates a new instance of the <typeparamref name="TSelf"/> class from the given <see cref="FormatConnectingMetadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="FormatConnectingMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

50
src/ImageSharp/Formats/IImageDecoderInternals.cs

@ -1,50 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Abstraction for shared internals for XXXDecoderCore implementations to be used with <see cref="ImageDecoderUtilities"/>.
/// </summary>
internal interface IImageDecoderInternals
{
/// <summary>
/// Gets the general decoder options.
/// </summary>
DecoderOptions Options { get; }
/// <summary>
/// Gets the dimensions of the image being decoded.
/// </summary>
Size Dimensions { get; }
/// <summary>
/// Decodes the image from the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
/// <returns>The decoded image.</returns>
/// <remarks>
/// Cancellable synchronous method. In case of cancellation,
/// an <see cref="OperationCanceledException"/> shall be thrown which will be handled on the call site.
/// </remarks>
Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The <see cref="ImageInfo"/>.</returns>
/// <remarks>
/// Cancellable synchronous method. In case of cancellation,
/// an <see cref="OperationCanceledException"/> shall be thrown which will be handled on the call site.
/// </remarks>
ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken);
}

22
src/ImageSharp/Formats/IImageEncoderInternals.cs

@ -1,22 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Abstraction for shared internals for ***DecoderCore implementations.
/// </summary>
internal interface IImageEncoderInternals
{
/// <summary>
/// Encodes the image.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="stream">The stream.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
}

6
src/ImageSharp/Formats/IImageFormat.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -14,12 +14,12 @@ public interface IImageFormat
string Name { get; } string Name { get; }
/// <summary> /// <summary>
/// Gets the default mimetype that the image format uses /// Gets the default mime type that the image format uses
/// </summary> /// </summary>
string DefaultMimeType { get; } string DefaultMimeType { get; }
/// <summary> /// <summary>
/// Gets all the mimetypes that have been used by this image format. /// Gets all the mime types that have been used by this image format.
/// </summary> /// </summary>
IEnumerable<string> MimeTypes { get; } IEnumerable<string> MimeTypes { get; }

16
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -15,16 +15,28 @@ internal sealed class IcoDecoderCore : IconDecoderCore
} }
protected override void SetFrameMetadata( protected override void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? colorTable) ReadOnlyMemory<Color>? colorTable)
{ {
IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata();
icoFrameMetadata.FromIconDirEntry(entry); icoFrameMetadata.FromIconDirEntry(entry);
icoFrameMetadata.Compression = compression; icoFrameMetadata.Compression = compression;
icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
icoFrameMetadata.ColorTable = colorTable; icoFrameMetadata.ColorTable = colorTable;
if (index == 0)
{
IcoMetadata curMetadata = imageMetadata.GetIcoMetadata();
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
}
} }
} }

133
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image frame. /// Provides Ico specific metadata information for the image frame.
/// </summary> /// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class. /// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
@ -53,7 +53,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary> /// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary> /// <summary>
/// Gets or sets the color table, if any. /// Gets or sets the color table, if any.
@ -62,11 +62,75 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this); public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
if (!metadata.PixelTypeInfo.HasValue)
{
return new IcoFrameMetadata
{
BmpBitsPerPixel = BmpBitsPerPixel.Bit32,
Compression = IconFrameCompression.Png
};
}
byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};
byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
IconFrameCompression compression = IconFrameCompression.Bmp;
if (bbpp is BmpBitsPerPixel.Bit32)
{
compression = IconFrameCompression.Png;
}
return new IcoFrameMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry) internal void FromIconDirEntry(IconDirEntry entry)
{ {
this.EncodingWidth = entry.Width; this.EncodingWidth = entry.Width;
@ -75,7 +139,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
internal IconDirEntry ToIconDirEntry() internal IconDirEntry ToIconDirEntry()
{ {
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0 ? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -92,4 +156,65 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
}, },
}; };
} }
private PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
PixelComponentInfo info;
PixelColorType color;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bpp = 32;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
info = PixelComponentInfo.Create(1, bpp, 1);
color = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
info = PixelComponentInfo.Create(1, bpp, 2);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
info = PixelComponentInfo.Create(1, bpp, 4);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Indexed;
break;
// Could be 555 with padding but 565 is more common in newer bitmaps and offers
// greater accuracy due to extra green precision.
case BmpBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(3, bpp, 5, 6, 5);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
} }

159
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -1,16 +1,171 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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; namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image. /// Provides Ico specific metadata information for the image.
/// </summary> /// </summary>
public class IcoMetadata : IDeepCloneable<IcoMetadata>, IDeepCloneable public class IcoMetadata : IFormatMetadata<IcoMetadata>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="IcoMetadata"/> class.
/// </summary>
public IcoMetadata()
{
}
private IcoMetadata(IcoMetadata 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. Derived from the root frame.
/// </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. Derived from the root frame.
/// </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. Derived from the root frame.
/// </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.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
public static IcoMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
IconFrameCompression compression = IconFrameCompression.Bmp;
if (bbpp is BmpBitsPerPixel.Bit32)
{
compression = IconFrameCompression.Png;
}
return new IcoMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
PixelComponentInfo info;
PixelColorType color;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bpp = 32;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
info = PixelComponentInfo.Create(1, bpp, 1);
color = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
info = PixelComponentInfo.Create(1, bpp, 2);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
info = PixelComponentInfo.Create(1, bpp, 4);
color = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Indexed;
break;
// Could be 555 with padding but 565 is more common in newer bitmaps and offers
// greater accuracy due to extra green precision.
case BmpBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(3, bpp, 5, 6, 5);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
/// <inheritdoc/> /// <inheritdoc/>
public IcoMetadata DeepClone() => new(); public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoMetadata DeepClone() => new(this);
} }

45
src/ImageSharp/Formats/Ico/MetadataExtensions.cs

@ -1,45 +0,0 @@
// 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);
}

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

@ -6,24 +6,21 @@ using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Png; 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;
namespace SixLabors.ImageSharp.Formats.Icon; namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconDecoderCore : IImageDecoderInternals internal abstract class IconDecoderCore : ImageDecoderCore
{ {
private IconDir fileHeader; private IconDir fileHeader;
private IconDirEntry[]? entries; private IconDirEntry[]? entries;
protected IconDecoderCore(DecoderOptions options) protected IconDecoderCore(DecoderOptions options)
=> this.Options = options; : base(options)
{
public DecoderOptions Options { get; } }
public Size Dimensions { get; private set; }
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) /// <inheritdoc />
where TPixel : unmanaged, IPixel<TPixel> protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
// Stream may not at 0. // Stream may not at 0.
long basePosition = stream.Position; long basePosition = stream.Position;
@ -61,7 +58,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); 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. // 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); Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(this.Options.Configuration, stream, cancellationToken);
decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i)); 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 // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
@ -74,7 +71,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
PngMetadata? pngMetadata = null; PngMetadata? pngMetadata = null;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{ {
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null; ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions); ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe; ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe;
@ -106,7 +103,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
} }
this.SetFrameMetadata( this.SetFrameMetadata(
metadata,
target.Metadata, target.Metadata,
x.Index,
this.entries[x.Index], this.entries[x.Index],
x.Compression, x.Compression,
bitsPerPixel, bitsPerPixel,
@ -131,7 +130,8 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
return result; return result;
} }
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) /// <inheritdoc />
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
// Stream may not at 0. // Stream may not at 0.
long basePosition = stream.Position; long basePosition = stream.Position;
@ -146,7 +146,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
int bpp = 0; int bpp = 0;
for (int i = 0; i < frames.Length; i++) for (int i = 0; i < frames.Length; i++)
{ {
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null; ReadOnlyMemory<Color>? colorTable = null;
ref IconDirEntry entry = ref this.entries[i]; ref IconDirEntry entry = ref this.entries[i];
@ -168,7 +168,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); 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. // 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); ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken);
ImageFrameMetadata frameMetadata = new(); ImageFrameMetadata frameMetadata = new();
@ -176,14 +176,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
{ {
if (i == 0) if (i == 0)
{ {
pngMetadata = temp.Metadata.GetPngMetadata(); pngMetadata = frameInfo.Metadata.GetPngMetadata();
} }
frameMetadata.SetFormatMetadata(PngFormat.Instance, temp.FrameMetadataCollection[0].GetPngMetadata()); frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata());
} }
else else
{ {
BmpMetadata meta = temp.Metadata.GetBmpMetadata(); BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata();
bitsPerPixel = meta.BitsPerPixel; bitsPerPixel = meta.BitsPerPixel;
colorTable = meta.ColorTable; colorTable = meta.ColorTable;
@ -198,7 +198,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
frames[i] = frameMetadata; frames[i] = frameMetadata;
this.SetFrameMetadata( this.SetFrameMetadata(
metadata,
frames[i], frames[i],
i,
this.entries[i], this.entries[i],
isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp,
bitsPerPixel, bitsPerPixel,
@ -206,7 +208,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data // 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. // 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)); this.Dimensions = new(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height));
} }
// Copy the format specific metadata to the image. // Copy the format specific metadata to the image.
@ -220,11 +222,13 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
} }
return new(new(bpp), this.Dimensions, metadata, frames); return new(this.Dimensions, metadata, frames);
} }
protected abstract void SetFrameMetadata( protected abstract void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,
@ -275,7 +279,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
this.Dimensions = new(width, height); this.Dimensions = new(width, height);
} }
private IImageDecoderInternals GetDecoder(bool isPng) private ImageDecoderCore GetDecoder(bool isPng)
{ {
if (isPng) if (isPng)
{ {

6
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -11,7 +11,7 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Icon; namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconEncoderCore : IImageEncoderInternals internal abstract class IconEncoderCore
{ {
private readonly QuantizingImageEncoder encoder; private readonly QuantizingImageEncoder encoder;
private readonly IconFileType iconFileType; private readonly IconFileType iconFileType;
@ -43,6 +43,8 @@ internal abstract class IconEncoderCore : IImageEncoderInternals
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
cancellationToken.ThrowIfCancellationRequested();
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data // 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. // 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]; ImageFrame<TPixel> frame = image.Frames[i];
@ -167,7 +169,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals
{ {
this.Compression = compression; this.Compression = compression;
this.BmpBitsPerPixel = compression == IconFrameCompression.Png this.BmpBitsPerPixel = compression == IconFrameCompression.Png
? BmpBitsPerPixel.Pixel32 ? BmpBitsPerPixel.Bit32
: bmpBitsPerPixel; : bmpBitsPerPixel;
this.ColorTable = colorTable; this.ColorTable = colorTable;
this.iconDirEntry = iconDirEntry; this.iconDirEntry = iconDirEntry;

26
src/ImageSharp/Formats/ImageDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@ -189,7 +190,7 @@ public abstract class ImageDecoder : IImageDecoder
throw new NotSupportedException("Cannot read from the stream."); throw new NotSupportedException("Cannot read from the stream.");
} }
T PeformActionAndResetPosition(Stream s, long position) T PerformActionAndResetPosition(Stream s, long position)
{ {
T result = action(s); T result = action(s);
@ -206,7 +207,7 @@ public abstract class ImageDecoder : IImageDecoder
if (stream.CanSeek) if (stream.CanSeek)
{ {
return PeformActionAndResetPosition(stream, stream.Position); return PerformActionAndResetPosition(stream, stream.Position);
} }
Configuration configuration = options.Configuration; Configuration configuration = options.Configuration;
@ -231,7 +232,7 @@ public abstract class ImageDecoder : IImageDecoder
throw new NotSupportedException("Cannot read from the stream."); throw new NotSupportedException("Cannot read from the stream.");
} }
Task<T> PeformActionAndResetPosition(Stream s, long position, CancellationToken ct) Task<T> PerformActionAndResetPosition(Stream s, long position, CancellationToken ct)
{ {
try try
{ {
@ -263,15 +264,15 @@ public abstract class ImageDecoder : IImageDecoder
// code below to copy the stream to an in-memory buffer before invoking the action. // code below to copy the stream to an in-memory buffer before invoking the action.
if (stream is MemoryStream ms) if (stream is MemoryStream ms)
{ {
return PeformActionAndResetPosition(ms, ms.Position, cancellationToken); return PerformActionAndResetPosition(ms, ms.Position, cancellationToken);
} }
if (stream is ChunkedMemoryStream cms) if (stream is ChunkedMemoryStream cms)
{ {
return PeformActionAndResetPosition(cms, cms.Position, cancellationToken); return PerformActionAndResetPosition(cms, cms.Position, cancellationToken);
} }
return CopyToMemoryStreamAndActionAsync(options, stream, PeformActionAndResetPosition, cancellationToken); return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken);
} }
private static async Task<T> CopyToMemoryStreamAndActionAsync<T>( private static async Task<T> CopyToMemoryStreamAndActionAsync<T>(
@ -282,7 +283,7 @@ public abstract class ImageDecoder : IImageDecoder
{ {
long position = stream.CanSeek ? stream.Position : 0; long position = stream.CanSeek ? stream.Position : 0;
Configuration configuration = options.Configuration; Configuration configuration = options.Configuration;
using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator);
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0; memoryStream.Position = 0;
return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); return await action(memoryStream, position, cancellationToken).ConfigureAwait(false);
@ -293,6 +294,11 @@ public abstract class ImageDecoder : IImageDecoder
if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format))
{ {
image.Metadata.DecodedImageFormat = format; image.Metadata.DecodedImageFormat = format;
foreach (ImageFrame frame in image.Frames)
{
frame.Metadata.DecodedImageFormat = format;
}
} }
} }
@ -301,6 +307,12 @@ public abstract class ImageDecoder : IImageDecoder
if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format)) if (configuration.ImageFormatsManager.TryFindFormatByDecoder(this, out IImageFormat? format))
{ {
info.Metadata.DecodedImageFormat = format; info.Metadata.DecodedImageFormat = format;
info.PixelType = info.Metadata.GetDecodedPixelTypeInfo();
foreach (ImageFrameMetadata frame in info.FrameMetadataCollection)
{
frame.DecodedImageFormat = format;
}
} }
} }
} }

127
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -0,0 +1,127 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// The base class for all stateful image decoders.
/// </summary>
internal abstract class ImageDecoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageDecoderCore"/> class.
/// </summary>
/// <param name="options">The general decoder options.</param>
protected ImageDecoderCore(DecoderOptions options)
=> this.Options = options;
/// <summary>
/// Gets the general decoder options.
/// </summary>
public DecoderOptions Options { get; }
/// <summary>
/// Gets or sets the dimensions of the image being decoded.
/// </summary>
public Size Dimensions { get; protected internal set; }
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>
/// <param name="configuration">The shared configuration.</param>
/// <param name="stream">The <see cref="Stream" /> containing image data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The <see cref="ImageInfo" />.</returns>
/// <exception cref="InvalidImageContentException">Thrown if the encoded image contains errors.</exception>
public ImageInfo Identify(
Configuration configuration,
Stream stream,
CancellationToken cancellationToken)
{
using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken);
try
{
return this.Identify(bufferedReadStream, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{
throw new InvalidImageContentException(this.Dimensions, ex);
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Decodes the image from the specified stream to an <see cref="Image{TPixel}" /> of a specific pixel type.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The shared configuration.</param>
/// <param name="stream">The <see cref="Stream" /> containing image data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The <see cref="Image{TPixel}" />.</returns>
/// <exception cref="InvalidImageContentException">Thrown if the encoded image contains errors.</exception>
public Image<TPixel> Decode<TPixel>(
Configuration configuration,
Stream stream,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
BufferedReadStream bufferedReadStream =
stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken);
try
{
return this.Decode<TPixel>(bufferedReadStream, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{
throw new InvalidImageContentException(this.Dimensions, ex);
}
catch (Exception)
{
throw;
}
finally
{
if (bufferedReadStream != stream)
{
bufferedReadStream.Dispose();
}
}
}
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The <see cref="ImageInfo"/>.</returns>
/// <remarks>
/// Cancellable synchronous method. In case of cancellation,
/// an <see cref="OperationCanceledException"/> shall be thrown which will be handled on the call site.
/// </remarks>
protected abstract ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken);
/// <summary>
/// Decodes the image from the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
/// <returns>The decoded image.</returns>
/// <remarks>
/// Cancellable synchronous method. In case of cancellation, an <see cref="OperationCanceledException"/> shall
/// be thrown which will be handled on the call site.
/// </remarks>
protected abstract Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
}

81
src/ImageSharp/Formats/ImageDecoderUtilities.cs

@ -1,81 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Utility methods for <see cref="IImageDecoderInternals"/>.
/// </summary>
internal static class ImageDecoderUtilities
{
internal static ImageInfo Identify(
this IImageDecoderInternals decoder,
Configuration configuration,
Stream stream,
CancellationToken cancellationToken)
{
using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken);
try
{
return decoder.Identify(bufferedReadStream, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{
throw new InvalidImageContentException(decoder.Dimensions, ex);
}
catch (Exception)
{
throw;
}
}
internal static Image<TPixel> Decode<TPixel>(
this IImageDecoderInternals decoder,
Configuration configuration,
Stream stream,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> decoder.Decode<TPixel>(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken);
internal static Image<TPixel> Decode<TPixel>(
this IImageDecoderInternals decoder,
Configuration configuration,
Stream stream,
Func<InvalidMemoryOperationException, Size, InvalidImageContentException> largeImageExceptionFactory,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken);
try
{
return decoder.Decode<TPixel>(bufferedReadStream, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{
throw largeImageExceptionFactory(ex, decoder.Dimensions);
}
catch (Exception)
{
throw;
}
finally
{
if (bufferedReadStream != stream)
{
bufferedReadStream.Dispose();
}
}
}
private static InvalidImageContentException DefaultLargeImageExceptionFactory(
InvalidMemoryOperationException memoryOperationException,
Size dimensions) =>
new(dimensions, memoryOperationException);
}

7
src/ImageSharp/Formats/ImageEncoder.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -42,6 +41,8 @@ public abstract class ImageEncoder : IImageEncoder
private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
image.SynchronizeMetadata();
Configuration configuration = image.Configuration; Configuration configuration = image.Configuration;
if (stream.CanSeek) if (stream.CanSeek)
{ {
@ -59,6 +60,8 @@ public abstract class ImageEncoder : IImageEncoder
private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
image.SynchronizeMetadata();
Configuration configuration = image.Configuration; Configuration configuration = image.Configuration;
if (stream.CanSeek) if (stream.CanSeek)
{ {
@ -66,7 +69,7 @@ public abstract class ImageEncoder : IImageEncoder
} }
else else
{ {
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await DoEncodeAsync(ms); await DoEncodeAsync(ms);
ms.Position = 0; ms.Position = 0;
await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken)

4
src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs

@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
internal class JpegFrameConfig internal class JpegFrameConfig
{ {
public JpegFrameConfig(JpegColorSpace colorType, JpegEncodingColor encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables) public JpegFrameConfig(JpegColorSpace colorType, JpegColorType encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables)
{ {
this.ColorType = colorType; this.ColorType = colorType;
this.EncodingColor = encodingColor; this.EncodingColor = encodingColor;
@ -25,7 +25,7 @@ internal class JpegFrameConfig
public JpegColorSpace ColorType { get; } public JpegColorSpace ColorType { get; }
public JpegEncodingColor EncodingColor { get; } public JpegColorType EncodingColor { get; }
public JpegComponentConfig[] Components { get; } public JpegComponentConfig[] Components { get; }

6
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -139,13 +139,13 @@ internal class HuffmanScanEncoder
/// <param name="frame">Frame to encode.</param> /// <param name="frame">Frame to encode.</param>
/// <param name="converter">Converter from color to spectral.</param> /// <param name="converter">Converter from color to spectral.</param>
/// <param name="cancellationToken">The token to request cancellation.</param> /// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeScanBaselineInterleaved<TPixel>(JpegEncodingColor color, JpegFrame frame, SpectralConverter<TPixel> converter, CancellationToken cancellationToken) public void EncodeScanBaselineInterleaved<TPixel>(JpegColorType color, JpegFrame frame, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (color) switch (color)
{ {
case JpegEncodingColor.YCbCrRatio444: case JpegColorType.YCbCrRatio444:
case JpegEncodingColor.Rgb: case JpegColorType.Rgb:
this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken); this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken);
break; break;
default: default:

2
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
/// <summary> /// <summary>
/// The Huffman encoding specifications. /// The Huffman encoding specifications.
/// </summary> /// </summary>
public readonly struct HuffmanSpec internal readonly struct HuffmanSpec
{ {
/// <summary> /// <summary>
/// Huffman talbe specification for luminance DC. /// Huffman talbe specification for luminance DC.

2
src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs → src/ImageSharp/Formats/Jpeg/JpegColorType.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary> /// <summary>
/// Provides enumeration of available JPEG color types. /// Provides enumeration of available JPEG color types.
/// </summary> /// </summary>
public enum JpegEncodingColor : byte public enum JpegColorType : byte
{ {
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.

42
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg;
/// Originally ported from <see href="https://github.com/mozilla/pdf.js/blob/master/src/core/jpg.js"/> /// Originally ported from <see href="https://github.com/mozilla/pdf.js/blob/master/src/core/jpg.js"/>
/// with additional fixes for both performance and common encoding errors. /// with additional fixes for both performance and common encoding errors.
/// </summary> /// </summary>
internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
/// <summary> /// <summary>
/// Whether the image has an EXIF marker. /// Whether the image has an EXIF marker.
@ -117,8 +117,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public JpegDecoderCore(JpegDecoderOptions options) public JpegDecoderCore(JpegDecoderOptions options)
: base(options.GeneralOptions)
{ {
this.Options = options.GeneralOptions;
this.resizeMode = options.ResizeMode; this.resizeMode = options.ResizeMode;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
@ -130,12 +130,6 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
// Refers to assembly's static data segment, no allocation occurs. // Refers to assembly's static data segment, no allocation occurs.
private static ReadOnlySpan<byte> SupportedPrecisions => new byte[] { 8, 12 }; private static ReadOnlySpan<byte> SupportedPrecisions => new byte[] { 8, 12 };
/// <inheritdoc />
public DecoderOptions Options { get; }
/// <inheritdoc/>
public Size Dimensions => this.Frame.PixelSize;
/// <summary> /// <summary>
/// Gets the frame /// Gets the frame
/// </summary> /// </summary>
@ -198,8 +192,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize);
this.ParseStream(stream, spectralConverter, cancellationToken); this.ParseStream(stream, spectralConverter, cancellationToken);
@ -216,7 +209,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ParseStream(stream, spectralConverter: null, cancellationToken); this.ParseStream(stream, spectralConverter: null, cancellationToken);
this.InitExifProfile(); this.InitExifProfile();
@ -226,7 +219,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
this.InitDerivedMetadataProperties(); this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize; Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), new(pixelSize.Width, pixelSize.Height), this.Metadata); return new ImageInfo(new(pixelSize.Width, pixelSize.Height), this.Metadata);
} }
/// <summary> /// <summary>
@ -603,58 +596,58 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// Returns the jpeg color type based on the colorspace and subsampling used. /// Returns the jpeg color type based on the colorspace and subsampling used.
/// </summary> /// </summary>
/// <returns>Jpeg color type.</returns> /// <returns>Jpeg color type.</returns>
private JpegEncodingColor DeduceJpegColorType() private JpegColorType DeduceJpegColorType()
{ {
switch (this.ColorSpace) switch (this.ColorSpace)
{ {
case JpegColorSpace.Grayscale: case JpegColorSpace.Grayscale:
return JpegEncodingColor.Luminance; return JpegColorType.Luminance;
case JpegColorSpace.RGB: case JpegColorSpace.RGB:
return JpegEncodingColor.Rgb; return JpegColorType.Rgb;
case JpegColorSpace.YCbCr: case JpegColorSpace.YCbCr:
if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegEncodingColor.YCbCrRatio444; return JpegColorType.YCbCrRatio444;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 1 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegEncodingColor.YCbCrRatio422; return JpegColorType.YCbCrRatio422;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegEncodingColor.YCbCrRatio420; return JpegColorType.YCbCrRatio420;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegEncodingColor.YCbCrRatio411; return JpegColorType.YCbCrRatio411;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegEncodingColor.YCbCrRatio410; return JpegColorType.YCbCrRatio410;
} }
else else
{ {
return JpegEncodingColor.YCbCrRatio420; return JpegColorType.YCbCrRatio420;
} }
case JpegColorSpace.Cmyk: case JpegColorSpace.Cmyk:
return JpegEncodingColor.Cmyk; return JpegColorType.Cmyk;
case JpegColorSpace.Ycck: case JpegColorSpace.Ycck:
return JpegEncodingColor.Ycck; return JpegColorType.Ycck;
default: default:
return JpegEncodingColor.YCbCrRatio420; return JpegColorType.YCbCrRatio420;
} }
} }
@ -1237,6 +1230,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount);
this.Dimensions = new(frameWidth, frameHeight);
this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive;
remaining -= length; remaining -= length;

2
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -45,7 +45,7 @@ public sealed class JpegEncoder : ImageEncoder
/// <summary> /// <summary>
/// Gets the jpeg color for encoding. /// Gets the jpeg color for encoding.
/// </summary> /// </summary>
public JpegEncodingColor? ColorType { get; init; } public JpegColorType? ColorType { get; init; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)

18
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs

@ -40,7 +40,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YCbCr 4:4:4 // YCbCr 4:4:4
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio444, JpegColorType.YCbCrRatio444,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -53,7 +53,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YCbCr 4:2:2 // YCbCr 4:2:2
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio422, JpegColorType.YCbCrRatio422,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -66,7 +66,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YCbCr 4:2:0 // YCbCr 4:2:0
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio420, JpegColorType.YCbCrRatio420,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -79,7 +79,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YCbCr 4:1:1 // YCbCr 4:1:1
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio411, JpegColorType.YCbCrRatio411,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -92,7 +92,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YCbCr 4:1:0 // YCbCr 4:1:0
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio410, JpegColorType.YCbCrRatio410,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -105,7 +105,7 @@ internal sealed unsafe partial class JpegEncoderCore
// Luminance // Luminance
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.Grayscale, JpegColorSpace.Grayscale,
JpegEncodingColor.Luminance, JpegColorType.Luminance,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -123,7 +123,7 @@ internal sealed unsafe partial class JpegEncoderCore
// Rgb // Rgb
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.RGB, JpegColorSpace.RGB,
JpegEncodingColor.Rgb, JpegColorType.Rgb,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -146,7 +146,7 @@ internal sealed unsafe partial class JpegEncoderCore
// Cmyk // Cmyk
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.Cmyk, JpegColorSpace.Cmyk,
JpegEncodingColor.Cmyk, JpegColorType.Cmyk,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
@ -170,7 +170,7 @@ internal sealed unsafe partial class JpegEncoderCore
// YccK // YccK
new JpegFrameConfig( new JpegFrameConfig(
JpegColorSpace.Ycck, JpegColorSpace.Ycck,
JpegEncodingColor.Ycck, JpegColorType.Ycck,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),

12
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a jpeg. /// Image encoder for writing an image to a stream as a jpeg.
/// </summary> /// </summary>
internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals internal sealed unsafe partial class JpegEncoderCore
{ {
/// <summary> /// <summary>
/// The available encodable frame configs. /// The available encodable frame configs.
@ -72,7 +72,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata); JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata);
bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved ?? true; bool interleaved = this.encoder.Interleaved ?? jpegMetadata.Interleaved;
using JpegFrame frame = new(image, frameConfig, interleaved); using JpegFrame frame = new(image, frameConfig, interleaved);
// Write the Start Of Image marker. // Write the Start Of Image marker.
@ -539,17 +539,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <param name="buffer">Temporary buffer.</param> /// <param name="buffer">Temporary buffer.</param>
private void WriteProfiles(ImageMetadata metadata, Span<byte> buffer) private void WriteProfiles(ImageMetadata metadata, Span<byte> buffer)
{ {
if (metadata is null)
{
return;
}
// For compatibility, place the profiles in the following order: // For compatibility, place the profiles in the following order:
// - APP1 EXIF // - APP1 EXIF
// - APP1 XMP // - APP1 XMP
// - APP2 ICC // - APP2 ICC
// - APP13 IPTC // - APP13 IPTC
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile, buffer); this.WriteExifProfile(metadata.ExifProfile, buffer);
this.WriteXmpProfile(metadata.XmpProfile, buffer); this.WriteXmpProfile(metadata.XmpProfile, buffer);
this.WriteIccProfile(metadata.IccProfile, buffer); this.WriteIccProfile(metadata.IccProfile, buffer);
@ -780,7 +774,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
private JpegFrameConfig GetFrameConfig(JpegMetadata metadata) private JpegFrameConfig GetFrameConfig(JpegMetadata metadata)
{ {
JpegEncodingColor color = this.encoder.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420; JpegColorType color = this.encoder.ColorType ?? metadata.ColorType;
JpegFrameConfig frameConfig = Array.Find( JpegFrameConfig frameConfig = Array.Find(
FrameConfigs, FrameConfigs,
cfg => cfg.EncodingColor == color); cfg => cfg.EncodingColor == color);

138
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -2,20 +2,20 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg; namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary> /// <summary>
/// Provides Jpeg specific metadata information for the image. /// Provides Jpeg specific metadata information for the image.
/// </summary> /// </summary>
public class JpegMetadata : IDeepCloneable public class JpegMetadata : IFormatMetadata<JpegMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JpegMetadata"/> class. /// Initializes a new instance of the <see cref="JpegMetadata"/> class.
/// </summary> /// </summary>
public JpegMetadata() public JpegMetadata()
{ {
this.Comments = new List<JpegComData>();
} }
/// <summary> /// <summary>
@ -36,7 +36,7 @@ public class JpegMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This value might not be accurate if it was calculated during jpeg decoding /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-compliant ITU quantization tables.
/// </remarks> /// </remarks>
internal int? LuminanceQuality { get; set; } internal int? LuminanceQuality { get; set; }
@ -45,16 +45,17 @@ public class JpegMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This value might not be accurate if it was calculated during jpeg decoding /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-compliant ITU quantization tables.
/// </remarks> /// </remarks>
internal int? ChrominanceQuality { get; set; } internal int? ChrominanceQuality { get; set; }
/// <summary> /// <summary>
/// Gets the encoded quality. /// Gets or sets the encoded quality.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Note that jpeg image can have different quality for luminance and chrominance components. /// Note that jpeg image can have different quality for luminance and chrominance components.
/// This property returns maximum value of luma/chroma qualities if both are present. /// This property returns maximum value of luma/chroma qualities if both are present.
/// Setting the quality will update both values.
/// </remarks> /// </remarks>
public int Quality public int Quality
{ {
@ -69,45 +70,138 @@ public class JpegMetadata : IDeepCloneable
return this.LuminanceQuality.Value; return this.LuminanceQuality.Value;
} }
else
{
if (this.ChrominanceQuality.HasValue)
{
return this.ChrominanceQuality.Value;
}
return Quantization.DefaultQualityFactor; return this.ChrominanceQuality ?? Quantization.DefaultQualityFactor;
} }
set
{
this.LuminanceQuality = value;
this.ChrominanceQuality = value;
} }
} }
/// <summary> /// <summary>
/// Gets the color type. /// Gets or sets the color type.
/// </summary> /// </summary>
public JpegEncodingColor? ColorType { get; internal set; } public JpegColorType ColorType { get; set; } = JpegColorType.YCbCrRatio420;
/// <summary> /// <summary>
/// Gets the component encoding mode. /// Gets or sets a value indicating whether the component encoding mode should be interleaved.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan. /// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan. /// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks> /// </remarks>
public bool? Interleaved { get; internal set; } public bool Interleaved { get; set; } = true;
/// <summary> /// <summary>
/// Gets the scan encoding mode. /// Gets or sets a value indicating whether the scan encoding mode is progressive.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Progressive jpeg images encode component data across multiple scans. /// Progressive jpeg images encode component data across multiple scans.
/// </remarks> /// </remarks>
public bool? Progressive { get; internal set; } public bool Progressive { get; set; }
/// <summary> /// <summary>
/// Gets the comments. /// Gets or sets collection of comments.
/// </summary> /// </summary>
public IList<JpegComData> Comments { get; } public IList<JpegComData> Comments { get; set; } = [];
/// <inheritdoc/>
public static JpegMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
JpegColorType color;
PixelColorType colorType = metadata.PixelTypeInfo.ColorType;
switch (colorType)
{
case PixelColorType.Luminance:
color = JpegColorType.Luminance;
break;
case PixelColorType.CMYK:
color = JpegColorType.Cmyk;
break;
case PixelColorType.YCCK:
color = JpegColorType.Ycck;
break;
default:
if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR))
{
color = JpegColorType.Rgb;
}
else
{
color = metadata.Quality <= Quantization.DefaultQualityFactor
? JpegColorType.YCbCrRatio420
: JpegColorType.YCbCrRatio444;
}
break;
}
return new JpegMetadata
{
ColorType = color,
ChrominanceQuality = metadata.Quality,
LuminanceQuality = metadata.Quality,
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp;
PixelColorType colorType;
PixelComponentInfo info;
switch (this.ColorType)
{
case JpegColorType.Luminance:
bpp = 8;
colorType = PixelColorType.Luminance;
info = PixelComponentInfo.Create(1, bpp, 8);
break;
case JpegColorType.Cmyk:
bpp = 32;
colorType = PixelColorType.CMYK;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
break;
case JpegColorType.Ycck:
bpp = 32;
colorType = PixelColorType.YCCK;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
break;
case JpegColorType.Rgb:
bpp = 24;
colorType = PixelColorType.RGB;
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
break;
default:
bpp = 24;
colorType = PixelColorType.YCbCr;
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = PixelAlphaRepresentation.None,
ColorType = colorType,
ComponentInfo = info,
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = EncodingType.Lossy,
PixelTypeInfo = this.GetPixelTypeInfo(),
Quality = this.Quality,
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetadata(this); public JpegMetadata DeepClone() => new(this);
} }

21
src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Text;
using SixLabors.ImageSharp.Formats.Jpeg;
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 jpeg format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="JpegMetadata"/>.</returns>
public static JpegMetadata GetJpegMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(JpegFormat.Instance);
}

25
src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary>
/// Configuration options for use during PBM encoding.
/// </summary>
internal interface IPbmEncoderOptions
{
/// <summary>
/// Gets the encoding of the pixels.
/// </summary>
PbmEncoding? Encoding { get; }
/// <summary>
/// Gets the Color type of the resulting image.
/// </summary>
PbmColorType? ColorType { get; }
/// <summary>
/// Gets the Data Type of the pixel components.
/// </summary>
PbmComponentType? ComponentType { get; }
}

20
src/ImageSharp/Formats/Pbm/MetadataExtensions.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Pbm;
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 pbm format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="PbmMetadata"/>.</returns>
public static PbmMetadata GetPbmMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PbmFormat.Instance);
}

26
src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary> /// <summary>
/// Performs the PBM decoding operation. /// Performs the PBM decoding operation.
/// </summary> /// </summary>
internal sealed class PbmDecoderCore : IImageDecoderInternals internal sealed class PbmDecoderCore : ImageDecoderCore
{ {
private int maxPixelValue; private int maxPixelValue;
@ -52,24 +52,17 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public PbmDecoderCore(DecoderOptions options) public PbmDecoderCore(DecoderOptions options)
: base(options)
{ {
this.Options = options;
this.configuration = options.Configuration; this.configuration = options.Configuration;
} }
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc/>
public Size Dimensions => this.pixelSize;
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
this.ProcessHeader(stream); this.ProcessHeader(stream);
var image = new Image<TPixel>(this.configuration, this.pixelSize.Width, this.pixelSize.Height, this.metadata); Image<TPixel> image = new(this.configuration, this.pixelSize.Width, this.pixelSize.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
@ -83,13 +76,12 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ProcessHeader(stream); this.ProcessHeader(stream);
return new ImageInfo(
// BlackAndWhite pixels are encoded into a byte. new(this.pixelSize.Width, this.pixelSize.Height),
int bitsPerPixel = this.componentType == PbmComponentType.Short ? 16 : 8; this.metadata);
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new(this.pixelSize.Width, this.pixelSize.Height), this.metadata);
} }
/// <summary> /// <summary>
@ -97,6 +89,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception> /// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception>
[MemberNotNull(nameof(metadata))]
private void ProcessHeader(BufferedReadStream stream) private void ProcessHeader(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[2]; Span<byte> buffer = stackalloc byte[2];
@ -179,6 +172,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
} }
this.pixelSize = new Size(width, height); this.pixelSize = new Size(width, height);
this.Dimensions = this.pixelSize;
this.metadata = new ImageMetadata(); this.metadata = new ImageMetadata();
PbmMetadata meta = this.metadata.GetPbmMetadata(); PbmMetadata meta = this.metadata.GetPbmMetadata();
meta.Encoding = this.encoding; meta.Encoding = this.encoding;

2
src/ImageSharp/Formats/Pbm/PbmEncoder.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
namespace SixLabors.ImageSharp.Formats.Pbm; namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary> /// <summary>

3
src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Text; using System.Buffers.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm; namespace SixLabors.ImageSharp.Formats.Pbm;
@ -10,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a PGM, PBM, PPM or PAM bitmap. /// Image encoder for writing an image to a stream as a PGM, PBM, PPM or PAM bitmap.
/// </summary> /// </summary>
internal sealed class PbmEncoderCore : IImageEncoderInternals internal sealed class PbmEncoderCore
{ {
private const byte NewLine = (byte)'\n'; private const byte NewLine = (byte)'\n';
private const byte Space = (byte)' '; private const byte Space = (byte)' ';

96
src/ImageSharp/Formats/Pbm/PbmMetadata.cs

@ -1,12 +1,14 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm; namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary> /// <summary>
/// Provides PBM specific metadata information for the image. /// Provides PBM specific metadata information for the image.
/// </summary> /// </summary>
public class PbmMetadata : IDeepCloneable public class PbmMetadata : IFormatMetadata<PbmMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PbmMetadata"/> class. /// Initializes a new instance of the <see cref="PbmMetadata"/> class.
@ -41,5 +43,95 @@ public class PbmMetadata : IDeepCloneable
public PbmComponentType ComponentType { get; set; } public PbmComponentType ComponentType { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new PbmMetadata(this); public static PbmMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
PbmColorType color;
PixelColorType colorType = metadata.PixelTypeInfo.ColorType;
switch (colorType)
{
case PixelColorType.Binary:
color = PbmColorType.BlackAndWhite;
break;
case PixelColorType.Luminance:
color = PbmColorType.Grayscale;
break;
default:
if (colorType.HasFlag(PixelColorType.RGB) || colorType.HasFlag(PixelColorType.BGR))
{
color = PbmColorType.Rgb;
}
else
{
color = PbmColorType.Grayscale;
}
break;
}
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
PbmComponentType componentType = bpp switch
{
1 => PbmComponentType.Bit,
<= 8 => PbmComponentType.Byte,
_ => PbmComponentType.Short
};
return new PbmMetadata
{
ColorType = color,
ComponentType = componentType
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp;
PixelColorType colorType;
PixelComponentInfo info;
switch (this.ColorType)
{
case PbmColorType.BlackAndWhite:
bpp = 1;
colorType = PixelColorType.Binary;
info = PixelComponentInfo.Create(1, bpp, 1);
break;
case PbmColorType.Rgb:
bpp = this.ComponentType == PbmComponentType.Short ? 48 : 24;
colorType = PixelColorType.RGB;
info = this.ComponentType == PbmComponentType.Short
? PixelComponentInfo.Create(3, bpp, 16, 16, 16)
: PixelComponentInfo.Create(3, bpp, 8, 8, 8);
break;
case PbmColorType.Grayscale:
default:
bpp = this.ComponentType == PbmComponentType.Short ? 16 : 8;
colorType = PixelColorType.Luminance;
info = this.ComponentType == PbmComponentType.Short
? PixelComponentInfo.Create(1, bpp, bpp)
: PixelComponentInfo.Create(1, bpp, bpp);
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = PixelAlphaRepresentation.None,
ColorType = colorType,
ComponentInfo = info,
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public PbmMetadata DeepClone() => new(this);
} }

20
src/ImageSharp/Formats/Png/Chunks/FrameControl.cs

@ -22,8 +22,8 @@ internal readonly struct FrameControl
uint yOffset, uint yOffset,
ushort delayNumerator, ushort delayNumerator,
ushort delayDenominator, ushort delayDenominator,
PngDisposalMethod disposeOperation, FrameDisposalMode disposalMode,
PngBlendMethod blendOperation) FrameBlendMode blendMode)
{ {
this.SequenceNumber = sequenceNumber; this.SequenceNumber = sequenceNumber;
this.Width = width; this.Width = width;
@ -32,8 +32,8 @@ internal readonly struct FrameControl
this.YOffset = yOffset; this.YOffset = yOffset;
this.DelayNumerator = delayNumerator; this.DelayNumerator = delayNumerator;
this.DelayDenominator = delayDenominator; this.DelayDenominator = delayDenominator;
this.DisposeOperation = disposeOperation; this.DisposalMode = disposalMode;
this.BlendOperation = blendOperation; this.BlendMode = blendMode;
} }
/// <summary> /// <summary>
@ -84,12 +84,12 @@ internal readonly struct FrameControl
/// <summary> /// <summary>
/// Gets the type of frame area disposal to be done after rendering this frame /// Gets the type of frame area disposal to be done after rendering this frame
/// </summary> /// </summary>
public PngDisposalMethod DisposeOperation { get; } public FrameDisposalMode DisposalMode { get; }
/// <summary> /// <summary>
/// Gets the type of frame area rendering for this frame /// Gets the type of frame area rendering for this frame
/// </summary> /// </summary>
public PngBlendMethod BlendOperation { get; } public FrameBlendMode BlendMode { get; }
public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height); public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height);
@ -137,8 +137,8 @@ internal readonly struct FrameControl
BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator); BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator);
BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator); BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator);
buffer[24] = (byte)this.DisposeOperation; buffer[24] = (byte)(this.DisposalMode - 1);
buffer[25] = (byte)this.BlendOperation; buffer[25] = (byte)this.BlendMode;
} }
/// <summary> /// <summary>
@ -155,6 +155,6 @@ internal readonly struct FrameControl
yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]), yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]),
delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]), delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]),
delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]), delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
disposeOperation: (PngDisposalMethod)data[24], disposalMode: (FrameDisposalMode)(data[24] + 1),
blendOperation: (PngBlendMethod)data[25]); blendMode: (FrameBlendMode)data[25]);
} }

84
src/ImageSharp/Formats/Png/MetadataExtensions.cs

@ -1,84 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
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 png format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngMetadata"/>.</returns>
public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the png format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the png metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(true)] out PngMetadata? metadata)
=> source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
/// <summary>
/// Gets the png format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the png format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the png frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetPngMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
=> source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source)
=> new()
{
ColorTable = source.ColorTable,
ColorTableMode = FrameColorTableMode.Global,
RepeatCount = (ushort)Numerics.Clamp(source.RepeatCount, 0, ushort.MaxValue),
};
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source)
{
double delay = source.FrameDelay.ToDouble();
if (double.IsNaN(delay))
{
delay = 0;
}
return new()
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(delay * 1000),
DisposalMode = GetMode(source.DisposalMethod),
BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch
{
PngDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
_ => FrameDisposalMode.Unspecified,
};
}

22
src/ImageSharp/Formats/Png/PngBlendMethod.cs

@ -1,22 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies whether the frame is to be alpha blended into the current output buffer content,
/// or whether it should completely replace its region in the output buffer.
/// </summary>
public enum PngBlendMethod
{
/// <summary>
/// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
/// </summary>
Source,
/// <summary>
/// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as
/// described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
/// </summary>
Over
}

6
src/ImageSharp/Formats/Png/PngDecoder.cs

@ -53,8 +53,8 @@ public sealed class PngDecoder : SpecializedImageDecoder<PngDecoderOptions>
stream.Position = 0; stream.Position = 0;
PngMetadata meta = info.Metadata.GetPngMetadata(); PngMetadata meta = info.Metadata.GetPngMetadata();
PngColorType color = meta.ColorType.GetValueOrDefault(); PngColorType color = meta.ColorType;
PngBitDepth bits = meta.BitDepth.GetValueOrDefault(); PngBitDepth bits = meta.BitDepth;
switch (color) switch (color)
{ {
@ -101,5 +101,5 @@ public sealed class PngDecoder : SpecializedImageDecoder<PngDecoderOptions>
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new PngDecoderOptions() { GeneralOptions = options }; protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options };
} }

87
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Performs the png decoding operation. /// Performs the png decoding operation.
/// </summary> /// </summary>
internal sealed class PngDecoderCore : IImageDecoderInternals internal sealed class PngDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// The general decoder options. /// The general decoder options.
@ -136,8 +136,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public PngDecoderCore(PngDecoderOptions options) public PngDecoderCore(PngDecoderOptions options)
: base(options.GeneralOptions)
{ {
this.Options = options.GeneralOptions;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.maxFrames = options.GeneralOptions.MaxFrames; this.maxFrames = options.GeneralOptions.MaxFrames;
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
@ -147,8 +147,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly) internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly)
: base(options.GeneralOptions)
{ {
this.Options = options.GeneralOptions;
this.colorMetadataOnly = colorMetadataOnly; this.colorMetadataOnly = colorMetadataOnly;
this.maxFrames = options.GeneralOptions.MaxFrames; this.maxFrames = options.GeneralOptions.MaxFrames;
this.skipMetadata = true; this.skipMetadata = true;
@ -159,14 +159,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc/>
public Size Dimensions => new(this.header.Width, this.header.Height);
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
uint frameCount = 0; uint frameCount = 0;
ImageMetadata metadata = new(); ImageMetadata metadata = new();
@ -241,7 +234,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
cancellationToken); cancellationToken);
// if current frame dispose is restore to previous, then from future frame's perspective, it never happened // if current frame dispose is restore to previous, then from future frame's perspective, it never happened
if (currentFrameControl.Value.DisposeOperation != PngDisposalMethod.RestoreToPrevious) if (currentFrameControl.Value.DisposalMode != FrameDisposalMode.RestoreToPrevious)
{ {
previousFrame = currentFrame; previousFrame = currentFrame;
previousFrameControl = currentFrameControl; previousFrameControl = currentFrameControl;
@ -341,7 +334,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
uint frameCount = 0; uint frameCount = 0;
ImageMetadata metadata = new(); ImageMetadata metadata = new();
@ -527,10 +520,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngThrowHelper.ThrowInvalidHeader(); PngThrowHelper.ThrowInvalidHeader();
} }
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec. return new ImageInfo(new(this.header.Width, this.header.Height), metadata, framesMetadata);
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata, framesMetadata);
} }
finally finally
{ {
@ -674,12 +664,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
// So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null). // So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null).
if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious)) if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToPrevious))
{ {
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(); Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion();
pixelRegion.Clear(); pixelRegion.Clear();
} }
else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground) else if (previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToBackground)
{ {
Rectangle restoreArea = previousFrameControl.Value.Bounds; Rectangle restoreArea = previousFrameControl.Value.Bounds;
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea); Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
@ -703,29 +693,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
imageFrameMetadata.Add(meta); imageFrameMetadata.Add(meta);
} }
/// <summary>
/// Calculates the correct number of bits per pixel for the given color type.
/// </summary>
/// <returns>The <see cref="int"/></returns>
private int CalculateBitsPerPixel()
{
switch (this.pngColorType)
{
case PngColorType.Grayscale:
case PngColorType.Palette:
return this.header.BitDepth;
case PngColorType.GrayscaleWithAlpha:
return this.header.BitDepth * 2;
case PngColorType.Rgb:
return this.header.BitDepth * 3;
case PngColorType.RgbWithAlpha:
return this.header.BitDepth * 4;
default:
PngThrowHelper.ThrowNotSupportedColor();
return -1;
}
}
/// <summary> /// <summary>
/// Calculates the correct number of bytes per pixel for the given color type. /// Calculates the correct number of bytes per pixel for the given color type.
/// </summary> /// </summary>
@ -817,8 +784,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int height = (int)frameControl.YMax; int height = (int)frameControl.YMax;
IMemoryOwner<TPixel>? blendMemory = null; IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty; Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendOperation == PngBlendMethod.Over) if (frameControl.BlendMode == FrameBlendMode.Over)
{ {
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean); blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span; blendRowBuffer = blendMemory.Memory.Span;
@ -910,8 +877,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer;
IMemoryOwner<TPixel>? blendMemory = null; IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty; Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendOperation == PngBlendMethod.Over) if (frameControl.BlendMode == FrameBlendMode.Over)
{ {
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean); blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span; blendRowBuffer = blendMemory.Memory.Span;
@ -1036,7 +1003,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
Span<TPixel> destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); Span<TPixel> destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
bool blend = frameControl.BlendOperation == PngBlendMethod.Over; bool blend = frameControl.BlendMode == FrameBlendMode.Over;
Span<TPixel> rowSpan = blend Span<TPixel> rowSpan = blend
? blendRowBuffer ? blendRowBuffer
: destination; : destination;
@ -1149,7 +1116,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int increment = 1) int increment = 1)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
bool blend = frameControl.BlendOperation == PngBlendMethod.Over; bool blend = frameControl.BlendMode == FrameBlendMode.Over;
Span<TPixel> rowSpan = blend Span<TPixel> rowSpan = blend
? blendRowBuffer ? blendRowBuffer
: destination; : destination;
@ -1365,6 +1332,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
pngMetadata.InterlaceMethod = this.header.InterlaceMethod; pngMetadata.InterlaceMethod = this.header.InterlaceMethod;
this.pngColorType = this.header.ColorType; this.pngColorType = this.header.ColorType;
this.Dimensions = new(this.header.Width, this.header.Height);
} }
/// <summary> /// <summary>
@ -1483,7 +1451,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
byte colorPrimaries = data[0]; byte colorPrimaries = data[0];
byte transferFunction = data[1]; byte transferFunction = data[1];
byte matrixCoefficients = data[2]; byte matrixCoefficients = data[2];
bool? fullRange = data[3] == 1 ? true : data[3] == 0 ? false : null; bool? fullRange;
if (data[3] == 1)
{
fullRange = true;
}
else if (data[3] == 0)
{
fullRange = false;
}
else
{
fullRange = null;
}
metadata.CicpProfile = new CicpProfile(colorPrimaries, transferFunction, matrixCoefficients, fullRange); metadata.CicpProfile = new CicpProfile(colorPrimaries, transferFunction, matrixCoefficients, fullRange);
} }
@ -1515,7 +1496,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes). // Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes).
// This doesn't actually allocate. // This doesn't actually allocate.
ReadOnlySpan<byte> exifHeader = new byte[] { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 }; ReadOnlySpan<byte> exifHeader = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00];
if (dataLength < exifHeader.Length) if (dataLength < exifHeader.Length)
{ {
@ -1626,7 +1607,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
Span<byte> destUncompressedData = destBuffer.GetSpan(); Span<byte> destUncompressedData = destBuffer.GetSpan();
if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
{ {
uncompressedBytesArray = Array.Empty<byte>(); uncompressedBytesArray = [];
return false; return false;
} }
@ -1635,7 +1616,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
if (memoryStreamOutput.Length > maxLength) if (memoryStreamOutput.Length > maxLength)
{ {
uncompressedBytesArray = Array.Empty<byte>(); uncompressedBytesArray = [];
return false; return false;
} }
@ -2002,7 +1983,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
if (length == 0) if (length == 0)
{ {
return new BasicArrayBuffer<byte>(Array.Empty<byte>()); return new BasicArrayBuffer<byte>([]);
} }
// We rent the buffer here to return it afterwards in Decode() // We rent the buffer here to return it afterwards in Decode()

25
src/ImageSharp/Formats/Png/PngDisposalMethod.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
/// </summary>
public enum PngDisposalMethod
{
/// <summary>
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
/// </summary>
DoNotDispose,
/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
/// </summary>
RestoreToBackground,
/// <summary>
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
/// </summary>
RestoreToPrevious
}

149
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -9,10 +9,8 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -23,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Performs the png encoding operation. /// Performs the png encoding operation.
/// </summary> /// </summary>
internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable internal sealed class PngEncoderCore : IDisposable
{ {
/// <summary> /// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks. /// The maximum block size, defaults at 64k for uncompressed blocks.
@ -160,7 +158,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.height = image.Height; this.height = image.Height;
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = GetPngMetadata(image); PngMetadata pngMetadata = metadata.ClonePngMetadata();
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
stream.Write(PngConstants.HeaderBytes); stream.Write(PngConstants.HeaderBytes);
@ -211,8 +209,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
// Write the first animated frame. // Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex]; currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
uint sequenceNumber = 1; uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame) if (pngMetadata.AnimateRootFrame)
@ -237,12 +235,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{ {
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex]; currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = GetPngFrameMetadata(currentFrame); frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
(bool difference, Rectangle bounds) = (bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels( AnimationUtilities.DeDuplicatePixels(
@ -268,7 +266,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame; previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod; previousDisposal = frameMetadata.DisposalMode;
} }
} }
@ -288,54 +286,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.currentScanline?.Dispose(); this.currentScanline?.Dispose();
} }
private static PngMetadata GetPngMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
return (PngMetadata)png.DeepClone();
}
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
private static PngFrameMetadata GetPngFrameMetadata<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
return (PngFrameMetadata)png.DeepClone();
}
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
/// <summary> /// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary> /// </summary>
@ -826,7 +776,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; return;
} }
meta.SyncProfiles();
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
} }
@ -881,6 +830,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="metaData">The image meta data.</param> /// <param name="metaData">The image meta data.</param>
/// <exception cref="NotSupportedException">CICP matrix coefficients other than Identity are not supported in PNG.</exception>
private void WriteCicpChunk(Stream stream, ImageMetadata metaData) private void WriteCicpChunk(Stream stream, ImageMetadata metaData)
{ {
if (metaData.CicpProfile is null) if (metaData.CicpProfile is null)
@ -1125,8 +1075,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
yOffset: (uint)bounds.Top, yOffset: (uint)bounds.Top,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod, disposalMode: frameMetadata.DisposalMode,
blendOperation: frameMetadata.BlendMethod); blendMode: frameMetadata.BlendMode);
fcTL.WriteTo(this.chunkDataBuffer.Span); fcTL.WriteTo(this.chunkDataBuffer.Span);
@ -1483,41 +1433,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// Use options, then check metadata, if nothing set there then we suggest // Use options, then check metadata, if nothing set there then we suggest
// a sensible default based upon the pixel format. // a sensible default based upon the pixel format.
PngColorType? colorType = encoder.ColorType ?? pngMetadata.ColorType; PngColorType color = encoder.ColorType ?? pngMetadata.ColorType;
byte? bits = (byte?)(encoder.BitDepth ?? pngMetadata.BitDepth); byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth);
if (colorType is null || bits is null)
{
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
PixelComponentInfo? componentInfo = info.ComponentInfo;
colorType ??= SuggestColorType<TPixel>(in info);
if (bits is null)
{
// TODO: Update once we stop abusing PixelTypeInfo in decoders.
if (componentInfo.HasValue)
{
PixelComponentInfo c = componentInfo.Value;
bits = (byte)SuggestBitDepth<TPixel>(in c);
}
else
{
bits = (byte)PngBitDepth.Bit8;
}
}
}
// Ensure bit depth and color type are a supported combination. // Ensure the bit depth and color type are a supported combination.
// Bit8 is the only bit depth supported by all color types. // Bit8 is the only bit depth supported by all color types.
byte[] validBitDepths = PngConstants.ColorTypes[colorType.Value]; byte[] validBitDepths = PngConstants.ColorTypes[color];
if (Array.IndexOf(validBitDepths, bits) == -1) if (Array.IndexOf(validBitDepths, bits) == -1)
{ {
bits = (byte)PngBitDepth.Bit8; bits = (byte)PngBitDepth.Bit8;
} }
this.colorType = colorType.Value; this.colorType = color;
this.bitDepth = bits.Value; this.bitDepth = bits;
if (encoder.FilterMethod.HasValue) if (encoder.FilterMethod.HasValue)
{ {
@ -1532,7 +1460,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
use16Bit = bits == (byte)PngBitDepth.Bit16; use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit); bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value; this.interlaceMode = encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None; this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
} }
@ -1672,47 +1600,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
_ => use16Bit ? 8 : 4, _ => use16Bit ? 8 : 4,
}; };
/// <summary>
/// Returns a suggested <see cref="PngColorType"/> for the given <typeparamref name="TPixel"/>
/// </summary>
/// <param name="info">The pixel type info.</param>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>(in PixelTypeInfo info)
where TPixel : unmanaged, IPixel<TPixel>
{
if (info.AlphaRepresentation == PixelAlphaRepresentation.None)
{
return info.ColorType switch
{
PixelColorType.Grayscale => PngColorType.Grayscale,
_ => PngColorType.Rgb,
};
}
return info.ColorType switch
{
PixelColorType.Grayscale | PixelColorType.Alpha or PixelColorType.Alpha => PngColorType.GrayscaleWithAlpha,
_ => PngColorType.RgbWithAlpha,
};
}
/// <summary>
/// Returns a suggested <see cref="PngBitDepth"/> for the given <typeparamref name="TPixel"/>
/// </summary>
/// <param name="info">The pixel type info.</param>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>(in PixelComponentInfo info)
where TPixel : unmanaged, IPixel<TPixel>
{
int bits = info.GetMaximumComponentPrecision();
if (bits > (int)PixelComponentBitDepth.Bit8)
{
return PngBitDepth.Bit16;
}
return PngBitDepth.Bit8;
}
private unsafe struct ScratchBuffer private unsafe struct ScratchBuffer
{ {
private const int Size = 26; private const int Size = 26;

56
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Provides APng specific metadata information for the image frame. /// Provides APng specific metadata information for the image frame.
/// </summary> /// </summary>
public class PngFrameMetadata : IDeepCloneable public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class. /// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
@ -24,8 +24,8 @@ public class PngFrameMetadata : IDeepCloneable
private PngFrameMetadata(PngFrameMetadata other) private PngFrameMetadata(PngFrameMetadata other)
{ {
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMode = other.DisposalMode;
this.BlendMethod = other.BlendMethod; this.BlendMode = other.BlendMode;
} }
/// <summary> /// <summary>
@ -39,12 +39,12 @@ public class PngFrameMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame /// Gets or sets the type of frame area disposal to be done after rendering this frame
/// </summary> /// </summary>
public PngDisposalMethod DisposalMethod { get; set; } public FrameDisposalMode DisposalMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of frame area rendering for this frame /// Gets or sets the type of frame area rendering for this frame
/// </summary> /// </summary>
public PngBlendMethod BlendMethod { get; set; } public FrameBlendMode BlendMode { get; set; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class. /// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
@ -53,26 +53,48 @@ public class PngFrameMetadata : IDeepCloneable
internal void FromChunk(in FrameControl frameControl) internal void FromChunk(in FrameControl frameControl)
{ {
this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator); this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
this.DisposalMethod = frameControl.DisposeOperation; this.DisposalMode = frameControl.DisposalMode;
this.BlendMethod = frameControl.BlendOperation; this.BlendMode = frameControl.BlendMode;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngFrameMetadata(this); public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
=> new() => new()
{ {
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
DisposalMethod = GetMode(metadata.DisposalMode), DisposalMode = GetMode(metadata.DisposalMode),
BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over, BlendMode = metadata.BlendMode,
}; };
private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch /// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
{
double delay = this.FrameDelay.ToDouble();
if (double.IsNaN(delay))
{
delay = 0;
}
return new()
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(delay * 1000),
DisposalMode = this.DisposalMode,
BlendMode = this.BlendMode,
};
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public PngFrameMetadata DeepClone() => new(this);
private static FrameDisposalMode GetMode(FrameDisposalMode mode) => mode switch
{ {
FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground, FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious, FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose, FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose,
_ => PngDisposalMethod.DoNotDispose, _ => FrameDisposalMode.DoNotDispose,
}; };
} }

165
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -2,13 +2,14 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Provides Png specific metadata information for the image. /// Provides Png specific metadata information for the image.
/// </summary> /// </summary>
public class PngMetadata : IDeepCloneable public class PngMetadata : IFormatMetadata<PngMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngMetadata"/> class. /// Initializes a new instance of the <see cref="PngMetadata"/> class.
@ -46,17 +47,17 @@ public class PngMetadata : IDeepCloneable
/// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Gets or sets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values. /// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary> /// </summary>
public PngBitDepth? BitDepth { get; set; } public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8;
/// <summary> /// <summary>
/// Gets or sets the color type. /// Gets or sets the color type.
/// </summary> /// </summary>
public PngColorType? ColorType { get; set; } public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image. /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary> /// </summary>
public PngInterlaceMode? InterlaceMethod { get; set; } = PngInterlaceMode.None; public PngInterlaceMode InterlaceMethod { get; set; } = PngInterlaceMode.None;
/// <summary> /// <summary>
/// Gets or sets the gamma value for the image. /// Gets or sets the gamma value for the image.
@ -77,7 +78,7 @@ public class PngMetadata : IDeepCloneable
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks.
/// Used for conveying textual information associated with the image. /// Used for conveying textual information associated with the image.
/// </summary> /// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>(); public IList<PngTextData> TextData { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
@ -90,35 +91,165 @@ public class PngMetadata : IDeepCloneable
public bool AnimateRootFrame { get; set; } = true; public bool AnimateRootFrame { get; set; } = true;
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this); public static PngMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
{ {
// Should the conversion be from a format that uses a 24bit palette entries (gif) // Should the conversion be from a format that uses a 24bit palette entries (gif)
// we need to clone and adjust the color table to allow for transparency. // we need to clone and adjust the color table to allow for transparency.
Color[]? colorTable = metadata.ColorTable.HasValue ? metadata.ColorTable.Value.ToArray() : null; Color[]? colorTable = metadata.ColorTable?.ToArray();
if (colorTable != null) if (colorTable != null)
{ {
for (int i = 0; i < colorTable.Length; i++) for (int i = 0; i < colorTable.Length; i++)
{ {
ref Color c = ref colorTable[i]; ref Color c = ref colorTable[i];
if (c == metadata.BackgroundColor) if (c != metadata.BackgroundColor)
{ {
// Png treats background as fully empty continue;
c = Color.Transparent;
break;
} }
// Png treats background as fully empty
c = Color.Transparent;
break;
} }
} }
PngColorType color;
PixelColorType colorType = metadata.PixelTypeInfo.ColorType;
switch (colorType)
{
case PixelColorType.Binary:
case PixelColorType.Indexed:
color = PngColorType.Palette;
break;
case PixelColorType.Luminance:
color = PngColorType.Grayscale;
break;
case PixelColorType.RGB:
case PixelColorType.BGR:
color = PngColorType.Rgb;
break;
default:
if (colorType.HasFlag(PixelColorType.Luminance))
{
color = PngColorType.GrayscaleWithAlpha;
break;
}
color = PngColorType.RgbWithAlpha;
break;
}
// PNG uses bits per component not per pixel.
int bpc = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8;
PngBitDepth bitDepth = bpc switch
{
1 => PngBitDepth.Bit1,
2 => PngBitDepth.Bit2,
4 => PngBitDepth.Bit4,
_ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16,
};
return new() return new()
{ {
ColorType = colorTable != null ? PngColorType.Palette : null, ColorType = color,
BitDepth = colorTable != null BitDepth = bitDepth,
? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8)
: null,
ColorTable = colorTable, ColorTable = colorTable,
RepeatCount = metadata.RepeatCount, RepeatCount = metadata.RepeatCount,
}; };
} }
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp;
PixelColorType colorType;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
PixelComponentInfo info;
switch (this.ColorType)
{
case PngColorType.Palette:
bpp = this.ColorTable.HasValue
? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.ColorTable.Value.Length), 1, 8)
: 8;
colorType = PixelColorType.Indexed;
info = PixelComponentInfo.Create(1, bpp, bpp);
break;
case PngColorType.Grayscale:
bpp = (int)this.BitDepth;
colorType = PixelColorType.Luminance;
info = PixelComponentInfo.Create(1, bpp, bpp);
break;
case PngColorType.GrayscaleWithAlpha:
alpha = PixelAlphaRepresentation.Unassociated;
if (this.BitDepth == PngBitDepth.Bit16)
{
bpp = 32;
colorType = PixelColorType.Luminance | PixelColorType.Alpha;
info = PixelComponentInfo.Create(2, bpp, 16, 16);
break;
}
bpp = 16;
colorType = PixelColorType.Luminance | PixelColorType.Alpha;
info = PixelComponentInfo.Create(2, bpp, 8, 8);
break;
case PngColorType.Rgb:
if (this.BitDepth == PngBitDepth.Bit16)
{
bpp = 48;
colorType = PixelColorType.RGB;
info = PixelComponentInfo.Create(3, bpp, 16, 16, 16);
break;
}
bpp = 24;
colorType = PixelColorType.RGB;
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
break;
case PngColorType.RgbWithAlpha:
default:
alpha = PixelAlphaRepresentation.Unassociated;
if (this.BitDepth == PngBitDepth.Bit16)
{
bpp = 64;
colorType = PixelColorType.RGB | PixelColorType.Alpha;
info = PixelComponentInfo.Create(4, bpp, 16, 16, 16, 16);
break;
}
bpp = 32;
colorType = PixelColorType.RGB | PixelColorType.Alpha;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ColorType = colorType,
ComponentInfo = info,
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
ColorTable = this.ColorTable,
ColorTableMode = FrameColorTableMode.Global,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public PngMetadata DeepClone() => new(this);
} }

20
src/ImageSharp/Formats/Qoi/MetadataExtensions.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Qoi;
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 qoi format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="QoiMetadata"/>.</returns>
public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance);
}

23
src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs

@ -13,7 +13,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.Qoi;
internal class QoiDecoderCore : IImageDecoderInternals internal class QoiDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// The global configuration. /// The global configuration.
@ -31,31 +31,20 @@ internal class QoiDecoderCore : IImageDecoderInternals
private QoiHeader header; private QoiHeader header;
public QoiDecoderCore(DecoderOptions options) public QoiDecoderCore(DecoderOptions options)
: base(options)
{ {
this.Options = options;
this.configuration = options.Configuration; this.configuration = options.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
} }
public DecoderOptions Options { get; }
public Size Dimensions { get; }
/// <inheritdoc /> /// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
// Process the header to get metadata // Process the header to get metadata
this.ProcessHeader(stream); this.ProcessHeader(stream);
// Create Image object // Create Image object
ImageMetadata metadata = new() ImageMetadata metadata = new();
{
DecodedImageFormat = QoiFormat.Instance,
HorizontalResolution = this.header.Width,
VerticalResolution = this.header.Height,
ResolutionUnits = PixelResolutionUnit.AspectRatio
};
QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
qoiMetadata.Channels = this.header.Channels; qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace; qoiMetadata.ColorSpace = this.header.ColorSpace;
@ -68,7 +57,7 @@ internal class QoiDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ProcessHeader(stream); this.ProcessHeader(stream);
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels); PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
@ -79,7 +68,7 @@ internal class QoiDecoderCore : IImageDecoderInternals
qoiMetadata.Channels = this.header.Channels; qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace; qoiMetadata.ColorSpace = this.header.ColorSpace;
return new ImageInfo(pixelType, size, metadata); return new ImageInfo(size, metadata);
} }
/// <summary> /// <summary>

10
src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a QOi image /// Image encoder for writing an image to a stream as a QOi image
/// </summary> /// </summary>
internal class QoiEncoderCore : IImageEncoderInternals internal class QoiEncoderCore
{ {
/// <summary> /// <summary>
/// The encoder with options /// The encoder with options
@ -41,7 +41,13 @@ internal class QoiEncoderCore : IImageEncoderInternals
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
} }
/// <inheritdoc /> /// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {

60
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -1,12 +1,14 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary> /// <summary>
/// Provides Qoi specific metadata information for the image. /// Provides Qoi specific metadata information for the image.
/// </summary> /// </summary>
public class QoiMetadata : IDeepCloneable public class QoiMetadata : IFormatMetadata<QoiMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class. /// Initializes a new instance of the <see cref="QoiMetadata"/> class.
@ -36,5 +38,59 @@ public class QoiMetadata : IDeepCloneable
public QoiColorSpace ColorSpace { get; set; } public QoiColorSpace ColorSpace { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new QoiMetadata(this); public static QoiMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
PixelColorType color = metadata.PixelTypeInfo.ColorType;
if (color.HasFlag(PixelColorType.Alpha))
{
return new QoiMetadata { Channels = QoiChannels.Rgba };
}
return new QoiMetadata { Channels = QoiChannels.Rgb };
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp;
PixelColorType colorType;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
PixelComponentInfo info;
switch (this.Channels)
{
case QoiChannels.Rgb:
bpp = 24;
colorType = PixelColorType.RGB;
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
break;
default:
bpp = 32;
colorType = PixelColorType.RGB | PixelColorType.Alpha;
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ColorType = colorType,
ComponentInfo = info,
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public QoiMetadata DeepClone() => new(this);
} }

20
src/ImageSharp/Formats/Tga/MetadataExtensions.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Tga;
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 tga format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="TgaMetadata"/>.</returns>
public static TgaMetadata GetTgaMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TgaFormat.Instance);
}

8
src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs

@ -11,20 +11,20 @@ public enum TgaBitsPerPixel : byte
/// <summary> /// <summary>
/// 8 bits per pixel. Each pixel consists of 1 byte. /// 8 bits per pixel. Each pixel consists of 1 byte.
/// </summary> /// </summary>
Pixel8 = 8, Bit8 = 8,
/// <summary> /// <summary>
/// 16 bits per pixel. Each pixel consists of 2 bytes. /// 16 bits per pixel. Each pixel consists of 2 bytes.
/// </summary> /// </summary>
Pixel16 = 16, Bit16 = 16,
/// <summary> /// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes. /// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary> /// </summary>
Pixel24 = 24, Bit24 = 24,
/// <summary> /// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes. /// 32 bits per pixel. Each pixel consists of 4 bytes.
/// </summary> /// </summary>
Pixel32 = 32 Bit32 = 32
} }

20
src/ImageSharp/Formats/Tga/TgaDecoderCore.cs

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Performs the tga decoding operation. /// Performs the tga decoding operation.
/// </summary> /// </summary>
internal sealed class TgaDecoderCore : IImageDecoderInternals internal sealed class TgaDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// General configuration options. /// General configuration options.
@ -52,21 +52,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
public TgaDecoderCore(DecoderOptions options) public TgaDecoderCore(DecoderOptions options)
: base(options)
{ {
this.Options = options;
this.configuration = options.Configuration; this.configuration = options.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
} }
/// <inheritdoc /> /// <inheritdoc />
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc />
public Size Dimensions => new(this.fileHeader.Width, this.fileHeader.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
try try
{ {
@ -642,11 +635,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ReadFileHeader(stream); this.ReadFileHeader(stream);
return new ImageInfo( return new ImageInfo(
new PixelTypeInfo(this.fileHeader.PixelDepth),
new(this.fileHeader.Width, this.fileHeader.Height), new(this.fileHeader.Width, this.fileHeader.Height),
this.metadata); this.metadata);
} }
@ -915,6 +907,8 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
stream.Read(buffer, 0, TgaFileHeader.Size); stream.Read(buffer, 0, TgaFileHeader.Size);
this.fileHeader = TgaFileHeader.Parse(buffer); this.fileHeader = TgaFileHeader.Parse(buffer);
this.Dimensions = new Size(this.fileHeader.Width, this.fileHeader.Height);
this.metadata = new ImageMetadata(); this.metadata = new ImageMetadata();
this.tgaMetadata = this.metadata.GetTgaMetadata(); this.tgaMetadata = this.metadata.GetTgaMetadata();
this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth;
@ -934,7 +928,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
return (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4); return (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4);
} }
private bool IsTrueColor32BitPerPixel(TgaBitsPerPixel bitsPerPixel) => bitsPerPixel == TgaBitsPerPixel.Pixel32 && private bool IsTrueColor32BitPerPixel(TgaBitsPerPixel bitsPerPixel) => bitsPerPixel == TgaBitsPerPixel.Bit32 &&
(this.fileHeader.ImageType == TgaImageType.TrueColor || (this.fileHeader.ImageType == TgaImageType.TrueColor ||
this.fileHeader.ImageType == TgaImageType.RleTrueColor); this.fileHeader.ImageType == TgaImageType.RleTrueColor);
} }

110
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -3,8 +3,6 @@
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -14,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a truevision targa image. /// Image encoder for writing an image to a stream as a truevision targa image.
/// </summary> /// </summary>
internal sealed class TgaEncoderCore : IImageEncoderInternals internal sealed class TgaEncoderCore
{ {
/// <summary> /// <summary>
/// Used for allocating memory during processing operations. /// Used for allocating memory during processing operations.
@ -61,7 +59,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
this.bitsPerPixel ??= tgaMetadata.BitsPerPixel; this.bitsPerPixel ??= tgaMetadata.BitsPerPixel;
TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor; TgaImageType imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleTrueColor : TgaImageType.TrueColor;
if (this.bitsPerPixel == TgaBitsPerPixel.Pixel8) if (this.bitsPerPixel == TgaBitsPerPixel.Bit8)
{ {
imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite; imageType = this.compression is TgaCompression.RunLength ? TgaImageType.RleBlackAndWhite : TgaImageType.BlackAndWhite;
} }
@ -73,13 +71,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
imageDescriptor |= 0x20; imageDescriptor |= 0x20;
} }
if (this.bitsPerPixel is TgaBitsPerPixel.Pixel32) if (this.bitsPerPixel is TgaBitsPerPixel.Bit32)
{ {
// Indicate, that 8 bit are used for the alpha channel. // Indicate, that 8 bit are used for the alpha channel.
imageDescriptor |= 0x8; imageDescriptor |= 0x8;
} }
if (this.bitsPerPixel is TgaBitsPerPixel.Pixel16) if (this.bitsPerPixel is TgaBitsPerPixel.Bit16)
{ {
// Indicate, that 1 bit is used for the alpha channel. // Indicate, that 1 bit is used for the alpha channel.
imageDescriptor |= 0x1; imageDescriptor |= 0x1;
@ -107,11 +105,11 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
stream.Write(buffer, 0, TgaFileHeader.Size); stream.Write(buffer, 0, TgaFileHeader.Size);
if (this.compression is TgaCompression.RunLength) if (this.compression is TgaCompression.RunLength)
{ {
this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame); this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame, cancellationToken);
} }
else else
{ {
this.WriteImage(image.Configuration, stream, image.Frames.RootFrame); this.WriteImage(image.Configuration, stream, image.Frames.RootFrame, cancellationToken);
} }
stream.Flush(); stream.Flush();
@ -123,29 +121,28 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> /// <param name="image"> /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data. /// <param name="cancellationToken">The token to request cancellation.</param>
/// </param> private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image, CancellationToken cancellationToken)
private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.PixelBuffer; Buffer2D<TPixel> pixels = image.PixelBuffer;
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case TgaBitsPerPixel.Pixel8: case TgaBitsPerPixel.Bit8:
this.Write8Bit(configuration, stream, pixels); this.Write8Bit(configuration, stream, pixels, cancellationToken);
break; break;
case TgaBitsPerPixel.Pixel16: case TgaBitsPerPixel.Bit16:
this.Write16Bit(configuration, stream, pixels); this.Write16Bit(configuration, stream, pixels, cancellationToken);
break; break;
case TgaBitsPerPixel.Pixel24: case TgaBitsPerPixel.Bit24:
this.Write24Bit(configuration, stream, pixels); this.Write24Bit(configuration, stream, pixels, cancellationToken);
break; break;
case TgaBitsPerPixel.Pixel32: case TgaBitsPerPixel.Bit32:
this.Write32Bit(configuration, stream, pixels); this.Write32Bit(configuration, stream, pixels, cancellationToken);
break; break;
} }
} }
@ -156,23 +153,33 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The stream to write the image to.</param> /// <param name="stream">The stream to write the image to.</param>
/// <param name="image">The image to encode.</param> /// <param name="image">The image to encode.</param>
private void WriteRunLengthEncodedImage<TPixel>(Stream stream, ImageFrame<TPixel> image) /// <param name="cancellationToken">The token to request cancellation.</param>
private void WriteRunLengthEncodedImage<TPixel>(Stream stream, ImageFrame<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.PixelBuffer; Buffer2D<TPixel> pixels = image.PixelBuffer;
using IMemoryOwner<Rgba32> rgbaOwner = this.memoryAllocator.Allocate<Rgba32>(image.Width);
Span<Rgba32> rgbaRow = rgbaOwner.GetSpan();
for (int y = 0; y < image.Height; y++) for (int y = 0; y < image.Height; y++)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgba32(image.Configuration, pixelRow, rgbaRow);
for (int x = 0; x < image.Width;) for (int x = 0; x < image.Width;)
{ {
TPixel currentPixel = pixelRow[x]; TPixel currentPixel = pixelRow[x];
Rgba32 rgba = rgbaRow[x];
byte equalPixelCount = FindEqualPixels(pixelRow, x); byte equalPixelCount = FindEqualPixels(pixelRow, x);
if (equalPixelCount > 0) if (equalPixelCount > 0)
{ {
// Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. // Write the number of equal pixels, with the high bit set, indicating it's a compressed pixel run.
stream.WriteByte((byte)(equalPixelCount | 128)); stream.WriteByte((byte)(equalPixelCount | 128));
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); this.WritePixel(stream, rgba);
x += equalPixelCount + 1; x += equalPixelCount + 1;
} }
else else
@ -180,12 +187,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
// Write Raw Packet (i.e., Non-Run-Length Encoded): // Write Raw Packet (i.e., Non-Run-Length Encoded):
byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x); byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x);
stream.WriteByte(unEqualPixelCount); stream.WriteByte(unEqualPixelCount);
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); this.WritePixel(stream, rgba);
x++; x++;
for (int i = 0; i < unEqualPixelCount; i++) for (int i = 0; i < unEqualPixelCount; i++)
{ {
currentPixel = pixelRow[x]; currentPixel = pixelRow[x];
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32()); rgba = rgbaRow[x];
this.WritePixel(stream, rgba);
x++; x++;
} }
} }
@ -196,22 +204,19 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes a the pixel to the stream. /// Writes a the pixel to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="currentPixel">The current pixel.</param>
/// <param name="color">The color of the pixel to write.</param> /// <param name="color">The color of the pixel to write.</param>
private void WritePixel<TPixel>(Stream stream, TPixel currentPixel, Rgba32 color) private void WritePixel(Stream stream, Rgba32 color)
where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case TgaBitsPerPixel.Pixel8: case TgaBitsPerPixel.Bit8:
int luminance = GetLuminance(currentPixel); L8 l8 = L8.FromRgba32(color);
stream.WriteByte((byte)luminance); stream.WriteByte(l8.PackedValue);
break; break;
case TgaBitsPerPixel.Pixel16: case TgaBitsPerPixel.Bit16:
Bgra5551 bgra5551 = new(color.ToVector4()); Bgra5551 bgra5551 = Bgra5551.FromRgba32(color);
Span<byte> buffer = stackalloc byte[2]; Span<byte> buffer = stackalloc byte[2];
BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue); BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue);
stream.WriteByte(buffer[0]); stream.WriteByte(buffer[0]);
@ -219,13 +224,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
break; break;
case TgaBitsPerPixel.Pixel24: case TgaBitsPerPixel.Bit24:
stream.WriteByte(color.B); stream.WriteByte(color.B);
stream.WriteByte(color.G); stream.WriteByte(color.G);
stream.WriteByte(color.R); stream.WriteByte(color.R);
break; break;
case TgaBitsPerPixel.Pixel32: case TgaBitsPerPixel.Bit32:
stream.WriteByte(color.B); stream.WriteByte(color.B);
stream.WriteByte(color.G); stream.WriteByte(color.G);
stream.WriteByte(color.R); stream.WriteByte(color.R);
@ -310,7 +315,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write8Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels) /// <param name="cancellationToken">The token to request cancellation.</param>
private void Write8Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 1); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 1);
@ -318,6 +324,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8Bytes( PixelOperations<TPixel>.Instance.ToL8Bytes(
configuration, configuration,
@ -335,7 +343,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels) /// <param name="cancellationToken">The token to request cancellation.</param>
private void Write16Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 2); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 2);
@ -343,6 +352,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes( PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
configuration, configuration,
@ -360,7 +371,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels) /// <param name="cancellationToken">The token to request cancellation.</param>
private void Write24Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 3); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 3);
@ -368,6 +380,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes( PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration, configuration,
@ -385,7 +399,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels) /// <param name="cancellationToken">The token to request cancellation.</param>
private void Write32Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -393,6 +408,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes( PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration, configuration,
@ -402,17 +419,4 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
stream.Write(rowSpan); stream.Write(rowSpan);
} }
} }
/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="sourcePixel">The pixel to get the luminance from.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance<TPixel>(TPixel sourcePixel)
where TPixel : unmanaged, IPixel<TPixel>
{
Vector4 vector = sourcePixel.ToVector4();
return ColorNumerics.GetBT709Luminance(ref vector, 256);
}
} }

71
src/ImageSharp/Formats/Tga/TgaMetadata.cs

@ -1,12 +1,14 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tga; namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Provides TGA specific metadata information for the image. /// Provides TGA specific metadata information for the image.
/// </summary> /// </summary>
public class TgaMetadata : IDeepCloneable public class TgaMetadata : IFormatMetadata<TgaMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TgaMetadata"/> class. /// Initializes a new instance of the <see cref="TgaMetadata"/> class.
@ -25,7 +27,7 @@ public class TgaMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel. /// Gets or sets the number of bits per pixel.
/// </summary> /// </summary>
public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24; public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Bit24;
/// <summary> /// <summary>
/// Gets or sets the number of alpha bits per pixel. /// Gets or sets the number of alpha bits per pixel.
@ -33,5 +35,68 @@ public class TgaMetadata : IDeepCloneable
public byte AlphaChannelBits { get; set; } public byte AlphaChannelBits { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new TgaMetadata(this); public static TgaMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
// TODO: AlphaChannelBits is not used during encoding.
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
return bpp switch
{
<= 8 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit8 },
<= 16 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit16 },
<= 24 => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit24 },
_ => new TgaMetadata { BitsPerPixel = TgaBitsPerPixel.Bit32 }
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BitsPerPixel;
PixelComponentInfo info;
PixelColorType color;
PixelAlphaRepresentation alpha;
switch (this.BitsPerPixel)
{
case TgaBitsPerPixel.Bit8:
info = PixelComponentInfo.Create(1, bpp, 8);
color = PixelColorType.Luminance;
alpha = PixelAlphaRepresentation.None;
break;
case TgaBitsPerPixel.Bit16:
info = PixelComponentInfo.Create(1, bpp, 5, 5, 5, 1);
color = PixelColorType.BGR | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
case TgaBitsPerPixel.Bit24:
info = PixelComponentInfo.Create(3, bpp, 8, 8, 8);
color = PixelColorType.RGB;
alpha = PixelAlphaRepresentation.None;
break;
case TgaBitsPerPixel.Bit32 or _:
info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8);
color = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
}
return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public TgaMetadata DeepClone() => new(this);
} }

2
src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs

@ -33,7 +33,7 @@ internal class TiffJpegCompressor : TiffBaseCompressor
var image = Image.LoadPixelData<Rgb24>(rows, width, height); var image = Image.LoadPixelData<Rgb24>(rows, width, height);
image.Save(memoryStream, new JpegEncoder() image.Save(memoryStream, new JpegEncoder()
{ {
ColorType = JpegEncodingColor.Rgb ColorType = JpegColorType.Rgb
}); });
memoryStream.Position = 0; memoryStream.Position = 0;
memoryStream.WriteTo(this.Output); memoryStream.WriteTo(this.Output);

4
src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs

@ -32,8 +32,8 @@ internal class WebpTiffCompression : TiffBaseDecompressor
/// <inheritdoc/> /// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken) protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{ {
using WebpDecoderCore decoder = new(new WebpDecoderOptions()); using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = this.options });
using Image<Rgb24> image = decoder.Decode<Rgb24>(stream, cancellationToken); using Image<Rgb24> image = decoder.Decode<Rgb24>(this.options.Configuration, stream, cancellationToken);
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
} }

45
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -39,9 +39,9 @@ internal static class TiffConstants
public const ushort BigTiffHeaderMagicNumber = 43; public const ushort BigTiffHeaderMagicNumber = 43;
/// <summary> /// <summary>
/// The big tiff bytesize of offsets value. /// The big tiff byte size of offsets value.
/// </summary> /// </summary>
public const ushort BigTiffBytesize = 8; public const ushort BigTiffByteSize = 8;
/// <summary> /// <summary>
/// RowsPerStrip default value, which is effectively infinity. /// RowsPerStrip default value, which is effectively infinity.
@ -58,38 +58,63 @@ internal static class TiffConstants
/// </summary> /// </summary>
public const int DefaultStripSize = 8 * 1024; public const int DefaultStripSize = 8 * 1024;
/// <summary>
/// The default predictor is None.
/// </summary>
public const TiffPredictor DefaultPredictor = TiffPredictor.None;
/// <summary>
/// The default bits per pixel is Bit24.
/// </summary>
public const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;
/// <summary>
/// The default bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly TiffBitsPerSample DefaultBitsPerSample = BitsPerSampleRgb8Bit;
/// <summary>
/// The default compression is None.
/// </summary>
public const TiffCompression DefaultCompression = TiffCompression.None;
/// <summary>
/// The default photometric interpretation is Rgb.
/// </summary>
public const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
/// <summary> /// <summary>
/// The bits per sample for 1 bit bicolor images. /// The bits per sample for 1 bit bicolor images.
/// </summary> /// </summary>
public static readonly TiffBitsPerSample BitsPerSample1Bit = new TiffBitsPerSample(1, 0, 0); public static readonly TiffBitsPerSample BitsPerSample1Bit = new(1, 0, 0);
/// <summary> /// <summary>
/// The bits per sample for images with a 4 color palette. /// The bits per sample for images with a 4 color palette.
/// </summary> /// </summary>
public static readonly TiffBitsPerSample BitsPerSample4Bit = new TiffBitsPerSample(4, 0, 0); public static readonly TiffBitsPerSample BitsPerSample4Bit = new(4, 0, 0);
/// <summary> /// <summary>
/// The bits per sample for 8 bit images. /// The bits per sample for 8 bit images.
/// </summary> /// </summary>
public static readonly TiffBitsPerSample BitsPerSample8Bit = new TiffBitsPerSample(8, 0, 0); public static readonly TiffBitsPerSample BitsPerSample8Bit = new(8, 0, 0);
/// <summary> /// <summary>
/// The bits per sample for 16-bit grayscale images. /// The bits per sample for 16-bit grayscale images.
/// </summary> /// </summary>
public static readonly TiffBitsPerSample BitsPerSample16Bit = new TiffBitsPerSample(16, 0, 0); public static readonly TiffBitsPerSample BitsPerSample16Bit = new(16, 0, 0);
/// <summary> /// <summary>
/// The bits per sample for color images with 8 bits for each color channel. /// The bits per sample for color images with 8 bits for each color channel.
/// </summary> /// </summary>
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new TiffBitsPerSample(8, 8, 8); public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8);
/// <summary> /// <summary>
/// The list of mimetypes that equate to a tiff. /// The list of mime types that equate to a tiff.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/tiff", "image/tiff-fx" }; public static readonly IEnumerable<string> MimeTypes = ["image/tiff", "image/tiff-fx"];
/// <summary> /// <summary>
/// The list of file extensions that equate to a tiff. /// The list of file extensions that equate to a tiff.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "tiff", "tif" }; public static readonly IEnumerable<string> FileExtensions = ["tiff", "tif"];
} }

2
src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs

@ -64,7 +64,7 @@ internal class HeaderReader : BaseExifReader
ushort bytesize = this.ReadUInt16(); ushort bytesize = this.ReadUInt16();
ushort reserve = this.ReadUInt16(); ushort reserve = this.ReadUInt16();
if (bytesize == TiffConstants.BigTiffBytesize && reserve == 0) if (bytesize == TiffConstants.BigTiffByteSize && reserve == 0)
{ {
this.FirstIfdOffset = this.ReadUInt64(); this.FirstIfdOffset = this.ReadUInt64();
return; return;

27
src/ImageSharp/Formats/Tiff/MetadataExtensions.cs

@ -1,27 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Tiff;
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 tiff format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="TiffMetadata"/>.</returns>
public static TiffMetadata GetTiffMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance);
/// <summary>
/// Gets the tiff format specific metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="TiffFrameMetadata"/>.</returns>
public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance);
}

8
src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs

@ -147,20 +147,20 @@ public readonly struct TiffBitsPerSample : IEquatable<TiffBitsPerSample>
{ {
if (this.Channel1 == 0) if (this.Channel1 == 0)
{ {
return new[] { this.Channel0 }; return [this.Channel0];
} }
if (this.Channel2 == 0) if (this.Channel2 == 0)
{ {
return new[] { this.Channel0, this.Channel1 }; return [this.Channel0, this.Channel1];
} }
if (this.Channel3 == 0) if (this.Channel3 == 0)
{ {
return new[] { this.Channel0, this.Channel1, this.Channel2 }; return [this.Channel0, this.Channel1, this.Channel2];
} }
return new[] { this.Channel0, this.Channel1, this.Channel2, this.Channel3 }; return [this.Channel0, this.Channel1, this.Channel2, this.Channel3];
} }
/// <summary> /// <summary>

17
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Performs the tiff decoding operation. /// Performs the tiff decoding operation.
/// </summary> /// </summary>
internal class TiffDecoderCore : IImageDecoderInternals internal class TiffDecoderCore : ImageDecoderCore
{ {
/// <summary> /// <summary>
/// General configuration options. /// General configuration options.
@ -60,8 +60,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public TiffDecoderCore(DecoderOptions options) public TiffDecoderCore(DecoderOptions options)
: base(options)
{ {
this.Options = options;
this.configuration = options.Configuration; this.configuration = options.Configuration;
this.skipMetadata = options.SkipMetadata; this.skipMetadata = options.SkipMetadata;
this.maxFrames = options.MaxFrames; this.maxFrames = options.MaxFrames;
@ -154,14 +154,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
public TiffPredictor Predictor { get; set; } public TiffPredictor Predictor { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions Options { get; } protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc/>
public Size Dimensions { get; private set; }
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
List<ImageFrame<TPixel>> frames = []; List<ImageFrame<TPixel>> frames = [];
List<ImageFrameMetadata> framesMetadata = []; List<ImageFrameMetadata> framesMetadata = [];
@ -215,7 +208,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
} }
/// <inheritdoc/> /// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.inputStream = stream; this.inputStream = stream;
DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); DirectoryReader reader = new(stream, this.configuration.MemoryAllocator);
@ -234,7 +227,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
int width = GetImageWidth(rootFrameExifProfile); int width = GetImageWidth(rootFrameExifProfile);
int height = GetImageHeight(rootFrameExifProfile); int height = GetImageHeight(rootFrameExifProfile);
return new ImageInfo(new PixelTypeInfo((int)framesMetadata[0].GetTiffMetadata().BitsPerPixel), new(width, height), metadata, framesMetadata); return new ImageInfo(new(width, height), metadata, framesMetadata);
} }
/// <summary> /// <summary>

16
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -23,7 +23,7 @@ internal static class TiffDecoderMetadataCreator
TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); TiffThrowHelper.ThrowImageFormatException("Expected at least one frame.");
} }
ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].ExifProfile); ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]);
if (!ignoreMetadata) if (!ignoreMetadata)
{ {
@ -50,14 +50,22 @@ internal static class TiffDecoderMetadataCreator
return imageMetaData; return imageMetaData;
} }
private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ExifProfile exifProfile) private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFrameMetadata rootFrameMetadata)
{ {
ImageMetadata imageMetaData = new(); ImageMetadata imageMetaData = new();
SetResolution(imageMetaData, exifProfile); SetResolution(imageMetaData, rootFrameMetadata.ExifProfile);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder; tiffMetadata.ByteOrder = byteOrder;
tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default; tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default;
TiffFrameMetadata tiffFrameMetadata = rootFrameMetadata.GetTiffMetadata();
tiffMetadata.BitsPerPixel = tiffFrameMetadata.BitsPerPixel;
tiffMetadata.BitsPerSample = tiffFrameMetadata.BitsPerSample;
tiffMetadata.Compression = tiffFrameMetadata.Compression;
tiffMetadata.PhotometricInterpretation = tiffFrameMetadata.PhotometricInterpretation;
tiffMetadata.Predictor = tiffFrameMetadata.Predictor;
return imageMetaData; return imageMetaData;
} }
@ -109,7 +117,7 @@ internal static class TiffDecoderMetadataCreator
return false; return false;
} }
// Probably wrong endianess, swap byte order. // Probably wrong endianness, swap byte order.
Span<byte> iptcBytesSpan = iptcBytes.AsSpan(); Span<byte> iptcBytesSpan = iptcBytes.AsSpan();
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
for (int i = 0; i < iptcBytes.Length; i += 4) for (int i = 0; i < iptcBytes.Length; i += 4)

78
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -63,7 +63,7 @@ internal static class TiffDecoderOptionsParser
} }
TiffSampleFormat? sampleFormat = null; TiffSampleFormat? sampleFormat = null;
if (exifProfile.TryGetValue(ExifTag.SampleFormat, out var formatValue)) if (exifProfile.TryGetValue(ExifTag.SampleFormat, out IExifValue<ushort[]> formatValue))
{ {
TiffSampleFormat[] sampleFormats = formatValue.Value.Select(a => (TiffSampleFormat)a).ToArray(); TiffSampleFormat[] sampleFormats = formatValue.Value.Select(a => (TiffSampleFormat)a).ToArray();
sampleFormat = sampleFormats[0]; sampleFormat = sampleFormats[0];
@ -106,11 +106,11 @@ internal static class TiffDecoderOptionsParser
options.PlanarConfiguration = DefaultPlanarConfiguration; options.PlanarConfiguration = DefaultPlanarConfiguration;
} }
options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None; options.Predictor = frameMetadata.Predictor;
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation;
options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger;
options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; options.BitsPerPixel = (int)frameMetadata.BitsPerPixel;
options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); options.BitsPerSample = frameMetadata.BitsPerSample;
if (exifProfile.TryGetValue(ExifTag.ReferenceBlackWhite, out IExifValue<Rational[]> blackWhiteValue)) if (exifProfile.TryGetValue(ExifTag.ReferenceBlackWhite, out IExifValue<Rational[]> blackWhiteValue))
{ {
@ -142,9 +142,7 @@ internal static class TiffDecoderOptionsParser
options.ParseCompression(frameMetadata.Compression, exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile);
options.ParseColorType(exifProfile); options.ParseColorType(exifProfile);
bool isTiled = VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration); return VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration);
return isTiled;
} }
/// <summary> /// <summary>
@ -194,13 +192,6 @@ internal static class TiffDecoderOptionsParser
} }
} }
// For BiColor compressed images, the BitsPerPixel value will be set explicitly to 1, so we don't throw in those cases.
// See: https://github.com/SixLabors/ImageSharp/issues/2587
if (frameMetadata.BitsPerPixel == null && !IsBiColorCompression(frameMetadata.Compression))
{
TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing which is required to decode the image!");
}
return isTiled; return isTiled;
} }
@ -224,7 +215,6 @@ internal static class TiffDecoderOptionsParser
switch (bitsPerChannel) switch (bitsPerChannel)
{ {
case 32: case 32:
{
if (options.SampleFormat == TiffSampleFormat.Float) if (options.SampleFormat == TiffSampleFormat.Float)
{ {
options.ColorType = TiffColorType.WhiteIsZero32Float; options.ColorType = TiffColorType.WhiteIsZero32Float;
@ -233,43 +223,30 @@ internal static class TiffDecoderOptionsParser
options.ColorType = TiffColorType.WhiteIsZero32; options.ColorType = TiffColorType.WhiteIsZero32;
break; break;
}
case 24: case 24:
{
options.ColorType = TiffColorType.WhiteIsZero24; options.ColorType = TiffColorType.WhiteIsZero24;
break; break;
}
case 16: case 16:
{
options.ColorType = TiffColorType.WhiteIsZero16; options.ColorType = TiffColorType.WhiteIsZero16;
break; break;
}
case 8: case 8:
{
options.ColorType = TiffColorType.WhiteIsZero8; options.ColorType = TiffColorType.WhiteIsZero8;
break; break;
}
case 4: case 4:
{
options.ColorType = TiffColorType.WhiteIsZero4; options.ColorType = TiffColorType.WhiteIsZero4;
break; break;
}
case 1: case 1:
{
options.ColorType = TiffColorType.WhiteIsZero1; options.ColorType = TiffColorType.WhiteIsZero1;
break; break;
}
default: default:
{
options.ColorType = TiffColorType.WhiteIsZero; options.ColorType = TiffColorType.WhiteIsZero;
break; break;
}
} }
break; break;
@ -291,7 +268,6 @@ internal static class TiffDecoderOptionsParser
switch (bitsPerChannel) switch (bitsPerChannel)
{ {
case 32: case 32:
{
if (options.SampleFormat == TiffSampleFormat.Float) if (options.SampleFormat == TiffSampleFormat.Float)
{ {
options.ColorType = TiffColorType.BlackIsZero32Float; options.ColorType = TiffColorType.BlackIsZero32Float;
@ -300,43 +276,30 @@ internal static class TiffDecoderOptionsParser
options.ColorType = TiffColorType.BlackIsZero32; options.ColorType = TiffColorType.BlackIsZero32;
break; break;
}
case 24: case 24:
{
options.ColorType = TiffColorType.BlackIsZero24; options.ColorType = TiffColorType.BlackIsZero24;
break; break;
}
case 16: case 16:
{
options.ColorType = TiffColorType.BlackIsZero16; options.ColorType = TiffColorType.BlackIsZero16;
break; break;
}
case 8: case 8:
{
options.ColorType = TiffColorType.BlackIsZero8; options.ColorType = TiffColorType.BlackIsZero8;
break; break;
}
case 4: case 4:
{
options.ColorType = TiffColorType.BlackIsZero4; options.ColorType = TiffColorType.BlackIsZero4;
break; break;
}
case 1: case 1:
{
options.ColorType = TiffColorType.BlackIsZero1; options.ColorType = TiffColorType.BlackIsZero1;
break; break;
}
default: default:
{
options.ColorType = TiffColorType.BlackIsZero; options.ColorType = TiffColorType.BlackIsZero;
break; break;
}
} }
break; break;
@ -535,29 +498,21 @@ internal static class TiffDecoderOptionsParser
switch (compression ?? TiffCompression.None) switch (compression ?? TiffCompression.None)
{ {
case TiffCompression.None: case TiffCompression.None:
{
options.CompressionType = TiffDecoderCompressionType.None; options.CompressionType = TiffDecoderCompressionType.None;
break; break;
}
case TiffCompression.PackBits: case TiffCompression.PackBits:
{
options.CompressionType = TiffDecoderCompressionType.PackBits; options.CompressionType = TiffDecoderCompressionType.PackBits;
break; break;
}
case TiffCompression.Deflate: case TiffCompression.Deflate:
case TiffCompression.OldDeflate: case TiffCompression.OldDeflate:
{
options.CompressionType = TiffDecoderCompressionType.Deflate; options.CompressionType = TiffDecoderCompressionType.Deflate;
break; break;
}
case TiffCompression.Lzw: case TiffCompression.Lzw:
{
options.CompressionType = TiffDecoderCompressionType.Lzw; options.CompressionType = TiffDecoderCompressionType.Lzw;
break; break;
}
case TiffCompression.CcittGroup3Fax: case TiffCompression.CcittGroup3Fax:
{ {
@ -599,16 +554,13 @@ internal static class TiffDecoderOptionsParser
} }
case TiffCompression.Ccitt1D: case TiffCompression.Ccitt1D:
{
options.CompressionType = TiffDecoderCompressionType.HuffmanRle; options.CompressionType = TiffDecoderCompressionType.HuffmanRle;
options.BitsPerSample = new TiffBitsPerSample(1, 0, 0); options.BitsPerSample = new TiffBitsPerSample(1, 0, 0);
options.BitsPerPixel = 1; options.BitsPerPixel = 1;
break; break;
}
case TiffCompression.OldJpeg: case TiffCompression.OldJpeg:
{
if (!options.OldJpegCompressionStartOfImageMarker.HasValue) if (!options.OldJpegCompressionStartOfImageMarker.HasValue)
{ {
TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression"); TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression");
@ -629,10 +581,8 @@ internal static class TiffDecoderOptionsParser
} }
break; break;
}
case TiffCompression.Jpeg: case TiffCompression.Jpeg:
{
options.CompressionType = TiffDecoderCompressionType.Jpeg; options.CompressionType = TiffDecoderCompressionType.Jpeg;
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null) if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null)
@ -643,30 +593,14 @@ internal static class TiffDecoderOptionsParser
} }
break; break;
}
case TiffCompression.Webp: case TiffCompression.Webp:
{
options.CompressionType = TiffDecoderCompressionType.Webp; options.CompressionType = TiffDecoderCompressionType.Webp;
break; break;
}
default: default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported");
break; break;
}
} }
} }
private static bool IsBiColorCompression(TiffCompression? compression)
{
if (compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or
TiffCompression.CcittGroup4Fax)
{
return true;
}
return false;
}
} }

2
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -47,7 +47,7 @@ public class TiffEncoder : QuantizingImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
TiffEncoderCore encode = new(this, image.Configuration.MemoryAllocator); TiffEncoderCore encode = new(this, image.Configuration);
encode.Encode(image, stream, cancellationToken); encode.Encode(image, stream, cancellationToken);
} }
} }

245
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -1,8 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Advanced; using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Performs the TIFF encoding operation. /// Performs the TIFF encoding operation.
/// </summary> /// </summary>
internal sealed class TiffEncoderCore : IImageEncoderInternals internal sealed class TiffEncoderCore
{ {
private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian
? TiffConstants.ByteOrderLittleEndianShort ? TiffConstants.ByteOrderLittleEndianShort
@ -50,41 +49,22 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly DeflateCompressionLevel compressionLevel; private readonly DeflateCompressionLevel compressionLevel;
/// <summary>
/// The default predictor is None.
/// </summary>
private const TiffPredictor DefaultPredictor = TiffPredictor.None;
/// <summary>
/// The default bits per pixel is Bit24.
/// </summary>
private const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;
/// <summary>
/// The default compression is None.
/// </summary>
private const TiffCompression DefaultCompression = TiffCompression.None;
/// <summary>
/// The default photometric interpretation is Rgb.
/// </summary>
private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
/// <summary> /// <summary>
/// Whether to skip metadata during encoding. /// Whether to skip metadata during encoding.
/// </summary> /// </summary>
private readonly bool skipMetadata; private readonly bool skipMetadata;
private readonly List<(long, uint)> frameMarkers = new(); private readonly List<(long, uint)> frameMarkers = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class. /// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The options for the encoder.</param> /// <param name="options">The options for the encoder.</param>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="configuration">The global configuration.</param>
public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator) public TiffEncoderCore(TiffEncoder options, Configuration configuration)
{ {
this.memoryAllocator = memoryAllocator; this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation; this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy; this.pixelSamplingStrategy = options.PixelSamplingStrategy;
@ -135,35 +115,29 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
// Determine the correct values to encode with. // Determine the correct values to encode with.
// EncoderOptions > Metadata > Default. // EncoderOptions > Metadata > Default.
TiffBitsPerPixel? bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; TiffBitsPerPixel bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel;
TiffPhotometricInterpretation? photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; TiffPhotometricInterpretation photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation;
TiffPredictor predictor = TiffPredictor predictor = this.HorizontalPredictor ?? rootFrameTiffMetaData.Predictor;
this.HorizontalPredictor
?? rootFrameTiffMetaData.Predictor
?? DefaultPredictor;
TiffCompression compression = TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression;
this.CompressionType
?? rootFrameTiffMetaData.Compression
?? DefaultCompression;
// Make sure, the Encoder options makes sense in combination with each other. // Make sure the Encoder options makes sense in combination with each other.
this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor);
using TiffStreamWriter writer = new(stream); using TiffStreamWriter writer = new(stream);
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
long ifdMarker = WriteHeader(writer, buffer); long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel> metadataImage = image; Image<TPixel>? metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
metadataImage = null; metadataImage = null;
} }
@ -199,6 +173,8 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// <param name="frame">The tiff frame.</param> /// <param name="frame">The tiff frame.</param>
/// <param name="imageMetadata">The image metadata (resolution values for each frame).</param> /// <param name="imageMetadata">The image metadata (resolution values for each frame).</param>
/// <param name="image">The image (common metadata for root frame).</param> /// <param name="image">The image (common metadata for root frame).</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="compression">The compression type.</param>
/// <param name="ifdOffset">The marker to write this IFD offset.</param> /// <param name="ifdOffset">The marker to write this IFD offset.</param>
/// <returns> /// <returns>
/// The next IFD offset value. /// The next IFD offset value.
@ -207,16 +183,18 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
TiffStreamWriter writer, TiffStreamWriter writer,
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
ImageMetadata imageMetadata, ImageMetadata imageMetadata,
Image<TPixel> image, Image<TPixel>? image,
TiffBitsPerPixel bitsPerPixel,
TiffCompression compression,
long ifdOffset) long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using TiffBaseCompressor compressor = TiffCompressorFactory.Create( using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
this.CompressionType ?? TiffCompression.None, compression,
writer.BaseStream, writer.BaseStream,
this.memoryAllocator, this.memoryAllocator,
frame.Width, frame.Width,
(int)this.BitsPerPixel, (int)bitsPerPixel,
this.compressionLevel, this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@ -229,7 +207,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
entriesCollector, entriesCollector,
(int)this.BitsPerPixel); (int)bitsPerPixel);
int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
@ -307,7 +285,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
} }
uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12));
List<byte[]> largeDataBlocks = new(); List<byte[]> largeDataBlocks = [];
entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag);
@ -354,135 +332,87 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return nextIfdMarker; return nextIfdMarker;
} }
[MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))]
private void SanitizeAndSetEncoderOptions( private void SanitizeAndSetEncoderOptions(
TiffBitsPerPixel? bitsPerPixel, TiffBitsPerPixel bitsPerPixel,
int inputBitsPerPixel, TiffPhotometricInterpretation photometricInterpretation,
TiffPhotometricInterpretation? photometricInterpretation,
TiffCompression compression, TiffCompression compression,
TiffPredictor predictor) TiffPredictor predictor)
{ {
// BitsPerPixel should be the primary source of truth for the encoder options. // Ensure 1 Bit compression is only used with 1 bit pixel type.
if (bitsPerPixel.HasValue) // Choose a sensible default based on the bits per pixel.
if (IsOneBitCompression(compression) && bitsPerPixel != TiffBitsPerPixel.Bit1)
{ {
switch (bitsPerPixel) compression = bitsPerPixel switch
{ {
case TiffBitsPerPixel.Bit1: < TiffBitsPerPixel.Bit8 => TiffCompression.None,
if (IsOneBitCompression(compression)) _ => TiffCompression.Deflate,
{ };
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None);
break;
}
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None);
break;
case TiffBitsPerPixel.Bit4:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None);
break;
case TiffBitsPerPixel.Bit8:
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit16:
// Assume desire to encode as L16 grayscale
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit10:
case TiffBitsPerPixel.Bit12:
case TiffBitsPerPixel.Bit14:
case TiffBitsPerPixel.Bit30:
case TiffBitsPerPixel.Bit36:
case TiffBitsPerPixel.Bit42:
case TiffBitsPerPixel.Bit48:
// Encoding not yet supported bits per pixel will default to 24 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None);
break;
case TiffBitsPerPixel.Bit64:
// Encoding not yet supported bits per pixel will default to 32 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None);
break;
default:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;
}
// Make sure 1 Bit compression is only used with 1 bit pixel type.
if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1)
{
// Invalid compression / bits per pixel combination, fallback to no compression.
this.CompressionType = DefaultCompression;
}
return;
} }
// If no photometric interpretation was chosen, the input image bit per pixel should be preserved. // Ensure predictor is only used with compression that supports it.
if (!photometricInterpretation.HasValue) predictor = HasPredictor(compression) ? predictor : TiffPredictor.None;
{
if (IsOneBitCompression(this.CompressionType))
{
// We need to make sure bits per pixel is set to Bit1 now. WhiteIsZero is set because its the default for bilevel compressed data.
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None);
return;
}
// At the moment only 8, 16 and 32 bits per pixel can be preserved by the tiff encoder. // BitsPerPixel should be the primary source of truth for the encoder options.
if (inputBitsPerPixel == 8) switch (bitsPerPixel)
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
return;
}
if (inputBitsPerPixel == 16)
{
// Assume desire to encode as L16 grayscale
this.SetEncoderOptions(TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
return;
}
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor);
return;
}
switch (photometricInterpretation)
{ {
case TiffPhotometricInterpretation.BlackIsZero: case TiffBitsPerPixel.Bit1:
case TiffPhotometricInterpretation.WhiteIsZero: if (IsOneBitCompression(compression))
if (IsOneBitCompression(this.CompressionType))
{ {
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None); // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
return; this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, predictor);
} break;
if (inputBitsPerPixel == 16)
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit16, photometricInterpretation, compression, predictor);
return;
} }
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
return; break;
case TiffBitsPerPixel.Bit4:
case TiffPhotometricInterpretation.PaletteColor: this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor);
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); break;
return; case TiffBitsPerPixel.Bit8:
case TiffPhotometricInterpretation.Rgb: // Allow any combination of the below for 8 bit images.
// Make sure 1 Bit compression is only used with 1 bit pixel type. if (photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero
if (IsOneBitCompression(this.CompressionType)) or TiffPhotometricInterpretation.WhiteIsZero
or TiffPhotometricInterpretation.PaletteColor)
{ {
// Invalid compression / bits per pixel combination, fallback to no compression. this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor);
compression = DefaultCompression; break;
} }
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor); this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor);
return; break;
case TiffBitsPerPixel.Bit16:
// Assume desire to encode as L16 grayscale
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit10:
case TiffBitsPerPixel.Bit12:
case TiffBitsPerPixel.Bit14:
case TiffBitsPerPixel.Bit30:
case TiffBitsPerPixel.Bit36:
case TiffBitsPerPixel.Bit42:
case TiffBitsPerPixel.Bit48:
// Encoding not yet supported bits per pixel will default to 24 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;
case TiffBitsPerPixel.Bit64:
// Encoding not yet supported bits per pixel will default to 32 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;
default:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;
} }
this.SetEncoderOptions(DefaultBitsPerPixel, DefaultPhotometricInterpretation, compression, predictor);
} }
private void SetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))]
private void SetEncoderOptions(
TiffBitsPerPixel bitsPerPixel,
TiffPhotometricInterpretation photometricInterpretation,
TiffCompression compression,
TiffPredictor predictor)
{ {
this.BitsPerPixel = bitsPerPixel; this.BitsPerPixel = bitsPerPixel;
this.PhotometricInterpretation = photometricInterpretation; this.PhotometricInterpretation = photometricInterpretation;
@ -492,4 +422,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
public static bool IsOneBitCompression(TiffCompression? compression) public static bool IsOneBitCompression(TiffCompression? compression)
=> compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or TiffCompression.CcittGroup4Fax; => compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or TiffCompression.CcittGroup4Fax;
public static bool HasPredictor(TiffCompression? compression)
=> compression is TiffCompression.Deflate or TiffCompression.Lzw;
} }

32
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Provides Tiff specific metadata information for the frame. /// Provides Tiff specific metadata information for the frame.
/// </summary> /// </summary>
public class TiffFrameMetadata : IDeepCloneable public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class. /// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
@ -34,33 +34,47 @@ public class TiffFrameMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the bits per pixel. /// Gets or sets the bits per pixel.
/// </summary> /// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; } public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel;
/// <summary> /// <summary>
/// Gets or sets number of bits per component. /// Gets or sets number of bits per component.
/// </summary> /// </summary>
public TiffBitsPerSample? BitsPerSample { get; set; } public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample;
/// <summary> /// <summary>
/// Gets or sets the compression scheme used on the image data. /// Gets or sets the compression scheme used on the image data.
/// </summary> /// </summary>
public TiffCompression? Compression { get; set; } public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression;
/// <summary> /// <summary>
/// Gets or sets the color space of the image data. /// Gets or sets the color space of the image data.
/// </summary> /// </summary>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation;
/// <summary> /// <summary>
/// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied.
/// </summary> /// </summary>
public TiffPredictor? Predictor { get; set; } public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor;
/// <summary> /// <summary>
/// Gets or sets the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image. /// Gets or sets the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image.
/// </summary> /// </summary>
public TiffInkSet? InkSet { get; set; } public TiffInkSet? InkSet { get; set; }
/// <inheritdoc/>
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
=> new();
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new();
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public TiffFrameMetadata DeepClone() => new(this);
/// <summary> /// <summary>
/// Returns a new <see cref="TiffFrameMetadata"/> instance parsed from the given Exif profile. /// Returns a new <see cref="TiffFrameMetadata"/> instance parsed from the given Exif profile.
/// </summary> /// </summary>
@ -89,7 +103,7 @@ public class TiffFrameMetadata : IDeepCloneable
meta.BitsPerSample = bitsPerSample; meta.BitsPerSample = bitsPerSample;
} }
meta.BitsPerPixel = meta.BitsPerSample?.BitsPerPixel(); meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue)) if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{ {
@ -111,13 +125,11 @@ public class TiffFrameMetadata : IDeepCloneable
meta.InkSet = (TiffInkSet)inkSetValue.Value; meta.InkSet = (TiffInkSet)inkSetValue.Value;
} }
// TODO: Why do we remove this? Encoding should overwrite.
profile.RemoveValue(ExifTag.BitsPerSample); profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression); profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation); profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor); profile.RemoveValue(ExifTag.Predictor);
} }
} }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffFrameMetadata(this);
} }

152
src/ImageSharp/Formats/Tiff/TiffMetadata.cs

@ -1,12 +1,15 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Provides Tiff specific metadata information for the image. /// Provides Tiff specific metadata information for the image.
/// </summary> /// </summary>
public class TiffMetadata : IDeepCloneable public class TiffMetadata : IFormatMetadata<TiffMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffMetadata"/> class. /// Initializes a new instance of the <see cref="TiffMetadata"/> class.
@ -23,6 +26,11 @@ public class TiffMetadata : IDeepCloneable
{ {
this.ByteOrder = other.ByteOrder; this.ByteOrder = other.ByteOrder;
this.FormatType = other.FormatType; this.FormatType = other.FormatType;
this.BitsPerPixel = other.BitsPerPixel;
this.BitsPerSample = other.BitsPerSample;
this.Compression = other.Compression;
this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor;
} }
/// <summary> /// <summary>
@ -35,6 +43,146 @@ public class TiffMetadata : IDeepCloneable
/// </summary> /// </summary>
public TiffFormatType FormatType { get; set; } public TiffFormatType FormatType { get; set; }
/// <summary>
/// Gets or sets the bits per pixel. Derived from the root frame.
/// </summary>
public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel;
/// <summary>
/// Gets or sets number of bits per component. Derived from the root frame.
/// </summary>
public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample;
/// <summary>
/// Gets or sets the compression scheme used on the image data. Derived from the root frame.
/// </summary>
public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression;
/// <summary>
/// Gets or sets the color space of the image data. Derived from the root frame.
/// </summary>
public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation;
/// <summary>
/// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied.
/// Derived from the root frame.
/// </summary>
public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor;
/// <inheritdoc/>
public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int bpp = metadata.PixelTypeInfo.BitsPerPixel;
return bpp switch
{
1 => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit1,
BitsPerSample = TiffConstants.BitsPerSample1Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero,
Compression = TiffCompression.CcittGroup4Fax,
Predictor = TiffPredictor.None
},
<= 4 => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit4,
BitsPerSample = TiffConstants.BitsPerSample4Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor,
Compression = TiffCompression.Deflate,
Predictor = TiffPredictor.None // Best match for low bit depth
},
8 => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit8,
BitsPerSample = TiffConstants.BitsPerSample8Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor,
Compression = TiffCompression.Deflate,
Predictor = TiffPredictor.Horizontal
},
16 => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit16,
BitsPerSample = TiffConstants.BitsPerSample16Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero,
Compression = TiffCompression.Deflate,
Predictor = TiffPredictor.Horizontal
},
32 or 64 => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit32,
BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.Rgb,
Compression = TiffCompression.Deflate,
Predictor = TiffPredictor.Horizontal
},
_ => new TiffMetadata
{
BitsPerPixel = TiffBitsPerPixel.Bit24,
BitsPerSample = TiffConstants.BitsPerSampleRgb8Bit,
PhotometricInterpretation = TiffPhotometricInterpretation.Rgb,
Compression = TiffCompression.Deflate,
Predictor = TiffPredictor.Horizontal
}
};
}
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BitsPerPixel;
TiffBitsPerSample samples = this.BitsPerSample;
PixelComponentInfo info = samples.Channels switch
{
1 => PixelComponentInfo.Create(1, bpp, bpp),
2 => PixelComponentInfo.Create(2, bpp, bpp, samples.Channel0, samples.Channel1),
3 => PixelComponentInfo.Create(3, bpp, samples.Channel0, samples.Channel1, samples.Channel2),
_ => PixelComponentInfo.Create(4, bpp, samples.Channel0, samples.Channel1, samples.Channel2, samples.Channel3)
};
PixelColorType colorType;
PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None;
switch (this.BitsPerPixel)
{
case TiffBitsPerPixel.Bit1:
colorType = PixelColorType.Binary;
break;
case TiffBitsPerPixel.Bit4:
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit8:
colorType = PixelColorType.Indexed;
break;
case TiffBitsPerPixel.Bit16:
colorType = PixelColorType.Luminance;
break;
case TiffBitsPerPixel.Bit32:
case TiffBitsPerPixel.Bit64:
colorType = PixelColorType.RGB | PixelColorType.Alpha;
alpha = PixelAlphaRepresentation.Unassociated;
break;
default:
colorType = PixelColorType.RGB;
break;
}
return new PixelTypeInfo(bpp)
{
ColorType = colorType,
ComponentInfo = info,
AlphaRepresentation = alpha
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this); public TiffMetadata DeepClone() => new(this);
} }

20
src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs

@ -10,7 +10,7 @@ internal readonly struct WebpFrameData
/// </summary> /// </summary>
public const uint HeaderSize = 16; public const uint HeaderSize = 16;
public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod) public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod)
{ {
this.DataSize = dataSize; this.DataSize = dataSize;
this.X = x; this.X = x;
@ -30,12 +30,12 @@ internal readonly struct WebpFrameData
width, width,
height, height,
duration, duration,
(flags & 2) == 0 ? WebpBlendMethod.Over : WebpBlendMethod.Source, (flags & 2) == 0 ? FrameBlendMode.Over : FrameBlendMode.Source,
(flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose) (flags & 1) == 1 ? FrameDisposalMode.RestoreToBackground : FrameDisposalMode.DoNotDispose)
{ {
} }
public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod) public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, FrameBlendMode blendingMethod, FrameDisposalMode disposalMethod)
: this(0, x, y, width, height, duration, blendingMethod, disposalMethod) : this(0, x, y, width, height, duration, blendingMethod, disposalMethod)
{ {
} }
@ -74,12 +74,12 @@ internal readonly struct WebpFrameData
/// <summary> /// <summary>
/// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary> /// </summary>
public WebpBlendMethod BlendingMethod { get; } public FrameBlendMode BlendingMethod { get; }
/// <summary> /// <summary>
/// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary> /// </summary>
public WebpDisposalMethod DisposalMethod { get; } public FrameDisposalMode DisposalMethod { get; }
public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height); public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height);
@ -91,13 +91,13 @@ internal readonly struct WebpFrameData
{ {
byte flags = 0; byte flags = 0;
if (this.BlendingMethod is WebpBlendMethod.Source) if (this.BlendingMethod is FrameBlendMode.Source)
{ {
// Set blending flag. // Set blending flag.
flags |= 2; flags |= 2;
} }
if (this.DisposalMethod is WebpDisposalMethod.RestoreToBackground) if (this.DisposalMethod is FrameDisposalMode.RestoreToBackground)
{ {
// Set disposal flag. // Set disposal flag.
flags |= 1; flags |= 1;
@ -124,7 +124,7 @@ internal readonly struct WebpFrameData
{ {
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
WebpFrameData data = new( return new(
dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2,
y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2,
@ -132,7 +132,5 @@ internal readonly struct WebpFrameData
height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
flags: stream.ReadByte()); flags: stream.ReadByte());
return data;
} }
} }

4
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -241,8 +241,6 @@ internal class Vp8LEncoder : IDisposable
{ {
// Write bytes from the bit-writer buffer to the stream. // Write bytes from the bit-writer buffer to the stream.
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
@ -259,7 +257,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation) if (hasAnimation)
{ {
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
} }

8
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -315,8 +315,6 @@ internal class Vp8Encoder : IDisposable
{ {
// Write bytes from the bitwriter buffer to the stream. // Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
@ -332,7 +330,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation) if (hasAnimation)
{ {
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
} }
@ -376,7 +374,7 @@ internal class Vp8Encoder : IDisposable
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrame<TPixel> frame = image.Frames.RootFrame; ImageFrame<TPixel> frame = image.Frames.RootFrame;
this.Encode(stream, frame, image.Bounds, WebpCommonUtils.GetWebpFrameMetadata(frame), false, image); this.Encode(stream, frame, image.Bounds, frame.Metadata.GetWebpMetadata(), false, image);
} }
/// <summary> /// <summary>
@ -462,7 +460,7 @@ internal class Vp8Encoder : IDisposable
// Extract and encode alpha channel data, if present. // Extract and encode alpha channel data, if present.
int alphaDataSize = 0; int alphaDataSize = 0;
bool alphaCompressionSucceeded = false; bool alphaCompressionSucceeded = false;
Span<byte> alphaData = Span<byte>.Empty; Span<byte> alphaData = [];
IMemoryOwner<byte> encodedAlphaData = null; IMemoryOwner<byte> encodedAlphaData = null;
try try
{ {

75
src/ImageSharp/Formats/Webp/MetadataExtensions.cs

@ -1,75 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp;
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 webp format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="WebpMetadata"/>.</returns>
public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the webp format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the webp metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetWebpMetadata(this ImageMetadata source, [NotNullWhen(true)] out WebpMetadata? metadata)
=> source.TryGetFormatMetadata(WebpFormat.Instance, out metadata);
/// <summary>
/// Gets the webp format specific metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="WebpFrameMetadata"/>.</returns>
public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the webp format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the webp frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetWebpFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out WebpFrameMetadata? metadata)
=> source.TryGetFormatMetadata(WebpFormat.Instance, out metadata);
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this WebpMetadata source)
=> new()
{
ColorTableMode = FrameColorTableMode.Global,
RepeatCount = source.RepeatCount,
BackgroundColor = source.BackgroundColor
};
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this WebpFrameMetadata source)
=> new()
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(source.FrameDelay),
DisposalMode = GetMode(source.DisposalMethod),
BlendMode = source.BlendMethod == WebpBlendMethod.Over ? FrameBlendMode.Over : FrameBlendMode.Source,
};
private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch
{
WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
WebpDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
_ => FrameDisposalMode.DoNotDispose,
};
}

4
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -195,14 +195,14 @@ internal class WebpAnimationDecoder : IDisposable
Rectangle regionRectangle = frameData.Bounds; Rectangle regionRectangle = frameData.Bounds;
if (frameData.DisposalMethod is WebpDisposalMethod.RestoreToBackground) if (frameData.DisposalMethod is FrameDisposalMode.RestoreToBackground)
{ {
this.RestoreToBackground(imageFrame, backgroundColor); this.RestoreToBackground(imageFrame, backgroundColor);
} }
using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo); using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over; bool blend = previousFrame != null && frameData.BlendingMethod == FrameBlendMode.Over;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame; previousFrame = currentFrame ?? image.Frames.RootFrame;

4
src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs

@ -11,10 +11,10 @@ public enum WebpBitsPerPixel : short
/// <summary> /// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes. /// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary> /// </summary>
Pixel24 = 24, Bit24 = 24,
/// <summary> /// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes (an alpha channel is present). /// 32 bits per pixel. Each pixel consists of 4 bytes (an alpha channel is present).
/// </summary> /// </summary>
Pixel32 = 32 Bit32 = 32
} }

22
src/ImageSharp/Formats/Webp/WebpBlendMethod.cs

@ -1,22 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public enum WebpBlendMethod
{
/// <summary>
/// Do not blend. After disposing of the previous frame,
/// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
/// </summary>
Source = 0,
/// <summary>
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
/// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
/// </summary>
Over = 1,
}

33
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -17,6 +17,10 @@ internal static class WebpChunkParsingUtils
/// <summary> /// <summary>
/// Reads the header of a lossy webp image. /// Reads the header of a lossy webp image.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="stream">The buffered read stream.</param>
/// <param name="buffer">The scratch buffer to use while reading.</param>
/// <param name="features">The webp features to parse.</param>
/// <returns>Information about this webp image.</returns> /// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features) public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
@ -114,13 +118,15 @@ internal static class WebpChunkParsingUtils
Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
return new WebpImageInfo return new()
{ {
Width = width, Width = width,
Height = height, Height = height,
XScale = xScale, XScale = xScale,
YScale = yScale, YScale = yScale,
BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
// Vp8 header can be parsed during the processing of the Vp8X header.
BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24,
IsLossless = false, IsLossless = false,
Features = features, Features = features,
Vp8Profile = (sbyte)version, Vp8Profile = (sbyte)version,
@ -132,7 +138,10 @@ internal static class WebpChunkParsingUtils
/// <summary> /// <summary>
/// Reads the header of a lossless webp image. /// Reads the header of a lossless webp image.
/// </summary> /// </summary>
/// <returns>Information about this image.</returns> /// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="stream">The buffered read stream.</param>
/// <param name="buffer">The scratch buffer to use while reading.</param>
/// <param name="features">The webp features to parse.</param>
public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features) public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
// VP8 data size. // VP8 data size.
@ -156,8 +165,8 @@ internal static class WebpChunkParsingUtils
} }
// The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
// TODO: this flag value is not used yet // Alpha may have already been set by the VP8X chunk.
bool alphaIsUsed = bitReader.ReadBit(); features.Alpha |= bitReader.ReadBit();
// The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0.
// Any other value should be treated as an error. // Any other value should be treated as an error.
@ -167,11 +176,11 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
} }
return new WebpImageInfo return new()
{ {
Width = width, Width = width,
Height = height, Height = height,
BitsPerPixel = WebpBitsPerPixel.Pixel32, BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24,
IsLossless = true, IsLossless = true,
Features = features, Features = features,
Vp8LBitReader = bitReader Vp8LBitReader = bitReader
@ -187,6 +196,9 @@ internal static class WebpChunkParsingUtils
/// - An optional 'ALPH' chunk with alpha channel data. /// - An optional 'ALPH' chunk with alpha channel data.
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
/// </summary> /// </summary>
/// <param name="stream">The buffered read stream.</param>
/// <param name="buffer">The scratch buffer to use while reading.</param>
/// <param name="features">The webp features to parse.</param>
/// <returns>Information about this webp image.</returns> /// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span<byte> buffer, WebpFeatures features) public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
@ -217,6 +229,7 @@ internal static class WebpChunkParsingUtils
features.Animation = (imageFeatures & (1 << 1)) != 0; features.Animation = (imageFeatures & (1 << 1)) != 0;
// 3 reserved bytes should follow which are supposed to be zero. // 3 reserved bytes should follow which are supposed to be zero.
// No other decoder actually checks this though.
stream.Read(buffer, 0, 3); stream.Read(buffer, 0, 3);
// 3 bytes for the width. // 3 bytes for the width.
@ -226,14 +239,14 @@ internal static class WebpChunkParsingUtils
uint height = ReadUInt24LittleEndian(stream, buffer) + 1; uint height = ReadUInt24LittleEndian(stream, buffer) + 1;
// Read all the chunks in the order they occur. // Read all the chunks in the order they occur.
WebpImageInfo info = new() return new()
{ {
Width = width, Width = width,
Height = height, Height = height,
Features = features Features = features
};
return info; // Additional properties are set during the parsing of the VP8 or VP8L headers.
};
} }
/// <summary> /// <summary>

25
src/ImageSharp/Formats/Webp/WebpColorType.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Provides enumeration of the various webp color types.
/// </summary>
public enum WebpColorType
{
/// <summary>
/// Yuv (luminance, blue chroma, red chroma) as defined in the ITU-R Rec. BT.709 specification.
/// </summary>
Yuv,
/// <summary>
/// Rgb color space.
/// </summary>
Rgb,
/// <summary>
/// Rgba color space.
/// </summary>
Rgba
}

50
src/ImageSharp/Formats/Webp/WebpCommonUtils.cs

@ -4,8 +4,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
@ -15,54 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// </summary> /// </summary>
internal static class WebpCommonUtils internal static class WebpCommonUtils
{ {
public static WebpMetadata GetWebpMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
return (WebpMetadata)webp.DeepClone();
}
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
return WebpMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
return WebpMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
public static WebpFrameMetadata GetWebpFrameMetadata<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
return (WebpFrameMetadata)webp.DeepClone();
}
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
return WebpFrameMetadata.FromAnimatedMetadata(ani);
}
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
return WebpFrameMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
/// <summary> /// <summary>
/// Checks if the pixel row is not opaque. /// Checks if the pixel row is not opaque.
/// </summary> /// </summary>

37
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Performs the webp decoding operation. /// Performs the webp decoding operation.
/// </summary> /// </summary>
internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
{ {
/// <summary> /// <summary>
/// General configuration options. /// General configuration options.
@ -61,8 +61,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public WebpDecoderCore(WebpDecoderOptions options) public WebpDecoderCore(WebpDecoderOptions options)
: base(options.GeneralOptions)
{ {
this.Options = options.GeneralOptions;
this.backgroundColorHandling = options.BackgroundColorHandling; this.backgroundColorHandling = options.BackgroundColorHandling;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
@ -70,15 +70,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
} }
/// <inheritdoc/>
public DecoderOptions Options { get; }
/// <inheritdoc/>
public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height);
/// <inheritdoc /> /// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel>? image = null; Image<TPixel>? image = null;
try try
@ -136,7 +129,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
ReadImageHeader(stream, stackalloc byte[4]); ReadImageHeader(stream, stackalloc byte[4]);
@ -144,7 +137,6 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true))
{ {
return new ImageInfo( return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
metadata); metadata);
} }
@ -186,36 +178,43 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
WebpImageInfo? info = null;
WebpFeatures features = new(); WebpFeatures features = new();
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Vp8: case WebpChunkType.Vp8:
info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features);
webpMetadata.FileFormat = WebpFileFormatType.Lossy; webpMetadata.FileFormat = WebpFileFormatType.Lossy;
return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); webpMetadata.ColorType = WebpColorType.Yuv;
return info;
case WebpChunkType.Vp8L: case WebpChunkType.Vp8L:
info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
webpMetadata.FileFormat = WebpFileFormatType.Lossless; webpMetadata.FileFormat = WebpFileFormatType.Lossless;
return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb;
return info;
case WebpChunkType.Vp8X: case WebpChunkType.Vp8X:
WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); info = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features);
while (stream.Position < stream.Length) while (stream.Position < stream.Length)
{ {
chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Vp8) if (chunkType == WebpChunkType.Vp8)
{ {
info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features);
webpMetadata.FileFormat = WebpFileFormatType.Lossy; webpMetadata.FileFormat = WebpFileFormatType.Lossy;
webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb;
} }
else if (chunkType == WebpChunkType.Vp8L) else if (chunkType == WebpChunkType.Vp8L)
{ {
info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
webpMetadata.FileFormat = WebpFileFormatType.Lossless; webpMetadata.FileFormat = WebpFileFormatType.Lossless;
webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb;
} }
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
{ {
bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer);
if (isAnimationChunk) if (isAnimationChunk)
{ {
return webpInfos; return info;
} }
} }
else else
@ -226,7 +225,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
} }
} }
return webpInfos; return info;
default: default:
WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header");
return return

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save