diff --git a/CameraUtility.Tests/CameraFiles/ImageFileTests.cs b/CameraUtility.Tests/CameraFiles/ImageFileTests.cs new file mode 100644 index 0000000..79227ac --- /dev/null +++ b/CameraUtility.Tests/CameraFiles/ImageFileTests.cs @@ -0,0 +1,54 @@ +using System; +using CameraUtility.CameraFiles; +using CameraUtility.Exif; +using FluentAssertions; +using Xunit; + +namespace CameraUtility.Tests.CameraFiles +{ + public sealed class ImageFileTests + { + [Theory] + [InlineData("42", 420)] + [InlineData("042", 42)] + [InlineData("042000", 42)] + public void SubSecondsOriginal_tag_gets_converted_to_milliseconds( + string subSecondsTagValue, + int expectedMilliseconds) + { + var exifTags = new[] + { + new Tag + { + Type = 0x9003, + Value = "2021:04:05 10:56:13" + }, + new Tag + { + Type = 0x9291, + Value = subSecondsTagValue + } + }; + + var sut = ImageFile.Create(new CameraFilePath("file.jpg"), exifTags).Value; + + var expected = new DateTime(2021, 04, 05, 10, 56, 13).AddMilliseconds(expectedMilliseconds); + sut.Created.Should().Be(expected); + } + + private sealed class Tag : + ITag + { + public int Type { get; internal init; } + public string Directory => string.Empty; + public string Value { get; internal init; } + } + + private sealed record TestTag(string Value) : + ITag + { + public int Type => 0x9003; + public string Directory => string.Empty; + } + } +} \ No newline at end of file diff --git a/CameraUtility/CameraFiles/AbstractCameraFile.cs b/CameraUtility/CameraFiles/AbstractCameraFile.cs index 3c78ad4..6ae11f0 100755 --- a/CameraUtility/CameraFiles/AbstractCameraFile.cs +++ b/CameraUtility/CameraFiles/AbstractCameraFile.cs @@ -4,7 +4,7 @@ namespace CameraUtility.CameraFiles { [DebuggerDisplay("{FullName} {Created}")] - internal abstract class AbstractCameraFile : ICameraFile + public abstract class AbstractCameraFile { protected AbstractCameraFile( CameraFilePath fullName) diff --git a/CameraUtility/CameraFiles/ICameraFile.cs b/CameraUtility/CameraFiles/ICameraFile.cs index ab0c03d..6c88f5b 100755 --- a/CameraUtility/CameraFiles/ICameraFile.cs +++ b/CameraUtility/CameraFiles/ICameraFile.cs @@ -2,7 +2,7 @@ namespace CameraUtility.CameraFiles { - internal interface ICameraFile + public interface ICameraFile { CameraFilePath FullName { get; } string Extension { get; } diff --git a/CameraUtility/CameraFiles/ImageFile.cs b/CameraUtility/CameraFiles/ImageFile.cs index 51babaa..9f19db0 100755 --- a/CameraUtility/CameraFiles/ImageFile.cs +++ b/CameraUtility/CameraFiles/ImageFile.cs @@ -10,7 +10,7 @@ namespace CameraUtility.CameraFiles /// /// Jpeg (Android and Canon) or Cr2 raw Canon photo. /// - internal sealed class ImageFile + public sealed class ImageFile : AbstractCameraFile, ICameraFile { /// @@ -19,11 +19,11 @@ internal sealed class ImageFile private const int DateTimeOriginalTagType = 0x9003; /// - /// Some older cameras don't use the 0x9003. We will try to read it from 0x0132 tag. + /// Some older cameras don't use the 0x9003. We will try to read it from 0x0132 tag (ModifyDate). /// private const int FallbackDateTimeTagType = 0x0132; - private const int SubSecondTagType = 0x9291; + private const int SubSecTimeOriginalTagType = 0x9291; private ImageFile( CameraFilePath fullName, @@ -36,7 +36,7 @@ private ImageFile( public override DateTime Created { get; } public override string DestinationNamePrefix => "IMG_"; - internal static Result Create( + public static Result Create( CameraFilePath fullName, IEnumerable exifTags) { @@ -54,14 +54,13 @@ internal static Result Create( return new ImageFile( fullName, - parsedDateTimeResult.Value.AddMilliseconds(FindSubSeconds(enumeratedExifTags))); + parsedDateTimeResult.Value.Add(FindSubSeconds(enumeratedExifTags))); } private static Result FindCreatedDateTimeTag( IList exifTags) { var tag = exifTags.FirstOrDefault(t => t.Type == DateTimeOriginalTagType) - /* Try fallback tag, if not found then an exception will be thrown */ ?? exifTags.FirstOrDefault(t => t.Type == FallbackDateTimeTagType); return tag is not null @@ -84,17 +83,28 @@ private static Result ParseCreatedDateTime( return Result.Failure("Invalid metadata"); } - private static int FindSubSeconds( + private static TimeSpan FindSubSeconds( IEnumerable exifTags) { - var subSeconds = exifTags.FirstOrDefault(t => t.Type == SubSecondTagType); - return subSeconds is null ? 0 : ToMilliseconds(int.Parse(subSeconds.Value)); + var subSeconds = exifTags.FirstOrDefault(t => t.Type == SubSecTimeOriginalTagType); + return subSeconds is null ? TimeSpan.Zero : ToMilliseconds(subSeconds.Value); } - private static int ToMilliseconds( - int subSeconds) + /// + /// EXIF specifies that SubSecOriginal tag contains "fractions" of a second. Depending on length of the + /// value a different fractional unit can be used, e.g. "042" is 42 milliseconds (0.042 of a second) but + /// "42" is 420 milliseconds (0.42 of a second). + /// + private static TimeSpan ToMilliseconds( + string subSeconds) { - return subSeconds * 10; + if (int.TryParse(subSeconds, out var tagIntValue) is false) + { + return TimeSpan.Zero; + } + var subSecondDenominator = Math.Pow(10, subSeconds.Trim().Length); + var millisecondMultiplier = 1000 / subSecondDenominator; + return TimeSpan.FromMilliseconds(tagIntValue * millisecondMultiplier); } } } \ No newline at end of file diff --git a/CameraUtility/Commands/ImageFilesTransfer/Execution/Orchestrator.cs b/CameraUtility/Commands/ImageFilesTransfer/Execution/Orchestrator.cs index afe7bcd..c5a4ad7 100644 --- a/CameraUtility/Commands/ImageFilesTransfer/Execution/Orchestrator.cs +++ b/CameraUtility/Commands/ImageFilesTransfer/Execution/Orchestrator.cs @@ -53,7 +53,10 @@ int IOrchestrator.Execute(AbstractTransferImageFilesCommand.OptionArgs args) catch (Exception exception) { OnException(this, (cameraFilePath, exception)); - if (!args.KeepGoing) return ErrorResultCode; + if (!args.KeepGoing) + { + return ErrorResultCode; + } result = ErrorResultCode; }