2023-06-30 02:10:24 +00:00
|
|
|
|
using OpenTK.Graphics.OpenGL4;
|
|
|
|
|
using OpenTK.Mathematics;
|
|
|
|
|
using OpenTK.Windowing.Common;
|
2023-07-26 14:00:46 +00:00
|
|
|
|
using OpenTK.Windowing.Common.Input;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
using OpenTK.Windowing.Desktop;
|
|
|
|
|
using OpenTK.Windowing.GraphicsLibraryFramework;
|
2023-07-08 03:41:32 +00:00
|
|
|
|
// https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Image.html
|
2023-06-30 02:10:24 +00:00
|
|
|
|
using Image = SixLabors.ImageSharp.Image;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using SixLabors.Fonts;
|
2023-07-16 23:13:08 +00:00
|
|
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using SixLabors.ImageSharp.Drawing.Processing;
|
|
|
|
|
using SixLabors.ImageSharp.Drawing;
|
2023-07-28 19:52:36 +00:00
|
|
|
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
2023-07-26 02:19:18 +00:00
|
|
|
|
using System;
|
2023-07-26 01:44:22 +00:00
|
|
|
|
using System.Diagnostics;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using System.Runtime.CompilerServices;
|
2023-08-01 00:50:53 +00:00
|
|
|
|
using System.Text;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
using System.Xml.Linq;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
namespace SemiColinGames;
|
|
|
|
|
|
2023-07-26 02:19:18 +00:00
|
|
|
|
public class FpsCounter {
|
2023-07-26 15:25:14 +00:00
|
|
|
|
private readonly int[] frameTimes = new int[30];
|
2023-07-26 02:19:18 +00:00
|
|
|
|
private double fps = 0;
|
|
|
|
|
private int idx = 0;
|
|
|
|
|
|
|
|
|
|
public int Fps {
|
|
|
|
|
get => (int) Math.Ceiling(fps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Update() {
|
|
|
|
|
var now = Environment.TickCount; // ms
|
|
|
|
|
if (frameTimes[idx] != 0) {
|
|
|
|
|
var timeElapsed = now - frameTimes[idx];
|
|
|
|
|
fps = 1000.0 * frameTimes.Length / timeElapsed;
|
|
|
|
|
}
|
|
|
|
|
frameTimes[idx] = now;
|
|
|
|
|
idx = (idx + 1) % frameTimes.Length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-06 22:31:50 +00:00
|
|
|
|
public class CameraInfo {
|
|
|
|
|
public readonly Vector2i Resolution;
|
|
|
|
|
|
|
|
|
|
private CameraInfo(Vector2i resolution) {
|
|
|
|
|
Resolution = resolution;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-08 04:33:15 +00:00
|
|
|
|
public static readonly CameraInfo NIKON_D7000 = new(new Vector2i(4928, 3264));
|
2023-07-16 23:13:08 +00:00
|
|
|
|
public static readonly CameraInfo CANON_EOS_R6M2 = new(new Vector2i(6000, 4000));
|
2023-07-08 04:33:15 +00:00
|
|
|
|
public static readonly CameraInfo IPHONE_12_MINI = new(new Vector2i(4032, 3024));
|
2023-07-06 22:31:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:38:21 +00:00
|
|
|
|
public enum ToolState {
|
|
|
|
|
Active,
|
|
|
|
|
Done,
|
|
|
|
|
Canceled
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:14:19 +00:00
|
|
|
|
public interface ITool {
|
|
|
|
|
void SetActivePhoto(Photo photo);
|
2023-08-03 22:38:21 +00:00
|
|
|
|
ToolState HandleInput(UiGeometry geometry, KeyboardState input, MouseState mouse, Game game);
|
2023-08-03 22:14:19 +00:00
|
|
|
|
string Status();
|
|
|
|
|
void Draw(UiGeometry geometry, Game game);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ViewTool : ITool {
|
|
|
|
|
Photo? activePhoto;
|
|
|
|
|
|
|
|
|
|
public void SetActivePhoto(Photo photo) {
|
|
|
|
|
activePhoto = photo;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:38:21 +00:00
|
|
|
|
public ToolState HandleInput(UiGeometry geometry, KeyboardState input, MouseState mouse, Game game) {
|
|
|
|
|
return ToolState.Active;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Status() {
|
2023-08-04 00:03:46 +00:00
|
|
|
|
return "";
|
2023-08-03 22:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Draw(UiGeometry geometry, Game game) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:38:21 +00:00
|
|
|
|
|
2023-08-03 22:14:19 +00:00
|
|
|
|
// FIXME: remove unneeded dependencies on "Game" or at least refactor them a bit.
|
|
|
|
|
public class CropTool : ITool {
|
|
|
|
|
|
2023-08-04 00:03:46 +00:00
|
|
|
|
Photo activePhoto;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
Vector2i mouseDragStart;
|
|
|
|
|
Vector2i mouseDragEnd;
|
2023-08-03 22:38:21 +00:00
|
|
|
|
string status = "";
|
2023-08-03 22:14:19 +00:00
|
|
|
|
|
2023-08-04 00:03:46 +00:00
|
|
|
|
public CropTool(Photo photo) {
|
|
|
|
|
activePhoto = photo;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:14:19 +00:00
|
|
|
|
public void SetActivePhoto(Photo photo) {
|
2023-08-04 00:03:46 +00:00
|
|
|
|
if (photo != activePhoto) {
|
|
|
|
|
// FIXME: handle this sensibly.
|
|
|
|
|
}
|
2023-08-03 22:14:19 +00:00
|
|
|
|
activePhoto = photo;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:38:21 +00:00
|
|
|
|
public ToolState HandleInput(UiGeometry geometry, KeyboardState input, MouseState mouse, Game game) {
|
2023-08-03 22:14:19 +00:00
|
|
|
|
Vector2i mousePosition = (Vector2i) mouse.Position;
|
|
|
|
|
|
|
|
|
|
if (mouse.IsButtonPressed(MouseButton.Button1)) {
|
|
|
|
|
if (geometry.PhotoBox.ContainsInclusive(mousePosition)) {
|
|
|
|
|
mouseDragStart = mousePosition;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mouse.IsButtonDown(MouseButton.Button1)) {
|
|
|
|
|
if (geometry.PhotoBox.ContainsInclusive(mousePosition)) {
|
|
|
|
|
// FIXME: really this should be clipped to the active photo's drawable area, not the whole photobox.
|
|
|
|
|
mouseDragEnd = mousePosition;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 22:38:21 +00:00
|
|
|
|
Vector2i start = game.ScreenToImage(mouseDragStart);
|
|
|
|
|
// FIXME: this needs to be the actual width of the computed box.
|
|
|
|
|
Vector2i size = game.ScreenToImage(mouseDragEnd) - start;
|
|
|
|
|
|
|
|
|
|
status = $"({start.X}, {start.Y}) {size.X}x{size.Y}";
|
2023-08-03 22:14:19 +00:00
|
|
|
|
|
|
|
|
|
if (input.IsKeyPressed(Keys.Enter)) {
|
|
|
|
|
ApplyCrop(game);
|
2023-08-03 22:38:21 +00:00
|
|
|
|
return ToolState.Done;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.IsKeyPressed(Keys.Escape)) {
|
|
|
|
|
activePhoto.CropRectangle = Rectangle.Empty;
|
|
|
|
|
return ToolState.Canceled;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
}
|
2023-08-03 22:38:21 +00:00
|
|
|
|
|
|
|
|
|
return ToolState.Active;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// left, right, top, bottom
|
|
|
|
|
(int, int, int, int) GetCrop() {
|
|
|
|
|
// FIXME: this expects the start point in the top left and the end point
|
|
|
|
|
// in the bottom right; some sign flipping needs to occur to make anchors
|
|
|
|
|
// in other direction work well.
|
|
|
|
|
Vector2i start = mouseDragStart;
|
|
|
|
|
Vector2i end = mouseDragEnd;
|
2023-08-04 00:03:46 +00:00
|
|
|
|
// FIXME: choose the aspect ratio based on the original image aspect ratio.
|
|
|
|
|
// FIXME: allow for unconstrained crop, 1:1, etc.
|
2023-08-03 22:14:19 +00:00
|
|
|
|
end.Y = Math.Min(end.Y, start.Y + (end.X - start.X) * 4 / 6);
|
|
|
|
|
end.X = start.X + (end.Y - start.Y) * 6 / 4;
|
|
|
|
|
int left = Math.Min(start.X, end.X);
|
|
|
|
|
int right = Math.Max(start.X, end.X);
|
|
|
|
|
int top = Math.Min(start.Y, end.Y);
|
|
|
|
|
int bottom = Math.Max(start.Y, end.Y);
|
|
|
|
|
return (left, right, top, bottom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ApplyCrop(Game game) {
|
|
|
|
|
var (left, right, top, bottom) = GetCrop();
|
|
|
|
|
int area = (right - left) * (bottom - top);
|
|
|
|
|
if (area == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector2i leftTop = game.ScreenToImage(left, top);
|
|
|
|
|
Vector2i rightBottom = game.ScreenToImage(right, bottom);
|
|
|
|
|
Rectangle crop = Rectangle.FromLTRB(leftTop.X, leftTop.Y, rightBottom.X, rightBottom.Y);
|
|
|
|
|
// FIXME: make sure this doesn't exceed image.Bounds.
|
|
|
|
|
// FIXME: once set, display it properly in the PhotoBox.
|
2023-08-04 00:03:46 +00:00
|
|
|
|
activePhoto.CropRectangle = crop;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
Console.WriteLine(crop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Draw(UiGeometry geometry, Game game) {
|
|
|
|
|
var (left, right, top, bottom) = GetCrop();
|
|
|
|
|
int area = (right - left) * (bottom - top);
|
|
|
|
|
|
|
|
|
|
if (area == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Color4 shadeColor = new Color4(0, 0, 0, 0.75f);
|
|
|
|
|
game.DrawFilledBox(new Box2i(0, 0, left, geometry.PhotoBox.Max.Y), shadeColor);
|
|
|
|
|
game.DrawFilledBox(new Box2i(left, 0, geometry.PhotoBox.Max.X, top), shadeColor);
|
|
|
|
|
game.DrawFilledBox(new Box2i(left, bottom, geometry.PhotoBox.Max.X, geometry.PhotoBox.Max.Y), shadeColor);
|
|
|
|
|
game.DrawFilledBox(new Box2i(right, top, geometry.PhotoBox.Max.X, bottom), shadeColor);
|
|
|
|
|
game.DrawBox(new Box2i(left, top, right, bottom), 1, Color4.White);
|
|
|
|
|
game.DrawBox(new Box2i(left - 1, top - 1 , right + 1, bottom + 1), 1, Color4.Black);
|
|
|
|
|
game.DrawBox(new Box2i(left - 2, top - 2 , right + 2, bottom + 2), 1, Color4.White);
|
|
|
|
|
game.DrawHorizontalLine(left, Util.Lerp(top, bottom, 1.0 / 3), right, Color4.White);
|
|
|
|
|
game.DrawHorizontalLine(left, Util.Lerp(top, bottom, 2.0 / 3), right, Color4.White);
|
|
|
|
|
game.DrawVerticalLine(Util.Lerp(left, right, 1.0 / 3), top, bottom, Color4.White);
|
|
|
|
|
game.DrawVerticalLine(Util.Lerp(left, right, 2.0 / 3), top, bottom, Color4.White);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Status() {
|
2023-08-03 22:38:21 +00:00
|
|
|
|
return "[crop] " + status;
|
2023-08-03 22:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-17 06:44:34 +00:00
|
|
|
|
// FIXME: this should probably be IDisposable?
|
2023-07-16 23:23:03 +00:00
|
|
|
|
public class Photo {
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public string Filename;
|
2023-07-24 20:07:17 +00:00
|
|
|
|
public bool Loaded = false;
|
2023-07-27 01:29:26 +00:00
|
|
|
|
public long LastTouch = 0;
|
2023-07-25 20:58:41 +00:00
|
|
|
|
public Vector2i Size;
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public DateTime DateTimeOriginal;
|
2023-07-24 22:55:59 +00:00
|
|
|
|
public string CameraModel = "";
|
|
|
|
|
public string LensModel = "";
|
2023-08-03 02:29:04 +00:00
|
|
|
|
public string ShortLensModel = "";
|
2023-07-24 20:39:01 +00:00
|
|
|
|
public string FocalLength = "<unk>";
|
2023-07-24 20:07:17 +00:00
|
|
|
|
public string FNumber = "<unk>";
|
|
|
|
|
public string ExposureTime = "<unk>";
|
|
|
|
|
public string IsoSpeed = "<unk>";
|
2023-07-25 13:41:13 +00:00
|
|
|
|
public int Rating = 0;
|
2023-07-25 20:43:11 +00:00
|
|
|
|
public ushort Orientation = 1;
|
2023-08-02 05:05:10 +00:00
|
|
|
|
public Rectangle CropRectangle = Rectangle.Empty;
|
2023-07-24 20:07:17 +00:00
|
|
|
|
|
2023-07-27 01:29:26 +00:00
|
|
|
|
private static long touchCounter = 0;
|
2023-07-17 03:45:50 +00:00
|
|
|
|
private Texture texture;
|
|
|
|
|
private Texture placeholder;
|
2023-07-17 06:44:34 +00:00
|
|
|
|
private Image<Rgba32>? image = null;
|
2023-07-16 23:23:03 +00:00
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public Photo(string filename, Texture placeholder) {
|
|
|
|
|
Filename = filename;
|
2023-07-17 03:45:50 +00:00
|
|
|
|
this.placeholder = placeholder;
|
|
|
|
|
texture = placeholder;
|
2023-07-25 20:58:41 +00:00
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
DateTime creationTime = File.GetCreationTime(filename); // Local time.
|
|
|
|
|
DateTimeOriginal = creationTime;
|
|
|
|
|
ImageInfo info = Image.Identify(filename);
|
2023-07-25 20:58:41 +00:00
|
|
|
|
Size = new(info.Size.Width, info.Size.Height);
|
2023-08-01 00:50:53 +00:00
|
|
|
|
Rating = ParseRating(info.Metadata.XmpProfile);
|
2023-07-25 20:58:41 +00:00
|
|
|
|
ParseExif(info.Metadata.ExifProfile);
|
2023-07-17 06:44:34 +00:00
|
|
|
|
}
|
2023-06-30 02:21:41 +00:00
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
public async void LoadAsync() {
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// 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.
|
2023-07-27 01:29:26 +00:00
|
|
|
|
LastTouch = touchCounter++;
|
2023-07-26 00:25:07 +00:00
|
|
|
|
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
|
2023-07-25 21:10:51 +00:00
|
|
|
|
Util.RotateImageFromExif(tmp, Orientation);
|
|
|
|
|
image = tmp;
|
2023-07-17 00:06:37 +00:00
|
|
|
|
}
|
2023-07-17 03:45:50 +00:00
|
|
|
|
|
2023-07-28 16:27:53 +00:00
|
|
|
|
public void Unload() {
|
2023-07-26 02:00:43 +00:00
|
|
|
|
Loaded = false;
|
2023-07-26 01:24:20 +00:00
|
|
|
|
if (texture != placeholder) {
|
2023-07-28 16:19:15 +00:00
|
|
|
|
texture.Dispose();
|
2023-07-26 01:24:20 +00:00
|
|
|
|
texture = placeholder;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 00:50:53 +00:00
|
|
|
|
public async void SaveAsJpegAsync(string outputRoot, JpegEncoder encoder) {
|
2023-07-28 20:00:08 +00:00
|
|
|
|
// FIXME: if nothing was changed about this image, just copy the file bytes directly, possibly with metadata changed?
|
2023-07-28 19:52:36 +00:00
|
|
|
|
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);
|
2023-07-28 21:54:00 +00:00
|
|
|
|
// FIXME: add comments / captions as ImageDescription?
|
|
|
|
|
// FIXME: strip some Exif tags for privacy reasons?
|
2023-07-28 20:28:18 +00:00
|
|
|
|
// FIXME: warn if the file already exists?
|
2023-07-28 20:32:54 +00:00
|
|
|
|
using (Image<Rgba32> image = await Image.LoadAsync<Rgba32>(Filename)) {
|
2023-07-28 21:54:00 +00:00
|
|
|
|
Util.RotateImageFromExif(image, Orientation);
|
2023-08-02 05:05:10 +00:00
|
|
|
|
if (CropRectangle != Rectangle.Empty) {
|
|
|
|
|
image.Mutate(x => x.Crop(CropRectangle));
|
|
|
|
|
}
|
2023-07-28 21:54:00 +00:00
|
|
|
|
|
|
|
|
|
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");
|
2023-08-01 15:30:06 +00:00
|
|
|
|
exif.SetValue<ushort>(ExifTag.Rating, (ushort) Rating);
|
2023-07-28 21:54:00 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2023-08-01 00:50:53 +00:00
|
|
|
|
image.Metadata.XmpProfile = UpdateXmp(image.Metadata.XmpProfile);
|
|
|
|
|
|
2023-07-28 20:28:18 +00:00
|
|
|
|
await image.SaveAsync(filename, encoder);
|
2023-07-28 19:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 00:50:53 +00:00
|
|
|
|
private XElement? GetXmpRoot(XmpProfile? xmp) {
|
2023-07-25 13:41:13 +00:00
|
|
|
|
if (xmp == null) {
|
2023-08-01 00:50:53 +00:00
|
|
|
|
return null;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
XDocument? doc = xmp.GetDocument();
|
|
|
|
|
if (doc == null) {
|
2023-08-01 00:50:53 +00:00
|
|
|
|
return null;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
2023-08-01 00:50:53 +00:00
|
|
|
|
return doc.Root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int ParseRating(XmpProfile? xmp) {
|
|
|
|
|
XElement? root = GetXmpRoot(xmp);
|
2023-07-25 13:41:13 +00:00
|
|
|
|
if (root == null) {
|
2023-08-01 00:50:53 +00:00
|
|
|
|
return 0;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
foreach (XElement elt in root.Descendants()) {
|
|
|
|
|
if (elt.Name == "{http://ns.adobe.com/xap/1.0/}Rating") {
|
2023-08-01 00:50:53 +00:00
|
|
|
|
int rating = 0;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
if (int.TryParse(elt.Value, out rating)) {
|
2023-08-01 00:50:53 +00:00
|
|
|
|
return rating;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-01 00:50:53 +00:00
|
|
|
|
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));
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
// 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)
|
2023-07-28 21:54:00 +00:00
|
|
|
|
//
|
|
|
|
|
// See also: https://exiftool.org/TagNames/EXIF.html
|
2023-07-25 20:43:11 +00:00
|
|
|
|
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 ?? "";
|
2023-08-03 02:29:04 +00:00
|
|
|
|
ShortLensModel = GetShortLensModel(LensModel);
|
2023-07-25 20:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2023-07-26 19:09:19 +00:00
|
|
|
|
if (r.Numerator % r.Denominator == 0) {
|
|
|
|
|
FNumber = $"f/{r.Numerator / r.Denominator}";
|
2023-07-25 20:43:11 +00:00
|
|
|
|
} else {
|
2023-07-26 19:09:19 +00:00
|
|
|
|
int fTimesTen = (int) Math.Round(10f * r.Numerator / r.Denominator);
|
|
|
|
|
FNumber = $"f/{fTimesTen / 10}.{fTimesTen % 10}";
|
2023-07-25 20:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 21:54:00 +00:00
|
|
|
|
// FIXME: could also show ExposureBiasValue, ExposureMode, ExposureProgram?
|
2023-07-25 20:43:11 +00:00
|
|
|
|
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]}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-26 00:25:07 +00:00
|
|
|
|
|
2023-07-28 16:21:38 +00:00
|
|
|
|
// FIXME: there is also a SubSecTimeOriginal tag we could use to get fractional seconds.
|
2023-07-26 00:25:07 +00:00
|
|
|
|
// 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}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 20:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 02:29:04 +00:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-17 03:45:50 +00:00
|
|
|
|
public Texture Texture() {
|
2023-07-27 01:29:26 +00:00
|
|
|
|
LastTouch = touchCounter++;
|
2023-07-23 23:29:47 +00:00
|
|
|
|
if (texture == placeholder && image != null) {
|
2023-07-25 22:26:46 +00:00
|
|
|
|
// 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.
|
2023-07-23 23:29:47 +00:00
|
|
|
|
texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
image = null;
|
2023-07-24 16:36:44 +00:00
|
|
|
|
Loaded = true;
|
2023-07-23 23:29:47 +00:00
|
|
|
|
}
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
2023-07-24 20:07:17 +00:00
|
|
|
|
|
|
|
|
|
public string Description() {
|
2023-07-26 00:35:12 +00:00
|
|
|
|
string date = DateTimeOriginal.ToString("yyyy-MM-dd HH:mm:ss");
|
2023-08-03 02:29:04 +00:00
|
|
|
|
return String.Format(
|
|
|
|
|
"{0,6} {1,-5} {2,-7} {3,-10} {4} {5,-20} {6}",
|
|
|
|
|
FocalLength, FNumber, ExposureTime, IsoSpeed, date, ShortLensModel, Filename);
|
2023-07-24 20:07:17 +00:00
|
|
|
|
}
|
2023-07-17 00:06:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class Texture : IDisposable {
|
|
|
|
|
public int Handle;
|
|
|
|
|
public Vector2i Size;
|
|
|
|
|
|
2023-07-27 01:29:26 +00:00
|
|
|
|
private static int maxHandle = -1;
|
|
|
|
|
private bool disposedValue = false;
|
|
|
|
|
|
2023-07-17 00:06:37 +00:00
|
|
|
|
public Texture(Image<Rgba32> image) {
|
|
|
|
|
Size = new Vector2i(image.Width, image.Height);
|
2023-07-08 03:41:32 +00:00
|
|
|
|
byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()];
|
2023-06-30 02:10:24 +00:00
|
|
|
|
image.CopyPixelDataTo(pixelBytes);
|
|
|
|
|
|
|
|
|
|
Handle = GL.GenTexture();
|
2023-07-24 16:13:13 +00:00
|
|
|
|
if (Handle > maxHandle) {
|
2023-07-26 01:44:22 +00:00
|
|
|
|
// Console.WriteLine("GL.GenTexture #" + Handle);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
maxHandle = Handle;
|
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
|
|
|
|
GL.BindTexture(TextureTarget.Texture2D, Handle);
|
2023-07-08 03:41:32 +00:00
|
|
|
|
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, Size.X, Size.Y, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelBytes);
|
2023-07-17 06:51:15 +00:00
|
|
|
|
//GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
|
|
|
|
|
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
2023-07-17 06:51:15 +00:00
|
|
|
|
// FIXME: should we use mipmaps?
|
|
|
|
|
//GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 18:24:43 +00:00
|
|
|
|
public class UiGeometry {
|
2023-07-26 19:09:19 +00:00
|
|
|
|
public static Vector2i MIN_WINDOW_SIZE = new(1024, 768);
|
2023-07-16 23:13:08 +00:00
|
|
|
|
private static CameraInfo activeCamera = CameraInfo.CANON_EOS_R6M2;
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
|
|
|
|
public readonly Vector2i WindowSize;
|
2023-07-25 21:18:11 +00:00
|
|
|
|
public readonly Box2i ThumbnailBox;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
public readonly List<Box2i> ThumbnailBoxes = new();
|
2023-07-26 19:09:19 +00:00
|
|
|
|
public readonly List<Box2i> StarBoxes = new();
|
2023-07-07 20:00:43 +00:00
|
|
|
|
public readonly Box2i PhotoBox;
|
2023-07-24 17:14:07 +00:00
|
|
|
|
public readonly Box2i StatusBox;
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
2023-07-26 19:09:19 +00:00
|
|
|
|
public UiGeometry() : this(MIN_WINDOW_SIZE, 0) {}
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
2023-07-26 19:09:19 +00:00
|
|
|
|
public UiGeometry(Vector2i windowSize, int starSize) {
|
2023-07-07 18:24:43 +00:00
|
|
|
|
WindowSize = windowSize;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
|
2023-07-31 20:44:01 +00:00
|
|
|
|
int numThumbnails = Math.Max(WindowSize.Y / 100, 1);
|
2023-07-08 04:04:45 +00:00
|
|
|
|
int thumbnailHeight = WindowSize.Y / numThumbnails;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
int thumbnailWidth = (int) 1.0 * thumbnailHeight * activeCamera.Resolution.X / activeCamera.Resolution.Y;
|
2023-07-28 18:36:02 +00:00
|
|
|
|
|
|
|
|
|
Console.WriteLine($"thumbnail size: {thumbnailWidth} x {thumbnailHeight}");
|
2023-07-08 04:04:45 +00:00
|
|
|
|
for (int i = 0; i < numThumbnails; i++) {
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Box2i box = Util.MakeBox(WindowSize.X - thumbnailWidth, i * thumbnailHeight, thumbnailWidth, thumbnailHeight);
|
2023-07-07 18:52:36 +00:00
|
|
|
|
ThumbnailBoxes.Add(box);
|
|
|
|
|
}
|
2023-07-07 20:00:43 +00:00
|
|
|
|
|
2023-08-02 05:05:10 +00:00
|
|
|
|
int statusBoxHeight = 40;
|
2023-07-24 17:14:07 +00:00
|
|
|
|
int statusBoxPadding = 4;
|
|
|
|
|
PhotoBox = new Box2i(0, 0, WindowSize.X - thumbnailWidth, WindowSize.Y - statusBoxHeight - statusBoxPadding);
|
|
|
|
|
StatusBox = new Box2i(0, WindowSize.Y - statusBoxHeight, WindowSize.X - thumbnailWidth, WindowSize.Y);
|
2023-07-25 21:18:11 +00:00
|
|
|
|
ThumbnailBox = new Box2i(ThumbnailBoxes[0].Min.X, ThumbnailBoxes[0].Min.Y, WindowSize.X, WindowSize.Y);
|
2023-07-26 19:09:19 +00:00
|
|
|
|
|
|
|
|
|
int starSpacing = 10;
|
|
|
|
|
int starBoxLeft = (int) (PhotoBox.Center.X - 2.5 * starSize - starSpacing * 2);
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
|
Box2i box = Util.MakeBox(starBoxLeft + i * (starSize + starSpacing), PhotoBox.Max.Y - starSize - 10, starSize, starSize);
|
|
|
|
|
StarBoxes.Add(box);
|
|
|
|
|
}
|
2023-07-07 18:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static class Util {
|
2023-07-23 22:42:08 +00:00
|
|
|
|
public const float PI = (float) Math.PI;
|
|
|
|
|
|
2023-08-01 18:12:52 +00:00
|
|
|
|
public static int Lerp(int start, int end, double fraction) {
|
|
|
|
|
return start + (int) ((end - start) * fraction);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Box2i MakeBox(int left, int top, int width, int height) {
|
2023-07-07 18:52:36 +00:00
|
|
|
|
return new Box2i(left, top, left + width, top + height);
|
2023-07-07 18:24:43 +00:00
|
|
|
|
}
|
2023-07-24 16:13:13 +00:00
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Image<Rgba32> MakeImage(float width, float height) {
|
2023-07-24 16:36:44 +00:00
|
|
|
|
return new((int) Math.Ceiling(width), (int) Math.Ceiling(height));
|
2023-07-24 16:13:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
|
|
|
|
public static void RotateImageFromExif(Image<Rgba32> image, ushort orientation) {
|
2023-07-25 18:48:02 +00:00
|
|
|
|
if (orientation <= 1) {
|
2023-07-25 18:28:57 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2023-07-25 19:00:30 +00:00
|
|
|
|
// FIXME: I'm not convinced that all of these are correct, especially the
|
|
|
|
|
// cases that involve flipping (because whether you're flipping before or
|
2023-08-04 00:03:46 +00:00
|
|
|
|
// after rotation matters.)
|
2023-07-25 18:48:02 +00:00
|
|
|
|
var operations = new Dictionary<ushort, (RotateMode, FlipMode)> {
|
|
|
|
|
{ 2, (RotateMode.None, FlipMode.Horizontal) },
|
|
|
|
|
{ 3, (RotateMode.Rotate180, FlipMode.None) },
|
2023-07-25 19:02:40 +00:00
|
|
|
|
{ 4, (RotateMode.None, FlipMode.Vertical) },
|
2023-07-25 18:48:02 +00:00
|
|
|
|
{ 5, (RotateMode.Rotate90, FlipMode.Vertical) },
|
|
|
|
|
{ 6, (RotateMode.Rotate90, FlipMode.None) },
|
|
|
|
|
{ 7, (RotateMode.Rotate270, FlipMode.Vertical) },
|
|
|
|
|
{ 8, (RotateMode.Rotate270, FlipMode.None) },
|
|
|
|
|
};
|
|
|
|
|
var (rotate, flip) = operations[orientation];
|
|
|
|
|
image.Mutate(x => x.RotateFlip(rotate, flip));
|
2023-07-25 18:28:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Texture RenderText(string text) {
|
|
|
|
|
return RenderText(text, 16);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Texture RenderText(string text, int size) {
|
2023-08-04 00:03:46 +00:00
|
|
|
|
// Make sure that 0-length text doesn't end up as a 0-size texture, which
|
|
|
|
|
// might cause problems.
|
|
|
|
|
if (text.Length == 0) {
|
|
|
|
|
text = " ";
|
|
|
|
|
}
|
2023-07-24 16:36:44 +00:00
|
|
|
|
Font font = SystemFonts.CreateFont("Consolas", size, FontStyle.Bold);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
TextOptions options = new(font);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
FontRectangle rect = TextMeasurer.Measure(text, new TextOptions(font));
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(rect.Width, rect.Height);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
IBrush brush = Brushes.Solid(Color.White);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
image.Mutate(x => x.DrawText(options, text, brush));
|
2023-07-24 16:13:13 +00:00
|
|
|
|
Texture texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 15:24:56 +00:00
|
|
|
|
// FIXME: make a real icon stored as a PNG...
|
2023-07-26 14:00:46 +00:00
|
|
|
|
public static OpenTK.Windowing.Common.Input.Image[] RenderAppIcon() {
|
2023-07-26 15:11:06 +00:00
|
|
|
|
int size = 64;
|
2023-07-26 15:14:23 +00:00
|
|
|
|
Font font = SystemFonts.CreateFont("MS Mincho", size, FontStyle.Bold);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
TextOptions options = new(font);
|
2023-07-26 15:11:06 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(size, size);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
IBrush brush = Brushes.Solid(Color.Black);
|
|
|
|
|
image.Mutate(x => x.DrawText(options, "æ’®", brush));
|
2023-07-26 15:11:06 +00:00
|
|
|
|
byte[] pixelBytes = new byte[size * size * 4];
|
2023-07-26 14:00:46 +00:00
|
|
|
|
image.CopyPixelDataTo(pixelBytes);
|
2023-07-26 15:11:06 +00:00
|
|
|
|
image.Dispose();
|
|
|
|
|
OpenTK.Windowing.Common.Input.Image opentkImage = new(size, size, pixelBytes);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
return new OpenTK.Windowing.Common.Input.Image[]{ opentkImage };
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 16:23:58 +00:00
|
|
|
|
public static Texture RenderStar(float radius, bool filled) {
|
2023-07-26 19:09:19 +00:00
|
|
|
|
IPath path = new Star(x: radius, y: radius + 1, prongs: 5, innerRadii: radius * 0.4f, outerRadii: radius, angle: Util.PI);
|
2023-07-26 17:10:19 +00:00
|
|
|
|
// We add a little bit to the width & height because the reported
|
|
|
|
|
// path.Bounds are often a little tighter than they should be & a couple
|
|
|
|
|
// pixels end up obviously missing...
|
2023-07-26 16:23:58 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(path.Bounds.Width + 2, path.Bounds.Height + 2);
|
|
|
|
|
IBrush brush = Brushes.Solid(Color.White);
|
2023-07-26 19:09:19 +00:00
|
|
|
|
IPen white = Pens.Solid(Color.White, 1.5f);
|
|
|
|
|
IPen black = Pens.Solid(Color.Black, 3f);
|
|
|
|
|
image.Mutate(x => x.Draw(black, path));
|
2023-07-26 16:23:58 +00:00
|
|
|
|
if (filled) {
|
|
|
|
|
image.Mutate(x => x.Fill(brush, path));
|
|
|
|
|
}
|
2023-07-26 19:09:19 +00:00
|
|
|
|
image.Mutate(x => x.Draw(white, path));
|
2023-07-24 16:13:13 +00:00
|
|
|
|
Texture texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
2023-07-07 18:24:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
public class Game : GameWindow {
|
2023-08-03 22:38:21 +00:00
|
|
|
|
public Game(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) : base(gwSettings, nwSettings) {
|
|
|
|
|
activeTool = viewTool;
|
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-08-02 05:05:10 +00:00
|
|
|
|
private static string outputRoot = @"c:\users\colin\desktop\totte-output";
|
|
|
|
|
// private static string outputRoot = @"c:\users\colin\pictures\photos";
|
|
|
|
|
|
2023-07-08 04:33:15 +00:00
|
|
|
|
private static Texture TEXTURE_WHITE = new(new Image<Rgba32>(1, 1, new Rgba32(255, 255, 255)));
|
2023-07-24 21:13:19 +00:00
|
|
|
|
private static Texture TEXTURE_BLACK = new(new Image<Rgba32>(1, 1, new Rgba32(0, 0, 0)));
|
2023-07-26 16:23:58 +00:00
|
|
|
|
private static Texture STAR_FILLED = Util.RenderStar(20, true);
|
|