Browse Source

merge

pull/2740/head
ardabada 2 years ago
parent
commit
3b46ce14ae
  1. 39
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 94
      src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
  3. 33
      src/ImageSharp/Formats/AnimatedImageMetadata.cs
  4. 14
      src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
  5. 19
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  6. 1
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  7. 26
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  8. 119
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  9. 20
      src/ImageSharp/Formats/Bmp/MetadataExtensions.cs
  10. 14
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  11. 153
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  12. 153
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  13. 45
      src/ImageSharp/Formats/Cur/MetadataExtensions.cs
  14. 5
      src/ImageSharp/Formats/DecoderOptions.cs
  15. 20
      src/ImageSharp/Formats/EncodingType.cs
  16. 54
      src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
  17. 70
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  18. 23
      src/ImageSharp/Formats/FrameBlendMode.cs
  19. 20
      src/ImageSharp/Formats/FrameColorTableMode.cs
  20. 38
      src/ImageSharp/Formats/FrameDisposalMode.cs
  21. 20
      src/ImageSharp/Formats/Gif/GifColorTableMode.cs
  22. 32
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  23. 37
      src/ImageSharp/Formats/Gif/GifDisposalMethod.cs
  24. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  25. 77
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  26. 55
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  27. 64
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  28. 105
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  29. 6
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  30. 44
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  31. 49
      src/ImageSharp/Formats/IFormatMetadata.cs
  32. 50
      src/ImageSharp/Formats/IImageDecoderInternals.cs
  33. 22
      src/ImageSharp/Formats/IImageEncoderInternals.cs
  34. 6
      src/ImageSharp/Formats/IImageFormat.cs
  35. 14
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  36. 153
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  37. 151
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  38. 45
      src/ImageSharp/Formats/Ico/MetadataExtensions.cs
  39. 46
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  40. 6
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  41. 26
      src/ImageSharp/Formats/ImageDecoder.cs
  42. 127
      src/ImageSharp/Formats/ImageDecoderCore.cs
  43. 81
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  44. 7
      src/ImageSharp/Formats/ImageEncoder.cs
  45. 4
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs
  46. 6
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  47. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs
  48. 2
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  49. 42
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  50. 2
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  51. 18
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs
  52. 12
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  53. 144
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  54. 21
      src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
  55. 25
      src/ImageSharp/Formats/Pbm/IPbmEncoderOptions.cs
  56. 20
      src/ImageSharp/Formats/Pbm/MetadataExtensions.cs
  57. 26
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  58. 2
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  59. 3
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  60. 102
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  61. 20
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  62. 84
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  63. 22
      src/ImageSharp/Formats/Png/PngBlendMethod.cs
  64. 10
      src/ImageSharp/Formats/Png/PngChunk.cs
  65. 30
      src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs
  66. 6
      src/ImageSharp/Formats/Png/PngDecoder.cs
  67. 99
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  68. 5
      src/ImageSharp/Formats/Png/PngDecoderOptions.cs
  69. 25
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  70. 151
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  71. 63
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  72. 171
      src/ImageSharp/Formats/Png/PngMetadata.cs
  73. 20
      src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
  74. 23
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  75. 10
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  76. 66
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  77. 30
      src/ImageSharp/Formats/SegmentIntegrityHandling.cs
  78. 20
      src/ImageSharp/Formats/Tga/MetadataExtensions.cs
  79. 8
      src/ImageSharp/Formats/Tga/TgaBitsPerPixel.cs
  80. 20
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  81. 110
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  82. 77
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  83. 2
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
  84. 4
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  85. 45
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  86. 2
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  87. 27
      src/ImageSharp/Formats/Tiff/MetadataExtensions.cs
  88. 8
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  89. 185
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  90. 16
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  91. 87
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  92. 2
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  93. 263
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  94. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  95. 177
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  96. 158
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  97. 27
      src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
  98. 19
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  99. 28
      src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
  100. 21
      src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.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.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -129,6 +132,7 @@ internal static class AotCompilerTools
AotCompileImageDecoderInternals<TPixel>();
AotCompileImageEncoders<TPixel>();
AotCompileImageDecoders<TPixel>();
AotCompileSpectralConverter<TPixel>();
AotCompileImageProcessors<TPixel>();
AotCompileGenericImageProcessors<TPixel>();
AotCompileResamplers<TPixel>();
@ -195,39 +199,41 @@ internal static class AotCompilerTools
=> default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext<TPixel>(default, default, default);
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompileImageEncoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
default(BmpEncoderCore).Encode<TPixel>(default, default, default);
default(GifEncoderCore).Encode<TPixel>(default, default, default);
default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PbmEncoderCore).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(TiffEncoderCore).Encode<TPixel>(default, default, default);
default(WebpEncoderCore).Encode<TPixel>(default, default, default);
}
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompileImageDecoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(WebpDecoderCore).Decode<TPixel>(default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default);
default(GifDecoderCore).Decode<TPixel>(default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default);
default(PngDecoderCore).Decode<TPixel>(default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default, default);
default(GifDecoderCore).Decode<TPixel>(default, default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default, default);
default(QoiDecoderCore).Decode<TPixel>(default, default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default, default);
default(WebpDecoderCore).Decode<TPixel>(default, default, default);
}
/// <summary>
@ -266,6 +272,17 @@ internal static class AotCompilerTools
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>
/// This method pre-seeds the <see cref="IImageEncoder"/> in the AoT compiler.
/// </summary>

94
src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs

@ -1,94 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageFrameMetadata
{
/// <summary>
/// Gets or sets the frame color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the duration of the frame.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets the frame alpha blending mode.
/// </summary>
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the frame disposal mode.
/// </summary>
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
}

33
src/ImageSharp/Formats/AnimatedImageMetadata.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageMetadata
{
/// <summary>
/// Gets or sets the shared color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the shared color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets 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>
public Color BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
/// </remarks>
/// </summary>
public ushort RepeatCount { get; set; }
}

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

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

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

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

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

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

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

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

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

@ -1,12 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
// TODO: Add color table information.
namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary>
/// Provides Bmp specific metadata information for the image.
/// </summary>
public class BmpMetadata : IDeepCloneable
public class BmpMetadata : IFormatMetadata<BmpMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="BmpMetadata"/> class.
@ -38,7 +41,7 @@ public class BmpMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the number of bits per pixel.
/// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Bit24;
/// <summary>
/// Gets or sets the color table, if any.
@ -46,5 +49,115 @@ public class BmpMetadata : IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <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);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
}

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);
}

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

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

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

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// IcoFrameMetadata.
/// </summary>
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
/// <summary>
/// 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/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any.
@ -69,11 +69,85 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <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/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
@ -84,7 +158,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
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)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -97,4 +171,75 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
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
};
}
private static byte Scale(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
}
}

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

@ -1,16 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// Provides Cur specific metadata information for the image.
/// </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.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 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 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/>
public CurMetadata DeepClone() => new();
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
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);
}

5
src/ImageSharp/Formats/DecoderOptions.cs

@ -55,5 +55,10 @@ public sealed class DecoderOptions
/// </summary>
public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); }
/// <summary>
/// Gets the segment error handling strategy to use during decoding.
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
internal void SetConfiguration(Configuration configuration) => this.configuration = configuration;
}

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>
/// Performs the gif decoding operation.
/// </summary>
internal sealed class GifDecoderCore : IImageDecoderInternals
internal sealed class GifDecoderCore : ImageDecoderCore
{
/// <summary>
/// The temp buffer used to reduce allocations.
@ -94,8 +94,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// </summary>
/// <param name="options">The decoder options.</param>
public GifDecoderCore(DecoderOptions options)
: base(options)
{
this.Options = options;
this.configuration = options.Configuration;
this.skipMetadata = options.SkipMetadata;
this.maxFrames = options.MaxFrames;
@ -103,14 +103,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
/// <inheritdoc />
public DecoderOptions Options { get; }
/// <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>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
Image<TPixel>? image = null;
@ -181,7 +174,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
/// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageFrameMetadata? previousFrame = null;
@ -249,7 +242,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
framesMetadata);
@ -287,6 +279,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
}
this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height);
}
/// <summary>
@ -498,7 +492,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
else
{
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
prevFrame = previousFrame;
}
@ -615,7 +609,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
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);
}
@ -690,14 +684,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableMode = FrameColorTableMode.Global;
}
if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableMode = FrameColorTableMode.Local;
Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
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.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
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.gifMetadata = meta.GetGifMetadata();
this.gifMetadata.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? GifColorTableMode.Global
: GifColorTableMode.Local;
? FrameColorTableMode.Global
: FrameColorTableMode.Local;
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>
/// Gets the color table mode: Global or local.
/// </summary>
public GifColorTableMode? ColorTableMode { get; init; }
public FrameColorTableMode? ColorTableMode { get; init; }
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)

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

@ -5,8 +5,6 @@ using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@ -19,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Implements the GIF encoding protocol.
/// </summary>
internal sealed class GifEncoderCore : IImageEncoderInternals
internal sealed class GifEncoderCore
{
/// <summary>
/// Used for allocating memory during processing operations.
@ -49,7 +47,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// The color table mode: Global or local.
/// </summary>
private GifColorTableMode? colorTableMode;
private FrameColorTableMode? colorTableMode;
/// <summary>
/// The pixel sampling strategy for global quantization.
@ -85,9 +83,9 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
GifMetadata gifMetadata = GetGifMetadata(image);
GifMetadata gifMetadata = image.Metadata.CloneGifMetadata();
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global;
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
@ -99,7 +97,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (this.quantizer is null)
{
// 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.
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.
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);
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)
where TPixel : unmanaged, IPixel<TPixel>
{
GifFrameMetadata? metadata = null;
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
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)
GifFrameMetadata metadata = frame.Metadata.CloneGifMetadata();
if (metadata.ColorTableMode == FrameColorTableMode.Global && transparencyIndex > -1)
{
metadata.HasTransparency = true;
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
return metadata ?? new();
return metadata;
}
private void EncodeAdditionalFrames<TPixel>(
@ -235,7 +194,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
GifDisposalMethod previousDisposalMethod)
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -250,7 +209,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
{
@ -258,7 +217,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
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)
{
@ -279,10 +238,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMethod);
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMethod = gifMetadata.DisposalMethod;
previousDisposalMode = gifMetadata.DisposalMode;
}
if (hasPaletteQuantizer)
@ -301,7 +260,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
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);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
@ -323,14 +282,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool useLocal,
GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
GifDisposalMethod previousDisposal)
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
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.
(bool difference, Rectangle bounds) =
@ -664,7 +623,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool hasTransparency = metadata.HasTransparency;
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod,
disposalMode: metadata.DisposalMode,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(

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

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides Gif specific metadata information for the image frame.
/// </summary>
public class GifFrameMetadata : IDeepCloneable
public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="GifFrameMetadata"/> class.
@ -26,7 +26,7 @@ public class GifFrameMetadata : IDeepCloneable
{
this.ColorTableMode = other.ColorTableMode;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.DisposalMode = other.DisposalMode;
if (other.LocalColorTable?.Length > 0)
{
@ -40,7 +40,7 @@ public class GifFrameMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the color table mode.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the local color table, if any.
@ -73,14 +73,11 @@ public class GifFrameMetadata : IDeepCloneable
/// Primarily used in Gif animation, this field indicates the way in which the graphic is to
/// be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod { get; set; }
public FrameDisposalMode DisposalMode { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
/// <inheritdoc />
public static GifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
// TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
int index = -1;
const float background = 1f;
if (metadata.ColorTable.HasValue)
@ -101,19 +98,43 @@ public class GifFrameMetadata : IDeepCloneable
return new()
{
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMethod = GetMode(metadata.DisposalMode),
DisposalMode = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
/// <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/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose,
FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious,
_ => GifDisposalMethod.Unspecified,
};
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public GifFrameMetadata DeepClone() => new(this);
}

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

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Provides Gif specific metadata information for the image.
/// </summary>
public class GifMetadata : IDeepCloneable
public class GifMetadata : IFormatMetadata<GifMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="GifMetadata"/> class.
@ -49,7 +49,7 @@ public class GifMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the color table mode.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// 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
/// other type of non-control and non-graphic data.
/// </summary>
public IList<string> Comments { get; set; } = new List<string>();
public IList<string> Comments { get; set; } = [];
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifMetadata(this);
internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
public static GifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
int index = 0;
Color background = metadata.BackgroundColor;
@ -81,20 +79,66 @@ public class GifMetadata : IDeepCloneable
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
if (background == colorTable[i])
if (background != colorTable[i])
{
index = i;
break;
continue;
}
index = i;
break;
}
}
return new()
{
GlobalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
ColorTableMode = metadata.ColorTableMode,
RepeatCount = metadata.RepeatCount,
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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <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
/// graphic is to be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2);
public FrameDisposalMode DisposalMethod => (FrameDisposalMode)((this.Packed & 0x1C) >> 2);
/// <summary>
/// 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)
=> 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
@ -91,7 +91,7 @@ internal readonly struct GifGraphicControlExtension : IGifExtension, IEquatable<
byte value = 0;
value |= (byte)((int)disposalMethod << 2);
value |= (byte)((int)disposalMode << 2);
if (userInputFlag)
{

44
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -0,0 +1,44 @@
// 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 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>
/// This method is called after a process has been applied to the image frame.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
/// <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
}

49
src/ImageSharp/Formats/IFormatMetadata.cs

@ -0,0 +1,49 @@
// 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>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
/// <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.
namespace SixLabors.ImageSharp.Formats;
@ -14,12 +14,12 @@ public interface IImageFormat
string Name { get; }
/// <summary>
/// Gets the default mimetype that the image format uses
/// Gets the default mime type that the image format uses
/// </summary>
string DefaultMimeType { get; }
/// <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>
IEnumerable<string> MimeTypes { get; }

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

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

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

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Provides Ico specific metadata information for the image frame.
/// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
/// <summary>
/// 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/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any.
@ -62,11 +62,85 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <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/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
@ -75,7 +149,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
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)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -92,4 +166,75 @@ 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
};
}
private static byte Scale(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
}
}

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

