130 lines
4.8 KiB
C#
130 lines
4.8 KiB
C#
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 16–19 and height at 20–23, both big-endian
|
||
/// uint32. Guards on the "PNG" signature at bytes 1–3.
|
||
/// </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 0–1.
|
||
/// </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;
|
||
}
|