|
|
@ -15,6 +15,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; |
|
|
|
using System; |
|
|
|
using System.Diagnostics; |
|
|
|
using System.Runtime.CompilerServices; |
|
|
|
using System.Text; |
|
|
|
using System.Xml.Linq; |
|
|
|
|
|
|
|
namespace SemiColinGames; |
|
|
@ -199,8 +200,8 @@ public class Photo { |
|
|
|
DateTimeOriginal = creationTime; |
|
|
|
ImageInfo info = Image.Identify(filename); |
|
|
|
Size = new(info.Size.Width, info.Size.Height); |
|
|
|
Rating = ParseRating(info.Metadata.XmpProfile); |
|
|
|
ParseExif(info.Metadata.ExifProfile); |
|
|
|
TryParseRating(info.Metadata.XmpProfile, out Rating); |
|
|
|
} |
|
|
|
|
|
|
|
public async void LoadAsync() { |
|
|
@ -221,7 +222,7 @@ public class Photo { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async void SaveAsJpeg(string outputRoot, JpegEncoder encoder) { |
|
|
|
public async void SaveAsJpegAsync(string outputRoot, JpegEncoder encoder) { |
|
|
|
// FIXME: if nothing was changed about this image, just copy the file bytes directly, possibly with metadata changed?
|
|
|
|
string directory = System.IO.Path.Combine( |
|
|
|
outputRoot, |
|
|
@ -231,7 +232,7 @@ public class Photo { |
|
|
|
Directory.CreateDirectory(directory); |
|
|
|
string filename = System.IO.Path.Combine(directory, System.IO.Path.GetFileName(Filename)); |
|
|
|
Console.WriteLine("saving " + filename); |
|
|
|
// FIXME: update Rating data.
|
|
|
|
// FIXME: what if we also saved Exif rating?
|
|
|
|
// FIXME: add comments / captions as ImageDescription?
|
|
|
|
// FIXME: strip some Exif tags for privacy reasons?
|
|
|
|
// FIXME: warn if the file already exists?
|
|
|
@ -249,31 +250,50 @@ public class Photo { |
|
|
|
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); |
|
|
|
exif.SetValue<string>(ExifTag.DateTime, datetime); |
|
|
|
|
|
|
|
image.Metadata.XmpProfile = UpdateXmp(image.Metadata.XmpProfile); |
|
|
|
|
|
|
|
await image.SaveAsync(filename, encoder); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private bool TryParseRating(XmpProfile? xmp, out int rating) { |
|
|
|
rating = 0; |
|
|
|
private XElement? GetXmpRoot(XmpProfile? xmp) { |
|
|
|
if (xmp == null) { |
|
|
|
return false; |
|
|
|
return null; |
|
|
|
} |
|
|
|
XDocument? doc = xmp.GetDocument(); |
|
|
|
if (doc == null) { |
|
|
|
return false; |
|
|
|
return null; |
|
|
|
} |
|
|
|
XElement? root = doc.Root; |
|
|
|
return doc.Root; |
|
|
|
} |
|
|
|
|
|
|
|
private int ParseRating(XmpProfile? xmp) { |
|
|
|
XElement? root = GetXmpRoot(xmp); |
|
|
|
if (root == null) { |
|
|
|
return false; |
|
|
|
return 0; |
|
|
|
} |
|
|
|
foreach (XElement elt in root.Descendants()) { |
|
|
|
if (elt.Name == "{http://ns.adobe.com/xap/1.0/}Rating") { |
|
|
|
int rating = 0; |
|
|
|
if (int.TryParse(elt.Value, out rating)) { |
|
|
|
return true; |
|
|
|
return rating; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return false; |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
private XmpProfile? UpdateXmp(XmpProfile? xmp) { |
|
|
|
if (xmp == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
string xmlIn = Encoding.UTF8.GetString(xmp.ToByteArray()); |
|
|
|
int index = xmlIn.IndexOf("</xmp:Rating>"); |
|
|
|
if (index == -1) { |
|
|
|
return xmp; |
|
|
|
} |
|
|
|
string xmlOut = xmlIn.Substring(0, index - 1) + Rating.ToString() + xmlIn.Substring(index); |
|
|
|
return new XmpProfile(Encoding.UTF8.GetBytes(xmlOut)); |
|
|
|
} |
|
|
|
|
|
|
|
// Exif (and other image metadata) reference, from the now-defunct Metadata Working Group:
|
|
|
@ -627,6 +647,7 @@ public class Game : GameWindow { |
|
|
|
bool ctrlIsDown = input.IsKeyDown(Keys.LeftControl) || input.IsKeyDown(Keys.RightControl); |
|
|
|
|
|
|
|
// FIXME: add a confirm dialog before closing. (Also for the window-close button.)
|
|
|
|
// FIXME: don't quit if there's pending file-write operations.
|
|
|
|
// Close when Escape is pressed.
|
|
|
|
if (input.IsKeyPressed(Keys.Escape)) { |
|
|
|
Close(); |
|
|
@ -826,7 +847,7 @@ public class Game : GameWindow { |
|
|
|
// 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\desktop\totte-output\2023\07\28");
|
|
|
|
// string[] files = Directory.GetFiles(@"c:\users\colin\desktop\totte-output\2023\07\31");
|
|
|
|
// 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\");
|
|
|
@ -913,9 +934,10 @@ public class Game : GameWindow { |
|
|
|
// FIXME: show a progress bar or something.
|
|
|
|
private async void ExportPhotos() { |
|
|
|
JpegEncoder encoder = new JpegEncoder() { Quality = 100 }; |
|
|
|
string outputRoot = @"c:\users\colin\pictures\photos\"; |
|
|
|
string outputRoot = @"c:\users\colin\desktop\totte-output"; |
|
|
|
// string outputRoot = @"c:\users\colin\pictures\photos";
|
|
|
|
foreach (Photo p in photos) { |
|
|
|
await Task.Run( () => { p.SaveAsJpeg(outputRoot, encoder); }); |
|
|
|
await Task.Run( () => { p.SaveAsJpegAsync(outputRoot, encoder); }); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -1088,7 +1110,8 @@ static class Program { |
|
|
|
nwSettings.WindowState = WindowState.Normal; |
|
|
|
nwSettings.CurrentMonitor = bestMonitor.Handle; |
|
|
|
nwSettings.Location = new Vector2i(bestMonitor.WorkArea.Min.X + 1, bestMonitor.WorkArea.Min.Y + 31); |
|
|
|
nwSettings.Size = new Vector2i(bestMonitor.WorkArea.Size.X - 2, bestMonitor.WorkArea.Size.Y - 32); |
|
|
|
// nwSettings.Size = new Vector2i(bestMonitor.WorkArea.Size.X - 2, bestMonitor.WorkArea.Size.Y - 32);
|
|
|
|
nwSettings.Size = new Vector2i(1600, 900); |
|
|
|
nwSettings.MinimumSize = UiGeometry.MIN_WINDOW_SIZE; |
|
|
|
nwSettings.Title = "Totte"; |
|
|
|
nwSettings.IsEventDriven = false; |
|
|
|