diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs index c8218b77e..f0fb03fd7 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants /// /// Enumeration representing the sub-file types defined by the Tiff file-format. /// - public enum TiffSubfileType : uint + public enum TiffSubfileType : ushort { /// /// Full-resolution image data. diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index d5387166c..3e8f138b7 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -1,10 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; - +using System.IO; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; -using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff @@ -14,92 +14,82 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// internal class DirectoryReader { - private readonly TiffStream stream; - - private readonly EntryReader tagReader; + private readonly Stream stream; private uint nextIfdOffset; - public DirectoryReader(TiffStream stream) + // used for sequential read big values (actual for multiframe big files) + // todo: different tags can link to the same data (stream offset) - investigate + private readonly SortedList lazyLoaders = new SortedList(new DuplicateKeyComparer()); + + public DirectoryReader(Stream stream) => this.stream = stream; + + public ByteOrder ByteOrder { get; private set; } + + public IEnumerable Read() { - this.stream = stream; - this.tagReader = new EntryReader(stream); + this.ByteOrder = ReadByteOrder(this.stream); + this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader(); + return this.ReadIfds(); } - public IEnumerable Read() + private static ByteOrder ReadByteOrder(Stream stream) { - if (this.ReadHeader()) + var headerBytes = new byte[2]; + stream.Read(headerBytes, 0, 2); + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { - return this.ReadIfds(); + return ByteOrder.LittleEndian; + } + else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) + { + return ByteOrder.BigEndian; } - return null; + throw TiffThrowHelper.InvalidHeader(); } - private bool ReadHeader() + private IEnumerable ReadIfds() { - ushort magic = this.stream.ReadUInt16(); - if (magic != TiffConstants.HeaderMagicNumber) + var readers = new List(); + while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length) { - TiffThrowHelper.ThrowInvalidHeader(); + var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders); + reader.ReadTags(); + + this.nextIfdOffset = reader.NextIfdOffset; + + readers.Add(reader); } - uint firstIfdOffset = this.stream.ReadUInt32(); - if (firstIfdOffset == 0) + // sequential reading big values + foreach (Action loader in this.lazyLoaders.Values) { - TiffThrowHelper.ThrowInvalidHeader(); + loader(); } - this.nextIfdOffset = firstIfdOffset; - - return true; - } - - private IEnumerable ReadIfds() - { - var list = new List(); - while (this.nextIfdOffset != 0) + var list = new List(); + foreach (EntryReader reader in readers) { - this.stream.Seek(this.nextIfdOffset); - IExifValue[] ifd = this.ReadIfd(); - list.Add(ifd); + var profile = new ExifProfile(reader.Values, reader.InvalidTags); + list.Add(profile); } - this.tagReader.LoadExtendedData(); - return list; } - private IExifValue[] ReadIfd() + /// used for possiblity add a duplicate offsets (but tags don't duplicate). + /// The type of the key. + private class DuplicateKeyComparer : IComparer + where TKey : IComparable { - long pos = this.stream.Position; - - ushort entryCount = this.stream.ReadUInt16(); - var entries = new List(entryCount); - for (int i = 0; i < entryCount; i++) + public int Compare(TKey x, TKey y) { - IExifValue tag = this.tagReader.ReadNext(); - if (tag != null) - { - entries.Add(tag); - } - } + int result = x.CompareTo(y); - 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); + // Handle equality as beeing greater + return (result == 0) ? 1 : result; } - else if (leftBytes < 0) - { - TiffThrowHelper.ThrowOutOfRange("IFD"); - } - - return entries.ToArray(); } } } diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs index b51997a7e..df45e5434 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -3,317 +3,63 @@ using System; using System.Collections.Generic; -using System.Text; +using System.IO; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; -using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { - internal class EntryReader + internal class EntryReader : BaseExifReader { - private readonly TiffStream stream; + private readonly uint startOffset; - private readonly SortedDictionary extValueLoaders = new SortedDictionary(); + private readonly SortedList lazyLoaders; - /// - /// Initializes a new instance of the class. - /// - /// The stream. - public EntryReader(TiffStream stream) + public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList lazyLoaders) + : base(stream) { - this.stream = stream; + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; + this.startOffset = ifdOffset; + this.lazyLoaders = lazyLoaders; } - public IExifValue ReadNext() - { - var tagId = (ExifTagValue)this.stream.ReadUInt16(); - ExifDataType dataType = EnumUtils.Parse(this.stream.ReadUInt16(), ExifDataType.Unknown); - uint count = this.stream.ReadUInt32(); - - 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 List Values { get; } = new List(); - public void LoadExtendedData() - { - foreach (Action action in this.extValueLoaders.Values) - { - action(); - } - } + public uint NextIfdOffset { get; private set; } - private static bool HasExtData(ExifValue tag, uint count) => ExifDataTypes.GetSize(tag.DataType) * count > 4; - - private static bool SetValue(ExifValue entry, object value) + public void ReadTags() { - 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); - } + this.ReadValues(this.Values, this.startOffset); + this.NextIfdOffset = this.ReadUInt32(); - return entry.TrySetValue(value); + this.ReadSubIfd(this.Values); } - 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; - } - } + protected override void RegisterExtLoader(uint offset, Action reader) => + this.lazyLoaders.Add(offset, reader); + } - 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: - case ExifTagValue.SampleFormat: - return true; + internal class HeaderReader : BaseExifReader + { + public HeaderReader(Stream stream, ByteOrder byteOrder) + : base(stream) => + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; - default: - return count > 1; - } - } + public uint FirstIfdOffset { get; private set; } - private bool ReadValueOrOffset(ExifValue entry, ExifDataType rawDataType, uint count) + public uint ReadFileHeader() { - if (entry.Tag == ExifTag.SubIFDOffset || entry.Tag == ExifTag.GPSIFDOffset /*|| entry.Tag == ExifTagValue.SubIFDs*/) + ushort magic = this.ReadUInt16(); + if (magic != TiffConstants.HeaderMagicNumber) { - // todo: ignore subIfds (exif, gps) - this.stream.Skip(4); - return false; + TiffThrowHelper.ThrowInvalidHeader(); } - 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) - { - TiffThrowHelper.ThrowOutOfRange("IFD entry"); - } - } - - return SetValue(entry, value); + this.FirstIfdOffset = this.ReadUInt32(); + return this.FirstIfdOffset; } - 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) - { - TiffThrowHelper.ThrowBadStringEntry(); - } - - 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; - } - } + protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException(); } } diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs deleted file mode 100644 index 021fb9d51..000000000 --- a/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; - -namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams -{ - internal class TiffBigEndianStream : TiffStream - { - public TiffBigEndianStream(Stream stream) - : base(stream) - { - } - - public override ByteOrder ByteOrder => ByteOrder.BigEndian; - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The converted value. - public override short ReadInt16() - { - byte[] bytes = this.ReadBytes(2); - return (short)((bytes[0] << 8) | bytes[1]); - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The converted value. - public override int ReadInt32() - { - byte[] bytes = this.ReadBytes(4); - return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override uint ReadUInt32() - { - return (uint)this.ReadInt32(); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override ushort ReadUInt16() - { - return (ushort)this.ReadInt16(); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override float ReadSingle() - { - byte[] bytes = this.ReadBytes(4); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - return BitConverter.ToSingle(bytes, 0); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override double ReadDouble() - { - byte[] bytes = this.ReadBytes(8); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - return BitConverter.ToDouble(bytes, 0); - } - } -} diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs deleted file mode 100644 index 43c27b98f..000000000 --- a/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; - -namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams -{ - internal class TiffLittleEndianStream : TiffStream - { - public TiffLittleEndianStream(Stream stream) - : base(stream) - { - } - - public override ByteOrder ByteOrder => ByteOrder.LittleEndian; - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The converted value. - public override short ReadInt16() - { - byte[] bytes = this.ReadBytes(2); - return (short)(bytes[0] | (bytes[1] << 8)); - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The converted value. - public override int ReadInt32() - { - byte[] bytes = this.ReadBytes(4); - return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override uint ReadUInt32() - { - return (uint)this.ReadInt32(); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override ushort ReadUInt16() - { - return (ushort)this.ReadInt16(); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override float ReadSingle() - { - byte[] bytes = this.ReadBytes(4); - - if (!BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - return BitConverter.ToSingle(bytes, 0); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public override double ReadDouble() - { - byte[] bytes = this.ReadBytes(8); - - if (!BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - return BitConverter.ToDouble(bytes, 0); - } - } -} diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs deleted file mode 100644 index e178dd179..000000000 --- a/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; - -namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams -{ - /// - /// The tiff data stream base class. - /// - internal abstract class TiffStream - { - /// - /// The input stream. - /// - private readonly Stream stream; - - /// - /// Initializes a new instance of the class. - /// - /// The stream. - protected TiffStream(Stream stream) - { - this.stream = stream; - } - - /// - /// Gets a value indicating whether the file is encoded in little-endian or big-endian format. - /// - public abstract ByteOrder ByteOrder { get; } - - /// - /// Gets the input stream. - /// - public Stream InputStream => this.stream; - - /// - /// Gets the stream position. - /// - 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); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The converted value. - public byte ReadByte() - { - return (byte)this.stream.ReadByte(); - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The converted value. - 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(); - } -} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index c6d3f42ba..80d5108dd 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -2,12 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; -using System.IO; using System.Threading; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; -using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -50,9 +48,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { options ??= new TiffDecoder(); - this.configuration = configuration ?? Configuration.Default; + this.Configuration = configuration ?? Configuration.Default; this.ignoreMetadata = options.IgnoreMetadata; - this.memoryAllocator = this.configuration.MemoryAllocator; + this.memoryAllocator = this.Configuration.MemoryAllocator; } /// @@ -101,7 +99,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public TiffPredictor Predictor { get; set; } /// - public Configuration Configuration => this.configuration; + public Configuration Configuration { get; } /// public Size Dimensions { get; private set; } @@ -111,21 +109,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff where TPixel : unmanaged, IPixel { this.inputStream = stream; - TiffStream tiffStream = CreateStream(stream); - var reader = new DirectoryReader(tiffStream); + var reader = new DirectoryReader(stream); - IEnumerable directories = reader.Read(); + IEnumerable directories = reader.Read(); var frames = new List>(); var framesMetadata = new List(); - foreach (IExifValue[] ifd in directories) + foreach (ExifProfile ifd in directories) { ImageFrame frame = this.DecodeFrame(ifd, out TiffFrameMetadata frameMetadata); frames.Add(frame); framesMetadata.Add(frameMetadata); } - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder); // todo: tiff frames can have different sizes { @@ -140,7 +137,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - var image = new Image(this.configuration, metadata, frames); + var image = new Image(this.Configuration, metadata, frames); return image; } @@ -149,56 +146,23 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; - TiffStream tiffStream = CreateStream(stream); - var reader = new DirectoryReader(tiffStream); + var reader = new DirectoryReader(stream); - IEnumerable directories = reader.Read(); + IEnumerable directories = reader.Read(); var framesMetadata = new List(); - foreach (IExifValue[] ifd in directories) + foreach (ExifProfile ifd in directories) { - var meta = new TiffFrameMetadata(); - meta.FrameTags.AddRange(ifd); + var meta = new TiffFrameMetadata() { ExifProfile = ifd }; framesMetadata.Add(meta); } - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder); TiffFrameMetadata root = framesMetadata[0]; return new ImageInfo(new PixelTypeInfo(root.BitsPerPixel), (int)root.Width, (int)root.Height, metadata); } - private static TiffStream CreateStream(Stream stream) - { - ByteOrder byteOrder = ReadByteOrder(stream); - if (byteOrder == ByteOrder.BigEndian) - { - return new TiffBigEndianStream(stream); - } - else if (byteOrder == ByteOrder.LittleEndian) - { - return new TiffLittleEndianStream(stream); - } - - throw TiffThrowHelper.InvalidHeader(); - } - - private static ByteOrder ReadByteOrder(Stream stream) - { - var headerBytes = new byte[2]; - stream.Read(headerBytes, 0, 2); - if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) - { - return ByteOrder.LittleEndian; - } - else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) - { - return ByteOrder.BigEndian; - } - - throw TiffThrowHelper.InvalidHeader(); - } - /// /// Decodes the image data from a specified IFD. /// @@ -208,22 +172,22 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// The tiff frame. /// - private ImageFrame DecodeFrame(IExifValue[] tags, out TiffFrameMetadata frameMetaData) + private ImageFrame DecodeFrame(ExifProfile tags, out TiffFrameMetadata frameMetaData) where TPixel : unmanaged, IPixel { var coreMetadata = new ImageFrameMetadata(); frameMetaData = coreMetadata.GetTiffMetadata(); - frameMetaData.FrameTags.AddRange(tags); + frameMetaData.ExifProfile = tags; this.VerifyAndParse(frameMetaData); int width = (int)frameMetaData.Width; int height = (int)frameMetaData.Height; - var frame = new ImageFrame(this.configuration, width, height, coreMetadata); + var frame = new ImageFrame(this.Configuration, width, height, coreMetadata); int rowsPerStrip = (int)frameMetaData.RowsPerStrip; - uint[] stripOffsets = frameMetaData.StripOffsets; - uint[] stripByteCounts = frameMetaData.StripByteCounts; + Number[] stripOffsets = frameMetaData.StripOffsets; + Number[] stripByteCounts = frameMetaData.StripByteCounts; if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { @@ -273,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The image width. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Length; @@ -304,7 +268,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { int stripIndex = (i * stripsPerPixel) + planeIndex; - decompressor.Decompress(this.inputStream, stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); + decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); } colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); @@ -319,7 +283,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); @@ -337,7 +301,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; - decompressor.Decompress(this.inputStream, stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBuffer.GetSpan()); + decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffer.GetSpan()); colorDecoder.Decode(stripBuffer.GetSpan(), pixels, 0, rowsPerStrip * stripIndex, frame.Width, stripHeight); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 99337a8b2..865c1eeba 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -47,37 +47,28 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { if (tiffMetadata.XmpProfile == null) { - byte[] buf = frame.GetArray(ExifTag.XMP, true); - if (buf != null) + IExifValue val = frame.ExifProfile.GetValue(ExifTag.XMP); + if (val != null) { - tiffMetadata.XmpProfile = buf; - } - } - - if (coreMetadata.ExifProfile == null) - { - byte[] buf = frame.GetArray(ExifTag.SubIFDOffset, true); - if (buf != null) - { - coreMetadata.ExifProfile = new ExifProfile(buf); + tiffMetadata.XmpProfile = val.Value; } } if (coreMetadata.IptcProfile == null) { - byte[] buf = frame.GetArray(ExifTag.IPTC, true); - if (buf != null) + IExifValue val = frame.ExifProfile.GetValue(ExifTag.IPTC); + if (val != null) { - coreMetadata.IptcProfile = new IptcProfile(buf); + coreMetadata.IptcProfile = new IptcProfile(val.Value); } } if (coreMetadata.IccProfile == null) { - byte[] buf = frame.GetArray(ExifTag.IccProfile, true); - if (buf != null) + IExifValue val = frame.ExifProfile.GetValue(ExifTag.IccProfile); + if (val != null) { - coreMetadata.IccProfile = new IccProfile(buf); + coreMetadata.IccProfile = new IccProfile(val.Value); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 5e1851018..e4c653209 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -30,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported."); } - if (entries.GetArray(ExifTag.TileOffsets, true) != null) + if (entries.ExifProfile.GetValue(ExifTag.TileOffsets) != null) { TiffThrowHelper.ThrowNotSupported("The Tile images is not supported."); } @@ -242,7 +241,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff case TiffCompression.CcittGroup3Fax: { options.CompressionType = TiffDecoderCompressionType.T4; - IExifValue t4options = entries.FrameTags.Find(tag => tag.Tag == ExifTag.T4Options); + IExifValue t4options = entries.ExifProfile.GetValue(ExifTag.T4Options); if (t4options != null) { var t4OptionValue = (FaxCompressionOptions)t4options.GetValue(); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 5b26a8551..9cdce21e2 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -114,17 +114,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff var xResolution = new ExifRational(ExifTagValue.XResolution) { - Value = frameMetadata.GetSingle(ExifTag.XResolution) + Value = frameMetadata.ExifProfile.GetValue(ExifTag.XResolution).Value }; var yResolution = new ExifRational(ExifTagValue.YResolution) { - Value = frameMetadata.GetSingle(ExifTag.YResolution) + Value = frameMetadata.ExifProfile.GetValue(ExifTag.YResolution).Value }; var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit) { - Value = frameMetadata.GetSingle(ExifTag.ResolutionUnit) + Value = frameMetadata.ExifProfile.GetValue(ExifTag.ResolutionUnit).Value }; this.collector.AddInternal(xResolution); @@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff private void ProcessMetadata(TiffFrameMetadata frameMetadata) { - foreach (IExifValue entry in frameMetadata.FrameTags) + foreach (IExifValue entry in frameMetadata.ExifProfile.Values) { // todo: skip subIfd if (entry.DataType == ExifDataType.Ifd) @@ -183,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } else { - tiffFrameMetadata.Remove(ExifTag.SubIFDOffset); + tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.SubIFDOffset); } if (imageMetadata.IptcProfile != null) @@ -198,7 +198,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } else { - tiffFrameMetadata.Remove(ExifTag.IPTC); + tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.IPTC); } if (imageMetadata.IccProfile != null) @@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } else { - tiffFrameMetadata.Remove(ExifTag.IccProfile); + tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.IccProfile); } TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata(); @@ -227,7 +227,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } else { - tiffFrameMetadata.Remove(ExifTag.XMP); + tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.XMP); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index c9bab385a..e89e337b7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; - +using System.Linq; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -21,6 +21,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff private const TiffPredictor DefaultPredictor = TiffPredictor.None; + private ExifProfile frameTags; + /// /// Initializes a new instance of the class. /// @@ -29,27 +31,31 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } /// - /// Gets the Tiff directory tags list. + /// Gets the Tiff directory tags. /// - public List FrameTags { get; internal set; } = new List(); + public ExifProfile ExifProfile + { + get => this.frameTags ??= new ExifProfile(); + internal set => this.frameTags = value; + } /// Gets a general indication of the kind of data contained in this subfile. /// A general indication of the kind of data contained in this subfile. - public TiffNewSubfileType NewSubfileType => this.GetSingleEnum(ExifTag.SubfileType, TiffNewSubfileType.FullImage); + public TiffNewSubfileType SubfileType => (TiffNewSubfileType?)this.ExifProfile.GetValue(ExifTag.SubfileType)?.Value ?? TiffNewSubfileType.FullImage; /// Gets a general indication of the kind of data contained in this subfile. /// A general indication of the kind of data contained in this subfile. - public TiffSubfileType? SubfileType => this.GetSingleEnumNullable(ExifTag.OldSubfileType); + public TiffSubfileType? OldSubfileType => (TiffSubfileType?)this.ExifProfile.GetValue(ExifTag.OldSubfileType)?.Value; /// /// Gets the number of columns in the image, i.e., the number of pixels per row. /// - public uint Width => this.GetSingle(ExifTag.ImageWidth); + public Number Width => this.ExifProfile.GetValue(ExifTag.ImageWidth).Value; /// /// Gets the number of rows of pixels in the image. /// - public uint Height => this.GetSingle(ExifTag.ImageLength); + public Number Height => this.ExifProfile.GetValue(ExifTag.ImageLength).Value; /// /// Gets the number of bits per component. @@ -58,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff { get { - var bits = this.GetArray(ExifTag.BitsPerSample, true); + var bits = this.ExifProfile.GetValue(ExifTag.BitsPerSample)?.Value; if (bits == null) { if (this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero @@ -92,25 +98,25 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Gets the compression scheme used on the image data. /// The compression scheme used on the image data. - public TiffCompression Compression => this.GetSingleEnum(ExifTag.Compression); + public TiffCompression Compression => (TiffCompression)this.ExifProfile.GetValue(ExifTag.Compression).Value; /// /// Gets the color space of the image data. /// - public TiffPhotometricInterpretation PhotometricInterpretation => this.GetSingleEnum(ExifTag.PhotometricInterpretation); + public TiffPhotometricInterpretation PhotometricInterpretation => (TiffPhotometricInterpretation)this.ExifProfile.GetValue(ExifTag.PhotometricInterpretation).Value; /// /// Gets the logical order of bits within a byte. /// - internal TiffFillOrder FillOrder => this.GetSingleEnum(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst); + internal TiffFillOrder FillOrder => (TiffFillOrder?)this.ExifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst; /// /// Gets or sets the a string that describes the subject of the image. /// public string ImageDescription { - get => this.GetString(ExifTag.ImageDescription); - set => this.SetString(ExifTag.ImageDescription, value); + get => this.ExifProfile.GetValue(ExifTag.ImageDescription)?.Value; + set => this.ExifProfile.SetValue(ExifTag.ImageDescription, value); } /// @@ -118,8 +124,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string Make { - get => this.GetString(ExifTag.Make); - set => this.SetString(ExifTag.Make, value); + get => this.ExifProfile.GetValue(ExifTag.Make)?.Value; + set => this.ExifProfile.SetValue(ExifTag.Make, value); } /// @@ -127,27 +133,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string Model { - get => this.GetString(ExifTag.Model); - set => this.SetString(ExifTag.Model, value); + get => this.ExifProfile.GetValue(ExifTag.Model)?.Value; + set => this.ExifProfile.SetValue(ExifTag.Model, value); } /// Gets for each strip, the byte offset of that strip.. - public uint[] StripOffsets => this.GetArray(ExifTag.StripOffsets); + public Number[] StripOffsets => this.ExifProfile.GetValue(ExifTag.StripOffsets).Value; /// /// Gets the number of components per pixel. /// - public ushort SamplesPerPixel => this.GetSingle(ExifTag.SamplesPerPixel); + public ushort SamplesPerPixel => this.ExifProfile.GetValue(ExifTag.SamplesPerPixel).Value; /// /// Gets the number of rows per strip. /// - public uint RowsPerStrip => this.GetSingle(ExifTag.RowsPerStrip); + public Number RowsPerStrip => this.ExifProfile.GetValue(ExifTag.RowsPerStrip).Value; /// /// Gets for each strip, the number of bytes in the strip after compression. /// - public uint[] StripByteCounts => this.GetArray(ExifTag.StripByteCounts); + public Number[] StripByteCounts => this.ExifProfile.GetValue(ExifTag.StripByteCounts).Value; /// Gets the resolution of the image in x- direction. /// The density of the image in x- direction. @@ -162,7 +168,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// /// Gets how the components of each pixel are stored. /// - public TiffPlanarConfiguration PlanarConfiguration => this.GetSingleEnum(ExifTag.PlanarConfiguration, DefaultPlanarConfiguration); + public TiffPlanarConfiguration PlanarConfiguration => (TiffPlanarConfiguration?)this.ExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration; /// /// Gets the unit of measurement for XResolution and YResolution. @@ -174,8 +180,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string Software { - get => this.GetString(ExifTag.Software); - set => this.SetString(ExifTag.Software, value); + get => this.ExifProfile.GetValue(ExifTag.Software)?.Value; + set => this.ExifProfile.SetValue(ExifTag.Software, value); } /// @@ -183,8 +189,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string DateTime { - get => this.GetString(ExifTag.DateTime); - set => this.SetString(ExifTag.DateTime, value); + get => this.ExifProfile.GetValue(ExifTag.DateTime)?.Value; + set => this.ExifProfile.SetValue(ExifTag.DateTime, value); } /// @@ -192,8 +198,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string Artist { - get => this.GetString(ExifTag.Artist); - set => this.SetString(ExifTag.Artist, value); + get => this.ExifProfile.GetValue(ExifTag.Artist)?.Value; + set => this.ExifProfile.SetValue(ExifTag.Artist, value); } /// @@ -201,39 +207,39 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// public string HostComputer { - get => this.GetString(ExifTag.HostComputer); - set => this.SetString(ExifTag.HostComputer, value); + get => this.ExifProfile.GetValue(ExifTag.HostComputer)?.Value; + set => this.ExifProfile.SetValue(ExifTag.HostComputer, value); } /// /// Gets a color map for palette color images. /// - public ushort[] ColorMap => this.GetArray(ExifTag.ColorMap, true); + public ushort[] ColorMap => this.ExifProfile.GetValue(ExifTag.ColorMap)?.Value; /// /// Gets the description of extra components. /// - public ushort[] ExtraSamples => this.GetArray(ExifTag.ExtraSamples, true); + public ushort[] ExtraSamples => this.ExifProfile.GetValue(ExifTag.ExtraSamples)?.Value; /// /// Gets or sets the copyright notice. /// public string Copyright { - get => this.GetString(ExifTag.Copyright); - set => this.SetString(ExifTag.Copyright, value); + get => this.ExifProfile.GetValue(ExifTag.Copyright)?.Value; + set => this.ExifProfile.SetValue(ExifTag.Copyright, value); } /// /// Gets a mathematical operator that is applied to the image data before an encoding scheme is applied. /// - public TiffPredictor Predictor => this.GetSingleEnum(ExifTag.Predictor, DefaultPredictor); + public TiffPredictor Predictor => (TiffPredictor?)this.ExifProfile.GetValue(ExifTag.Predictor)?.Value ?? DefaultPredictor; /// /// Gets the specifies how to interpret each data sample in a pixel. /// /// - public TiffSampleFormat[] SampleFormat => this.GetEnumArray(ExifTag.SampleFormat, true); + public TiffSampleFormat[] SampleFormat => this.ExifProfile.GetValue(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray(); /// /// Clears the metadata. @@ -241,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public void ClearMetadata() { var tags = new List(); - foreach (IExifValue entry in this.FrameTags) + foreach (IExifValue entry in this.ExifProfile.Values) { switch ((ExifTagValue)(ushort)entry.Tag) { @@ -261,19 +267,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - this.FrameTags = tags; + this.ExifProfile = new ExifProfile(tags, this.ExifProfile.InvalidTags); } /// - public IDeepCloneable DeepClone() - { - var tags = new List(); - foreach (IExifValue entry in this.FrameTags) - { - tags.Add(entry.DeepClone()); - } - - return new TiffFrameMetadata() { FrameTags = tags }; - } + public IDeepCloneable DeepClone() => new TiffFrameMetadata() { ExifProfile = this.ExifProfile.DeepClone() }; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs deleted file mode 100644 index aea12dbe5..000000000 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Linq; - -using SixLabors.ImageSharp.Metadata.Profiles.Exif; - -namespace SixLabors.ImageSharp.Formats.Experimental.Tiff -{ - /// - /// The tiff metadata extensions - /// - internal static class TiffFrameMetadataExtensions - { - public static T[] GetArray(this TiffFrameMetadata meta, ExifTag tag, bool optional = false) - where T : struct - { - if (meta.TryGetArray(tag, out T[] result)) - { - return result; - } - - if (!optional) - { - TiffThrowHelper.ThrowTagNotFound(nameof(tag)); - } - - return null; - } - - public static bool TryGetArray(this TiffFrameMetadata meta, ExifTag tag, out T[] result) - where T : struct - { - foreach (IExifValue entry in meta.FrameTags) - { - if (entry.Tag == tag) - { - DebugGuard.IsTrue(entry.IsArray, "Expected array entry"); - - result = (T[])entry.GetValue(); - return true; - } - } - - result = null; - return false; - } - - public static TEnum[] GetEnumArray(this TiffFrameMetadata meta, ExifTag tag, bool optional = false) - where TEnum : struct - where TTagValue : struct - { - if (meta.TryGetArray(tag, out TTagValue[] result)) - { - return result.Select(a => (TEnum)(object)a).ToArray(); - } - - if (!optional) - { - TiffThrowHelper.ThrowTagNotFound(nameof(tag)); - } - - return null; - } - - public static string GetString(this TiffFrameMetadata meta, ExifTag tag) - { - foreach (IExifValue entry in meta.FrameTags) - { - if (entry.Tag == tag) - { - DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry"); - object value = entry.GetValue(); - DebugGuard.IsTrue(value is string, "Expected string entry"); - - return (string)value; - } - } - - return null; - } - - public static bool SetString(this TiffFrameMetadata meta, ExifTag tag, string value) - { - IExifValue obj = FindOrCreate(meta, tag); - DebugGuard.IsTrue(obj.DataType == ExifDataType.Ascii, "Expected string entry"); - - return obj.TrySetValue(value); - } - - public static TEnum? GetSingleEnumNullable(this TiffFrameMetadata meta, ExifTag tag) - where TEnum : struct - where TTagValue : struct - { - if (!meta.TryGetSingle(tag, out TTagValue value)) - { - return null; - } - - return (TEnum)(object)value; - } - - public static TEnum GetSingleEnum(this TiffFrameMetadata meta, ExifTag tag, TEnum? defaultValue = null) - where TEnum : struct - where TTagValue : struct - => meta.GetSingleEnumNullable(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag))); - - public static bool SetSingleEnum(this TiffFrameMetadata meta, ExifTag tag, TEnum value) - where TEnum : struct - where TTagValue : struct - { - IExifValue obj = FindOrCreate(meta, tag); - - object val = (TTagValue)(object)value; - return obj.TrySetValue(val); - } - - public static T GetSingle(this TiffFrameMetadata meta, ExifTag tag) - where T : struct - { - if (meta.TryGetSingle(tag, out T result)) - { - return result; - } - - throw TiffThrowHelper.TagNotFound(nameof(tag)); - } - - public static bool TryGetSingle(this TiffFrameMetadata meta, ExifTag tag, out T result) - where T : struct - { - foreach (IExifValue entry in meta.FrameTags) - { - if (entry.Tag == tag) - { - DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry"); - - object value = entry.GetValue(); - - result = (T)value; - return true; - } - } - - result = default; - return false; - } - - public static bool SetSingle(this TiffFrameMetadata meta, ExifTag tag, T value) - where T : struct - { - IExifValue obj = FindOrCreate(meta, tag); - DebugGuard.IsTrue(!obj.IsArray, "Expected non array entry"); - - object val = value; - return obj.TrySetValue(val); - } - - public static bool Remove(this TiffFrameMetadata meta, ExifTag tag) - { - IExifValue obj = null; - foreach (IExifValue entry in meta.FrameTags) - { - if (entry.Tag == tag) - { - obj = entry; - break; - } - } - - if (obj != null) - { - return meta.FrameTags.Remove(obj); - } - - return false; - } - - private static IExifValue FindOrCreate(TiffFrameMetadata meta, ExifTag tag) - { - IExifValue obj = null; - foreach (IExifValue entry in meta.FrameTags) - { - if (entry.Tag == tag) - { - obj = entry; - break; - } - } - - if (obj == null) - { - obj = ExifValues.Create(tag); - meta.FrameTags.Add(obj); - } - - return obj; - } - } -} diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs index f17e9dddc..5a7ad1ac9 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs @@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff break; case PixelResolutionUnit.PixelsPerMeter: { - unit = PixelResolutionUnit.PixelsPerCentimeter; - horizontal = UnitConverter.MeterToCm(horizontal); - vertical = UnitConverter.MeterToCm(vertical); + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = UnitConverter.MeterToCm(horizontal); + vertical = UnitConverter.MeterToCm(vertical); } break; @@ -30,29 +30,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff break; } - meta.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1); + meta.ExifProfile.SetValue(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; - } + ushort res = meta.ExifProfile.GetValue(ExifTag.ResolutionUnit)?.Value ?? TiffFrameMetadata.DefaultResolutionUnit; return (PixelResolutionUnit)(res - 1); } - public static double? GetResolution(this TiffFrameMetadata meta, ExifTag tag) + public static double? GetResolution(this TiffFrameMetadata meta, ExifTag tag) { - if (!meta.TryGetSingle(tag, out Rational resolution)) + IExifValue resolution = meta.ExifProfile.GetValue(tag); + if (resolution == null) { return null; } - double res = resolution.ToDouble(); + double res = resolution.Value.ToDouble(); switch (meta.ResolutionUnit) { case PixelResolutionUnit.AspectRatio: @@ -68,11 +66,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff } } - private static void SetResolution(this TiffFrameMetadata meta, ExifTag tag, double? value) + private static void SetResolution(this TiffFrameMetadata meta, ExifTag tag, double? value) { if (value == null) { - meta.Remove(tag); + meta.ExifProfile.RemoveValue(tag); return; } else @@ -94,7 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff break; } - meta.SetSingle(tag, new Rational(res)); + meta.ExifProfile.SetValue(tag, new Rational(res)); } } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 55af45fb4..39c8c2293 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; + using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif @@ -52,6 +53,18 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif this.InvalidTags = Array.Empty(); } + /// + /// Initializes a new instance of the class. + /// + /// The values. + /// The invalid tags. + internal ExifProfile(List values, IReadOnlyList invalidTags) + { + this.Parts = ExifParts.All; + this.values = values; + this.InvalidTags = invalidTags; + } + /// /// Initializes a new instance of the class /// by making a copy from another EXIF profile. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 749c69186..57a9a8cc6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -5,27 +5,103 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal class ExifReader : BaseExifReader + { + private readonly List loaders = new List(); + + public ExifReader(byte[] exifData) + : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)))) + { + } + + /// + /// Reads and returns the collection of EXIF values. + /// + /// + /// The . + /// + public List ReadValues() + { + var values = new List(); + + // II == 0x4949 + this.IsBigEndian = this.ReadUInt16() != 0x4949; + + if (this.ReadUInt16() != 0x002A) + { + return values; + } + + uint ifdOffset = this.ReadUInt32(); + this.ReadValues(values, ifdOffset); + + uint thumbnailOffset = this.ReadUInt32(); + this.GetThumbnail(thumbnailOffset); + + this.ReadSubIfd(values); + + foreach (Action loader in this.loaders) + { + loader(); + } + + return values; + } + + protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader); + + private void GetThumbnail(uint offset) + { + if (offset == 0) + { + return; + } + + var values = new List(); + this.ReadValues(values, offset); + + foreach (ExifValue value in values) + { + if (value == ExifTag.JPEGInterchangeFormat) + { + this.ThumbnailOffset = ((ExifLong)value).Value; + } + else if (value == ExifTag.JPEGInterchangeFormatLength) + { + this.ThumbnailLength = ((ExifLong)value).Value; + } + } + } + } + /// - /// Reads and parses EXIF data from a byte array. + /// Reads and parses EXIF data from a stream. /// - internal sealed class ExifReader + internal abstract class BaseExifReader { - private List invalidTags; - private readonly byte[] exifData; - private int position; + private readonly byte[] offsetBuffer = new byte[4]; + private readonly byte[] buf4 = new byte[4]; + private readonly byte[] buf2 = new byte[2]; + + private readonly Stream data; + private bool isBigEndian; + + private List invalidTags; + private uint exifOffset; + private uint gpsOffset; - public ExifReader(byte[] exifData) - { - this.exifData = exifData ?? throw new ArgumentNullException(nameof(exifData)); - } + protected BaseExifReader(Stream stream) => + this.data = stream ?? throw new ArgumentNullException(nameof(stream)); private delegate TDataType ConverterMethod(ReadOnlySpan data); @@ -35,66 +111,55 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public IReadOnlyList InvalidTags => this.invalidTags ?? (IReadOnlyList)Array.Empty(); /// - /// Gets the thumbnail length in the byte stream. + /// Gets or sets the thumbnail length in the byte stream. /// - public uint ThumbnailLength { get; private set; } + public uint ThumbnailLength { get; protected set; } /// - /// Gets the thumbnail offset position in the byte stream. + /// Gets or sets the thumbnail offset position in the byte stream. /// - public uint ThumbnailOffset { get; private set; } + public uint ThumbnailOffset { get; protected set; } - /// - /// Gets the remaining length. - /// - private int RemainingLength + public bool IsBigEndian { - get - { - if (this.position >= this.exifData.Length) - { - return 0; - } - - return this.exifData.Length - this.position; - } + get => this.isBigEndian; + protected set => this.isBigEndian = value; } + protected abstract void RegisterExtLoader(uint offset, Action loader); + /// - /// Reads and returns the collection of EXIF values. + /// Reads the values to the values collection. /// - /// - /// The . - /// - public List ReadValues() + /// The values. + /// The IFD offset. + protected void ReadValues(List values, uint offset) { - var values = new List(); - - // II == 0x4949 - this.isBigEndian = this.ReadUInt16() != 0x4949; - - if (this.ReadUInt16() != 0x002A) + if (offset > this.data.Length) { - return values; + return; } - uint ifdOffset = this.ReadUInt32(); - this.AddValues(values, ifdOffset); + this.Seek(offset); + int count = this.ReadUInt16(); - uint thumbnailOffset = this.ReadUInt32(); - this.GetThumbnail(thumbnailOffset); + for (int i = 0; i < count; i++) + { + this.ReadValue(values); + } + } + protected void ReadSubIfd(List values) + { if (this.exifOffset != 0) { - this.AddValues(values, this.exifOffset); + this.ReadValues(values, this.exifOffset); } if (this.gpsOffset != 0) { - this.AddValues(values, this.gpsOffset); + this.ReadValues(values, this.gpsOffset); } - - return values; } private static TDataType[] ToArray(ExifDataType dataType, ReadOnlySpan data, ConverterMethod converter) @@ -128,58 +193,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return Encoding.UTF8.GetString(buffer); } - /// - /// Adds the collection of EXIF values to the reader. - /// - /// The values. - /// The index. - private void AddValues(List values, uint index) - { - if (index > (uint)this.exifData.Length) - { - return; - } - - this.position = (int)index; - int count = this.ReadUInt16(); - - for (int i = 0; i < count; i++) - { - if (!this.TryReadValue(out ExifValue value)) - { - continue; - } - - bool duplicate = false; - foreach (IExifValue val in values) - { - if (val == value) - { - duplicate = true; - break; - } - } - - if (duplicate) - { - continue; - } - - if (value == ExifTag.SubIFDOffset) - { - this.exifOffset = ((ExifLong)value).Value; - } - else if (value == ExifTag.GPSIFDOffset) - { - this.gpsOffset = ((ExifLong)value).Value; - } - else - { - values.Add(value); - } - } - } - private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, uint numberOfComponents) { if (buffer.Length == 0) @@ -275,28 +288,28 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } - private bool TryReadValue(out ExifValue exifValue) + private void ReadValue(List values) { - exifValue = default; - // 2 | 2 | 4 | 4 // tag | type | count | value offset - if (this.RemainingLength < 12) + if ((this.data.Length - this.data.Position) < 12) { - return false; + return; } var tag = (ExifTagValue)this.ReadUInt16(); ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + uint numberOfComponents = this.ReadUInt32(); + + this.TryReadSpan(this.offsetBuffer); + // Ensure that the data type is valid if (dataType == ExifDataType.Unknown) { - return false; + return; } - uint numberOfComponents = this.ReadUInt32(); - // Issue #132: ExifDataType == Undefined is treated like a byte array. // If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes) if (dataType == ExifDataType.Undefined && numberOfComponents == 0) @@ -304,110 +317,105 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif numberOfComponents = 4; } - uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); + ExifValue exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); - this.TryReadSpan(4, out ReadOnlySpan offsetBuffer); + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } - object value; + uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); if (size > 4) { - int oldIndex = this.position; - uint newIndex = this.ConvertToUInt32(offsetBuffer); + uint newOffset = this.ConvertToUInt32(this.offsetBuffer); // Ensure that the new index does not overrun the data - if (newIndex > int.MaxValue) + if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) { this.AddInvalidTag(new UnkownExifTag(tag)); - return false; + return; } - this.position = (int)newIndex; - - if (this.RemainingLength < size) + this.RegisterExtLoader(newOffset, () => { - this.AddInvalidTag(new UnkownExifTag(tag)); - - this.position = oldIndex; - return false; - } - - this.TryReadSpan((int)size, out ReadOnlySpan dataBuffer); - - value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); - this.position = oldIndex; + var dataBuffer = new byte[size]; + this.Seek(newOffset); + if (this.TryReadSpan(dataBuffer)) + { + object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); + this.Add(values, exifValue, value); + } + }); } else { - value = this.ConvertValue(dataType, offsetBuffer, numberOfComponents); - } + object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents); - exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + this.Add(values, exifValue, value); + } + } - if (exifValue is null) + private void Add(IList values, IExifValue exif, object value) + { + if (!exif.TrySetValue(value)) { - this.AddInvalidTag(new UnkownExifTag(tag)); - return false; + return; } - if (!exifValue.TrySetValue(value)) + foreach (IExifValue val in values) { - return false; + // sometimes duplicates appear, + // can compare val.Tag == exif.Tag + if (val == exif) + { + Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}"); + return; + } } - return true; + if (exif.Tag == ExifTag.SubIFDOffset) + { + this.exifOffset = (uint)value; + } + else if (exif.Tag == ExifTag.GPSIFDOffset) + { + this.gpsOffset = (uint)value; + } + else + { + values.Add(exif); + } } private void AddInvalidTag(ExifTag tag) - => (this.invalidTags ?? (this.invalidTags = new List())).Add(tag); + => (this.invalidTags ??= new List()).Add(tag); + + private void Seek(long pos) + => this.data.Seek(pos, SeekOrigin.Begin); - private bool TryReadSpan(int length, out ReadOnlySpan span) + private bool TryReadSpan(Span span) { - if (this.RemainingLength < length) + int length = span.Length; + if ((this.data.Length - this.data.Position) < length) { span = default; - return false; } - span = new ReadOnlySpan(this.exifData, this.position, length); - - this.position += length; - - return true; + int readed = this.data.Read(span); + return readed == length; } - private uint ReadUInt32() - { - // Known as Long in Exif Specification - return this.TryReadSpan(4, out ReadOnlySpan span) - ? this.ConvertToUInt32(span) + // Known as Long in Exif Specification + protected uint ReadUInt32() => + this.TryReadSpan(this.buf4) + ? this.ConvertToUInt32(this.buf4) : default; - } - private ushort ReadUInt16() - { - return this.TryReadSpan(2, out ReadOnlySpan span) - ? this.ConvertToShort(span) + protected ushort ReadUInt16() => this.TryReadSpan(this.buf2) + ? this.ConvertToShort(this.buf2) : default; - } - - private void GetThumbnail(uint offset) - { - var values = new List(); - this.AddValues(values, offset); - - foreach (ExifValue value in values) - { - if (value == ExifTag.JPEGInterchangeFormat) - { - this.ThumbnailOffset = ((ExifLong)value).Value; - } - else if (value == ExifTag.JPEGInterchangeFormatLength) - { - this.ThumbnailLength = ((ExifLong)value).Value; - } - } - } private double ConvertToDouble(ReadOnlySpan buffer) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs index 68156fbb3..8aae08160 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs @@ -11,11 +11,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag SubfileType { get; } = new ExifTag(ExifTagValue.SubfileType); - /// - /// Gets the RowsPerStrip exif tag. - /// - public static ExifTag RowsPerStrip { get; } = new ExifTag(ExifTagValue.RowsPerStrip); - /// /// Gets the SubIFDOffset exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index e08a0b1e7..ac4b0a1bf 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -56,11 +56,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag StripRowCounts { get; } = new ExifTag(ExifTagValue.StripRowCounts); - /// - /// Gets the StripByteCounts exif tag. - /// - public static ExifTag StripByteCounts { get; } = new ExifTag(ExifTagValue.StripByteCounts); - /// /// Gets the IntergraphRegisters exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs index 7e73b75aa..680136b03 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs @@ -16,6 +16,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag ImageLength { get; } = new ExifTag(ExifTagValue.ImageLength); + /// + /// Gets the RowsPerStrip exif tag. + /// + public static ExifTag RowsPerStrip { get; } = new ExifTag(ExifTagValue.RowsPerStrip); + /// /// Gets the TileWidth exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs index e9440119e..e4ea6d0de 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs @@ -11,6 +11,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag StripOffsets { get; } = new ExifTag(ExifTagValue.StripOffsets); + /// + /// Gets the StripByteCounts exif tag. + /// + public static ExifTag StripByteCounts { get; } = new ExifTag(ExifTagValue.StripByteCounts); + /// /// Gets the TileByteCounts exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs index f52045531..d27c26ee5 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs @@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag PlanarConfiguration { get; } = new ExifTag(ExifTagValue.PlanarConfiguration); + /// + /// Gets the Predictor exif tag. + /// + public static ExifTag Predictor { get; } = new ExifTag(ExifTagValue.Predictor); + /// /// Gets the GrayResponseUnit exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs index ce0bb36f0..6ed9131c8 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs @@ -46,11 +46,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag TransferFunction { get; } = new ExifTag(ExifTagValue.TransferFunction); - /// - /// Gets the Predictor exif tag. - /// - public static ExifTag Predictor { get; } = new ExifTag(ExifTagValue.Predictor); - /// /// Gets the HalftoneHints exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 28002f0b7..f447173c7 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { internal sealed class ExifNumberArray : ExifArrayValue @@ -36,6 +38,54 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle(val, v => (Number)v); + case uint val: + return this.SetSingle(val, v => (Number)v); + case short val: + return this.SetSingle(val, v => (Number)v); + case ushort val: + return this.SetSingle(val, v => (Number)v); + case int[] array: + return this.SetArray(array, v => (Number)v); + case uint[] array: + return this.SetArray(array, v => (Number)v); + case short[] array: + return this.SetArray(array, v => (Number)v); + case ushort[] array: + return this.SetArray(array, v => (Number)v); + } + + return false; + } + public override IExifValue DeepClone() => new ExifNumberArray(this); + + private bool SetSingle(T value, Func converter) + { + this.Value = new Number[] { converter(value) }; + return true; + } + + private bool SetArray(T[] values, Func converter) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = converter(values[i]); + } + + this.Value = numbers; + return true; + } } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index e47d5da25..af1eee2dc 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -93,6 +93,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth); case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength); + case ExifTagValue.RowsPerStrip: return new ExifNumber(ExifTag.RowsPerStrip); case ExifTagValue.TileWidth: return new ExifNumber(ExifTag.TileWidth); case ExifTagValue.TileLength: return new ExifNumber(ExifTag.TileLength); case ExifTagValue.BadFaxLines: return new ExifNumber(ExifTag.BadFaxLines); @@ -100,6 +101,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifTagValue.PixelXDimension: return new ExifNumber(ExifTag.PixelXDimension); case ExifTagValue.PixelYDimension: return new ExifNumber(ExifTag.PixelYDimension); + case ExifTagValue.StripByteCounts: return new ExifNumberArray(ExifTag.StripByteCounts); case ExifTagValue.StripOffsets: return new ExifNumberArray(ExifTag.StripOffsets); case ExifTagValue.TileByteCounts: return new ExifNumberArray(ExifTag.TileByteCounts); case ExifTagValue.ImageLayer: return new ExifNumberArray(ExifTag.ImageLayer); @@ -158,6 +160,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifTagValue.Orientation: return new ExifShort(ExifTag.Orientation); case ExifTagValue.SamplesPerPixel: return new ExifShort(ExifTag.SamplesPerPixel); case ExifTagValue.PlanarConfiguration: return new ExifShort(ExifTag.PlanarConfiguration); + case ExifTagValue.Predictor: return new ExifShort(ExifTag.Predictor); case ExifTagValue.GrayResponseUnit: return new ExifShort(ExifTag.GrayResponseUnit); case ExifTagValue.ResolutionUnit: return new ExifShort(ExifTag.ResolutionUnit); case ExifTagValue.CleanFaxData: return new ExifShort(ExifTag.CleanFaxData); @@ -203,7 +206,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifTagValue.ExtraSamples: return new ExifShortArray(ExifTag.ExtraSamples); case ExifTagValue.PageNumber: return new ExifShortArray(ExifTag.PageNumber); case ExifTagValue.TransferFunction: return new ExifShortArray(ExifTag.TransferFunction); - case ExifTagValue.Predictor: return new ExifShortArray(ExifTag.Predictor); case ExifTagValue.HalftoneHints: return new ExifShortArray(ExifTag.HalftoneHints); case ExifTagValue.SampleFormat: return new ExifShortArray(ExifTag.SampleFormat); case ExifTagValue.TransferRange: return new ExifShortArray(ExifTag.TransferRange); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index be9f18907..75538a3f7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -8,6 +8,7 @@ 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.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.PixelFormats; @@ -145,6 +146,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(10, image.Metadata.VerticalResolution); TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(30, frame.ExifProfile.Values.Count); + Assert.Equal(32u, frame.Width); Assert.Equal(32u, frame.Height); Assert.Equal(new ushort[] { 4 }, frame.BitsPerSample); @@ -153,10 +156,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff 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(new Number[] { 8u }, frame.StripOffsets, new NumberComparer()); Assert.Equal(1, frame.SamplesPerPixel); Assert.Equal(32u, frame.RowsPerStrip); - Assert.Equal(new uint[] { 297 }, frame.StripByteCounts); + Assert.Equal(new Number[] { 297u }, frame.StripByteCounts, new NumberComparer()); Assert.Equal(10, frame.HorizontalResolution); Assert.Equal(10, frame.VerticalResolution); Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration); @@ -175,6 +178,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(TiffPredictor.None, frame.Predictor); Assert.Null(frame.SampleFormat); Assert.Equal("This is Авторские права", frame.Copyright); + Assert.Equal(4, frame.ExifProfile.GetValue(ExifTag.Rating).Value); + Assert.Equal(75, frame.ExifProfile.GetValue(ExifTag.RatingPercent).Value); } } @@ -191,14 +196,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(2, image.Frames.Count); TiffFrameMetadata frame0 = image.Frames[0].Metadata.GetTiffMetadata(); - Assert.Equal(TiffNewSubfileType.FullImage, frame0.NewSubfileType); - Assert.Null(frame0.SubfileType); + Assert.Equal(TiffNewSubfileType.FullImage, frame0.SubfileType); + Assert.Null(frame0.OldSubfileType); 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(TiffNewSubfileType.Preview, frame1.SubfileType); + Assert.Null(frame1.OldSubfileType); Assert.Equal(255u, frame1.Width); Assert.Equal(255u, frame1.Height); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs index 5d81d3b3d..5ac2f475e 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.IO; using ImageMagick; + using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -54,4 +56,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff return result; } } + + internal class NumberComparer : IEqualityComparer + { + public bool Equals(Number x, Number y) => x.Equals(y); + + public int GetHashCode(Number obj) => obj.GetHashCode(); + } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 466568bfe..10f6ff9bf 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; + using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -14,6 +15,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifProfileTests { public enum TestImageWriteFormat @@ -201,10 +203,14 @@ namespace SixLabors.ImageSharp.Tests IExifValue latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); Assert.Equal(expectedLatitude, latitude.Value); + // todo: duplicate tags + Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); + int profileCount = image.Metadata.ExifProfile.Values.Count; image = WriteAndRead(image, imageFormat); Assert.NotNull(image.Metadata.ExifProfile); + Assert.Equal(0, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); // Should be 3 less. // 1 x due to setting of null "ReferenceBlackWhite" value. @@ -363,8 +369,14 @@ namespace SixLabors.ImageSharp.Tests // Force parsing of the profile. Assert.Equal(25, profile.Values.Count); + // todo: duplicate tags (from root container and subIfd) + Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime)); + byte[] bytes = profile.ToByteArray(); Assert.Equal(525, bytes.Length); + + var profile2 = new ExifProfile(bytes); + Assert.Equal(25, profile2.Values.Count); } [Theory] @@ -477,6 +489,9 @@ namespace SixLabors.ImageSharp.Tests { Assert.NotNull(profile); + // todo: duplicate tags + Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932)); + Assert.Equal(16, profile.Values.Count); foreach (IExifValue value in profile.Values) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs index 401546e5c..4ff37eb6b 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs @@ -8,6 +8,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifReaderTests { [Fact] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs index 2b00cc5b4..42a3b72a6 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs @@ -6,6 +6,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifTagDescriptionAttributeTests { [Fact] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs index 5fe1b51ba..a2c01ea61 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs @@ -7,6 +7,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifValueTests { private ExifProfile profile; diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 898c69356..3358b1f97 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -6,6 +6,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { + [Trait("Profile", "Exif")] public class ExifValuesTests { public static TheoryData ByteTags => new TheoryData @@ -94,6 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values public static TheoryData NumberArrayTags => new TheoryData { { ExifTag.StripOffsets }, + { ExifTag.StripByteCounts }, { ExifTag.TileByteCounts }, { ExifTag.ImageLayer } }; @@ -160,6 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.Orientation }, { ExifTag.SamplesPerPixel }, { ExifTag.PlanarConfiguration }, + { ExifTag.Predictor }, { ExifTag.GrayResponseUnit }, { ExifTag.ResolutionUnit }, { ExifTag.CleanFaxData }, @@ -208,7 +211,6 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.ExtraSamples }, { ExifTag.PageNumber }, { ExifTag.TransferFunction }, - { ExifTag.Predictor }, { ExifTag.HalftoneHints }, { ExifTag.SampleFormat }, { ExifTag.TransferRange }, diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 4edfdc925..1c86289c5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -569,13 +569,15 @@ namespace SixLabors.ImageSharp.Tests public const string FillOrder2 = "Tiff/b0350_fillorder2.tiff"; public const string LittleEndianByteOrder = "Tiff/little_endian.tiff"; + public const string Fax4_Motorola = "Tiff/moy.tiff"; + public const string SampleMetadata = "Tiff/metadata_sample.tiff"; public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ }; public static readonly string[] Metadata = { SampleMetadata }; - public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2 }; + public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2, Calliphora_Fax4Compressed, Fax4_Motorola }; } } } diff --git a/tests/Images/Input/Tiff/moy.tiff b/tests/Images/Input/Tiff/moy.tiff new file mode 100644 index 000000000..197aade49 --- /dev/null +++ b/tests/Images/Input/Tiff/moy.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:026bb9372882d8fc15540b4f94e23138e75aacb0ebf2f5940b056fc66819ec46 +size 1968862