diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index ba5c588ca..885443e86 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -226,6 +226,38 @@ namespace SixLabors.ImageSharp return value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Clamp(ulong value, ulong min, ulong max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Clamp(long value, long min, long max) + { + if (value > max) + { + return max; + } + + if (value < min) + { + return min; + } + + return value; + } + /// /// Returns the value clamped to the inclusive range of min and max. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs index 733eb4a79..0185afb50 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs @@ -80,6 +80,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// Reference to an IFD (32-bit (4-byte) unsigned integer). /// - Ifd = 13 + Ifd = 13, + + /// + /// A 64-bit (8-byte) unsigned integer. + /// + Long8 = 16, + + /// + /// A 64-bit (8-byte) signed integer (2's complement notation). + /// + SignedLong8 = 17, + + /// + /// Reference to an IFD (64-bit (8-byte) unsigned integer). + /// + Ifd8 = 18, } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs index 4f75999bb..ee30c6b08 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs @@ -32,10 +32,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.Long: case ExifDataType.SignedLong: case ExifDataType.SingleFloat: + case ExifDataType.Ifd: return 4; case ExifDataType.DoubleFloat: case ExifDataType.Rational: - case ExifDataType.SignedRational: + case ExifDataType.Long8: + case ExifDataType.SignedLong8: + case ExifDataType.Ifd8: return 8; default: throw new NotSupportedException(dataType.ToString()); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 6e671b3ec..5c26fde7b 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return values; } - protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader); + protected override void RegisterExtLoader(ulong offset, Action loader) => this.loaders.Add(loader); private void GetThumbnail(uint offset) { @@ -87,6 +87,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif internal abstract class BaseExifReader { private readonly byte[] offsetBuffer = new byte[4]; + private readonly byte[] offsetBuffer8 = new byte[8]; + private readonly byte[] buf8 = new byte[8]; private readonly byte[] buf4 = new byte[4]; private readonly byte[] buf2 = new byte[2]; @@ -119,7 +121,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public bool IsBigEndian { get; protected set; } - protected abstract void RegisterExtLoader(uint offset, Action loader); + protected abstract void RegisterExtLoader(ulong offset, Action loader); /// /// Reads the values to the values collection. @@ -155,6 +157,35 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + protected void ReadValues64(List values, ulong offset) + { + if (offset > (ulong)this.data.Length) + { + return; + } + + this.Seek(offset); + ulong count = this.ReadUInt64(); + + for (ulong i = 0; i < count; i++) + { + this.ReadValue64(values); + } + } + + protected void ReadSubIfd64(List values) + { + if (this.exifOffset != 0) + { + this.ReadValues64(values, this.exifOffset); + } + + if (this.gpsOffset != 0) + { + this.ReadValues64(values, this.gpsOffset); + } + } + private static TDataType[] ToArray(ExifDataType dataType, ReadOnlySpan data, ConverterMethod converter) { int dataTypeSize = (int)ExifDataTypes.GetSize(dataType); @@ -186,7 +217,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return Encoding.UTF8.GetString(buffer); } - private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, uint numberOfComponents) + private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, ulong numberOfComponents) { if (buffer.Length == 0) { @@ -269,6 +300,20 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } return ToArray(dataType, buffer, this.ConvertToSingle); + case ExifDataType.Long8: + if (numberOfComponents == 1) + { + return this.ConvertToUInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); + case ExifDataType.SignedLong8: + if (numberOfComponents == 1) + { + return this.ConvertToInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); case ExifDataType.Undefined: if (numberOfComponents == 1) { @@ -349,6 +394,69 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + private void ReadValue64(List values) + { + if ((this.data.Length - this.data.Position) < 20) + { + return; + } + + var tag = (ExifTagValue)this.ReadUInt16(); + ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + + ulong numberOfComponents = this.ReadUInt64(); + + this.TryReadSpan(this.offsetBuffer8); + + if (dataType == ExifDataType.Unknown) + { + return; + } + + if (dataType == ExifDataType.Undefined && numberOfComponents == 0) + { + numberOfComponents = 8; + } + + // The StripOffsets, StripByteCounts, TileOffsets, and TileByteCounts tags are allowed to have the datatype TIFF_LONG8 in BigTIFF. + // Old datatypes TIFF_LONG, and TIFF_SHORT where allowed in the TIFF 6.0 specification, are still valid in BigTIFF, too. + // https://www.awaresystems.be/imaging/tiff/bigtiff.html + ExifValue exifValue = exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + if (size > 8) + { + ulong newOffset = this.ConvertToUInt64(this.offsetBuffer8); + if (newOffset > ulong.MaxValue || (newOffset + size) > (ulong)this.data.Length) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + this.RegisterExtLoader(newOffset, () => + { + byte[] dataBuffer = new byte[size]; + this.Seek(newOffset); + if (this.TryReadSpan(dataBuffer)) + { + object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); + this.Add(values, exifValue, value); + } + }); + } + else + { + object value = this.ConvertValue(dataType, ((Span)this.offsetBuffer8).Slice(0, (int)size), numberOfComponents); + this.Add(values, exifValue, value); + } + } + private void Add(IList values, IExifValue exif, object value) { if (!exif.TrySetValue(value)) @@ -383,8 +491,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif private void AddInvalidTag(ExifTag tag) => (this.invalidTags ??= new List()).Add(tag); - private void Seek(long pos) - => this.data.Seek(pos, SeekOrigin.Begin); + private void Seek(ulong pos) + => this.data.Seek((long)pos, SeekOrigin.Begin); private bool TryReadSpan(Span span) { @@ -398,6 +506,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return read == length; } + protected ulong ReadUInt64() => + this.TryReadSpan(this.buf8) + ? this.ConvertToUInt64(this.buf8) + : default; + // Known as Long in Exif Specification. protected uint ReadUInt32() => this.TryReadSpan(this.buf4) @@ -408,6 +521,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif ? this.ConvertToShort(this.buf2) : default; + private long ConvertToInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(buffer) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + + private ulong ConvertToUInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(buffer) + : BinaryPrimitives.ReadUInt64LittleEndian(buffer); + } + private double ConvertToDouble(ReadOnlySpan buffer) { if (buffer.Length < 8) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs new file mode 100644 index 000000000..47da962f3 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8 : ExifValue + { + public ExifLong8(ExifTag tag) + : base(tag) + { + } + + public ExifLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8(ExifLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case long intValue: + if (intValue >= 0) + { + this.Value = (ulong)intValue; + return true; + } + + return false; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs new file mode 100644 index 000000000..3cf59adac --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8Array : ExifArrayValue + { + public ExifLong8Array(ExifTag tag) + : base(tag) + { + } + + public ExifLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8Array(ExifLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long8; + + public override IExifValue DeepClone() => new ExifLong8Array(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs index 9e206b23d..d16de4d22 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs @@ -21,6 +21,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { get { + if (this.Value > uint.MaxValue) + { + return ExifDataType.Long8; + } + if (this.Value > ushort.MaxValue) { return ExifDataType.Long; @@ -41,6 +46,18 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif switch (value) { + case long longValue: + if (longValue >= 0) + { + this.Value = (ulong)longValue; + return true; + } + + return false; + case ulong ulongValue: + this.Value = ulongValue; + + return true; case int intValue: if (intValue >= uint.MinValue) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 2d3a93aed..15b29dcec 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif for (int i = 0; i < this.Value.Length; i++) { + if (this.Value[i] > uint.MaxValue) + { + return ExifDataType.Long8; + } + if (this.Value[i] > ushort.MaxValue) { return ExifDataType.Long; @@ -45,6 +50,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif switch (value) { + case long val: + return this.SetSingle(val); + case ulong val: + return this.SetSingle(val); case int val: return this.SetSingle(val); case uint val: @@ -53,6 +62,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return this.SetSingle(val); case ushort val: return this.SetSingle(val); + case long[] array: + return this.SetArray(array); + case ulong[] array: + return this.SetArray(array); case int[] array: return this.SetArray(array); case uint[] array: @@ -74,6 +87,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return true; } + private bool SetArray(long[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ulong[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + private bool SetArray(int[] values) { var numbers = new Number[values.Length]; diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs new file mode 100644 index 000000000..8362dcf2c --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8 : ExifValue + { + public ExifSignedLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8(ExifSignedLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs new file mode 100644 index 000000000..34ce20c8f --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8Array : ExifArrayValue + { + public ExifSignedLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8Array(ExifSignedLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + public override IExifValue DeepClone() => new ExifSignedLong8Array(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index af1eee2dc..7e5b35f49 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public static ExifValue Create(ExifTag tag) => (ExifValue)CreateValue((ExifTagValue)(ushort)tag); - public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, uint numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); + public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, ulong numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, bool isArray) { @@ -19,10 +19,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.DoubleFloat: return isArray ? (ExifValue)new ExifDoubleArray(tag) : new ExifDouble(tag); case ExifDataType.SingleFloat: return isArray ? (ExifValue)new ExifFloatArray(tag) : new ExifFloat(tag); case ExifDataType.Long: return isArray ? (ExifValue)new ExifLongArray(tag) : new ExifLong(tag); + case ExifDataType.Long8: return isArray ? (ExifValue)new ExifLong8Array(tag) : new ExifLong8(tag); case ExifDataType.Rational: return isArray ? (ExifValue)new ExifRationalArray(tag) : new ExifRational(tag); case ExifDataType.Short: return isArray ? (ExifValue)new ExifShortArray(tag) : new ExifShort(tag); case ExifDataType.SignedByte: return isArray ? (ExifValue)new ExifSignedByteArray(tag) : new ExifSignedByte(tag); case ExifDataType.SignedLong: return isArray ? (ExifValue)new ExifSignedLongArray(tag) : new ExifSignedLong(tag); + case ExifDataType.SignedLong8: return isArray ? (ExifValue)new ExifSignedLong8Array(tag) : new ExifSignedLong8(tag); case ExifDataType.SignedRational: return isArray ? (ExifValue)new ExifSignedRationalArray(tag) : new ExifSignedRational(tag); case ExifDataType.SignedShort: return isArray ? (ExifValue)new ExifSignedShortArray(tag) : new ExifSignedShort(tag); case ExifDataType.Ascii: return new ExifString(tag); diff --git a/src/ImageSharp/Primitives/Number.cs b/src/ImageSharp/Primitives/Number.cs index e33d2344a..690b72d79 100644 --- a/src/ImageSharp/Primitives/Number.cs +++ b/src/ImageSharp/Primitives/Number.cs @@ -14,19 +14,19 @@ namespace SixLabors.ImageSharp public struct Number : IEquatable, IComparable { [FieldOffset(0)] - private readonly int signedValue; + private readonly long signedValue; [FieldOffset(0)] - private readonly uint unsignedValue; + private readonly ulong unsignedValue; - [FieldOffset(4)] + [FieldOffset(8)] private readonly bool isSigned; /// /// Initializes a new instance of the struct. /// /// The value of the number. - public Number(int value) + public Number(long value) : this() { this.signedValue = value; @@ -37,30 +37,70 @@ namespace SixLabors.ImageSharp /// Initializes a new instance of the struct. /// /// The value of the number. - public Number(uint value) + public Number(ulong value) : this() { this.unsignedValue = value; this.isSigned = false; } + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(long value) => new Number(value); + + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(ulong value) => new Number(value); + /// /// Converts the specified to an instance of this type. /// /// The value. - public static implicit operator Number(int value) => new Number(value); + public static implicit operator Number(int value) => new Number((long)value); /// /// Converts the specified to an instance of this type. /// /// The value. - public static implicit operator Number(uint value) => new Number(value); + public static implicit operator Number(uint value) => new Number((ulong)value); + + /// + /// Converts the specified to an instance of this type. + /// + /// The value. + public static implicit operator Number(short value) => new Number((long)value); /// /// Converts the specified to an instance of this type. /// /// The value. - public static implicit operator Number(ushort value) => new Number((uint)value); + public static implicit operator Number(ushort value) => new Number((ulong)value); + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert. + public static explicit operator long(Number number) + { + return number.isSigned + ? number.signedValue + : (long)Numerics.Clamp(number.unsignedValue, 0, long.MaxValue); + } + + /// + /// Converts the specified to an instance of this type. + /// + /// The to convert. + public static explicit operator ulong(Number number) + { + return number.isSigned + ? (ulong)Numerics.Clamp(number.signedValue, 0, long.MaxValue) + : number.unsignedValue; + } /// /// Converts the specified to a . @@ -69,8 +109,8 @@ namespace SixLabors.ImageSharp public static explicit operator int(Number number) { return number.isSigned - ? number.signedValue - : (int)Numerics.Clamp(number.unsignedValue, 0, int.MaxValue); + ? (int)Numerics.Clamp(number.signedValue, int.MinValue, int.MaxValue) + : (int)Numerics.Clamp(number.unsignedValue, 0, (uint)int.MaxValue); } /// @@ -80,8 +120,19 @@ namespace SixLabors.ImageSharp public static explicit operator uint(Number number) { return number.isSigned - ? (uint)Numerics.Clamp(number.signedValue, 0, int.MaxValue) - : number.unsignedValue; + ? (uint)Numerics.Clamp(number.signedValue, uint.MinValue, uint.MaxValue) + : (uint)Numerics.Clamp(number.unsignedValue, uint.MinValue, uint.MaxValue); + } + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator short(Number number) + { + return number.isSigned + ? (short)Numerics.Clamp(number.signedValue, short.MinValue, short.MaxValue) + : (short)Numerics.Clamp(number.unsignedValue, 0, (ushort)short.MaxValue); } ///