mirror of https://github.com/SixLabors/ImageSharp
Browse Source
In particular: * Support multi framing. * Using Metadata\Profiles\Exif\* types instead their duplicate tag types (moved to __obsolete directory). * Implement useful Metadata classes. * Add extensions. * Test coverage.pull/1570/head
85 changed files with 2875 additions and 1586 deletions
@ -0,0 +1,42 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
internal static class CompressionFactory |
|||
{ |
|||
/// <summary>
|
|||
/// Decompresses an image block from the input stream into the specified buffer.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
/// <param name="compressionType">Type of the compression.</param>
|
|||
/// <param name="offset">The offset within the file of the image block.</param>
|
|||
/// <param name="byteCount">The size (in bytes) of the compressed data.</param>
|
|||
/// <param name="buffer">The buffer to write the uncompressed data.</param>
|
|||
public static void DecompressImageBlock(Stream stream, TiffCompressionType compressionType, uint offset, uint byteCount, byte[] buffer) |
|||
{ |
|||
stream.Seek(offset, SeekOrigin.Begin); |
|||
|
|||
switch (compressionType) |
|||
{ |
|||
case TiffCompressionType.None: |
|||
NoneTiffCompression.Decompress(stream, (int)byteCount, buffer); |
|||
break; |
|||
case TiffCompressionType.PackBits: |
|||
PackBitsTiffCompression.Decompress(stream, (int)byteCount, buffer); |
|||
break; |
|||
case TiffCompressionType.Deflate: |
|||
DeflateTiffCompression.Decompress(stream, (int)byteCount, buffer); |
|||
break; |
|||
case TiffCompressionType.Lzw: |
|||
LzwTiffCompression.Decompress(stream, (int)byteCount, buffer); |
|||
break; |
|||
default: |
|||
throw new InvalidOperationException(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// The tiff data stream byte order enum.
|
|||
/// </summary>
|
|||
public enum TiffByteOrder |
|||
{ |
|||
/// <summary>
|
|||
/// The big-endian byte order (Motorola).
|
|||
/// </summary>
|
|||
BigEndian, |
|||
|
|||
/// <summary>
|
|||
/// The little-endian byte order (Intel).
|
|||
/// </summary>
|
|||
LittleEndian |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Tiff; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
|
|||
namespace SixLabors.ImageSharp |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for the <see cref="ImageMetadata"/> type.
|
|||
/// </summary>
|
|||
public static partial class MetadataExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the tiff format specific metadata for the image.
|
|||
/// </summary>
|
|||
/// <param name="metadata">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="TiffMetadata"/>.</returns>
|
|||
public static TiffMetadata GetTiffMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); |
|||
|
|||
/// <summary>
|
|||
/// Gets the tiff format specific metadata for the image frame.
|
|||
/// </summary>
|
|||
/// <param name="metadata">The metadata this method extends.</param>
|
|||
/// <returns>The <see cref="TiffFrameMetadata"/>.</returns>
|
|||
public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// The base class for photometric interpretation decoders.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal abstract class TiffColorDecoder<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
private readonly ushort[] bitsPerSample; |
|||
|
|||
private readonly ushort[] colorMap; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffColorDecoder{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="bitsPerSample">The number of bits per sample for each pixel.</param>
|
|||
/// <param name="colorMap">The RGB color lookup table to use for decoding the image.</param>
|
|||
protected TiffColorDecoder(ushort[] bitsPerSample, ushort[] colorMap) |
|||
{ |
|||
this.bitsPerSample = bitsPerSample; |
|||
this.colorMap = colorMap; |
|||
} |
|||
|
|||
/* |
|||
/// <summary>
|
|||
/// Gets the photometric interpretation value.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The photometric interpretation value.
|
|||
/// </value>
|
|||
public TiffColorType ColorType { get; } |
|||
*/ |
|||
|
|||
/// <summary>
|
|||
/// Decodes source raw pixel data using the current photometric interpretation.
|
|||
/// </summary>
|
|||
/// <param name="data">The buffer to read image data from.</param>
|
|||
/// <param name="pixels">The image buffer to write pixels to.</param>
|
|||
/// <param name="left">The x-coordinate of the left-hand side of the image block.</param>
|
|||
/// <param name="top">The y-coordinate of the top of the image block.</param>
|
|||
/// <param name="width">The width of the image block.</param>
|
|||
/// <param name="height">The height of the image block.</param> [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|||
public abstract void Decode(byte[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height); |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
internal static class TiffColorDecoderFactory<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
public static TiffColorDecoder<TPixel> Create(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap) |
|||
{ |
|||
switch (colorType) |
|||
{ |
|||
case TiffColorType.WhiteIsZero: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new WhiteIsZeroTiffColor<TPixel>(bitsPerSample); |
|||
|
|||
case TiffColorType.WhiteIsZero1: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new WhiteIsZero1TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.WhiteIsZero4: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new WhiteIsZero4TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.WhiteIsZero8: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new WhiteIsZero8TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.BlackIsZero: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new BlackIsZeroTiffColor<TPixel>(bitsPerSample); |
|||
|
|||
case TiffColorType.BlackIsZero1: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new BlackIsZero1TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.BlackIsZero4: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new BlackIsZero4TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.BlackIsZero8: |
|||
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new BlackIsZero8TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.Rgb: |
|||
DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new RgbTiffColor<TPixel>(bitsPerSample); |
|||
|
|||
case TiffColorType.Rgb888: |
|||
DebugGuard.IsTrue( |
|||
bitsPerSample.Length == 3 |
|||
&& bitsPerSample[0] == 8 |
|||
&& bitsPerSample[1] == 8 |
|||
&& bitsPerSample[2] == 8, |
|||
"bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new Rgb888TiffColor<TPixel>(); |
|||
|
|||
case TiffColorType.PaletteColor: |
|||
DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); |
|||
DebugGuard.NotNull(colorMap, "colorMap"); |
|||
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap); |
|||
|
|||
default: |
|||
throw new InvalidOperationException(); |
|||
} |
|||
} |
|||
|
|||
public static RgbPlanarTiffColor<TPixel> CreatePlanar(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap) |
|||
{ |
|||
switch (colorType) |
|||
{ |
|||
case TiffColorType.RgbPlanar: |
|||
DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); |
|||
DebugGuard.IsTrue(colorMap == null, "colorMap"); |
|||
return new RgbPlanarTiffColor<TPixel>(bitsPerSample); |
|||
|
|||
default: |
|||
throw new InvalidOperationException(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
internal class TiffBigEndianStream : TiffStream |
|||
{ |
|||
public TiffBigEndianStream(Stream stream) |
|||
: base(stream) |
|||
{ |
|||
} |
|||
|
|||
public override TiffByteOrder ByteOrder => TiffByteOrder.BigEndian; |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into an <see cref="short"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override short ReadInt16() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(2); |
|||
return (short)((bytes[0] << 8) | bytes[1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into an <see cref="int"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override int ReadInt32() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(4); |
|||
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="uint"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override uint ReadUInt32() |
|||
{ |
|||
return (uint)this.ReadInt32(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="ushort"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override ushort ReadUInt16() |
|||
{ |
|||
return (ushort)this.ReadInt16(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="float"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override float ReadSingle() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(4); |
|||
|
|||
if (BitConverter.IsLittleEndian) |
|||
{ |
|||
Array.Reverse(bytes); |
|||
} |
|||
|
|||
return BitConverter.ToSingle(bytes, 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="double"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override double ReadDouble() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(8); |
|||
|
|||
if (BitConverter.IsLittleEndian) |
|||
{ |
|||
Array.Reverse(bytes); |
|||
} |
|||
|
|||
return BitConverter.ToDouble(bytes, 0); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
internal class TiffLittleEndianStream : TiffStream |
|||
{ |
|||
public TiffLittleEndianStream(Stream stream) |
|||
: base(stream) |
|||
{ |
|||
} |
|||
|
|||
public override TiffByteOrder ByteOrder => TiffByteOrder.LittleEndian; |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into an <see cref="short"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override short ReadInt16() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(2); |
|||
return (short)(bytes[0] | (bytes[1] << 8)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into an <see cref="int"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override int ReadInt32() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(4); |
|||
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="uint"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override uint ReadUInt32() |
|||
{ |
|||
return (uint)this.ReadInt32(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="ushort"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override ushort ReadUInt16() |
|||
{ |
|||
return (ushort)this.ReadInt16(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="float"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override float ReadSingle() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(4); |
|||
|
|||
if (!BitConverter.IsLittleEndian) |
|||
{ |
|||
Array.Reverse(bytes); |
|||
} |
|||
|
|||
return BitConverter.ToSingle(bytes, 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="double"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public override double ReadDouble() |
|||
{ |
|||
byte[] bytes = this.ReadBytes(8); |
|||
|
|||
if (!BitConverter.IsLittleEndian) |
|||
{ |
|||
Array.Reverse(bytes); |
|||
} |
|||
|
|||
return BitConverter.ToDouble(bytes, 0); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// The tiff data stream base class.
|
|||
/// </summary>
|
|||
internal abstract class TiffStream |
|||
{ |
|||
/// <summary>
|
|||
/// The input stream.
|
|||
/// </summary>
|
|||
private readonly Stream stream; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream.</param>
|
|||
protected TiffStream(Stream stream) |
|||
{ |
|||
this.stream = stream; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the file is encoded in little-endian or big-endian format.
|
|||
/// </summary>
|
|||
public abstract TiffByteOrder ByteOrder { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the input stream.
|
|||
/// </summary>
|
|||
public Stream InputStream => this.stream; |
|||
|
|||
/// <summary>
|
|||
/// Gets the stream position.
|
|||
/// </summary>
|
|||
public long Position => this.stream.Position; |
|||
|
|||
public void Seek(uint offset) |
|||
{ |
|||
this.stream.Seek(offset, SeekOrigin.Begin); |
|||
} |
|||
|
|||
public void Skip(uint offset) |
|||
{ |
|||
this.stream.Seek(offset, SeekOrigin.Current); |
|||
} |
|||
|
|||
public void Skip(int offset) |
|||
{ |
|||
this.stream.Seek(offset, SeekOrigin.Current); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into a <see cref="byte"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public byte ReadByte() |
|||
{ |
|||
return (byte)this.stream.ReadByte(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts buffer data into an <see cref="sbyte"/> using the correct endianness.
|
|||
/// </summary>
|
|||
/// <returns>The converted value.</returns>
|
|||
public sbyte ReadSByte() |
|||
{ |
|||
return (sbyte)this.stream.ReadByte(); |
|||
} |
|||
|
|||
public byte[] ReadBytes(uint count) |
|||
{ |
|||
byte[] buf = new byte[count]; |
|||
this.stream.Read(buf, 0, buf.Length); |
|||
return buf; |
|||
} |
|||
|
|||
public abstract short ReadInt16(); |
|||
|
|||
public abstract int ReadInt32(); |
|||
|
|||
public abstract uint ReadUInt32(); |
|||
|
|||
public abstract ushort ReadUInt16(); |
|||
|
|||
public abstract float ReadSingle(); |
|||
|
|||
public abstract double ReadDouble(); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// The tiff data stream factory class.
|
|||
/// </summary>
|
|||
internal static class TiffStreamFactory |
|||
{ |
|||
public static TiffStream CreateBySignature(Stream stream) |
|||
{ |
|||
TiffByteOrder order = ReadByteOrder(stream); |
|||
return Create(order, stream); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the specified byte order.
|
|||
/// </summary>
|
|||
/// <param name="byteOrder">The byte order.</param>
|
|||
/// <param name="stream">The stream.</param>
|
|||
public static TiffStream Create(TiffByteOrder byteOrder, Stream stream) |
|||
{ |
|||
if (byteOrder == TiffByteOrder.BigEndian) |
|||
{ |
|||
return new TiffBigEndianStream(stream); |
|||
} |
|||
else if (byteOrder == TiffByteOrder.LittleEndian) |
|||
{ |
|||
return new TiffLittleEndianStream(stream); |
|||
} |
|||
|
|||
throw new ArgumentOutOfRangeException(nameof(byteOrder)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads the byte order of stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream.</param>
|
|||
private static TiffByteOrder ReadByteOrder(Stream stream) |
|||
{ |
|||
byte[] headerBytes = new byte[2]; |
|||
stream.Read(headerBytes, 0, 2); |
|||
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) |
|||
{ |
|||
return TiffByteOrder.LittleEndian; |
|||
} |
|||
else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) |
|||
{ |
|||
return TiffByteOrder.BigEndian; |
|||
} |
|||
|
|||
throw new ImageFormatException("Invalid TIFF file header."); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,344 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Net.Http.Headers; |
|||
using SixLabors.ImageSharp.Formats.Tiff; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Exif; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// The decoder helper methods.
|
|||
/// </summary>
|
|||
internal static class TiffDecoderHelpers |
|||
{ |
|||
public static ImageMetadata CreateMetadata<TPixel>(this IList<ImageFrame<TPixel>> frames, bool ignoreMetadata, TiffByteOrder byteOrder) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
var coreMetadata = new ImageMetadata(); |
|||
TiffMetadata tiffMetadata = coreMetadata.GetTiffMetadata(); |
|||
tiffMetadata.ByteOrder = byteOrder; |
|||
|
|||
TiffFrameMetadata rootFrameMetadata = frames.First().Metadata.GetTiffMetadata(); |
|||
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 (ImageFrame<TPixel> frame in frames) |
|||
{ |
|||
TiffFrameMetadata frameMetadata = frame.Metadata.GetTiffMetadata(); |
|||
|
|||
if (tiffMetadata.XmpProfile == null) |
|||
{ |
|||
byte[] buf = frameMetadata.GetArrayValue<byte>(ExifTag.XMP, true); |
|||
if (buf != null) |
|||
{ |
|||
tiffMetadata.XmpProfile = buf; |
|||
} |
|||
} |
|||
|
|||
if (coreMetadata.IptcProfile == null) |
|||
{ |
|||
byte[] buf = frameMetadata.GetArrayValue<byte>(ExifTag.IPTC, true); |
|||
if (buf != null) |
|||
{ |
|||
coreMetadata.IptcProfile = new IptcProfile(buf); |
|||
} |
|||
} |
|||
|
|||
if (coreMetadata.IccProfile == null) |
|||
{ |
|||
byte[] buf = frameMetadata.GetArrayValue<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) |
|||
{ |
|||
if (entries.ExtraSamples != null) |
|||
{ |
|||
throw new NotSupportedException("ExtraSamples is not supported."); |
|||
} |
|||
|
|||
if (entries.FillOrder != TiffFillOrder.MostSignificantBitFirst) |
|||
{ |
|||
throw new NotSupportedException("The lower-order bits of the byte FillOrder is not supported."); |
|||
} |
|||
|
|||
if (entries.GetArrayValue<uint>(ExifTag.TileOffsets, true) != null) |
|||
{ |
|||
throw new NotSupportedException("The Tile images is not supported."); |
|||
} |
|||
|
|||
ParseCompression(options, entries.Compression); |
|||
|
|||
options.PlanarConfiguration = entries.PlanarConfiguration; |
|||
|
|||
ParsePhotometric(options, entries); |
|||
|
|||
ParseBitsPerSample(options, entries); |
|||
|
|||
ParseColorType(options, entries); |
|||
} |
|||
|
|||
private static void ParseColorType(this TiffDecoderCore options, TiffFrameMetadata entries) |
|||
{ |
|||
switch (options.PhotometricInterpretation) |
|||
{ |
|||
case TiffPhotometricInterpretation.WhiteIsZero: |
|||
{ |
|||
if (options.BitsPerSample.Length == 1) |
|||
{ |
|||
switch (options.BitsPerSample[0]) |
|||
{ |
|||
case 8: |
|||
{ |
|||
options.ColorType = TiffColorType.WhiteIsZero8; |
|||
break; |
|||
} |
|||
|
|||
case 4: |
|||
{ |
|||
options.ColorType = TiffColorType.WhiteIsZero4; |
|||
break; |
|||
} |
|||
|
|||
case 1: |
|||
{ |
|||
options.ColorType = TiffColorType.WhiteIsZero1; |
|||
break; |
|||
} |
|||
|
|||
default: |
|||
{ |
|||
options.ColorType = TiffColorType.WhiteIsZero; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case TiffPhotometricInterpretation.BlackIsZero: |
|||
{ |
|||
if (options.BitsPerSample.Length == 1) |
|||
{ |
|||
switch (options.BitsPerSample[0]) |
|||
{ |
|||
case 8: |
|||
{ |
|||
options.ColorType = TiffColorType.BlackIsZero8; |
|||
break; |
|||
} |
|||
|
|||
case 4: |
|||
{ |
|||
options.ColorType = TiffColorType.BlackIsZero4; |
|||
break; |
|||
} |
|||
|
|||
case 1: |
|||
{ |
|||
options.ColorType = TiffColorType.BlackIsZero1; |
|||
break; |
|||
} |
|||
|
|||
default: |
|||
{ |
|||
options.ColorType = TiffColorType.BlackIsZero; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case TiffPhotometricInterpretation.Rgb: |
|||
{ |
|||
if (options.BitsPerSample.Length == 3) |
|||
{ |
|||
if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) |
|||
{ |
|||
if (options.BitsPerSample[0] == 8 && options.BitsPerSample[1] == 8 && options.BitsPerSample[2] == 8) |
|||
{ |
|||
options.ColorType = TiffColorType.Rgb888; |
|||
} |
|||
else |
|||
{ |
|||
options.ColorType = TiffColorType.Rgb; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
options.ColorType = TiffColorType.RgbPlanar; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case TiffPhotometricInterpretation.PaletteColor: |
|||
{ |
|||
options.ColorMap = entries.ColorMap; |
|||
if (options.ColorMap != null) |
|||
{ |
|||
if (options.BitsPerSample.Length == 1) |
|||
{ |
|||
switch (options.BitsPerSample[0]) |
|||
{ |
|||
default: |
|||
{ |
|||
options.ColorType = TiffColorType.PaletteColor; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new ImageFormatException("The TIFF ColorMap entry is missing for a palette color image."); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
default: |
|||
throw new NotSupportedException("The specified TIFF photometric interpretation is not supported: " + options.PhotometricInterpretation); |
|||
} |
|||
} |
|||
|
|||
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 |
|||
{ |
|||
throw new ImageFormatException("The TIFF BitsPerSample entry is missing."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void ParsePhotometric(this TiffDecoderCore options, TiffFrameMetadata entries) |
|||
{ |
|||
/* |
|||
if (!entries.TryGetSingleNumber(ExifTag.PhotometricInterpretation, out uint photometricInterpretation)) |
|||
{ |
|||
if (entries.Compression == TiffCompression.Ccitt1D) |
|||
{ |
|||
photometricInterpretation = (uint)TiffPhotometricInterpretation.WhiteIsZero; |
|||
} |
|||
else |
|||
{ |
|||
throw new ImageFormatException("The TIFF photometric interpretation entry is missing."); |
|||
} |
|||
} |
|||
|
|||
options.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretation; |
|||
/* */ |
|||
|
|||
// 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) |
|||
{ |
|||
case TiffCompression.None: |
|||
{ |
|||
options.CompressionType = TiffCompressionType.None; |
|||
break; |
|||
} |
|||
|
|||
case TiffCompression.PackBits: |
|||
{ |
|||
options.CompressionType = TiffCompressionType.PackBits; |
|||
break; |
|||
} |
|||
|
|||
case TiffCompression.Deflate: |
|||
case TiffCompression.OldDeflate: |
|||
{ |
|||
options.CompressionType = TiffCompressionType.Deflate; |
|||
break; |
|||
} |
|||
|
|||
case TiffCompression.Lzw: |
|||
{ |
|||
options.CompressionType = TiffCompressionType.Lzw; |
|||
break; |
|||
} |
|||
|
|||
default: |
|||
{ |
|||
throw new NotSupportedException("The specified TIFF compression format is not supported: " + compression); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Exif; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// Provides Tiff specific metadata information for the frame.
|
|||
/// </summary>
|
|||
public class TiffFrameMetadata : IDeepCloneable |
|||
{ |
|||
private const TiffResolutionUnit DefaultResolutionUnit = TiffResolutionUnit.Inch; |
|||
|
|||
private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
|
|||
/// </summary>
|
|||
public TiffFrameMetadata() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the Tiff directory tags list.
|
|||
/// </summary>
|
|||
public IList<IExifValue> Tags { get; set; } |
|||
|
|||
/// <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>
|
|||
public TiffNewSubfileType NewSubfileType => this.GetSingleEnum<TiffNewSubfileType, uint>(ExifTag.SubfileType, TiffNewSubfileType.FullImage); |
|||
|
|||
/// <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>
|
|||
public TiffSubfileType? SubfileType => this.GetSingleEnumNullable<TiffSubfileType, uint>(ExifTag.OldSubfileType); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of columns in the image, i.e., the number of pixels per row.
|
|||
/// </summary>
|
|||
public uint Width => this.GetSingleUInt(ExifTag.ImageWidth); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of rows of pixels in the image.
|
|||
/// </summary>
|
|||
public uint Height => this.GetSingleUInt(ExifTag.ImageLength); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of bits per component.
|
|||
/// </summary>
|
|||
public ushort[] BitsPerSample => this.GetArrayValue<ushort>(ExifTag.BitsPerSample, true); |
|||
|
|||
/// <summary>Gets the compression scheme used on the image data.</summary>
|
|||
/// <value>The compression scheme used on the image data.</value>
|
|||
public TiffCompression Compression => this.GetSingleEnum<TiffCompression, ushort>(ExifTag.Compression); |
|||
|
|||
/// <summary>
|
|||
/// Gets the color space of the image data.
|
|||
/// </summary>
|
|||
public TiffPhotometricInterpretation PhotometricInterpretation => this.GetSingleEnum<TiffPhotometricInterpretation, ushort>(ExifTag.PhotometricInterpretation); |
|||
|
|||
/// <summary>
|
|||
/// Gets the logical order of bits within a byte.
|
|||
/// </summary>
|
|||
internal TiffFillOrder FillOrder => this.GetSingleEnum<TiffFillOrder, ushort>(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst); |
|||
|
|||
/// <summary>
|
|||
/// Gets the a string that describes the subject of the image.
|
|||
/// </summary>
|
|||
public string ImageDescription => this.GetString(ExifTag.ImageDescription); |
|||
|
|||
/// <summary>
|
|||
/// Gets the scanner manufacturer.
|
|||
/// </summary>
|
|||
public string Make => this.GetString(ExifTag.Make); |
|||
|
|||
/// <summary>
|
|||
/// Gets the scanner model name or number.
|
|||
/// </summary>
|
|||
public string Model => this.GetString(ExifTag.Model); |
|||
|
|||
/// <summary>Gets for each strip, the byte offset of that strip..</summary>
|
|||
public uint[] StripOffsets => this.GetArrayValue<uint>(ExifTag.StripOffsets); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of rows per strip.
|
|||
/// </summary>
|
|||
public uint RowsPerStrip => this.GetSingleUInt(ExifTag.RowsPerStrip); |
|||
|
|||
/// <summary>
|
|||
/// Gets for each strip, the number of bytes in the strip after compression.
|
|||
/// </summary>
|
|||
public uint[] StripByteCounts => this.GetArrayValue<uint>(ExifTag.StripByteCounts); |
|||
|
|||
/// <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; |
|||
} |
|||
} |
|||
|
|||
/// <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; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets how the components of each pixel are stored.
|
|||
/// </summary>
|
|||
public TiffPlanarConfiguration PlanarConfiguration => this.GetSingleEnum<TiffPlanarConfiguration, ushort>(ExifTag.PlanarConfiguration, DefaultPlanarConfiguration); |
|||
|
|||
/// <summary>
|
|||
/// Gets the unit of measurement for XResolution and YResolution.
|
|||
/// </summary>
|
|||
public TiffResolutionUnit ResolutionUnit => this.GetSingleEnum<TiffResolutionUnit, ushort>(ExifTag.ResolutionUnit, DefaultResolutionUnit); |
|||
|
|||
/// <summary>
|
|||
/// Gets the name and version number of the software package(s) used to create the image.
|
|||
/// </summary>
|
|||
public string Software => this.GetString(ExifTag.Software); |
|||
|
|||
/// <summary>
|
|||
/// Gets the date and time of image creation.
|
|||
/// </summary>
|
|||
public string DateTime => this.GetString(ExifTag.DateTime); |
|||
|
|||
/// <summary>
|
|||
/// Gets the person who created the image.
|
|||
/// </summary>
|
|||
public string Artist => this.GetString(ExifTag.Artist); |
|||
|
|||
/// <summary>
|
|||
/// Gets the computer and/or operating system in use at the time of image creation.
|
|||
/// </summary>
|
|||
public string HostComputer => this.GetString(ExifTag.HostComputer); |
|||
|
|||
/// <summary>
|
|||
/// Gets a color map for palette color images.
|
|||
/// </summary>
|
|||
public ushort[] ColorMap => this.GetArrayValue<ushort>(ExifTag.ColorMap, true); |
|||
|
|||
/// <summary>
|
|||
/// Gets the description of extra components.
|
|||
/// </summary>
|
|||
public ushort[] ExtraSamples => this.GetArrayValue<ushort>(ExifTag.ExtraSamples, true); |
|||
|
|||
/// <summary>
|
|||
/// Gets the copyright notice.
|
|||
/// </summary>
|
|||
public string Copyright => this.GetString(ExifTag.Copyright); |
|||
|
|||
internal T[] GetArrayValue<T>(ExifTag tag, bool optional = false) |
|||
where T : struct |
|||
{ |
|||
if (this.TryGetArrayValue(tag, out T[] result)) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
if (!optional) |
|||
{ |
|||
throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private bool TryGetArrayValue<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 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 |
|||
{ |
|||
if (!this.TryGetSingle(tag, out TTagValue value)) |
|||
{ |
|||
if (defaultValue != null) |
|||
{ |
|||
return defaultValue.Value; |
|||
} |
|||
|
|||
throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); |
|||
} |
|||
|
|||
return (TEnum)(object)value; |
|||
} |
|||
|
|||
/* |
|||
private TEnum GetSingleEnum<TEnum, TEnumParent, TTagValue>(ExifTag tag, TEnum? defaultValue = null) |
|||
where TEnum : struct |
|||
where TEnumParent : struct |
|||
where TTagValue : struct |
|||
{ |
|||
if (!this.TryGetSingle(tag, out TTagValue value)) |
|||
{ |
|||
if (defaultValue != null) |
|||
{ |
|||
return defaultValue.Value; |
|||
} |
|||
|
|||
throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); |
|||
} |
|||
|
|||
return (TEnum)(object)(TEnumParent)Convert.ChangeType(value, typeof(TEnumParent)); |
|||
} */ |
|||
|
|||
private uint GetSingleUInt(ExifTag tag) |
|||
{ |
|||
if (this.TryGetSingle(tag, out uint result)) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); |
|||
} |
|||
|
|||
private bool TryGetSingle<T>(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; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IDeepCloneable DeepClone() |
|||
{ |
|||
var tags = new List<IExifValue>(); |
|||
foreach (IExifValue entry in this.Tags) |
|||
{ |
|||
tags.Add(entry.DeepClone()); |
|||
} |
|||
|
|||
return new TiffFrameMetadata() { Tags = tags }; |
|||
} |
|||
} |
|||
} |
|||
@ -1,63 +1,104 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Exif; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// Data structure for holding details of each TIFF IFD.
|
|||
/// The TIFF IFD reader class.
|
|||
/// </summary>
|
|||
internal struct TiffIfd |
|||
internal class DirectoryReader |
|||
{ |
|||
/// <summary>
|
|||
/// An array of the entries within this IFD.
|
|||
/// </summary>
|
|||
public TiffIfdEntry[] Entries; |
|||
|
|||
/// <summary>
|
|||
/// Offset (in bytes) to the next IFD, or zero if this is the last IFD.
|
|||
/// </summary>
|
|||
public uint NextIfdOffset; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffIfd"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="entries">An array of the entries within the IFD.</param>
|
|||
/// <param name="nextIfdOffset">Offset (in bytes) to the next IFD, or zero if this is the last IFD.</param>
|
|||
public TiffIfd(TiffIfdEntry[] entries, uint nextIfdOffset) |
|||
private readonly TiffStream stream; |
|||
|
|||
private readonly EntryReader tagReader; |
|||
|
|||
private uint nextIfdOffset; |
|||
|
|||
public DirectoryReader(TiffStream stream) |
|||
{ |
|||
this.Entries = entries; |
|||
this.NextIfdOffset = nextIfdOffset; |
|||
this.stream = stream; |
|||
this.tagReader = new EntryReader(stream); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the child <see cref="TiffIfdEntry"/> with the specified tag ID.
|
|||
/// </summary>
|
|||
/// <param name="tag">The tag ID to search for.</param>
|
|||
/// <returns>The resulting <see cref="TiffIfdEntry"/>, or null if it does not exists.</returns>
|
|||
public TiffIfdEntry? GetIfdEntry(ushort tag) |
|||
public IEnumerable<IExifValue[]> Read() |
|||
{ |
|||
for (int i = 0; i < this.Entries.Length; i++) |
|||
if (this.ReadHeader()) |
|||
{ |
|||
if (this.Entries[i].Tag == tag) |
|||
{ |
|||
return this.Entries[i]; |
|||
} |
|||
return this.ReadIfds(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the child <see cref="TiffIfdEntry"/> with the specified tag ID.
|
|||
/// </summary>
|
|||
/// <param name="tag">The tag ID to search for.</param>
|
|||
/// <param name="entry">The resulting <see cref="TiffIfdEntry"/>, if it exists.</param>
|
|||
/// <returns>A flag indicating whether the requested entry exists.</returns>
|
|||
public bool TryGetIfdEntry(ushort tag, out TiffIfdEntry entry) |
|||
private bool ReadHeader() |
|||
{ |
|||
ushort magic = this.stream.ReadUInt16(); |
|||
if (magic != TiffConstants.HeaderMagicNumber) |
|||
{ |
|||
throw new ImageFormatException("Invalid TIFF header magic number: " + magic); |
|||
} |
|||
|
|||
uint firstIfdOffset = this.stream.ReadUInt32(); |
|||
if (firstIfdOffset == 0) |
|||
{ |
|||
throw new ImageFormatException("Invalid TIFF file header."); |
|||
} |
|||
|
|||
this.nextIfdOffset = firstIfdOffset; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private IEnumerable<IExifValue[]> ReadIfds() |
|||
{ |
|||
TiffIfdEntry? nullableEntry = this.GetIfdEntry(tag); |
|||
entry = nullableEntry ?? default(TiffIfdEntry); |
|||
return nullableEntry.HasValue; |
|||
var list = new List<IExifValue[]>(); |
|||
while (this.nextIfdOffset != 0) |
|||
{ |
|||
this.stream.Seek(this.nextIfdOffset); |
|||
IExifValue[] ifd = this.ReadIfd(); |
|||
list.Add(ifd); |
|||
} |
|||
|
|||
this.tagReader.LoadExtendedData(); |
|||
|
|||
return list; |
|||
} |
|||
|
|||
private IExifValue[] ReadIfd() |
|||
{ |
|||
long pos = this.stream.Position; |
|||
|
|||
ushort entryCount = this.stream.ReadUInt16(); |
|||
var entries = new List<IExifValue>(entryCount); |
|||
for (int i = 0; i < entryCount; i++) |
|||
{ |
|||
IExifValue tag = this.tagReader.ReadNext(); |
|||
if (tag != null) |
|||
{ |
|||
entries.Add(tag); |
|||
} |
|||
} |
|||
|
|||
this.nextIfdOffset = this.stream.ReadUInt32(); |
|||
|
|||
int ifdSize = 2 + (entryCount * TiffConstants.SizeOfIfdEntry) + 4; |
|||
int readedBytes = (int)(this.stream.Position - pos); |
|||
int leftBytes = ifdSize - readedBytes; |
|||
if (leftBytes > 0) |
|||
{ |
|||
this.stream.Skip(leftBytes); |
|||
} |
|||
else if (leftBytes < 0) |
|||
{ |
|||
throw new InvalidDataException("Out of range of IFD structure."); |
|||
} |
|||
|
|||
return entries.ToArray(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,46 +1,310 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Runtime.InteropServices; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Exif; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// Data structure for holding details of each TIFF IFD entry.
|
|||
/// </summary>
|
|||
internal struct TiffIfdEntry |
|||
internal class EntryReader |
|||
{ |
|||
/// <summary>
|
|||
/// The Tag ID for this entry. See <see cref="TiffTags"/> for typical values.
|
|||
/// </summary>
|
|||
public ushort Tag; |
|||
private readonly TiffStream stream; |
|||
|
|||
/// <summary>
|
|||
/// The data-type of this entry.
|
|||
/// </summary>
|
|||
public TiffType Type; |
|||
private readonly SortedDictionary<uint, Action> extValueLoaders = new SortedDictionary<uint, Action>(); |
|||
|
|||
/// <summary>
|
|||
/// The number of array items in this entry, or one if only a single value.
|
|||
/// Initializes a new instance of the <see cref="EntryReader" /> class.
|
|||
/// </summary>
|
|||
public uint Count; |
|||
/// <param name="stream">The stream.</param>
|
|||
public EntryReader(TiffStream stream) |
|||
{ |
|||
this.stream = stream; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The raw byte data for this entry.
|
|||
/// </summary>
|
|||
public byte[] Value; |
|||
public IExifValue ReadNext() |
|||
{ |
|||
var tagId = (ExifTagValue)this.stream.ReadUInt16(); |
|||
var dataType = (ExifDataType)EnumUtils.Parse(this.stream.ReadUInt16(), ExifDataType.Unknown); |
|||
uint count = this.stream.ReadUInt32(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffIfdEntry"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="tag">The Tag ID for this entry.</param>
|
|||
/// <param name="type">The data-type of this entry.</param>
|
|||
/// <param name="count">The number of array items in this entry.</param>
|
|||
/// <param name="value">The raw byte data for this entry.</param>
|
|||
public TiffIfdEntry(ushort tag, TiffType type, uint count, byte[] value) |
|||
{ |
|||
this.Tag = tag; |
|||
this.Type = type; |
|||
this.Count = count; |
|||
this.Value = value; |
|||
ExifDataType rawDataType = dataType; |
|||
dataType = LongOrShortFiltering(tagId, dataType); |
|||
bool isArray = GetIsArray(tagId, count); |
|||
|
|||
ExifValue entry = ExifValues.Create(tagId, dataType, isArray); |
|||
if (rawDataType == ExifDataType.Undefined && count == 0) |
|||
{ |
|||
// todo: investgate
|
|||
count = 4; |
|||
} |
|||
|
|||
if (this.ReadValueOrOffset(entry, rawDataType, count)) |
|||
{ |
|||
return entry; |
|||
} |
|||
|
|||
return null; // new UnkownExifTag(tagId);
|
|||
} |
|||
|
|||
public void LoadExtendedData() |
|||
{ |
|||
foreach (Action action in this.extValueLoaders.Values) |
|||
{ |
|||
action(); |
|||
} |
|||
} |
|||
|
|||
private static bool HasExtData(ExifValue tag, uint count) => ExifDataTypes.GetSize(tag.DataType) * count > 4; |
|||
|
|||
private static bool SetValue(ExifValue entry, object value) |
|||
{ |
|||
if (!entry.IsArray && entry.DataType != ExifDataType.Ascii) |
|||
{ |
|||
DebugGuard.IsTrue(((Array)value).Length == 1, "Expected a length is 1"); |
|||
|
|||
var single = ((Array)value).GetValue(0); |
|||
return entry.TrySetValue(single); |
|||
} |
|||
|
|||
return entry.TrySetValue(value); |
|||
} |
|||
|
|||
private static ExifDataType LongOrShortFiltering(ExifTagValue tagId, ExifDataType dataType) |
|||
{ |
|||
switch (tagId) |
|||
{ |
|||
case ExifTagValue.ImageWidth: |
|||
case ExifTagValue.ImageLength: |
|||
case ExifTagValue.StripOffsets: |
|||
case ExifTagValue.RowsPerStrip: |
|||
case ExifTagValue.StripByteCounts: |
|||
case ExifTagValue.TileWidth: |
|||
case ExifTagValue.TileLength: |
|||
case ExifTagValue.TileOffsets: |
|||
case ExifTagValue.TileByteCounts: |
|||
case ExifTagValue.OldSubfileType: // by spec SHORT, but can be LONG
|
|||
return ExifDataType.Long; |
|||
|
|||
default: |
|||
return dataType; |
|||
} |
|||
} |
|||
|
|||
private static bool GetIsArray(ExifTagValue tagId, uint count) |
|||
{ |
|||
switch (tagId) |
|||
{ |
|||
case ExifTagValue.BitsPerSample: |
|||
case ExifTagValue.StripOffsets: |
|||
case ExifTagValue.StripByteCounts: |
|||
case ExifTagValue.TileOffsets: |
|||
case ExifTagValue.TileByteCounts: |
|||
case ExifTagValue.ColorMap: |
|||
case ExifTagValue.ExtraSamples: |
|||
return true; |
|||
|
|||
default: |
|||
return count > 1; |
|||
} |
|||
} |
|||
|
|||
private bool ReadValueOrOffset(ExifValue entry, ExifDataType rawDataType, uint count) |
|||
{ |
|||
if (HasExtData(entry, count)) |
|||
{ |
|||
uint offset = this.stream.ReadUInt32(); |
|||
this.extValueLoaders.Add(offset, () => |
|||
{ |
|||
this.ReadExtValue(entry, rawDataType, offset, count); |
|||
}); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
long pos = this.stream.Position; |
|||
object value = this.ReadData(entry.DataType, rawDataType, count); |
|||
if (value == null) |
|||
{ |
|||
// read unknown type value
|
|||
value = this.stream.ReadBytes(4); |
|||
} |
|||
else |
|||
{ |
|||
int leftBytes = 4 - (int)(this.stream.Position - pos); |
|||
if (leftBytes > 0) |
|||
{ |
|||
this.stream.Skip(leftBytes); |
|||
} |
|||
else if (leftBytes < 0) |
|||
{ |
|||
throw new InvalidDataException("Out of range of IFD entry structure."); |
|||
} |
|||
} |
|||
|
|||
return SetValue(entry, value); |
|||
} |
|||
|
|||
private void ReadExtValue(ExifValue entry, ExifDataType rawDataType, uint offset, uint count) |
|||
{ |
|||
DebugGuard.IsTrue(HasExtData(entry, count), "Excepted extended data"); |
|||
DebugGuard.MustBeGreaterThanOrEqualTo(offset, (uint)TiffConstants.SizeOfTiffHeader, nameof(offset)); |
|||
|
|||
this.stream.Seek(offset); |
|||
var value = this.ReadData(entry.DataType, rawDataType, count); |
|||
|
|||
SetValue(entry, value); |
|||
|
|||
DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii || count > 1 ^ !entry.IsArray, "Invalid tag"); |
|||
DebugGuard.IsTrue(entry.GetValue() != null, "Invalid tag"); |
|||
} |
|||
|
|||
private object ReadData(ExifDataType entryDataType, ExifDataType rawDataType, uint count) |
|||
{ |
|||
switch (rawDataType) |
|||
{ |
|||
case ExifDataType.Byte: |
|||
case ExifDataType.Undefined: |
|||
{ |
|||
return this.stream.ReadBytes(count); |
|||
} |
|||
|
|||
case ExifDataType.SignedByte: |
|||
{ |
|||
sbyte[] res = new sbyte[count]; |
|||
byte[] buf = this.stream.ReadBytes(count); |
|||
Array.Copy(buf, res, buf.Length); |
|||
return res; |
|||
} |
|||
|
|||
case ExifDataType.Short: |
|||
{ |
|||
if (entryDataType == ExifDataType.Long) |
|||
{ |
|||
uint[] buf = new uint[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadUInt16(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
else |
|||
{ |
|||
ushort[] buf = new ushort[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadUInt16(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
} |
|||
|
|||
case ExifDataType.SignedShort: |
|||
{ |
|||
short[] buf = new short[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadInt16(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.Long: |
|||
{ |
|||
uint[] buf = new uint[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadUInt32(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.SignedLong: |
|||
{ |
|||
int[] buf = new int[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadInt32(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.Ascii: |
|||
{ |
|||
byte[] buf = this.stream.ReadBytes(count); |
|||
|
|||
if (buf[buf.Length - 1] != 0) |
|||
{ |
|||
throw new ImageFormatException("The retrieved string is not null terminated."); |
|||
} |
|||
|
|||
return Encoding.UTF8.GetString(buf, 0, buf.Length - 1); |
|||
} |
|||
|
|||
case ExifDataType.SingleFloat: |
|||
{ |
|||
float[] buf = new float[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadSingle(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.DoubleFloat: |
|||
{ |
|||
double[] buf = new double[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
buf[i] = this.stream.ReadDouble(); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.Rational: |
|||
{ |
|||
var buf = new Rational[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
uint numerator = this.stream.ReadUInt32(); |
|||
uint denominator = this.stream.ReadUInt32(); |
|||
buf[i] = new Rational(numerator, denominator); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.SignedRational: |
|||
{ |
|||
var buf = new SignedRational[count]; |
|||
for (int i = 0; i < buf.Length; i++) |
|||
{ |
|||
int numerator = this.stream.ReadInt32(); |
|||
int denominator = this.stream.ReadInt32(); |
|||
buf[i] = new SignedRational(numerator, denominator); |
|||
} |
|||
|
|||
return buf; |
|||
} |
|||
|
|||
case ExifDataType.Ifd: |
|||
{ |
|||
return this.stream.ReadUInt32(); |
|||
} |
|||
|
|||
default: |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,44 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff |
|||
{ |
|||
/// <summary>
|
|||
/// Provides Tiff specific metadata information for the image.
|
|||
/// </summary>
|
|||
public class TiffMetadata : IDeepCloneable |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffMetadata"/> class.
|
|||
/// </summary>
|
|||
public TiffMetadata() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TiffMetadata"/> class.
|
|||
/// </summary>
|
|||
/// <param name="other">The metadata to create an instance from.</param>
|
|||
private TiffMetadata(TiffMetadata other) |
|||
{ |
|||
this.ByteOrder = other.ByteOrder; |
|||
this.XmpProfile = other.XmpProfile; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the byte order.
|
|||
/// </summary>
|
|||
public TiffByteOrder ByteOrder { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the XMP profile.
|
|||
/// </summary>
|
|||
public byte[] XmpProfile { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public IDeepCloneable DeepClone() => new TiffMetadata(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using SixLabors.ImageSharp.Formats; |
|||
using SixLabors.ImageSharp.Formats.Tiff; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Tiff |
|||
{ |
|||
[Trait("Category", "Tiff.BlackBox.Encoder")] |
|||
[Trait("Category", "Tiff")] |
|||
public class ImageExtensionsTest |
|||
{ |
|||
[Theory] |
|||
[WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] |
|||
public void ThrowsSavingNotImplemented<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
Assert.Throws<NotImplementedException>(() => |
|||
{ |
|||
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); |
|||
string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); |
|||
using var image = provider.GetImage(new TiffDecoder()); |
|||
image.SaveAsTiff(file); |
|||
}); |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public void SaveAsTiff_Path() |
|||
{ |
|||
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); |
|||
string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
image.SaveAsTiff(file); |
|||
} |
|||
|
|||
using (Image.Load(file, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public async Task SaveAsTiffAsync_Path() |
|||
{ |
|||
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); |
|||
string file = Path.Combine(dir, "SaveAsTiffAsync_Path.tiff"); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
await image.SaveAsTiffAsync(file); |
|||
} |
|||
|
|||
using (Image.Load(file, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public void SaveAsTiff_Path_Encoder() |
|||
{ |
|||
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); |
|||
string file = Path.Combine(dir, "SaveAsTiff_Path_Encoder.tiff"); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
image.SaveAsTiff(file, new TiffEncoder()); |
|||
} |
|||
|
|||
using (Image.Load(file, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public async Task SaveAsTiffAsync_Path_Encoder() |
|||
{ |
|||
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); |
|||
string file = Path.Combine(dir, "SaveAsTiffAsync_Path_Encoder.tiff"); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
await image.SaveAsTiffAsync(file, new TiffEncoder()); |
|||
} |
|||
|
|||
using (Image.Load(file, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public void SaveAsTiff_Stream() |
|||
{ |
|||
using var memoryStream = new MemoryStream(); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
image.SaveAsTiff(memoryStream); |
|||
} |
|||
|
|||
memoryStream.Position = 0; |
|||
|
|||
using (Image.Load(memoryStream, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public async Task SaveAsTiffAsync_StreamAsync() |
|||
{ |
|||
using var memoryStream = new MemoryStream(); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
await image.SaveAsTiffAsync(memoryStream); |
|||
} |
|||
|
|||
memoryStream.Position = 0; |
|||
|
|||
using (Image.Load(memoryStream, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public void SaveAsTiff_Stream_Encoder() |
|||
{ |
|||
using var memoryStream = new MemoryStream(); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
image.SaveAsTiff(memoryStream, new TiffEncoder()); |
|||
} |
|||
|
|||
memoryStream.Position = 0; |
|||
|
|||
using (Image.Load(memoryStream, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
|
|||
[Fact(Skip = "Saving not implemented")] |
|||
public async Task SaveAsTiffAsync_Stream_Encoder() |
|||
{ |
|||
using var memoryStream = new MemoryStream(); |
|||
|
|||
using (var image = new Image<Rgba32>(10, 10)) |
|||
{ |
|||
await image.SaveAsTiffAsync(memoryStream, new TiffEncoder()); |
|||
} |
|||
|
|||
memoryStream.Position = 0; |
|||
|
|||
using (Image.Load(memoryStream, out IImageFormat mime)) |
|||
{ |
|||
Assert.Equal("image/tiff", mime.DefaultMimeType); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Tiff; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Tiff |
|||
{ |
|||
[Trait("Category", "Tiff.BlackBox")] |
|||
[Trait("Category", "Tiff")] |
|||
public class TiffMetadataTests |
|||
{ |
|||
public static readonly string[] MetadataImages = TestImages.Tiff.Metadata; |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32, false)] |
|||
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32, true)] |
|||
public void MetadataProfiles<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage(new TiffDecoder() { IgnoreMetadata = ignoreMetadata })) |
|||
{ |
|||
TiffMetadata meta = image.Metadata.GetTiffMetadata(); |
|||
Assert.NotNull(meta); |
|||
if (ignoreMetadata) |
|||
{ |
|||
Assert.Null(meta.XmpProfile); |
|||
} |
|||
else |
|||
{ |
|||
Assert.NotNull(meta.XmpProfile); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] |
|||
public void BaselineTags<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage(new TiffDecoder())) |
|||
{ |
|||
TiffMetadata meta = image.Metadata.GetTiffMetadata(); |
|||
|
|||
Assert.NotNull(meta); |
|||
Assert.Equal(TiffByteOrder.LittleEndian, meta.ByteOrder); |
|||
Assert.Equal(PixelResolutionUnit.PixelsPerInch, image.Metadata.ResolutionUnits); |
|||
Assert.Equal(10, image.Metadata.HorizontalResolution); |
|||
Assert.Equal(10, image.Metadata.VerticalResolution); |
|||
|
|||
TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata(); |
|||
Assert.Equal(32u, frame.Width); |
|||
Assert.Equal(32u, frame.Height); |
|||
Assert.Equal(new ushort[] { 8, 8, 8 }, frame.BitsPerSample); |
|||
Assert.Equal(TiffCompression.Lzw, frame.Compression); |
|||
Assert.Equal(TiffPhotometricInterpretation.Rgb, frame.PhotometricInterpretation); |
|||
Assert.Equal("This is Название", frame.ImageDescription); |
|||
Assert.Equal("This is Изготовитель камеры", frame.Make); |
|||
Assert.Equal("This is Модель камеры", frame.Model); |
|||
Assert.Equal(new uint[] { 8 }, frame.StripOffsets); |
|||
Assert.Equal(32u, frame.RowsPerStrip); |
|||
Assert.Equal(new uint[] { 750 }, frame.StripByteCounts); |
|||
Assert.Equal(10, frame.HorizontalResolution); |
|||
Assert.Equal(10, frame.VerticalResolution); |
|||
Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration); |
|||
Assert.Equal(TiffResolutionUnit.Inch, frame.ResolutionUnit); |
|||
Assert.Equal("IrfanView", frame.Software); |
|||
Assert.Equal(null, frame.DateTime); |
|||
Assert.Equal("This is;Автор", frame.Artist); |
|||
Assert.Equal(null, frame.HostComputer); |
|||
Assert.Equal(null, frame.ColorMap); |
|||
Assert.Equal(null, frame.ExtraSamples); |
|||
Assert.Equal("This is Авторские права", frame.Copyright); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Tiff.MultiframeDeflateWithPreview, PixelTypes.Rgba32)] |
|||
public void SubfileType<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage(new TiffDecoder())) |
|||
{ |
|||
TiffMetadata meta = image.Metadata.GetTiffMetadata(); |
|||
Assert.NotNull(meta); |
|||
|
|||
Assert.Equal(2, image.Frames.Count); |
|||
|
|||
TiffFrameMetadata frame0 = image.Frames[0].Metadata.GetTiffMetadata(); |
|||
Assert.Equal(TiffNewSubfileType.FullImage, frame0.NewSubfileType); |
|||
Assert.Equal(null, frame0.SubfileType); |
|||
Assert.Equal(255u, frame0.Width); |
|||
Assert.Equal(255u, frame0.Height); |
|||
|
|||
TiffFrameMetadata frame1 = image.Frames[1].Metadata.GetTiffMetadata(); |
|||
Assert.Equal(TiffNewSubfileType.Preview, frame1.NewSubfileType); |
|||
Assert.Equal(TiffSubfileType.Preview, frame1.SubfileType); |
|||
Assert.Equal(255u, frame1.Width); |
|||
Assert.Equal(255u, frame1.Height); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue