Browse Source

Begin migrating TiffMetadata

pull/2751/head
James Jackson-South 2 years ago
parent
commit
fdcd60200e
  1. 29
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  2. 24
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  3. 8
      src/ImageSharp/Formats/IFormatMetadata.cs
  4. 22
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  5. 20
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  6. 22
      src/ImageSharp/Formats/Png/PngMetadata.cs
  7. 20
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  8. 21
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  9. 45
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  10. 2
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  11. 8
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  12. 78
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  13. 2
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  14. 239
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  15. 13
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  16. 18
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  17. 2
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs
  18. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs
  19. 36
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  20. 17
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

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

@ -50,15 +50,18 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>, IFormatFrameMetadata<Bm
<= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 }, <= 8 => new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel8 },
<= 16 => new BmpMetadata <= 16 => new BmpMetadata
{ {
BitsPerPixel = BmpBitsPerPixel.Pixel16, InfoHeaderType = BmpInfoHeaderType.WinVersion3 BitsPerPixel = BmpBitsPerPixel.Pixel16,
InfoHeaderType = BmpInfoHeaderType.WinVersion3
}, },
<= 24 => new BmpMetadata <= 24 => new BmpMetadata
{ {
BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.WinVersion4 BitsPerPixel = BmpBitsPerPixel.Pixel24,
InfoHeaderType = BmpInfoHeaderType.WinVersion4
}, },
_ => new BmpMetadata _ => new BmpMetadata
{ {
BitsPerPixel = BmpBitsPerPixel.Pixel32, InfoHeaderType = BmpInfoHeaderType.WinVersion5 BitsPerPixel = BmpBitsPerPixel.Pixel32,
InfoHeaderType = BmpInfoHeaderType.WinVersion5
} }
}; };
} }
@ -68,7 +71,7 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>, IFormatFrameMetadata<Bm
=> new(); => new();
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp = (int)this.BitsPerPixel; int bpp = (int)this.BitsPerPixel;
@ -122,17 +125,21 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>, IFormatFrameMetadata<Bm
break; break;
} }
return new() return new PixelTypeInfo(bpp)
{ {
PixelTypeInfo = new PixelTypeInfo(bpp) AlphaRepresentation = alpha,
{ ComponentInfo = info,
AlphaRepresentation = alpha, ColorType = color
ComponentInfo = info,
ColorType = color
}
}; };
} }
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new(); => new();

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

@ -126,6 +126,20 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
}; };
} }
/// <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/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public FormatConnectingMetadata ToFormatConnectingMetadata()
{ {
@ -133,21 +147,13 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex] ? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex]
: Color.Transparent; : Color.Transparent;
int bpp = this.GlobalColorTable.HasValue
? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8)
: 8;
return new() return new()
{ {
AnimateRootFrame = true, AnimateRootFrame = true,
BackgroundColor = color, BackgroundColor = color,
ColorTable = this.GlobalColorTable, ColorTable = this.GlobalColorTable,
ColorTableMode = this.ColorTableMode, ColorTableMode = this.ColorTableMode,
PixelTypeInfo = new PixelTypeInfo(bpp) PixelTypeInfo = this.GetPixelTypeInfo(),
{
ColorType = PixelColorType.Indexed,
ComponentInfo = PixelComponentInfo.Create(1, bpp, bpp),
},
RepeatCount = this.RepeatCount, RepeatCount = this.RepeatCount,
}; };
} }