@ -1,16 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// </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.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 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/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
};
/// <inheritdoc/>
public IcoMetadata DeepClone() => new();
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
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.IO;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconDecoderCore : IImageDecoderInternals
internal abstract class IconDecoderCore : ImageDecoderCore
{
private IconDir fileHeader;
private IconDirEntry[]? entries;
protected IconDecoderCore(DecoderOptions options)
=> this.Options = options;
public DecoderOptions Options { get; }
public Size Dimensions { get; private set; }
: base(options)
{
}
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
// Stream may not at 0.
long basePosition = stream.Position;
@ -61,7 +58,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(stream, cancellationToken);
Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(this.Options.Configuration, stream, cancellationToken);
decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i));
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
@ -74,7 +71,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
PngMetadata? pngMetadata = null;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe;
@ -106,7 +103,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
}
this.SetFrameMetadata(
metadata,
target.Metadata,
x.Index,
this.entries[x.Index],
x.Compression,
bitsPerPixel,
@ -131,7 +130,8 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
return result;
}
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
/// <inheritdoc />
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
// Stream may not at 0.
long basePosition = stream.Position;
@ -146,7 +146,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
int bpp = 0;
for (int i = 0; i < frames.Length; i++)
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ref IconDirEntry entry = ref this.entries[i];
@ -168,7 +168,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
ImageInfo temp = this.GetDecoder(isPng).Identify(stream, cancellationToken);
ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken);
ImageFrameMetadata frameMetadata = new();
@ -176,14 +176,14 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
{
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
{
BmpMetadata meta = temp.Metadata.GetBmpMetadata();
BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata();
bitsPerPixel = meta.BitsPerPixel;
colorTable = meta.ColorTable;
@ -198,7 +198,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
frames[i] = frameMetadata;
this.SetFrameMetadata(
metadata,
frames[i],
i,
this.entries[i],
isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp,
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
// 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.
@ -220,11 +222,13 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
}
return new(new(bpp), this.Dimensions, metadata, frames);
return new(this.Dimensions, metadata, frames);
}
protected abstract void SetFrameMetadata(
ImageFrameMetadata metadata,
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry,
IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel,
@ -275,7 +279,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
this.Dimensions = new(width, height);
}
private IImageDecoderInternals GetDecoder(bool isPng)
private ImageDecoderCore GetDecoder(bool 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;
internal abstract class IconEncoderCore : IImageEncoderInternals
internal abstract class IconEncoderCore
{
private readonly QuantizingImageEncoder encoder;
private readonly IconFileType iconFileType;
@ -43,6 +43,8 @@ internal abstract class IconEncoderCore : IImageEncoderInternals
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
// 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];
@ -167,7 +169,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals
{
this.Compression = compression;
this.BmpBitsPerPixel = compression == IconFrameCompression.Png
? BmpBitsPerPixel.Pixel32
? BmpBitsPerPixel.Bit32
: bmpBitsPerPixel;
this.ColorTable = colorTable;
this.iconDirEntry = iconDirEntry;

26
src/ImageSharp/Formats/ImageDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
@ -189,7 +190,7 @@ public abstract class ImageDecoder : IImageDecoder
throw new NotSupportedException("Cannot read from the stream.");
}
T PeformActionAndResetPosition(Stream s, long position)
T PerformActionAndResetPosition(Stream s, long position)
{
T result = action(s);
@ -206,7 +207,7 @@ public abstract class ImageDecoder : IImageDecoder
if (stream.CanSeek)
{
return PeformActionAndResetPosition(stream, stream.Position);
return PerformActionAndResetPosition(stream, stream.Position);
}
Configuration configuration = options.Configuration;
@ -231,7 +232,7 @@ public abstract class ImageDecoder : IImageDecoder
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
{
@ -263,15 +264,15 @@ public abstract class ImageDecoder : IImageDecoder
// code below to copy the stream to an in-memory buffer before invoking the action.
if (stream is MemoryStream ms)
{
return PeformActionAndResetPosition(ms, ms.Position, cancellationToken);
return PerformActionAndResetPosition(ms, ms.Position, cancellationToken);
}
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>(
@ -282,7 +283,7 @@ public abstract class ImageDecoder : IImageDecoder
{
long position = stream.CanSeek ? stream.Position : 0;
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);
memoryStream.Position = 0;
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))
{
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))
{
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.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
@ -42,6 +41,8 @@ public abstract class ImageEncoder : IImageEncoder
private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
image.SynchronizeMetadata();
Configuration configuration = image.Configuration;
if (stream.CanSeek)
{
@ -59,6 +60,8 @@ public abstract class ImageEncoder : IImageEncoder
private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
image.SynchronizeMetadata();
Configuration configuration = image.Configuration;
if (stream.CanSeek)
{
@ -66,7 +69,7 @@ public abstract class ImageEncoder : IImageEncoder
}
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
await DoEncodeAsync(ms);
ms.Position = 0;
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
{
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.EncodingColor = encodingColor;
@ -25,7 +25,7 @@ internal class JpegFrameConfig
public JpegColorSpace ColorType { get; }
public JpegEncodingColor EncodingColor { get; }
public JpegColorType EncodingColor { get; }
public JpegComponentConfig[] Components { get; }

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

@ -144,13 +144,13 @@ internal class HuffmanScanEncoder
/// <param name="frame">Frame to encode.</param>
/// <param name="converter">Converter from color to spectral.</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>
{
switch (color)
{
case JpegEncodingColor.YCbCrRatio444:
case JpegEncodingColor.Rgb:
case JpegColorType.YCbCrRatio444:
case JpegColorType.Rgb:
this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken);
break;
default:

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
/// <summary>
/// The Huffman encoding specifications.
/// </summary>
public readonly struct HuffmanSpec
internal readonly struct HuffmanSpec
{
/// <summary>
/// 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>
/// Provides enumeration of available JPEG color types.
/// </summary>
public enum JpegEncodingColor : byte
public enum JpegColorType : byte
{
/// <summary>
/// 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"/>
/// with additional fixes for both performance and common encoding errors.
/// </summary>
internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
/// <summary>
/// Whether the image has an EXIF marker.
@ -117,8 +117,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// </summary>
/// <param name="options">The decoder options.</param>
public JpegDecoderCore(JpegDecoderOptions options)
: base(options.GeneralOptions)
{
this.Options = options.GeneralOptions;
this.resizeMode = options.ResizeMode;
this.configuration = options.GeneralOptions.Configuration;
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.
private static ReadOnlySpan<byte> SupportedPrecisions => new byte[] { 8, 12 };
/// <inheritdoc />
public DecoderOptions Options { get; }
/// <inheritdoc/>
public Size Dimensions => this.Frame.PixelSize;
/// <summary>
/// Gets the frame
/// </summary>
@ -198,8 +192,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
}
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize);
this.ParseStream(stream, spectralConverter, cancellationToken);
@ -216,7 +209,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
}
/// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseStream(stream, spectralConverter: null, cancellationToken);
this.InitExifProfile();
@ -226,7 +219,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
this.InitDerivedMetadataProperties();
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>
@ -603,58 +596,58 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// Returns the jpeg color type based on the colorspace and subsampling used.
/// </summary>
/// <returns>Jpeg color type.</returns>
private JpegEncodingColor DeduceJpegColorType()
private JpegColorType DeduceJpegColorType()
{
switch (this.ColorSpace)
{
case JpegColorSpace.Grayscale:
return JpegEncodingColor.Luminance;
return JpegColorType.Luminance;
case JpegColorSpace.RGB:
return JpegEncodingColor.Rgb;
return JpegColorType.Rgb;
case JpegColorSpace.YCbCr:
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[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 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].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 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].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 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].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 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{
return JpegEncodingColor.YCbCrRatio410;
return JpegColorType.YCbCrRatio410;
}
else
{
return JpegEncodingColor.YCbCrRatio420;
return JpegColorType.YCbCrRatio420;
}
case JpegColorSpace.Cmyk:
return JpegEncodingColor.Cmyk;
return JpegColorType.Cmyk;
case JpegColorSpace.Ycck:
return JpegEncodingColor.Ycck;
return JpegColorType.Ycck;
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.Dimensions = new(frameWidth, frameHeight);
this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive;
remaining -= length;

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

@ -105,7 +105,7 @@ public sealed class JpegEncoder : ImageEncoder
/// <summary>
/// Gets the jpeg color for encoding.
/// </summary>
public JpegEncodingColor? ColorType { get; init; }
public JpegColorType? ColorType { get; init; }
/// <inheritdoc/>
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
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio444,
JpegColorType.YCbCrRatio444,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio422,
JpegColorType.YCbCrRatio422,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio420,
JpegColorType.YCbCrRatio420,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio411,
JpegColorType.YCbCrRatio411,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio410,
JpegColorType.YCbCrRatio410,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.Grayscale,
JpegEncodingColor.Luminance,
JpegColorType.Luminance,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.RGB,
JpegEncodingColor.Rgb,
JpegColorType.Rgb,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.Cmyk,
JpegEncodingColor.Cmyk,
JpegColorType.Cmyk,
new JpegComponentConfig[]
{
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
new JpegFrameConfig(
JpegColorSpace.Ycck,
JpegEncodingColor.Ycck,
JpegColorType.Ycck,
new JpegComponentConfig[]
{
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>
/// Image encoder for writing an image to a stream as a jpeg.
/// </summary>
internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
internal sealed unsafe partial class JpegEncoderCore
{
/// <summary>
/// The available encodable frame configs.
@ -72,7 +72,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
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);
// Write the Start Of Image marker.
@ -561,17 +561,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <param name="buffer">Temporary buffer.</param>
private void WriteProfiles(ImageMetadata metadata, Span<byte> buffer)
{
if (metadata is null)
{
return;
}
// For compatibility, place the profiles in the following order:
// - APP1 EXIF
// - APP1 XMP
// - APP2 ICC
// - APP13 IPTC
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile, buffer);
this.WriteXmpProfile(metadata.XmpProfile, buffer);
this.WriteIccProfile(metadata.IccProfile, buffer);
@ -864,7 +858,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
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(
FrameConfigs,
cfg => cfg.EncodingColor == color);

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

@ -2,20 +2,20 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary>
/// Provides Jpeg specific metadata information for the image.
/// </summary>
public class JpegMetadata : IDeepCloneable
public class JpegMetadata : IFormatMetadata<JpegMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegMetadata"/> class.
/// </summary>
public JpegMetadata()
{
this.Comments = new List<JpegComData>();
}
/// <summary>
@ -36,7 +36,7 @@ public class JpegMetadata : IDeepCloneable
/// </summary>
/// <remarks>
/// 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>
internal int? LuminanceQuality { get; set; }
@ -45,16 +45,17 @@ public class JpegMetadata : IDeepCloneable
/// </summary>
/// <remarks>
/// 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>
internal int? ChrominanceQuality { get; set; }
/// <summary>
/// Gets the encoded quality.
/// Gets or sets the encoded quality.
/// </summary>
/// <remarks>
/// 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.
/// Setting the quality will update both values.
/// </remarks>
public int Quality
{
@ -69,45 +70,144 @@ public class JpegMetadata : IDeepCloneable
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>
/// Gets the color type.
/// Gets or sets the color type.
/// </summary>
public JpegEncodingColor? ColorType { get; internal set; }
public JpegColorType ColorType { get; set; } = JpegColorType.YCbCrRatio420;
/// <summary>
/// Gets the component encoding mode.
/// Gets or sets a value indicating whether the component encoding mode should be interleaved.
/// </summary>
/// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks>
public bool? Interleaved { get; internal set; }
public bool Interleaved { get; set; } = true;
/// <summary>
/// Gets the scan encoding mode.
/// Gets or sets a value indicating whether the scan encoding mode is progressive.
/// </summary>
/// <remarks>
/// Progressive jpeg images encode component data across multiple scans.
/// </remarks>
public bool? Progressive { get; internal set; }
public bool Progressive { get; set; }
/// <summary>
/// Gets the comments.
/// Gets or sets collection of comments.
/// </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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <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>
/// Performs the PBM decoding operation.
/// </summary>
internal sealed class PbmDecoderCore : IImageDecoderInternals
internal sealed class PbmDecoderCore : ImageDecoderCore
{
private int maxPixelValue;
@ -52,24 +52,17 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// </summary>
/// <param name="options">The decoder options.</param>
public PbmDecoderCore(DecoderOptions options)
: base(options)
{
this.Options = options;
this.configuration = options.Configuration;
}
/// <inheritdoc/>
public DecoderOptions Options { get; }
/// <inheritdoc/>
public Size Dimensions => this.pixelSize;
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
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();
@ -83,13 +76,12 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
}
/// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ProcessHeader(stream);
// BlackAndWhite pixels are encoded into a byte.
int bitsPerPixel = this.componentType == PbmComponentType.Short ? 16 : 8;
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new(this.pixelSize.Width, this.pixelSize.Height), this.metadata);
return new ImageInfo(
new(this.pixelSize.Width, this.pixelSize.Height),
this.metadata);
}
/// <summary>
@ -97,6 +89,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// </summary>
/// <param name="stream">The input stream.</param>
/// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception>
[MemberNotNull(nameof(metadata))]
private void ProcessHeader(BufferedReadStream stream)
{
Span<byte> buffer = stackalloc byte[2];
@ -179,6 +172,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
}
this.pixelSize = new Size(width, height);
this.Dimensions = this.pixelSize;
this.metadata = new ImageMetadata();
PbmMetadata meta = this.metadata.GetPbmMetadata();
meta.Encoding = this.encoding;

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

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

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

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

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

@ -1,12 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary>
/// Provides PBM specific metadata information for the image.
/// </summary>
public class PbmMetadata : IDeepCloneable
public class PbmMetadata : IFormatMetadata<PbmMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="PbmMetadata"/> class.
@ -41,5 +43,101 @@ public class PbmMetadata : IDeepCloneable
public PbmComponentType ComponentType { get; set; }
/// <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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <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,
ushort delayNumerator,
ushort delayDenominator,
PngDisposalMethod disposeOperation,
PngBlendMethod blendOperation)
FrameDisposalMode disposalMode,
FrameBlendMode blendMode)
{
this.SequenceNumber = sequenceNumber;
this.Width = width;
@ -32,8 +32,8 @@ internal readonly struct FrameControl
this.YOffset = yOffset;
this.DelayNumerator = delayNumerator;
this.DelayDenominator = delayDenominator;
this.DisposeOperation = disposeOperation;
this.BlendOperation = blendOperation;
this.DisposalMode = disposalMode;
this.BlendMode = blendMode;
}
/// <summary>
@ -84,12 +84,12 @@ internal readonly struct FrameControl
/// <summary>
/// Gets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposeOperation { get; }
public FrameDisposalMode DisposalMode { get; }
/// <summary>
/// Gets the type of frame area rendering for this frame
/// </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);
@ -137,8 +137,8 @@ internal readonly struct FrameControl
BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator);
BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator);
buffer[24] = (byte)this.DisposeOperation;
buffer[25] = (byte)this.BlendOperation;
buffer[24] = (byte)(this.DisposalMode - 1);
buffer[25] = (byte)this.BlendMode;
}
/// <summary>
@ -155,6 +155,6 @@ internal readonly struct FrameControl
yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]),
delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]),
delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
disposeOperation: (PngDisposalMethod)data[24],
blendOperation: (PngBlendMethod)data[25]);
disposalMode: (FrameDisposalMode)(data[24] + 1),
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
}

