diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 1ef2478e3d..28565cac47 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/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 iptcBytesSpan = iptcBytes.AsSpan(); Span buffer = stackalloc byte[4]; for (int i = 0; i < iptcBytes.Length; i += 4) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index c702252cc7..546ff7947d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -339,6 +339,20 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals TiffCompression compression, TiffPredictor predictor) { + // 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) + { + compression = bitsPerPixel switch + { + < TiffBitsPerPixel.Bit8 => TiffCompression.None, + _ => TiffCompression.Deflate, + }; + } + + // Ensure predictor is only used with compression that supports it. + predictor = HasPredictor(compression) ? predictor : TiffPredictor.None; + // BitsPerPixel should be the primary source of truth for the encoder options. switch (bitsPerPixel) { @@ -346,14 +360,14 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals if (IsOneBitCompression(compression)) { // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, predictor); break; } - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); break; case TiffBitsPerPixel.Bit4: - this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); break; case TiffBitsPerPixel.Bit8: @@ -381,23 +395,17 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals 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); + + 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, TiffPredictor.None); + this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, predictor); 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 = TiffCompression.None; - } } [MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] @@ -415,4 +423,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; } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index b95b9f92f1..cc70941d51 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -1,6 +1,7 @@ // 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; @@ -25,6 +26,11 @@ public class TiffMetadata : IFormatMetadata { 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; } /// @@ -37,14 +43,142 @@ public class TiffMetadata : IFormatMetadata /// public TiffFormatType FormatType { get; set; } + /// + /// Gets or sets the bits per pixel. Derived from the root frame. + /// + public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffConstants.DefaultBitsPerPixel; + + /// + /// Gets or sets number of bits per component. Derived from the root frame. + /// + public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + + /// + /// Gets or sets the compression scheme used on the image data. Derived from the root frame. + /// + public TiffCompression Compression { get; set; } = TiffConstants.DefaultCompression; + + /// + /// Gets or sets the color space of the image data. Derived from the root frame. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } = TiffConstants.DefaultPhotometricInterpretation; + + /// + /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. + /// Derived from the root frame. + /// + public TiffPredictor Predictor { get; set; } = TiffConstants.DefaultPredictor; + /// - public static TiffMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + 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 + } + }; + } /// - public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + 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 + }; + } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo() + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();