From e78190f647185c7a8eb68145333d9a8992dd7ba2 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Thu, 6 Jan 2022 11:16:05 +0300 Subject: [PATCH] implement read&write encoded string tags --- .../Metadata/Profiles/Exif/ExifConstants.cs | 64 +++++++++++++++++++ .../Metadata/Profiles/Exif/ExifReader.cs | 9 ++- .../Metadata/Profiles/Exif/ExifWriter.cs | 36 ++++++++--- .../Profiles/Exif/Values/EncodedString.cs | 52 +++++++++++++++ .../Profiles/Exif/Values/EncodedStringCode.cs | 31 +++++++++ .../Profiles/Exif/Values/ExifEncodedString.cs | 48 ++++++++++++++ .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 32 ++++++++++ 7 files changed, 262 insertions(+), 10 deletions(-) create mode 100644 src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs create mode 100644 src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedStringCode.cs create mode 100644 src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs index 0c81f14dd4..f00c7f5b28 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs @@ -2,11 +2,25 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; +using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { internal static class ExifConstants { + private const ulong AsciiCode = 0x_41_53_43_49_49_00_00_00; + private const ulong JISCode = 0x_4A_49_53_00_00_00_00_00; + private const ulong UnicodeCode = 0x_55_4E_49_43_4F_44_45_00; + private const ulong UndefinedCode = 0x_00_00_00_00_00_00_00_00; + + private static readonly byte[] AsciiCodeBytes = { 0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0 }; + private static readonly byte[] JISCodeBytes = { 0x4A, 0x49, 0x53, 0, 0, 0, 0, 0 }; + private static readonly byte[] UnicodeCodeBytes = { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0 }; + private static readonly byte[] UndefinedCodeBytes = { 0, 0, 0, 0, 0, 0, 0, 0 }; + + public const int CharacterCodeBytesLength = 8; + public static ReadOnlySpan LittleEndianByteOrderMarker => new byte[] { (byte)'I', @@ -22,5 +36,55 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 0x00, 0x2A }; + + public static Encoding DefaultAsciiEncoding => Encoding.UTF8; + + public static Encoding JIS0208Encoding => Encoding.GetEncoding(932); + + public static bool TryDetect(ReadOnlySpan buffer, out EncodedStringCode code) + { + if (buffer.Length >= CharacterCodeBytesLength) + { + ulong test = BinaryPrimitives.ReadUInt64LittleEndian(buffer); + switch (test) + { + case AsciiCode: + code = EncodedStringCode.ASCII; + return true; + case JISCode: + code = EncodedStringCode.JIS; + return true; + case UnicodeCode: + code = EncodedStringCode.Unicode; + return true; + case UndefinedCode: + code = EncodedStringCode.Undefined; + return true; + default: + break; + } + } + + code = default; + return false; + } + + public static ReadOnlySpan GetCodeBytes(EncodedStringCode code) => code switch + { + EncodedStringCode.ASCII => AsciiCodeBytes, + EncodedStringCode.JIS => JISCodeBytes, + EncodedStringCode.Unicode => UnicodeCodeBytes, + EncodedStringCode.Undefined => UndefinedCodeBytes, + _ => UndefinedCodeBytes + }; + + public static Encoding GetEncoding(EncodedStringCode code) => code switch + { + EncodedStringCode.ASCII => Encoding.ASCII, + EncodedStringCode.JIS => JIS0208Encoding, + EncodedStringCode.Unicode => Encoding.Unicode, + EncodedStringCode.Undefined => Encoding.UTF8, + _ => Encoding.UTF8 + }; } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 2fcd1cc07d..496035073c 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -252,7 +252,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif buffer = buffer.Slice(0, nullCharIndex); } - return Encoding.UTF8.GetString(buffer); + return ExifConstants.DefaultAsciiEncoding.GetString(buffer); } private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, bool isArray) @@ -360,6 +360,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return this.ConvertToByte(buffer); } + // ext processing + if (ExifConstants.TryDetect(buffer, out EncodedStringCode code)) + { + string text = ExifConstants.GetEncoding(code).GetString(buffer.Slice(ExifConstants.CharacterCodeBytesLength)); + return new EncodedString(text, code); + } + return buffer.ToArray(); default: throw new NotSupportedException($"Data type {dataType} is not supported."); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index e2ed569548..d6fe6fe5f9 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -276,7 +276,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif if (exifValue.DataType == ExifDataType.Ascii) { - return (uint)Encoding.UTF8.GetBytes((string)value).Length + 1; + return (uint)ExifConstants.DefaultAsciiEncoding.GetByteCount((string)value) + 1; + } + + if (value is EncodedString encodedString) + { + return (uint)ExifConstants.GetEncoding(encodedString.Code).GetByteCount(encodedString.Text) + 8; } if (value is Array arrayValue) @@ -289,11 +294,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif private static int WriteArray(IExifValue value, Span destination, int offset) { - if (value.DataType == ExifDataType.Ascii) - { - return WriteValue(ExifDataType.Ascii, value.GetValue(), destination, offset); - } - int newOffset = offset; foreach (object obj in (Array)value.GetValue()) { @@ -378,13 +378,31 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif switch (dataType) { case ExifDataType.Ascii: - offset = Write(Encoding.UTF8.GetBytes((string)value), destination, offset); + offset = Write(ExifConstants.DefaultAsciiEncoding.GetBytes((string)value), destination, offset); destination[offset] = 0; return offset + 1; case ExifDataType.Byte: - case ExifDataType.Undefined: destination[offset] = (byte)value; return offset + 1; + case ExifDataType.Undefined: + if (value is EncodedString encodedString) + { + ReadOnlySpan codeBytes = ExifConstants.GetCodeBytes(encodedString.Code); + codeBytes.CopyTo(destination.Slice(offset)); + offset += codeBytes.Length; + + ReadOnlySpan dataBytes = ExifConstants.GetEncoding(encodedString.Code).GetBytes(encodedString.Text); + dataBytes.CopyTo(destination.Slice(offset)); + offset += dataBytes.Length; + + return offset; + } + else + { + destination[offset] = (byte)value; + return offset + 1; + } + case ExifDataType.DoubleFloat: return WriteDouble((double)value, destination, offset); case ExifDataType.Short: @@ -427,7 +445,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif internal static int WriteValue(IExifValue value, Span destination, int offset) { - if (value.IsArray && value.DataType != ExifDataType.Ascii) + if (value.IsArray) { return WriteArray(value, destination, offset); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs new file mode 100644 index 0000000000..d950812965 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + public readonly struct EncodedString : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The text. + public EncodedString(string text) + : this(text, EncodedStringCode.Unicode) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The text. + /// The code. + public EncodedString(string text, EncodedStringCode code) + { + this.Text = text; + this.Code = code; + } + + /// + /// Gets the text. + /// + public string Text { get; } + + /// + /// Gets the character ode. + /// + public EncodedStringCode Code { get; } + + /// + public override bool Equals(object obj) => obj is EncodedString other && this.Equals(other); + + /// + public bool Equals(EncodedString other) + { + return this.Text == other.Text && this.Code == other.Code; + } + + /// + public override string ToString() => this.Text; + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedStringCode.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedStringCode.cs new file mode 100644 index 0000000000..ee88b4dc09 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedStringCode.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + /// + /// The 8-byte The character code enum. + /// + public enum EncodedStringCode + { + /// + /// The ASCII ITU-T T.50 IA5 character code. + /// + ASCII, + + /// + /// The JIS X208-1990 character code. + /// + JIS, + + /// + /// The Unicode character code. + /// + Unicode, + + /// + /// The undefined character code. + /// + Undefined + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs new file mode 100644 index 0000000000..c2e98783a0 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifEncodedString : ExifValue + { + public ExifEncodedString(ExifTag tag) + : base(tag) + { + } + + public ExifEncodedString(ExifTagValue tag) + : base(tag) + { + } + + private ExifEncodedString(ExifEncodedString value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Undefined; + + protected override string StringValue => this.Value.Text; + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case string stringValue: + this.Value = new EncodedString(stringValue); + return true; + default: + return false; + } + } + + public override IExifValue DeepClone() => new ExifEncodedString(this); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 7b3e20aa2a..07f36cba18 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Metadata; @@ -287,5 +288,36 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(72, imageInfo.Metadata.HorizontalResolution); Assert.Equal(72, imageInfo.Metadata.VerticalResolution); }); + + [Fact] + public void ExifIfdStructure() + { + byte[] exifBytes; + using var memoryStream = new MemoryStream(); + using (var image = Image.Load(TestFile.GetInputFileFullPath(TestImages.Jpeg.Baseline.Calliphora))) + { + var exif = new ExifProfile(); + exif.SetValue(ExifTag.XPAuthor, Encoding.GetEncoding("UCS-2").GetBytes("Dan Petitt")); + + exif.SetValue(ExifTag.XPTitle, Encoding.GetEncoding("UCS-2").GetBytes("A bit of test metadata for image title")); + exif.SetValue(ExifTag.UserComment, Encoding.ASCII.GetBytes("A bit of normal comment text")); + + exif.SetValue(ExifTag.GPSDateStamp, "2022-01-06"); + exif.SetValue(ExifTag.XPKeywords, new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 00, 00, 00, 0x41, 0x41, 0x41 }); + + image.Metadata.ExifProfile = exif; + + exifBytes = exif.ToByteArray(); + + image.Save("c:\\temp\\1.jpeg"); + image.Save(memoryStream, new JpegEncoder()); + } + + memoryStream.Seek(0, SeekOrigin.Begin); + using (var image = Image.Load(memoryStream)) + { + Assert.NotNull(image.Metadata.ExifProfile); + } + } } }