10
src/ImageSharp/Formats/Png/PngChunk.cs

@ -41,13 +41,13 @@ internal readonly struct PngChunk
/// <summary>
/// Gets a value indicating whether the given chunk is critical to decoding
/// </summary>
/// <param name="handling">The chunk CRC handling behavior.</param>
public bool IsCritical(PngCrcChunkHandling handling)
/// <param name="handling">The segment handling behavior.</param>
public bool IsCritical(SegmentIntegrityHandling handling)
=> handling switch
{
PngCrcChunkHandling.IgnoreNone => true,
PngCrcChunkHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData,
PngCrcChunkHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette,
SegmentIntegrityHandling.IgnoreNone => true,
SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData,
SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette,
_ => false,
};
}

30
src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs

@ -1,30 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
/// </summary>
public enum PngCrcChunkHandling
{
/// <summary>
/// Do not ignore any CRC chunk errors.
/// </summary>
IgnoreNone,
/// <summary>
/// Ignore CRC errors in non critical chunks.
/// </summary>
IgnoreNonCritical,
/// <summary>
/// Ignore CRC errors in data chunks.
/// </summary>
IgnoreData,
/// <summary>
/// Ignore CRC errors in all chunks.
/// </summary>
IgnoreAll
}

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

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

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

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Performs the png decoding operation.
/// </summary>
internal sealed class PngDecoderCore : IImageDecoderInternals
internal sealed class PngDecoderCore : ImageDecoderCore
{
/// <summary>
/// The general decoder options.
@ -119,7 +119,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary>
/// How to handle CRC errors.
/// </summary>
private readonly PngCrcChunkHandling pngCrcChunkHandling;
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary>
/// A reusable Crc32 hashing instance.
@ -136,37 +136,30 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// </summary>
/// <param name="options">The decoder options.</param>
public PngDecoderCore(PngDecoderOptions options)
: base(options.GeneralOptions)
{
this.Options = options.GeneralOptions;
this.configuration = options.GeneralOptions.Configuration;
this.maxFrames = options.GeneralOptions.MaxFrames;
this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
}
internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly)
: base(options.GeneralOptions)
{
this.Options = options.GeneralOptions;
this.colorMetadataOnly = colorMetadataOnly;
this.maxFrames = options.GeneralOptions.MaxFrames;
this.skipMetadata = true;
this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
}
/// <inheritdoc/>
public DecoderOptions Options { get; }
/// <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>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageMetadata metadata = new();
@ -241,7 +234,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
cancellationToken);
// 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;
previousFrameControl = currentFrameControl;
@ -341,7 +334,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
}
/// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageMetadata metadata = new();
@ -527,10 +520,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngThrowHelper.ThrowInvalidHeader();
}
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata, framesMetadata);
return new ImageInfo(new(this.header.Width, this.header.Height), metadata, framesMetadata);
}
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.
// 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();
pixelRegion.Clear();
}
else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground)
else if (previousFrameControl.Value.DisposalMode == FrameDisposalMode.RestoreToBackground)
{
Rectangle restoreArea = previousFrameControl.Value.Bounds;
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
@ -703,29 +693,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
@ -817,8 +784,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int height = (int)frameControl.YMax;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty;
if (frameControl.BlendOperation == PngBlendMethod.Over)
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
@ -866,7 +833,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
default:
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
@ -910,8 +877,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty;
if (frameControl.BlendOperation == PngBlendMethod.Over)
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
@ -972,7 +939,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
default:
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
@ -1036,7 +1003,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
Span<TPixel> destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
bool blend = frameControl.BlendMode == FrameBlendMode.Over;
Span<TPixel> rowSpan = blend
? blendRowBuffer
: destination;
@ -1149,7 +1116,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int increment = 1)
where TPixel : unmanaged, IPixel<TPixel>
{
bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
bool blend = frameControl.BlendMode == FrameBlendMode.Over;
Span<TPixel> rowSpan = blend
? blendRowBuffer
: destination;
@ -1365,6 +1332,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
pngMetadata.InterlaceMethod = this.header.InterlaceMethod;
this.pngColorType = this.header.ColorType;
this.Dimensions = new(this.header.Width, this.header.Height);
}
/// <summary>
@ -1483,7 +1451,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
byte colorPrimaries = data[0];
byte transferFunction = data[1];
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);
}
@ -1515,7 +1496,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes).
// 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)
{
@ -1626,7 +1607,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
Span<byte> destUncompressedData = destBuffer.GetSpan();
if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
{
uncompressedBytesArray = Array.Empty<byte>();
uncompressedBytesArray = [];
return false;
}
@ -1635,7 +1616,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
if (memoryStreamOutput.Length > maxLength)
{
uncompressedBytesArray = Array.Empty<byte>();
uncompressedBytesArray = [];
return false;
}
@ -1946,7 +1927,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private void ValidateChunk(in PngChunk chunk, Span<byte> buffer)
{
uint inputCrc = this.ReadChunkCrc(buffer);
if (chunk.IsCritical(this.pngCrcChunkHandling))
if (chunk.IsCritical(this.segmentIntegrityHandling))
{
Span<byte> chunkType = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type);
@ -2002,7 +1983,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
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()

5
src/ImageSharp/Formats/Png/PngDecoderOptions.cs

@ -11,11 +11,6 @@ public sealed class PngDecoderOptions : ISpecializedDecoderOptions
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
/// <summary>
/// Gets a value indicating how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
/// </summary>
public PngCrcChunkHandling PngCrcChunkHandling { get; init; } = PngCrcChunkHandling.IgnoreNonCritical;
/// <summary>
/// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed.
/// Defaults to 8MB

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
}

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

@ -9,10 +9,8 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -23,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Performs the png encoding operation.
/// </summary>
internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
internal sealed class PngEncoderCore : IDisposable
{
/// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks.
@ -160,7 +158,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = GetPngMetadata(image);
PngMetadata pngMetadata = metadata.ClonePngMetadata();
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
stream.Write(PngConstants.HeaderBytes);
@ -211,8 +209,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
@ -233,16 +231,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size());
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
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];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = GetPngFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -268,7 +266,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
previousDisposal = frameMetadata.DisposalMode;
}
}
@ -288,54 +286,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
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>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
@ -826,7 +776,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return;
}
meta.SyncProfiles();
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
}
@ -881,6 +830,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image 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)
{
if (metaData.CicpProfile is null)
@ -1125,8 +1075,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
yOffset: (uint)bounds.Top,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod,
blendOperation: frameMetadata.BlendMethod);
disposalMode: frameMetadata.DisposalMode,
blendMode: frameMetadata.BlendMode);
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
// a sensible default based upon the pixel format.
PngColorType? colorType = encoder.ColorType ?? pngMetadata.ColorType;
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;
}
}
}
PngColorType color = encoder.ColorType ?? pngMetadata.ColorType;
byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth);
// 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.
byte[] validBitDepths = PngConstants.ColorTypes[colorType.Value];
byte[] validBitDepths = PngConstants.ColorTypes[color];
if (Array.IndexOf(validBitDepths, bits) == -1)
{
bits = (byte)PngBitDepth.Bit8;
}
this.colorType = colorType.Value;
this.bitDepth = bits.Value;
this.colorType = color;
this.bitDepth = bits;
if (encoder.FilterMethod.HasValue)
{
@ -1532,7 +1460,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
use16Bit = bits == (byte)PngBitDepth.Bit16;
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;
}
@ -1672,47 +1600,6 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
_ => 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 const int Size = 26;

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

@ -2,13 +2,14 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Provides APng specific metadata information for the image frame.
/// </summary>
public class PngFrameMetadata : IDeepCloneable
public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
@ -24,8 +25,8 @@ public class PngFrameMetadata : IDeepCloneable
private PngFrameMetadata(PngFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
this.DisposalMode = other.DisposalMode;
this.BlendMode = other.BlendMode;
}
/// <summary>
@ -39,12 +40,12 @@ public class PngFrameMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposalMethod { get; set; }
public FrameDisposalMode DisposalMode { get; set; }
/// <summary>
/// Gets or sets the type of frame area rendering for this frame
/// </summary>
public PngBlendMethod BlendMethod { get; set; }
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
@ -53,26 +54,54 @@ public class PngFrameMetadata : IDeepCloneable
internal void FromChunk(in FrameControl frameControl)
{
this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
this.DisposalMethod = frameControl.DisposeOperation;
this.BlendMethod = frameControl.BlendOperation;
this.DisposalMode = frameControl.DisposalMode;
this.BlendMode = frameControl.BlendMode;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
=> new()
{
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
DisposalMethod = GetMode(metadata.DisposalMode),
BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over,
DisposalMode = GetMode(metadata.DisposalMode),
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/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <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.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious,
FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose,
_ => PngDisposalMethod.DoNotDispose,
FrameDisposalMode.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
FrameDisposalMode.DoNotDispose => FrameDisposalMode.DoNotDispose,
_ => FrameDisposalMode.DoNotDispose,
};
}

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

