sort photos by creation time

This commit is contained in:
Colin McMillen 2023-07-25 20:25:07 -04:00
parent af4827a127
commit 3b85c5d21a

View File

@ -147,9 +147,10 @@ void main() {
// FIXME: this should probably be IDisposable? // FIXME: this should probably be IDisposable?
public class Photo { public class Photo {
public string File; public string Filename;
public bool Loaded = false; public bool Loaded = false;
public Vector2i Size; public Vector2i Size;
public DateTime DateTimeOriginal;
public string CameraModel = ""; public string CameraModel = "";
public string LensModel = ""; public string LensModel = "";
public string FocalLength = "<unk>"; public string FocalLength = "<unk>";
@ -163,12 +164,14 @@ public class Photo {
private Texture placeholder; private Texture placeholder;
private Image<Rgba32>? image = null; private Image<Rgba32>? image = null;
public Photo(string file, Texture placeholder) { public Photo(string filename, Texture placeholder) {
File = file; Filename = filename;
this.placeholder = placeholder; this.placeholder = placeholder;
texture = placeholder; texture = placeholder;
ImageInfo info = Image.Identify(file); DateTime creationTime = File.GetCreationTime(filename); // Local time.
DateTimeOriginal = creationTime;
ImageInfo info = Image.Identify(filename);
Size = new(info.Size.Width, info.Size.Height); Size = new(info.Size.Width, info.Size.Height);
ParseExif(info.Metadata.ExifProfile); ParseExif(info.Metadata.ExifProfile);
TryParseRating(info.Metadata.XmpProfile, out Rating); TryParseRating(info.Metadata.XmpProfile, out Rating);
@ -178,7 +181,7 @@ public class Photo {
// We don't assign to this.image until Load() is done, because we might // We don't assign to this.image until Load() is done, because we might
// edit the image due to rotation (etc) and don't want to try generating // edit the image due to rotation (etc) and don't want to try generating
// a texture for it until that's already happened. // a texture for it until that's already happened.
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(File); Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
Util.RotateImageFromExif(tmp, Orientation); Util.RotateImageFromExif(tmp, Orientation);
image = tmp; image = tmp;
} }
@ -206,14 +209,34 @@ public class Photo {
return false; return false;
} }
// Exif (and other image metadata) reference, from the now-defunct Metadata Working Group:
// https://web.archive.org/web/20180919181934/http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
//
// Specifically:
//
// In general, date/time metadata is being used to describe the following scenarios:
// * Date/time original specifies when a photo was taken
// * Date/time digitized specifies when an image was digitized
// * Date/time modified specifies when a file was modified by the user
//
// Original Date/Time Creation date of the intellectual content (e.g. the photograph), rather than the creation date of the content being shown
// Exif DateTimeOriginal (36867, 0x9003) and SubSecTimeOriginal (37521, 0x9291)
// IPTC DateCreated (IIM 2:55, 0x0237) and TimeCreated (IIM 2:60, 0x023C)
// XMP (photoshop:DateCreated)
//
// Digitized Date/Time Creation date of the digital representation
// Exif DateTimeDigitized (36868, 0x9004) and SubSecTimeDigitized (37522, 0x9292)
// IPTC DigitalCreationDate (IIM 2:62, 0x023E) and DigitalCreationTime (IIM 2:63, 0x023F)
// XMP (xmp:CreateDate)
//
// Modification Date/Time Modification date of the digital image file
// Exif DateTime (306, 0x132) and SubSecTime (37520, 0x9290)
// XMP (xmp:ModifyDate)
private void ParseExif(ExifProfile? exifs) { private void ParseExif(ExifProfile? exifs) {
if (exifs == null) { if (exifs == null) {
return; return;
} }
// FIXME: when we write out images, we'll want to correct the Exif Orientation to 1.
// FIXME: handle date shot / edited (and sort by shot date?)
IExifValue<ushort>? orientation; IExifValue<ushort>? orientation;
if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) { if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) {
Orientation = orientation.Value; Orientation = orientation.Value;
@ -281,6 +304,23 @@ public class Photo {
} }
} }
} }
// FIXME: I think the iPhone stores time in UTC but other cameras report it in local time.
IExifValue<string>? dateTimeOriginal;
if (exifs.TryGetValue(ExifTag.DateTimeOriginal, out dateTimeOriginal)) {
DateTime date;
if (DateTime.TryParseExact(
dateTimeOriginal.Value ?? "",
"yyyy:MM:dd HH:mm:ss",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeLocal,
out date)) {
DateTimeOriginal = date;
} else {
Console.WriteLine($"*** WARNING: unexpected DateTimeOriginal value: {dateTimeOriginal.Value}");
}
}
// foreach (IExifValue exif in exifs.Values) { // foreach (IExifValue exif in exifs.Values) {
// Console.WriteLine(exif.Tag.ToString() + " " + exif.GetValue().ToString()); // Console.WriteLine(exif.Tag.ToString() + " " + exif.GetValue().ToString());
// } // }
@ -301,7 +341,7 @@ public class Photo {
public string Description() { public string Description() {
string shootingInfo = $"{FocalLength}, {FNumber} at {ExposureTime}, {IsoSpeed}"; string shootingInfo = $"{FocalLength}, {FNumber} at {ExposureTime}, {IsoSpeed}";
return String.Format("{0,-40} {1,-50} {2}", shootingInfo, $"{CameraModel} {LensModel}", File); return String.Format("{0,-40} {1,-50} {2}", shootingInfo, $"{CameraModel} {LensModel}", Filename);
} }
} }
@ -603,8 +643,8 @@ public class Game : GameWindow {
// Load textures from JPEGs. // Load textures from JPEGs.
// string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\"); // string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\");
// string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\"); string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\");
string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\"); // string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23"); // string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000"); // string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\"); // string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\");
@ -617,11 +657,23 @@ public class Game : GameWindow {
} }
} }
photos.Sort(ComparePhotosByDate);
for (int i = 0; i < 40 && i < photos.Count; i++) { for (int i = 0; i < 40 && i < photos.Count; i++) {
await Task.Run( () => { photos[i].Load(); }); await Task.Run( () => { photos[i].Load(); });
} }
} }
private static int ComparePhotosByDate(Photo x, Photo y) {
int compare = x.DateTimeOriginal.CompareTo(y.DateTimeOriginal);
if (compare != 0) {
return compare;
}
// If the photos have the same seconds value, sort by filename
// (since cameras usually increment the filename for successive shots.)
return x.Filename.CompareTo(y.Filename);
}
protected override void OnUnload() { protected override void OnUnload() {
base.OnUnload(); base.OnUnload();
} }