diff --git a/Program.cs b/Program.cs index 2e5a3de..25092fb 100644 --- a/Program.cs +++ b/Program.cs @@ -147,9 +147,10 @@ void main() { // FIXME: this should probably be IDisposable? public class Photo { - public string File; + public string Filename; public bool Loaded = false; public Vector2i Size; + public DateTime DateTimeOriginal; public string CameraModel = ""; public string LensModel = ""; public string FocalLength = ""; @@ -163,12 +164,14 @@ public class Photo { private Texture placeholder; private Image? image = null; - public Photo(string file, Texture placeholder) { - File = file; + public Photo(string filename, Texture placeholder) { + Filename = filename; this.placeholder = 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); ParseExif(info.Metadata.ExifProfile); 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 // edit the image due to rotation (etc) and don't want to try generating // a texture for it until that's already happened. - Image tmp = await Image.LoadAsync(File); + Image tmp = await Image.LoadAsync(Filename); Util.RotateImageFromExif(tmp, Orientation); image = tmp; } @@ -206,14 +209,34 @@ public class Photo { 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) { if (exifs == null) { 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? orientation; if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) { 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? 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) { // Console.WriteLine(exif.Tag.ToString() + " " + exif.GetValue().ToString()); // } @@ -301,7 +341,7 @@ public class Photo { public string Description() { 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. // 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(@"G:\DCIM\100EOSR6\"); + string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\"); + // 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\Desktop\Germany all\104D7000"); // 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++) { 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() { base.OnUnload(); }