@ -2,13 +2,14 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Provides Png specific metadata information for the image.
/// </summary>
public class PngMetadata : IDeepCloneable
public class PngMetadata : IFormatMetadata<PngMetadata>
{
/// <summary>
/// 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).
/// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary>
public PngBitDepth? BitDepth { get; set; }
public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8;
/// <summary>
/// Gets or sets the color type.
/// </summary>
public PngColorType? ColorType { get; set; }
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
/// <summary>
/// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
public PngInterlaceMode? InterlaceMethod { get; set; } = PngInterlaceMode.None;
public PngInterlaceMode InterlaceMethod { get; set; } = PngInterlaceMode.None;
/// <summary>
/// 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.
/// Used for conveying textual information associated with the image.
/// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
public IList<PngTextData> TextData { get; set; } = [];
/// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
@ -90,35 +91,171 @@ public class PngMetadata : IDeepCloneable
public bool AnimateRootFrame { get; set; } = true;
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);
internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
public static PngMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
// 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.
Color[]? colorTable = metadata.ColorTable.HasValue ? metadata.ColorTable.Value.ToArray() : null;
Color[]? colorTable = metadata.ColorTable?.ToArray();
if (colorTable != null)
{
for (int i = 0; i < colorTable.Length; i++)
{
ref Color c = ref colorTable[i];
if (c == metadata.BackgroundColor)
if (c != metadata.BackgroundColor)
{
// Png treats background as fully empty
c = Color.Transparent;
break;
continue;
}
// 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()
{
ColorType = colorTable != null ? PngColorType.Palette : null,
BitDepth = colorTable != null
? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8)
: null,
ColorType = color,
BitDepth = bitDepth,
ColorTable = colorTable,
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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <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;
internal class QoiDecoderCore : IImageDecoderInternals
internal class QoiDecoderCore : ImageDecoderCore
{
/// <summary>
/// The global configuration.
@ -31,31 +31,20 @@ internal class QoiDecoderCore : IImageDecoderInternals
private QoiHeader header;
public QoiDecoderCore(DecoderOptions options)
: base(options)
{
this.Options = options;
this.configuration = options.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
}
public DecoderOptions Options { get; }
public Size Dimensions { get; }
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
// Process the header to get metadata
this.ProcessHeader(stream);
// Create Image object
ImageMetadata metadata = new()
{
DecodedImageFormat = QoiFormat.Instance,
HorizontalResolution = this.header.Width,
VerticalResolution = this.header.Height,
ResolutionUnits = PixelResolutionUnit.AspectRatio
};
ImageMetadata metadata = new();
QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace;
@ -68,7 +57,7 @@ internal class QoiDecoderCore : IImageDecoderInternals
}
/// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ProcessHeader(stream);
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
@ -79,7 +68,7 @@ internal class QoiDecoderCore : IImageDecoderInternals
qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace;
return new ImageInfo(pixelType, size, metadata);
return new ImageInfo(size, metadata);
}
/// <summary>

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

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Image encoder for writing an image to a stream as a QOi image
/// </summary>
internal class QoiEncoderCore : IImageEncoderInternals
internal class QoiEncoderCore
{
/// <summary>
/// The encoder with options
@ -41,7 +41,13 @@ internal class QoiEncoderCore : IImageEncoderInternals
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)
where TPixel : unmanaged, IPixel<TPixel>
{

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

@ -1,12 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Provides Qoi specific metadata information for the image.
/// </summary>
public class QoiMetadata : IDeepCloneable
public class QoiMetadata : IFormatMetadata<QoiMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
@ -36,5 +38,65 @@ public class QoiMetadata : IDeepCloneable
public QoiColorSpace ColorSpace { get; set; }
/// <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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public QoiMetadata DeepClone() => new(this);
}

30
src/ImageSharp/Formats/SegmentIntegrityHandling.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Specifies how to handle validation of errors in different segments of encoded image files.
/// </summary>
public enum SegmentIntegrityHandling
{
/// <summary>
/// Do not ignore any errors.
/// </summary>
IgnoreNone,
/// <summary>
/// Ignore errors in non-critical segments of the encoded image.
/// </summary>
IgnoreNonCritical,
/// <summary>
/// Ignore errors in data segments (e.g., image data, metadata).
/// </summary>
IgnoreData,
/// <summary>
/// Ignore errors in all segments.
/// </summary>
IgnoreAll
}

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>
/// 8 bits per pixel. Each pixel consists of 1 byte.
/// </summary>
Pixel8 = 8,
Bit8 = 8,
/// <summary>
/// 16 bits per pixel. Each pixel consists of 2 bytes.
/// </summary>
Pixel16 = 16,
Bit16 = 16,
/// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary>
Pixel24 = 24,
Bit24 = 24,
/// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes.
/// </summary>
Pixel32 = 32
Bit32 = 32
}

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

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

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

