Browse Source

Merge pull request #1471 from IldarKhayrutdinov/tiff_meta

Ability to save Tiff metadata
pull/1570/head
Brian Popow 5 years ago
committed by GitHub
parent
commit
7a0127ffd2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
  2. 12
      src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
  3. 26
      src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs
  4. 5
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  5. 9
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  6. 22
      src/ImageSharp/Formats/Tiff/README.md
  7. 5
      src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
  8. 113
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  9. 92
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  10. 112
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  11. 3
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  12. 208
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  13. 363
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  14. 281
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  15. 200
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs
  16. 101
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs
  17. 16
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  18. 8
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  19. 202
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

2
src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// <summary>
/// Methods for undoing the horizontal prediction used in combination with deflate and LZW compressed TIFF images.
/// </summary>
public static class HorizontalPredictor
internal static class HorizontalPredictor
{
/// <summary>
/// Inverts the horizontal prediction.

12
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);
}
}

26
src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs

@ -1,26 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants
{
/// <summary>
/// Enumeration representing the resolution units defined by the Tiff file-format.
/// </summary>
public enum TiffResolutionUnit : ushort
{
/// <summary>
/// No absolute unit of measurement.
/// </summary>
None = 1,
/// <summary>
/// Inch.
/// </summary>
Inch = 2,
/// <summary>
/// Centimeter.
/// </summary>
Centimeter = 3
}
}

5
src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs

@ -11,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
internal interface ITiffEncoderOptions
{
/// <summary>
/// Gets the byte order to use.
/// </summary>
ByteOrder ByteOrder { get; }
/// <summary>
/// Gets the compression type to use.
/// </summary>

9
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();

22
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 | | | |

5
src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs

@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
Pixel1 = 1,
/// <summary>
/// 4 bits per pixel, grayscale or indexed image. Each pixel consists of 4 bit.
/// </summary>
Pixel4 = 4,
/// <summary>
/// 8 bits per pixel, grayscale or indexed image. Each pixel consists of 1 byte.
/// </summary>

113
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
@ -37,16 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
private readonly bool ignoreMetadata;
/// <summary>
/// The image metadata.
/// </summary>
private ImageMetadata metadata;
/// <summary>
/// The tiff specific metadata.
/// </summary>
private TiffMetadata tiffMetaData;
/// <summary>
/// The stream to decode from.
/// </summary>
@ -71,6 +60,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public ushort[] BitsPerSample { get; set; }
public int BitsPerPixel { get; set; }
/// <summary>
/// Gets or sets the lookup table for RGB palette colored images.
/// </summary>
@ -96,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
/// <summary>
/// Gets or sets the predictor.
/// </summary>
public TiffPredictor Predictor { get; set; }
/// <inheritdoc/>
public Configuration Configuration => this.configuration;
@ -121,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<TPixel> root = frames.First();
ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames)
{
@ -137,7 +132,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
var image = new Image<TPixel>(this.configuration, this.metadata, frames);
var image = new Image<TPixel>(this.configuration, metadata, frames);
return image;
}
@ -154,51 +149,15 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
var framesMetadata = new List<TiffFrameMetadata>();
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);
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);
}
private void SetTiffFormatMetaData(List<TiffFrameMetadata> framesMetadata, ByteOrder byteOrder)
{
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;
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder);
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;
}
TiffFrameMetadata root = framesMetadata[0];
return new ImageInfo(new PixelTypeInfo(root.BitsPerPixel), (int)root.Width, (int)root.Height, metadata);
}
private static TiffStream CreateStream(Stream stream)
@ -246,11 +205,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
var coreMetadata = new ImageFrameMetadata();
frameMetaData = coreMetadata.GetTiffMetadata();
frameMetaData.Tags = tags;
TiffFrameMetadata tiffFormatMetaData = coreMetadata.GetFormatMetadata(TiffFormat.Instance);
TiffPredictor predictor = tiffFormatMetaData.Predictor;
frameMetaData.FrameTags.AddRange(tags);
this.VerifyAndParseOptions(frameMetaData);
this.VerifyAndParse(frameMetaData);
int width = (int)frameMetaData.Width;
int height = (int)frameMetaData.Height;
@ -262,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;
@ -281,22 +238,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <returns>The size (in bytes) of the required pixel buffer.</returns>
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.BitsPerPixel;
}
else
{
bitsPerPixel = this.BitsPerSample[plane];
}
int bytesPerRow = ((width * (int)bitsPerPixel) + 7) / 8;
int bytesPerRow = ((width * bitsPerPixel) + 7) / 8;
int stripBytes = bytesPerRow * height;
return stripBytes;
@ -311,17 +265,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="width">The image width.</param>
/// <param name="predictor">The tiff predictor used.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width, TiffPredictor predictor)
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width)
where TPixel : unmanaged, IPixel<TPixel>
{
int stripsPerPixel = this.BitsPerSample.Length;
int stripsPerPlane = stripOffsets.Length / stripsPerPixel;
int bitsPerPixel = 0;
foreach (var bits in this.BitsPerSample)
{
bitsPerPixel += bits;
}
int bitsPerPixel = this.BitsPerPixel;
Buffer2D<TPixel> pixels = frame.PixelBuffer;
@ -335,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<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap);
@ -362,21 +311,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width, TiffPredictor predictor)
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width)
where TPixel : unmanaged, IPixel<TPixel>
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
int bitsPerPixel = 0;
foreach (var bits in this.BitsPerSample)
{
bitsPerPixel += bits;
}
int bitsPerPixel = this.BitsPerPixel;
using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean);
Buffer2D<TPixel> 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<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample, this.ColorMap);