8
src/ImageSharp/Formats/IFormatMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
@ -8,6 +10,12 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary> /// </summary>
public interface IFormatMetadata : IDeepCloneable public interface IFormatMetadata : IDeepCloneable
{ {
/// <summary>
/// Converts the metadata to a <see cref="PixelTypeInfo"/> instance.
/// </summary>
/// <returns>The pixel type info.</returns>
PixelTypeInfo GetPixelTypeInfo();
/// <summary> /// <summary>
/// Converts the metadata to a <see cref="FormatConnectingMetadata"/> instance. /// Converts the metadata to a <see cref="FormatConnectingMetadata"/> instance.
/// </summary> /// </summary>

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

@ -139,7 +139,7 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp; int bpp;
PixelColorType colorType; PixelColorType colorType;
@ -173,18 +173,22 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
break; break;
} }
return new FormatConnectingMetadata return new PixelTypeInfo(bpp)
{ {
PixelTypeInfo = new PixelTypeInfo(bpp) AlphaRepresentation = PixelAlphaRepresentation.None,
{ ColorType = colorType,
AlphaRepresentation = PixelAlphaRepresentation.None, ComponentInfo = info,
ColorType = colorType,
ComponentInfo = info,
},
Quality = this.Quality,
}; };
} }
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
Quality = this.Quality,
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -85,7 +85,7 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp; int bpp;
PixelColorType colorType; PixelColorType colorType;
@ -114,17 +114,21 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
break; break;
} }
return new FormatConnectingMetadata return new PixelTypeInfo(bpp)
{ {
PixelTypeInfo = new PixelTypeInfo(bpp) AlphaRepresentation = PixelAlphaRepresentation.None,
{ ColorType = colorType,
AlphaRepresentation = PixelAlphaRepresentation.None, ComponentInfo = info,
ColorType = colorType,
ComponentInfo = info,
},
}; };
} }
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -191,7 +191,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp; int bpp;
PixelColorType colorType; PixelColorType colorType;
@ -262,19 +262,23 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
break; break;
} }
return new() return new PixelTypeInfo(bpp)
{
AlphaRepresentation = alpha,
ColorType = colorType,
ComponentInfo = info,
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{ {
ColorTable = this.ColorTable, ColorTable = this.ColorTable,
ColorTableMode = FrameColorTableMode.Global, ColorTableMode = FrameColorTableMode.Global,
PixelTypeInfo = new PixelTypeInfo(bpp) PixelTypeInfo = this.GetPixelTypeInfo(),
{
AlphaRepresentation = alpha,
ColorType = colorType,
ComponentInfo = info,
},
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
}; };
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -51,7 +51,7 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp; int bpp;
PixelColorType colorType; PixelColorType colorType;
@ -73,17 +73,21 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
break; break;
} }
return new() return new PixelTypeInfo(bpp)
{ {
PixelTypeInfo = new PixelTypeInfo(bpp) AlphaRepresentation = alpha,
{ ColorType = colorType,
AlphaRepresentation = alpha, ComponentInfo = info,
ColorType = colorType,
ComponentInfo = info,
}
}; };
} }
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -49,10 +49,9 @@ public class TgaMetadata : IFormatMetadata<TgaMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() public PixelTypeInfo GetPixelTypeInfo()
{ {
int bpp = (int)this.BitsPerPixel; int bpp = (int)this.BitsPerPixel;
PixelComponentInfo info; PixelComponentInfo info;
PixelColorType color; PixelColorType color;
PixelAlphaRepresentation alpha; PixelAlphaRepresentation alpha;
@ -80,17 +79,21 @@ public class TgaMetadata : IFormatMetadata<TgaMetadata>
break; break;
} }
return new() return new PixelTypeInfo(bpp)
{ {
PixelTypeInfo = new PixelTypeInfo(bpp) AlphaRepresentation = alpha,
{ ComponentInfo = info,
AlphaRepresentation = alpha, ColorType = color
ComponentInfo = info,
ColorType = color
}
}; };
} }
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -34,27 +34,27 @@ public class TiffFrameMetadata : IDeepCloneable
/// <summary> /// <summary>
/// Gets or sets the bits per pixel. /// Gets or sets the bits per pixel.
/// </summary> /// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; } public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel;
/// <summary> /// <summary>
/// Gets or sets number of bits per component. /// Gets or sets number of bits per component.
/// </summary> /// </summary>
public TiffBitsPerSample? BitsPerSample { get; set; } public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample;
/// <summary> /// <summary>
/// Gets or sets the compression scheme used on the image data. /// Gets or sets the compression scheme used on the image data.
/// </summary> /// </summary>
public TiffCompression? Compression { get; set; } public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression;
/// <summary> /// <summary>
/// Gets or sets the color space of the image data. /// Gets or sets the color space of the image data.
/// </summary> /// </summary>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation;
/// <summary> /// <summary>
/// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied.
/// </summary> /// </summary>
public TiffPredictor? Predictor { get; set; } public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor;
/// <summary> /// <summary>
/// Gets or sets the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image. /// Gets or sets the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image.
@ -89,7 +89,7 @@ public class TiffFrameMetadata : IDeepCloneable
meta.BitsPerSample = bitsPerSample; meta.BitsPerSample = bitsPerSample;
} }
meta.BitsPerPixel = meta.BitsPerSample?.BitsPerPixel(); meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue)) if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{ {
@ -111,6 +111,7 @@ public class TiffFrameMetadata : IDeepCloneable
meta.InkSet = (TiffInkSet)inkSetValue.Value; meta.InkSet = (TiffInkSet)inkSetValue.Value;
} }
// TODO: Why do we remove this? Encoding should overwrite.
profile.RemoveValue(ExifTag.BitsPerSample); profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression); profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation); profile.RemoveValue(ExifTag.PhotometricInterpretation);

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