@ -3,8 +3,6 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -14,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary>
/// Image encoder for writing an image to a stream as a truevision targa image.
/// </summary>
internal sealed class TgaEncoderCore : IImageEncoderInternals
internal sealed class TgaEncoderCore
{
/// <summary>
/// Used for allocating memory during processing operations.
@ -61,7 +59,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
this.bitsPerPixel ??= tgaMetadata.BitsPerPixel;
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;
}
@ -73,13 +71,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
imageDescriptor |= 0x20;
}
if (this.bitsPerPixel is TgaBitsPerPixel.Pixel32)
if (this.bitsPerPixel is TgaBitsPerPixel.Bit32)
{
// Indicate, that 8 bit are used for the alpha channel.
imageDescriptor |= 0x8;
}
if (this.bitsPerPixel is TgaBitsPerPixel.Pixel16)
if (this.bitsPerPixel is TgaBitsPerPixel.Bit16)
{
// Indicate, that 1 bit is used for the alpha channel.
imageDescriptor |= 0x1;
@ -107,11 +105,11 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
stream.Write(buffer, 0, TgaFileHeader.Size);
if (this.compression is TgaCompression.RunLength)
{
this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame);
this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame, cancellationToken);
}
else
{
this.WriteImage(image.Configuration, stream, image.Frames.RootFrame);
this.WriteImage(image.Configuration, stream, image.Frames.RootFrame, cancellationToken);
}
stream.Flush();
@ -123,29 +121,28 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
/// <param name="image"> /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = image.PixelBuffer;
switch (this.bitsPerPixel)
{
case TgaBitsPerPixel.Pixel8:
this.Write8Bit(configuration, stream, pixels);
case TgaBitsPerPixel.Bit8:
this.Write8Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Pixel16:
this.Write16Bit(configuration, stream, pixels);
case TgaBitsPerPixel.Bit16:
this.Write16Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Pixel24:
this.Write24Bit(configuration, stream, pixels);
case TgaBitsPerPixel.Bit24:
this.Write24Bit(configuration, stream, pixels, cancellationToken);
break;
case TgaBitsPerPixel.Pixel32:
this.Write32Bit(configuration, stream, pixels);
case TgaBitsPerPixel.Bit32:
this.Write32Bit(configuration, stream, pixels, cancellationToken);
break;
}
}
@ -156,23 +153,33 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The stream to write the image to.</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>
{
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++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgba32(image.Configuration, pixelRow, rgbaRow);
for (int x = 0; x < image.Width;)
{
TPixel currentPixel = pixelRow[x];
Rgba32 rgba = rgbaRow[x];
byte equalPixelCount = FindEqualPixels(pixelRow, x);
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));
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32());
this.WritePixel(stream, rgba);
x += equalPixelCount + 1;
}
else
@ -180,12 +187,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
// Write Raw Packet (i.e., Non-Run-Length Encoded):
byte unEqualPixelCount = FindUnEqualPixels(pixelRow, x);
stream.WriteByte(unEqualPixelCount);
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32());
this.WritePixel(stream, rgba);
x++;
for (int i = 0; i < unEqualPixelCount; i++)
{
currentPixel = pixelRow[x];
this.WritePixel(stream, currentPixel, currentPixel.ToRgba32());
rgba = rgbaRow[x];
this.WritePixel(stream, rgba);
x++;
}
}
@ -196,22 +204,19 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes a the pixel to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <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>
private void WritePixel<TPixel>(Stream stream, TPixel currentPixel, Rgba32 color)
where TPixel : unmanaged, IPixel<TPixel>
private void WritePixel(Stream stream, Rgba32 color)
{
switch (this.bitsPerPixel)
{
case TgaBitsPerPixel.Pixel8:
int luminance = GetLuminance(currentPixel);
stream.WriteByte((byte)luminance);
case TgaBitsPerPixel.Bit8:
L8 l8 = L8.FromRgba32(color);
stream.WriteByte(l8.PackedValue);
break;
case TgaBitsPerPixel.Pixel16:
Bgra5551 bgra5551 = new(color.ToVector4());
case TgaBitsPerPixel.Bit16:
Bgra5551 bgra5551 = Bgra5551.FromRgba32(color);
Span<byte> buffer = stackalloc byte[2];
BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue);
stream.WriteByte(buffer[0]);
@ -219,13 +224,13 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
break;
case TgaBitsPerPixel.Pixel24:
case TgaBitsPerPixel.Bit24:
stream.WriteByte(color.B);
stream.WriteByte(color.G);
stream.WriteByte(color.R);
break;
case TgaBitsPerPixel.Pixel32:
case TgaBitsPerPixel.Bit32:
stream.WriteByte(color.B);
stream.WriteByte(color.G);
stream.WriteByte(color.R);
@ -310,7 +315,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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>
{
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--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8Bytes(
configuration,
@ -335,7 +343,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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>
{
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--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
configuration,
@ -360,7 +371,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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>
{
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--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration,
@ -385,7 +399,8 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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>
{
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--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration,
@ -402,17 +419,4 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
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);
}
}

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

@ -1,12 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary>
/// Provides TGA specific metadata information for the image.
/// </summary>
public class TgaMetadata : IDeepCloneable
public class TgaMetadata : IFormatMetadata<TgaMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="TgaMetadata"/> class.
@ -25,7 +27,7 @@ public class TgaMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the number of bits per pixel.
/// </summary>
public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Pixel24;
public TgaBitsPerPixel BitsPerPixel { get; set; } = TgaBitsPerPixel.Bit24;
/// <summary>
/// Gets or sets the number of alpha bits per pixel.
@ -33,5 +35,74 @@ public class TgaMetadata : IDeepCloneable
public byte AlphaChannelBits { get; set; }
/// <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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <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);
image.Save(memoryStream, new JpegEncoder()
{
ColorType = JpegEncodingColor.Rgb
ColorType = JpegColorType.Rgb
});
memoryStream.Position = 0;
memoryStream.WriteTo(this.Output);

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

@ -32,8 +32,8 @@ internal class WebpTiffCompression : TiffBaseDecompressor
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{
using WebpDecoderCore decoder = new(new WebpDecoderOptions());
using Image<Rgb24> image = decoder.Decode<Rgb24>(stream, cancellationToken);
using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = this.options });
using Image<Rgb24> image = decoder.Decode<Rgb24>(this.options.Configuration, stream, cancellationToken);
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;
/// <summary>
/// The big tiff bytesize of offsets value.
/// The big tiff byte size of offsets value.
/// </summary>
public const ushort BigTiffBytesize = 8;
public const ushort BigTiffByteSize = 8;
/// <summary>
/// RowsPerStrip default value, which is effectively infinity.
@ -58,38 +58,63 @@ internal static class TiffConstants
/// </summary>
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>
/// The bits per sample for 1 bit bicolor images.
/// </summary>
public static readonly TiffBitsPerSample BitsPerSample1Bit = new TiffBitsPerSample(1, 0, 0);
public static readonly TiffBitsPerSample BitsPerSample1Bit = new(1, 0, 0);
/// <summary>
/// The bits per sample for images with a 4 color palette.
/// </summary>
public static readonly TiffBitsPerSample BitsPerSample4Bit = new TiffBitsPerSample(4, 0, 0);
public static readonly TiffBitsPerSample BitsPerSample4Bit = new(4, 0, 0);
/// <summary>
/// The bits per sample for 8 bit images.
/// </summary>
public static readonly TiffBitsPerSample BitsPerSample8Bit = new TiffBitsPerSample(8, 0, 0);
public static readonly TiffBitsPerSample BitsPerSample8Bit = new(8, 0, 0);
/// <summary>
/// The bits per sample for 16-bit grayscale images.
/// </summary>
public static readonly TiffBitsPerSample BitsPerSample16Bit = new TiffBitsPerSample(16, 0, 0);
public static readonly TiffBitsPerSample BitsPerSample16Bit = new(16, 0, 0);
/// <summary>
/// The bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new TiffBitsPerSample(8, 8, 8);
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new(8, 8, 8);
/// <summary>
/// The list of mimetypes that equate to a tiff.
/// The list of mime types that equate to a tiff.
/// </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>
/// The list of file extensions that equate to a tiff.
/// </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 reserve = this.ReadUInt16();
if (bytesize == TiffConstants.BigTiffBytesize && reserve == 0)
if (bytesize == TiffConstants.BigTiffByteSize && reserve == 0)
{
this.FirstIfdOffset = this.ReadUInt64();
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)
{
return new[] { this.Channel0 };
return [this.Channel0];
}
if (this.Channel2 == 0)
{
return new[] { this.Channel0, this.Channel1 };
return [this.Channel0, this.Channel1];
}
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>

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

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Performs the tiff decoding operation.
/// </summary>
internal class TiffDecoderCore : IImageDecoderInternals
internal class TiffDecoderCore : ImageDecoderCore
{
/// <summary>
/// General configuration options.
@ -60,8 +60,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary>
/// <param name="options">The decoder options.</param>
public TiffDecoderCore(DecoderOptions options)
: base(options)
{
this.Options = options;
this.configuration = options.Configuration;
this.skipMetadata = options.SkipMetadata;
this.maxFrames = options.MaxFrames;
@ -154,14 +154,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
public TiffPredictor Predictor { get; set; }
/// <inheritdoc/>
public DecoderOptions Options { get; }
/// <inheritdoc/>
public Size Dimensions { get; private set; }
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
List<ImageFrame<TPixel>> frames = [];
List<ImageFrameMetadata> framesMetadata = [];
@ -174,11 +167,18 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
Size? size = null;
uint frameCount = 0;
foreach (ExifProfile ifd in directories)
{
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken);
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, size, cancellationToken);
if (!size.HasValue)
{
size = frame.Size;
}
frames.Add(frame);
framesMetadata.Add(frame.Metadata);
@ -188,19 +188,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
}
}
this.Dimensions = frames[0].Size;
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
// TODO: Tiff frames can have different sizes.
ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames)
{
if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
}
return new Image<TPixel>(this.configuration, metadata, frames);
}
catch
@ -215,26 +204,30 @@ internal class TiffDecoderCore : IImageDecoderInternals
}
/// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.inputStream = stream;
DirectoryReader reader = new(stream, this.configuration.MemoryAllocator);
IList<ExifProfile> directories = reader.Read();
List<ImageFrameMetadata> framesMetadata = [];
foreach (ExifProfile dir in directories)
int width = 0;
int height = 0;
for (int i = 0; i < directories.Count; i++)
{
framesMetadata.Add(this.CreateFrameMetadata(dir));
}
(ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta
= this.CreateFrameMetadata(directories[i]);
ExifProfile rootFrameExifProfile = directories[0];
framesMetadata.Add(meta.FrameMetadata);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
width = Math.Max(width, meta.TiffMetadata.EncodingWidth);
height = Math.Max(height, meta.TiffMetadata.EncodingHeight);
}
int width = GetImageWidth(rootFrameExifProfile);
int height = GetImageHeight(rootFrameExifProfile);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
return new ImageInfo(new PixelTypeInfo((int)framesMetadata[0].GetTiffMetadata().BitsPerPixel), new(width, height), metadata, framesMetadata);
return new ImageInfo(new(width, height), metadata, framesMetadata);
}
/// <summary>
@ -242,31 +235,46 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="size">The previously determined root frame size if decoded.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
/// <returns>The tiff frame.</returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, Size? size, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags);
bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata());
(ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags);
bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata);
int width = metadata.TiffFrameMetadata.EncodingWidth;
int height = metadata.TiffFrameMetadata.EncodingHeight;
// If size has a value and the width/height off the tiff is smaller we much capture the delta.
if (size.HasValue)
{
if (size.Value.Width < width || size.Value.Height < height)
{
TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported.");
}
}
else
{
size = new Size(width, height);
}
int width = GetImageWidth(tags);
int height = GetImageHeight(tags);
ImageFrame<TPixel> frame = new(this.configuration, width, height, imageFrameMetaData);
ImageFrame<TPixel> frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
if (isTiled)
{
this.DecodeImageWithTiles(tags, frame, cancellationToken);
this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
}
else
{
this.DecodeImageWithStrips(tags, frame, cancellationToken);
this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
}
return frame;
}
private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags)
private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
{
ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata)
@ -274,9 +282,10 @@ internal class TiffDecoderCore : IImageDecoderInternals
imageFrameMetaData.ExifProfile = tags;
}
TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags);
TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags);
imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata);
return imageFrameMetaData;
return (imageFrameMetaData, tiffMetadata);
}
/// <summary>
@ -285,8 +294,10 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int rowsPerStrip;
@ -309,6 +320,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
{
this.DecodeStripsPlanar(
frame,
width,
height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@ -318,6 +331,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
{
this.DecodeStripsChunky(
frame,
width,
height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@ -331,13 +346,13 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = frame.PixelBuffer;
int width = pixels.Width;
int height = pixels.Height;
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueWidth))
{
@ -391,11 +406,20 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The number of rows per strip of data.</param>
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
private void DecodeStripsPlanar<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int stripsPerPixel = this.BitsPerSample.Channels;
@ -410,18 +434,18 @@ internal class TiffDecoderCore : IImageDecoderInternals
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize);
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel);
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
for (int i = 0; i < stripsPerPlane; i++)
{
cancellationToken.ThrowIfCancellationRequested();
int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip;
int stripIndex = i;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
@ -437,7 +461,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
stripIndex += stripsPerPlane;
}
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight);
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
}
}
finally
@ -454,39 +478,48 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The rows per strip.</param>
/// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
private void DecodeStripsChunky<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{
rowsPerStrip = frame.Height;
rowsPerStrip = height;
}
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean);
Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel);
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip
: frame.Height % rowsPerStrip;
: height % rowsPerStrip;
int top = rowsPerStrip * stripIndex;
if (top + stripHeight > frame.Height)
if (top + stripHeight > height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present).
break;
@ -500,7 +533,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
stripBufferSpan,
cancellationToken);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
}
}
@ -797,38 +830,6 @@ internal class TiffDecoderCore : IImageDecoderInternals
return bytesPerRow * height;
}
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number> width))
{
TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
}
DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
return (int)width.Value;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number> height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
}

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