92
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -0,0 +1,92 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
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
{
/// <summary>
/// The decoder metadata creator.
/// </summary>
internal static class TiffDecoderMetadataCreator
{
public static ImageMetadata Create(List<TiffFrameMetadata> frames, bool ignoreMetadata, ByteOrder byteOrder)
{
if (frames.Count < 1)
{
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;
}
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<byte>(ExifTag.XMP, true);
if (buf != null)
{
tiffMetadata.XmpProfile = buf;
}
}
if (coreMetadata.ExifProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.SubIFDOffset, true);
if (buf != null)
{
coreMetadata.ExifProfile = new ExifProfile(buf);
}
}
if (coreMetadata.IptcProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IPTC, true);
if (buf != null)
{
coreMetadata.IptcProfile = new IptcProfile(buf);
}
}
if (coreMetadata.IccProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IccProfile, true);
if (buf != null)
{
coreMetadata.IccProfile = new IccProfile(buf);
}
}
}
}
return coreMetadata;
}
private static TiffBitsPerPixel GetBitsPerPixel(TiffFrameMetadata firstFrameMetaData)
=> (TiffBitsPerPixel)firstFrameMetaData.BitsPerPixel;
}
}

112
src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs → src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -1,95 +1,23 @@
// 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
{
/// <summary>
/// The decoder helper methods.
/// The decoder options parser.
/// </summary>
internal static class TiffDecoderHelpers
internal static class TiffDecoderOptionsParser
{
public static ImageMetadata CreateMetadata(this IList<TiffFrameMetadata> 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<byte>(ExifTag.XMP, true);
if (buf != null)
{
tiffMetadata.XmpProfile = buf;
}
}
if (coreMetadata.IptcProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IPTC, true);
if (buf != null)
{
coreMetadata.IptcProfile = new IptcProfile(buf);
}
}
if (coreMetadata.IccProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IccProfile, true);
if (buf != null)
{
coreMetadata.IccProfile = new IccProfile(buf);
}
}
}
}
return coreMetadata;
}
/// <summary>
/// Determines the TIFF compression and color types, and reads any associated parameters.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="entries">The IFD entries container to read the image format information for.</param>
public static void VerifyAndParseOptions(this TiffDecoderCore options, TiffFrameMetadata entries)
public static void VerifyAndParse(this TiffDecoderCore options, TiffFrameMetadata entries)
{
if (entries.ExtraSamples != null)
{
@ -122,15 +50,14 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
ParseCompression(options, entries.Compression);
options.PlanarConfiguration = entries.PlanarConfiguration;
ParsePhotometric(options, entries);
ParseBitsPerSample(options, entries);
options.Predictor = entries.Predictor;
options.PhotometricInterpretation = entries.PhotometricInterpretation;
options.BitsPerSample = entries.BitsPerSample;
options.BitsPerPixel = entries.BitsPerPixel;
ParseColorType(options, entries);
ParseCompression(options, entries.Compression);
}
private static void ParseColorType(this TiffDecoderCore options, TiffFrameMetadata entries)
@ -281,29 +208,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)

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

