From 9d39c3810d866eeab1cd6c1a830dff43b178de9d Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 12 Dec 2020 20:33:36 +0300 Subject: [PATCH] Improvements of tiff metadata - API and saving --- .../Tiff/Compression/HorizontalPredictor.cs | 2 +- .../Formats/Tiff/Compression/T4BitReader.cs | 12 +- .../Tiff/Constants/TiffResolutionUnit.cs | 26 -- .../Formats/Tiff/ITiffEncoderOptions.cs | 10 +- .../Formats/Tiff/Ifd/EntryReader.cs | 9 + .../Formats/Tiff/TiffBitsPerPixel.cs | 5 + .../Formats/Tiff/TiffDecoderCore.cs | 82 ++----- ...lpers.cs => TiffDecoderMetadataCreator.cs} | 60 ++--- ...Helpers.cs => TiffDecoderOptionsParser.cs} | 40 +--- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 6 +- .../Formats/Tiff/TiffEncoderCore.cs | 19 +- .../Tiff/TiffEncoderEntriesCollector.cs | 159 ++++++++----- .../Formats/Tiff/TiffFrameMetadata.cs | 225 ++++++++++++------ .../Tiff/TiffFrameMetadataExtensions.cs | 5 +- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 13 +- .../Formats/Tiff/TiffMetadataTests.cs | 137 ++++++++--- 16 files changed, 460 insertions(+), 350 deletions(-) delete mode 100644 src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs rename src/ImageSharp/Formats/Tiff/{TiffDecoderMetadataHelpers.cs => TiffDecoderMetadataCreator.cs} (62%) rename src/ImageSharp/Formats/Tiff/{TiffDecoderHelpers.cs => TiffDecoderOptionsParser.cs} (89%) diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index 108b6ae6e..10ac39747 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// Methods for undoing the horizontal prediction used in combination with deflate and LZW compressed TIFF images. /// - public static class HorizontalPredictor + internal static class HorizontalPredictor { /// /// Inverts the horizontal prediction. diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs index a614235be..f7b6200fa 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs @@ -5,7 +5,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Linq; + using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression @@ -673,27 +673,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression { case 4: { - return WhiteLen4TermCodes.Keys.Contains(this.value); + return WhiteLen4TermCodes.ContainsKey(this.value); } case 5: { - return WhiteLen5TermCodes.Keys.Contains(this.value); + return WhiteLen5TermCodes.ContainsKey(this.value); } case 6: { - return WhiteLen6TermCodes.Keys.Contains(this.value); + return WhiteLen6TermCodes.ContainsKey(this.value); } case 7: { - return WhiteLen7TermCodes.Keys.Contains(this.value); + return WhiteLen7TermCodes.ContainsKey(this.value); } case 8: { - return WhiteLen8TermCodes.Keys.Contains(this.value); + return WhiteLen8TermCodes.ContainsKey(this.value); } } diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs deleted file mode 100644 index 0fab224de..000000000 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants -{ - /// - /// Enumeration representing the resolution units defined by the Tiff file-format. - /// - public enum TiffResolutionUnit : ushort - { - /// - /// No absolute unit of measurement. - /// - None = 1, - - /// - /// Inch. - /// - Inch = 2, - - /// - /// Centimeter. - /// - Centimeter = 3 - } -} diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs index 37492ea31..481463136 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -11,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// internal interface ITiffEncoderOptions { + /// + /// Gets the byte order to use. + /// + ByteOrder ByteOrder { get; } + /// /// Gets the compression type to use. /// @@ -36,10 +41,5 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Gets the quantizer for creating a color palette image. /// IQuantizer Quantizer { get; } - - /// - /// Gets a value indicating whether preserve metadata. - /// - bool PreserveMetadata { get; } } } diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs index b8532e0c5..da2f96211 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -116,6 +116,15 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff private bool ReadValueOrOffset(ExifValue entry, ExifDataType rawDataType, uint count) { + if (entry.Tag == ExifTag.SubIFDOffset + || entry.Tag == ExifTag.GPSIFDOffset + /*|| entry.Tag == ExifTagValue.SubIFDs*/) + { + // todo: ignore subIfds (exif, gps) + this.stream.Skip(4); + return false; + } + if (HasExtData(entry, count)) { uint offset = this.stream.ReadUInt32(); diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs index 0e314e6ee..ea53fa061 100644 --- a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs @@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Pixel1 = 1, + /// + /// 4 bits per pixel, grayscale or indexed image. Each pixel consists of 4 bit. + /// + Pixel4 = 4, + /// /// 8 bits per pixel, grayscale or indexed image. Each pixel consists of 1 byte. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index b4350c287..c44854081 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -1,10 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; @@ -38,16 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// private readonly bool ignoreMetadata; - /// - /// The image metadata. - /// - private ImageMetadata metadata; - - /// - /// The tiff specific metadata. - /// - private TiffMetadata tiffMetaData; - /// /// The stream to decode from. /// @@ -72,6 +60,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public ushort[] BitsPerSample { get; set; } + public int ChunkyBitsPerPixel { get; set; } + /// /// Gets or sets the lookup table for RGB palette colored images. /// @@ -97,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + /// + /// Gets or sets the predictor. + /// + public TiffPredictor Predictor { get; set; } + /// public Configuration Configuration => this.configuration; @@ -122,12 +117,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff framesMetadata.Add(frameMetadata); } - this.metadata = framesMetadata.CreateMetadata(this.ignoreMetadata, tiffStream.ByteOrder); - this.SetTiffFormatMetaData(framesMetadata, tiffStream.ByteOrder); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder); // todo: tiff frames can have different sizes { - ImageFrame root = frames.First(); + ImageFrame root = frames[0]; this.Dimensions = root.Size(); foreach (ImageFrame frame in frames) { @@ -138,7 +132,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - var image = new Image(this.configuration, this.metadata, frames); + var image = new Image(this.configuration, metadata, frames); return image; } @@ -160,22 +154,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff framesMetadata.Add(meta); } - this.SetTiffFormatMetaData(framesMetadata, tiffStream.ByteOrder); - - TiffFrameMetadata root = framesMetadata.First(); - int bitsPerPixel = 0; - foreach (var bits in root.BitsPerSample) - { - bitsPerPixel += bits; - } - - return new ImageInfo(new PixelTypeInfo(bitsPerPixel), (int)root.Width, (int)root.Height, this.metadata); - } + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder); - private void SetTiffFormatMetaData(List framesMetadata, ByteOrder byteOrder) - { - this.metadata = framesMetadata.CreateMetadata(this.ignoreMetadata, byteOrder); - this.tiffMetaData = this.metadata.GetTiffMetadata(); + TiffFrameMetadata root = framesMetadata[0]; + return new ImageInfo(new PixelTypeInfo(root.BitsPerPixel), (int)root.Width, (int)root.Height, metadata); } private static TiffStream CreateStream(Stream stream) @@ -224,10 +206,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff var coreMetadata = new ImageFrameMetadata(); frameMetaData = coreMetadata.GetTiffMetadata(); frameMetaData.FrameTags.AddRange(tags); - TiffFrameMetadata tiffFormatMetaData = coreMetadata.GetFormatMetadata(TiffFormat.Instance); - TiffPredictor predictor = tiffFormatMetaData.Predictor; - this.VerifyAndParseOptions(frameMetaData); + this.VerifyAndParse(frameMetaData); int width = (int)frameMetaData.Width; int height = (int)frameMetaData.Height; @@ -239,11 +219,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, width, predictor); + this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, width); } else { - this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, width, predictor); + this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, width); } return frame; @@ -258,22 +238,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// The size (in bytes) of the required pixel buffer. private int CalculateStripBufferSize(int width, int height, int plane = -1) { - uint bitsPerPixel = 0; + int bitsPerPixel = 0; if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { DebugGuard.IsTrue(plane == -1, "Excepted Chunky planar."); - for (int i = 0; i < this.BitsPerSample.Length; i++) - { - bitsPerPixel += this.BitsPerSample[i]; - } + bitsPerPixel = this.ChunkyBitsPerPixel; } else { bitsPerPixel = this.BitsPerSample[plane]; } - int bytesPerRow = ((width * (int)bitsPerPixel) + 7) / 8; + int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; int stripBytes = bytesPerRow * height; return stripBytes; @@ -288,17 +265,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The image width. - /// The tiff predictor used. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width, TiffPredictor predictor) + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Length; int stripsPerPlane = stripOffsets.Length / stripsPerPixel; - int bitsPerPixel = 0; - foreach (var bits in this.BitsPerSample) - { - bitsPerPixel += bits; - } + int bitsPerPixel = this.ChunkyBitsPerPixel; // todo? Buffer2D pixels = frame.PixelBuffer; @@ -312,7 +284,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff stripBuffers[stripIndex] = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize); } - TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width, bitsPerPixel, predictor); + TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width, bitsPerPixel, this.Predictor); RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap); @@ -339,21 +311,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width, TiffPredictor predictor) + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); - int bitsPerPixel = 0; - foreach (var bits in this.BitsPerSample) - { - bitsPerPixel += bits; - } + int bitsPerPixel = this.ChunkyBitsPerPixel; using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean); Buffer2D pixels = frame.PixelBuffer; - TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width, bitsPerPixel, predictor); + TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width, bitsPerPixel, this.Predictor); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs similarity index 62% rename from src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs rename to src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 0eddca4bb..99337a8b2 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; -using System.Linq; -using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -13,28 +11,21 @@ using SixLabors.ImageSharp.Metadata.Profiles.Iptc; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { /// - /// The decoder metadata helper methods. + /// The decoder metadata creator. /// - internal static class TiffDecoderMetadataHelpers + internal static class TiffDecoderMetadataCreator { - public static ImageMetadata CreateMetadata(this IList frames, bool ignoreMetadata, ByteOrder byteOrder) + public static ImageMetadata Create(List frames, bool ignoreMetadata, ByteOrder byteOrder) { - var coreMetadata = new ImageMetadata(); - - TiffFrameMetadata rootFrameMetadata = frames.First(); - switch (rootFrameMetadata.ResolutionUnit) + if (frames.Count < 1) { - case TiffResolutionUnit.None: - coreMetadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; - break; - case TiffResolutionUnit.Inch: - coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; - break; - case TiffResolutionUnit.Centimeter: - coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerCentimeter; - break; + TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); } + var coreMetadata = new ImageMetadata(); + TiffFrameMetadata rootFrameMetadata = frames[0]; + + coreMetadata.ResolutionUnits = rootFrameMetadata.ResolutionUnit; if (rootFrameMetadata.HorizontalResolution != null) { coreMetadata.HorizontalResolution = rootFrameMetadata.HorizontalResolution.Value; @@ -63,6 +54,15 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } + if (coreMetadata.ExifProfile == null) + { + byte[] buf = frame.GetArray(ExifTag.SubIFDOffset, true); + if (buf != null) + { + coreMetadata.ExifProfile = new ExifProfile(buf); + } + } + if (coreMetadata.IptcProfile == null) { byte[] buf = frame.GetArray(ExifTag.IPTC, true); @@ -87,28 +87,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } private static TiffBitsPerPixel GetBitsPerPixel(TiffFrameMetadata firstFrameMetaData) - { - ushort[] bitsPerSample = firstFrameMetaData.BitsPerSample; - var bitsPerPixel = 0; - foreach (var bps in bitsPerSample) - { - bitsPerPixel += bps; - } - - if (bitsPerPixel == 24) - { - return TiffBitsPerPixel.Pixel24; - } - else if (bitsPerPixel == 8) - { - return TiffBitsPerPixel.Pixel8; - } - else if (bitsPerPixel == 1) - { - return TiffBitsPerPixel.Pixel1; - } - - return 0; - } + => (TiffBitsPerPixel)firstFrameMetaData.BitsPerPixel; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs similarity index 89% rename from src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs rename to src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 7fd76d04d..e81e51946 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -8,16 +8,16 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { /// - /// The decoder helper methods. + /// The decoder options parser. /// - internal static class TiffDecoderHelpers + internal static class TiffDecoderOptionsParser { /// /// Determines the TIFF compression and color types, and reads any associated parameters. /// /// The options. /// The IFD entries container to read the image format information for. - public static void VerifyAndParseOptions(this TiffDecoderCore options, TiffFrameMetadata entries) + public static void VerifyAndParse(this TiffDecoderCore options, TiffFrameMetadata entries) { if (entries.ExtraSamples != null) { @@ -50,15 +50,16 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - ParseCompression(options, entries.Compression); - options.PlanarConfiguration = entries.PlanarConfiguration; + options.Predictor = entries.Predictor; - ParsePhotometric(options, entries); - - ParseBitsPerSample(options, entries); + // todo: There is no default for PhotometricInterpretation, and it is required. + options.PhotometricInterpretation = entries.PhotometricInterpretation; + options.BitsPerSample = entries.BitsPerSample; + options.ChunkyBitsPerPixel = entries.BitsPerPixel; ParseColorType(options, entries); + ParseCompression(options, entries.Compression); } private static void ParseColorType(this TiffDecoderCore options, TiffFrameMetadata entries) @@ -209,29 +210,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - private static void ParseBitsPerSample(this TiffDecoderCore options, TiffFrameMetadata entries) - { - options.BitsPerSample = entries.BitsPerSample; - if (options.BitsPerSample == null) - { - if (options.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero - || options.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) - { - options.BitsPerSample = new[] { (ushort)1 }; - } - else - { - TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing."); - } - } - } - - private static void ParsePhotometric(this TiffDecoderCore options, TiffFrameMetadata entries) - { - // There is no default for PhotometricInterpretation, and it is required. - options.PhotometricInterpretation = entries.PhotometricInterpretation; - } - private static void ParseCompression(this TiffDecoderCore options, TiffCompression compression) { switch (compression) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 1ed416878..cddc962fc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -17,6 +17,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public class TiffEncoder : IImageEncoder, ITiffEncoderOptions { + /// + public ByteOrder ByteOrder { get; } = TiffEncoderCore.ByteOrder; + /// public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None; @@ -32,9 +35,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public IQuantizer Quantizer { get; set; } - /// - public bool PreserveMetadata { get; set; } - /// public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 44a3140de..c7301049d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -24,6 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// internal sealed class TiffEncoderCore : IImageEncoderInternals { + public static readonly ByteOrder ByteOrder = BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian; + + private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian + ? TiffConstants.ByteOrderLittleEndianShort + : TiffConstants.ByteOrderBigEndianShort; + /// /// Used for allocating memory during processing operations. /// @@ -54,8 +60,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// private readonly DeflateCompressionLevel compressionLevel; - private readonly bool preserveMetadata; - /// /// Initializes a new instance of the class. /// @@ -69,7 +73,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.useHorizontalPredictor = options.UseHorizontalPredictor; this.compressionLevel = options.CompressionLevel; - this.preserveMetadata = options.PreserveMetadata; } /// @@ -137,12 +140,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// The marker to write the first IFD offset. public long WriteHeader(TiffWriter writer) { - ushort byteOrderMarker = BitConverter.IsLittleEndian - ? TiffConstants.ByteOrderLittleEndianShort - : TiffConstants.ByteOrderBigEndianShort; - - writer.Write(byteOrderMarker); - writer.Write((ushort)42); + writer.Write(ByteOrderMarker); + writer.Write((ushort)TiffConstants.HeaderMagicNumber); long firstIfdMarker = writer.PlaceMarker(); return firstIfdMarker; @@ -182,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.AddStripTags(image, entriesCollector, imageDataStart, imageDataBytes); entriesCollector.ProcessImageFormat(this); - entriesCollector.ProcessGeneral(image, this.preserveMetadata); + entriesCollector.ProcessGeneral(image); writer.WriteMarker(ifdOffset, (uint)writer.Position); long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 656ce858b..41d833299 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { public List Entries { get; } = new List(); - public void ProcessGeneral(Image image, bool preserveMetadata) + public void ProcessGeneral(Image image) where TPixel : unmanaged, IPixel - => new GeneralProcessor(this).Process(image, preserveMetadata); + => new GeneralProcessor(this).Process(image); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; - public void Process(Image image, bool preserveMetadata) + public void Process(Image image) where TPixel : unmanaged, IPixel { TiffFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); @@ -67,15 +67,51 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.ProcessResolution(image.Metadata, frameMetadata); - if (preserveMetadata) + this.ProcessProfiles(image.Metadata, frameMetadata); + this.ProcessMetadata(frameMetadata); + } + + private static bool IsMetadata(ExifTag tag) + { + switch ((ExifTagValue)(ushort)tag) { - this.ProcessMetadata(frameMetadata); + case ExifTagValue.DocumentName: + case ExifTagValue.ImageDescription: + case ExifTagValue.Make: + case ExifTagValue.Model: + case ExifTagValue.Software: + case ExifTagValue.DateTime: + case ExifTagValue.Artist: + case ExifTagValue.HostComputer: + case ExifTagValue.TargetPrinter: + case ExifTagValue.XMP: + case ExifTagValue.Rating: + case ExifTagValue.RatingPercent: + case ExifTagValue.ImageID: + case ExifTagValue.Copyright: + case ExifTagValue.MDLabName: + case ExifTagValue.MDSampleInfo: + case ExifTagValue.MDPrepDate: + case ExifTagValue.MDPrepTime: + case ExifTagValue.MDFileUnits: + case ExifTagValue.SEMInfo: + case ExifTagValue.XPTitle: + case ExifTagValue.XPComment: + case ExifTagValue.XPAuthor: + case ExifTagValue.XPKeywords: + case ExifTagValue.XPSubject: + return true; + default: + return false; } } private void ProcessResolution(ImageMetadata imageMetadata, TiffFrameMetadata frameMetadata) { - SynchResolution(imageMetadata, frameMetadata); + frameMetadata.SetResolutions( + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); var xResolution = new ExifRational(ExifTagValue.XResolution) { @@ -107,6 +143,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff continue; } + switch ((ExifTagValue)(ushort)entry.Tag) + { + case ExifTagValue.SubIFDOffset: + case ExifTagValue.GPSIFDOffset: + case ExifTagValue.SubIFDs: + case ExifTagValue.XMP: + case ExifTagValue.IPTC: + case ExifTagValue.IccProfile: + continue; + } + switch (ExifTags.GetPart(entry.Tag)) { case ExifParts.ExifTags: @@ -129,71 +176,59 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - private static void SynchResolution(ImageMetadata imageMetadata, TiffFrameMetadata tiffFrameMetadata) + private void ProcessProfiles(ImageMetadata imageMetadata, TiffFrameMetadata tiffFrameMetadata) { - double xres = imageMetadata.HorizontalResolution; - double yres = imageMetadata.VerticalResolution; + if (imageMetadata.ExifProfile != null) + { + // todo: implement processing exif profile + } + else + { + tiffFrameMetadata.Remove(ExifTag.SubIFDOffset); + } - switch (imageMetadata.ResolutionUnits) + if (imageMetadata.IptcProfile != null) { - case PixelResolutionUnit.AspectRatio: - tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.None; - break; - case PixelResolutionUnit.PixelsPerInch: - tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.Inch; - break; - case PixelResolutionUnit.PixelsPerCentimeter: - tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.Centimeter; - break; - case PixelResolutionUnit.PixelsPerMeter: + imageMetadata.IptcProfile.UpdateData(); + var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte) { - tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.Centimeter; - xres = UnitConverter.MeterToCm(xres); - yres = UnitConverter.MeterToCm(yres); - } + Value = imageMetadata.IptcProfile.Data + }; - break; - default: - tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.None; - break; + this.collector.AddInternal(iptc); + } + else + { + tiffFrameMetadata.Remove(ExifTag.IPTC); } - tiffFrameMetadata.HorizontalResolution = xres; - tiffFrameMetadata.VerticalResolution = yres; - } + if (imageMetadata.IccProfile != null) + { + var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined) + { + Value = imageMetadata.IccProfile.ToByteArray() + }; - private static bool IsMetadata(ExifTag tag) - { - switch ((ExifTagValue)(ushort)tag) + this.collector.AddInternal(icc); + } + else { - case ExifTagValue.DocumentName: - case ExifTagValue.ImageDescription: - case ExifTagValue.Make: - case ExifTagValue.Model: - case ExifTagValue.Software: - case ExifTagValue.DateTime: - case ExifTagValue.Artist: - case ExifTagValue.HostComputer: - case ExifTagValue.TargetPrinter: - case ExifTagValue.XMP: - case ExifTagValue.Rating: - case ExifTagValue.RatingPercent: - case ExifTagValue.ImageID: - case ExifTagValue.Copyright: - case ExifTagValue.MDLabName: - case ExifTagValue.MDSampleInfo: - case ExifTagValue.MDPrepDate: - case ExifTagValue.MDPrepTime: - case ExifTagValue.MDFileUnits: - case ExifTagValue.SEMInfo: - case ExifTagValue.XPTitle: - case ExifTagValue.XPComment: - case ExifTagValue.XPAuthor: - case ExifTagValue.XPKeywords: - case ExifTagValue.XPSubject: - return true; - default: - return false; + tiffFrameMetadata.Remove(ExifTag.IccProfile); + } + + TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata(); + if (tiffMetadata.XmpProfile != null) + { + var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) + { + Value = tiffMetadata.XmpProfile + }; + + this.collector.AddInternal(xmp); + } + else + { + tiffFrameMetadata.Remove(ExifTag.XMP); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index 36fd3ad20..0e698c568 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -2,9 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; -using System.Linq; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff @@ -14,7 +15,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public class TiffFrameMetadata : IDeepCloneable { - private const TiffResolutionUnit DefaultResolutionUnit = TiffResolutionUnit.Inch; + // 2 (Inch) + private const ushort DefaultResolutionUnit = 2; private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; @@ -28,9 +30,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } /// - /// Gets or sets the Tiff directory tags list. + /// Gets the Tiff directory tags list. /// - public List FrameTags { get; set; } = new List(); + public List FrameTags { get; internal set; } = new List(); /// Gets a general indication of the kind of data contained in this subfile. /// A general indication of the kind of data contained in this subfile. @@ -53,7 +55,41 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets the number of bits per component. /// - public ushort[] BitsPerSample => this.GetArray(ExifTag.BitsPerSample, true); + public ushort[] BitsPerSample + { + get + { + var bits = this.GetArray(ExifTag.BitsPerSample, true); + if (bits == null) + { + if (this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero + || this.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) + { + bits = new[] { (ushort)1 }; + } + else + { + TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing."); + } + } + + return bits; + } + } + + internal int BitsPerPixel + { + get + { + int bitsPerPixel = 0; + foreach (var bits in this.BitsPerSample) + { + bitsPerPixel += bits; + } + + return bitsPerPixel; + } + } /// Gets the compression scheme used on the image data. /// The compression scheme used on the image data. @@ -116,61 +152,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Gets the resolution of the image in x- direction. /// The density of the image in x- direction. - public double? HorizontalResolution - { - get - { - if (this.TryGetSingle(ExifTag.XResolution, out Rational xResolution)) - { - return xResolution.ToDouble() * this.ResolutionFactor; - } - - return null; - } - - internal set - { - if (value == null) - { - this.Remove(ExifTag.XResolution); - } - else - { - double tag = value.Value / this.ResolutionFactor; - this.SetSingle(ExifTag.XResolution, new Rational(tag)); - } - } - } + public double? HorizontalResolution => this.GetResolution(ExifTag.XResolution); /// /// Gets the resolution of the image in y- direction. /// /// The density of the image in y- direction. - public double? VerticalResolution - { - get - { - if (this.TryGetSingle(ExifTag.YResolution, out Rational yResolution)) - { - return yResolution.ToDouble() * this.ResolutionFactor; - } - - return null; - } - - internal set - { - if (value == null) - { - this.Remove(ExifTag.YResolution); - } - else - { - double tag = value.Value / this.ResolutionFactor; - this.SetSingle(ExifTag.YResolution, new Rational(tag)); - } - } - } + public double? VerticalResolution => this.GetResolution(ExifTag.YResolution); /// /// Gets how the components of each pixel are stored. @@ -180,10 +168,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets the unit of measurement for XResolution and YResolution. /// - public TiffResolutionUnit ResolutionUnit + public PixelResolutionUnit ResolutionUnit { - get => this.GetSingleEnum(ExifTag.ResolutionUnit, DefaultResolutionUnit); - internal set => this.SetSingleEnum(ExifTag.ResolutionUnit, value); + get + { + if (!this.TryGetSingle(ExifTag.ResolutionUnit, out ushort res)) + { + res = DefaultResolutionUnit; + } + + return (PixelResolutionUnit)(res - 1); + } } /// @@ -252,23 +247,34 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public TiffSampleFormat[] SampleFormat => this.GetEnumArray(ExifTag.SampleFormat, true); - private double ResolutionFactor + + /// + /// Clears the metadata. + /// + public void ClearMetadata() { - get + var tags = new List(); + foreach (IExifValue entry in this.FrameTags) { - TiffResolutionUnit unit = this.ResolutionUnit; - if (unit == TiffResolutionUnit.Centimeter) - { - return 2.54; - } - else if (unit == TiffResolutionUnit.Inch) + switch ((ExifTagValue)(ushort)entry.Tag) { - return 1.0; + case ExifTagValue.ImageWidth: + case ExifTagValue.ImageLength: + case ExifTagValue.ResolutionUnit: + case ExifTagValue.XResolution: + case ExifTagValue.YResolution: + //// image format tags + case ExifTagValue.Predictor: + case ExifTagValue.PlanarConfiguration: + case ExifTagValue.PhotometricInterpretation: + case ExifTagValue.BitsPerSample: + case ExifTagValue.ColorMap: + tags.Add(entry); + break; } - - // DefaultResolutionUnit is Inch - return 1.0; } + + this.FrameTags = tags; } /// @@ -282,5 +288,84 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff return new TiffFrameMetadata() { FrameTags = tags }; } + + internal void SetResolutions(PixelResolutionUnit unit, double horizontal, double vertical) + { + switch (unit) + { + case PixelResolutionUnit.AspectRatio: + case PixelResolutionUnit.PixelsPerInch: + case PixelResolutionUnit.PixelsPerCentimeter: + break; + case PixelResolutionUnit.PixelsPerMeter: + { + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = UnitConverter.MeterToCm(horizontal); + vertical = UnitConverter.MeterToCm(vertical); + } + + break; + default: + unit = PixelResolutionUnit.PixelsPerInch; + break; + } + + this.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1); + this.SetResolution(ExifTag.XResolution, horizontal); + this.SetResolution(ExifTag.YResolution, vertical); + } + + private double? GetResolution(ExifTag tag) + { + if (!this.TryGetSingle(tag, out Rational resolution)) + { + return null; + } + + double res = resolution.ToDouble(); + switch (this.ResolutionUnit) + { + case PixelResolutionUnit.AspectRatio: + return 0; + case PixelResolutionUnit.PixelsPerCentimeter: + return UnitConverter.CmToInch(res); + case PixelResolutionUnit.PixelsPerMeter: + return UnitConverter.MeterToInch(res); + case PixelResolutionUnit.PixelsPerInch: + default: + // DefaultResolutionUnit is Inch + return res; + } + } + + private void SetResolution(ExifTag tag, double? value) + { + if (value == null) + { + this.Remove(tag); + return; + } + else + { + double res = value.Value; + switch (this.ResolutionUnit) + { + case PixelResolutionUnit.AspectRatio: + res = 0; + break; + case PixelResolutionUnit.PixelsPerCentimeter: + res = UnitConverter.InchToCm(res); + break; + case PixelResolutionUnit.PixelsPerMeter: + res = UnitConverter.InchToMeter(res); + break; + case PixelResolutionUnit.PixelsPerInch: + default: + break; + } + + this.SetSingle(tag, new Rational(res)); + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs index 263b8cd55..39d0f34db 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Linq; - using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff @@ -52,8 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { if (meta.TryGetArray(tag, out TTagValue[] result)) { - // todo: improve - return result.Select(a => (TEnum)(object)a).ToArray(); + return System.Array.ConvertAll(result, a => (TEnum)(object)a); } if (!optional) diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index 6f31fd549..f8df21c1e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -26,22 +26,23 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.ByteOrder = other.ByteOrder; this.XmpProfile = other.XmpProfile; this.BitsPerPixel = other.BitsPerPixel; + this.Compression = other.Compression; } /// - /// Gets or sets the byte order. + /// Gets the byte order. /// - public ByteOrder ByteOrder { get; set; } + public ByteOrder ByteOrder { get; internal set; } /// - /// Gets or sets the number of bits per pixel. + /// Gets the number of bits per pixel. /// - public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffBitsPerPixel.Pixel24; + public TiffBitsPerPixel BitsPerPixel { get; internal set; } = TiffBitsPerPixel.Pixel24; /// - /// Gets or sets the compression used to create the TIFF file. + /// Gets the compression used to create the TIFF file. /// - public TiffCompression Compression { get; set; } = TiffCompression.None; + public TiffCompression Compression { get; internal set; } = TiffCompression.None; /// /// Gets or sets the XMP profile. diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index ce20fb77d..be14f7019 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Linq; using SixLabors.ImageSharp.Formats.Experimental.Tiff; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -157,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(10, frame.HorizontalResolution); Assert.Equal(10, frame.VerticalResolution); Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration); - Assert.Equal(TiffResolutionUnit.Inch, frame.ResolutionUnit); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, frame.ResolutionUnit); Assert.Equal("IrfanView", frame.Software); Assert.Null(frame.DateTime); Assert.Equal("This is author1;Author2", frame.Artist); @@ -204,22 +207,27 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(SampleMetadata, PixelTypes.Rgba32, true)] [WithFile(SampleMetadata, PixelTypes.Rgba32, false)] - public void Tiff_PreserveMetadata(TestImageProvider provider, bool preserveMetadata) + public void PreserveMetadata(TestImageProvider provider, bool preserveMetadata) where TPixel : unmanaged, IPixel { + // Load Tiff image using Image image = provider.GetImage(new TiffDecoder() { IgnoreMetadata = false }); ImageMetadata coreMeta = image.Metadata; TiffMetadata tiffMeta = image.Metadata.GetTiffMetadata(); TiffFrameMetadata frameMeta = image.Frames.RootFrame.Metadata.GetTiffMetadata(); - var tiffEncoder = new TiffEncoder() { PreserveMetadata = preserveMetadata }; - using var ms = new MemoryStream(); + // Save to Tiff + var tiffEncoder = new TiffEncoder() { Mode = TiffEncodingMode.Rgb }; + if (!preserveMetadata) + { + ClearMeta(image); + } - // act + using var ms = new MemoryStream(); image.Save(ms, tiffEncoder); - // assert + // Assert ms.Position = 0; using var output = Image.Load(ms); @@ -227,6 +235,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff TiffMetadata tiffMetaOut = output.Metadata.GetTiffMetadata(); TiffFrameMetadata frameMetaOut = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(TiffBitsPerPixel.Pixel4, tiffMeta.BitsPerPixel); + Assert.Equal(TiffBitsPerPixel.Pixel24, tiffMetaOut.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, tiffMeta.Compression); + Assert.Equal(TiffCompression.None, tiffMetaOut.Compression); + Assert.Equal(coreMeta.HorizontalResolution, coreMetaOut.HorizontalResolution); Assert.Equal(coreMeta.VerticalResolution, coreMetaOut.VerticalResolution); Assert.Equal(coreMeta.ResolutionUnits, coreMetaOut.ResolutionUnits); @@ -237,14 +250,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(frameMeta.HorizontalResolution, frameMetaOut.HorizontalResolution); Assert.Equal(frameMeta.VerticalResolution, frameMetaOut.VerticalResolution); - Assert.Equal("IrfanView", frameMeta.Software); - Assert.Equal("This is Название", frameMeta.ImageDescription); - Assert.Equal("This is Изготовитель камеры", frameMeta.Make); - Assert.Equal("This is Авторские права", frameMeta.Copyright); + Assert.Equal("ImageSharp", frameMetaOut.Software); if (preserveMetadata) { - Assert.Equal("ImageSharp", frameMetaOut.Software); + Assert.Equal(tiffMeta.XmpProfile, tiffMetaOut.XmpProfile); + + Assert.Equal("IrfanView", frameMeta.Software); + Assert.Equal("This is Название", frameMeta.ImageDescription); + Assert.Equal("This is Изготовитель камеры", frameMeta.Make); + Assert.Equal("This is Авторские права", frameMeta.Copyright); Assert.Equal(frameMeta.ImageDescription, frameMetaOut.ImageDescription); Assert.Equal(frameMeta.Make, frameMetaOut.Make); @@ -252,7 +267,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff } else { - Assert.Equal("ImageSharp", frameMetaOut.Software); + Assert.Null(tiffMetaOut.XmpProfile); + + Assert.Null(frameMeta.Software); + Assert.Null(frameMeta.ImageDescription); + Assert.Null(frameMeta.Make); + Assert.Null(frameMeta.Copyright); Assert.Null(frameMetaOut.ImageDescription); Assert.Null(frameMetaOut.Make); @@ -263,28 +283,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [InlineData(true)] [InlineData(false)] - public void Tiff_CreateMetadata(bool preserveMetadata) + public void CreateMetadata(bool preserveMetadata) { - var tiffEncoder = new TiffEncoder { Mode = TiffEncodingMode.Default, Compression = TiffEncoderCompression.Deflate, PreserveMetadata = preserveMetadata }; - + // Create image int w = 10; int h = 20; - using Image input = new Image(w, h); - ImageMetadata coreMeta = input.Metadata; - TiffMetadata tiffMeta = input.Metadata.GetTiffMetadata(); - TiffFrameMetadata frameMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); + using Image image = new Image(w, h); + + // set metadata + ImageMetadata coreMeta = image.Metadata; + TiffMetadata tiffMeta = image.Metadata.GetTiffMetadata(); + TiffFrameMetadata frameMeta = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + + tiffMeta.XmpProfile = new byte[] { 1, 2, 3, 4, 5 }; + + coreMeta.IptcProfile = new IptcProfile(); + coreMeta.IptcProfile.SetValue(IptcTag.Caption, "iptc caption"); + + coreMeta.IccProfile = new IccProfile( + new IccProfileHeader() { CreationDate = DateTime.Now }, + new IccTagDataEntry[] + { + new IccTextTagDataEntry("test string"), + new IccDataTagDataEntry(new byte[] { 11, 22, 33, 44 }) + }); - coreMeta.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; - coreMeta.HorizontalResolution = 45; - coreMeta.VerticalResolution = 54; + coreMeta.ResolutionUnits = PixelResolutionUnit.PixelsPerMeter; + coreMeta.HorizontalResolution = 4500; + coreMeta.VerticalResolution = 5400; + var datetime = System.DateTime.Now.ToString(); frameMeta.ImageDescription = "test ImageDescription"; - frameMeta.DateTime = System.DateTime.Now.ToString(); + frameMeta.DateTime = datetime; + + // Save to Tiff + var tiffEncoder = new TiffEncoder { Mode = TiffEncodingMode.Default, Compression = TiffEncoderCompression.Deflate }; + if (!preserveMetadata) + { + ClearMeta(image); + } using var ms = new MemoryStream(); - input.Save(ms, tiffEncoder); + image.Save(ms, tiffEncoder); - // assert + // Assert ms.Position = 0; using var output = Image.Load(ms); TiffMetadata meta = output.Metadata.GetTiffMetadata(); @@ -293,9 +335,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff TiffMetadata tiffMetaOut = output.Metadata.GetTiffMetadata(); TiffFrameMetadata frameMetaOut = output.Frames.RootFrame.Metadata.GetTiffMetadata(); - Assert.Equal(coreMeta.HorizontalResolution, coreMetaOut.HorizontalResolution); - Assert.Equal(coreMeta.VerticalResolution, coreMetaOut.VerticalResolution); - Assert.Equal(coreMeta.ResolutionUnits, coreMetaOut.ResolutionUnits); + Assert.Equal(PixelResolutionUnit.PixelsPerCentimeter, coreMetaOut.ResolutionUnits); + Assert.Equal(45, coreMetaOut.HorizontalResolution); + Assert.Equal(54, coreMetaOut.VerticalResolution, 8); + + //// Assert.Equal(tiffEncoder.Compression, tiffMetaOut.Compression); + Assert.Equal(TiffBitsPerPixel.Pixel24, tiffMetaOut.BitsPerPixel); Assert.Equal((uint)w, frameMetaOut.Width); Assert.Equal((uint)h, frameMetaOut.Height); @@ -303,16 +348,52 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(frameMeta.HorizontalResolution, frameMetaOut.HorizontalResolution); Assert.Equal(frameMeta.VerticalResolution, frameMetaOut.VerticalResolution); + Assert.Equal("ImageSharp", frameMetaOut.Software); + if (preserveMetadata) { + Assert.NotNull(tiffMeta.XmpProfile); + Assert.NotNull(coreMeta.IptcProfile); + Assert.NotNull(coreMeta.IccProfile); + + Assert.Equal(tiffMeta.XmpProfile, tiffMetaOut.XmpProfile); + //// todo: failure Assert.Equal(coreMeta.IptcProfile, coreMetaOut.IptcProfile); + Assert.Equal(coreMeta.IptcProfile.Data, coreMetaOut.IptcProfile.Data); + Assert.Equal(coreMeta.IccProfile.ToByteArray(), coreMetaOut.IccProfile.ToByteArray()); + + Assert.Equal("test ImageDescription", frameMeta.ImageDescription); + Assert.Equal(datetime, frameMeta.DateTime); + Assert.Equal(frameMeta.ImageDescription, frameMetaOut.ImageDescription); Assert.Equal(frameMeta.DateTime, frameMetaOut.DateTime); } else { + Assert.Null(tiffMetaOut.XmpProfile); + Assert.Null(coreMetaOut.IptcProfile); + Assert.Null(coreMetaOut.IccProfile); + + Assert.Null(frameMeta.ImageDescription); + Assert.Null(frameMeta.DateTime); + Assert.Null(frameMetaOut.ImageDescription); Assert.Null(frameMetaOut.DateTime); } } + + private static void ClearMeta(Image image) + { + ImageMetadata coreMeta = image.Metadata; + TiffMetadata tiffMeta = image.Metadata.GetTiffMetadata(); + TiffFrameMetadata frameMeta = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + + coreMeta.ExifProfile = null; + coreMeta.IccProfile = null; + coreMeta.IptcProfile = null; + + tiffMeta.XmpProfile = null; + + frameMeta.ClearMetadata(); + } } }