using System.Buffers.Binary; using DeepDrftContent.FileDatabase.Models; namespace DeepDrftContent.Processors; /// /// Processes raw image bytes into an , mirroring the shape of /// . 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. /// /// /// 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. /// public class ImageProcessor { /// /// Builds an from raw image bytes and a MIME content type. /// Returns null when the content type does not resolve to a recognised image extension /// (the .bin sentinel from ). /// 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); } /// /// 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. /// 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; } } /// /// 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. /// 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); } /// /// 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. /// 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; }