From f15124bc7ec8a6b1b6e3306bc3786cd34c8cd1c3 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Sun, 12 Apr 2020 20:45:27 +1000 Subject: [PATCH] WIP --- MetadataExtractor/ExifTags.cs | 92 +- .../Formats/Exif/ExifDescriptorBase.cs | 1138 +---------------- MetadataExtractor/Formats/Xmp/XmpDirectory.cs | 1 - MetadataExtractor/MetadataExtractor.csproj | 2 +- MetadataExtractor/NewApi.cs | 445 +++++-- MetadataExtractor/Rational.cs | 6 +- MetadataExtractor/URational.cs | 348 +++++ 7 files changed, 781 insertions(+), 1251 deletions(-) create mode 100644 MetadataExtractor/URational.cs diff --git a/MetadataExtractor/ExifTags.cs b/MetadataExtractor/ExifTags.cs index bfe80dbf5..1dc2c6be4 100644 --- a/MetadataExtractor/ExifTags.cs +++ b/MetadataExtractor/ExifTags.cs @@ -1,3 +1,5 @@ +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System; using System.Collections.Generic; using System.Text; @@ -160,7 +162,7 @@ public static class ExifTags public static readonly TiffUInt16Tag MaxSampleValue = new TiffUInt16Tag(0x0119, "Maximum Sample Value"); - // TODO the descriptor function for these two tags attempts to read units from the directory, which is not yet available via this new API + // TODO the descriptor function for these two tags attempts to read ResolutionUnit from the directory, which is not yet available via this new API public static readonly TiffUInt16Tag XResolution = new TiffUInt16Tag(0x011A, "X Resolution"); public static readonly TiffUInt16Tag YResolution = new TiffUInt16Tag(0x011B, "Y Resolution"); @@ -263,6 +265,7 @@ public static class ExifTags public static readonly TiffUInt16Tag CfaRepeatPatternDim = new TiffUInt16Tag(0x828D, "CFA Repeat Pattern Dim"); /// There are two definitions for CFA pattern, I don't know the difference... + // TODO interpreting this tag requires the value of CfaRepeatPatternDim too public static readonly TiffUInt16Tag CfaPattern2 = new TiffUInt16Tag(0x828E, "CFA Pattern 2"); public static readonly TiffUInt16Tag BatteryLevel = new TiffUInt16Tag(0x828F, "Battery Level"); @@ -274,7 +277,8 @@ public static class ExifTags public static readonly TiffURationalTag ExposureTime = new TiffURationalTag(0x829A, "Exposure Time", (value, format) => $"{value.ToSimpleString()} sec"); /// The actual F-number(F-stop) of lens when the image was taken. - public static readonly TiffURationalTag FNumber = new TiffURationalTag(0x829D, "F-Number", DescribeFStop); + public static readonly TiffURationalTag FNumber = new TiffURationalTag(0x829D, "F-Number", + (value, format) => DescribeFStop(PhotographicConversions.ApertureToFStop(value.ToDouble()), format)); // TODO what type is this? public static readonly TiffUInt16Tag IptcNaa = new TiffUInt16Tag(0x83BB, "Iptc Naa"); @@ -365,7 +369,7 @@ public static class ExifTags // TODO is this URational /// Average (rough estimate) compression level in JPEG bits per pixel. - public static readonly RationalTag CompressedAverageBitsPerPixel = new RationalTag(0x9102, "Compressed Average Bits Per Pixel", + public static readonly TiffRationalTag CompressedAverageBitsPerPixel = new TiffRationalTag(0x9102, "Compressed Average Bits Per Pixel", (r, p) => $"{r.ToSimpleString(provider: p)} bit{(r.IsInteger && r.ToInt32() == 1 ? "" : "s")}/pixel"); /// Shutter speed by APEX value. @@ -397,18 +401,18 @@ public static class ExifTags /// Lens aperture used in an image. public static readonly TiffURationalTag Aperture = new TiffURationalTag(0x9202, "Aperture", - (r, p) => DescribeFStop(PhotographicConversions.ApertureToFStop(r))); + (value, p) => DescribeFStop(PhotographicConversions.ApertureToFStop(value.ToDouble()), p)); - public static readonly RationalTag BrightnessValue = new RationalTag(0x9203, "Brightness"); + public static readonly TiffRationalTag BrightnessValue = new TiffRationalTag(0x9203, "Brightness"); - public static readonly RationalTag ExposureBias = new RationalTag(0x9204, "Exposure Bias", (value, format) => string.Format(format, "{0} EV", value.ToSimpleString())); + public static readonly TiffRationalTag ExposureBias = new TiffRationalTag(0x9204, "Exposure Bias", (value, format) => string.Format(format, "{0} EV", value.ToSimpleString())); /// Maximum aperture of lens. - public static readonly RationalTag MaxAperture = new RationalTag(0x9205, "Max Aperture", (value, format) => DescribeFStop(PhotographicConversions.ApertureToFStop(value.ToDouble()), format)); + public static readonly TiffRationalTag MaxAperture = new TiffRationalTag(0x9205, "Max Aperture", (value, format) => DescribeFStop(PhotographicConversions.ApertureToFStop(value.ToDouble()), format)); /// The distance autofocus focused to. /// Tends to be less accurate as distance increases. - public static readonly RationalTag SubjectDistance = new RationalTag(0x9206, "Subject Distance", (value, format) => $"{value.ToDouble():0.0##} metres"); + public static readonly TiffRationalTag SubjectDistance = new TiffRationalTag(0x9206, "Subject Distance", (value, format) => $"{value.ToDouble():0.0##} metres"); /// Exposure metering method. public static readonly TiffMappedUInt16Tag MeteringMode = new TiffMappedUInt16Tag(0x9207, "Metering Mode", new Dictionary @@ -522,10 +526,10 @@ public static class ExifTags /// /// The component count for this tag includes all of the bytes needed for the makernote. /// - // TODO should this be public? + // TODO should this be public? probably shouldn't be storing the byte array for makernotes, unless it's unknown (in which case it's in a makernote directory) public static readonly TiffByteArrayTag Makernote = new TiffByteArrayTag(0x927C, "Makernote"); - public static readonly TiffStringTag UserComment = new TiffStringTag(0x9286, "User Comment"); + public static readonly TiffStringTag UserComment = new TiffStringTag(0x9286, "User Comment", DescribeUserComment); public static readonly TiffUInt16Tag SubsecondTime = new TiffUInt16Tag(0x9290, "Subsecond Time"); @@ -652,7 +656,7 @@ public static class ExifTags /// The digital zoom ratio, or zero if digital zoom was not used. public static readonly TiffURationalTag DigitalZoomRatio = new TiffURationalTag(0xA404, "Digital Zoom Ratio", - (ratio, format) => ratio.Numerator == 0 ? "Digital zoom not used" : ratio.ToSimpleString(format)); + (ratio, format) => ratio.Numerator == 0 ? "Digital zoom not used" : ratio.ToSimpleString(provider: format)); /// /// The equivalent focal length assuming a 35mm film camera, in millimetres. @@ -763,7 +767,7 @@ public static class ExifTags /// public static readonly TiffURationalArrayTag LensSpecification = new TiffURationalArrayTag(0xA432, "Lens Specification", 4, (values, format) => { - if (values.Length != 4 || (values[0] == 0 && values[2] == 0)) + if (values.Length != 4 || (values[0].IsZero && values[2].IsZero)) return null; var sb = new StringBuilder(); @@ -771,10 +775,10 @@ public static class ExifTags sb.AppendFormat( format, values[0] == values[1] ? "{0}mm" : "{0}-{1}mm", - values[0].ToSimpleString(format), - values[1].ToSimpleString(format)); + values[0].ToSimpleString(provider: format), + values[1].ToSimpleString(provider: format)); - if (values[2] != 0) + if (!values[2].IsZero) { sb.Append(' '); sb.AppendFormat( @@ -810,11 +814,11 @@ public static class ExifTags // TODO what type is this? public static readonly TiffUInt16Tag Lens = new TiffUInt16Tag(0xFDEA, "Lens"); - private static string DescribeFStop(double fStop, IFormatProvider p) => string.Format(p, "f/{0:0.0}", fStop); + private static string DescribeFStop(double fStop, IFormatProvider format) => string.Format(format, "f/{0:0.0}", fStop); - private static string DescribeFocalLength(double mm, IFormatProvider p) => string.Format(p, "{0:0.#} mm", mm); + private static string DescribeFocalLength(double mm, IFormatProvider format) => string.Format(format, "{0:0.#} mm", mm); - private static string DescribePixels(int i, IFormatProvider p) => string.Format(p, "{0} pixel{1}", i, i == 1 ? "" : "s"); + private static string DescribePixels(int i, IFormatProvider format) => string.Format(format, "{0} pixel{1}", i, i == 1 ? "" : "s"); private static string DescribeVersion(IReadOnlyList components, int majorDigits) { @@ -835,5 +839,57 @@ private static string DescribeVersion(IReadOnlyList components, int majorDi } return version.ToString(); } + + private static string DescribeUserComment(byte[] bytes, IFormatProvider provider) + { + if (bytes.Length == 0) + return string.Empty; + + // TODO use ByteTrie here + // Someone suggested "ISO-8859-1". + var encodingMap = new Dictionary + { + ["ASCII"] = Encoding.ASCII, + ["UTF8"] = Encoding.UTF8, + ["UTF7"] = Encoding.UTF7, + ["UTF32"] = Encoding.UTF32, + ["UNICODE"] = Encoding.Unicode, + ["JIS"] = Encoding.GetEncoding("Shift-JIS") + }; + + try + { + if (bytes.Length >= 10) + { + // TODO no guarantee bytes after the UTF8 name are valid UTF8 -- only read as many as needed + var firstTenBytesString = Encoding.UTF8.GetString(bytes, 0, 10); + // try each encoding name + foreach (var pair in encodingMap) + { + var encodingName = pair.Key; + var encoding = pair.Value; + if (firstTenBytesString.StartsWith(encodingName)) + { + // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start + for (var j = encodingName.Length; j < 10; j++) + { + var b = bytes[j]; + if (b != '\0' && b != ' ') + { + return encoding.GetString(bytes, j, bytes.Length - j).Trim('\0', ' '); + } + } + return encoding.GetString(bytes, 10, bytes.Length - 10).Trim('\0', ' '); + } + } + } + // special handling fell through, return a plain string representation + return Encoding.UTF8.GetString(bytes, 0, bytes.Length).Trim('\0', ' '); + } + catch + { + return null; + } + } } } \ No newline at end of file diff --git a/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs b/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs index e9343fb58..a83dff752 100644 --- a/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs +++ b/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs @@ -1,44 +1,17 @@ -#region License -// -// Copyright 2002-2017 Drew Noakes -// Ported from Java to C# by Yakov Danilov for Imazen LLC in 2014 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// More information about this project is available at: -// -// https://github.com/drewnoakes/metadata-extractor-dotnet -// https://drewnoakes.com/code/exif/ -// -#endregion +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; -using JetBrains.Annotations; using MetadataExtractor.IO; -// ReSharper disable MemberCanBePrivate.Global - namespace MetadataExtractor.Formats.Exif { /// Base class for several Exif format descriptor classes. - /// Drew Noakes https://drewnoakes.com [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class ExifDescriptorBase : TagDescriptor where T : Directory { - protected ExifDescriptorBase([NotNull] T directory) + protected ExifDescriptorBase(T directory) : base(directory) { } @@ -49,191 +22,24 @@ public override string GetDescription(int tagType) switch (tagType) { - case ExifDirectoryBase.TagInteropIndex: - return GetInteropIndexDescription(); - case ExifDirectoryBase.TagInteropVersion: - return GetInteropVersionDescription(); - case ExifDirectoryBase.TagOrientation: - return GetOrientationDescription(); - case ExifDirectoryBase.TagResolutionUnit: - return GetResolutionDescription(); - case ExifDirectoryBase.TagYCbCrPositioning: - return GetYCbCrPositioningDescription(); case ExifDirectoryBase.TagXResolution: return GetXResolutionDescription(); case ExifDirectoryBase.TagYResolution: return GetYResolutionDescription(); - case ExifDirectoryBase.TagImageWidth: - return GetImageWidthDescription(); - case ExifDirectoryBase.TagImageHeight: - return GetImageHeightDescription(); - case ExifDirectoryBase.TagBitsPerSample: - return GetBitsPerSampleDescription(); - case ExifDirectoryBase.TagPhotometricInterpretation: - return GetPhotometricInterpretationDescription(); - case ExifDirectoryBase.TagRowsPerStrip: - return GetRowsPerStripDescription(); - case ExifDirectoryBase.TagStripByteCounts: - return GetStripByteCountsDescription(); - case ExifDirectoryBase.TagSamplesPerPixel: - return GetSamplesPerPixelDescription(); - case ExifDirectoryBase.TagPlanarConfiguration: - return GetPlanarConfigurationDescription(); - case ExifDirectoryBase.TagYCbCrSubsampling: - return GetYCbCrSubsamplingDescription(); - case ExifDirectoryBase.TagReferenceBlackWhite: - return GetReferenceBlackWhiteDescription(); - case ExifDirectoryBase.TagWinAuthor: - return GetWindowsAuthorDescription(); - case ExifDirectoryBase.TagWinComment: - return GetWindowsCommentDescription(); - case ExifDirectoryBase.TagWinKeywords: - return GetWindowsKeywordsDescription(); - case ExifDirectoryBase.TagWinSubject: - return GetWindowsSubjectDescription(); - case ExifDirectoryBase.TagWinTitle: - return GetWindowsTitleDescription(); - case ExifDirectoryBase.TagNewSubfileType: - return GetNewSubfileTypeDescription(); - case ExifDirectoryBase.TagSubfileType: - return GetSubfileTypeDescription(); - case ExifDirectoryBase.TagThresholding: - return GetThresholdingDescription(); - case ExifDirectoryBase.TagFillOrder: - return GetFillOrderDescription(); case ExifDirectoryBase.TagCfaPattern2: return GetCfaPattern2Description(); - case ExifDirectoryBase.TagExposureTime: - return GetExposureTimeDescription(); - case ExifDirectoryBase.TagShutterSpeed: - return GetShutterSpeedDescription(); - case ExifDirectoryBase.TagFNumber: - return GetFNumberDescription(); - case ExifDirectoryBase.TagCompressedAverageBitsPerPixel: - return GetCompressedAverageBitsPerPixelDescription(); - case ExifDirectoryBase.TagSubjectDistance: - return GetSubjectDistanceDescription(); - case ExifDirectoryBase.TagMeteringMode: - return GetMeteringModeDescription(); - case ExifDirectoryBase.TagWhiteBalance: - return GetWhiteBalanceDescription(); - case ExifDirectoryBase.TagFlash: - return GetFlashDescription(); - case ExifDirectoryBase.TagFocalLength: - return GetFocalLengthDescription(); - case ExifDirectoryBase.TagColorSpace: - return GetColorSpaceDescription(); - case ExifDirectoryBase.TagExifImageWidth: - return GetExifImageWidthDescription(); - case ExifDirectoryBase.TagExifImageHeight: - return GetExifImageHeightDescription(); - case ExifDirectoryBase.TagFocalPlaneResolutionUnit: - return GetFocalPlaneResolutionUnitDescription(); case ExifDirectoryBase.TagFocalPlaneXResolution: return GetFocalPlaneXResolutionDescription(); case ExifDirectoryBase.TagFocalPlaneYResolution: return GetFocalPlaneYResolutionDescription(); - case ExifDirectoryBase.TagExposureProgram: - return GetExposureProgramDescription(); - case ExifDirectoryBase.TagAperture: - return GetApertureValueDescription(); - case ExifDirectoryBase.TagMaxAperture: - return GetMaxApertureValueDescription(); - case ExifDirectoryBase.TagSensingMethod: - return GetSensingMethodDescription(); - case ExifDirectoryBase.TagExposureBias: - return GetExposureBiasDescription(); - case ExifDirectoryBase.TagFileSource: - return GetFileSourceDescription(); - case ExifDirectoryBase.TagSceneType: - return GetSceneTypeDescription(); case ExifDirectoryBase.TagCfaPattern: return GetCfaPatternDescription(); - case ExifDirectoryBase.TagComponentsConfiguration: - return GetComponentConfigurationDescription(); - case ExifDirectoryBase.TagExifVersion: - return GetExifVersionDescription(); - case ExifDirectoryBase.TagFlashpixVersion: - return GetFlashPixVersionDescription(); - case ExifDirectoryBase.TagIsoEquivalent: - return GetIsoEquivalentDescription(); - case ExifDirectoryBase.TagUserComment: - return GetUserCommentDescription(); - case ExifDirectoryBase.TagCustomRendered: - return GetCustomRenderedDescription(); - case ExifDirectoryBase.TagExposureMode: - return GetExposureModeDescription(); - case ExifDirectoryBase.TagWhiteBalanceMode: - return GetWhiteBalanceModeDescription(); - case ExifDirectoryBase.TagDigitalZoomRatio: - return GetDigitalZoomRatioDescription(); - case ExifDirectoryBase.Tag35MMFilmEquivFocalLength: - return Get35MMFilmEquivFocalLengthDescription(); - case ExifDirectoryBase.TagSceneCaptureType: - return GetSceneCaptureTypeDescription(); - case ExifDirectoryBase.TagGainControl: - return GetGainControlDescription(); - case ExifDirectoryBase.TagContrast: - return GetContrastDescription(); - case ExifDirectoryBase.TagSaturation: - return GetSaturationDescription(); - case ExifDirectoryBase.TagSharpness: - return GetSharpnessDescription(); - case ExifDirectoryBase.TagSubjectDistanceRange: - return GetSubjectDistanceRangeDescription(); - case ExifDirectoryBase.TagSensitivityType: - return GetSensitivityTypeDescription(); - case ExifDirectoryBase.TagCompression: - return GetCompressionDescription(); - case ExifDirectoryBase.TagJpegProc: - return GetJpegProcDescription(); - case ExifDirectoryBase.TagLensSpecification: - return GetLensSpecificationDescription(); default: return base.GetDescription(tagType); } } - -/* - [CanBeNull] - public string GetInteropVersionDescription() - { - return GetVersionBytesDescription(ExifDirectoryBase.TagInteropVersion, 2); - } -*/ - -/* - [CanBeNull] - public string GetInteropIndexDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagInteropIndex); - if (value == null) - return null; - return string.Equals("R98", value.Trim(), StringComparison.OrdinalIgnoreCase) - ? "Recommended Exif Interoperability Rules (ExifR98)" - : "Unknown (" + value + ")"; - } -*/ - -/* - [CanBeNull] - public string GetReferenceBlackWhiteDescription() - { - var ints = Directory.GetInt32Array(ExifDirectoryBase.TagReferenceBlackWhite); - if (ints == null || ints.Length < 6) - return null; - var blackR = ints[0]; - var whiteR = ints[1]; - var blackG = ints[2]; - var whiteG = ints[3]; - var blackB = ints[4]; - var whiteB = ints[5]; - return $"[{blackR},{blackG},{blackB}] [{whiteR},{whiteG},{whiteB}]"; - } -*/ - - [CanBeNull] - public string GetYResolutionDescription() + + public string? GetYResolutionDescription() { var resolution = GetRationalOrDoubleString(ExifDirectoryBase.TagYResolution); if (resolution == null) @@ -242,8 +48,7 @@ public string GetYResolutionDescription() return $"{resolution} dots per {unit?.ToLower() ?? "unit"}"; } - [CanBeNull] - public string GetXResolutionDescription() + public string? GetXResolutionDescription() { var resolution = GetRationalOrDoubleString(ExifDirectoryBase.TagXResolution); if (resolution == null) @@ -252,484 +57,6 @@ public string GetXResolutionDescription() return $"{resolution} dots per {unit?.ToLower() ?? "unit"}"; } -/* - [CanBeNull] - public string GetYCbCrPositioningDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagYCbCrPositioning, 1, - "Center of pixel array", - "Datum point"); - } -*/ - -/* - [CanBeNull] - public string GetOrientationDescription() - { - return base.GetOrientationDescription(ExifDirectoryBase.TagOrientation); - } - - [CanBeNull] - public string GetResolutionDescription() - { - // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) - return GetIndexedDescription(ExifDirectoryBase.TagResolutionUnit, 1, - "(No unit)", - "Inch", - "cm"); - } -*/ -/* - /// The Windows specific tags uses plain Unicode. - [CanBeNull] - private string GetUnicodeDescription(int tag) - { - var bytes = Directory.GetByteArray(tag); - if (bytes == null) - return null; - try - { - // Decode the Unicode string and trim the Unicode zero "\0" from the end. - return Encoding.Unicode.GetString(bytes, 0, bytes.Length).TrimEnd('\0'); - } - catch - { - return null; - } - } - - [CanBeNull] - public string GetWindowsAuthorDescription() - { - return GetUnicodeDescription(ExifDirectoryBase.TagWinAuthor); - } - - [CanBeNull] - public string GetWindowsCommentDescription() - { - return GetUnicodeDescription(ExifDirectoryBase.TagWinComment); - } - - [CanBeNull] - public string GetWindowsKeywordsDescription() - { - return GetUnicodeDescription(ExifDirectoryBase.TagWinKeywords); - } - - [CanBeNull] - public string GetWindowsTitleDescription() - { - return GetUnicodeDescription(ExifDirectoryBase.TagWinTitle); - } - - [CanBeNull] - public string GetWindowsSubjectDescription() - { - return GetUnicodeDescription(ExifDirectoryBase.TagWinSubject); - } -*/ - -/* - [CanBeNull] - public string GetYCbCrSubsamplingDescription() - { - var positions = Directory.GetInt32Array(ExifDirectoryBase.TagYCbCrSubsampling); - if (positions == null || positions.Length < 2) - return null; - if (positions[0] == 2 && positions[1] == 1) - return "YCbCr4:2:2"; - if (positions[0] == 2 && positions[1] == 2) - return "YCbCr4:2:0"; - return "(Unknown)"; - } -*/ - -/* - [CanBeNull] - public string GetPlanarConfigurationDescription() - { - // When image format is no compression YCbCr, this value shows byte aligns of YCbCr data. - // If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling pixel. - // If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr plane format. - return GetIndexedDescription(ExifDirectoryBase.TagPlanarConfiguration, 1, - "Chunky (contiguous for each subsampling pixel)", - "Separate (Y-plane/Cb-plane/Cr-plane format)"); - } - - [CanBeNull] - public string GetSamplesPerPixelDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagSamplesPerPixel); - return value == null ? null : value + " samples/pixel"; - } - - [CanBeNull] - public string GetRowsPerStripDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagRowsPerStrip); - return value == null ? null : value + " rows/strip"; - } - - [CanBeNull] - public string GetStripByteCountsDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagStripByteCounts); - return value == null ? null : value + " bytes"; - } -*/ - -/* - [CanBeNull] - public string GetPhotometricInterpretationDescription() - { - // Shows the color space of the image data components - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagPhotometricInterpretation, out value)) - return null; - - switch (value) - { - case 0: - return "WhiteIsZero"; - case 1: - return "BlackIsZero"; - case 2: - return "RGB"; - case 3: - return "RGB Palette"; - case 4: - return "Transparency Mask"; - case 5: - return "CMYK"; - case 6: - return "YCbCr"; - case 8: - return "CIELab"; - case 9: - return "ICCLab"; - case 10: - return "ITULab"; - case 32803: - return "Color Filter Array"; - case 32844: - return "Pixar LogL"; - case 32845: - return "Pixar LogLuv"; - case 32892: - return "Linear Raw"; - default: - return "Unknown colour space"; - } - } -*/ - -/* - [CanBeNull] - public string GetBitsPerSampleDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagBitsPerSample); - return value == null ? null : value + " bits/component/pixel"; - } -*/ - -/* - [CanBeNull] - public string GetImageWidthDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagImageWidth); - return value == null ? null : value + " pixels"; - } -*/ - -/* - [CanBeNull] - public string GetImageHeightDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagImageHeight); - return value == null ? null : value + " pixels"; - } -*/ - -/* - [CanBeNull] - public string GetNewSubfileTypeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagNewSubfileType, 0, - "Full-resolution image", - "Reduced-resolution image", - "Single page of multi-page image", - "Single page of multi-page reduced-resolution image", - "Transparency mask", - "Transparency mask of reduced-resolution image", - "Transparency mask of multi-page image", - "Transparency mask of reduced-resolution multi-page image"); - } - - [CanBeNull] - public string GetSubfileTypeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSubfileType, 1, - "Full-resolution image", - "Reduced-resolution image", - "Single page of multi-page image"); - } -*/ - -/* - [CanBeNull] - public string GetThresholdingDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagThresholding, 1, - "No dithering or halftoning", - "Ordered dither or halftone", - "Randomized dither"); - } -*/ - -/* - [CanBeNull] - public string GetFillOrderDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagFillOrder, 1, - "Normal", - "Reversed"); - } -*/ - -/* - [CanBeNull] - public string GetSubjectDistanceRangeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSubjectDistanceRange, - "Unknown", - "Macro", - "Close view", - "Distant view"); - } -*/ - -/* - [CanBeNull] - public string GetSensitivityTypeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSensitivityType, - "Unknown", - "Standard Output Sensitivity", - "Recommended Exposure Index", - "ISO Speed", - "Standard Output Sensitivity and Recommended Exposure Index", - "Standard Output Sensitivity and ISO Speed", - "Recommended Exposure Index and ISO Speed", - "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed"); - } -*/ - -/* - [CanBeNull] - public string GetLensSpecificationDescription() - { - return GetLensSpecificationDescription(ExifDirectoryBase.TagLensSpecification); - } - - [CanBeNull] - public string GetSharpnessDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSharpness, - "None", - "Low", - "Hard"); - } - - [CanBeNull] - public string GetSaturationDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSaturation, - "None", - "Low saturation", - "High saturation"); - } - - [CanBeNull] - public string GetContrastDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagContrast, - "None", - "Soft", - "Hard"); - } - - [CanBeNull] - public string GetGainControlDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagGainControl, - "None", - "Low gain up", - "Low gain down", - "High gain up", - "High gain down"); - } - - [CanBeNull] - public string GetSceneCaptureTypeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSceneCaptureType, - "Standard", - "Landscape", - "Portrait", - "Night scene"); - } - - [CanBeNull] - public string Get35MMFilmEquivFocalLengthDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.Tag35MMFilmEquivFocalLength, out value)) - return null; - return value == 0 ? "Unknown" : GetFocalLengthDescription(value); - } -*/ - -/* - [CanBeNull] - public string GetDigitalZoomRatioDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagDigitalZoomRatio, out value)) - return null; - return value.Numerator == 0 - ? "Digital zoom not used" - : value.ToDouble().ToString("0.#"); - } -*/ - -/* - [CanBeNull] - public string GetWhiteBalanceModeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagWhiteBalanceMode, - "Auto white balance", - "Manual white balance"); - } -*/ - -/* - [CanBeNull] - public string GetExposureModeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagExposureMode, - "Auto exposure", - "Manual exposure", - "Auto bracket"); - } -*/ - -/* - [CanBeNull] - public string GetCustomRenderedDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagCustomRendered, - "Normal process", - "Custom process"); - } -*/ - - [CanBeNull] - public string GetUserCommentDescription() - { - var commentBytes = Directory.GetByteArray(ExifDirectoryBase.TagUserComment); - - if (commentBytes == null) - return null; - - if (commentBytes.Length == 0) - return string.Empty; - - // TODO use ByteTrie here - // Someone suggested "ISO-8859-1". - var encodingMap = new Dictionary - { - ["ASCII"] = Encoding.ASCII, - ["UTF8"] = Encoding.UTF8, - ["UTF7"] = Encoding.UTF7, - ["UTF32"] = Encoding.UTF32, - ["UNICODE"] = Encoding.Unicode, - ["JIS"] = Encoding.GetEncoding("Shift-JIS") - }; - - try - { - if (commentBytes.Length >= 10) - { - // TODO no guarantee bytes after the UTF8 name are valid UTF8 -- only read as many as needed - var firstTenBytesString = Encoding.UTF8.GetString(commentBytes, 0, 10); - // try each encoding name - foreach (var pair in encodingMap) - { - var encodingName = pair.Key; - var encoding = pair.Value; - if (firstTenBytesString.StartsWith(encodingName)) - { - // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start - for (var j = encodingName.Length; j < 10; j++) - { - var b = commentBytes[j]; - if (b != '\0' && b != ' ') - { - return encoding.GetString(commentBytes, j, commentBytes.Length - j).Trim('\0', ' '); - } - } - return encoding.GetString(commentBytes, 10, commentBytes.Length - 10).Trim('\0', ' '); - } - } - } - // special handling fell through, return a plain string representation - return Encoding.UTF8.GetString(commentBytes, 0, commentBytes.Length).Trim('\0', ' '); - } - catch - { - return null; - } - } - -/* - [CanBeNull] - public string GetIsoEquivalentDescription() - { - // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values - // There used to be a check here that multiplied ISO values < 50 by 200. - // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40. - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagIsoEquivalent, out value)) - return null; - return value.ToString(); - } -*/ - -/* - [CanBeNull] - public string GetExifVersionDescription() - { - return GetVersionBytesDescription(ExifDirectoryBase.TagExifVersion, 2); - } -*/ - -/* - [CanBeNull] - public string GetFlashPixVersionDescription() - { - return GetVersionBytesDescription(ExifDirectoryBase.TagFlashpixVersion, 2); - } -*/ - -/* - [CanBeNull] - public string GetSceneTypeDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagSceneType, 1, - "Directly photographed image"); - } -*/ - /// /// String description of CFA Pattern /// @@ -741,8 +68,7 @@ public string GetSceneTypeDescription() /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. /// It does not apply to all sensing methods. /// - [CanBeNull] - public string GetCfaPatternDescription() + public string? GetCfaPatternDescription() { return FormatCFAPattern(DecodeCFAPattern(ExifDirectoryBase.TagCfaPattern)); } @@ -757,8 +83,7 @@ public string GetCfaPatternDescription() /// holds only the pixel pattern. is expected to exist and pass /// some conditional tests. /// - [CanBeNull] - public string GetCfaPattern2Description() + public string? GetCfaPattern2Description() { var values = Directory.GetByteArray(ExifDirectoryBase.TagCfaPattern2); if (values == null) @@ -782,8 +107,7 @@ public string GetCfaPattern2Description() return $"Unknown Pattern ({base.GetDescription(ExifDirectoryBase.TagCfaPattern2)})"; } - [CanBeNull] - private static string FormatCFAPattern(int[] pattern) + private static string? FormatCFAPattern(int[] pattern) { if (pattern.Length < 2) return ""; @@ -827,7 +151,7 @@ private static string FormatCFAPattern(int[] pattern) /// - Two short, being the grid width and height of the repeated pattern. /// - Next, for every pixel in that pattern, an identification code. /// - private int[] DecodeCFAPattern(int tagType) + private int[]? DecodeCFAPattern(int tagType) { int[] ret; @@ -880,456 +204,20 @@ private int[] DecodeCFAPattern(int tagType) return ret; } -/* ->>>>>>> Exploration of ideas for a new API. - [CanBeNull] - public string GetFileSourceDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagFileSource, 1, - "Film Scanner", - "Reflection Print Scanner", - "Digital Still Camera (DSC)"); - } -*/ - -/* - [CanBeNull] - public string GetExposureBiasDescription() + public string? GetFocalPlaneXResolutionDescription() { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagExposureBias, out value)) - return null; - return value.ToSimpleString() + " EV"; - } -*/ - -/* - [CanBeNull] - public string GetMaxApertureValueDescription() - { - double aperture; - if (!Directory.TryGetDouble(ExifDirectoryBase.TagMaxAperture, out aperture)) - return null; - return GetFStopDescription(PhotographicConversions.ApertureToFStop(aperture)); - } -*/ - -/* - [CanBeNull] - public string GetApertureValueDescription() - { - double aperture; - if (!Directory.TryGetDouble(ExifDirectoryBase.TagAperture, out aperture)) - return null; - return GetFStopDescription(PhotographicConversions.ApertureToFStop(aperture)); - } -*/ - -/* - [CanBeNull] - public string GetExposureProgramDescription() - { - return GetIndexedDescription(ExifDirectoryBase.TagExposureProgram, 1, - "Manual control", - "Program normal", - "Aperture priority", - "Shutter priority", - "Program creative (slow program)", - "Program action (high-speed program)", - "Portrait mode", - "Landscape mode"); - } -*/ - - [CanBeNull] - public string GetFocalPlaneXResolutionDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagFocalPlaneXResolution, out value)) + if (!Directory.TryGetRational(ExifDirectoryBase.TagFocalPlaneXResolution, out var value)) return null; var unit = GetFocalPlaneResolutionUnitDescription(); return value.Reciprocal.ToSimpleString() + (unit == null ? string.Empty : " " + unit.ToLower()); } - [CanBeNull] - public string GetFocalPlaneYResolutionDescription() + public string? GetFocalPlaneYResolutionDescription() { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagFocalPlaneYResolution, out value)) + if (!Directory.TryGetRational(ExifDirectoryBase.TagFocalPlaneYResolution, out var value)) return null; var unit = GetFocalPlaneResolutionUnitDescription(); return value.Reciprocal.ToSimpleString() + (unit == null ? string.Empty : " " + unit.ToLower()); } - -/* - [CanBeNull] - public string GetFocalPlaneResolutionUnitDescription() - { - // Unit of FocalPlaneXResolution/FocalPlaneYResolution. - // '1' means no-unit, '2' inch, '3' centimeter. - return GetIndexedDescription(ExifDirectoryBase.TagFocalPlaneResolutionUnit, 1, - "(No unit)", - "Inches", - "cm"); - } -*/ - -/* - [CanBeNull] - public string GetExifImageWidthDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagExifImageWidth, out value)) - return null; - return value + " pixels"; - } - - [CanBeNull] - public string GetExifImageHeightDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagExifImageHeight, out value)) - return null; - return value + " pixels"; - } -*/ - -/* - [CanBeNull] - public string GetColorSpaceDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagColorSpace, out value)) - return null; - if (value == 1) - return "sRGB"; - if (value == 65535) - return "Undefined"; - return "Unknown (" + value + ")"; - } -*/ - -/* - [CanBeNull] - public string GetFocalLengthDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagFocalLength, out value)) - return null; - return GetFocalLengthDescription(value.ToDouble()); - } -*/ - -/* - [CanBeNull] - public string GetFlashDescription() - { - /* - * This is a bit mask. - * 0 = flash fired - * 1 = return detected - * 2 = return able to be detected - * 3 = unknown - * 4 = auto used - * 5 = unknown - * 6 = red eye reduction used - #1# - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagFlash, out value)) - return null; - - var sb = new StringBuilder(); - sb.Append((value & 0x1) != 0 ? "Flash fired" : "Flash did not fire"); - // check if we're able to detect a return, before we mention it - if ((value & 0x4) != 0) - sb.Append((value & 0x2) != 0 ? ", return detected" : ", return not detected"); - if ((value & 0x10) != 0) - sb.Append(", auto"); - if ((value & 0x40) != 0) - sb.Append(", red-eye reduction"); - return sb.ToString(); - } -*/ - -/* - [CanBeNull] - public string GetWhiteBalanceDescription() - { - // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35 - - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagWhiteBalance, out value)) - return null; - - switch (value) - { - case 0: return "Unknown"; - case 1: return "Daylight"; - case 2: return "Florescent"; - case 3: return "Tungsten"; - case 4: return "Flash"; - case 9: return "Fine Weather"; - case 10: return "Cloudy"; - case 11: return "Shade"; - case 12: return "Daylight Fluorescent"; - case 13: return "Day White Fluorescent"; - case 14: return "Cool White Fluorescent"; - case 15: return "White Fluorescent"; - case 16: return "Warm White Fluorescent"; - case 17: return "Standard light"; - case 18: return "Standard light (B)"; - case 19: return "Standard light (C)"; - case 20: return "D55"; - case 21: return "D65"; - case 22: return "D75"; - case 23: return "D50"; - case 24: return "Studio Tungsten"; - case 255: return "(Other)"; - default: - return "Unknown (" + value + ")"; - } - } -*/ - -/* - [CanBeNull] - public string GetMeteringModeDescription() - { - // '0' means unknown, '1' average, '2' center weighted average, '3' spot - // '4' multi-spot, '5' multi-segment, '6' partial, '255' other - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagMeteringMode, out value)) - return null; - - switch (value) - { - case 0: - return "Unknown"; - case 1: - return "Average"; - case 2: - return "Center weighted average"; - case 3: - return "Spot"; - case 4: - return "Multi-spot"; - case 5: - return "Multi-segment"; - case 6: - return "Partial"; - case 255: - return "(Other)"; - default: - return "Unknown (" + value + ")"; - } - } -*/ - -/* - [CanBeNull] - public string GetCompressionDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagCompression, out value)) - return null; - - switch (value) - { - case 1: - return "Uncompressed"; - case 2: - return "CCITT 1D"; - case 3: - return "T4/Group 3 Fax"; - case 4: - return "T6/Group 4 Fax"; - case 5: - return "LZW"; - case 6: - return "JPEG (old-style)"; - case 7: - return "JPEG"; - case 8: - return "Adobe Deflate"; - case 9: - return "JBIG B&W"; - case 10: - return "JBIG Color"; - case 99: - return "JPEG"; - case 262: - return "Kodak 262"; - case 32766: - return "Next"; - case 32767: - return "Sony ARW Compressed"; - case 32769: - return "Packed RAW"; - case 32770: - return "Samsung SRW Compressed"; - case 32771: - return "CCIRLEW"; - case 32772: - return "Samsung SRW Compressed 2"; - case 32773: - return "PackBits"; - case 32809: - return "Thunderscan"; - case 32867: - return "Kodak KDC Compressed"; - case 32895: - return "IT8CTPAD"; - case 32896: - return "IT8LW"; - case 32897: - return "IT8MP"; - case 32898: - return "IT8BL"; - case 32908: - return "PixarFilm"; - case 32909: - return "PixarLog"; - case 32946: - return "Deflate"; - case 32947: - return "DCS"; - case 34661: - return "JBIG"; - case 34676: - return "SGILog"; - case 34677: - return "SGILog24"; - case 34712: - return "JPEG 2000"; - case 34713: - return "Nikon NEF Compressed"; - case 34715: - return "JBIG2 TIFF FX"; - case 34718: - return "Microsoft Document Imaging (MDI) Binary Level Codec"; - case 34719: - return "Microsoft Document Imaging (MDI) Progressive Transform Codec"; - case 34720: - return "Microsoft Document Imaging (MDI) Vector"; - case 34892: - return "Lossy JPEG"; - case 65000: - return "Kodak DCR Compressed"; - case 65535: - return "Pentax PEF Compressed"; - default: - return "Unknown (" + value + ")"; - } - } -*/ - -/* - [CanBeNull] - public string GetSubjectDistanceDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagSubjectDistance, out value)) - return null; - return $"{value.ToDouble():0.0##} metres"; - } -*/ - -/* - [CanBeNull] - public string GetCompressedAverageBitsPerPixelDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagCompressedAverageBitsPerPixel, out value)) - return null; - var ratio = value.ToSimpleString(); - return value.IsInteger && value.ToInt32() == 1 ? ratio + " bit/pixel" : ratio + " bits/pixel"; - } -*/ - -/* - [CanBeNull] - public string GetExposureTimeDescription() - { - var value = Directory.GetString(ExifDirectoryBase.TagExposureTime); - return value == null ? null : value + " sec"; - } -*/ - -/* - [CanBeNull] - public string GetShutterSpeedDescription() - { - return GetShutterSpeedDescription(ExifDirectoryBase.TagShutterSpeed); - } -*/ - -/* - [CanBeNull] - public string GetFNumberDescription() - { - Rational value; - if (!Directory.TryGetRational(ExifDirectoryBase.TagFNumber, out value)) - return null; - return GetFStopDescription(value.ToDouble()); - } -*/ - -/* - [CanBeNull] - public string GetSensingMethodDescription() - { - // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor - // '4' Three-chip color area sensor, '5' Color sequential area sensor - // '7' Trilinear sensor '8' Color sequential linear sensor, 'Other' reserved - return GetIndexedDescription(ExifDirectoryBase.TagSensingMethod, 1, - "(Not defined)", - "One-chip color area sensor", - "Two-chip color area sensor", - "Three-chip color area sensor", - "Color sequential area sensor", - null, - "Trilinear sensor", - "Color sequential linear sensor"); - } -*/ - -/* - [CanBeNull] - public string GetComponentConfigurationDescription() - { - var components = Directory.GetInt32Array(ExifDirectoryBase.TagComponentsConfiguration); - if (components == null) - return null; - var componentStrings = new[] { string.Empty, "Y", "Cb", "Cr", "R", "G", "B" }; - var componentConfig = new StringBuilder(); - for (var i = 0; i < Math.Min(4, components.Length); i++) - { - var j = components[i]; - if (j > 0 && j < componentStrings.Length) - componentConfig.Append(componentStrings[j]); - } - return componentConfig.ToString(); - } -*/ - -/* - [CanBeNull] - public string GetJpegProcDescription() - { - int value; - if (!Directory.TryGetInt32(ExifDirectoryBase.TagJpegProc, out value)) - return null; - - switch (value) - { - case 1: - return "Baseline"; - case 14: - return "Lossless"; - default: - return "Unknown (" + value + ")"; - } - } -*/ } } diff --git a/MetadataExtractor/Formats/Xmp/XmpDirectory.cs b/MetadataExtractor/Formats/Xmp/XmpDirectory.cs index 2f3ffef5d..2fd9e64cc 100644 --- a/MetadataExtractor/Formats/Xmp/XmpDirectory.cs +++ b/MetadataExtractor/Formats/Xmp/XmpDirectory.cs @@ -23,7 +23,6 @@ public sealed class XmpDirectory : Directory { public const int TagXmpValueCount = 0xFFFF; - private static readonly Dictionary _tagNameMap = new Dictionary { { TagXmpValueCount, "XMP Value Count" } diff --git a/MetadataExtractor/MetadataExtractor.csproj b/MetadataExtractor/MetadataExtractor.csproj index 010ba732a..3a37a9b42 100644 --- a/MetadataExtractor/MetadataExtractor.csproj +++ b/MetadataExtractor/MetadataExtractor.csproj @@ -8,7 +8,7 @@ MOV and related QuickTime video formats such as MP4, M4V, 3G2, 3GP are supported Camera manufacturer specific support exists for Agfa, Canon, Casio, DJI, Epson, Fujifilm, Kodak, Kyocera, Leica, Minolta, Nikon, Olympus, Panasonic, Pentax, Reconyx, Sanyo, Sigma/Foveon and Sony models. Metadata Extractor - netstandard1.3;netstandard2.0;net35;net45 + netstandard1.3;netstandard2.0;net45 $(NoWarn);1591 true MetadataExtractor diff --git a/MetadataExtractor/NewApi.cs b/MetadataExtractor/NewApi.cs index 959582bc3..8367335dd 100644 --- a/MetadataExtractor/NewApi.cs +++ b/MetadataExtractor/NewApi.cs @@ -1,27 +1,8 @@ -#region License -// -// Copyright 2002-2016 Drew Noakes -// Ported from Java to C# by Yakov Danilov for Imazen LLC in 2014 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// More information about this project is available at: -// -// https://github.com/drewnoakes/metadata-extractor-dotnet -// https://drewnoakes.com/code/exif/ -// -#endregion +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +#nullable enable +// TODO remove this // ReSharper disable CheckNamespace using System; using System.Collections; @@ -30,6 +11,10 @@ using System.Runtime.InteropServices; using System.Text; using MetadataExtractor.Formats.Tiff; +using MetadataExtractor.IO; +using XmpCore; +using XmpCore.Impl; +using XmpCore.Options; namespace MetadataExtractor.NewApi { @@ -40,28 +25,92 @@ public interface IDirectory : IReadOnlyCollection IEnumerable SubDirectories { get; } } - public abstract class Directory : IDirectory where TEntry : IEntry +// /// +// /// Base class for directories whose contents are stored by index. +// /// +// /// +// public abstract class Directory : IDirectory, IEnumerable where TEntry : IEntry +// { +// // TODO need to maintain order of values if we are to write data again +// +// private readonly List _entries = new List(); +// +// public abstract string Name { get; } +// +// public int Count => _entries.Count; +// +// // TODO can we store IDirectory as IEntry too? A directory may have different entry metadata based upon how it's embedded in outer data. something like ILinkedDirectoryEntry? +// public IEnumerable SubDirectories => _entries.Select(entry => entry.Value).OfType(); +// +// public bool TryGetValue(int index, out TEntry entry) +// { +// if (index >= 0 && index < _entries.Count) +// { +// entry = _entries[index]; +// return true; +// } +// +// entry = default; +// return false; +// } +// +// #region IEnumerable +// +// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +// +// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +// +// public IEnumerator GetEnumerator() => _entries.GetEnumerator(); +// +// #endregion +// } + + /// + /// Based class for directories whose contents are stored by key. + /// + /// + /// + public abstract class Directory : IDirectory, IEnumerable where TEntry : IEntry { // TODO need to maintain order of values if we are to write data again - private readonly List _entries = new List(); + private readonly Dictionary _entryByKey; + + protected Directory(IEqualityComparer comparator = null) + { + _entryByKey = new Dictionary(comparator); + } public abstract string Name { get; } - public int Count => _entries.Count; + public int Count => _entryByKey.Count; - public IEnumerable SubDirectories => _entries.Select(entry => entry.Value).OfType(); + // TODO can we store IDirectory as IEntry too? A directory may have different entry metadata based upon how it's embedded in outer data. something like ILinkedDirectoryEntry? + public IEnumerable SubDirectories => _entryByKey.Select(entry => entry.Value.Value).OfType(); -// public bool TryGetValue(TKey key, out TEntry entry) -// { -// return _entryByKey.TryGetValue(key, out entry); -// } + public bool TryGetValue(TKey key, out TEntry entry) + { + return _entryByKey.TryGetValue(key, out entry); + } + + public virtual void Add(TKey key, TEntry entry) + { + _entryByKey.Add(key, entry); + } + + public virtual TEntry this[TKey key] + { + get => _entryByKey[key]; + set => _entryByKey[key] = value; + } #region IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerator GetEnumerator() => _entries.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() => ((IEnumerable)_entryByKey.Values).GetEnumerator(); #endregion } @@ -73,11 +122,6 @@ public interface IEntry string? Description { get; } } -// public static class ExifTags -// { -// public static readonly TiffTag Width = new TiffUInt16Tag(0x1234, "Width", TiffDataFormat.Int32, v => $"{v} pixel{v==0?"":"s"}"); -// } - [StructLayout(LayoutKind.Auto)] public readonly struct TiffValue : IEntry { @@ -126,17 +170,39 @@ private TiffValue(TiffDataFormat format, int componentCount, object value, TiffT public bool TryGetByte(out byte b) { - throw new NotImplementedException(); - } + object value = Value; + + if (value is byte int8) + { + b = int8; + return true; + } + + // TODO coercion could call other TryGet* methods to reduce logic duplication + if (value is sbyte int8u && int8u >= byte.MinValue) + { + b = (byte)int8u; + return true; + } - public bool TryGetInt(out int value) - { + if (value is short int16 && int16 >= byte.MinValue && int16 <= byte.MaxValue) + { + b = (byte) int16; + return true; + } + // TODO further coercions + + b = default; + return false; } - public bool TryGetIntArray(out int[] ints) + public bool TryGetInt32(out int value) { + } + public bool TryGetInt32Array(out int[] ints) + { } public bool TryGetSingle(out float f) @@ -149,28 +215,23 @@ public bool TryGetString(out string s) public bool TryGetRational(out Rational rational) { - } public bool TryGetURational(out URational uRational) { - throw new NotImplementedException(); } public bool TryGetByteArray(out byte[] bytes) { - throw new NotImplementedException(); } public bool TryGetURationalArray(out URational[] uRationals) { - throw new NotImplementedException(); } - public string ToString(IFormatProvider? provider) - { - - } + public override string ToString() => ToString(provider: null); + + public string ToString(IFormatProvider? provider) => Tag.Describe(this, provider); } /// @@ -180,13 +241,24 @@ public abstract class TiffTag { public abstract bool IsKnown { get; } - public int Id { get; } // TODO should this be int? + public abstract string Name { get; } + public int Id { get; } // TODO should this be int? + protected TiffTag(int id) => Id = id; public abstract string? Describe(TiffValue value, IFormatProvider? provider = null); } + public sealed class TiffTagIdComparator : IEqualityComparer + { + public static TiffTagIdComparator Instance { get; } = new TiffTagIdComparator(); + + public bool Equals(TiffTag x, TiffTag y) => x?.Id == y?.Id; + + public int GetHashCode(TiffTag obj) => obj.Id; + } + /// /// Base class for TIFF tags known to exist within some TIFF-compliant format. /// @@ -209,7 +281,7 @@ public abstract class KnownTiffTag : TiffTag /// /// Gets the display name for this tag. /// - public string Name { get; } + public override string Name { get; } protected KnownTiffTag(int id, string name, int expectedCount = 1) : base(id) @@ -246,7 +318,7 @@ public TiffUInt16Tag(int id, string name, Func? : base(id, name) { // TODO store describer delegate in base class if all subclasses end up using it - Describer = (value, format) => value.TryGetInt(out var i) + Describer = (value, format) => value.TryGetInt32(out var i) ? describer?.Invoke(i, format) ?? i.ToString(format) : null; } @@ -265,7 +337,7 @@ public TiffUInt32Tag(int id, string name, Func? d : base(id, name) { // TODO store describer delegate in base class if all subclasses end up using it - Describer = (value, format) => value.TryGetInt(out var i) + Describer = (value, format) => value.TryGetInt32(out var i) ? describer?.Invoke(i, format) ?? i.ToString(format) : null; } @@ -295,11 +367,11 @@ public TiffSingleTag(int id, string name, Func public override TiffDataFormat ExpectedFormat => TiffDataFormat.Single; } - public class RationalTag : KnownTiffTag + public class TiffRationalTag : KnownTiffTag { private Func Describer { get; } - public RationalTag(int id, string name, Func? describer = null) + public TiffRationalTag(int id, string name, Func? describer = null) : base(id, name) { // TODO store describer delegate in base class if all subclasses end up using it @@ -341,16 +413,22 @@ public TiffURationalTag(int id, string name, Func? _describer; + private readonly Func? _describer; public Encoding ExpectedEncoding { get; } - public TiffStringTag(int id, string name, Encoding? expectedEncoding = null, Func? describer = null) + public TiffStringTag(int id, string name, Encoding? expectedEncoding = null) : base(id, name) { - _describer = describer; ExpectedEncoding = expectedEncoding ?? Encoding.UTF8; } + + public TiffStringTag(int id, string name, Func describer) + : base(id, name) + { + _describer = describer; + ExpectedEncoding = Encoding.UTF8; + } public override string? Describe(TiffValue value, IFormatProvider? provider = null) { @@ -359,9 +437,14 @@ public TiffStringTag(int id, string name, Encoding? expectedEncoding = null, Fun try { - // Decode the Unicode string and trim the Unicode zero "\0" from the end. - var s = ExpectedEncoding.GetString(bytes, 0, bytes.Length).TrimEnd('\0'); - return _describer?.Invoke(s, provider) ?? s; + if (_describer == null) + { + // Decode the Unicode string and trim the Unicode zero "\0" from the end. + // TODO remove trailing zeroes before conversion to reduce allocations + return ExpectedEncoding.GetString(bytes, 0, bytes.Length).TrimEnd('\0'); + } + + return _describer(bytes, provider); } catch { @@ -374,44 +457,30 @@ public TiffStringTag(int id, string name, Encoding? expectedEncoding = null, Fun public class TiffIndexedUInt16Tag : TiffUInt16Tag { - public int BaseIndex { get; } - public string[] Descriptions { get; } - public TiffIndexedUInt16Tag(int id, string name, int baseIndex, string[] descriptions) - : base(id, name, DecodeIndex) - { - BaseIndex = baseIndex; - Descriptions = descriptions; - } + : base(id, name, (i, provider) => DecodeIndex(baseIndex, descriptions, i)) + {} - private string? DecodeIndex(int index, IFormatProvider? provider) + private static string DecodeIndex(int baseIndex, string[] descriptions, int index) { - var arrayIndex = index - BaseIndex; - - if (arrayIndex >= 0 && arrayIndex < Descriptions.Length) - { - var description = Descriptions[arrayIndex]; - if (description != null) - return description; - } + int arrayIndex = index - baseIndex; - return null; + if (arrayIndex < 0 || arrayIndex >= descriptions.Length) + return null; + + return descriptions[arrayIndex]; } } public class TiffMappedUInt16Tag : TiffUInt16Tag { - public IReadOnlyDictionary Descriptions { get; } - public TiffMappedUInt16Tag(int id, string name, IReadOnlyDictionary descriptions) - : base(id, name, DecodeIndex) - { - Descriptions = descriptions; - } + : base(id, name, (i, provider) => DecodeIndex(descriptions, i)) + {} - private string? DecodeIndex(int value, IFormatProvider provider) + private static string? DecodeIndex(IReadOnlyDictionary readOnlyDictionary, int value) { - return !Descriptions.TryGetValue(value, out var description) ? null : description; + return !readOnlyDictionary.TryGetValue(value, out var description) ? null : description; } } @@ -423,7 +492,7 @@ public TiffUInt16ArrayTag(int id, string name, int expectedCount, Func value.TryGetIntArray(out var i) + Describer = (value, format) => value.TryGetInt32Array(out var i) // TODO int16[] here? ? describer?.Invoke(i, format) ?? i.ToString() : null; } @@ -457,6 +526,8 @@ public class UnknownTiffTag : TiffTag { public override bool IsKnown => false; + public override string Name => $"Unknown ({Id})"; // TODO Hex display? + public UnknownTiffTag(int id) : base(id) { } @@ -467,30 +538,182 @@ public UnknownTiffTag(int id) : base(id) } } - public abstract class TiffDirectory : Directory + public abstract class TiffDirectory : Directory { - protected TiffDirectory(string name) : base(name) - {} + protected TiffDirectory() : base(TiffTagIdComparator.Instance) + { + } + + public bool TryGetInt32(TiffTag tag, out int value) // TODO should this be an override of the base? + { + if (TryGetValue(tag, out TiffValue tiffValue) && + tiffValue.TryGetInt32(out value)) + { + return true; + } - // TODO store values -- Dictionary + value = default; + return false; + } } - public class ExifIfd0Directory : TiffDirectory + public sealed class ExifIfd0Directory : TiffDirectory { - public ExifIfd0Directory() - : base("Exif IFD0") - {} + public override string Name => "Exif IFD0"; - public int? Width + public int? Width => TryGetInt32(ExifTags.ImageWidth, out int value) ? value : default; + public int? Height => TryGetInt32(ExifTags.ImageHeight, out int value) ? value : default; + } + + public readonly struct XmpName : IEquatable + { + public string Namespace { get; } + public string Name { get; } + + public XmpName(string @namespace, string name) { - get { this.GetInt32 } + Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public override string ToString() => $"{Namespace}.{Name}"; // TODO review this formatting + + public bool Equals(XmpName other) => Namespace == other.Namespace && Name == other.Name; + + public override bool Equals(object obj) => obj is XmpName other && Equals(other); + + public override int GetHashCode() => Namespace == null ? 0 : unchecked((Namespace.GetHashCode() * 397) ^ Name.GetHashCode()); + } + + public sealed class XmpValue : IEntry + { + public XmpName XmpName { get; } + public IXmpProperty XmpProperty { get; } + + public XmpValue(XmpName xmpName, IXmpProperty xmpProperty) + { + XmpName = xmpName; + XmpProperty = xmpProperty; + } + + public object Value => XmpProperty.Value; + + public string Name => XmpName.ToString(); + + public string Description => XmpProperty.Value; + } + + public sealed class XmpDirectory : Directory + { + private static readonly IteratorOptions _iteratorOptions = new IteratorOptions { IsJustLeafNodes = true }; + + public override string Name => "XMP"; + + public XmpDirectory(XmpMeta xmpMeta) + { + try + { + var i = new XmpIterator(xmpMeta, null, null, _iteratorOptions); + + while (i.HasNext()) + { + var prop = (IXmpPropertyInfo)i.Next(); + string @namespace = prop.Namespace; + string path = prop.Path; + string value = prop.Value; + if (@namespace != null && path != null && value != null) + { + var name = new XmpName(@namespace, path); + Add(name, new XmpValue(name, prop)); + } + } + } + catch (XmpException) { } // ignored + } + } + + public enum PcxProperty + { + Version, + } + + public sealed class PcxDirectory : Directory + { + // NOTE this directory must be strict about the values it can receive for properties. + // Version, for example, must be a byte. Other types are invalid and cannot be written. + // This is different to TIFF, for example, where a value has an expected type, but may + // actually be stored and written as something else. + // + // Would be good to have validation for this at the entry level. + // 1) IEntryKey has "ValidateValue" method, or + // 2) IEntryValue validates its value at construction time (using info from assigned key) + + // Alternative design: This directory keeps a fixed-length byte array in memory, and get/set + // operations work directly on that byte array. Maybe all read/write operations provide the + // directory instance. This would also help with Exif tags that need to read multiple values + // as part of their description, but will make the API uglier I think. + } + + public sealed class PcxReader + { + public PcxDirectory Extract(SequentialReader reader) + { + reader = reader.WithByteOrder(isMotorolaByteOrder: false); + + var directory = new PcxDirectory(); + + try + { + var identifier = reader.GetSByte(); + + if (identifier != 0x0A) + throw new ImageProcessingException("Invalid PCX identifier byte"); + + directory.Set(PcxDirectory.TagVersion, reader.GetSByte()); + + var encoding = reader.GetSByte(); + if (encoding != 0x01) + throw new ImageProcessingException("Invalid PCX encoding byte"); + + directory.Set(PcxDirectory.TagBitsPerPixel, reader.GetByte()); + directory.Set(PcxDirectory.TagXMin, reader.GetUInt16()); + directory.Set(PcxDirectory.TagYMin, reader.GetUInt16()); + directory.Set(PcxDirectory.TagXMax, reader.GetUInt16()); + directory.Set(PcxDirectory.TagYMax, reader.GetUInt16()); + directory.Set(PcxDirectory.TagHorizontalDpi, reader.GetUInt16()); + directory.Set(PcxDirectory.TagVerticalDpi, reader.GetUInt16()); + directory.Set(PcxDirectory.TagPalette, reader.GetBytes(48)); + reader.Skip(1); + directory.Set(PcxDirectory.TagColorPlanes, reader.GetByte()); + directory.Set(PcxDirectory.TagBytesPerLine, reader.GetUInt16()); + + var paletteType = reader.GetUInt16(); + if (paletteType != 0) + directory.Set(PcxDirectory.TagPaletteType, paletteType); + + var hScrSize = reader.GetUInt16(); + if (hScrSize != 0) + directory.Set(PcxDirectory.TagHScrSize, hScrSize); + + var vScrSize = reader.GetUInt16(); + if (vScrSize != 0) + directory.Set(PcxDirectory.TagVScrSize, vScrSize); + } + catch (Exception ex) + { + directory.AddError("Exception reading PCX file metadata: " + ex.Message); + } + + return directory; } } public static class MetadataReader { public static IReadOnlyList Read(string path) - {} + { + // TODO + } } internal static class Program @@ -505,7 +728,27 @@ private static void Main(string[] args) var ifd0 = directories.OfType().SingleOrDefault(); - ifd0.Width + if (ifd0 != null) + { + Console.Out.WriteLine($"Image dimensions: {ifd0.Width} x {ifd0.Height}"); + + foreach (TiffValue tiffValue in ifd0) + { + var tag = tiffValue.Tag; + Console.Out.WriteLine($"Tag {tag.Name} ({tag.Id}) {tiffValue.Format} {tiffValue.ComponentCount} component(s) = {tiffValue.Value}"); + } + + // TODO printing errors + // TODO recurring through sub-directories, vs flat-structure (IsTopLevel flag on IDirectory?) + } + + // TODO some directories are flexible (eg. TIFF _can_ store unexpected types of values for tags) and some aren't (eg. PCX version _must_ be a byte) which becomes important if we want to write metadata + + // TODO sketch out an index-based directory type (eg. fixed offsets and all fields present) + // TODO sketch out an enum-based directory type (simple key) + + // TODO sketch out multi-value getter, for tags whose values come from more than one field + // - TiffTags.XResolution/YResolution + ResolutionUnits } } } diff --git a/MetadataExtractor/Rational.cs b/MetadataExtractor/Rational.cs index 21e1b657d..03862e42c 100644 --- a/MetadataExtractor/Rational.cs +++ b/MetadataExtractor/Rational.cs @@ -10,12 +10,8 @@ namespace MetadataExtractor { - public readonly struct URational : IConvertible, IEquatable - { } - - /// Immutable type for representing a rational number. + /// Immutable type for representing a rational number with components. /// - /// Underlying values are stored as a numerator and denominator, each of type . /// Note that any with a numerator of zero will be treated as zero, even if the denominator is also zero. /// /// Drew Noakes https://drewnoakes.com diff --git a/MetadataExtractor/URational.cs b/MetadataExtractor/URational.cs new file mode 100644 index 000000000..071fc7494 --- /dev/null +++ b/MetadataExtractor/URational.cs @@ -0,0 +1,348 @@ +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +#if !NETSTANDARD1_3 +using System.Globalization; +using System.ComponentModel; +#endif + +// TODO operator overloads + +namespace MetadataExtractor +{ + /// Immutable type for representing a rational number with components. + /// + /// Note that any with a numerator of zero will be treated as zero, even if the denominator is also zero. + /// + /// Drew Noakes https://drewnoakes.com +#if !NETSTANDARD1_3 + [Serializable] + [TypeConverter(typeof(URationalConverter))] +#endif + public readonly struct URational : IConvertible, IEquatable + { + /// Gets the denominator. + public ulong Denominator { get; } + + /// Gets the numerator. + public ulong Numerator { get; } + + /// Initialises a new instance with the and . + public URational(ulong numerator, ulong denominator) + { + Numerator = numerator; + Denominator = denominator; + } + + #region Conversion methods + + /// Returns the value of the specified number as a . + /// This may involve rounding. + public double ToDouble() => Numerator == 0 ? 0.0 : Numerator/(double)Denominator; + + /// Returns the value of the specified number as a . + /// May incur rounding. + public float ToSingle() => Numerator == 0 ? 0.0f : Numerator/(float)Denominator; + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public byte ToByte() => (byte)ToDouble(); + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public sbyte ToSByte() => (sbyte)ToDouble(); + + /// Returns the value of the specified number as an . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public int ToInt32() => (int)ToDouble(); + + /// Returns the value of the specified number as an . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public uint ToUInt32() => (uint)ToDouble(); + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public long ToInt64() => (long)ToDouble(); + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public ulong ToUInt64() => (ulong)ToDouble(); + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public short ToInt16() => (short)ToDouble(); + + /// Returns the value of the specified number as a . + /// + /// May incur rounding or truncation. This implementation simply + /// casts the result of to . + /// + public ushort ToUInt16() => (ushort)ToDouble(); + + /// Returns the value of the specified number as a . + /// May incur truncation. + public decimal ToDecimal() => Denominator == 0 ? 0M : Numerator / (decimal)Denominator; + + /// Returns true if the value is non-zero, otherwise false. + public bool ToBoolean() => Numerator != 0 && Denominator != 0; + + #region IConvertible + + TypeCode IConvertible.GetTypeCode() => TypeCode.Object; + + bool IConvertible.ToBoolean(IFormatProvider provider) => ToBoolean(); + + char IConvertible.ToChar(IFormatProvider provider) + { + throw new NotSupportedException(); + } + + sbyte IConvertible.ToSByte(IFormatProvider provider) => ToSByte(); + + byte IConvertible.ToByte(IFormatProvider provider) => ToByte(); + + short IConvertible.ToInt16(IFormatProvider provider) => ToInt16(); + + ushort IConvertible.ToUInt16(IFormatProvider provider) => ToUInt16(); + + int IConvertible.ToInt32(IFormatProvider provider) => ToInt32(); + + uint IConvertible.ToUInt32(IFormatProvider provider) => ToUInt32(); + + long IConvertible.ToInt64(IFormatProvider provider) => ToInt64(); + + ulong IConvertible.ToUInt64(IFormatProvider provider) => ToUInt64(); + + float IConvertible.ToSingle(IFormatProvider provider) => ToSingle(); + + double IConvertible.ToDouble(IFormatProvider provider) => ToDouble(); + + decimal IConvertible.ToDecimal(IFormatProvider provider) => ToDecimal(); + + DateTime IConvertible.ToDateTime(IFormatProvider provider) + { + throw new NotSupportedException(); + } + + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + { + throw new NotSupportedException(); + } + + #endregion + + #endregion + + /// Gets the reciprocal value of this object as a new . + /// the reciprocal in a new object + public URational Reciprocal => new URational(Denominator, Numerator); + + /// + /// Checks if this number is expressible as an integer, either positive or negative. + /// + public bool IsInteger => Denominator == 1 || (Denominator != 0 && Numerator%Denominator == 0) || (Denominator == 0 && Numerator == 0); + + /// + /// True if either or are zero. + /// + public bool IsZero => Denominator == 0 || Numerator == 0; + + #region Formatting + + /// Returns a string representation of the object of form numerator/denominator. + /// a string representation of the object. + public override string ToString() => Numerator + "/" + Denominator; + + public string ToString(IFormatProvider? provider) => Numerator.ToString(provider) + "/" + Denominator.ToString(provider); + + /// + /// Returns the simplest representation of this 's value possible. + /// + public string ToSimpleString(bool allowDecimal = true, IFormatProvider? provider = null) + { + if (Denominator == 0 && Numerator != 0) + return ToString(provider); + + if (IsInteger) + return ToInt32().ToString(provider); + + if (Numerator != 1 && Denominator%Numerator == 0) + { + // common factor between denominator and numerator + var newDenominator = Denominator/Numerator; + return new URational(1, newDenominator).ToSimpleString(allowDecimal, provider); + } + + var simplifiedInstance = GetSimplifiedInstance(); + if (allowDecimal) + { + var doubleString = simplifiedInstance.ToDouble().ToString(provider); + if (doubleString.Length < 5) + return doubleString; + } + + return simplifiedInstance.ToString(provider); + } + + #endregion + + #region Equality and hashing + + /// + /// Indicates whether this instance and are numerically equal, + /// even if their representations differ. + /// + /// + /// For example, 1/2 is equal to 10/20 by this method. + /// Similarly, 1/0 is equal to 100/0 by this method. + /// To test equal representations, use . + /// + /// + /// + public bool Equals(URational other) => other.ToDecimal().Equals(ToDecimal()); + + /// + /// Indicates whether this instance and have identical + /// and . + /// + /// + /// For example, 1/2 is not equal to 10/20 by this method. + /// Similarly, 1/0 is not equal to 100/0 by this method. + /// To test numerically equivalence, use . + /// + /// + /// + public bool EqualsExact(URational other) => Denominator == other.Denominator && Numerator == other.Numerator; + + public override bool Equals(object obj) + { + if (obj is null) + return false; + return obj is URational rational && Equals(rational); + } + + public override int GetHashCode() => unchecked(Denominator.GetHashCode()*397) ^ Numerator.GetHashCode(); + + #endregion + + /// + /// Simplifies the representation of this number. + /// + /// + /// For example, 5/10 simplifies to 1/2 because both + /// and share a common factor of 5. + /// + /// Uses the Euclidean Algorithm to find the greatest common divisor. + /// + /// + /// A simplified instance if one exists, otherwise a copy of the original value. + /// + public URational GetSimplifiedInstance() + { + static ulong GCD(ulong a, ulong b) + { + while (a != 0 && b != 0) + { + if (a > b) + a %= b; + else + b %= a; + } + + return a == 0 ? b : a; + } + + var gcd = GCD(Numerator, Denominator); + + return new URational(Numerator / gcd, Denominator / gcd); + } + + #region Equality operators + + public static bool operator==(URational a, URational b) + { + return Equals(a, b); + } + + public static bool operator!=(URational a, URational b) + { + return !Equals(a, b); + } + + #endregion + + #region URationalConverter + +#if !NETSTANDARD1_3 + private sealed class URationalConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string) || + sourceType == typeof(URational) || + typeof(IConvertible).IsAssignableFrom(sourceType) || + (sourceType.IsArray && typeof(IConvertible).IsAssignableFrom(sourceType.GetElementType()))) + return true; + + return base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value == null) + return base.ConvertFrom(context, culture, null); + + var type = value.GetType(); + + if (type == typeof(string)) + { + var v = ((string)value).Split('/'); + if (v.Length == 2 && ulong.TryParse(v[0], out ulong numerator) && ulong.TryParse(v[1], out ulong denominator)) + return new URational(numerator, denominator); + } + + if (type == typeof(URational)) + return value; + + if (type.IsArray) + { + var array = (Array)value; + if (array.Rank == 1 && (array.Length == 1 || array.Length == 2)) + { + return new URational( + numerator: Convert.ToUInt64(array.GetValue(0)), + denominator: array.Length == 2 ? Convert.ToUInt64(array.GetValue(1)) : 1); + } + } + + return new URational(Convert.ToUInt64(value), 1); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => false; + } +#endif + + #endregion + } +} \ No newline at end of file