@ -17,6 +17,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public class TiffEncoder : IImageEncoder, ITiffEncoderOptions
{
/// <inheritdoc/>
public ByteOrder ByteOrder { get; } = TiffEncoderCore.ByteOrder;
/// <inheritdoc/>
public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None;

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

@ -24,6 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
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;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
@ -70,19 +76,21 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
/// <summary>
/// Gets or sets the photometric interpretation implementation to use when encoding the image.
/// Gets the photometric interpretation implementation to use when encoding the image.
/// </summary>
private TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
internal TiffPhotometricInterpretation PhotometricInterpretation { get; private set; }
/// <summary>
/// Gets the compression implementation to use when encoding the image.
/// </summary>
private TiffEncoderCompression CompressionType { get; }
internal TiffEncoderCompression CompressionType { get; }
/// <summary>
/// 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.
/// </summary>
private TiffEncodingMode Mode { get; set; }
internal TiffEncodingMode Mode { get; private set; }
internal bool UseHorizontalPredictor => this.useHorizontalPredictor;
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
@ -132,12 +140,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <returns>The marker to write the first IFD offset.</returns>
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;
@ -154,8 +158,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public long WriteImage<TPixel>(TiffWriter writer, Image<TPixel> image, long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
{
IExifValue colorMap = null;
var ifdEntries = new List<IExifValue>();
var entriesCollector = new TiffEncoderEntriesCollector();
// Write the image bytes to the steam.
var imageDataStart = (uint)writer.Position;
@ -163,7 +166,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 +179,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);
writer.WriteMarker(ifdOffset, (uint)writer.Position);
long nextIfdMarker = this.WriteIfd(writer, ifdEntries);
long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries);
return nextIfdMarker + imageDataBytes;
}
@ -250,51 +250,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// Adds image format information to the specified IFD.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="ifdEntries">The image format entries to add to the IFD.</param>
/// <param name="image">The <see cref="Image{TPixel}" /> to encode from.</param>
/// <param name="entriesCollector">The entries collector.</param>
/// <param name="imageDataStartOffset">The start of the image data in the stream.</param>
/// <param name="imageDataBytes">The image data in bytes to write.</param>
public void AddImageFormat<TPixel>(Image<TPixel> image, List<IExifValue> ifdEntries, uint imageDataStartOffset, int imageDataBytes)
public void AddStripTags<TPixel>(Image<TPixel> image, TiffEncoderEntriesCollector entriesCollector, uint imageDataStartOffset, int imageDataBytes)
where TPixel : unmanaged, IPixel<TPixel>
{
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 +274,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 +307,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;
}
}
}

363
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -0,0 +1,363 @@
// 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<IExifValue> Entries { get; } = new List<IExifValue>();
public void ProcessGeneral<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
=> new GeneralProcessor(this).Process(image);
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<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
this.ProcessProfiles(image.Metadata, frameMetadata);
this.ProcessMetadata(frameMetadata);
}
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 void ProcessResolution(ImageMetadata imageMetadata, TiffFrameMetadata frameMetadata)
{
frameMetadata.SetResolutions(
imageMetadata.ResolutionUnits,
imageMetadata.HorizontalResolution,
imageMetadata.VerticalResolution);
var xResolution = new ExifRational(ExifTagValue.XResolution)
{
Value = frameMetadata.GetSingle<Rational>(ExifTag.XResolution)
};
var yResolution = new ExifRational(ExifTagValue.YResolution)
{
Value = frameMetadata.GetSingle<Rational>(ExifTag.YResolution)
};
var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit)
{
Value = frameMetadata.GetSingle<ushort>(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 ((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:
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 void ProcessProfiles(ImageMetadata imageMetadata, TiffFrameMetadata tiffFrameMetadata)
{
if (imageMetadata.ExifProfile != null)
{
// todo: implement processing exif profile
}
else
{
tiffFrameMetadata.Remove(ExifTag.SubIFDOffset);
}
if (imageMetadata.IptcProfile != null)
{
imageMetadata.IptcProfile.UpdateData();
var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte)
{
Value = imageMetadata.IptcProfile.Data
};
this.collector.AddInternal(iptc);
}
else
{
tiffFrameMetadata.Remove(ExifTag.IPTC);
}
if (imageMetadata.IccProfile != null)
{
var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined)
{
Value = imageMetadata.IccProfile.ToByteArray()
};
this.collector.AddInternal(icc);
}
else
{
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);
}
}
}
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;
}
}
}
}

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