@ -1,12 +1,14 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
/// <summary> /// <summary>
/// Provides Tiff specific metadata information for the image. /// Provides Tiff specific metadata information for the image.
/// </summary> /// </summary>
public class TiffMetadata : IDeepCloneable public class TiffMetadata : IFormatMetadata<TiffMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffMetadata"/> class. /// Initializes a new instance of the <see cref="TiffMetadata"/> class.
@ -36,5 +38,17 @@ public class TiffMetadata : IDeepCloneable
public TiffFormatType FormatType { get; set; } public TiffFormatType FormatType { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this); public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException();
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException();
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException();
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public TiffMetadata DeepClone() => new(this);
} }

2
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs

@ -30,7 +30,7 @@ public abstract class TiffEncoderBaseTester
using Image<TPixel> input = provider.GetImage(); using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream(); using var memStream = new MemoryStream();
TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata();
TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; TiffCompression inputCompression = inputMeta.Compression;
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);

4
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs

@ -15,7 +15,7 @@ public class TiffEncoderHeaderTests
public void WriteHeader_WritesValidHeader() public void WriteHeader_WritesValidHeader()
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); TiffEncoderCore encoder = new(Encoder, Configuration.Default);
using (TiffStreamWriter writer = new(stream)) using (TiffStreamWriter writer = new(stream))
{ {
@ -29,7 +29,7 @@ public class TiffEncoderHeaderTests
public void WriteHeader_ReturnsFirstIfdMarker() public void WriteHeader_ReturnsFirstIfdMarker()
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); TiffEncoderCore encoder = new(Encoder, Configuration.Default);
using TiffStreamWriter writer = new(stream); using TiffStreamWriter writer = new(stream);
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]); long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]);

36
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -31,7 +31,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel) public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel)
{ {
// arrange // arrange
TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation }; TiffEncoder tiffEncoder = new() { BitsPerPixel = expectedBitsPerPixel, PhotometricInterpretation = photometricInterpretation };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10) ? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10); : new Image<Rgb24>(10, 10);
@ -57,8 +57,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel)
{ {
// arrange // arrange
TiffEncoder tiffEncoder = new() TiffEncoder tiffEncoder = new() { BitsPerPixel = bitsPerPixel };
{ BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10); using Image input = new Image<Rgb24>(10, 10);
using MemoryStream memStream = new(); using MemoryStream memStream = new();
@ -156,7 +155,11 @@ public class TiffEncoderTests : TiffEncoderBaseTester
{ {
// arrange // arrange
TiffEncoder tiffEncoder = new() TiffEncoder tiffEncoder = new()
{ PhotometricInterpretation = photometricInterpretation, Compression = compression }; {
BitsPerPixel = expectedBitsPerPixel,
PhotometricInterpretation = photometricInterpretation,
Compression = compression
};
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10) ? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10); : new Image<Rgb24>(10, 10);
@ -199,25 +202,6 @@ public class TiffEncoderTests : TiffEncoderBaseTester
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
} }
[Fact]
public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8()
{
// arrange
TiffEncoder tiffEncoder = new();
using Image input = new Image<L8>(10, 10);
using MemoryStream memStream = new();
const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
}
[Theory] [Theory]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.None)] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.None)]
[WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffCompression.Lzw)] [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffCompression.Lzw)]
@ -241,11 +225,11 @@ public class TiffEncoderTests : TiffEncoderBaseTester
} }
[Theory] [Theory]
[WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, null)] [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffPredictor.None)]
[WithFile(RgbLzwPredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] [WithFile(RgbLzwPredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)]
[WithFile(RgbDeflate, PixelTypes.Rgba32, null)] [WithFile(RgbDeflate, PixelTypes.Rgba32, TiffPredictor.None)]
[WithFile(RgbDeflatePredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] [WithFile(RgbDeflatePredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)]
public void TiffEncoder_PreservesPredictor<TPixel>(TestImageProvider<TPixel> provider, TiffPredictor? expectedPredictor) public void TiffEncoder_PreservesPredictor<TPixel>(TestImageProvider<TPixel> provider, TiffPredictor expectedPredictor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange

17
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -697,23 +697,6 @@ public static class TestImageExtensions
return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf<TPixel>()); return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf<TPixel>());
} }
internal static Image<Rgba32> ToGrayscaleImage(this Buffer2D<float> buffer, float scale)
{
Image<Rgba32> image = new(buffer.Width, buffer.Height);
Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> pixelMem));
Span<Rgba32> pixels = pixelMem.Span;
Span<float> bufferSpan = buffer.DangerousGetSingleSpan();
for (int i = 0; i < bufferSpan.Length; i++)
{
float value = bufferSpan[i] * scale;
pixels[i] = Rgba32.FromVector4(new Vector4(value, value, value, 1f));
}
return image;
}
private class MakeOpaqueProcessor : IImageProcessor private class MakeOpaqueProcessor : IImageProcessor
{ {
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)

Loading…
Cancel
Save