From 688d3dde9d39f12f5c9ba894daabbabd0a5d3039 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Thu, 10 Dec 2020 23:08:02 +0300 Subject: [PATCH] WIP Metadata refactory - implement TiffEncoderEntriesCollector; implement setters for metadata properties and possibility of saving --- .../Formats/Tiff/ITiffEncoderOptions.cs | 5 + .../Formats/Tiff/TiffDecoderCore.cs | 33 +- .../Formats/Tiff/TiffDecoderHelpers.cs | 72 ---- .../Tiff/TiffDecoderMetadataHelpers.cs | 114 ++++++ src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 3 + .../Formats/Tiff/TiffEncoderCore.cs | 197 ++--------- .../Tiff/TiffEncoderEntriesCollector.cs | 328 ++++++++++++++++++ .../Formats/Tiff/TiffFrameMetadata.cs | 236 +++++-------- .../Tiff/TiffFrameMetadataExtensions.cs | 201 +++++++++++ src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 3 +- .../Formats/Tiff/Utils/TiffWriter.cs | 8 +- .../Formats/Tiff/TiffMetadataTests.cs | 119 ++++++- 12 files changed, 897 insertions(+), 422 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs create mode 100644 src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs create mode 100644 src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs index 46fbf1c576..37492ea31c 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -36,5 +36,10 @@ 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/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index a62c5946f7..b4350c287f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -154,7 +155,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff var framesMetadata = new List(); foreach (IExifValue[] ifd in directories) { - framesMetadata.Add(new TiffFrameMetadata() { Tags = ifd }); + var meta = new TiffFrameMetadata(); + meta.FrameTags.AddRange(ifd); + framesMetadata.Add(meta); } this.SetTiffFormatMetaData(framesMetadata, tiffStream.ByteOrder); @@ -173,32 +176,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { this.metadata = framesMetadata.CreateMetadata(this.ignoreMetadata, byteOrder); this.tiffMetaData = this.metadata.GetTiffMetadata(); - TiffFrameMetadata firstFrameMetaData = framesMetadata.First(); - this.SetBitsPerPixel(firstFrameMetaData); - this.tiffMetaData.Compression = firstFrameMetaData.Compression; - } - - private void SetBitsPerPixel(TiffFrameMetadata firstFrameMetaData) - { - ushort[] bitsPerSample = firstFrameMetaData.BitsPerSample; - var bitsPerPixel = 0; - foreach (var bps in bitsPerSample) - { - bitsPerPixel += bps; - } - - if (bitsPerPixel == 24) - { - this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel24; - } - else if (bitsPerPixel == 8) - { - this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel8; - } - else if (bitsPerPixel == 1) - { - this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel1; - } } private static TiffStream CreateStream(Stream stream) @@ -246,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { var coreMetadata = new ImageFrameMetadata(); frameMetaData = coreMetadata.GetTiffMetadata(); - frameMetaData.Tags = tags; + frameMetaData.FrameTags.AddRange(tags); TiffFrameMetadata tiffFormatMetaData = coreMetadata.GetFormatMetadata(TiffFormat.Instance); TiffPredictor predictor = tiffFormatMetaData.Predictor; diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs index 25f43a0a82..7fd76d04d4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs @@ -1,15 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Collections.Generic; -using System.Linq; - using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Iptc; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { @@ -18,72 +12,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// internal static class TiffDecoderHelpers { - public static ImageMetadata CreateMetadata(this IList frames, bool ignoreMetadata, ByteOrder byteOrder) - { - var coreMetadata = new ImageMetadata(); - TiffMetadata tiffMetadata = coreMetadata.GetTiffMetadata(); - tiffMetadata.ByteOrder = byteOrder; - - TiffFrameMetadata rootFrameMetadata = frames.First(); - switch (rootFrameMetadata.ResolutionUnit) - { - case TiffResolutionUnit.None: - coreMetadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; - break; - case TiffResolutionUnit.Inch: - coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; - break; - case TiffResolutionUnit.Centimeter: - coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerCentimeter; - break; - } - - if (rootFrameMetadata.HorizontalResolution != null) - { - coreMetadata.HorizontalResolution = rootFrameMetadata.HorizontalResolution.Value; - } - - if (rootFrameMetadata.VerticalResolution != null) - { - coreMetadata.VerticalResolution = rootFrameMetadata.VerticalResolution.Value; - } - - if (!ignoreMetadata) - { - foreach (TiffFrameMetadata frame in frames) - { - if (tiffMetadata.XmpProfile == null) - { - byte[] buf = frame.GetArray(ExifTag.XMP, true); - if (buf != null) - { - tiffMetadata.XmpProfile = buf; - } - } - - if (coreMetadata.IptcProfile == null) - { - byte[] buf = frame.GetArray(ExifTag.IPTC, true); - if (buf != null) - { - coreMetadata.IptcProfile = new IptcProfile(buf); - } - } - - if (coreMetadata.IccProfile == null) - { - byte[] buf = frame.GetArray(ExifTag.IccProfile, true); - if (buf != null) - { - coreMetadata.IccProfile = new IccProfile(buf); - } - } - } - } - - return coreMetadata; - } - /// /// Determines the TIFF compression and color types, and reads any associated parameters. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs new file mode 100644 index 0000000000..0eddca4bb7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataHelpers.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// 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; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff +{ + /// + /// The decoder metadata helper methods. + /// + internal static class TiffDecoderMetadataHelpers + { + public static ImageMetadata CreateMetadata(this IList frames, bool ignoreMetadata, ByteOrder byteOrder) + { + var coreMetadata = new ImageMetadata(); + + TiffFrameMetadata rootFrameMetadata = frames.First(); + switch (rootFrameMetadata.ResolutionUnit) + { + case TiffResolutionUnit.None: + coreMetadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; + break; + case TiffResolutionUnit.Inch: + coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; + break; + case TiffResolutionUnit.Centimeter: + coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerCentimeter; + break; + } + + if (rootFrameMetadata.HorizontalResolution != null) + { + coreMetadata.HorizontalResolution = rootFrameMetadata.HorizontalResolution.Value; + } + + if (rootFrameMetadata.VerticalResolution != null) + { + coreMetadata.VerticalResolution = rootFrameMetadata.VerticalResolution.Value; + } + + TiffMetadata tiffMetadata = coreMetadata.GetTiffMetadata(); + tiffMetadata.ByteOrder = byteOrder; + tiffMetadata.BitsPerPixel = GetBitsPerPixel(rootFrameMetadata); + tiffMetadata.Compression = rootFrameMetadata.Compression; + + if (!ignoreMetadata) + { + foreach (TiffFrameMetadata frame in frames) + { + if (tiffMetadata.XmpProfile == null) + { + byte[] buf = frame.GetArray(ExifTag.XMP, true); + if (buf != null) + { + tiffMetadata.XmpProfile = buf; + } + } + + if (coreMetadata.IptcProfile == null) + { + byte[] buf = frame.GetArray(ExifTag.IPTC, true); + if (buf != null) + { + coreMetadata.IptcProfile = new IptcProfile(buf); + } + } + + if (coreMetadata.IccProfile == null) + { + byte[] buf = frame.GetArray(ExifTag.IccProfile, true); + if (buf != null) + { + coreMetadata.IccProfile = new IccProfile(buf); + } + } + } + } + + return coreMetadata; + } + + 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; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 0f333679e9..1ed416878d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -32,6 +32,9 @@ 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 1d16d51c4d..44a3140deb 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -54,6 +54,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// private readonly DeflateCompressionLevel compressionLevel; + private readonly bool preserveMetadata; + /// /// Initializes a new instance of the class. /// @@ -67,22 +69,25 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.useHorizontalPredictor = options.UseHorizontalPredictor; this.compressionLevel = options.CompressionLevel; + this.preserveMetadata = options.PreserveMetadata; } /// - /// Gets or sets the photometric interpretation implementation to use when encoding the image. + /// Gets the photometric interpretation implementation to use when encoding the image. /// - private TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + internal TiffPhotometricInterpretation PhotometricInterpretation { get; private set; } /// /// Gets the compression implementation to use when encoding the image. /// - private TiffEncoderCompression CompressionType { get; } + internal TiffEncoderCompression CompressionType { get; } /// - /// Gets or sets the encoding mode to use. RGB, RGB with color palette or gray. + /// Gets the encoding mode to use. RGB, RGB with color palette or gray. /// - private TiffEncodingMode Mode { get; set; } + internal TiffEncodingMode Mode { get; private set; } + + internal bool UseHorizontalPredictor => this.useHorizontalPredictor; /// /// Encodes the image to the specified stream from the . @@ -154,8 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public long WriteImage(TiffWriter writer, Image image, long ifdOffset) where TPixel : unmanaged, IPixel { - IExifValue colorMap = null; - var ifdEntries = new List(); + var entriesCollector = new TiffEncoderEntriesCollector(); // Write the image bytes to the steam. var imageDataStart = (uint)writer.Position; @@ -163,7 +167,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff switch (this.Mode) { case TiffEncodingMode.ColorPalette: - imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.CompressionType, this.compressionLevel, this.useHorizontalPredictor, out colorMap); + imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.CompressionType, this.compressionLevel, this.useHorizontalPredictor, entriesCollector); break; case TiffEncodingMode.Gray: imageDataBytes = writer.WriteGray(image, this.CompressionType, this.compressionLevel, this.useHorizontalPredictor); @@ -176,15 +180,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff break; } - // Write info's about the image to the stream. - this.AddImageFormat(image, ifdEntries, imageDataStart, imageDataBytes); - if (this.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor) - { - ifdEntries.Add(colorMap); - } + this.AddStripTags(image, entriesCollector, imageDataStart, imageDataBytes); + entriesCollector.ProcessImageFormat(this); + entriesCollector.ProcessGeneral(image, this.preserveMetadata); writer.WriteMarker(ifdOffset, (uint)writer.Position); - long nextIfdMarker = this.WriteIfd(writer, ifdEntries); + long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); return nextIfdMarker + imageDataBytes; } @@ -250,51 +251,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Adds image format information to the specified IFD. /// /// The pixel format. - /// The to encode from. - /// The image format entries to add to the IFD. + /// The to encode from. + /// The entries collector. /// The start of the image data in the stream. /// The image data in bytes to write. - public void AddImageFormat(Image image, List ifdEntries, uint imageDataStartOffset, int imageDataBytes) + public void AddStripTags(Image image, TiffEncoderEntriesCollector entriesCollector, uint imageDataStartOffset, int imageDataBytes) where TPixel : unmanaged, IPixel { - var width = new ExifLong(ExifTagValue.ImageWidth) - { - Value = (uint)image.Width - }; - - var height = new ExifLong(ExifTagValue.ImageLength) - { - Value = (uint)image.Height - }; - - ushort[] bitsPerSampleValue = this.GetBitsPerSampleValue(); - var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) - { - Value = bitsPerSampleValue - }; - - ushort compressionType = this.GetCompressionType(); - var compression = new ExifShort(ExifTagValue.Compression) - { - Value = compressionType - }; - - var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) - { - Value = (ushort)this.PhotometricInterpretation - }; - var stripOffsets = new ExifLongArray(ExifTagValue.StripOffsets) { // TODO: we only write one image strip for the start. Value = new[] { imageDataStartOffset } }; - var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) - { - Value = this.GetSamplesPerPixel() - }; - var rowsPerStrip = new ExifLong(ExifTagValue.RowsPerStrip) { // All rows in one strip. @@ -306,51 +275,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff Value = new[] { (uint)imageDataBytes } }; - var xResolution = new ExifRational(ExifTagValue.XResolution) - { - // TODO: This field is required according to the spec, what to use here as a default? - Value = Rational.FromDouble(1.0d) - }; - - var yResolution = new ExifRational(ExifTagValue.YResolution) - { - // TODO: This field is required according to the spec, what to use here as a default? - Value = Rational.FromDouble(1.0d) - }; - - var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit) - { - Value = 3 // 3 is centimeter. - }; - - var software = new ExifString(ExifTagValue.Software) - { - Value = "ImageSharp" - }; - - ifdEntries.Add(width); - ifdEntries.Add(height); - ifdEntries.Add(bitPerSample); - ifdEntries.Add(compression); - ifdEntries.Add(photometricInterpretation); - ifdEntries.Add(stripOffsets); - ifdEntries.Add(samplesPerPixel); - ifdEntries.Add(rowsPerStrip); - ifdEntries.Add(stripByteCounts); - ifdEntries.Add(xResolution); - ifdEntries.Add(yResolution); - ifdEntries.Add(resolutionUnit); - ifdEntries.Add(software); - - if (this.useHorizontalPredictor) - { - if (this.Mode == TiffEncodingMode.Rgb || this.Mode == TiffEncodingMode.Gray || this.Mode == TiffEncodingMode.ColorPalette) - { - var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; - - ifdEntries.Add(predictor); - } - } + entriesCollector.Add(stripOffsets); + entriesCollector.Add(rowsPerStrip); + entriesCollector.Add(stripByteCounts); } private void SetPhotometricInterpretation() @@ -381,85 +308,5 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff break; } } - - private uint GetSamplesPerPixel() - { - switch (this.PhotometricInterpretation) - { - case TiffPhotometricInterpretation.Rgb: - return 3; - case TiffPhotometricInterpretation.PaletteColor: - case TiffPhotometricInterpretation.BlackIsZero: - case TiffPhotometricInterpretation.WhiteIsZero: - return 1; - default: - return 3; - } - } - - private ushort[] GetBitsPerSampleValue() - { - switch (this.PhotometricInterpretation) - { - case TiffPhotometricInterpretation.PaletteColor: - return new ushort[] { 8 }; - case TiffPhotometricInterpretation.Rgb: - return new ushort[] { 8, 8, 8 }; - case TiffPhotometricInterpretation.WhiteIsZero: - if (this.Mode == TiffEncodingMode.BiColor) - { - return new ushort[] { 1 }; - } - - return new ushort[] { 8 }; - case TiffPhotometricInterpretation.BlackIsZero: - if (this.Mode == TiffEncodingMode.BiColor) - { - return new ushort[] { 1 }; - } - - return new ushort[] { 8 }; - default: - return new ushort[] { 8, 8, 8 }; - } - } - - private ushort GetCompressionType() - { - switch (this.CompressionType) - { - case TiffEncoderCompression.Deflate: - // Deflate is allowed for all modes. - return (ushort)TiffCompression.Deflate; - case TiffEncoderCompression.PackBits: - // PackBits is allowed for all modes. - return (ushort)TiffCompression.PackBits; - case TiffEncoderCompression.Lzw: - if (this.Mode == TiffEncodingMode.Rgb || this.Mode == TiffEncodingMode.Gray || this.Mode == TiffEncodingMode.ColorPalette) - { - return (ushort)TiffCompression.Lzw; - } - - break; - - case TiffEncoderCompression.CcittGroup3Fax: - if (this.Mode == TiffEncodingMode.BiColor) - { - return (ushort)TiffCompression.CcittGroup3Fax; - } - - break; - - case TiffEncoderCompression.ModifiedHuffman: - if (this.Mode == TiffEncodingMode.BiColor) - { - return (ushort)TiffCompression.Ccitt1D; - } - - break; - } - - return (ushort)TiffCompression.None; - } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs new file mode 100644 index 0000000000..656ce858b6 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -0,0 +1,328 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff +{ + internal class TiffEncoderEntriesCollector + { + public List Entries { get; } = new List(); + + public void ProcessGeneral(Image image, bool preserveMetadata) + where TPixel : unmanaged, IPixel + => new GeneralProcessor(this).Process(image, preserveMetadata); + + public void ProcessImageFormat(TiffEncoderCore encoder) + => new ImageFormatProcessor(this).Process(encoder); + + public void Add(IExifValue entry) + { + IExifValue exist = this.Entries.Find(t => t.Tag == entry.Tag); + if (exist != null) + { + this.Entries.Remove(exist); + } + + this.Entries.Add(entry); + } + + private void AddInternal(IExifValue entry) => this.Entries.Add(entry); + + private class GeneralProcessor + { + private readonly TiffEncoderEntriesCollector collector; + + public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + + public void Process(Image image, bool preserveMetadata) + where TPixel : unmanaged, IPixel + { + TiffFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + + var width = new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)image.Width + }; + + var height = new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)image.Height + }; + + var software = new ExifString(ExifTagValue.Software) + { + Value = "ImageSharp" + }; + + this.collector.AddInternal(width); + this.collector.AddInternal(height); + this.collector.AddInternal(software); + + this.ProcessResolution(image.Metadata, frameMetadata); + + if (preserveMetadata) + { + this.ProcessMetadata(frameMetadata); + } + } + + private void ProcessResolution(ImageMetadata imageMetadata, TiffFrameMetadata frameMetadata) + { + SynchResolution(imageMetadata, frameMetadata); + + var xResolution = new ExifRational(ExifTagValue.XResolution) + { + Value = frameMetadata.GetSingle(ExifTag.XResolution) + }; + + var yResolution = new ExifRational(ExifTagValue.YResolution) + { + Value = frameMetadata.GetSingle(ExifTag.YResolution) + }; + + var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit) + { + Value = frameMetadata.GetSingle(ExifTag.ResolutionUnit) + }; + + this.collector.AddInternal(xResolution); + this.collector.AddInternal(yResolution); + this.collector.AddInternal(resolutionUnit); + } + + private void ProcessMetadata(TiffFrameMetadata frameMetadata) + { + foreach (IExifValue entry in frameMetadata.FrameTags) + { + // todo: skip subIfd + if (entry.DataType == ExifDataType.Ifd) + { + continue; + } + + switch (ExifTags.GetPart(entry.Tag)) + { + case ExifParts.ExifTags: + case ExifParts.GpsTags: + break; + + case ExifParts.IfdTags: + if (!IsMetadata(entry.Tag)) + { + continue; + } + + break; + } + + if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag)) + { + this.collector.AddInternal(entry.DeepClone()); + } + } + } + + private static void SynchResolution(ImageMetadata imageMetadata, TiffFrameMetadata tiffFrameMetadata) + { + double xres = imageMetadata.HorizontalResolution; + double yres = imageMetadata.VerticalResolution; + + switch (imageMetadata.ResolutionUnits) + { + 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: + { + tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.Centimeter; + xres = UnitConverter.MeterToCm(xres); + yres = UnitConverter.MeterToCm(yres); + } + + break; + default: + tiffFrameMetadata.ResolutionUnit = TiffResolutionUnit.None; + break; + } + + tiffFrameMetadata.HorizontalResolution = xres; + tiffFrameMetadata.VerticalResolution = yres; + } + + private static bool IsMetadata(ExifTag tag) + { + switch ((ExifTagValue)(ushort)tag) + { + 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 class ImageFormatProcessor + { + private readonly TiffEncoderEntriesCollector collector; + + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + + public void Process(TiffEncoderCore encoder) + { + var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) + { + Value = GetSamplesPerPixel(encoder) + }; + + ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder); + var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) + { + Value = bitsPerSampleValue + }; + + ushort compressionType = GetCompressionType(encoder); + var compression = new ExifShort(ExifTagValue.Compression) + { + Value = compressionType + }; + + var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) + { + Value = (ushort)encoder.PhotometricInterpretation + }; + + this.collector.Add(samplesPerPixel); + this.collector.Add(bitPerSample); + this.collector.Add(compression); + this.collector.Add(photometricInterpretation); + + if (encoder.UseHorizontalPredictor) + { + if (encoder.Mode == TiffEncodingMode.Rgb || encoder.Mode == TiffEncodingMode.Gray || encoder.Mode == TiffEncodingMode.ColorPalette) + { + var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; + + this.collector.Add(predictor); + } + } + } + + private static uint GetSamplesPerPixel(TiffEncoderCore encoder) + { + switch (encoder.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.Rgb: + return 3; + case TiffPhotometricInterpretation.PaletteColor: + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + return 1; + default: + return 3; + } + } + + private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) + { + switch (encoder.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.PaletteColor: + return new ushort[] { 8 }; + case TiffPhotometricInterpretation.Rgb: + return new ushort[] { 8, 8, 8 }; + case TiffPhotometricInterpretation.WhiteIsZero: + if (encoder.Mode == TiffEncodingMode.BiColor) + { + return new ushort[] { 1 }; + } + + return new ushort[] { 8 }; + case TiffPhotometricInterpretation.BlackIsZero: + if (encoder.Mode == TiffEncodingMode.BiColor) + { + return new ushort[] { 1 }; + } + + return new ushort[] { 8 }; + default: + return new ushort[] { 8, 8, 8 }; + } + } + + private static ushort GetCompressionType(TiffEncoderCore encoder) + { + switch (encoder.CompressionType) + { + case TiffEncoderCompression.Deflate: + // Deflate is allowed for all modes. + return (ushort)TiffCompression.Deflate; + case TiffEncoderCompression.PackBits: + // PackBits is allowed for all modes. + return (ushort)TiffCompression.PackBits; + case TiffEncoderCompression.Lzw: + if (encoder.Mode == TiffEncodingMode.Rgb || encoder.Mode == TiffEncodingMode.Gray || encoder.Mode == TiffEncodingMode.ColorPalette) + { + return (ushort)TiffCompression.Lzw; + } + + break; + + case TiffEncoderCompression.CcittGroup3Fax: + if (encoder.Mode == TiffEncodingMode.BiColor) + { + return (ushort)TiffCompression.CcittGroup3Fax; + } + + break; + + case TiffEncoderCompression.ModifiedHuffman: + if (encoder.Mode == TiffEncodingMode.BiColor) + { + return (ushort)TiffCompression.Ccitt1D; + } + + break; + } + + return (ushort)TiffCompression.None; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index d4577159f5..36fd3ad200 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets or sets the Tiff directory tags list. /// - public IList Tags { get; set; } + public List FrameTags { get; 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. @@ -70,19 +70,31 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff internal TiffFillOrder FillOrder => this.GetSingleEnum(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst); /// - /// Gets the a string that describes the subject of the image. + /// Gets or sets the a string that describes the subject of the image. /// - public string ImageDescription => this.GetString(ExifTag.ImageDescription); + public string ImageDescription + { + get => this.GetString(ExifTag.ImageDescription); + set => this.SetString(ExifTag.ImageDescription, value); + } /// - /// Gets the scanner manufacturer. + /// Gets or sets the scanner manufacturer. /// - public string Make => this.GetString(ExifTag.Make); + public string Make + { + get => this.GetString(ExifTag.Make); + set => this.SetString(ExifTag.Make, value); + } /// - /// Gets the scanner model name or number. + /// Gets or sets the scanner model name or number. /// - public string Model => this.GetString(ExifTag.Model); + public string Model + { + get => this.GetString(ExifTag.Model); + set => this.SetString(ExifTag.Model, value); + } /// Gets for each strip, the byte offset of that strip.. public uint[] StripOffsets => this.GetArray(ExifTag.StripOffsets); @@ -108,18 +120,26 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { get { - if (this.ResolutionUnit != TiffResolutionUnit.None) + if (this.TryGetSingle(ExifTag.XResolution, out Rational xResolution)) { - double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; - - if (this.TryGetSingle(ExifTag.XResolution, out Rational xResolution)) - { - return xResolution.ToDouble() * resolutionUnitFactor; - } + 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)); + } + } } /// @@ -130,18 +150,26 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { get { - if (this.ResolutionUnit != TiffResolutionUnit.None) + if (this.TryGetSingle(ExifTag.YResolution, out Rational yResolution)) { - double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; - - if (this.TryGetSingle(ExifTag.YResolution, out Rational yResolution)) - { - return yResolution.ToDouble() * resolutionUnitFactor; - } + 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)); + } + } } /// @@ -152,27 +180,47 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets the unit of measurement for XResolution and YResolution. /// - public TiffResolutionUnit ResolutionUnit => this.GetSingleEnum(ExifTag.ResolutionUnit, DefaultResolutionUnit); + public TiffResolutionUnit ResolutionUnit + { + get => this.GetSingleEnum(ExifTag.ResolutionUnit, DefaultResolutionUnit); + internal set => this.SetSingleEnum(ExifTag.ResolutionUnit, value); + } /// - /// Gets the name and version number of the software package(s) used to create the image. + /// Gets or sets the name and version number of the software package(s) used to create the image. /// - public string Software => this.GetString(ExifTag.Software); + public string Software + { + get => this.GetString(ExifTag.Software); + set => this.SetString(ExifTag.Software, value); + } /// - /// Gets the date and time of image creation. + /// Gets or sets the date and time of image creation. /// - public string DateTime => this.GetString(ExifTag.DateTime); + public string DateTime + { + get => this.GetString(ExifTag.DateTime); + set => this.SetString(ExifTag.DateTime, value); + } /// - /// Gets the person who created the image. + /// Gets or sets the person who created the image. /// - public string Artist => this.GetString(ExifTag.Artist); + public string Artist + { + get => this.GetString(ExifTag.Artist); + set => this.SetString(ExifTag.Artist, value); + } /// - /// Gets the computer and/or operating system in use at the time of image creation. + /// Gets or sets the computer and/or operating system in use at the time of image creation. /// - public string HostComputer => this.GetString(ExifTag.HostComputer); + public string HostComputer + { + get => this.GetString(ExifTag.HostComputer); + set => this.SetString(ExifTag.HostComputer, value); + } /// /// Gets a color map for palette color images. @@ -185,9 +233,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public ushort[] ExtraSamples => this.GetArray(ExifTag.ExtraSamples, true); /// - /// Gets the copyright notice. + /// Gets or sets the copyright notice. /// - public string Copyright => this.GetString(ExifTag.Copyright); + public string Copyright + { + get => this.GetString(ExifTag.Copyright); + set => this.SetString(ExifTag.Copyright, value); + } /// /// Gets a mathematical operator that is applied to the image data before an encoding scheme is applied. @@ -200,133 +252,35 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public TiffSampleFormat[] SampleFormat => this.GetEnumArray(ExifTag.SampleFormat, true); - internal T[] GetArray(ExifTag tag, bool optional = false) - where T : struct - { - if (this.TryGetArray(tag, out T[] result)) - { - return result; - } - - if (!optional) - { - TiffThrowHelper.ThrowTagNotFound(nameof(tag)); - } - - return null; - } - - private bool TryGetArray(ExifTag tag, out T[] result) - where T : struct + private double ResolutionFactor { - foreach (IExifValue entry in this.Tags) + get { - if (entry.Tag == tag) + TiffResolutionUnit unit = this.ResolutionUnit; + if (unit == TiffResolutionUnit.Centimeter) { - DebugGuard.IsTrue(entry.IsArray, "Expected array entry"); - - result = (T[])entry.GetValue(); - return true; + return 2.54; } - } - - result = null; - return false; - } - - private TEnum[] GetEnumArray(ExifTag tag, bool optional = false) - where TEnum : struct - where TTagValue : struct - { - if (this.TryGetArray(tag, out TTagValue[] result)) - { - // todo: improve - return result.Select(a => (TEnum)(object)a).ToArray(); - } - - if (!optional) - { - TiffThrowHelper.ThrowTagNotFound(nameof(tag)); - } - - return null; - } - - private string GetString(ExifTag tag) - { - foreach (IExifValue entry in this.Tags) - { - if (entry.Tag == tag) + else if (unit == TiffResolutionUnit.Inch) { - DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry"); - object value = entry.GetValue(); - DebugGuard.IsTrue(value is string, "Expected string entry"); - - return (string)value; + return 1.0; } - } - - return null; - } - private TEnum? GetSingleEnumNullable(ExifTag tag) - where TEnum : struct - where TTagValue : struct - { - if (!this.TryGetSingle(tag, out TTagValue value)) - { - return null; + // DefaultResolutionUnit is Inch + return 1.0; } - - return (TEnum)(object)value; - } - - private TEnum GetSingleEnum(ExifTag tag, TEnum? defaultValue = null) - where TEnum : struct - where TTagValue : struct - => this.GetSingleEnumNullable(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag))); - - private T GetSingle(ExifTag tag) - where T : struct - { - if (this.TryGetSingle(tag, out T result)) - { - return result; - } - - throw TiffThrowHelper.TagNotFound(nameof(tag)); - } - - private bool TryGetSingle(ExifTag tag, out T result) - where T : struct - { - foreach (IExifValue entry in this.Tags) - { - if (entry.Tag == tag) - { - DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry"); - - object value = entry.GetValue(); - - result = (T)value; - return true; - } - } - - result = default; - return false; } /// public IDeepCloneable DeepClone() { var tags = new List(); - foreach (IExifValue entry in this.Tags) + foreach (IExifValue entry in this.FrameTags) { tags.Add(entry.DeepClone()); } - return new TiffFrameMetadata() { Tags = tags }; + return new TiffFrameMetadata() { FrameTags = tags }; } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs new file mode 100644 index 0000000000..263b8cd553 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs @@ -0,0 +1,201 @@ +// 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 +{ + /// + /// The tiff metadata extensions + /// + internal static class TiffFrameMetadataExtensions + { + public static T[] GetArray(this TiffFrameMetadata meta, ExifTag tag, bool optional = false) + where T : struct + { + if (meta.TryGetArray(tag, out T[] result)) + { + return result; + } + + if (!optional) + { + TiffThrowHelper.ThrowTagNotFound(nameof(tag)); + } + + return null; + } + + public static bool TryGetArray(this TiffFrameMetadata meta, ExifTag tag, out T[] result) + where T : struct + { + foreach (IExifValue entry in meta.FrameTags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(entry.IsArray, "Expected array entry"); + + result = (T[])entry.GetValue(); + return true; + } + } + + result = null; + return false; + } + + public static TEnum[] GetEnumArray(this TiffFrameMetadata meta, ExifTag tag, bool optional = false) + where TEnum : struct + where TTagValue : struct + { + if (meta.TryGetArray(tag, out TTagValue[] result)) + { + // todo: improve + return result.Select(a => (TEnum)(object)a).ToArray(); + } + + if (!optional) + { + TiffThrowHelper.ThrowTagNotFound(nameof(tag)); + } + + return null; + } + + public static string GetString(this TiffFrameMetadata meta, ExifTag tag) + { + foreach (IExifValue entry in meta.FrameTags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry"); + object value = entry.GetValue(); + DebugGuard.IsTrue(value is string, "Expected string entry"); + + return (string)value; + } + } + + return null; + } + + public static bool SetString(this TiffFrameMetadata meta, ExifTag tag, string value) + { + IExifValue obj = FindOrCreate(meta, tag); + DebugGuard.IsTrue(obj.DataType == ExifDataType.Ascii, "Expected string entry"); + + return obj.TrySetValue(value); + } + + public static TEnum? GetSingleEnumNullable(this TiffFrameMetadata meta, ExifTag tag) + where TEnum : struct + where TTagValue : struct + { + if (!meta.TryGetSingle(tag, out TTagValue value)) + { + return null; + } + + return (TEnum)(object)value; + } + + public static TEnum GetSingleEnum(this TiffFrameMetadata meta, ExifTag tag, TEnum? defaultValue = null) + where TEnum : struct + where TTagValue : struct + => meta.GetSingleEnumNullable(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag))); + + public static bool SetSingleEnum(this TiffFrameMetadata meta, ExifTag tag, TEnum value) + where TEnum : struct + where TTagValue : struct + { + IExifValue obj = FindOrCreate(meta, tag); + + object val = (TTagValue)(object)value; + return obj.TrySetValue(val); + } + + public static T GetSingle(this TiffFrameMetadata meta, ExifTag tag) + where T : struct + { + if (meta.TryGetSingle(tag, out T result)) + { + return result; + } + + throw TiffThrowHelper.TagNotFound(nameof(tag)); + } + + public static bool TryGetSingle(this TiffFrameMetadata meta, ExifTag tag, out T result) + where T : struct + { + foreach (IExifValue entry in meta.FrameTags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry"); + + object value = entry.GetValue(); + + result = (T)value; + return true; + } + } + + result = default; + return false; + } + + public static bool SetSingle(this TiffFrameMetadata meta, ExifTag tag, T value) + where T : struct + { + IExifValue obj = FindOrCreate(meta, tag); + DebugGuard.IsTrue(!obj.IsArray, "Expected non array entry"); + + object val = (T)(object)value; + return obj.TrySetValue(val); + } + + public static bool Remove(this TiffFrameMetadata meta, ExifTag tag) + { + IExifValue obj = null; + foreach (IExifValue entry in meta.FrameTags) + { + if (entry.Tag == tag) + { + obj = entry; + break; + } + } + + if (obj != null) + { + return meta.FrameTags.Remove(obj); + } + + return false; + } + + private static IExifValue FindOrCreate(TiffFrameMetadata meta, ExifTag tag) + { + IExifValue obj = null; + foreach (IExifValue entry in meta.FrameTags) + { + if (entry.Tag == tag) + { + obj = entry; + break; + } + } + + if (obj == null) + { + obj = ExifValues.Create(tag); + meta.FrameTags.Add(obj); + } + + return obj; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index 276e8ad809..6f31fd5495 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -45,8 +45,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets or sets the XMP profile. + /// For internal use only. ImageSharp not support XMP profile. /// - public byte[] XmpProfile { get; set; } + internal byte[] XmpProfile { get; set; } /// public IDeepCloneable DeepClone() => new TiffMetadata(this); diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 3605869256..f8f5f8ab8a 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -285,9 +285,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The compression to use. /// The compression level for deflate compression. /// Indicates if horizontal prediction should be used. Should only be used in combination with deflate or LZW compression. - /// The color map. + /// The entries collector. /// The number of bytes written. - public int WritePalettedRgb(Image image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, out IExifValue colorMap) + public int WritePalettedRgb(Image image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, TiffEncoderEntriesCollector entriesCollector) where TPixel : unmanaged, IPixel { int colorsPerChannel = 256; @@ -332,11 +332,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils palette[paletteIdx++] = quantizedColorRgb48[i].B; } - colorMap = new ExifShortArray(ExifTagValue.ColorMap) + var colorMap = new ExifShortArray(ExifTagValue.ColorMap) { Value = palette }; + entriesCollector.Add(colorMap); + if (compression == TiffEncoderCompression.Deflate) { return this.WriteDeflateCompressedPalettedRgb(image, quantized, compressionLevel, useHorizontalPredictor); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 4afbb5469d..ce20fb77d9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; + using SixLabors.ImageSharp.Formats.Experimental.Tiff; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; using SixLabors.ImageSharp.Metadata; @@ -126,7 +127,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff } [Theory] - [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] public void BaselineTags(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -175,7 +176,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff } [Theory] - [WithFile(TestImages.Tiff.MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] public void SubfileType(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -199,5 +200,119 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(255u, frame1.Height); } } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32, true)] + [WithFile(SampleMetadata, PixelTypes.Rgba32, false)] + public void Tiff_PreserveMetadata(TestImageProvider provider, bool preserveMetadata) + where TPixel : unmanaged, IPixel + { + 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(); + + // act + image.Save(ms, tiffEncoder); + + // assert + ms.Position = 0; + using var output = Image.Load(ms); + + ImageMetadata coreMetaOut = output.Metadata; + 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(frameMeta.Width, frameMetaOut.Width); + Assert.Equal(frameMeta.Height, frameMetaOut.Height); + Assert.Equal(frameMeta.ResolutionUnit, frameMetaOut.ResolutionUnit); + 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); + + if (preserveMetadata) + { + Assert.Equal("ImageSharp", frameMetaOut.Software); + + Assert.Equal(frameMeta.ImageDescription, frameMetaOut.ImageDescription); + Assert.Equal(frameMeta.Make, frameMetaOut.Make); + Assert.Equal(frameMeta.Copyright, frameMetaOut.Copyright); + } + else + { + Assert.Equal("ImageSharp", frameMetaOut.Software); + + Assert.Null(frameMetaOut.ImageDescription); + Assert.Null(frameMetaOut.Make); + Assert.Null(frameMetaOut.Copyright); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Tiff_CreateMetadata(bool preserveMetadata) + { + var tiffEncoder = new TiffEncoder { Mode = TiffEncodingMode.Default, Compression = TiffEncoderCompression.Deflate, PreserveMetadata = preserveMetadata }; + + 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(); + + coreMeta.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; + coreMeta.HorizontalResolution = 45; + coreMeta.VerticalResolution = 54; + + frameMeta.ImageDescription = "test ImageDescription"; + frameMeta.DateTime = System.DateTime.Now.ToString(); + + using var ms = new MemoryStream(); + input.Save(ms, tiffEncoder); + + // assert + ms.Position = 0; + using var output = Image.Load(ms); + TiffMetadata meta = output.Metadata.GetTiffMetadata(); + + ImageMetadata coreMetaOut = output.Metadata; + 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((uint)w, frameMetaOut.Width); + Assert.Equal((uint)h, frameMetaOut.Height); + Assert.Equal(frameMeta.ResolutionUnit, frameMetaOut.ResolutionUnit); + Assert.Equal(frameMeta.HorizontalResolution, frameMetaOut.HorizontalResolution); + Assert.Equal(frameMeta.VerticalResolution, frameMetaOut.VerticalResolution); + + if (preserveMetadata) + { + Assert.Equal(frameMeta.ImageDescription, frameMetaOut.ImageDescription); + Assert.Equal(frameMeta.DateTime, frameMetaOut.DateTime); + } + else + { + Assert.Null(frameMetaOut.ImageDescription); + Assert.Null(frameMetaOut.DateTime); + } + } } }