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;
}