From 688d3dde9d39f12f5c9ba894daabbabd0a5d3039 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Thu, 10 Dec 2020 23:08:02 +0300 Subject: [PATCH 1/6] 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); + } + } } } From 9d39c3810d866eeab1cd6c1a830dff43b178de9d Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 12 Dec 2020 20:33:36 +0300 Subject: [PATCH 2/6] 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 108b6ae6ed..10ac39747d 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 a614235be8..f7b6200fa4 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 0fab224dec..0000000000 --- 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 37492ea31c..4814631364 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 b8532e0c59..da2f96211e 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 0e314e6ee0..ea53fa0617 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 b4350c287f..c448540810 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 0eddca4bb7..99337a8b2a 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 7fd76d04d4..e81e519460 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 1ed416878d..cddc962fcb 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 44a3140deb..c7301049de 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 656ce858b6..41d833299a 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 36fd3ad200..0e698c5685 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 263b8cd553..39d0f34dbd 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 6f31fd5495..f8df21c1ea 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 ce20fb77d9..be14f7019b 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(); + } } } From a16fa9ece2b111b5ab5cb96a29f910e8553189e0 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 12 Dec 2020 21:12:25 +0300 Subject: [PATCH 3/6] Update README.md --- src/ImageSharp/Formats/Tiff/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index bdf5cf8a63..7e88310484 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -50,7 +50,7 @@ Decoder: |PackBits | Y | Y | | |CcittGroup3Fax | Y | Y | | |CcittGroup4Fax | | | | -|Lzw | | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | +|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | |Old Jpeg | | | We should not even try to support this | |Jpeg (Technote 2) | | | | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | @@ -103,14 +103,14 @@ Decoder: |FreeByteCounts | | | | |GrayResponseUnit | | | | |GrayResponseCurve | | | | -|ResolutionUnit | | Y | | +|ResolutionUnit | Y | Y | | |Software | Y | Y | | -|DateTime | | Y | | -|Artist | | Y | | -|HostComputer | | Y | | -|ColorMap | | Y | | +|DateTime | Y | Y | | +|Artist | Y | Y | | +|HostComputer | Y | Y | | +|ColorMap | Y | Y | | |ExtraSamples | | - | | -|Copyright | | Y | | +|Copyright | Y | Y | | ### Extension TIFF Tags @@ -125,7 +125,7 @@ Decoder: |T6Options | | | | |PageNumber | | | | |TransferFunction | | | | -|Predictor | | - | priority | +|Predictor | Y | Y | only Horizontal | |WhitePoint | | | | |PrimaryChromaticities | | | | |HalftoneHints | | | | @@ -174,7 +174,7 @@ Decoder: |YCbCrPositioning | | | | |ReferenceBlackWhite | | | | |StripRowCounts | | - | | -|XMP | | Y | | +|XMP | Y | Y | | |ImageID | | | | |ImageLayer | | | | @@ -192,7 +192,7 @@ Decoder: |MD PrepTime | | | | |MD FileUnits | | | | |ModelPixelScaleTag | | | | -|IPTC | | Y | | +|IPTC | Y | Y | | |INGR Packet Data Tag | | | | |INGR Flag Registers | | | | |IrasB Transformation Matrix| | | | @@ -200,7 +200,7 @@ Decoder: |ModelTransformationTag | | | | |Photoshop | | | | |Exif IFD | | - | 0x8769 SubExif | -|ICC Profile | | Y | | +|ICC Profile | Y | Y | | |GeoKeyDirectoryTag | | | | |GeoDoubleParamsTag | | | | |GeoAsciiParamsTag | | | | From c14b7eb31f912f97bbeadd417239063194c46aaf Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 12 Dec 2020 21:25:20 +0300 Subject: [PATCH 4/6] Move resolution related methods to the extensions --- .../Formats/Tiff/TiffFrameMetadata.cs | 96 +---------------- .../TiffFrameMetadataResolutionExtensions.cs | 101 ++++++++++++++++++ 2 files changed, 103 insertions(+), 94 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index 0e698c5685..c9bab385a8 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -3,7 +3,6 @@ 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; @@ -16,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public class TiffFrameMetadata : IDeepCloneable { // 2 (Inch) - private const ushort DefaultResolutionUnit = 2; + internal const ushort DefaultResolutionUnit = 2; private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; @@ -168,18 +167,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets the unit of measurement for XResolution and YResolution. /// - public PixelResolutionUnit ResolutionUnit - { - get - { - if (!this.TryGetSingle(ExifTag.ResolutionUnit, out ushort res)) - { - res = DefaultResolutionUnit; - } - - return (PixelResolutionUnit)(res - 1); - } - } + public PixelResolutionUnit ResolutionUnit => this.GetResolutionUnit(); /// /// Gets or sets the name and version number of the software package(s) used to create the image. @@ -247,7 +235,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public TiffSampleFormat[] SampleFormat => this.GetEnumArray(ExifTag.SampleFormat, true); - /// /// Clears the metadata. /// @@ -288,84 +275,5 @@ 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/TiffFrameMetadataResolutionExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs new file mode 100644 index 0000000000..f17e9dddc2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff +{ + internal static class TiffFrameMetadataResolutionExtensions + { + public static void SetResolutions(this TiffFrameMetadata meta, 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; + } + + meta.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1); + meta.SetResolution(ExifTag.XResolution, horizontal); + meta.SetResolution(ExifTag.YResolution, vertical); + } + + public static PixelResolutionUnit GetResolutionUnit(this TiffFrameMetadata meta) + { + if (!meta.TryGetSingle(ExifTag.ResolutionUnit, out ushort res)) + { + res = TiffFrameMetadata.DefaultResolutionUnit; + } + + return (PixelResolutionUnit)(res - 1); + } + + public static double? GetResolution(this TiffFrameMetadata meta, ExifTag tag) + { + if (!meta.TryGetSingle(tag, out Rational resolution)) + { + return null; + } + + double res = resolution.ToDouble(); + switch (meta.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 static void SetResolution(this TiffFrameMetadata meta, ExifTag tag, double? value) + { + if (value == null) + { + meta.Remove(tag); + return; + } + else + { + double res = value.Value; + switch (meta.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; + } + + meta.SetSingle(tag, new Rational(res)); + } + } + } +} From 3e73a38a19dce0a4b98f4f1585213865c7ae4706 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Mon, 14 Dec 2020 12:58:31 +0300 Subject: [PATCH 5/6] netstandard 1.3 compatibility fix --- src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs index 39d0f34dbd..78e2174845 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs @@ -1,6 +1,8 @@ // 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 @@ -50,7 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { if (meta.TryGetArray(tag, out TTagValue[] result)) { - return System.Array.ConvertAll(result, a => (TEnum)(object)a); + return result.Select(a => (TEnum)(object)a).ToArray(); } if (!optional) From bc07090d28d962c8a066384731b08e991415f544 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Mon, 14 Dec 2020 19:59:39 +0300 Subject: [PATCH 6/6] Renaming --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 8 ++++---- src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index c448540810..db8b59858b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public ushort[] BitsPerSample { get; set; } - public int ChunkyBitsPerPixel { get; set; } + public int BitsPerPixel { get; set; } /// /// Gets or sets the lookup table for RGB palette colored images. @@ -243,7 +243,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { DebugGuard.IsTrue(plane == -1, "Excepted Chunky planar."); - bitsPerPixel = this.ChunkyBitsPerPixel; + bitsPerPixel = this.BitsPerPixel; } else { @@ -270,7 +270,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { int stripsPerPixel = this.BitsPerSample.Length; int stripsPerPlane = stripOffsets.Length / stripsPerPixel; - int bitsPerPixel = this.ChunkyBitsPerPixel; // todo? + int bitsPerPixel = this.BitsPerPixel; Buffer2D pixels = frame.PixelBuffer; @@ -315,7 +315,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff where TPixel : unmanaged, IPixel { int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); - int bitsPerPixel = this.ChunkyBitsPerPixel; + int bitsPerPixel = this.BitsPerPixel; using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index e81e519460..713c85c06d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -52,11 +52,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff options.PlanarConfiguration = entries.PlanarConfiguration; options.Predictor = entries.Predictor; - - // todo: There is no default for PhotometricInterpretation, and it is required. options.PhotometricInterpretation = entries.PhotometricInterpretation; options.BitsPerSample = entries.BitsPerSample; - options.ChunkyBitsPerPixel = entries.BitsPerPixel; + options.BitsPerPixel = entries.BitsPerPixel; ParseColorType(options, entries); ParseCompression(options, entries.Compression);