@ -23,7 +23,7 @@ internal static class TiffDecoderMetadataCreator
TiffThrowHelper.ThrowImageFormatException("Expected at least one frame.");
}
ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].ExifProfile);
ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]);
if (!ignoreMetadata)
{
@ -50,14 +50,22 @@ internal static class TiffDecoderMetadataCreator
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();
SetResolution(imageMetaData, exifProfile);
SetResolution(imageMetaData, rootFrameMetadata.ExifProfile);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder;
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;
}
@ -109,7 +117,7 @@ internal static class TiffDecoderMetadataCreator
return false;
}
// Probably wrong endianess, swap byte order.
// Probably wrong endianness, swap byte order.
Span<byte> iptcBytesSpan = iptcBytes.AsSpan();
Span<byte> buffer = stackalloc byte[4];
for (int i = 0; i < iptcBytes.Length; i += 4)

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

@ -63,7 +63,7 @@ internal static class TiffDecoderOptionsParser
}
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();
sampleFormat = sampleFormats[0];
@ -106,11 +106,11 @@ internal static class TiffDecoderOptionsParser
options.PlanarConfiguration = DefaultPlanarConfiguration;
}
options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None;
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb;
options.Predictor = frameMetadata.Predictor;
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation;
options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger;
options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24;
options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0);
options.BitsPerPixel = (int)frameMetadata.BitsPerPixel;
options.BitsPerSample = frameMetadata.BitsPerSample;
if (exifProfile.TryGetValue(ExifTag.ReferenceBlackWhite, out IExifValue<Rational[]> blackWhiteValue))
{
@ -142,9 +142,7 @@ internal static class TiffDecoderOptionsParser
options.ParseCompression(frameMetadata.Compression, exifProfile);
options.ParseColorType(exifProfile);
bool isTiled = VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration);
return isTiled;
return VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration);
}
/// <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;
}
@ -224,7 +215,6 @@ internal static class TiffDecoderOptionsParser
switch (bitsPerChannel)
{
case 32:
{
if (options.SampleFormat == TiffSampleFormat.Float)
{
options.ColorType = TiffColorType.WhiteIsZero32Float;
@ -233,43 +223,30 @@ internal static class TiffDecoderOptionsParser
options.ColorType = TiffColorType.WhiteIsZero32;
break;
}
case 24:
{
options.ColorType = TiffColorType.WhiteIsZero24;
break;
}
case 16:
{
options.ColorType = TiffColorType.WhiteIsZero16;
break;
}
case 8:
{
options.ColorType = TiffColorType.WhiteIsZero8;
break;
}
case 4:
{
options.ColorType = TiffColorType.WhiteIsZero4;
break;
}
case 1:
{
options.ColorType = TiffColorType.WhiteIsZero1;
break;
}
default:
{
options.ColorType = TiffColorType.WhiteIsZero;
break;
}
}
break;
@ -291,7 +268,6 @@ internal static class TiffDecoderOptionsParser
switch (bitsPerChannel)
{
case 32:
{
if (options.SampleFormat == TiffSampleFormat.Float)
{
options.ColorType = TiffColorType.BlackIsZero32Float;
@ -300,43 +276,30 @@ internal static class TiffDecoderOptionsParser
options.ColorType = TiffColorType.BlackIsZero32;
break;
}
case 24:
{
options.ColorType = TiffColorType.BlackIsZero24;
break;
}
case 16:
{
options.ColorType = TiffColorType.BlackIsZero16;
break;
}
case 8:
{
options.ColorType = TiffColorType.BlackIsZero8;
break;
}
case 4:
{
options.ColorType = TiffColorType.BlackIsZero4;
break;
}
case 1:
{
options.ColorType = TiffColorType.BlackIsZero1;
break;
}
default:
{
options.ColorType = TiffColorType.BlackIsZero;
break;
}
}
break;
@ -535,29 +498,21 @@ internal static class TiffDecoderOptionsParser
switch (compression ?? TiffCompression.None)
{
case TiffCompression.None:
{
options.CompressionType = TiffDecoderCompressionType.None;
break;
}
case TiffCompression.PackBits:
{
options.CompressionType = TiffDecoderCompressionType.PackBits;
break;
}
case TiffCompression.Deflate:
case TiffCompression.OldDeflate:
{
options.CompressionType = TiffDecoderCompressionType.Deflate;
break;
}
case TiffCompression.Lzw:
{
options.CompressionType = TiffDecoderCompressionType.Lzw;
break;
}
case TiffCompression.CcittGroup3Fax:
{
@ -599,16 +554,13 @@ internal static class TiffDecoderOptionsParser
}
case TiffCompression.Ccitt1D:
{
options.CompressionType = TiffDecoderCompressionType.HuffmanRle;
options.BitsPerSample = new TiffBitsPerSample(1, 0, 0);
options.BitsPerPixel = 1;
break;
}
case TiffCompression.OldJpeg:
{
if (!options.OldJpegCompressionStartOfImageMarker.HasValue)
{
TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression");
@ -620,7 +572,6 @@ internal static class TiffDecoderOptionsParser
}
options.CompressionType = TiffDecoderCompressionType.OldJpeg;
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr)
{
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
@ -629,12 +580,18 @@ internal static class TiffDecoderOptionsParser
}
break;
}
case TiffCompression.Jpeg:
{
options.CompressionType = TiffDecoderCompressionType.Jpeg;
// Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this,
// so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.YcbcrSubSampling != null)
{
options.YcbcrSubSampling[0] = 1;
options.YcbcrSubSampling[1] = 1;
}
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null)
{
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
@ -643,30 +600,14 @@ internal static class TiffDecoderOptionsParser
}
break;
}
case TiffCompression.Webp:
{
options.CompressionType = TiffDecoderCompressionType.Webp;
break;
}
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported");
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/>
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);
}
}

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

@ -1,8 +1,7 @@
// Copyright (c) Six Labors.
// 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.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Performs the TIFF encoding operation.
/// </summary>
internal sealed class TiffEncoderCore : IImageEncoderInternals
internal sealed class TiffEncoderCore
{
private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian
? TiffConstants.ByteOrderLittleEndianShort
@ -50,41 +49,22 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary>
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>
/// Whether to skip metadata during encoding.
/// </summary>
private readonly bool skipMetadata;
private readonly List<(long, uint)> frameMarkers = new();
private readonly List<(long, uint)> frameMarkers = [];
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
/// <param name="options">The options for the encoder.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator)
/// <param name="configuration">The global configuration.</param>
public TiffEncoderCore(TiffEncoder options, Configuration configuration)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
@ -135,35 +115,29 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
// Determine the correct values to encode with.
// 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 =
this.HorizontalPredictor
?? rootFrameTiffMetaData.Predictor
?? DefaultPredictor;
TiffPredictor predictor = this.HorizontalPredictor ?? rootFrameTiffMetaData.Predictor;
TiffCompression compression =
this.CompressionType
?? rootFrameTiffMetaData.Compression
?? DefaultCompression;
TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression;
// Make sure, the Encoder options makes sense in combination with each other.
this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor);
// Make sure the Encoder options makes sense in combination with each other.
this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor);
using TiffStreamWriter writer = new(stream);
Span<byte> buffer = stackalloc byte[4];
long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel> metadataImage = image;
Image<TPixel>? metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
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;
}
@ -199,6 +173,8 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// <param name="frame">The tiff 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="bitsPerPixel">The bits per pixel.</param>
/// <param name="compression">The compression type.</param>
/// <param name="ifdOffset">The marker to write this IFD offset.</param>
/// <returns>
/// The next IFD offset value.
@ -207,16 +183,29 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
TiffStreamWriter writer,
ImageFrame<TPixel> frame,
ImageMetadata imageMetadata,
Image<TPixel> image,
Image<TPixel>? image,
TiffBitsPerPixel bitsPerPixel,
TiffCompression compression,
long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
{
// Get the width and height of the frame.
// This can differ from the frame bounds in-memory if the image represents only
// a subregion.
TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata();
int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width;
int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height;
width = Math.Min(width, frame.Width);
height = Math.Min(height, frame.Height);
Size encodingSize = new(width, height);
using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
this.CompressionType ?? TiffCompression.None,
compression,
writer.BaseStream,
this.memoryAllocator,
frame.Width,
(int)this.BitsPerPixel,
width,
(int)bitsPerPixel,
this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@ -224,14 +213,15 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation,
frame,
encodingSize,
this.quantizer,
this.pixelSamplingStrategy,
this.memoryAllocator,
this.configuration,
entriesCollector,
(int)this.BitsPerPixel);
(int)bitsPerPixel);
int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip);
@ -244,7 +234,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
// Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
entriesCollector.ProcessFrameInfo(frame, imageMetadata);
entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata);
entriesCollector.ProcessImageFormat(this);
if (writer.Position % 2 != 0)
@ -307,7 +297,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
}
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);
@ -354,135 +344,87 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return nextIfdMarker;
}
[MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))]
private void SanitizeAndSetEncoderOptions(
TiffBitsPerPixel? bitsPerPixel,
int inputBitsPerPixel,
TiffPhotometricInterpretation? photometricInterpretation,
TiffBitsPerPixel bitsPerPixel,
TiffPhotometricInterpretation photometricInterpretation,
TiffCompression compression,
TiffPredictor predictor)
{
// BitsPerPixel should be the primary source of truth for the encoder options.
if (bitsPerPixel.HasValue)
// Ensure 1 Bit compression is only used with 1 bit pixel type.
// Choose a sensible default based on the bits per pixel.
if (IsOneBitCompression(compression) && bitsPerPixel != TiffBitsPerPixel.Bit1)
{
switch (bitsPerPixel)
compression = bitsPerPixel switch
{
case TiffBitsPerPixel.Bit1:
if (IsOneBitCompression(compression))
{
// 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;
< TiffBitsPerPixel.Bit8 => TiffCompression.None,
_ => TiffCompression.Deflate,
};
}
// If no photometric interpretation was chosen, the input image bit per pixel should be preserved.
if (!photometricInterpretation.HasValue)
{
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.
if (inputBitsPerPixel == 8)
{
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;
}
// Ensure predictor is only used with compression that supports it.
predictor = HasPredictor(compression) ? predictor : TiffPredictor.None;
switch (photometricInterpretation)
// BitsPerPixel should be the primary source of truth for the encoder options.
switch (bitsPerPixel)
{
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
if (IsOneBitCompression(this.CompressionType))
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None);
return;
}
if (inputBitsPerPixel == 16)
case TiffBitsPerPixel.Bit1:
if (IsOneBitCompression(compression))
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit16, photometricInterpretation, compression, predictor);
return;
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, predictor);
break;
}
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor);
return;
case TiffPhotometricInterpretation.PaletteColor:
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor);
return;
case TiffPhotometricInterpretation.Rgb:
// Make sure 1 Bit compression is only used with 1 bit pixel type.
if (IsOneBitCompression(this.CompressionType))
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit4:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor);
break;
case TiffBitsPerPixel.Bit8:
// Allow any combination of the below for 8 bit images.
if (photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero
or TiffPhotometricInterpretation.WhiteIsZero
or TiffPhotometricInterpretation.PaletteColor)
{
// Invalid compression / bits per pixel combination, fallback to no compression.
compression = DefaultCompression;
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor);
break;
}
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor);
return;
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, 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, 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.PhotometricInterpretation = photometricInterpretation;
@ -492,4 +434,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
public static bool IsOneBitCompression(TiffCompression? compression)
=> compression is TiffCompression.Ccitt1D or TiffCompression.CcittGroup3Fax or TiffCompression.CcittGroup4Fax;
public static bool HasPredictor(TiffCompression? compression)
=> compression is TiffCompression.Deflate or TiffCompression.Lzw;
}

