Files
deepdrft/DeepDrftContent/Processors/ImageProcessor.cs
T

130 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Buffers.Binary;
using DeepDrftContent.FileDatabase.Models;
namespace DeepDrftContent.Processors;
/// <summary>
/// Processes raw image bytes into an <see cref="ImageBinary"/>, mirroring the shape of
/// <see cref="AudioProcessor"/>. Validates the content type resolves to a known image
/// extension, derives the aspect ratio from the image dimensions where cheaply parseable
/// (PNG, JPEG), and defaults to 1.0 for formats whose headers we don't parse.
/// </summary>
/// <remarks>
/// Operates entirely in memory — no disk I/O. Follows the FileDatabase error-handling
/// philosophy: dimension parsing logs a warning and falls back to a best-effort aspect
/// ratio of 1.0 rather than throwing. Content-type rejection is a caller-facing validation
/// failure (returns null), distinct from a parse hiccup.
/// </remarks>
public class ImageProcessor
{
/// <summary>
/// Builds an <see cref="ImageBinary"/> from raw image bytes and a MIME content type.
/// Returns null when the content type does not resolve to a recognised image extension
/// (the <c>.bin</c> sentinel from <see cref="MimeTypeExtensions.GetExtension"/>).
/// </summary>
public ImageBinary? Process(byte[] imageBytes, string contentType)
{
var extension = MimeTypeExtensions.GetExtension(contentType);
if (extension == ".bin")
{
Console.WriteLine($"Warning: ImageProcessor rejected unsupported content type '{contentType}'");
return null;
}
var aspectRatio = ComputeAspectRatio(imageBytes, extension);
var parameters = new ImageBinaryParams(
Buffer: imageBytes,
Size: imageBytes.Length,
Extension: extension,
AspectRatio: aspectRatio);
return new ImageBinary(parameters);
}
/// <summary>
/// Derives width/height from the format header and returns width/height. Defaults to 1.0
/// for unparsed formats (gif, webp, bmp, svg) and on any parse failure.
/// </summary>
private static double ComputeAspectRatio(byte[] bytes, string extension)
{
try
{
return extension switch
{
".png" => ParsePngAspectRatio(bytes),
".jpg" or ".jpeg" => ParseJpegAspectRatio(bytes),
_ => 1.0,
};
}
catch (Exception ex)
{
Console.WriteLine($"Warning: image dimension parsing failed for '{extension}', defaulting aspect ratio to 1.0: {ex.Message}");
return 1.0;
}
}
/// <summary>
/// PNG: the IHDR chunk places width at bytes 1619 and height at 2023, both big-endian
/// uint32. Guards on the "PNG" signature at bytes 13.
/// </summary>
private static double ParsePngAspectRatio(byte[] bytes)
{
if (bytes.Length < 24 || bytes[1] != 'P' || bytes[2] != 'N' || bytes[3] != 'G')
{
return 1.0;
}
var width = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(16, 4));
var height = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(20, 4));
return Ratio(width, height);
}
/// <summary>
/// JPEG: walk the marker segments from byte 2 looking for SOF0 (0xFF 0xC0) or SOF2
/// (0xFF 0xC2). Height is a big-endian uint16 at marker+5, width at marker+7. Guards on
/// the SOI marker (0xFF 0xD8) at bytes 01.
/// </summary>
private static double ParseJpegAspectRatio(byte[] bytes)
{
if (bytes.Length < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8)
{
return 1.0;
}
var pos = 2;
while (pos + 9 < bytes.Length)
{
// Marker segments begin with 0xFF; skip any fill bytes before the marker id.
if (bytes[pos] != 0xFF)
{
pos++;
continue;
}
var marker = bytes[pos + 1];
if (marker == 0xC0 || marker == 0xC2)
{
var height = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(pos + 5, 2));
var width = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(pos + 7, 2));
return Ratio(width, height);
}
// Standalone markers (RSTn, SOI, EOI, TEM) carry no length payload; everything
// else has a 2-byte big-endian segment length immediately after the marker id.
if (marker is 0xD8 or 0xD9 or 0x01 || (marker >= 0xD0 && marker <= 0xD7))
{
pos += 2;
continue;
}
var segmentLength = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(pos + 2, 2));
pos += 2 + segmentLength;
}
return 1.0;
}
private static double Ratio(uint width, uint height) => height == 0 ? 1.0 : (double)width / height;
}