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.

373 lines
13 KiB

11 months ago
using System.Diagnostics;
using System.Text;
using System.Text.Json;
11 months ago
using ExifLibrary;
using MetadataExtractor;
using MetadataExtractor.Formats.Exif;
using MetadataExtractor.Formats.Png;
using MetadataExtractor.Formats.WebP;
11 months ago
using Microsoft.VisualBasic;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using Directory = MetadataExtractor.Directory;
namespace StabilityMatrix.Core.Helper;
public class ImageMetadata
{
private IReadOnlyList<Directory>? Directories { get; set; }
11 months ago
private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
private static readonly byte[] Idat = "IDAT"u8.ToArray();
private static readonly byte[] Text = "tEXt"u8.ToArray();
11 months ago
private static readonly byte[] Riff = "RIFF"u8.ToArray();
private static readonly byte[] Webp = "WEBP"u8.ToArray();
public static ImageMetadata ParseFile(FilePath path)
{
return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) };
}
public static ImageMetadata ParseFile(Stream stream)
{
return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(stream) };
}
public System.Drawing.Size? GetImageSize()
{
if (Directories?.OfType<PngDirectory>().FirstOrDefault() is { } header)
{
header.TryGetInt32(PngDirectory.TagImageWidth, out var width);
header.TryGetInt32(PngDirectory.TagImageHeight, out var height);
return new System.Drawing.Size(width, height);
}
return null;
}
public static System.Drawing.Size GetImageSize(byte[] inputImage)
{
var imageWidthBytes = inputImage[0x10..0x14];
var imageHeightBytes = inputImage[0x14..0x18];
var imageWidth = BitConverter.ToInt32(imageWidthBytes.Reverse().ToArray());
var imageHeight = BitConverter.ToInt32(imageHeightBytes.Reverse().ToArray());
return new System.Drawing.Size(imageWidth, imageHeight);
}
public static System.Drawing.Size GetImageSize(BinaryReader reader)
{
var oldPosition = reader.BaseStream.Position;
reader.BaseStream.Position = 0x10;
var imageWidthBytes = reader.ReadBytes(4);
var imageHeightBytes = reader.ReadBytes(4);
var imageWidth = BitConverter.ToInt32(imageWidthBytes.Reverse().ToArray());
var imageHeight = BitConverter.ToInt32(imageHeightBytes.Reverse().ToArray());
reader.BaseStream.Position = oldPosition;
return new System.Drawing.Size(imageWidth, imageHeight);
}
public static (
string? Parameters,
string? ParametersJson,
string? SMProject,
string? ComfyNodes
) GetAllFileMetadata(FilePath filePath)
{
if (filePath.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{
var paramsJson = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagImageDescription);
var smProj = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagSoftware);
return (null, paramsJson, smProj, null);
}
using var stream = filePath.Info.OpenRead();
using var reader = new BinaryReader(stream);
var parameters = ReadTextChunk(reader, "parameters");
var parametersJson = ReadTextChunk(reader, "parameters-json");
var smProject = ReadTextChunk(reader, "smproj");
var comfyNodes = ReadTextChunk(reader, "prompt");
return (
string.IsNullOrEmpty(parameters) ? null : parameters,
string.IsNullOrEmpty(parametersJson) ? null : parametersJson,
string.IsNullOrEmpty(smProject) ? null : smProject,
string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes
);
}
public IEnumerable<Tag>? GetTextualData()
{
// Get the PNG-tEXt directory
return Directories
?.Where(d => d.Name == "PNG-tEXt")
.SelectMany(d => d.Tags)
.Where(t => t.Name == "Textual Data");
}
public GenerationParameters? GetGenerationParameters()
{
var textualData = GetTextualData()?.ToArray();
if (textualData is null)
{
return null;
}
// Use "parameters-json" tag if exists
if (
textualData.FirstOrDefault(
tag => tag.Description is { } desc && desc.StartsWith("parameters-json: ")
) is
{ Description: { } description }
)
{
description = description.StripStart("parameters-json: ");
return JsonSerializer.Deserialize<GenerationParameters>(description);
}
// Otherwise parse "parameters" tag
if (
textualData.FirstOrDefault(
tag => tag.Description is { } desc && desc.StartsWith("parameters: ")
) is
{ Description: { } parameters }
)
{
parameters = parameters.StripStart("parameters: ");
if (GenerationParameters.TryParse(parameters, out var generationParameters))
{
return generationParameters;
}
}
return null;
}
public static string ReadTextChunk(BinaryReader byteStream, string key)
{
byteStream.BaseStream.Position = 0;
// Read first 8 bytes and make sure they match the png header
if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader))
{
return string.Empty;
}
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).Reverse().ToArray());
var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
if (chunkType == Encoding.UTF8.GetString(Idat))
{
return string.Empty;
}
if (chunkType == Encoding.UTF8.GetString(Text))
{
var textBytes = byteStream.ReadBytes(chunkSize);
var text = Encoding.UTF8.GetString(textBytes);
if (text.StartsWith($"{key}\0"))
{
return text[(key.Length + 1)..];
}
}
else
{
// skip chunk data
byteStream.BaseStream.Position += chunkSize;
}
// skip crc
byteStream.BaseStream.Position += 4;
}
return string.Empty;
}
public static MemoryStream? BuildImageWithoutMetadata(FilePath imagePath)
{
using var byteStream = new BinaryReader(File.OpenRead(imagePath));
byteStream.BaseStream.Position = 0;
if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader))
{
return null;
}
var memoryStream = new MemoryStream();
memoryStream.Write(PngHeader);
// add the IHDR chunk
var ihdrStuff = byteStream.ReadBytes(25);
memoryStream.Write(ihdrStuff);
// find IDATs
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkSizeBytes = byteStream.ReadBytes(4);
var chunkSize = BitConverter.ToInt32(chunkSizeBytes.Reverse().ToArray());
var chunkTypeBytes = byteStream.ReadBytes(4);
var chunkType = Encoding.UTF8.GetString(chunkTypeBytes);
if (chunkType != Encoding.UTF8.GetString(Idat))
{
// skip chunk data
byteStream.BaseStream.Position += chunkSize;
// skip crc
byteStream.BaseStream.Position += 4;
continue;
}
memoryStream.Write(chunkSizeBytes);
memoryStream.Write(chunkTypeBytes);
var idatBytes = byteStream.ReadBytes(chunkSize);
memoryStream.Write(idatBytes);
var crcBytes = byteStream.ReadBytes(4);
memoryStream.Write(crcBytes);
}
// Add IEND chunk
memoryStream.Write([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
memoryStream.Position = 0;
return memoryStream;
}
11 months ago
/// <summary>
/// Reads an EXIF tag from a webp file and returns the value as string
/// </summary>
/// <param name="filePath">The webp file to read EXIF data from</param>
/// <param name="exifTag">Use <see cref="ExifDirectoryBase"/> constants for the tag you'd like to search for</param>
/// <returns></returns>
public static string ReadTextChunkFromWebp(FilePath filePath, int exifTag)
11 months ago
{
var exifDirs = WebPMetadataReader.ReadMetadata(filePath).OfType<ExifIfd0Directory>().FirstOrDefault();
return exifDirs is null ? string.Empty : exifDirs.GetString(exifTag) ?? string.Empty;
11 months ago
}
public static IEnumerable<byte> AddMetadataToWebp(
byte[] inputImage,
Dictionary<ExifTag, string> exifTagData
11 months ago
)
{
using var byteStream = new BinaryReader(new MemoryStream(inputImage));
byteStream.BaseStream.Position = 0;
// Read first 8 bytes and make sure they match the RIFF header
if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
{
return Array.Empty<byte>();
}
// skip 4 bytes then read next 4 for webp header
byteStream.BaseStream.Position += 4;
if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
{
return Array.Empty<byte>();
}
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
if (chunkType != "EXIF")
{
// skip chunk data
byteStream.BaseStream.Position += chunkSize;
continue;
}
var exifStart = byteStream.BaseStream.Position - 8;
var exifBytes = byteStream.ReadBytes(chunkSize);
Debug.WriteLine($"Found exif chunk of size {chunkSize}");
using var stream = new MemoryStream(exifBytes[6..]);
var img = new MyTiffFile(stream, Encoding.UTF8);
foreach (var (key, value) in exifTagData)
{
img.Properties.Set(key, value);
}
11 months ago
using var newStream = new MemoryStream();
img.Save(newStream);
newStream.Seek(0, SeekOrigin.Begin);
var newExifBytes = exifBytes[..6].Concat(newStream.ToArray());
var newExifSize = newExifBytes.Count();
var newChunkSize = BitConverter.GetBytes(newExifSize);
var newChunk = "EXIF"u8.ToArray().Concat(newChunkSize).Concat(newExifBytes).ToArray();
var inputEndIndex = (int)exifStart;
var newImage = inputImage[..inputEndIndex].Concat(newChunk).ToArray();
// webp or tiff or something requires even number of bytes
if (newImage.Length % 2 != 0)
{
newImage = newImage.Concat(new byte[] { 0x00 }).ToArray();
}
// no clue why the minus 8 is needed but it is
11 months ago
var newImageSize = BitConverter.GetBytes(newImage.Length - 8);
newImage[4] = newImageSize[0];
newImage[5] = newImageSize[1];
newImage[6] = newImageSize[2];
newImage[7] = newImageSize[3];
return newImage;
}
return Array.Empty<byte>();
}
private static byte[] GetExifChunks(FilePath imagePath)
{
using var byteStream = new BinaryReader(File.OpenRead(imagePath));
byteStream.BaseStream.Position = 0;
// Read first 8 bytes and make sure they match the RIFF header
if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
{
return Array.Empty<byte>();
}
// skip 4 bytes then read next 4 for webp header
byteStream.BaseStream.Position += 4;
if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
{
return Array.Empty<byte>();
}
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
if (chunkType != "EXIF")
{
// skip chunk data
byteStream.BaseStream.Position += chunkSize;
continue;
}
var exifStart = byteStream.BaseStream.Position;
var exifBytes = byteStream.ReadBytes(chunkSize);
var exif = Encoding.UTF8.GetString(exifBytes);
Debug.WriteLine($"Found exif chunk of size {chunkSize}");
return exifBytes;
}
return Array.Empty<byte>();
}
}