Compare commits
No commits in common. "304097177ce41b188ca80816b0d045a944d1f8f3" and "2a28f94dc11b16302218df5626e22d2b7acb735f" have entirely different histories.
304097177c
...
2a28f94dc1
291
Photo.cs
291
Photo.cs
@ -1,291 +0,0 @@
|
||||
using OpenTK.Mathematics;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace SemiColinGames;
|
||||
|
||||
|
||||
public class Photo {
|
||||
public string Filename;
|
||||
public bool Loaded = false;
|
||||
public long LastTouch = 0;
|
||||
public Vector2i Size;
|
||||
public DateTime DateTimeOriginal;
|
||||
public string CameraModel = "";
|
||||
public string LensModel = "";
|
||||
public string ShortLensModel = "";
|
||||
public string FocalLength = "<unk>";
|
||||
public string FNumber = "<unk>";
|
||||
public string ExposureTime = "<unk>";
|
||||
public string IsoSpeed = "<unk>";
|
||||
public int Rating = 0;
|
||||
public ushort Orientation = 1;
|
||||
public Rectangle CropRectangle = Rectangle.Empty;
|
||||
|
||||
private static long touchCounter = 0;
|
||||
private Texture texture;
|
||||
private Texture placeholder;
|
||||
private Image<Rgba32>? image = null;
|
||||
|
||||
public Photo(string filename, Texture placeholder) {
|
||||
Filename = filename;
|
||||
this.placeholder = placeholder;
|
||||
texture = placeholder;
|
||||
|
||||
DateTime creationTime = File.GetCreationTime(filename); // Local time.
|
||||
DateTimeOriginal = creationTime;
|
||||
ImageInfo info = Image.Identify(filename);
|
||||
Size = new(info.Size.Width, info.Size.Height);
|
||||
Rating = ParseRating(info.Metadata.XmpProfile);
|
||||
ParseExif(info.Metadata.ExifProfile);
|
||||
}
|
||||
|
||||
public async void LoadAsync() {
|
||||
// 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.
|
||||
LastTouch = touchCounter++;
|
||||
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
|
||||
Util.RotateImageFromExif(tmp, Orientation);
|
||||
image = tmp;
|
||||
}
|
||||
|
||||
public void Unload() {
|
||||
Loaded = false;
|
||||
if (texture != placeholder) {
|
||||
texture.Dispose();
|
||||
texture = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
String.Format("{0:D4}", DateTimeOriginal.Year),
|
||||
String.Format("{0:D2}", DateTimeOriginal.Month),
|
||||
String.Format("{0:D2}", DateTimeOriginal.Day));
|
||||
Directory.CreateDirectory(directory);
|
||||
string filename = System.IO.Path.Combine(directory, System.IO.Path.GetFileName(Filename));
|
||||
Console.WriteLine("saving " + filename);
|
||||
// FIXME: add comments / captions as ImageDescription?
|
||||
// FIXME: strip some Exif tags for privacy reasons?
|
||||
// FIXME: warn if the file already exists?
|
||||
using (Image<Rgba32> image = await Image.LoadAsync<Rgba32>(Filename)) {
|
||||
Util.RotateImageFromExif(image, Orientation);
|
||||
if (CropRectangle != Rectangle.Empty) {
|
||||
image.Mutate(x => x.Crop(CropRectangle));
|
||||
}
|
||||
|
||||
ExifProfile exif = image.Metadata.ExifProfile ?? new();
|
||||
exif.SetValue<ushort>(ExifTag.Orientation, 1);
|
||||
exif.SetValue<string>(ExifTag.Artist, "Colin McMillen");
|
||||
exif.SetValue<string>(ExifTag.Copyright, "Colin McMillen");
|
||||
exif.SetValue<string>(ExifTag.Software, "Totte");
|
||||
exif.SetValue<ushort>(ExifTag.Rating, (ushort) Rating);
|
||||
DateTime now = DateTime.Now;
|
||||
string datetime = String.Format(
|
||||
"{0:D4}:{1:D2}:{2:D2} {3:D2}:{4:D2}:{5:D2}",
|
||||
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 XElement? GetXmpRoot(XmpProfile? xmp) {
|
||||
if (xmp == null) {
|
||||
return null;
|
||||
}
|
||||
XDocument? doc = xmp.GetDocument();
|
||||
if (doc == null) {
|
||||
return null;
|
||||
}
|
||||
return doc.Root;
|
||||
}
|
||||
|
||||
private int ParseRating(XmpProfile? xmp) {
|
||||
XElement? root = GetXmpRoot(xmp);
|
||||
if (root == null) {
|
||||
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 rating;
|
||||
}
|
||||
}
|
||||
}
|
||||
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:
|
||||
// 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)
|
||||
//
|
||||
// See also: https://exiftool.org/TagNames/EXIF.html
|
||||
private void ParseExif(ExifProfile? exifs) {
|
||||
if (exifs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IExifValue<ushort>? orientation;
|
||||
if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) {
|
||||
Orientation = orientation.Value;
|
||||
}
|
||||
|
||||
IExifValue<string>? model;
|
||||
if (exifs.TryGetValue(ExifTag.Model, out model)) {
|
||||
CameraModel = model.Value ?? "";
|
||||
}
|
||||
|
||||
IExifValue<string>? lensModel;
|
||||
if (exifs.TryGetValue(ExifTag.LensModel, out lensModel)) {
|
||||
LensModel = lensModel.Value ?? "";
|
||||
ShortLensModel = GetShortLensModel(LensModel);
|
||||
}
|
||||
|
||||
IExifValue<Rational>? focalLength;
|
||||
if (exifs.TryGetValue(ExifTag.FocalLength, out focalLength)) {
|
||||
Rational r = focalLength.Value;
|
||||
FocalLength = $"{r.Numerator / r.Denominator}mm";
|
||||
}
|
||||
|
||||
IExifValue<Rational>? fNumber;
|
||||
if (exifs.TryGetValue(ExifTag.FNumber, out fNumber)) {
|
||||
Rational r = fNumber.Value;
|
||||
if (r.Numerator % r.Denominator == 0) {
|
||||
FNumber = $"f/{r.Numerator / r.Denominator}";
|
||||
} else {
|
||||
int fTimesTen = (int) Math.Round(10f * r.Numerator / r.Denominator);
|
||||
FNumber = $"f/{fTimesTen / 10}.{fTimesTen % 10}";
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: could also show ExposureBiasValue, ExposureMode, ExposureProgram?
|
||||
IExifValue<Rational>? exposureTime;
|
||||
if (exifs.TryGetValue(ExifTag.ExposureTime, out exposureTime)) {
|
||||
Rational r = exposureTime.Value;
|
||||
if (r.Numerator == 1) {
|
||||
ExposureTime = $"1/{r.Denominator}";
|
||||
} else if (r.Numerator == 10) {
|
||||
ExposureTime = $"1/{r.Denominator / 10}";
|
||||
} else if (r.Denominator == 1) {
|
||||
ExposureTime = $"{r.Numerator }\"";
|
||||
} else if (r.Denominator == 10) {
|
||||
ExposureTime = $"{r.Numerator / 10}.{r.Numerator % 10}\"";
|
||||
} else {
|
||||
Console.WriteLine($"*** WARNING: unexpected ExposureTime: {r.Numerator}/{r.Denominator}");
|
||||
ExposureTime = r.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
IExifValue<ushort[]>? isoSpeed;
|
||||
if (exifs.TryGetValue(ExifTag.ISOSpeedRatings, out isoSpeed)) {
|
||||
ushort[]? iso = isoSpeed.Value;
|
||||
if (iso != null) {
|
||||
if (iso.Length != 1) {
|
||||
Console.WriteLine($"*** WARNING: unexpected ISOSpeedRatings array length: {iso.Length}");
|
||||
}
|
||||
if (iso.Length >= 1) {
|
||||
IsoSpeed = $"ISO {iso[0]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: there is also a SubSecTimeOriginal tag we could use to get fractional seconds.
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetShortLensModel(string lensModel) {
|
||||
// Example Canon RF lens names:
|
||||
// RF16mm F2.8 STM
|
||||
// RF24-105mm F4-7.1 IS STM
|
||||
// RF35mm F1.8 MACRO IS STM
|
||||
// RF100-400mm F5.6-8 IS USM
|
||||
string[] tokens = lensModel.Split(' ');
|
||||
string result = "";
|
||||
foreach (string token in tokens) {
|
||||
if (token == "STM" || token == "IS" || token == "USM") {
|
||||
continue;
|
||||
}
|
||||
result += token + " ";
|
||||
}
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
public Texture Texture() {
|
||||
LastTouch = touchCounter++;
|
||||
if (texture == placeholder && image != null) {
|
||||
// The texture needs to be created on the GL thread, so we instantiate
|
||||
// it here (since this is called from OnRenderFrame), as long as the
|
||||
// image is ready to go.
|
||||
texture = new Texture(image);
|
||||
image.Dispose();
|
||||
image = null;
|
||||
Loaded = true;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
public string Description() {
|
||||
string date = DateTimeOriginal.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
return String.Format(
|
||||
"{0,6} {1,-5} {2,-7} {3,-10} {4} {5,-20} {6}",
|
||||
FocalLength, FNumber, ExposureTime, IsoSpeed, date, ShortLensModel, Filename);
|
||||
}
|
||||
}
|
288
Program.cs
288
Program.cs
@ -5,17 +5,21 @@ using OpenTK.Windowing.Common.Input;
|
||||
using OpenTK.Windowing.Desktop;
|
||||
using OpenTK.Windowing.GraphicsLibraryFramework;
|
||||
// https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Image.html
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.Drawing;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace SemiColinGames;
|
||||
|
||||
|
||||
public class FpsCounter {
|
||||
private readonly int[] frameTimes = new int[30];
|
||||
private double fps = 0;
|
||||
@ -156,6 +160,288 @@ public class CropTool : ITool {
|
||||
}
|
||||
|
||||
|
||||
public class Photo {
|
||||
public string Filename;
|
||||
public bool Loaded = false;
|
||||
public long LastTouch = 0;
|
||||
public Vector2i Size;
|
||||
public DateTime DateTimeOriginal;
|
||||
public string CameraModel = "";
|
||||
public string LensModel = "";
|
||||
public string ShortLensModel = "";
|
||||
public string FocalLength = "<unk>";
|
||||
public string FNumber = "<unk>";
|
||||
public string ExposureTime = "<unk>";
|
||||
public string IsoSpeed = "<unk>";
|
||||
public int Rating = 0;
|
||||
public ushort Orientation = 1;
|
||||
public Rectangle CropRectangle = Rectangle.Empty;
|
||||
|
||||
private static long touchCounter = 0;
|
||||
private Texture texture;
|
||||
private Texture placeholder;
|
||||
private Image<Rgba32>? image = null;
|
||||
|
||||
public Photo(string filename, Texture placeholder) {
|
||||
Filename = filename;
|
||||
this.placeholder = placeholder;
|
||||
texture = placeholder;
|
||||
|
||||
DateTime creationTime = File.GetCreationTime(filename); // Local time.
|
||||
DateTimeOriginal = creationTime;
|
||||
ImageInfo info = Image.Identify(filename);
|
||||
Size = new(info.Size.Width, info.Size.Height);
|
||||
Rating = ParseRating(info.Metadata.XmpProfile);
|
||||
ParseExif(info.Metadata.ExifProfile);
|
||||
}
|
||||
|
||||
public async void LoadAsync() {
|
||||
// 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.
|
||||
LastTouch = touchCounter++;
|
||||
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
|
||||
Util.RotateImageFromExif(tmp, Orientation);
|
||||
image = tmp;
|
||||
}
|
||||
|
||||
public void Unload() {
|
||||
Loaded = false;
|
||||
if (texture != placeholder) {
|
||||
texture.Dispose();
|
||||
texture = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
String.Format("{0:D4}", DateTimeOriginal.Year),
|
||||
String.Format("{0:D2}", DateTimeOriginal.Month),
|
||||
String.Format("{0:D2}", DateTimeOriginal.Day));
|
||||
Directory.CreateDirectory(directory);
|
||||
string filename = System.IO.Path.Combine(directory, System.IO.Path.GetFileName(Filename));
|
||||
Console.WriteLine("saving " + filename);
|
||||
// FIXME: add comments / captions as ImageDescription?
|
||||
// FIXME: strip some Exif tags for privacy reasons?
|
||||
// FIXME: warn if the file already exists?
|
||||
using (Image<Rgba32> image = await Image.LoadAsync<Rgba32>(Filename)) {
|
||||
Util.RotateImageFromExif(image, Orientation);
|
||||
if (CropRectangle != Rectangle.Empty) {
|
||||
image.Mutate(x => x.Crop(CropRectangle));
|
||||
}
|
||||
|
||||
ExifProfile exif = image.Metadata.ExifProfile ?? new();
|
||||
exif.SetValue<ushort>(ExifTag.Orientation, 1);
|
||||
exif.SetValue<string>(ExifTag.Artist, "Colin McMillen");
|
||||
exif.SetValue<string>(ExifTag.Copyright, "Colin McMillen");
|
||||
exif.SetValue<string>(ExifTag.Software, "Totte");
|
||||
exif.SetValue<ushort>(ExifTag.Rating, (ushort) Rating);
|
||||
DateTime now = DateTime.Now;
|
||||
string datetime = String.Format(
|
||||
"{0:D4}:{1:D2}:{2:D2} {3:D2}:{4:D2}:{5:D2}",
|
||||
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 XElement? GetXmpRoot(XmpProfile? xmp) {
|
||||
if (xmp == null) {
|
||||
return null;
|
||||
}
|
||||
XDocument? doc = xmp.GetDocument();
|
||||
if (doc == null) {
|
||||
return null;
|
||||
}
|
||||
return doc.Root;
|
||||
}
|
||||
|
||||
private int ParseRating(XmpProfile? xmp) {
|
||||
XElement? root = GetXmpRoot(xmp);
|
||||
if (root == null) {
|
||||
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 rating;
|
||||
}
|
||||
}
|
||||
}
|
||||
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:
|
||||
// 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)
|
||||
//
|
||||
// See also: https://exiftool.org/TagNames/EXIF.html
|
||||
private void ParseExif(ExifProfile? exifs) {
|
||||
if (exifs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IExifValue<ushort>? orientation;
|
||||
if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) {
|
||||
Orientation = orientation.Value;
|
||||
}
|
||||
|
||||
IExifValue<string>? model;
|
||||
if (exifs.TryGetValue(ExifTag.Model, out model)) {
|
||||
CameraModel = model.Value ?? "";
|
||||
}
|
||||
|
||||
IExifValue<string>? lensModel;
|
||||
if (exifs.TryGetValue(ExifTag.LensModel, out lensModel)) {
|
||||
LensModel = lensModel.Value ?? "";
|
||||
ShortLensModel = GetShortLensModel(LensModel);
|
||||
}
|
||||
|
||||
IExifValue<Rational>? focalLength;
|
||||
if (exifs.TryGetValue(ExifTag.FocalLength, out focalLength)) {
|
||||
Rational r = focalLength.Value;
|
||||
FocalLength = $"{r.Numerator / r.Denominator}mm";
|
||||
}
|
||||
|
||||
IExifValue<Rational>? fNumber;
|
||||
if (exifs.TryGetValue(ExifTag.FNumber, out fNumber)) {
|
||||
Rational r = fNumber.Value;
|
||||
if (r.Numerator % r.Denominator == 0) {
|
||||
FNumber = $"f/{r.Numerator / r.Denominator}";
|
||||
} else {
|
||||
int fTimesTen = (int) Math.Round(10f * r.Numerator / r.Denominator);
|
||||
FNumber = $"f/{fTimesTen / 10}.{fTimesTen % 10}";
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: could also show ExposureBiasValue, ExposureMode, ExposureProgram?
|
||||
IExifValue<Rational>? exposureTime;
|
||||
if (exifs.TryGetValue(ExifTag.ExposureTime, out exposureTime)) {
|
||||
Rational r = exposureTime.Value;
|
||||
if (r.Numerator == 1) {
|
||||
ExposureTime = $"1/{r.Denominator}";
|
||||
} else if (r.Numerator == 10) {
|
||||
ExposureTime = $"1/{r.Denominator / 10}";
|
||||
} else if (r.Denominator == 1) {
|
||||
ExposureTime = $"{r.Numerator }\"";
|
||||
} else if (r.Denominator == 10) {
|
||||
ExposureTime = $"{r.Numerator / 10}.{r.Numerator % 10}\"";
|
||||
} else {
|
||||
Console.WriteLine($"*** WARNING: unexpected ExposureTime: {r.Numerator}/{r.Denominator}");
|
||||
ExposureTime = r.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
IExifValue<ushort[]>? isoSpeed;
|
||||
if (exifs.TryGetValue(ExifTag.ISOSpeedRatings, out isoSpeed)) {
|
||||
ushort[]? iso = isoSpeed.Value;
|
||||
if (iso != null) {
|
||||
if (iso.Length != 1) {
|
||||
Console.WriteLine($"*** WARNING: unexpected ISOSpeedRatings array length: {iso.Length}");
|
||||
}
|
||||
if (iso.Length >= 1) {
|
||||
IsoSpeed = $"ISO {iso[0]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: there is also a SubSecTimeOriginal tag we could use to get fractional seconds.
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetShortLensModel(string lensModel) {
|
||||
// Example Canon RF lens names:
|
||||
// RF16mm F2.8 STM
|
||||
// RF24-105mm F4-7.1 IS STM
|
||||
// RF35mm F1.8 MACRO IS STM
|
||||
// RF100-400mm F5.6-8 IS USM
|
||||
string[] tokens = lensModel.Split(' ');
|
||||
string result = "";
|
||||
foreach (string token in tokens) {
|
||||
if (token == "STM" || token == "IS" || token == "USM") {
|
||||
continue;
|
||||
}
|
||||
result += token + " ";
|
||||
}
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
public Texture Texture() {
|
||||
LastTouch = touchCounter++;
|
||||
if (texture == placeholder && image != null) {
|
||||
// The texture needs to be created on the GL thread, so we instantiate
|
||||
// it here (since this is called from OnRenderFrame), as long as the
|
||||
// image is ready to go.
|
||||
texture = new Texture(image);
|
||||
image.Dispose();
|
||||
image = null;
|
||||
Loaded = true;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
public string Description() {
|
||||
string date = DateTimeOriginal.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
return String.Format(
|
||||
"{0,6} {1,-5} {2,-7} {3,-10} {4} {5,-20} {6}",
|
||||
FocalLength, FNumber, ExposureTime, IsoSpeed, date, ShortLensModel, Filename);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Texture : IDisposable {
|
||||
public int Handle;
|
||||
public Vector2i Size;
|
||||
|
Loading…
Reference in New Issue
Block a user