Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
7.1 KiB

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AsyncImageLoader;
using Avalonia.Media.Imaging;
using Blake3;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Webp;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models;
public record ImageSource : IDisposable, ITemplateKey<ImageSourceTemplateType>
{
private Hash? contentHashBlake3;
/// <summary>
/// Local file path
/// </summary>
public FilePath? LocalFile { get; init; }
/// <summary>
/// Remote URL
/// </summary>
public Uri? RemoteUrl { get; init; }
/// <summary>
/// Bitmap
/// </summary>
[JsonIgnore]
public Bitmap? Bitmap { get; set; }
/// <summary>
/// Optional label for the image
/// </summary>
public string? Label { get; set; }
[JsonConstructor]
public ImageSource() { }
public ImageSource(FilePath localFile)
{
LocalFile = localFile;
}
public ImageSource(Uri remoteUrl)
{
RemoteUrl = remoteUrl;
}
public ImageSource(Bitmap bitmap)
{
Bitmap = bitmap;
}
[JsonIgnore]
public Uri? Uri => LocalFile?.FullPath != null ? new Uri(LocalFile.FullPath) : RemoteUrl;
/// <inheritdoc />
public ImageSourceTemplateType TemplateKey { get; private set; }
private async Task<bool> TryRefreshTemplateKeyAsync()
{
if ((LocalFile?.Extension ?? Path.GetExtension(RemoteUrl?.ToString())) is not { } extension)
{
return false;
}
if (extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{
if (LocalFile is not null && LocalFile.Exists)
{
await using var stream = LocalFile.Info.OpenRead();
using var reader = new WebpReader(stream);
try
{
TemplateKey = reader.GetIsAnimatedFlag()
? ImageSourceTemplateType.WebpAnimation
: ImageSourceTemplateType.Image;
}
catch (InvalidDataException)
{
return false;
}
return true;
}
if (RemoteUrl is not null)
{
var httpClientFactory = App.Services.GetRequiredService<IHttpClientFactory>();
using var client = httpClientFactory.CreateClient();
try
{
await using var stream = await client.GetStreamAsync(RemoteUrl);
using var reader = new WebpReader(stream);
TemplateKey = reader.GetIsAnimatedFlag()
? ImageSourceTemplateType.WebpAnimation
: ImageSourceTemplateType.Image;
}
catch (Exception)
{
return false;
}
return true;
}
return false;
}
TemplateKey = ImageSourceTemplateType.Image;
return true;
}
public async Task<ImageSourceTemplateType> GetOrRefreshTemplateKeyAsync()
{
if (TemplateKey is ImageSourceTemplateType.Default)
{
await TryRefreshTemplateKeyAsync();
}
return TemplateKey;
}
[JsonIgnore]
public Task<ImageSourceTemplateType> TemplateKeyAsync => GetOrRefreshTemplateKeyAsync();
[JsonIgnore]
public Task<Bitmap?> BitmapAsync => GetBitmapAsync();
/// <summary>
/// Get the bitmap
/// </summary>
public async Task<Bitmap?> GetBitmapAsync()
{
if (Bitmap is not null)
return Bitmap;
var loader = ImageLoader.AsyncImageLoader;
// Use local file path if available, otherwise remote URL
var path = LocalFile?.FullPath ?? RemoteUrl?.ToString();
if (path is null)
return null;
// Load the image
Bitmap = await loader.ProvideImageAsync(path).ConfigureAwait(false);
return Bitmap;
}
public async Task<Hash> GetBlake3HashAsync()
{
// Use cached value if available
if (contentHashBlake3 is not null)
{
return contentHashBlake3.Value;
}
// Only available for local files
if (LocalFile is null)
{
throw new InvalidOperationException("ImageSource is not a local file");
}
var data = await LocalFile.ReadAllBytesAsync();
contentHashBlake3 = await FileHash.GetBlake3ParallelAsync(data);
return contentHashBlake3.Value;
}
/// <summary>
/// Return a file name with Guid from Blake3 hash
/// </summary>
public async Task<string> GetHashGuidFileNameAsync()
{
if (LocalFile is null)
{
throw new InvalidOperationException("ImageSource is not a local file");
}
var extension = LocalFile.Info.Extension;
var hash = await GetBlake3HashAsync();
var guid = hash.ToGuid();
return guid + extension;
}
/// <summary>
/// Return a file name with Guid from Blake3 hash
/// This will throw if the Blake3 hash has not been calculated yet
/// </summary>
public string GetHashGuidFileNameCached()
{
if (LocalFile is null)
{
throw new InvalidOperationException("ImageSource is not a local file");
}
// Calculate hash if not available
if (contentHashBlake3 is null)
{
// File must exist
if (!LocalFile.Exists)
{
throw new FileNotFoundException("Image file does not exist", LocalFile);
}
// Fail in debug since hash should have been pre-calculated
Debug.Fail("Hash has not been calculated when GetHashGuidFileNameCached() was called");
var data = LocalFile.ReadAllBytes();
contentHashBlake3 = FileHash.GetBlake3Parallel(data);
}
var extension = LocalFile.Info.Extension;
var guid = contentHashBlake3.Value.ToGuid();
return guid + extension;
}
public string GetHashGuidFileNameCached(string pathPrefix)
{
return Path.Combine(pathPrefix, GetHashGuidFileNameCached());
}
/// <summary>
/// Clears the cached bitmap
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
Bitmap?.Dispose();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public override string ToString()
{
return LocalFile?.FullPath ?? RemoteUrl?.ToString() ?? "";
}
/// <summary>
/// Implicit conversion to string for async image loader.
/// Resolves with the local file path if available, otherwise the remote URL.
/// Otherwise returns null.
/// </summary>
public static implicit operator string(ImageSource imageSource) => imageSource.ToString();
}