@ -2,9 +2,9 @@
// 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;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
@ -14,7 +14,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public class TiffFrameMetadata : IDeepCloneable
{
private const TiffResolutionUnit DefaultResolutionUnit = TiffResolutionUnit.Inch;
// 2 (Inch)
internal const ushort DefaultResolutionUnit = 2;
private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky;
@ -28,9 +29,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
/// <summary>
/// Gets or sets the Tiff directory tags list.
/// Gets the Tiff directory tags list.
/// </summary>
public IList<IExifValue> Tags { get; set; }
public List<IExifValue> FrameTags { get; internal set; } = new List<IExifValue>();
/// <summary>Gets a general indication of the kind of data contained in this subfile.</summary>
/// <value>A general indication of the kind of data contained in this subfile.</value>
@ -53,7 +54,41 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>
/// Gets the number of bits per component.
/// </summary>
public ushort[] BitsPerSample => this.GetArray<ushort>(ExifTag.BitsPerSample, true);
public ushort[] BitsPerSample
{
get
{
var bits = this.GetArray<ushort>(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;
}
}
/// <summary>Gets the compression scheme used on the image data.</summary>
/// <value>The compression scheme used on the image data.</value>
@ -70,19 +105,31 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
internal TiffFillOrder FillOrder => this.GetSingleEnum<TiffFillOrder, ushort>(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst);
/// <summary>
/// Gets the a string that describes the subject of the image.
/// Gets or sets the a string that describes the subject of the image.
/// </summary>
public string ImageDescription => this.GetString(ExifTag.ImageDescription);
public string ImageDescription
{
get => this.GetString(ExifTag.ImageDescription);
set => this.SetString(ExifTag.ImageDescription, value);
}
/// <summary>
/// Gets the scanner manufacturer.
/// Gets or sets the scanner manufacturer.
/// </summary>
public string Make => this.GetString(ExifTag.Make);
public string Make
{
get => this.GetString(ExifTag.Make);
set => this.SetString(ExifTag.Make, value);
}
/// <summary>
/// Gets the scanner model name or number.
/// Gets or sets the scanner model name or number.
/// </summary>
public string Model => this.GetString(ExifTag.Model);
public string Model
{
get => this.GetString(ExifTag.Model);
set => this.SetString(ExifTag.Model, value);
}
/// <summary>Gets for each strip, the byte offset of that strip..</summary>
public uint[] StripOffsets => this.GetArray<uint>(ExifTag.StripOffsets);
@ -104,45 +151,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>Gets the resolution of the image in x- direction.</summary>
/// <value>The density of the image in x- direction.</value>
public double? HorizontalResolution
{
get
{
if (this.ResolutionUnit != TiffResolutionUnit.None)
{
double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0;
if (this.TryGetSingle(ExifTag.XResolution, out Rational xResolution))
{
return xResolution.ToDouble() * resolutionUnitFactor;
}
}
return null;
}
}
public double? HorizontalResolution => this.GetResolution(ExifTag.XResolution);
/// <summary>
/// Gets the resolution of the image in y- direction.
/// </summary>
/// <value>The density of the image in y- direction.</value>
public double? VerticalResolution
{
get
{
if (this.ResolutionUnit != TiffResolutionUnit.None)
{
double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0;
if (this.TryGetSingle(ExifTag.YResolution, out Rational yResolution))
{
return yResolution.ToDouble() * resolutionUnitFactor;
}
}
return null;
}
}
public double? VerticalResolution => this.GetResolution(ExifTag.YResolution);
/// <summary>
/// Gets how the components of each pixel are stored.
@ -152,27 +167,43 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>
/// Gets the unit of measurement for XResolution and YResolution.
/// </summary>
public TiffResolutionUnit ResolutionUnit => this.GetSingleEnum<TiffResolutionUnit, ushort>(ExifTag.ResolutionUnit, DefaultResolutionUnit);
public PixelResolutionUnit ResolutionUnit => this.GetResolutionUnit();
/// <summary>
/// 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.
/// </summary>
public string Software => this.GetString(ExifTag.Software);
public string Software
{
get => this.GetString(ExifTag.Software);
set => this.SetString(ExifTag.Software, value);
}
/// <summary>
/// Gets the date and time of image creation.
/// Gets or sets the date and time of image creation.
/// </summary>
public string DateTime => this.GetString(ExifTag.DateTime);
public string DateTime
{
get => this.GetString(ExifTag.DateTime);
set => this.SetString(ExifTag.DateTime, value);
}
/// <summary>
/// Gets the person who created the image.
/// Gets or sets the person who created the image.
/// </summary>
public string Artist => this.GetString(ExifTag.Artist);
public string Artist
{
get => this.GetString(ExifTag.Artist);
set => this.SetString(ExifTag.Artist, value);
}
/// <summary>
/// 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.
/// </summary>
public string HostComputer => this.GetString(ExifTag.HostComputer);
public string HostComputer
{
get => this.GetString(ExifTag.HostComputer);
set => this.SetString(ExifTag.HostComputer, value);
}
/// <summary>
/// Gets a color map for palette color images.
@ -185,9 +216,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public ushort[] ExtraSamples => this.GetArray<ushort>(ExifTag.ExtraSamples, true);
/// <summary>
/// Gets the copyright notice.
/// Gets or sets the copyright notice.
/// </summary>
public string Copyright => this.GetString(ExifTag.Copyright);
public string Copyright
{
get => this.GetString(ExifTag.Copyright);
set => this.SetString(ExifTag.Copyright, value);
}
/// <summary>
/// Gets a mathematical operator that is applied to the image data before an encoding scheme is applied.
@ -200,133 +235,45 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public TiffSampleFormat[] SampleFormat => this.GetEnumArray<TiffSampleFormat, ushort>(ExifTag.SampleFormat, true);
internal T[] GetArray<T>(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<T>(ExifTag tag, out T[] result)
where T : struct
{
foreach (IExifValue entry in this.Tags)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(entry.IsArray, "Expected array entry");
result = (T[])entry.GetValue();
return true;
}
}
result = null;
return false;
}
private TEnum[] GetEnumArray<TEnum, TTagValue>(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)
{
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;
}
private TEnum? GetSingleEnumNullable<TEnum, TTagValue>(ExifTag tag)
where TEnum : struct
where TTagValue : struct
{
if (!this.TryGetSingle(tag, out TTagValue value))
{
return null;
}
return (TEnum)(object)value;
}
private TEnum GetSingleEnum<TEnum, TTagValue>(ExifTag tag, TEnum? defaultValue = null)
where TEnum : struct
where TTagValue : struct
=> this.GetSingleEnumNullable<TEnum, TTagValue>(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag)));
private T GetSingle<T>(ExifTag tag)
where T : struct
{
if (this.TryGetSingle(tag, out T result))
{
return result;
}
throw TiffThrowHelper.TagNotFound(nameof(tag));
}
private bool TryGetSingle<T>(ExifTag tag, out T result)
where T : struct
/// <summary>
/// Clears the metadata.
/// </summary>
public void ClearMetadata()
{
foreach (IExifValue entry in this.Tags)
var tags = new List<IExifValue>();
foreach (IExifValue entry in this.FrameTags)
{
if (entry.Tag == tag)
switch ((ExifTagValue)(ushort)entry.Tag)
{
DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry");
object value = entry.GetValue();
result = (T)value;
return true;
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;
}
}
result = default;
return false;
this.FrameTags = tags;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone()
{
var tags = new List<IExifValue>();
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 };
}
}
}

