Switched to ThumbHasher over BlurHasher
This commit is contained in:
@ -56,15 +56,15 @@ public static class Core
|
|||||||
{
|
{
|
||||||
double basis = xCosines[xPixel] * yCosines[yPixel];
|
double basis = xCosines[xPixel] * yCosines[yPixel];
|
||||||
Pixel pixel = pixels[xPixel, yPixel];
|
Pixel pixel = pixels[xPixel, yPixel];
|
||||||
r += basis * pixel._Red;
|
r += basis * pixel.Red;
|
||||||
g += basis * pixel._Green;
|
g += basis * pixel.Green;
|
||||||
b += basis * pixel._Blue;
|
b += basis * pixel.Blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double scale = normalization / (width * height);
|
double scale = normalization / (width * height);
|
||||||
factors[componentsX * yComponent + xComponent]._Red = r * scale;
|
factors[componentsX * yComponent + xComponent].Red = r * scale;
|
||||||
factors[componentsX * yComponent + xComponent]._Green = g * scale;
|
factors[componentsX * yComponent + xComponent].Green = g * scale;
|
||||||
factors[componentsX * yComponent + xComponent]._Blue = b * scale;
|
factors[componentsX * yComponent + xComponent].Blue = b * scale;
|
||||||
|
|
||||||
progressCallback?.Report(processedFactors * 100 / factorCount);
|
progressCallback?.Report(processedFactors * 100 / factorCount);
|
||||||
processedFactors++;
|
processedFactors++;
|
||||||
@ -90,9 +90,9 @@ public static class Core
|
|||||||
|
|
||||||
int factorIndex = componentsX * yComponent + xComponent;
|
int factorIndex = componentsX * yComponent + xComponent;
|
||||||
|
|
||||||
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Red), actualMaximumValue);
|
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Red), actualMaximumValue);
|
||||||
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Green), actualMaximumValue);
|
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Green), actualMaximumValue);
|
||||||
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Blue), actualMaximumValue);
|
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Blue), actualMaximumValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
int quantizedMaximumValue = (int)Math.Max(0.0, Math.Min(82.0, Math.Floor(actualMaximumValue * 166 - 0.5)));
|
int quantizedMaximumValue = (int)Math.Max(0.0, Math.Min(82.0, Math.Floor(actualMaximumValue * 166 - 0.5)));
|
||||||
@ -105,7 +105,7 @@ public static class Core
|
|||||||
resultBuffer[1] = '0';
|
resultBuffer[1] = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
EncodeDc(dc._Red, dc._Green, dc._Blue).EncodeBase83(resultBuffer.Slice(2, 4));
|
EncodeDc(dc.Red, dc.Green, dc.Blue).EncodeBase83(resultBuffer.Slice(2, 4));
|
||||||
|
|
||||||
for (int yComponent = 0; yComponent < componentsY; yComponent++)
|
for (int yComponent = 0; yComponent < componentsY; yComponent++)
|
||||||
for (int xComponent = 0; xComponent < componentsX; xComponent++)
|
for (int xComponent = 0; xComponent < componentsX; xComponent++)
|
||||||
@ -116,7 +116,7 @@ public static class Core
|
|||||||
|
|
||||||
int factorIndex = componentsX * yComponent + xComponent;
|
int factorIndex = componentsX * yComponent + xComponent;
|
||||||
|
|
||||||
EncodeAc(factors[factorIndex]._Red, factors[factorIndex]._Green, factors[factorIndex]._Blue, maximumValue).EncodeBase83(resultBuffer.Slice(6 + (factorIndex - 1) * 2, 2));
|
EncodeAc(factors[factorIndex].Red, factors[factorIndex].Green, factors[factorIndex].Blue, maximumValue).EncodeBase83(resultBuffer.Slice(6 + (factorIndex - 1) * 2, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultBuffer.ToString();
|
return resultBuffer.ToString();
|
||||||
@ -184,9 +184,9 @@ public static class Core
|
|||||||
{
|
{
|
||||||
ref Pixel result = ref pixels[xPixel, yPixel];
|
ref Pixel result = ref pixels[xPixel, yPixel];
|
||||||
|
|
||||||
result._Red = 0.0;
|
result.Red = 0.0;
|
||||||
result._Green = 0.0;
|
result.Green = 0.0;
|
||||||
result._Blue = 0.0;
|
result.Blue = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
double[] xCosines = new double[outputWidth];
|
double[] xCosines = new double[outputWidth];
|
||||||
@ -215,9 +215,9 @@ public static class Core
|
|||||||
|
|
||||||
double basis = xCosines[xPixel] * yCosines[yPixel];
|
double basis = xCosines[xPixel] * yCosines[yPixel];
|
||||||
|
|
||||||
result._Red += coefficient._Red * basis;
|
result.Red += coefficient.Red * basis;
|
||||||
result._Green += coefficient._Green * basis;
|
result.Green += coefficient.Green * basis;
|
||||||
result._Blue += coefficient._Blue * basis;
|
result.Blue += coefficient.Blue * basis;
|
||||||
}
|
}
|
||||||
|
|
||||||
progressCallback?.Report(componentIndex * 100 / componentCount);
|
progressCallback?.Report(componentIndex * 100 / componentCount);
|
||||||
|
@ -5,14 +5,14 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public struct Pixel
|
public struct Pixel
|
||||||
{
|
{
|
||||||
public double _Red;
|
public double Red { get; set; }
|
||||||
public double _Green;
|
public double Green { get; set; }
|
||||||
public double _Blue;
|
public double Blue { get; set; }
|
||||||
|
|
||||||
public Pixel(double red, double green, double blue)
|
public Pixel(double red, double green, double blue)
|
||||||
{
|
{
|
||||||
_Red = red;
|
Red = red;
|
||||||
_Green = green;
|
Green = green;
|
||||||
_Blue = blue;
|
Blue = blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -63,9 +63,9 @@ public static class BlurHasher
|
|||||||
for (int x = 0; x < width; x++)
|
for (int x = 0; x < width; x++)
|
||||||
{
|
{
|
||||||
ref Pixel res = ref result[x, y];
|
ref Pixel res = ref result[x, y];
|
||||||
res._Blue = MathUtils.SRgbToLinear(rgb[index++]);
|
res.Blue = MathUtils.SRgbToLinear(rgb[index++]);
|
||||||
res._Green = MathUtils.SRgbToLinear(rgb[index++]);
|
res.Green = MathUtils.SRgbToLinear(rgb[index++]);
|
||||||
res._Red = MathUtils.SRgbToLinear(rgb[index++]);
|
res.Red = MathUtils.SRgbToLinear(rgb[index++]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,9 +92,9 @@ public static class BlurHasher
|
|||||||
{
|
{
|
||||||
Pixel pixel = pixelData[xPixel, yPixel];
|
Pixel pixel = pixelData[xPixel, yPixel];
|
||||||
|
|
||||||
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Blue);
|
data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Blue);
|
||||||
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Green);
|
data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Green);
|
||||||
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Red);
|
data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Red);
|
||||||
data[index++] = 0;
|
data[index++] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ public class BlurHasher : IBlurHasher
|
|||||||
string result;
|
string result;
|
||||||
int actualByte;
|
int actualByte;
|
||||||
result = System.Drawing.BlurHash.BlurHasher.Encode(image, x, y);
|
result = System.Drawing.BlurHash.BlurHasher.Encode(image, x, y);
|
||||||
|
using Image actualImage = System.Drawing.BlurHash.BlurHasher.Decode(result, width, height);
|
||||||
byte[] blurHashBytes = Encoding.UTF8.GetBytes(result);
|
byte[] blurHashBytes = Encoding.UTF8.GetBytes(result);
|
||||||
using Bitmap actualImage = (Bitmap)System.Drawing.BlurHash.BlurHasher.Decode(result, width, height);
|
|
||||||
string joined = string.Join(string.Empty, blurHashBytes.Select(l => l.ToString("000")));
|
string joined = string.Join(string.Empty, blurHashBytes.Select(l => l.ToString("000")));
|
||||||
string fileName = Path.Combine(directory, $"{x}x{y}-{width}x{height}-{joined}.png");
|
string fileName = Path.Combine(directory, $"{x}x{y}-{width}x{height}-{joined}.png");
|
||||||
if (!File.Exists(fileName))
|
if (!File.Exists(fileName))
|
||||||
|
@ -50,9 +50,9 @@
|
|||||||
<PackageReference Include="System.Text.Json" Version="7.0.2" />
|
<PackageReference Include="System.Text.Json" Version="7.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
|
|
||||||
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
||||||
<ProjectReference Include="..\Property\Property.csproj" />
|
<ProjectReference Include="..\Property\Property.csproj" />
|
||||||
|
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="appsettings.json">
|
<None Include="appsettings.json">
|
||||||
|
@ -65,8 +65,8 @@ public class DateGroup
|
|||||||
throw new Exception();
|
throw new Exception();
|
||||||
if (propertyConfiguration.PopulatePropertyId && (configuration.ByCreateDateShortcut || configuration.ByHash) && Shared.Models.Stateless.Methods.IProperty.Any(containers))
|
if (propertyConfiguration.PopulatePropertyId && (configuration.ByCreateDateShortcut || configuration.ByHash) && Shared.Models.Stateless.Methods.IProperty.Any(containers))
|
||||||
{
|
{
|
||||||
IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher();
|
IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
|
||||||
propertyLogic.SavePropertyParallelWork(ticks, t, containers, blurHasher);
|
propertyLogic.SavePropertyParallelWork(ticks, t, containers, thumbHasher);
|
||||||
if (appSettings.MaxDegreeOfParallelism < 2)
|
if (appSettings.MaxDegreeOfParallelism < 2)
|
||||||
ticks = LogDelta(ticks, nameof(A_Property.SavePropertyParallelWork));
|
ticks = LogDelta(ticks, nameof(A_Property.SavePropertyParallelWork));
|
||||||
if (propertyLogic.ExceptionsDirectories.Any())
|
if (propertyLogic.ExceptionsDirectories.Any())
|
||||||
@ -338,10 +338,10 @@ public class DateGroup
|
|||||||
private (Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] GetFileMoveCollectionAll(Property.Models.Configuration configuration, string destinationRoot, Container[] containers)
|
private (Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] GetFileMoveCollectionAll(Property.Models.Configuration configuration, string destinationRoot, Container[] containers)
|
||||||
{
|
{
|
||||||
(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] results;
|
(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] results;
|
||||||
|
Item[] filteredItems;
|
||||||
string? topDirectory;
|
string? topDirectory;
|
||||||
string? checkDirectory;
|
string? checkDirectory;
|
||||||
string destinationDirectory;
|
string destinationDirectory;
|
||||||
Item[] filteredItems;
|
|
||||||
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollection = new();
|
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollection = new();
|
||||||
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollectionDirectory;
|
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollectionDirectory;
|
||||||
foreach (Container container in containers)
|
foreach (Container container in containers)
|
||||||
|
@ -195,7 +195,7 @@ public partial class E_Distance
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, string eDistanceContentDirectory, MappingFromItem mappingFromItem, List<Face> faces, List<LocationContainer<MetadataExtractor.Directory>> collection)
|
public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, MappingFromItem mappingFromItem, List<Face> faces, List<LocationContainer<MetadataExtractor.Directory>> collection)
|
||||||
{
|
{
|
||||||
string? json;
|
string? json;
|
||||||
string fileName;
|
string fileName;
|
||||||
|
@ -202,11 +202,11 @@ public partial class DragDropMove : Form
|
|||||||
List<(string, int, DateTime)> results = new();
|
List<(string, int, DateTime)> results = new();
|
||||||
DateTime dateTime;
|
DateTime dateTime;
|
||||||
Shared.Models.Property property;
|
Shared.Models.Property property;
|
||||||
Shared.Models.Methods.IBlurHasher? blurHasher = null;
|
Shared.Models.Methods.IThumbHasher? thumbHasher = null;
|
||||||
string[] files = Directory.GetFiles(checkDirectory, "*", SearchOption.TopDirectoryOnly);
|
string[] files = Directory.GetFiles(checkDirectory, "*", SearchOption.TopDirectoryOnly);
|
||||||
foreach (string file in files)
|
foreach (string file in files)
|
||||||
{
|
{
|
||||||
property = Property.Models.A_Property.GetImageProperty(blurHasher, file);
|
property = Property.Models.A_Property.GetImageProperty(thumbHasher, file);
|
||||||
if (property.Id is null || property.DateTimeOriginal is null)
|
if (property.Id is null || property.DateTimeOriginal is null)
|
||||||
continue;
|
continue;
|
||||||
dateTime = property.DateTimeOriginal.Value.AddTicks(ticks);
|
dateTime = property.DateTimeOriginal.Value.AddTicks(ticks);
|
||||||
@ -240,7 +240,7 @@ public partial class DragDropMove : Form
|
|||||||
ticks++;
|
ticks++;
|
||||||
|
|
||||||
}
|
}
|
||||||
Shared.Models.Methods.IBlurHasher? blurHasher = null;
|
Shared.Models.Methods.IThumbHasher? thumbHasher = null;
|
||||||
List<(string, int, DateTime)> collection = GetCollection(checkDirectory, minimumDateTime, maximumDateTime, ticks);
|
List<(string, int, DateTime)> collection = GetCollection(checkDirectory, minimumDateTime, maximumDateTime, ticks);
|
||||||
ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty<Type>(), null) ?? throw new Exception();
|
ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty<Type>(), null) ?? throw new Exception();
|
||||||
foreach ((string file, int id, DateTime dateTime) in collection)
|
foreach ((string file, int id, DateTime dateTime) in collection)
|
||||||
@ -255,7 +255,7 @@ public partial class DragDropMove : Form
|
|||||||
bitmap.SetPropertyItem(propertyItem);
|
bitmap.SetPropertyItem(propertyItem);
|
||||||
bitmap.Save(checkFile);
|
bitmap.Save(checkFile);
|
||||||
bitmap.Dispose();
|
bitmap.Dispose();
|
||||||
property = Property.Models.A_Property.GetImageProperty(blurHasher, checkFile);
|
property = Property.Models.A_Property.GetImageProperty(thumbHasher, checkFile);
|
||||||
if (property.Id is null || property.Id.Value != id)
|
if (property.Id is null || property.Id.Value != id)
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}
|
}
|
||||||
@ -290,11 +290,11 @@ public partial class DragDropMove : Form
|
|||||||
_Logger.Error("bad file(s) or target file(s) or maximum directory doesn't equal 1!");
|
_Logger.Error("bad file(s) or target file(s) or maximum directory doesn't equal 1!");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Shared.Models.Methods.IBlurHasher? blurHasher = null;
|
Shared.Models.Methods.IThumbHasher? thumbHasher = null;
|
||||||
DateTime minimumDateTime = DateTime.ParseExact(Path.GetFileName(minimumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None);
|
DateTime minimumDateTime = DateTime.ParseExact(Path.GetFileName(minimumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None);
|
||||||
DateTime maximumDateTime = DateTime.ParseExact(Path.GetFileName(maximumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None).AddHours(23);
|
DateTime maximumDateTime = DateTime.ParseExact(Path.GetFileName(maximumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None).AddHours(23);
|
||||||
Shared.Models.Property badProperty = Property.Models.A_Property.GetImageProperty(blurHasher, badFiles.First());
|
Shared.Models.Property badProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, badFiles.First());
|
||||||
Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(blurHasher, targetFiles.First());
|
Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, targetFiles.First());
|
||||||
if (badProperty.DateTimeOriginal is null || targetProperty.DateTimeOriginal is null)
|
if (badProperty.DateTimeOriginal is null || targetProperty.DateTimeOriginal is null)
|
||||||
_Logger.Error("Date is null!");
|
_Logger.Error("Date is null!");
|
||||||
else
|
else
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using Phares.Shared;
|
using Phares.Shared;
|
||||||
using ShellProgressBar;
|
using ShellProgressBar;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@ -32,8 +31,8 @@ public partial class DlibDotNet
|
|||||||
private readonly Serilog.ILogger? _Log;
|
private readonly Serilog.ILogger? _Log;
|
||||||
private readonly D2_FaceParts _FaceParts;
|
private readonly D2_FaceParts _FaceParts;
|
||||||
private readonly AppSettings _AppSettings;
|
private readonly AppSettings _AppSettings;
|
||||||
private readonly IBlurHasher _IBlurHasher;
|
|
||||||
private readonly List<string> _Exceptions;
|
private readonly List<string> _Exceptions;
|
||||||
|
private readonly IThumbHasher _IThumbHasher;
|
||||||
private readonly IsEnvironment _IsEnvironment;
|
private readonly IsEnvironment _IsEnvironment;
|
||||||
private readonly bool _PropertyRootExistedBefore;
|
private readonly bool _PropertyRootExistedBefore;
|
||||||
private readonly Models.Configuration _Configuration;
|
private readonly Models.Configuration _Configuration;
|
||||||
@ -59,7 +58,7 @@ public partial class DlibDotNet
|
|||||||
long ticks = DateTime.Now.Ticks;
|
long ticks = DateTime.Now.Ticks;
|
||||||
_Exceptions = new List<string>();
|
_Exceptions = new List<string>();
|
||||||
_Log = Serilog.Log.ForContext<DlibDotNet>();
|
_Log = Serilog.Log.ForContext<DlibDotNet>();
|
||||||
_IBlurHasher = new BlurHash.Models.BlurHasher();
|
_IThumbHasher = new ThumbHash.Models.C2_ThumbHasher();
|
||||||
Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot);
|
Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot);
|
||||||
Models.Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration);
|
Models.Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration);
|
||||||
_Log.Information(propertyConfiguration.RootDirectory);
|
_Log.Information(propertyConfiguration.RootDirectory);
|
||||||
@ -355,9 +354,8 @@ public partial class DlibDotNet
|
|||||||
MapLogic mapLogic,
|
MapLogic mapLogic,
|
||||||
string outputResolution,
|
string outputResolution,
|
||||||
string cResultsFullGroupDirectory,
|
string cResultsFullGroupDirectory,
|
||||||
string dResultsDateGroupDirectory,
|
string c2ResultsFullGroupDirectory,
|
||||||
string dResultsFullGroupDirectory,
|
string dResultsFullGroupDirectory,
|
||||||
string eDistanceContentDirectory,
|
|
||||||
List<Tuple<string, DateTime>> sourceDirectoryChanges,
|
List<Tuple<string, DateTime>> sourceDirectoryChanges,
|
||||||
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
|
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
|
||||||
Container container,
|
Container container,
|
||||||
@ -377,7 +375,8 @@ public partial class DlibDotNet
|
|||||||
List<string> parseExceptions = new();
|
List<string> parseExceptions = new();
|
||||||
List<Tuple<string, DateTime>> subFileTuples = new();
|
List<Tuple<string, DateTime>> subFileTuples = new();
|
||||||
List<KeyValuePair<string, string>> metadataCollection;
|
List<KeyValuePair<string, string>> metadataCollection;
|
||||||
if (item.Property is not null && item.Property.Id is not null && !item.Any() && item.Property.BlurHash is null)
|
FileHolder resizedFileHolder = _Resize.GetResizedFileHolder(item);
|
||||||
|
if (item.Property is not null && item.Property.Id is not null && resizedFileHolder.Exists && item.Property.ThumbHashBytes is null)
|
||||||
{
|
{
|
||||||
(string aResultsFullGroupDirectory, _) = GetResultsFullGroupDirectories();
|
(string aResultsFullGroupDirectory, _) = GetResultsFullGroupDirectories();
|
||||||
string aPropertySingletonDirectory = Path.Combine(aResultsFullGroupDirectory, "{}");
|
string aPropertySingletonDirectory = Path.Combine(aResultsFullGroupDirectory, "{}");
|
||||||
@ -389,19 +388,60 @@ public partial class DlibDotNet
|
|||||||
string find = "\"CreationTime\":";
|
string find = "\"CreationTime\":";
|
||||||
if (!json.Contains(find))
|
if (!json.Contains(find))
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
#pragma warning disable CA1416
|
byte[]? thumbHashBytes = _IThumbHasher.Encode(resizedFileHolder.FullName);
|
||||||
using Image image = Image.FromFile(item.ImageFileHolder.FullName);
|
string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes);
|
||||||
#pragma warning restore CA1416
|
json = json.Replace(find, $"\"{nameof(property.ThumbHashBytes)}\": {thumbHashJson}, {find}");
|
||||||
string blurHash = _IBlurHasher.Encode(image);
|
|
||||||
json = json.Replace(find, $"\"{nameof(item.Property.BlurHash)}\": \"{blurHash}\", {find}");
|
|
||||||
property = JsonSerializer.Deserialize<Shared.Models.Property>(json);
|
property = JsonSerializer.Deserialize<Shared.Models.Property>(json);
|
||||||
if (property is null || property.BlurHash is null)
|
if (property is null || property.ThumbHashBytes is null)
|
||||||
throw new NullReferenceException(nameof(property));
|
throw new NullReferenceException(nameof(property));
|
||||||
json = JsonSerializer.Serialize(property, new JsonSerializerOptions { WriteIndented = true });
|
json = JsonSerializer.Serialize(property, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
if (thumbHashBytes is null || thumbHashBytes.Length != property.ThumbHashBytes.Length)
|
||||||
|
throw new Exception(nameof(property.ThumbHashBytes));
|
||||||
|
for (int i = 0; i < thumbHashBytes.Length; i++)
|
||||||
|
{
|
||||||
|
if (thumbHashBytes[i] != property.ThumbHashBytes[i])
|
||||||
|
throw new Exception(nameof(property.ThumbHashBytes));
|
||||||
|
}
|
||||||
|
thumbHashBytes = JsonSerializer.Deserialize<byte[]>(thumbHashJson);
|
||||||
|
if (thumbHashBytes is null || thumbHashBytes.Length != property.ThumbHashBytes.Length)
|
||||||
|
throw new Exception(nameof(property.ThumbHashBytes));
|
||||||
|
for (int i = 0; i < thumbHashBytes.Length; i++)
|
||||||
|
{
|
||||||
|
if (thumbHashBytes[i] != property.ThumbHashBytes[i])
|
||||||
|
throw new Exception(nameof(property.ThumbHashBytes));
|
||||||
|
}
|
||||||
File.WriteAllText(matchFile, json);
|
File.WriteAllText(matchFile, json);
|
||||||
File.SetLastWriteTime(matchFile, item.Property.LastWriteTime);
|
File.SetLastWriteTime(matchFile, item.Property.LastWriteTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (item.Property is not null && item.Property.Id is not null && resizedFileHolder.Exists && item.Property.Width is not null && item.Property.Height is not null && item.Property.ThumbHashBytes is not null)
|
||||||
|
{
|
||||||
|
string fileName;
|
||||||
|
string c2ThumbHasherContentDirectory = Path.Combine(c2ResultsFullGroupDirectory, "()");
|
||||||
|
string c2ThumbHasherSingletonDirectory = Path.Combine(c2ResultsFullGroupDirectory, "{}");
|
||||||
|
if (!Directory.Exists(c2ThumbHasherContentDirectory))
|
||||||
|
_ = Directory.CreateDirectory(c2ThumbHasherContentDirectory);
|
||||||
|
if (!Directory.Exists(c2ThumbHasherSingletonDirectory))
|
||||||
|
_ = Directory.CreateDirectory(c2ThumbHasherSingletonDirectory);
|
||||||
|
MemoryStream memoryStream = _IThumbHasher.GetMemoryStream(item.Property.ThumbHashBytes, item.Property.Width.Value, item.Property.Height.Value);
|
||||||
|
string thumbHashJson = JsonSerializer.Serialize(item.Property.ThumbHashBytes)[1..^1];
|
||||||
|
if (!Regex.Matches(thumbHashJson, @"[\\,\/,\:,\*,\?,\"",\<,\>,\|]").Any())
|
||||||
|
fileName = Path.Combine(c2ThumbHasherSingletonDirectory, $"{thumbHashJson}.png");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// string thumbHash = BitConverter.ToString(item.Property.ThumbHashBytes).Replace("-", string.Empty);
|
||||||
|
// fileName = Path.Combine(c2ThumbHasherContentDirectory, $"{thumbHash}.png");
|
||||||
|
fileName = Path.Combine(c2ThumbHasherContentDirectory, $"{resizedFileHolder.NameWithoutExtension}.png");
|
||||||
|
}
|
||||||
|
if (!File.Exists(fileName))
|
||||||
|
{
|
||||||
|
using FileStream fileStream = new(fileName, FileMode.CreateNew);
|
||||||
|
memoryStream.WriteTo(fileStream);
|
||||||
|
memoryStream.Dispose();
|
||||||
|
if (resizedFileHolder.LastWriteTime is not null)
|
||||||
|
File.SetLastWriteTime(fileName, resizedFileHolder.LastWriteTime.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (item.Property is not null && item.Property.Id is not null && !item.Any())
|
if (item.Property is not null && item.Property.Id is not null && !item.Any())
|
||||||
{
|
{
|
||||||
property = item.Property;
|
property = item.Property;
|
||||||
@ -427,7 +467,7 @@ public partial class DlibDotNet
|
|||||||
_Log.Information(string.Concat("LastWriteTimeChanged <", item.ImageFileHolder.FullName, '>'));
|
_Log.Information(string.Concat("LastWriteTimeChanged <", item.ImageFileHolder.FullName, '>'));
|
||||||
else if (item.Moved.HasValue && item.Moved.Value)
|
else if (item.Moved.HasValue && item.Moved.Value)
|
||||||
_Log.Information(string.Concat("Moved <", item.ImageFileHolder.FullName, '>'));
|
_Log.Information(string.Concat("Moved <", item.ImageFileHolder.FullName, '>'));
|
||||||
property = propertyLogic.GetProperty(_IBlurHasher, item, subFileTuples, parseExceptions);
|
property = propertyLogic.GetProperty(_IThumbHasher, item, subFileTuples, parseExceptions);
|
||||||
item.Update(property);
|
item.Update(property);
|
||||||
if (propertyHashCode is null)
|
if (propertyHashCode is null)
|
||||||
{
|
{
|
||||||
@ -442,7 +482,6 @@ public partial class DlibDotNet
|
|||||||
}
|
}
|
||||||
if (property is null || item.Property is null)
|
if (property is null || item.Property is null)
|
||||||
throw new NullReferenceException(nameof(property));
|
throw new NullReferenceException(nameof(property));
|
||||||
FileHolder resizedFileHolder = _Resize.GetResizedFileHolder(item);
|
|
||||||
item.SetResizedFileHolder(_Resize.FileNameExtension, resizedFileHolder);
|
item.SetResizedFileHolder(_Resize.FileNameExtension, resizedFileHolder);
|
||||||
string facesDirectory = _Configuration.LoadOrCreateThenSaveImageFacesResultsForOutputResolutions.Contains(outputResolution) ? _Faces.GetFacesDirectory(dResultsFullGroupDirectory, item) : string.Empty;
|
string facesDirectory = _Configuration.LoadOrCreateThenSaveImageFacesResultsForOutputResolutions.Contains(outputResolution) ? _Faces.GetFacesDirectory(dResultsFullGroupDirectory, item) : string.Empty;
|
||||||
string facePartsDirectory = _Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution) ? _FaceParts.GetFacePartsDirectory(_Configuration.PropertyConfiguration, dResultsFullGroupDirectory, item, includeNameWithoutExtension: true) : string.Empty;
|
string facePartsDirectory = _Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution) ? _FaceParts.GetFacePartsDirectory(_Configuration.PropertyConfiguration, dResultsFullGroupDirectory, item, includeNameWithoutExtension: true) : string.Empty;
|
||||||
@ -485,7 +524,7 @@ public partial class DlibDotNet
|
|||||||
if ((_Configuration.DistanceMoveUnableToMatch || _Configuration.DistanceRenameToMatch)
|
if ((_Configuration.DistanceMoveUnableToMatch || _Configuration.DistanceRenameToMatch)
|
||||||
&& _Configuration.LoadOrCreateThenSaveDistanceResultsForOutputResolutions.Contains(outputResolution)
|
&& _Configuration.LoadOrCreateThenSaveDistanceResultsForOutputResolutions.Contains(outputResolution)
|
||||||
&& collection is not null && faceCollection.All(l => !l.Saved))
|
&& collection is not null && faceCollection.All(l => !l.Saved))
|
||||||
_Distance.LookForMatchFacesAndPossiblyRename(_Faces.FileNameExtension, eDistanceContentDirectory, mappingFromItem, faces, collection);
|
_Distance.LookForMatchFacesAndPossiblyRename(_Faces.FileNameExtension, mappingFromItem, faces, collection);
|
||||||
if (_Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution))
|
if (_Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution))
|
||||||
{
|
{
|
||||||
bool saveRotated = false;
|
bool saveRotated = false;
|
||||||
@ -509,10 +548,9 @@ public partial class DlibDotNet
|
|||||||
MapLogic mapLogic,
|
MapLogic mapLogic,
|
||||||
string outputResolution,
|
string outputResolution,
|
||||||
string cResultsFullGroupDirectory,
|
string cResultsFullGroupDirectory,
|
||||||
string dResultsDateGroupDirectory,
|
string c2ResultsFullGroupDirectory,
|
||||||
string dResultsFullGroupDirectory,
|
string dResultsFullGroupDirectory,
|
||||||
string d2ResultsFullGroupDirectory,
|
string d2ResultsFullGroupDirectory,
|
||||||
string eDistanceContentDirectory,
|
|
||||||
List<Tuple<string, DateTime>> sourceDirectoryChanges,
|
List<Tuple<string, DateTime>> sourceDirectoryChanges,
|
||||||
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
|
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
|
||||||
Container container,
|
Container container,
|
||||||
@ -540,9 +578,8 @@ public partial class DlibDotNet
|
|||||||
mapLogic,
|
mapLogic,
|
||||||
outputResolution,
|
outputResolution,
|
||||||
cResultsFullGroupDirectory,
|
cResultsFullGroupDirectory,
|
||||||
dResultsDateGroupDirectory,
|
c2ResultsFullGroupDirectory,
|
||||||
dResultsFullGroupDirectory,
|
dResultsFullGroupDirectory,
|
||||||
eDistanceContentDirectory,
|
|
||||||
sourceDirectoryChanges,
|
sourceDirectoryChanges,
|
||||||
fileNameToCollection,
|
fileNameToCollection,
|
||||||
container,
|
container,
|
||||||
@ -620,7 +657,7 @@ public partial class DlibDotNet
|
|||||||
return new(aResultsFullGroupDirectory, bResultsFullGroupDirectory);
|
return new(aResultsFullGroupDirectory, bResultsFullGroupDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string, string, string) GetResultsFullGroupDirectories(string outputResolution)
|
private (string, string, string, string) GetResultsFullGroupDirectories(string outputResolution)
|
||||||
{
|
{
|
||||||
string cResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
|
string cResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
|
||||||
_Configuration.PropertyConfiguration,
|
_Configuration.PropertyConfiguration,
|
||||||
@ -629,6 +666,13 @@ public partial class DlibDotNet
|
|||||||
includeResizeGroup: true,
|
includeResizeGroup: true,
|
||||||
includeModel: false,
|
includeModel: false,
|
||||||
includePredictorModel: false);
|
includePredictorModel: false);
|
||||||
|
string c2ResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
|
||||||
|
_Configuration.PropertyConfiguration,
|
||||||
|
nameof(ThumbHash.Models.C2_ThumbHasher),
|
||||||
|
outputResolution,
|
||||||
|
includeResizeGroup: true,
|
||||||
|
includeModel: false,
|
||||||
|
includePredictorModel: false);
|
||||||
string dResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
|
string dResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
|
||||||
_Configuration.PropertyConfiguration,
|
_Configuration.PropertyConfiguration,
|
||||||
nameof(D_Face),
|
nameof(D_Face),
|
||||||
@ -643,10 +687,10 @@ public partial class DlibDotNet
|
|||||||
includeResizeGroup: true,
|
includeResizeGroup: true,
|
||||||
includeModel: true,
|
includeModel: true,
|
||||||
includePredictorModel: true);
|
includePredictorModel: true);
|
||||||
return new(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory);
|
return new(cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FullDoWork(string argZero, string propertyRoot, long ticks, string aResultsFullGroupDirectory, string bResultsFullGroupDirectory, int t, Container[] containers, A_Property propertyLogic, B_Metadata metadata, string eDistanceContentDirectory, Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection, ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers, MapLogic mapLogic)
|
private void FullDoWork(string argZero, string propertyRoot, long ticks, string aResultsFullGroupDirectory, string bResultsFullGroupDirectory, int t, Container[] containers, A_Property propertyLogic, B_Metadata metadata, Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection, ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers, MapLogic mapLogic)
|
||||||
{
|
{
|
||||||
if (_Log is null)
|
if (_Log is null)
|
||||||
throw new NullReferenceException(nameof(_Log));
|
throw new NullReferenceException(nameof(_Log));
|
||||||
@ -659,6 +703,7 @@ public partial class DlibDotNet
|
|||||||
bool anyNullOrNoIsUniqueFileName;
|
bool anyNullOrNoIsUniqueFileName;
|
||||||
string cResultsFullGroupDirectory;
|
string cResultsFullGroupDirectory;
|
||||||
string dResultsFullGroupDirectory;
|
string dResultsFullGroupDirectory;
|
||||||
|
string c2ResultsFullGroupDirectory;
|
||||||
string d2ResultsFullGroupDirectory;
|
string d2ResultsFullGroupDirectory;
|
||||||
int containersLength = containers.Length;
|
int containersLength = containers.Length;
|
||||||
List<Tuple<string, DateTime>> sourceDirectoryChanges = new();
|
List<Tuple<string, DateTime>> sourceDirectoryChanges = new();
|
||||||
@ -667,7 +712,7 @@ public partial class DlibDotNet
|
|||||||
foreach (string outputResolution in _Configuration.OutputResolutions)
|
foreach (string outputResolution in _Configuration.OutputResolutions)
|
||||||
{
|
{
|
||||||
total = 0;
|
total = 0;
|
||||||
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
(cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
||||||
for (int i = 0; i < containers.Length; i++)
|
for (int i = 0; i < containers.Length; i++)
|
||||||
{
|
{
|
||||||
container = containers[i];
|
container = containers[i];
|
||||||
@ -695,10 +740,9 @@ public partial class DlibDotNet
|
|||||||
mapLogic,
|
mapLogic,
|
||||||
outputResolution,
|
outputResolution,
|
||||||
cResultsFullGroupDirectory,
|
cResultsFullGroupDirectory,
|
||||||
dResultsDateGroupDirectory,
|
c2ResultsFullGroupDirectory,
|
||||||
dResultsFullGroupDirectory,
|
dResultsFullGroupDirectory,
|
||||||
d2ResultsFullGroupDirectory,
|
d2ResultsFullGroupDirectory,
|
||||||
eDistanceContentDirectory,
|
|
||||||
sourceDirectoryChanges,
|
sourceDirectoryChanges,
|
||||||
fileNameToCollection,
|
fileNameToCollection,
|
||||||
container,
|
container,
|
||||||
@ -911,13 +955,14 @@ public partial class DlibDotNet
|
|||||||
string? directoryName;
|
string? directoryName;
|
||||||
string cResultsFullGroupDirectory;
|
string cResultsFullGroupDirectory;
|
||||||
string dResultsFullGroupDirectory;
|
string dResultsFullGroupDirectory;
|
||||||
|
string c2ResultsFullGroupDirectory;
|
||||||
string d2ResultsFullGroupDirectory;
|
string d2ResultsFullGroupDirectory;
|
||||||
List<int> distinctFilteredIds = Shared.Models.Stateless.Methods.IContainer.GetFilteredDistinctIds(_Configuration.PropertyConfiguration, containers);
|
List<int> distinctFilteredIds = Shared.Models.Stateless.Methods.IContainer.GetFilteredDistinctIds(_Configuration.PropertyConfiguration, containers);
|
||||||
LookForAbandoned(idToLocationContainers, distinctFilteredIds);
|
LookForAbandoned(idToLocationContainers, distinctFilteredIds);
|
||||||
LookForAbandoned(bResultsFullGroupDirectory, distinctFilteredIds);
|
LookForAbandoned(bResultsFullGroupDirectory, distinctFilteredIds);
|
||||||
foreach (string outputResolution in _Configuration.OutputResolutions)
|
foreach (string outputResolution in _Configuration.OutputResolutions)
|
||||||
{
|
{
|
||||||
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
(cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
||||||
directories = Directory.GetDirectories(cResultsFullGroupDirectory, "*", SearchOption.TopDirectoryOnly);
|
directories = Directory.GetDirectories(cResultsFullGroupDirectory, "*", SearchOption.TopDirectoryOnly);
|
||||||
foreach (string directory in directories)
|
foreach (string directory in directories)
|
||||||
{
|
{
|
||||||
@ -1101,6 +1146,7 @@ public partial class DlibDotNet
|
|||||||
string bResultsFullGroupDirectory;
|
string bResultsFullGroupDirectory;
|
||||||
string cResultsFullGroupDirectory;
|
string cResultsFullGroupDirectory;
|
||||||
string dResultsFullGroupDirectory;
|
string dResultsFullGroupDirectory;
|
||||||
|
string c2ResultsFullGroupDirectory;
|
||||||
string d2ResultsFullGroupDirectory;
|
string d2ResultsFullGroupDirectory;
|
||||||
string fPhotoPrismContentDirectory;
|
string fPhotoPrismContentDirectory;
|
||||||
string fPhotoPrismSingletonDirectory;
|
string fPhotoPrismSingletonDirectory;
|
||||||
@ -1152,7 +1198,7 @@ public partial class DlibDotNet
|
|||||||
Shared.Models.Stateless.Methods.IGenealogicalDataCommunication.CreateTree(_Configuration.MappingDefaultName, _Configuration.PersonBirthdayFormat, _Configuration.PropertyConfiguration.ResultAllInOne, _PersonContainers, _GenealogicalDataCommunicationHeaderLines, _GenealogicalDataCommunicationFooterLines, ticks, a2PeopleContentDirectory, personKeyToIds);
|
Shared.Models.Stateless.Methods.IGenealogicalDataCommunication.CreateTree(_Configuration.MappingDefaultName, _Configuration.PersonBirthdayFormat, _Configuration.PropertyConfiguration.ResultAllInOne, _PersonContainers, _GenealogicalDataCommunicationHeaderLines, _GenealogicalDataCommunicationFooterLines, ticks, a2PeopleContentDirectory, personKeyToIds);
|
||||||
ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers = mapLogic.GetIdToLocationContainers();
|
ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers = mapLogic.GetIdToLocationContainers();
|
||||||
fileNameToCollection = !Directory.Exists(fPhotoPrismSingletonDirectory) ? fileNameToCollection = new() : F_PhotoPrism.GetFileNameToCollection(fPhotoPrismSingletonDirectory);
|
fileNameToCollection = !Directory.Exists(fPhotoPrismSingletonDirectory) ? fileNameToCollection = new() : F_PhotoPrism.GetFileNameToCollection(fPhotoPrismSingletonDirectory);
|
||||||
FullDoWork(argZero, propertyRoot, ticks, aResultsFullGroupDirectory, bResultsFullGroupDirectory, t, containers, propertyLogic, metadata, eDistanceContentDirectory, fileNameToCollection, idToLocationContainers, mapLogic);
|
FullDoWork(argZero, propertyRoot, ticks, aResultsFullGroupDirectory, bResultsFullGroupDirectory, t, containers, propertyLogic, metadata, fileNameToCollection, idToLocationContainers, mapLogic);
|
||||||
if (_Configuration.LookForAbandoned)
|
if (_Configuration.LookForAbandoned)
|
||||||
LookForAbandoned(bResultsFullGroupDirectory, containers, idToLocationContainers);
|
LookForAbandoned(bResultsFullGroupDirectory, containers, idToLocationContainers);
|
||||||
_Distance.Clear();
|
_Distance.Clear();
|
||||||
@ -1175,7 +1221,7 @@ public partial class DlibDotNet
|
|||||||
mapLogic.SaveShortcutsForOutputResolutionsPreMapLogic(eDistanceContentDirectory, personKeyToIds, distinctFilteredMappingCollection);
|
mapLogic.SaveShortcutsForOutputResolutionsPreMapLogic(eDistanceContentDirectory, personKeyToIds, distinctFilteredMappingCollection);
|
||||||
if (!string.IsNullOrEmpty(a2PeopleContentDirectory) && _Configuration.SaveFilteredOriginalImagesFromJLinksForOutputResolutions.Contains(outputResolution))
|
if (!string.IsNullOrEmpty(a2PeopleContentDirectory) && _Configuration.SaveFilteredOriginalImagesFromJLinksForOutputResolutions.Contains(outputResolution))
|
||||||
mapLogic.SaveFilteredOriginalImagesFromJLinks(_Configuration.JLinks, _PersonContainers, a2PeopleContentDirectory, personKeyToIds, distinctFilteredMappingCollection, totalNotMapped);
|
mapLogic.SaveFilteredOriginalImagesFromJLinks(_Configuration.JLinks, _PersonContainers, a2PeopleContentDirectory, personKeyToIds, distinctFilteredMappingCollection, totalNotMapped);
|
||||||
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
(cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
|
||||||
if (_ArgZeroIsConfigurationRootDirectory
|
if (_ArgZeroIsConfigurationRootDirectory
|
||||||
&& _Configuration.SaveResizedSubfiles
|
&& _Configuration.SaveResizedSubfiles
|
||||||
&& outputResolution == _Configuration.OutputResolutions[0]
|
&& outputResolution == _Configuration.OutputResolutions[0]
|
||||||
|
@ -49,7 +49,6 @@
|
|||||||
<PackageReference Include="WindowsShortcutFactory" Version="1.1.0" />
|
<PackageReference Include="WindowsShortcutFactory" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
|
|
||||||
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
|
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
|
||||||
<ProjectReference Include="..\Distance\Distance.csproj" />
|
<ProjectReference Include="..\Distance\Distance.csproj" />
|
||||||
<ProjectReference Include="..\Face\Face.csproj" />
|
<ProjectReference Include="..\Face\Face.csproj" />
|
||||||
@ -61,6 +60,7 @@
|
|||||||
<ProjectReference Include="..\Property\Property.csproj" />
|
<ProjectReference Include="..\Property\Property.csproj" />
|
||||||
<ProjectReference Include="..\Resize\Resize.csproj" />
|
<ProjectReference Include="..\Resize\Resize.csproj" />
|
||||||
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
||||||
|
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="appsettings.json">
|
<None Include="appsettings.json">
|
||||||
|
@ -102,7 +102,7 @@ public class A_Property
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IBlurHasher? blurHasher, FileHolder fileHolder, Shared.Models.Property? property, bool populateId, bool isIgnoreExtension, bool isValidImageFormatExtension, bool isValidMetadataExtensions, int? id, ASCIIEncoding asciiEncoding, bool writeBitmapDataBytes, string? angleBracket)
|
private static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, FileHolder fileHolder, Shared.Models.Property? property, bool populateId, bool isIgnoreExtension, bool isValidImageFormatExtension, bool isValidMetadataExtensions, int? id, ASCIIEncoding asciiEncoding, bool writeBitmapDataBytes, string? angleBracket)
|
||||||
{
|
{
|
||||||
Shared.Models.Property result;
|
Shared.Models.Property result;
|
||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
@ -114,10 +114,10 @@ public class A_Property
|
|||||||
string? model = null;
|
string? model = null;
|
||||||
string dateTimeFormat;
|
string dateTimeFormat;
|
||||||
DateTime checkDateTime;
|
DateTime checkDateTime;
|
||||||
string? blurHash = null;
|
|
||||||
DateTime? dateTime = null;
|
DateTime? dateTime = null;
|
||||||
PropertyItem? propertyItem;
|
PropertyItem? propertyItem;
|
||||||
string? orientation = null;
|
string? orientation = null;
|
||||||
|
byte[]? thumbHashBytes = null;
|
||||||
DateTime? gpsDateStamp = null;
|
DateTime? gpsDateStamp = null;
|
||||||
DateTime? dateTimeOriginal = null;
|
DateTime? dateTimeOriginal = null;
|
||||||
DateTime? dateTimeDigitized = null;
|
DateTime? dateTimeDigitized = null;
|
||||||
@ -134,6 +134,8 @@ public class A_Property
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using Image image = Image.FromFile(fileHolder.FullName);
|
using Image image = Image.FromFile(fileHolder.FullName);
|
||||||
|
width = image.Width;
|
||||||
|
height = image.Height;
|
||||||
if (populateId && id is null)
|
if (populateId && id is null)
|
||||||
{
|
{
|
||||||
using Bitmap bitmap = new(image);
|
using Bitmap bitmap = new(image);
|
||||||
@ -151,18 +153,6 @@ public class A_Property
|
|||||||
File.WriteAllBytes(Path.ChangeExtension(contentFileInfo.FullName, string.Empty), bytes);
|
File.WriteAllBytes(Path.ChangeExtension(contentFileInfo.FullName, string.Empty), bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (blurHasher is not null && property?.BlurHash is null)
|
|
||||||
{
|
|
||||||
if (angleBracket is null)
|
|
||||||
blurHash = blurHasher.Encode(image);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string blurHashDirectory = angleBracket.Replace("<>", "()");
|
|
||||||
blurHash = blurHasher.EncodeAndSave(image, blurHashDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width = image.Width;
|
|
||||||
height = image.Height;
|
|
||||||
dateTimeFormat = Shared.Models.Stateless.Methods.IProperty.DateTimeFormat();
|
dateTimeFormat = Shared.Models.Stateless.Methods.IProperty.DateTimeFormat();
|
||||||
if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime))
|
if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime))
|
||||||
{
|
{
|
||||||
@ -252,16 +242,18 @@ public class A_Property
|
|||||||
throw new NullReferenceException(nameof(fileHolder.CreationTime));
|
throw new NullReferenceException(nameof(fileHolder.CreationTime));
|
||||||
if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null)
|
if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null)
|
||||||
throw new NullReferenceException(nameof(fileHolder.LastWriteTime));
|
throw new NullReferenceException(nameof(fileHolder.LastWriteTime));
|
||||||
|
if (thumbHasher is not null && property?.ThumbHashBytes is null)
|
||||||
|
thumbHashBytes = thumbHasher.Encode(fileHolder.FullName);
|
||||||
if (fileHolder.CreationTime is not null && fileHolder.LastWriteTime is not null)
|
if (fileHolder.CreationTime is not null && fileHolder.LastWriteTime is not null)
|
||||||
result = new(blurHash, fileHolder.CreationTime.Value, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, fileHolder.LastWriteTime.Value, make, model, orientation, width);
|
result = new(fileHolder.CreationTime.Value, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, fileHolder.LastWriteTime.Value, make, model, orientation, thumbHashBytes, width);
|
||||||
else if (property is not null)
|
else if (property is not null)
|
||||||
result = new(blurHash, property.CreationTime, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, property.LastWriteTime, make, model, orientation, width);
|
result = new(property.CreationTime, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, property.LastWriteTime, make, model, orientation, thumbHashBytes, width);
|
||||||
else
|
else
|
||||||
throw new NullReferenceException(nameof(property));
|
throw new NullReferenceException(nameof(property));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IBlurHasher? blurHasher, string fileName)
|
public static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, string fileName)
|
||||||
{
|
{
|
||||||
int? id = null;
|
int? id = null;
|
||||||
bool populateId = true;
|
bool populateId = true;
|
||||||
@ -273,13 +265,13 @@ public class A_Property
|
|||||||
FileHolder fileHolder = new(fileName);
|
FileHolder fileHolder = new(fileName);
|
||||||
bool isValidImageFormatExtension = true;
|
bool isValidImageFormatExtension = true;
|
||||||
Shared.Models.Property? property = null;
|
Shared.Models.Property? property = null;
|
||||||
Shared.Models.Property result = GetImageProperty(blurHasher, fileHolder, property, populateId, isIgnoreExtension, isValidImageFormatExtension, isValidMetadataExtensions, id, asciiEncoding, writeBitmapDataBytes, angleBracket);
|
Shared.Models.Property result = GetImageProperty(thumbHasher, fileHolder, property, populateId, isIgnoreExtension, isValidImageFormatExtension, isValidMetadataExtensions, id, asciiEncoding, writeBitmapDataBytes, angleBracket);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning restore CA1416
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IBlurHasher? blurHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions)
|
private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions)
|
||||||
{
|
{
|
||||||
Shared.Models.Property? result;
|
Shared.Models.Property? result;
|
||||||
int? id = null;
|
int? id = null;
|
||||||
@ -367,19 +359,16 @@ public class A_Property
|
|||||||
parseExceptions.Add(nameof(A_Property));
|
parseExceptions.Add(nameof(A_Property));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(json) && result is not null && blurHasher is not null && result.BlurHash is null)
|
if (!string.IsNullOrEmpty(json) && result is not null && thumbHasher is not null && result.ThumbHashBytes is null)
|
||||||
{
|
{
|
||||||
string find = "\"CreationTime\":";
|
string find = "\"CreationTime\":";
|
||||||
if (!json.Contains(find))
|
if (!json.Contains(find))
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
string blurHashDirectory = angleBracket.Replace("<>", "()");
|
byte[] thumbHashBytes = thumbHasher.Encode(item.ImageFileHolder.FullName);
|
||||||
#pragma warning disable CA1416
|
string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes);
|
||||||
using Image image = Image.FromFile(item.ImageFileHolder.FullName);
|
json = json.Replace(find, $"\"{nameof(result.ThumbHashBytes)}\": {thumbHashJson}, {find}");
|
||||||
#pragma warning restore CA1416
|
|
||||||
string blurHash = blurHasher.EncodeAndSave(image, blurHashDirectory);
|
|
||||||
json = json.Replace(find, $"\"{nameof(result.BlurHash)}\": \"{blurHash}\", {find}");
|
|
||||||
result = JsonSerializer.Deserialize<Shared.Models.Property>(json);
|
result = JsonSerializer.Deserialize<Shared.Models.Property>(json);
|
||||||
if (result is null || result.BlurHash is null)
|
if (result is null || result.ThumbHashBytes is null)
|
||||||
throw new NullReferenceException(nameof(result));
|
throw new NullReferenceException(nameof(result));
|
||||||
json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
|
json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
|
||||||
File.WriteAllText(fileInfo.FullName, json);
|
File.WriteAllText(fileInfo.FullName, json);
|
||||||
@ -388,7 +377,7 @@ public class A_Property
|
|||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
id ??= item.ImageFileHolder.Id;
|
id ??= item.ImageFileHolder.Id;
|
||||||
result = GetImageProperty(blurHasher, item.ImageFileHolder, result, populateId, isIgnoreExtension, item.IsValidImageFormatExtension, isValidMetadataExtensions, id, _ASCIIEncoding, _Configuration.WriteBitmapDataBytes, angleBracket);
|
result = GetImageProperty(thumbHasher, item.ImageFileHolder, result, populateId, isIgnoreExtension, item.IsValidImageFormatExtension, isValidMetadataExtensions, id, _ASCIIEncoding, _Configuration.WriteBitmapDataBytes, angleBracket);
|
||||||
json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
|
json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
|
||||||
if (populateId && Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true))
|
if (populateId && Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true))
|
||||||
{
|
{
|
||||||
@ -417,7 +406,7 @@ public class A_Property
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SavePropertyParallelForWork(Shared.Models.Methods.IBlurHasher? blurHasher, string sourceDirectory, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<Tuple<string, DateTime>> sourceDirectoryChanges, Item item)
|
private void SavePropertyParallelForWork(Shared.Models.Methods.IThumbHasher? thumbHasher, string sourceDirectory, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<Tuple<string, DateTime>> sourceDirectoryChanges, Item item)
|
||||||
{
|
{
|
||||||
Shared.Models.Property property;
|
Shared.Models.Property property;
|
||||||
List<string> parseExceptions = new();
|
List<string> parseExceptions = new();
|
||||||
@ -428,7 +417,7 @@ public class A_Property
|
|||||||
File.Move(item.ImageFileHolder.FullName, filteredSourceDirectoryFileExtensionLowered);
|
File.Move(item.ImageFileHolder.FullName, filteredSourceDirectoryFileExtensionLowered);
|
||||||
if (item.FileSizeChanged is null || item.FileSizeChanged.Value || item.LastWriteTimeChanged is null || item.LastWriteTimeChanged.Value || item.Property is null)
|
if (item.FileSizeChanged is null || item.FileSizeChanged.Value || item.LastWriteTimeChanged is null || item.LastWriteTimeChanged.Value || item.Property is null)
|
||||||
{
|
{
|
||||||
property = GetPropertyOfPrivate(blurHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
|
property = GetPropertyOfPrivate(thumbHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
|
||||||
lock (sourceDirectoryChanges)
|
lock (sourceDirectoryChanges)
|
||||||
sourceDirectoryChanges.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now));
|
sourceDirectoryChanges.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now));
|
||||||
lock (item)
|
lock (item)
|
||||||
@ -436,7 +425,7 @@ public class A_Property
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IBlurHasher? blurHasher, List<Exception> exceptions, List<Tuple<string, DateTime>> sourceDirectoryChanges, Container container, List<Item> items, string message)
|
private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IThumbHasher? thumbHasher, List<Exception> exceptions, List<Tuple<string, DateTime>> sourceDirectoryChanges, Container container, List<Item> items, string message)
|
||||||
{
|
{
|
||||||
List<Tuple<string, DateTime>> sourceDirectoryFileTuples = new();
|
List<Tuple<string, DateTime>> sourceDirectoryFileTuples = new();
|
||||||
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||||
@ -449,7 +438,7 @@ public class A_Property
|
|||||||
long ticks = DateTime.Now.Ticks;
|
long ticks = DateTime.Now.Ticks;
|
||||||
DateTime dateTime = DateTime.Now;
|
DateTime dateTime = DateTime.Now;
|
||||||
List<Tuple<string, DateTime>> collection;
|
List<Tuple<string, DateTime>> collection;
|
||||||
SavePropertyParallelForWork(blurHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]);
|
SavePropertyParallelForWork(thumbHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]);
|
||||||
if (i == 0 || sourceDirectoryChanges.Any())
|
if (i == 0 || sourceDirectoryChanges.Any())
|
||||||
progressBar.Tick();
|
progressBar.Tick();
|
||||||
lock (sourceDirectoryFileTuples)
|
lock (sourceDirectoryFileTuples)
|
||||||
@ -478,9 +467,6 @@ public class A_Property
|
|||||||
singletonDescription: "Properties for each image",
|
singletonDescription: "Properties for each image",
|
||||||
collectionDescription: string.Empty,
|
collectionDescription: string.Empty,
|
||||||
converted: false));
|
converted: false));
|
||||||
string directory = _AngleBracketCollection[0].Replace("<>", "()");
|
|
||||||
if (!Directory.Exists(directory))
|
|
||||||
_ = Directory.CreateDirectory(directory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetAngleBracketCollection(string sourceDirectory, bool anyNullOrNoIsUniqueFileName)
|
private void SetAngleBracketCollection(string sourceDirectory, bool anyNullOrNoIsUniqueFileName)
|
||||||
@ -495,7 +481,7 @@ public class A_Property
|
|||||||
SetAngleBracketCollection(aResultsFullGroupDirectory, sourceDirectory, anyNullOrNoIsUniqueFileName);
|
SetAngleBracketCollection(aResultsFullGroupDirectory, sourceDirectory, anyNullOrNoIsUniqueFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SavePropertyParallelWork(long ticks, int t, Container[] containers, Shared.Models.Methods.IBlurHasher? blurHasher)
|
public void SavePropertyParallelWork(long ticks, int t, Container[] containers, Shared.Models.Methods.IThumbHasher? thumbHasher)
|
||||||
{
|
{
|
||||||
if (_Log is null)
|
if (_Log is null)
|
||||||
throw new NullReferenceException(nameof(_Log));
|
throw new NullReferenceException(nameof(_Log));
|
||||||
@ -521,7 +507,7 @@ public class A_Property
|
|||||||
SetAngleBracketCollection(container.SourceDirectory, anyNullOrNoIsUniqueFileName);
|
SetAngleBracketCollection(container.SourceDirectory, anyNullOrNoIsUniqueFileName);
|
||||||
totalSeconds = (int)Math.Truncate(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds);
|
totalSeconds = (int)Math.Truncate(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds);
|
||||||
message = $"{i + 1:000} [{container.Items.Count:000}] / {containersLength:000} - {total} / {t} total - {totalSeconds} total second(s) - {outputResolution} - {container.SourceDirectory}";
|
message = $"{i + 1:000} [{container.Items.Count:000}] / {containersLength:000} - {total} / {t} total - {totalSeconds} total second(s) - {outputResolution} - {container.SourceDirectory}";
|
||||||
SavePropertyParallelWork(_MaxDegreeOfParallelism, blurHasher, exceptions, sourceDirectoryChanges, container, container.Items, message);
|
SavePropertyParallelWork(_MaxDegreeOfParallelism, thumbHasher, exceptions, sourceDirectoryChanges, container, container.Items, message);
|
||||||
foreach (Exception exception in exceptions)
|
foreach (Exception exception in exceptions)
|
||||||
_Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception);
|
_Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception);
|
||||||
if (exceptions.Count == container.Items.Count)
|
if (exceptions.Count == container.Items.Count)
|
||||||
@ -542,7 +528,7 @@ public class A_Property
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Shared.Models.Property GetProperty(Shared.Models.Methods.IBlurHasher? blurHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions)
|
public Shared.Models.Property GetProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions)
|
||||||
{
|
{
|
||||||
Shared.Models.Property result;
|
Shared.Models.Property result;
|
||||||
bool angleBracketCollectionAny = _AngleBracketCollection.Any();
|
bool angleBracketCollectionAny = _AngleBracketCollection.Any();
|
||||||
@ -554,7 +540,7 @@ public class A_Property
|
|||||||
}
|
}
|
||||||
bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
|
bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
|
||||||
bool isIgnoreExtension = item.IsValidImageFormatExtension && _Configuration.IgnoreExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
|
bool isIgnoreExtension = item.IsValidImageFormatExtension && _Configuration.IgnoreExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
|
||||||
result = GetPropertyOfPrivate(blurHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
|
result = GetPropertyOfPrivate(thumbHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
|
||||||
if (!angleBracketCollectionAny)
|
if (!angleBracketCollectionAny)
|
||||||
_AngleBracketCollection.Clear();
|
_AngleBracketCollection.Clear();
|
||||||
return result;
|
return result;
|
||||||
|
10
Shared/Models/Methods/IThumbHasher.cs
Normal file
10
Shared/Models/Methods/IThumbHasher.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace View_by_Distance.Shared.Models.Methods;
|
||||||
|
|
||||||
|
public interface IThumbHasher
|
||||||
|
{
|
||||||
|
|
||||||
|
byte[] Encode(string path);
|
||||||
|
(byte[], MemoryStream) EncodeAndSave(string path, int width, int height);
|
||||||
|
MemoryStream GetMemoryStream(byte[] thumbHashBytes, int width, int height);
|
||||||
|
|
||||||
|
}
|
@ -3,7 +3,6 @@ namespace View_by_Distance.Shared.Models.Properties;
|
|||||||
public interface IProperty
|
public interface IProperty
|
||||||
{
|
{
|
||||||
|
|
||||||
public string? BlurHash { init; get; }
|
|
||||||
public DateTime CreationTime { init; get; }
|
public DateTime CreationTime { init; get; }
|
||||||
public DateTime? DateTime { init; get; }
|
public DateTime? DateTime { init; get; }
|
||||||
public DateTime? DateTimeDigitized { init; get; }
|
public DateTime? DateTimeDigitized { init; get; }
|
||||||
@ -17,6 +16,7 @@ public interface IProperty
|
|||||||
public string? Make { init; get; }
|
public string? Make { init; get; }
|
||||||
public string? Model { init; get; }
|
public string? Model { init; get; }
|
||||||
public string? Orientation { init; get; }
|
public string? Orientation { init; get; }
|
||||||
|
public byte[]? ThumbHashBytes { init; get; }
|
||||||
public int? Width { init; get; }
|
public int? Width { init; get; }
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ namespace View_by_Distance.Shared.Models;
|
|||||||
public class Property : Properties.IProperty
|
public class Property : Properties.IProperty
|
||||||
{
|
{
|
||||||
|
|
||||||
public string? BlurHash { init; get; }
|
|
||||||
public DateTime CreationTime { init; get; }
|
public DateTime CreationTime { init; get; }
|
||||||
public DateTime? DateTime { init; get; }
|
public DateTime? DateTime { init; get; }
|
||||||
public DateTime? DateTimeDigitized { init; get; }
|
public DateTime? DateTimeDigitized { init; get; }
|
||||||
@ -20,12 +19,12 @@ public class Property : Properties.IProperty
|
|||||||
public string? Make { init; get; }
|
public string? Make { init; get; }
|
||||||
public string? Model { init; get; }
|
public string? Model { init; get; }
|
||||||
public string? Orientation { init; get; }
|
public string? Orientation { init; get; }
|
||||||
|
public byte[]? ThumbHashBytes { init; get; }
|
||||||
public int? Width { init; get; }
|
public int? Width { init; get; }
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Property(string? blurHash, DateTime creationTime, DateTime? dateTime, DateTime? dateTimeDigitized, DateTime? dateTimeFromName, DateTime? dateTimeOriginal, long fileSize, DateTime? gpsDateStamp, int? height, int? id, DateTime lastWriteTime, string? make, string? model, string? orientation, int? width)
|
public Property(DateTime creationTime, DateTime? dateTime, DateTime? dateTimeDigitized, DateTime? dateTimeFromName, DateTime? dateTimeOriginal, long fileSize, DateTime? gpsDateStamp, int? height, int? id, DateTime lastWriteTime, string? make, string? model, string? orientation, byte[]? thumbHashBytes, int? width)
|
||||||
{
|
{
|
||||||
BlurHash = blurHash;
|
|
||||||
DateTimeFromName = dateTimeFromName;
|
DateTimeFromName = dateTimeFromName;
|
||||||
CreationTime = creationTime;
|
CreationTime = creationTime;
|
||||||
DateTime = dateTime;
|
DateTime = dateTime;
|
||||||
@ -39,6 +38,7 @@ public class Property : Properties.IProperty
|
|||||||
Make = make;
|
Make = make;
|
||||||
Model = model;
|
Model = model;
|
||||||
Orientation = orientation;
|
Orientation = orientation;
|
||||||
|
ThumbHashBytes = thumbHashBytes;
|
||||||
Width = width;
|
Width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +41,11 @@
|
|||||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
|
|
||||||
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
||||||
<ProjectReference Include="..\Property\Property.csproj" />
|
<ProjectReference Include="..\Property\Property.csproj" />
|
||||||
<ProjectReference Include="..\Metadata\Metadata.csproj" />
|
<ProjectReference Include="..\Metadata\Metadata.csproj" />
|
||||||
<ProjectReference Include="..\Resize\Resize.csproj" />
|
<ProjectReference Include="..\Resize\Resize.csproj" />
|
||||||
|
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\Instance\appsettings.json">
|
<None Include="..\Instance\appsettings.json">
|
||||||
|
@ -168,8 +168,8 @@ public class UnitTestResize
|
|||||||
Assert.IsNotNull(item.ImageFileHolder);
|
Assert.IsNotNull(item.ImageFileHolder);
|
||||||
if (item.Property is null)
|
if (item.Property is null)
|
||||||
{
|
{
|
||||||
Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher();
|
Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
|
||||||
property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions);
|
property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions);
|
||||||
item.Update(property);
|
item.Update(property);
|
||||||
}
|
}
|
||||||
if (property is null || item.Property is null)
|
if (property is null || item.Property is null)
|
||||||
|
@ -40,13 +40,13 @@
|
|||||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
|
|
||||||
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
||||||
<ProjectReference Include="..\Property\Property.csproj" />
|
<ProjectReference Include="..\Property\Property.csproj" />
|
||||||
<ProjectReference Include="..\Metadata\Metadata.csproj" />
|
<ProjectReference Include="..\Metadata\Metadata.csproj" />
|
||||||
<ProjectReference Include="..\Resize\Resize.csproj" />
|
<ProjectReference Include="..\Resize\Resize.csproj" />
|
||||||
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
|
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
|
||||||
<ProjectReference Include="..\Property-Compare\Property-Compare.csproj" />
|
<ProjectReference Include="..\Property-Compare\Property-Compare.csproj" />
|
||||||
|
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\Instance\appsettings.json">
|
<None Include="..\Instance\appsettings.json">
|
||||||
|
@ -242,8 +242,8 @@ public class UnitTestFace
|
|||||||
Assert.IsNotNull(item.ImageFileHolder);
|
Assert.IsNotNull(item.ImageFileHolder);
|
||||||
if (item.Property is null)
|
if (item.Property is null)
|
||||||
{
|
{
|
||||||
Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher();
|
Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
|
||||||
property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions);
|
property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions);
|
||||||
item.Update(property);
|
item.Update(property);
|
||||||
}
|
}
|
||||||
if (property is null || item.Property is null)
|
if (property is null || item.Property is null)
|
||||||
|
1
ThumbHash/.vscode/format-report.json
vendored
Normal file
1
ThumbHash/.vscode/format-report.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
53
ThumbHash/Models/SpanOwner.cs
Normal file
53
ThumbHash/Models/SpanOwner.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace View_by_Distance.ThumbHash.Models;
|
||||||
|
|
||||||
|
internal readonly ref struct SpanOwner<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
private static ArrayPool<T> DefaultPool
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => ArrayPool<T>.Shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly T[] _Buffer;
|
||||||
|
private readonly int _Length;
|
||||||
|
|
||||||
|
public static SpanOwner<T> Empty
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => new(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Span<T> Span
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ref T r0 = ref MemoryMarshal.GetArrayDataReference(_Buffer);
|
||||||
|
return MemoryMarshal.CreateSpan(ref r0, _Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public SpanOwner<T> WithLength(int length) => new(length, _Buffer);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public SpanOwner(int length) : this(length, DefaultPool.Rent(length))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private SpanOwner(int length, T[] buffer)
|
||||||
|
{
|
||||||
|
_Length = length;
|
||||||
|
_Buffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Dispose() =>
|
||||||
|
DefaultPool.Return(_Buffer);
|
||||||
|
|
||||||
|
}
|
31
ThumbHash/Models/ThumbHash.Channel.cs
Normal file
31
ThumbHash/Models/ThumbHash.Channel.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace View_by_Distance.ThumbHash.Models;
|
||||||
|
|
||||||
|
public static partial class ThumbHash
|
||||||
|
{
|
||||||
|
private readonly ref struct Channel
|
||||||
|
{
|
||||||
|
|
||||||
|
public float DC { init; get; }
|
||||||
|
public SpanOwner<float> AC { init; get; }
|
||||||
|
public float Scale { init; get; }
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public Channel(float dc, SpanOwner<float> ac, float scale)
|
||||||
|
{
|
||||||
|
DC = dc;
|
||||||
|
AC = ac;
|
||||||
|
Scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Deconstruct(out float dc, out SpanOwner<float> ac, out float scale)
|
||||||
|
{
|
||||||
|
dc = DC;
|
||||||
|
ac = AC;
|
||||||
|
scale = Scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
38
ThumbHash/Models/ThumbHash.RGBA.cs
Normal file
38
ThumbHash/Models/ThumbHash.RGBA.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace View_by_Distance.ThumbHash.Models;
|
||||||
|
|
||||||
|
public static partial class ThumbHash
|
||||||
|
{
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private readonly struct RGBA
|
||||||
|
{
|
||||||
|
|
||||||
|
public byte R { init; get; }
|
||||||
|
public byte G { init; get; }
|
||||||
|
public byte B { init; get; }
|
||||||
|
public byte A { init; get; }
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public RGBA(byte r, byte g, byte b, byte a)
|
||||||
|
{
|
||||||
|
R = r;
|
||||||
|
G = g;
|
||||||
|
B = b;
|
||||||
|
A = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Deconstruct(out byte r, out byte g, out byte b, out byte a)
|
||||||
|
{
|
||||||
|
r = R;
|
||||||
|
g = G;
|
||||||
|
b = B;
|
||||||
|
a = A;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
409
ThumbHash/Models/ThumbHash.cs
Normal file
409
ThumbHash/Models/ThumbHash.cs
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace View_by_Distance.ThumbHash.Models;
|
||||||
|
|
||||||
|
public static partial class ThumbHash
|
||||||
|
{
|
||||||
|
|
||||||
|
private const int _MaxHash = 25;
|
||||||
|
private const int _MinHash = 5;
|
||||||
|
|
||||||
|
[DoesNotReturn]
|
||||||
|
static void ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{value}' must be greater than or equal to '{other}'.");
|
||||||
|
|
||||||
|
[DoesNotReturn]
|
||||||
|
static void ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{paramName}' must be less than or equal to '{other}'.");
|
||||||
|
|
||||||
|
[DoesNotReturn]
|
||||||
|
static void ThrowNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null, [CallerArgumentExpression(nameof(other))] string? otherName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{paramName}' must be equal to '{other}' ('{otherName}').");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes an RGBA image to a ThumbHash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width">The width of the input image. Must be ≤100px.</param>
|
||||||
|
/// <param name="height">The height of the input image. Must be ≤100px.</param>
|
||||||
|
/// <param name="rgba">The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements.</param>
|
||||||
|
/// <returns>Byte array containing the ThumbHash</returns>
|
||||||
|
public static byte[] RgbaToThumbHash(int width, int height, ReadOnlySpan<byte> rgba)
|
||||||
|
{
|
||||||
|
Span<byte> hash = stackalloc byte[_MaxHash];
|
||||||
|
int bytesWritten = RgbaToThumbHash(hash, width, height, rgba);
|
||||||
|
return hash[..bytesWritten].ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes an RGBA image to a ThumbHash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash"></param>
|
||||||
|
/// <param name="w">The width of the input image. Must be ≤100px.</param>
|
||||||
|
/// <param name="h">The height of the input image. Must be ≤100px.</param>
|
||||||
|
/// <param name="rgba_bytes">The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements.</param>
|
||||||
|
/// <returns>Number of bytes written into hash span</returns>
|
||||||
|
public static int RgbaToThumbHash(Span<byte> hash, int w, int h, ReadOnlySpan<byte> rgba_bytes)
|
||||||
|
{
|
||||||
|
if (hash.Length < _MinHash)
|
||||||
|
ThrowIfLessThan(hash.Length, _MinHash);
|
||||||
|
|
||||||
|
// Encoding an image larger than 100x100 is slow with no benefit
|
||||||
|
if (rgba_bytes.Length != w * h * 4)
|
||||||
|
ThrowNotEqual(rgba_bytes.Length, w * h * 4);
|
||||||
|
|
||||||
|
// Determine the average color
|
||||||
|
float avg_r = 0.0f;
|
||||||
|
float avg_g = 0.0f;
|
||||||
|
float avg_b = 0.0f;
|
||||||
|
float avg_a = 0.0f;
|
||||||
|
|
||||||
|
ReadOnlySpan<RGBA> rgba = MemoryMarshal.Cast<byte, RGBA>(rgba_bytes);
|
||||||
|
foreach (ref readonly RGBA pixel in rgba)
|
||||||
|
{
|
||||||
|
float alpha = pixel.A / 255.0f;
|
||||||
|
avg_b += alpha / 255.0f * pixel.B;
|
||||||
|
avg_g += alpha / 255.0f * pixel.G;
|
||||||
|
avg_r += alpha / 255.0f * pixel.R;
|
||||||
|
avg_a += alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avg_a > 0.0f)
|
||||||
|
{
|
||||||
|
avg_r /= avg_a;
|
||||||
|
avg_g /= avg_a;
|
||||||
|
avg_b /= avg_a;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_alpha = avg_a < (w * h);
|
||||||
|
int l_limit = has_alpha ? 5 : 7; // Use fewer luminance bits if there's alpha
|
||||||
|
int lx = Math.Max((int)MathF.Round(l_limit * w / MathF.Max(w, h)), 1);
|
||||||
|
int ly = Math.Max((int)MathF.Round(l_limit * h / MathF.Max(w, h)), 1);
|
||||||
|
|
||||||
|
using SpanOwner<float> l_owner = new(w * h); // l: luminance
|
||||||
|
using SpanOwner<float> p_owner = new(w * h); // p: yellow - blue
|
||||||
|
using SpanOwner<float> q_owner = new(w * h); // q: red - green
|
||||||
|
using SpanOwner<float> a_owner = new(w * h); // a: alpha
|
||||||
|
|
||||||
|
Span<float> l = l_owner.Span;
|
||||||
|
Span<float> p = p_owner.Span;
|
||||||
|
Span<float> q = q_owner.Span;
|
||||||
|
Span<float> a = a_owner.Span;
|
||||||
|
|
||||||
|
// Convert the image from RGBA to LPQA (composite atop the average color)
|
||||||
|
int j = 0;
|
||||||
|
foreach (ref readonly RGBA pixel in rgba)
|
||||||
|
{
|
||||||
|
float alpha = pixel.A / 255.0f;
|
||||||
|
float b = avg_b * (1.0f - alpha) + alpha / 255.0f * pixel.B;
|
||||||
|
float g = avg_g * (1.0f - alpha) + alpha / 255.0f * pixel.G;
|
||||||
|
float r = avg_r * (1.0f - alpha) + alpha / 255.0f * pixel.R;
|
||||||
|
a[j] = alpha;
|
||||||
|
q[j] = r - g;
|
||||||
|
p[j] = (r + g) / 2.0f - b;
|
||||||
|
l[j] = (r + g + b) / 3.0f;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
|
||||||
|
Channel encode_channel(ReadOnlySpan<float> channel, int nx, int ny)
|
||||||
|
{
|
||||||
|
float dc = 0.0f;
|
||||||
|
SpanOwner<float> ac_owner = new(nx * ny);
|
||||||
|
float scale = 0.0f;
|
||||||
|
|
||||||
|
Span<float> fx = stackalloc float[w];
|
||||||
|
Span<float> ac = ac_owner.Span;
|
||||||
|
int n = 0;
|
||||||
|
for (int cy = 0; cy < ny; cy++)
|
||||||
|
{
|
||||||
|
int cx = 0;
|
||||||
|
while (cx * ny < nx * (ny - cy))
|
||||||
|
{
|
||||||
|
float f = 0.0f;
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
fx[x] = MathF.Cos(MathF.PI / w * cx * (x + 0.5f));
|
||||||
|
}
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
float fy = MathF.Cos(MathF.PI / h * cy * (y + 0.5f));
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
f += channel[x + y * w] * fx[x] * fy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f /= w * h;
|
||||||
|
if (cx > 0 || cy > 0)
|
||||||
|
{
|
||||||
|
ac[n++] = f;
|
||||||
|
scale = MathF.Max(MathF.Abs(f), scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dc = f;
|
||||||
|
}
|
||||||
|
cx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ac_owner = ac_owner.WithLength(n);
|
||||||
|
ac = ac_owner.Span;
|
||||||
|
|
||||||
|
if (scale > 0.0f)
|
||||||
|
{
|
||||||
|
foreach (ref float aci in ac)
|
||||||
|
{
|
||||||
|
aci = 0.5f + 0.5f / scale * aci;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Channel(dc, ac_owner, scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
(float l_dc, SpanOwner<float> l_ac, float l_scale) = encode_channel(l, Math.Max(lx, 3), Math.Max(ly, 3));
|
||||||
|
(float p_dc, SpanOwner<float> p_ac, float p_scale) = encode_channel(p, 3, 3);
|
||||||
|
(float q_dc, SpanOwner<float> q_ac, float q_scale) = encode_channel(q, 3, 3);
|
||||||
|
(float a_dc, SpanOwner<float> a_ac, float a_scale) = has_alpha ? encode_channel(a, 5, 5) : new Channel(1.0f, SpanOwner<float>.Empty, 1.0f);
|
||||||
|
|
||||||
|
// Write the constants
|
||||||
|
bool is_landscape = w > h;
|
||||||
|
uint header24 = (uint)MathF.Round(63.0f * l_dc)
|
||||||
|
| (((uint)MathF.Round(31.5f + 31.5f * p_dc)) << 6)
|
||||||
|
| (((uint)MathF.Round(31.5f + 31.5f * q_dc)) << 12)
|
||||||
|
| (((uint)MathF.Round(31.0f * l_scale)) << 18)
|
||||||
|
| (has_alpha ? 1u << 23 : 0);
|
||||||
|
int header16 = (ushort)(is_landscape ? ly : lx)
|
||||||
|
| (((ushort)MathF.Round(63.0f * p_scale)) << 3)
|
||||||
|
| (((ushort)MathF.Round(63.0f * q_scale)) << 9)
|
||||||
|
| (is_landscape ? 1 << 15 : 0);
|
||||||
|
|
||||||
|
int hi = 0;
|
||||||
|
hash[hi++] = (byte)header24;
|
||||||
|
hash[hi++] = (byte)(header24 >> 8);
|
||||||
|
hash[hi++] = (byte)(header24 >> 16);
|
||||||
|
hash[hi++] = (byte)header16;
|
||||||
|
hash[hi++] = (byte)(header16 >> 8);
|
||||||
|
if (has_alpha)
|
||||||
|
{
|
||||||
|
float fa_dc = MathF.Round(15.0f * a_dc);
|
||||||
|
float fa_scale = MathF.Round(15.0f * a_scale);
|
||||||
|
byte ia_dc = (byte)fa_dc;
|
||||||
|
byte ia_scale = (byte)fa_scale;
|
||||||
|
hash[hi++] = (byte)(ia_dc | (ia_scale << 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the varying factors
|
||||||
|
static void WriteFactor(ReadOnlySpan<float> ac, ref bool is_odd, ref int hi, Span<byte> hash)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ac.Length; i++)
|
||||||
|
{
|
||||||
|
byte u = (byte)MathF.Round(15.0f * ac[i]);
|
||||||
|
if (is_odd)
|
||||||
|
{
|
||||||
|
hash[hi - 1] |= (byte)(u << 4);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hash[hi++] = u;
|
||||||
|
}
|
||||||
|
is_odd = !is_odd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (l_ac)
|
||||||
|
using (p_ac)
|
||||||
|
using (q_ac)
|
||||||
|
using (a_ac)
|
||||||
|
{
|
||||||
|
bool is_odd = false;
|
||||||
|
WriteFactor(l_ac.Span, ref is_odd, ref hi, hash);
|
||||||
|
WriteFactor(p_ac.Span, ref is_odd, ref hi, hash);
|
||||||
|
WriteFactor(q_ac.Span, ref is_odd, ref hi, hash);
|
||||||
|
if (has_alpha)
|
||||||
|
{
|
||||||
|
WriteFactor(a_ac.Span, ref is_odd, ref hi, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a ThumbHash to an RGBA image.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash.</returns>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if the input is too short.</exception>
|
||||||
|
public static byte[] ThumbHashToRgba(ReadOnlySpan<byte> hash, int w, int h)
|
||||||
|
{
|
||||||
|
using SpanOwner<byte> rgba_owner = new(w * h * 4);
|
||||||
|
Span<byte> rgba = rgba_owner.Span;
|
||||||
|
ThumbHashToRgba(hash, w, h, rgba);
|
||||||
|
return rgba[..(w * h * 4)].ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a ThumbHash to an RGBA image.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash.</returns>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if the input is too short.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if the RGBA span length is less than `w * h * 4` bytes.</exception>
|
||||||
|
public static void ThumbHashToRgba(ReadOnlySpan<byte> hash, int w, int h, Span<byte> rgba)
|
||||||
|
{
|
||||||
|
// Read the constants
|
||||||
|
uint header24 = hash[0]
|
||||||
|
| (((uint)hash[1]) << 8)
|
||||||
|
| (((uint)hash[2]) << 16);
|
||||||
|
int header16 = hash[3] | (hash[4] << 8);
|
||||||
|
float l_dc = (header24 & 63) / 63.0f;
|
||||||
|
float p_dc = ((header24 >> 6) & 63) / 31.5f - 1.0f;
|
||||||
|
float q_dc = ((header24 >> 12) & 63) / 31.5f - 1.0f;
|
||||||
|
float l_scale = ((header24 >> 18) & 31) / 31.0f;
|
||||||
|
bool has_alpha = (header24 >> 23) != 0;
|
||||||
|
float p_scale = ((header16 >> 3) & 63) / 63.0f;
|
||||||
|
float q_scale = ((header16 >> 9) & 63) / 63.0f;
|
||||||
|
bool is_landscape = (header16 >> 15) != 0;
|
||||||
|
int l_max = has_alpha ? 5 : 7;
|
||||||
|
int lx = Math.Max(3, is_landscape ? l_max : header16 & 7);
|
||||||
|
int ly = Math.Max(3, is_landscape ? header16 & 7 : l_max);
|
||||||
|
(float a_dc, float a_scale) = has_alpha ? ((hash[5] & 15) / 15.0f, (hash[5] >> 4) / 15.0f) : (1.0f, 1.0f);
|
||||||
|
|
||||||
|
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
||||||
|
static SpanOwner<float> decode_channel(ReadOnlySpan<byte> hash, int start, ref int index, int nx, int ny, float scale)
|
||||||
|
{
|
||||||
|
SpanOwner<float> ac_owner = new(nx * ny);
|
||||||
|
Span<float> ac = ac_owner.Span;
|
||||||
|
int n = 0;
|
||||||
|
for (int cy = 0; cy < ny; cy++)
|
||||||
|
{
|
||||||
|
for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++, n++, index++)
|
||||||
|
{
|
||||||
|
int data = hash[start + (index >> 1)] >> ((index & 1) << 2);
|
||||||
|
ac[n] = ((data & 15) / 7.5f - 1.0f) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ac_owner.WithLength(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decode using the DCT into RGB
|
||||||
|
if (rgba.Length < w * h * 4)
|
||||||
|
ThrowIfLessThan(rgba.Length, w * h * 4);
|
||||||
|
|
||||||
|
int ac_start = has_alpha ? 6 : 5;
|
||||||
|
int ac_index = 0;
|
||||||
|
|
||||||
|
using SpanOwner<float> l_ac_owner = decode_channel(hash, ac_start, ref ac_index, lx, ly, l_scale);
|
||||||
|
using SpanOwner<float> p_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, p_scale * 1.25f);
|
||||||
|
using SpanOwner<float> q_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, q_scale * 1.25f);
|
||||||
|
using SpanOwner<float> a_ac_owner = has_alpha ? decode_channel(hash, ac_start, ref ac_index, 5, 5, a_scale) : SpanOwner<float>.Empty;
|
||||||
|
Span<float> l_ac = l_ac_owner.Span;
|
||||||
|
Span<float> p_ac = p_ac_owner.Span;
|
||||||
|
Span<float> q_ac = q_ac_owner.Span;
|
||||||
|
Span<float> a_ac = a_ac_owner.Span;
|
||||||
|
|
||||||
|
Span<float> fx = stackalloc float[7];
|
||||||
|
Span<float> fy = stackalloc float[7];
|
||||||
|
|
||||||
|
ref RGBA pixel = ref MemoryMarshal.AsRef<RGBA>(rgba);
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < w; x++, pixel = ref Unsafe.AddByteOffset(ref pixel, 4))
|
||||||
|
{
|
||||||
|
float l = l_dc;
|
||||||
|
float p = p_dc;
|
||||||
|
float q = q_dc;
|
||||||
|
float a = a_dc;
|
||||||
|
|
||||||
|
// Precompute the coefficients
|
||||||
|
for (int cx = 0; cx < Math.Max(lx, has_alpha ? 5 : 3); cx++)
|
||||||
|
{
|
||||||
|
fx[cx] = MathF.Cos(MathF.PI / w * (x + 0.5f) * cx);
|
||||||
|
}
|
||||||
|
for (int cy = 0; cy < Math.Max(ly, has_alpha ? 5 : 3); cy++)
|
||||||
|
{
|
||||||
|
fy[cy] = MathF.Cos(MathF.PI / h * (y + 0.5f) * cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode L
|
||||||
|
for (int cy = 0, j = 0; cy < ly; cy++)
|
||||||
|
{
|
||||||
|
int cx = cy > 0 ? 0 : 1;
|
||||||
|
float fy2 = fy[cy] * 2.0f;
|
||||||
|
while (cx * ly < lx * (ly - cy))
|
||||||
|
{
|
||||||
|
l += l_ac[j] * fx[cx] * fy2;
|
||||||
|
j += 1;
|
||||||
|
cx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode P and Q
|
||||||
|
for (int cy = 0, j = 0; cy < 3; cy++)
|
||||||
|
{
|
||||||
|
int cx = cy > 0 ? 0 : 1;
|
||||||
|
float fy2 = fy[cy] * 2.0f;
|
||||||
|
while (cx < 3 - cy)
|
||||||
|
{
|
||||||
|
float f = fx[cx] * fy2;
|
||||||
|
p += p_ac[j] * f;
|
||||||
|
q += q_ac[j] * f;
|
||||||
|
j += 1;
|
||||||
|
cx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode A
|
||||||
|
if (has_alpha)
|
||||||
|
{
|
||||||
|
for (int cy = 0, j = 0; cy < 5; cy++)
|
||||||
|
{
|
||||||
|
int cx = cy > 0 ? 0 : 1;
|
||||||
|
float fy2 = fy[cy] * 2.0f;
|
||||||
|
while (cx < 5 - cy)
|
||||||
|
{
|
||||||
|
a += a_ac[j] * fx[cx] * fy2;
|
||||||
|
j += 1;
|
||||||
|
cx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to RGB
|
||||||
|
float b = l - 2.0f / 3.0f * p;
|
||||||
|
float r = (3.0f * l - b + q) / 2.0f;
|
||||||
|
float g = r - q;
|
||||||
|
|
||||||
|
pixel = new(
|
||||||
|
r: (byte)(Math.Clamp(r, 0.0f, 1.0f) * 255.0f),
|
||||||
|
g: (byte)(Math.Clamp(g, 0.0f, 1.0f) * 255.0f),
|
||||||
|
b: (byte)(Math.Clamp(b, 0.0f, 1.0f) * 255.0f),
|
||||||
|
a: (byte)(Math.Clamp(a, 0.0f, 1.0f) * 255.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the average color from a ThumbHash.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Unpremultiplied RGBA values where each value ranges from 0 to 1. </returns>
|
||||||
|
/// <exception cref="NotImplementedException">Thrown if the input is too short.</exception>
|
||||||
|
public static (float r, float g, float b, float a) ThumbHashToAverageRgba(ReadOnlySpan<byte> hash)
|
||||||
|
{
|
||||||
|
if (hash.Length < _MinHash)
|
||||||
|
ThrowIfLessThan(hash.Length, _MinHash);
|
||||||
|
|
||||||
|
uint header = hash[0] | ((uint)hash[1] << 8) | ((uint)hash[2] << 16);
|
||||||
|
float l = (header & 63) / 63.0f;
|
||||||
|
float p = ((header >> 6) & 63) / 31.5f - 1.0f;
|
||||||
|
float q = ((header >> 12) & 63) / 31.5f - 1.0f;
|
||||||
|
bool has_alpha = (header >> 23) != 0;
|
||||||
|
float a = has_alpha ? (hash[5] & 15) / 15.0f : 1.0f;
|
||||||
|
float b = l - 2.0f / 3.0f * p;
|
||||||
|
float r = (3.0f * l - b + q) / 2.0f;
|
||||||
|
float g = r - q;
|
||||||
|
|
||||||
|
return (r: Math.Clamp(r, 0.0f, 1.0f),
|
||||||
|
g: Math.Clamp(g, 0.0f, 1.0f),
|
||||||
|
b: Math.Clamp(b, 0.0f, 1.0f),
|
||||||
|
a);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
ThumbHash/Models/ThumbHasher.cs
Normal file
57
ThumbHash/Models/ThumbHasher.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
using View_by_Distance.Shared.Models.Methods;
|
||||||
|
|
||||||
|
namespace View_by_Distance.ThumbHash.Models;
|
||||||
|
|
||||||
|
public class C2_ThumbHasher : IThumbHasher
|
||||||
|
{
|
||||||
|
|
||||||
|
private static SKBitmap GetBitmap(string path)
|
||||||
|
{
|
||||||
|
SKBitmap result;
|
||||||
|
using SKBitmap skBitmap = SKBitmap.Decode(path);
|
||||||
|
result = skBitmap.Copy(SKColorType.Rgba8888);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] Encode(SKBitmap skBitmap) =>
|
||||||
|
ThumbHash.RgbaToThumbHash(skBitmap.Width, skBitmap.Height, skBitmap.GetPixelSpan());
|
||||||
|
|
||||||
|
private static byte[] Encode(string path)
|
||||||
|
{
|
||||||
|
byte[] results;
|
||||||
|
using SKBitmap skBitmap = GetBitmap(path);
|
||||||
|
results = Encode(skBitmap);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] IThumbHasher.Encode(string path) =>
|
||||||
|
Encode(path);
|
||||||
|
|
||||||
|
private static MemoryStream GetMemoryStream(byte[] thumbHashBytes, int width, int height)
|
||||||
|
{
|
||||||
|
MemoryStream result = new();
|
||||||
|
(int w, int h) = (width / 2, height / 2);
|
||||||
|
using SKManagedWStream skManagedWStream = new(result);
|
||||||
|
byte[] bytes = ThumbHash.ThumbHashToRgba(thumbHashBytes, w, h);
|
||||||
|
SKImageInfo skImageInfo = new(w, h, SKColorType.Rgba8888, SKAlphaType.Unpremul);
|
||||||
|
using SKImage skImage = SKImage.FromPixelCopy(skImageInfo, bytes);
|
||||||
|
using SKData sdData = skImage.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
|
sdData.SaveTo(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryStream IThumbHasher.GetMemoryStream(byte[] thumbHashBytes, int width, int height) =>
|
||||||
|
GetMemoryStream(thumbHashBytes, width, height);
|
||||||
|
|
||||||
|
(byte[], MemoryStream) IThumbHasher.EncodeAndSave(string path, int width, int height)
|
||||||
|
{
|
||||||
|
byte[] results;
|
||||||
|
MemoryStream result;
|
||||||
|
using SKBitmap skBitmap = GetBitmap(path);
|
||||||
|
results = Encode(skBitmap);
|
||||||
|
result = GetMemoryStream(results, width, height);
|
||||||
|
return (results, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
ThumbHash/ThumbHash.csproj
Normal file
45
ThumbHash/ThumbHash.csproj
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>10.0</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<OutputType>library</OutputType>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>Phares.View.by.Distance.ThumbHash</PackageId>
|
||||||
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
|
<Version>7.0.101.1</Version>
|
||||||
|
<Authors>Mike Phares</Authors>
|
||||||
|
<Company>Phares</Company>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
|
||||||
|
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
|
||||||
|
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(IsWindows)'=='true'">
|
||||||
|
<DefineConstants>Windows</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(IsOSX)'=='true'">
|
||||||
|
<DefineConstants>OSX</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(IsLinux)'=='true'">
|
||||||
|
<DefineConstants>Linux</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
|
||||||
|
<SupportedPlatform Include="browser" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||||
|
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="7.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsWithFaceRecognitionDotNet", "TestsWithFaceRecognitionDotNet\TestsWithFaceRecognitionDotNet.csproj", "{A67D73C7-A1A1-4443-B681-776339CFA08A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsWithFaceRecognitionDotNet", "TestsWithFaceRecognitionDotNet\TestsWithFaceRecognitionDotNet.csproj", "{A67D73C7-A1A1-4443-B681-776339CFA08A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThumbHash", "ThumbHash\ThumbHash.csproj", "{6F146145-F7A4-4AB7-94C4-C822A9FC8269}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "View-by-Distance.Shared", "shared\View-by-Distance.Shared.csproj", "{1D231660-33B4-4763-9C9F-C6ACC8BA600D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "View-by-Distance.Shared", "shared\View-by-Distance.Shared.csproj", "{1D231660-33B4-4763-9C9F-C6ACC8BA600D}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@ -172,5 +174,9 @@ Global
|
|||||||
{9689371E-F67C-4392-A636-C398D28C089B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9689371E-F67C-4392-A636-C398D28C089B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Reference in New Issue
Block a user