10
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata);
public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata);
public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(encoder);
@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector
{
}
public void Process(ImageFrame frame, ImageMetadata imageMetadata)
public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
{
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth)
{
Value = (uint)frame.Width
Value = (uint)encodingSize.Width
});
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength)
{
Value = (uint)frame.Height
Value = (uint)encodingSize.Height
});
this.ProcessResolution(imageMetadata);

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

@ -3,13 +3,14 @@
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Provides Tiff specific metadata information for the frame.
/// </summary>
public class TiffFrameMetadata : IDeepCloneable
public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
@ -29,38 +30,100 @@ public class TiffFrameMetadata : IDeepCloneable
this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor;
this.InkSet = other.InkSet;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
}
/// <summary>
/// Gets or sets the bits per pixel.
/// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; }
public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel;
/// <summary>
/// Gets or sets number of bits per component.
/// </summary>
public TiffBitsPerSample? BitsPerSample { get; set; }
public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample;
/// <summary>
/// Gets or sets the compression scheme used on the image data.
/// </summary>
public TiffCompression? Compression { get; set; }
public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression;
/// <summary>
/// Gets or sets the color space of the image data.
/// </summary>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; }
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.
/// </summary>
public TiffPredictor? Predictor { get; set; }
public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor;
/// <summary>
/// Gets or sets the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image.
/// </summary>
public TiffInkSet? InkSet { get; set; }
/// <summary>
/// Gets or sets the encoding width.
/// </summary>
public int EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height.
/// </summary>
public int EncodingHeight { get; set; }
/// <inheritdoc/>
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
TiffFrameMetadata frameMetadata = new();
if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue)
{
frameMetadata.EncodingWidth = metadata.EncodingWidth.Value;
frameMetadata.EncodingHeight = metadata.EncodingHeight.Value;
}
return frameMetadata;
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
// Overwrite the EXIF dimensional metadata with the encoding dimensions of the image.
destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight);
}
private static int Scale(int value, int destination, float ratio)
{
if (value <= 0)
{
return destination;
}
return Math.Min((int)MathF.Ceiling(value * ratio), destination);
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public TiffFrameMetadata DeepClone() => new(this);
/// <summary>
/// Returns a new <see cref="TiffFrameMetadata"/> instance parsed from the given Exif profile.
/// </summary>
@ -79,45 +142,75 @@ public class TiffFrameMetadata : IDeepCloneable
/// </summary>
/// <param name="meta">The tiff frame meta data.</param>
/// <param name="profile">The Exif profile containing tiff frame directory tags.</param>
internal static void Parse(TiffFrameMetadata meta, ExifProfile profile)
private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
{
if (profile != null)
meta.EncodingWidth = GetImageWidth(profile);
meta.EncodingHeight = GetImageHeight(profile);
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue)
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue)
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample?.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{
meta.Compression = (TiffCompression)compressionValue.Value;
}
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? inkSetValue))
{
meta.InkSet = (TiffInkSet)inkSetValue.Value;
}
profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor);
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{
meta.Compression = (TiffCompression)compressionValue.Value;
}
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? inkSetValue))
{
meta.InkSet = (TiffInkSet)inkSetValue.Value;
}
// Remove values, we've explicitly captured them and they could change on encode.
profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor);
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffFrameMetadata(this);
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number>? width))
{
TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
}
DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
return (int)width.Value;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number>? height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
}

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

@ -1,12 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary>
/// Provides Tiff specific metadata information for the image.
/// </summary>
public class TiffMetadata : IDeepCloneable
public class TiffMetadata : IFormatMetadata<TiffMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffMetadata"/> class.
@ -23,6 +26,11 @@ public class TiffMetadata : IDeepCloneable
{
this.ByteOrder = other.ByteOrder;
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>
@ -35,6 +43,152 @@ public class TiffMetadata : IDeepCloneable
/// </summary>
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/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this);
public TiffMetadata DeepClone() => new(this);
}

27
src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs

@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
{
private bool isDisposed;
protected TiffBaseColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
protected TiffBaseColorWriter(
ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
{
this.Width = encodingSize.Width;
this.Height = encodingSize.Height;
this.Image = image;
this.MemoryAllocator = memoryAllocator;
this.Configuration = configuration;
@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
/// </summary>
public abstract int BitsPerPixel { get; }
/// <summary>
/// Gets the width of the portion of the image to be encoded.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the portion of the image to be encoded.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the bytes per row.
/// </summary>
public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8);
public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8);
protected ImageFrame<TPixel> Image { get; }
@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip)
{
DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer");
int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip;
int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip;
uint[] stripOffsets = new uint[stripsCount];
uint[] stripByteCounts = new uint[stripsCount];
int stripIndex = 0;
compressor.Initialize(rowsPerStrip);
for (int y = 0; y < this.Image.Height; y += rowsPerStrip)
for (int y = 0; y < this.Height; y += rowsPerStrip)
{
long offset = compressor.Output.Position;
int height = Math.Min(rowsPerStrip, this.Image.Height - y);
int height = Math.Min(rowsPerStrip, this.Height - y);
this.EncodeStrip(y, height, compressor);
long endOffset = compressor.Output.Position;

19
src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs

@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
private IMemoryOwner<byte> bitStrip;
public TiffBiColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
: base(image, memoryAllocator, configuration, entriesCollector)
public TiffBiColorWriter(
ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
// Convert image to black and white.
this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), new[] { image.Clone() });
this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), [image.Clone()]);
this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg));
}
@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc/>
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
int width = this.Image.Width;
int width = this.Width;
if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax)
if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax)
{
// Special case for T4BitCompressor.
int stripPixels = width * height;
@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
int bitIndex = 0;
int byteIndex = 0;
Span<byte> outputRow = rows[(outputRowIdx * this.BytesPerRow)..];
Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row);
Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width];
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width);
for (int x = 0; x < this.Image.Width; x++)
for (int x = 0; x < this.Width; x++)
{
int shift = 7 - bitIndex;
if (pixelAsGraySpan[x] == 255)

28
src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs

@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory
public static TiffBaseColorWriter<TPixel> Create<TPixel>(
TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame<TPixel> image,
Size encodingSize,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
switch (photometricInterpretation)
=> photometricInterpretation switch
{
case TiffPhotometricInterpretation.PaletteColor:
return new TiffPaletteWriter<TPixel>(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
return bitsPerPixel switch
{
1 => new TiffBiColorWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector),
16 => new TiffGrayL16Writer<TPixel>(image, memoryAllocator, configuration, entriesCollector),
_ => new TiffGrayWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector)
};
default:
return new TiffRgbWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector);
}
}
TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter<TPixel>(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel),
TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch
{
1 => new TiffBiColorWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
16 => new TiffGrayL16Writer<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
_ => new TiffGrayWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector)
},
_ => new TiffRgbWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
};
}

21
src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs

@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
/// <summary>
/// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr).
/// </summary>
/// <typeparam name="TPixel">The tpe of pixel format.</typeparam>
internal abstract class TiffCompositeColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<byte> rowBuffer;
protected TiffCompositeColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
: base(image, memoryAllocator, configuration, entriesCollector)
protected TiffCompositeColorWriter(
ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
if (this.rowBuffer == null)
{
this.rowBuffer = this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height);
}
this.rowBuffer.Clear();
(this.rowBuffer ??= this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height)).Clear();
Span<byte> outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)];
int width = this.Image.Width;
int width = this.Width;
using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width);
Span<TPixel> stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height;
int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row);
Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width];
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++;
}

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

Loading…
Cancel
Save