Compare commits
No commits in common. "main" and "5e178aae1febd119e6e588aa8e47a789dbd63762" have entirely different histories.
main
...
5e178aae1f
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,6 +4,3 @@
|
|||||||
# Build results
|
# Build results
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
[Oo]bj/
|
[Oo]bj/
|
||||||
|
|
||||||
# Packaged Zip file.
|
|
||||||
totte.zip
|
|
||||||
|
440
Photo.cs
440
Photo.cs
@ -1,440 +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;
|
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
||||||
using SixLabors.ImageSharp.Formats.Png;
|
|
||||||
using static System.IO.Path;
|
|
||||||
using System.Text;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
|
|
||||||
namespace SemiColinGames;
|
|
||||||
|
|
||||||
// https://exiftool.org/TagNames/GPS.html
|
|
||||||
public struct GpsInfo {
|
|
||||||
public byte[] VersionId;
|
|
||||||
public string Status;
|
|
||||||
public string Datestamp;
|
|
||||||
public Rational[] Timestamp;
|
|
||||||
public Rational[] Latitude;
|
|
||||||
public string LatitudeRef;
|
|
||||||
public Rational[] Longitude;
|
|
||||||
public string LongitudeRef;
|
|
||||||
public Rational Altitude;
|
|
||||||
public byte AltitudeRef;
|
|
||||||
|
|
||||||
public static GpsInfo? ParseExif(ExifProfile exif) {
|
|
||||||
GpsInfo gps;
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSVersionID, out gps.VersionId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSStatus, out gps.Status)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSDateStamp, out gps.Datestamp)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSTimestamp, out gps.Timestamp)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSLatitude, out gps.Latitude)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSLatitudeRef, out gps.LatitudeRef)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSLongitude, out gps.Longitude)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSLongitudeRef, out gps.LongitudeRef)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSAltitude, out gps.Altitude)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Parse(exif, ExifTag.GPSAltitudeRef, out gps.AltitudeRef)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return gps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: use this Parse() function in Photo.ParseExif() as well?
|
|
||||||
private static bool Parse<T>(ExifProfile exif, ExifTag<T> tag, out T result) {
|
|
||||||
IExifValue<T>? data;
|
|
||||||
if (exif.TryGetValue(tag, out data)) {
|
|
||||||
if (data != null && data.Value != null) {
|
|
||||||
result = data.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#pragma warning disable CS8601
|
|
||||||
result = default(T);
|
|
||||||
#pragma warning restore CS8601
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 string ExposureBiasValue = "<unk>";
|
|
||||||
public int Rating = 0;
|
|
||||||
public ushort Orientation = 1;
|
|
||||||
public GpsInfo? Gps = null;
|
|
||||||
public Rectangle CropRectangle = Rectangle.Empty;
|
|
||||||
public Vector2i ViewOffset = Vector2i.Zero;
|
|
||||||
public int RotationDegreeHundredths = 0;
|
|
||||||
|
|
||||||
private static long touchCounter = 0;
|
|
||||||
private Texture texture;
|
|
||||||
private Texture placeholder;
|
|
||||||
private Texture thumbnailTexture;
|
|
||||||
private Image<Rgba32>? image = null;
|
|
||||||
private Image<Rgba32>? thumbnail = null;
|
|
||||||
|
|
||||||
public Photo(string filename, Texture placeholder) {
|
|
||||||
Filename = filename;
|
|
||||||
this.placeholder = placeholder;
|
|
||||||
texture = placeholder;
|
|
||||||
thumbnailTexture = 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(Vector2i size) {
|
|
||||||
// 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++;
|
|
||||||
// FIXME: if we zoom in to more than the display size, actually load the whole image?
|
|
||||||
DecoderOptions options = new DecoderOptions {
|
|
||||||
TargetSize = new Size(size.X, size.Y),
|
|
||||||
SkipMetadata = true
|
|
||||||
};
|
|
||||||
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(options, Filename);
|
|
||||||
Util.RotateImageFromExif(tmp, Orientation);
|
|
||||||
image = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void LoadThumbnailAsync(Vector2i size) {
|
|
||||||
DecoderOptions options = new DecoderOptions {
|
|
||||||
TargetSize = new Size(size.X, size.Y),
|
|
||||||
SkipMetadata = true
|
|
||||||
};
|
|
||||||
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(options, Filename);
|
|
||||||
Util.RotateImageFromExif(tmp, Orientation);
|
|
||||||
thumbnail = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unload() {
|
|
||||||
Loaded = false;
|
|
||||||
if (texture != placeholder) {
|
|
||||||
texture.Dispose();
|
|
||||||
texture = placeholder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void SaveAsync(string outputRoot, Toast toast) {
|
|
||||||
// FIXME: if nothing was changed about this image, just copy the file bytes directly, possibly with metadata changed?
|
|
||||||
string directory = Path.Combine(
|
|
||||||
outputRoot,
|
|
||||||
String.Format("{0:D4}", DateTimeOriginal.Year),
|
|
||||||
String.Format("{0:D2}", DateTimeOriginal.Month),
|
|
||||||
String.Format("{0:D2}", DateTimeOriginal.Day));
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
Directory.CreateDirectory(Path.Combine(directory, "jpg"));
|
|
||||||
string baseFilename = Path.GetFileName(Filename);
|
|
||||||
|
|
||||||
string rawFilename = Path.ChangeExtension(Filename, "cr3");
|
|
||||||
// FIXME: if there's a JPG but not a RAW, make a "RAW" that's the
|
|
||||||
// camera's original JPG converted to something lossless (PNG? TIFF?).
|
|
||||||
if (Path.Exists(rawFilename)) {
|
|
||||||
string rawOut = Path.Combine(directory, Path.GetFileName(rawFilename));
|
|
||||||
if (!Path.Exists(rawOut)) {
|
|
||||||
System.IO.File.Copy(rawFilename, rawOut);
|
|
||||||
toast.Set($"{rawFilename} => {rawOut}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (Image<Rgba32> image = await Image.LoadAsync<Rgba32>(Filename)) {
|
|
||||||
Util.RotateImageFromExif(image, Orientation);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (Gps != null) {
|
|
||||||
GpsInfo gps = (GpsInfo) Gps;
|
|
||||||
exif.SetValue<byte[]>(ExifTag.GPSVersionID, gps.VersionId);
|
|
||||||
exif.SetValue<string>(ExifTag.GPSStatus, gps.Status);
|
|
||||||
exif.SetValue<string>(ExifTag.GPSDateStamp, gps.Datestamp);
|
|
||||||
exif.SetValue<Rational[]>(ExifTag.GPSTimestamp, gps.Timestamp);
|
|
||||||
exif.SetValue<Rational[]>(ExifTag.GPSLatitude, gps.Latitude);
|
|
||||||
exif.SetValue<string>(ExifTag.GPSLatitudeRef, gps.LatitudeRef);
|
|
||||||
exif.SetValue<Rational[]>(ExifTag.GPSLongitude, gps.Longitude);
|
|
||||||
exif.SetValue<string>(ExifTag.GPSLongitudeRef, gps.LongitudeRef);
|
|
||||||
exif.SetValue<Rational>(ExifTag.GPSAltitude, gps.Altitude);
|
|
||||||
exif.SetValue<byte>(ExifTag.GPSAltitudeRef, gps.AltitudeRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
image.Metadata.XmpProfile = UpdateXmp(image.Metadata.XmpProfile);
|
|
||||||
|
|
||||||
if (RotationDegreeHundredths != 0) {
|
|
||||||
image.Mutate(x => x.Rotate(RotationDegreeHundredths / 100f));
|
|
||||||
}
|
|
||||||
if (CropRectangle != Rectangle.Empty) {
|
|
||||||
image.Mutate(x => x.Crop(CropRectangle));
|
|
||||||
}
|
|
||||||
|
|
||||||
string jpgOut = Path.Combine(directory, "jpg", baseFilename);
|
|
||||||
await image.SaveAsync(jpgOut, new JpegEncoder() { Quality = 100 });
|
|
||||||
toast.Set($"{Filename} => {jpgOut}");
|
|
||||||
|
|
||||||
// await image.SaveAsync(editOut, new PngEncoder() {
|
|
||||||
// BitDepth = PngBitDepth.Bit8, ChunkFilter = PngChunkFilter.None, ColorType = PngColorType.Rgb,
|
|
||||||
// CompressionLevel = PngCompressionLevel.BestCompression, FilterMethod = PngFilterMethod.Adaptive,
|
|
||||||
// InterlaceMethod = PngInterlaceMode.None });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.Orientation, out var orientation)) {
|
|
||||||
Orientation = orientation.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.Model, out var model)) {
|
|
||||||
CameraModel = model.Value ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.LensModel, out var lensModel)) {
|
|
||||||
LensModel = lensModel.Value ?? "";
|
|
||||||
ShortLensModel = GetShortLensModel(LensModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.FocalLength, out var focalLength)) {
|
|
||||||
Rational r = focalLength.Value;
|
|
||||||
FocalLength = $"{r.Numerator / r.Denominator}mm";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.FNumber, out var 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 ExposureProgram.
|
|
||||||
if (exifs.TryGetValue(ExifTag.ExposureTime, out var 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.ISOSpeedRatings, out var 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: I think the iPhone stores time in UTC but other cameras report it in local time.
|
|
||||||
if (exifs.TryGetValue(ExifTag.DateTimeOriginal, out var 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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.SubsecTimeOriginal, out var subsecTimeOriginal)) {
|
|
||||||
double fractionalSeconds;
|
|
||||||
Double.TryParse("0." + subsecTimeOriginal.Value, out fractionalSeconds);
|
|
||||||
DateTimeOriginal = DateTimeOriginal.AddSeconds(fractionalSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exifs.TryGetValue(ExifTag.ExposureBiasValue, out var exposureBiasValue)) {
|
|
||||||
SignedRational r = exposureBiasValue.Value;
|
|
||||||
ExposureBiasValue = r.ToString();
|
|
||||||
if (r.Numerator >= 0) {
|
|
||||||
ExposureBiasValue = "+" + ExposureBiasValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Gps = GpsInfo.ParseExif(exifs);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(image);
|
|
||||||
image.Dispose();
|
|
||||||
image = null;
|
|
||||||
Loaded = true;
|
|
||||||
}
|
|
||||||
return texture != placeholder ? texture : thumbnailTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Texture ThumbnailTexture() {
|
|
||||||
if (thumbnailTexture == placeholder && thumbnail != null) {
|
|
||||||
thumbnailTexture = new(thumbnail);
|
|
||||||
thumbnail.Dispose();
|
|
||||||
thumbnail = null;
|
|
||||||
}
|
|
||||||
return thumbnailTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description() {
|
|
||||||
string date = DateTimeOriginal.ToString("yyyy-MM-dd HH:mm:ss.ff");
|
|
||||||
return String.Format(
|
|
||||||
"{0,6} {1,-5} {2,-7} {3,-10} EV {9,-4} {7,4}x{8,-4} {4} {5,-20} {6}",
|
|
||||||
FocalLength, FNumber, ExposureTime, IsoSpeed, date, ShortLensModel, Filename, Size.X, Size.Y, ExposureBiasValue);
|
|
||||||
}
|
|
||||||
}
|
|
1569
Program.cs
1569
Program.cs
File diff suppressed because it is too large
Load Diff
118
Shader.cs
118
Shader.cs
@ -1,118 +0,0 @@
|
|||||||
using OpenTK.Graphics.OpenGL4;
|
|
||||||
|
|
||||||
namespace SemiColinGames;
|
|
||||||
|
|
||||||
public class Shader : IDisposable {
|
|
||||||
public int Handle;
|
|
||||||
private bool init = false;
|
|
||||||
|
|
||||||
public Shader() {}
|
|
||||||
|
|
||||||
public void Init() {
|
|
||||||
init = true;
|
|
||||||
int VertexShader;
|
|
||||||
int FragmentShader;
|
|
||||||
|
|
||||||
string VertexShaderSource = @"
|
|
||||||
#version 330
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 aPosition;
|
|
||||||
layout(location = 1) in vec2 aTexCoord;
|
|
||||||
|
|
||||||
out vec2 texCoord;
|
|
||||||
|
|
||||||
uniform mat4 projection;
|
|
||||||
|
|
||||||
void main(void) {
|
|
||||||
texCoord = aTexCoord;
|
|
||||||
gl_Position = vec4(aPosition, 1.0) * projection;
|
|
||||||
}";
|
|
||||||
|
|
||||||
string FragmentShaderSource = @"
|
|
||||||
#version 330
|
|
||||||
|
|
||||||
out vec4 outputColor;
|
|
||||||
in vec2 texCoord;
|
|
||||||
uniform sampler2D texture0;
|
|
||||||
uniform vec4 color;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
outputColor = texture(texture0, texCoord) * color;
|
|
||||||
}";
|
|
||||||
|
|
||||||
VertexShader = GL.CreateShader(ShaderType.VertexShader);
|
|
||||||
GL.ShaderSource(VertexShader, VertexShaderSource);
|
|
||||||
|
|
||||||
FragmentShader = GL.CreateShader(ShaderType.FragmentShader);
|
|
||||||
GL.ShaderSource(FragmentShader, FragmentShaderSource);
|
|
||||||
|
|
||||||
GL.CompileShader(VertexShader);
|
|
||||||
|
|
||||||
int success;
|
|
||||||
GL.GetShader(VertexShader, ShaderParameter.CompileStatus, out success);
|
|
||||||
if (success == 0) {
|
|
||||||
string infoLog = GL.GetShaderInfoLog(VertexShader);
|
|
||||||
Console.WriteLine(infoLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
GL.CompileShader(FragmentShader);
|
|
||||||
|
|
||||||
GL.GetShader(FragmentShader, ShaderParameter.CompileStatus, out success);
|
|
||||||
if (success == 0) {
|
|
||||||
string infoLog = GL.GetShaderInfoLog(FragmentShader);
|
|
||||||
Console.WriteLine(infoLog);
|
|
||||||
}
|
|
||||||
Handle = GL.CreateProgram();
|
|
||||||
|
|
||||||
GL.AttachShader(Handle, VertexShader);
|
|
||||||
GL.AttachShader(Handle, FragmentShader);
|
|
||||||
|
|
||||||
GL.LinkProgram(Handle);
|
|
||||||
|
|
||||||
GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out success);
|
|
||||||
if (success == 0) {
|
|
||||||
string infoLog = GL.GetProgramInfoLog(Handle);
|
|
||||||
Console.WriteLine(infoLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
GL.DetachShader(Handle, VertexShader);
|
|
||||||
GL.DetachShader(Handle, FragmentShader);
|
|
||||||
GL.DeleteShader(FragmentShader);
|
|
||||||
GL.DeleteShader(VertexShader);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Use() {
|
|
||||||
if (!init) {
|
|
||||||
Console.WriteLine("Shader.Use(): must call Init() first");
|
|
||||||
}
|
|
||||||
GL.UseProgram(Handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool disposedValue = false;
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing) {
|
|
||||||
if (!disposedValue) {
|
|
||||||
GL.DeleteProgram(Handle);
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Shader() {
|
|
||||||
if (disposedValue == false) {
|
|
||||||
Console.WriteLine("~Shader(): resource leak? Dispose() should be called manually.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetAttribLocation(string name) {
|
|
||||||
return GL.GetAttribLocation(Handle, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetUniformLocation(string name) {
|
|
||||||
return GL.GetUniformLocation(Handle, name);
|
|
||||||
}
|
|
||||||
}
|
|
55
Texture.cs
55
Texture.cs
@ -1,55 +0,0 @@
|
|||||||
using OpenTK.Graphics.OpenGL4;
|
|
||||||
using OpenTK.Mathematics;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace SemiColinGames;
|
|
||||||
|
|
||||||
public class Texture : IDisposable {
|
|
||||||
public int Handle;
|
|
||||||
public Vector2i Size;
|
|
||||||
|
|
||||||
private static int maxHandle = -1;
|
|
||||||
private bool disposedValue = false;
|
|
||||||
|
|
||||||
public Texture(Image<Rgba32> image) {
|
|
||||||
Size = new Vector2i(image.Width, image.Height);
|
|
||||||
byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()];
|
|
||||||
image.CopyPixelDataTo(pixelBytes);
|
|
||||||
|
|
||||||
Handle = GL.GenTexture();
|
|
||||||
if (Handle > maxHandle) {
|
|
||||||
// Console.WriteLine("GL.GenTexture #" + Handle);
|
|
||||||
maxHandle = Handle;
|
|
||||||
}
|
|
||||||
GL.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
GL.BindTexture(TextureTarget.Texture2D, Handle);
|
|
||||||
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, Size.X, Size.Y, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelBytes);
|
|
||||||
//GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
|
|
||||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
|
|
||||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Nearest);
|
|
||||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) TextureWrapMode.ClampToBorder);
|
|
||||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) TextureWrapMode.ClampToBorder);
|
|
||||||
float[] borderColor = { 0.0f, 0.0f, 0.0f, 1.0f };
|
|
||||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBorderColor, borderColor);
|
|
||||||
// FIXME: should we use mipmaps?
|
|
||||||
//GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing) {
|
|
||||||
if (!disposedValue) {
|
|
||||||
GL.DeleteTexture(Handle);
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Texture() {
|
|
||||||
if (!disposedValue) {
|
|
||||||
Console.WriteLine("~Texture(): resource leak? Dispose() should be called manually.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,16 +2,14 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="OpenTK" Version="4.7.7" />
|
<PackageReference Include="OpenTK" Version="4.7.7" />
|
||||||
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta15" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
revision=r$(git log | egrep "^commit [0-9a-f]{40}$" | wc -l)
|
|
||||||
mkdir totte-$revision && cp -r bin/Debug/net7.0/* totte-$revision/ && zip -r totte-$revision.zip totte-$revision/ && rm -rf totte-$revision
|
|
Loading…
Reference in New Issue
Block a user