200
src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs

@ -0,0 +1,200 @@
// 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
{
/// <summary>
/// The tiff metadata extensions
/// </summary>
internal static class TiffFrameMetadataExtensions
{
public static T[] GetArray<T>(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<T>(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<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, bool optional = false)
where TEnum : struct
where TTagValue : struct
{
if (meta.TryGetArray(tag, out TTagValue[] result))
{
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<TEnum, TTagValue>(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<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, TEnum? defaultValue = null)
where TEnum : struct
where TTagValue : struct
=> meta.GetSingleEnumNullable<TEnum, TTagValue>(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag)));
public static bool SetSingleEnum<TEnum, TTagValue>(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<T>(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<T>(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<T>(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;
}
}
}

101
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));
}
}
}
}

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

@ -26,27 +26,29 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
this.ByteOrder = other.ByteOrder;
this.XmpProfile = other.XmpProfile;
this.BitsPerPixel = other.BitsPerPixel;
this.Compression = other.Compression;
}
/// <summary>
/// Gets or sets the byte order.
/// Gets the byte order.
/// </summary>
public ByteOrder ByteOrder { get; set; }
public ByteOrder ByteOrder { get; internal set; }
/// <summary>
/// Gets or sets the number of bits per pixel.
/// Gets the number of bits per pixel.
/// </summary>
public TiffBitsPerPixel BitsPerPixel { get; set; } = TiffBitsPerPixel.Pixel24;
public TiffBitsPerPixel BitsPerPixel { get; internal set; } = TiffBitsPerPixel.Pixel24;
/// <summary>
/// Gets or sets the compression used to create the TIFF file.
/// Gets the compression used to create the TIFF file.
/// </summary>
public TiffCompression Compression { get; set; } = TiffCompression.None;
public TiffCompression Compression { get; internal set; } = TiffCompression.None;
/// <summary>
/// Gets or sets the XMP profile.
/// For internal use only. ImageSharp not support XMP profile.
/// </summary>
public byte[] XmpProfile { get; set; }
internal byte[] XmpProfile { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this);

