Skip to content

Commit 1439249

Browse files
committed
Fixes invalid millisecond calculations for SubSecOriginal tag where
value is not in Canon's format.
1 parent 8c3e798 commit 1439249

File tree

5 files changed

+82
-15
lines changed

5 files changed

+82
-15
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using CameraUtility.CameraFiles;
3+
using CameraUtility.Exif;
4+
using FluentAssertions;
5+
using Xunit;
6+
7+
namespace CameraUtility.Tests.CameraFiles
8+
{
9+
public sealed class ImageFileTests
10+
{
11+
[Theory]
12+
[InlineData("42", 420)]
13+
[InlineData("042", 42)]
14+
[InlineData("042000", 42)]
15+
public void SubSecondsOriginal_tag_gets_converted_to_milliseconds(
16+
string subSecondsTagValue,
17+
int expectedMilliseconds)
18+
{
19+
var exifTags = new[]
20+
{
21+
new Tag
22+
{
23+
Type = 0x9003,
24+
Value = "2021:04:05 10:56:13"
25+
},
26+
new Tag
27+
{
28+
Type = 0x9291,
29+
Value = subSecondsTagValue
30+
}
31+
};
32+
33+
var sut = ImageFile.Create(new CameraFilePath("file.jpg"), exifTags).Value;
34+
35+
var expected = new DateTime(2021, 04, 05, 10, 56, 13).AddMilliseconds(expectedMilliseconds);
36+
sut.Created.Should().Be(expected);
37+
}
38+
39+
private sealed class Tag :
40+
ITag
41+
{
42+
public int Type { get; internal init; }
43+
public string Directory => string.Empty;
44+
public string Value { get; internal init; }
45+
}
46+
47+
private sealed record TestTag(string Value) :
48+
ITag
49+
{
50+
public int Type => 0x9003;
51+
public string Directory => string.Empty;
52+
}
53+
}
54+
}

CameraUtility/CameraFiles/AbstractCameraFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace CameraUtility.CameraFiles
55
{
66
[DebuggerDisplay("{FullName} {Created}")]
7-
internal abstract class AbstractCameraFile : ICameraFile
7+
public abstract class AbstractCameraFile
88
{
99
protected AbstractCameraFile(
1010
CameraFilePath fullName)

CameraUtility/CameraFiles/ICameraFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace CameraUtility.CameraFiles
44
{
5-
internal interface ICameraFile
5+
public interface ICameraFile
66
{
77
CameraFilePath FullName { get; }
88
string Extension { get; }

CameraUtility/CameraFiles/ImageFile.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace CameraUtility.CameraFiles
1010
/// <summary>
1111
/// Jpeg (Android and Canon) or Cr2 raw Canon photo.
1212
/// </summary>
13-
internal sealed class ImageFile
13+
public sealed class ImageFile
1414
: AbstractCameraFile, ICameraFile
1515
{
1616
/// <summary>
@@ -19,11 +19,11 @@ internal sealed class ImageFile
1919
private const int DateTimeOriginalTagType = 0x9003;
2020

2121
/// <summary>
22-
/// Some older cameras don't use the 0x9003. We will try to read it from 0x0132 tag.
22+
/// Some older cameras don't use the 0x9003. We will try to read it from 0x0132 tag (ModifyDate).
2323
/// </summary>
2424
private const int FallbackDateTimeTagType = 0x0132;
2525

26-
private const int SubSecondTagType = 0x9291;
26+
private const int SubSecTimeOriginalTagType = 0x9291;
2727

2828
private ImageFile(
2929
CameraFilePath fullName,
@@ -36,7 +36,7 @@ private ImageFile(
3636
public override DateTime Created { get; }
3737
public override string DestinationNamePrefix => "IMG_";
3838

39-
internal static Result<ICameraFile> Create(
39+
public static Result<ICameraFile> Create(
4040
CameraFilePath fullName,
4141
IEnumerable<ITag> exifTags)
4242
{
@@ -54,14 +54,13 @@ internal static Result<ICameraFile> Create(
5454

5555
return new ImageFile(
5656
fullName,
57-
parsedDateTimeResult.Value.AddMilliseconds(FindSubSeconds(enumeratedExifTags)));
57+
parsedDateTimeResult.Value.Add(FindSubSeconds(enumeratedExifTags)));
5858
}
5959

6060
private static Result<ITag> FindCreatedDateTimeTag(
6161
IList<ITag> exifTags)
6262
{
6363
var tag = exifTags.FirstOrDefault(t => t.Type == DateTimeOriginalTagType)
64-
/* Try fallback tag, if not found then an exception will be thrown */
6564
?? exifTags.FirstOrDefault(t => t.Type == FallbackDateTimeTagType);
6665
return
6766
tag is not null
@@ -84,17 +83,28 @@ private static Result<DateTime> ParseCreatedDateTime(
8483
return Result.Failure<DateTime>("Invalid metadata");
8584
}
8685

87-
private static int FindSubSeconds(
86+
private static TimeSpan FindSubSeconds(
8887
IEnumerable<ITag> exifTags)
8988
{
90-
var subSeconds = exifTags.FirstOrDefault(t => t.Type == SubSecondTagType);
91-
return subSeconds is null ? 0 : ToMilliseconds(int.Parse(subSeconds.Value));
89+
var subSeconds = exifTags.FirstOrDefault(t => t.Type == SubSecTimeOriginalTagType);
90+
return subSeconds is null ? TimeSpan.Zero : ToMilliseconds(subSeconds.Value);
9291
}
9392

94-
private static int ToMilliseconds(
95-
int subSeconds)
93+
/// <summary>
94+
/// EXIF specifies that SubSecOriginal tag contains "fractions" of a second. Depending on length of the
95+
/// value a different fractional unit can be used, e.g. "042" is 42 milliseconds (0.042 of a second) but
96+
/// "42" is 420 milliseconds (0.42 of a second).
97+
/// </summary>
98+
private static TimeSpan ToMilliseconds(
99+
string subSeconds)
96100
{
97-
return subSeconds * 10;
101+
if (int.TryParse(subSeconds, out var tagIntValue) is false)
102+
{
103+
return TimeSpan.Zero;
104+
}
105+
var subSecondDenominator = Math.Pow(10, subSeconds.Trim().Length);
106+
var millisecondMultiplier = 1000 / subSecondDenominator;
107+
return TimeSpan.FromMilliseconds(tagIntValue * millisecondMultiplier);
98108
}
99109
}
100110
}

CameraUtility/Commands/ImageFilesTransfer/Execution/Orchestrator.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ int IOrchestrator.Execute(AbstractTransferImageFilesCommand.OptionArgs args)
5353
catch (Exception exception)
5454
{
5555
OnException(this, (cameraFilePath, exception));
56-
if (!args.KeepGoing) return ErrorResultCode;
56+
if (!args.KeepGoing)
57+
{
58+
return ErrorResultCode;
59+
}
5760

5861
result = ErrorResultCode;
5962
}

0 commit comments

Comments
 (0)