8
src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs

@ -285,9 +285,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// <param name="compression">The compression to use.</param>
/// <param name="compressionLevel">The compression level for deflate compression.</param>
/// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used. Should only be used in combination with deflate or LZW compression.</param>
/// <param name="colorMap">The color map.</param>
/// <param name="entriesCollector">The entries collector.</param>
/// <returns>The number of bytes written.</returns>
public int WritePalettedRgb<TPixel>(Image<TPixel> image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, out IExifValue colorMap)
public int WritePalettedRgb<TPixel>(Image<TPixel> image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, TiffEncoderEntriesCollector entriesCollector)
where TPixel : unmanaged, IPixel<TPixel>
{
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);

202
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -1,11 +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;
@ -126,7 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
}
[Theory]
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)]
[WithFile(SampleMetadata, PixelTypes.Rgba32)]
public void BaselineTags<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -156,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);
@ -175,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
}
[Theory]
[WithFile(TestImages.Tiff.MultiframeDeflateWithPreview, PixelTypes.Rgba32)]
[WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)]
public void SubfileType<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -199,5 +203,197 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(255u, frame1.Height);
}
}
[Theory]
[WithFile(SampleMetadata, PixelTypes.Rgba32, true)]
[WithFile(SampleMetadata, PixelTypes.Rgba32, false)]
public void PreserveMetadata<TPixel>(TestImageProvider<TPixel> provider, bool preserveMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
// Load Tiff image
using Image<TPixel> image = provider.GetImage(new TiffDecoder() { IgnoreMetadata = false });
ImageMetadata coreMeta = image.Metadata;
TiffMetadata tiffMeta = image.Metadata.GetTiffMetadata();
TiffFrameMetadata frameMeta = image.Frames.RootFrame.Metadata.GetTiffMetadata();
// Save to Tiff
var tiffEncoder = new TiffEncoder() { Mode = TiffEncodingMode.Rgb };
if (!preserveMetadata)
{
ClearMeta(image);
}
using var ms = new MemoryStream();
image.Save(ms, tiffEncoder);
// Assert
ms.Position = 0;
using var output = Image.Load<Rgba32>(ms);
ImageMetadata coreMetaOut = output.Metadata;
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);
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("ImageSharp", frameMetaOut.Software);
if (preserveMetadata)
{
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);
Assert.Equal(frameMeta.Copyright, frameMetaOut.Copyright);
}
else
{
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);
Assert.Null(frameMetaOut.Copyright);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CreateMetadata(bool preserveMetadata)
{
// Create image
int w = 10;
int h = 20;
using Image image = new Image<Rgb24>(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.PixelsPerMeter;
coreMeta.HorizontalResolution = 4500;
coreMeta.VerticalResolution = 5400;
var datetime = System.DateTime.Now.ToString();
frameMeta.ImageDescription = "test ImageDescription";
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();
image.Save(ms, tiffEncoder);
// Assert
ms.Position = 0;
using var output = Image.Load<Rgba32>(ms);
TiffMetadata meta = output.Metadata.GetTiffMetadata();
ImageMetadata coreMetaOut = output.Metadata;
TiffMetadata tiffMetaOut = output.Metadata.GetTiffMetadata();
TiffFrameMetadata frameMetaOut = output.Frames.RootFrame.Metadata.GetTiffMetadata();
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);
Assert.Equal(frameMeta.ResolutionUnit, frameMetaOut.ResolutionUnit);
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();
}
}
}

Loading…
Cancel
Save