namespace SharedKernel.Storage;
public static class ImageDimensionHelper
{
///
/// 이미지 스트림에서 해상도(Width, Height)를 추출합니다.
/// 지원 포맷: PNG, JPEG, GIF, BMP, WebP
///
public static (short Width, short Height)? GetDimensions(Stream stream)
{
if (stream is null || !stream.CanRead)
{
return null;
}
var buffer = new byte[32];
var bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead < 10)
{
return null;
}
// PNG: 89 50 4E 47 0D 0A 1A 0A
if (bytesRead >= 24 && buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47)
{
var w = ReadInt32BigEndian(buffer, 16);
var h = ReadInt32BigEndian(buffer, 20);
return ToShortPair(w, h);
}
// GIF: 47 49 46 38 (GIF8)
if (bytesRead >= 10 && buffer[0] == 0x47 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x38)
{
var w = ReadUInt16LittleEndian(buffer, 6);
var h = ReadUInt16LittleEndian(buffer, 8);
return ToShortPair(w, h);
}
// BMP: 42 4D (BM)
if (bytesRead >= 26 && buffer[0] == 0x42 && buffer[1] == 0x4D)
{
var w = ReadInt32LittleEndian(buffer, 18);
var h = Math.Abs(ReadInt32LittleEndian(buffer, 22));
return ToShortPair(w, h);
}
// WebP: RIFF....WEBP
if (bytesRead >= 30 && buffer[0] == 0x52 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x46
&& buffer[8] == 0x57 && buffer[9] == 0x45 && buffer[10] == 0x42 && buffer[11] == 0x50)
{
return GetWebPDimensions(stream, buffer, bytesRead);
}
// JPEG: FF D8
if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xD8)
{
return GetJpegDimensions(stream);
}
return null;
}
///
/// 바이트 배열에서 이미지 해상도(Width, Height)를 추출합니다.
///
public static (short Width, short Height)? GetDimensions(ReadOnlyMemory bytes)
{
if (bytes.Length < 10)
{
return null;
}
using var stream = new MemoryStream(bytes.ToArray(), writable: false);
return GetDimensions(stream);
}
private static (short Width, short Height)? GetJpegDimensions(Stream stream)
{
// 스트림 위치를 2로 이동 (SOI 마커 이후)
stream.Position = 2;
var marker = new byte[2];
var sizeBuffer = new byte[2];
while (stream.Position < stream.Length - 1)
{
// 마커 읽기 (0xFF xx)
if (stream.Read(marker, 0, 2) != 2)
{
break;
}
if (marker[0] != 0xFF)
{
break;
}
// 패딩 0xFF 건너뛰기
while (marker[1] == 0xFF && stream.Position < stream.Length)
{
marker[1] = (byte)stream.ReadByte();
}
var markerType = marker[1];
// SOF 마커 (0xC0 ~ 0xCF, 0xC4/0xC8/0xCC 제외)
if (markerType is >= 0xC0 and <= 0xCF && markerType is not (0xC4 or 0xC8 or 0xCC))
{
var header = new byte[7];
if (stream.Read(header, 0, 7) != 7)
{
break;
}
// header[0..1] = segment length
// header[2] = precision
// header[3..4] = height (big-endian)
// header[5..6] = width (big-endian)
var h = (header[3] << 8) | header[4];
var w = (header[5] << 8) | header[6];
return ToShortPair(w, h);
}
// SOS(0xDA) 또는 EOI(0xD9) → 이미지 데이터 시작/끝
if (markerType is 0xDA or 0xD9)
{
break;
}
// 나머지 세그먼트는 길이만큼 건너뛰기
if (stream.Read(sizeBuffer, 0, 2) != 2)
{
break;
}
var segmentLength = (sizeBuffer[0] << 8) | sizeBuffer[1];
if (segmentLength < 2)
{
break;
}
stream.Position += segmentLength - 2;
}
return null;
}
private static (short Width, short Height)? GetWebPDimensions(Stream stream, byte[] initialBuffer, int initialBytesRead)
{
// VP8 청크 시작: offset 12
if (initialBytesRead < 16)
{
return null;
}
var chunkType = new string(new[] { (char)initialBuffer[12], (char)initialBuffer[13], (char)initialBuffer[14], (char)initialBuffer[15] });
// VP8 (lossy)
if (chunkType == "VP8 ")
{
// VP8 bitstream: offset 12 + 4(chunk header) + 4(chunk size) + 10(frame header)
stream.Position = 26;
var buf = new byte[4];
if (stream.Read(buf, 0, 4) != 4)
{
return null;
}
var w = (buf[0] | (buf[1] << 8)) & 0x3FFF;
var h = (buf[2] | (buf[3] << 8)) & 0x3FFF;
return ToShortPair(w, h);
}
// VP8L (lossless)
if (chunkType == "VP8L")
{
// VP8L: offset 12 + 4 + 4 + 1(signature) = 21
stream.Position = 21;
var buf = new byte[4];
if (stream.Read(buf, 0, 4) != 4)
{
return null;
}
var bits = (uint)(buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
var w = (int)((bits & 0x3FFF) + 1);
var h = (int)(((bits >> 14) & 0x3FFF) + 1);
return ToShortPair(w, h);
}
// VP8X (extended)
if (chunkType == "VP8X")
{
// VP8X: offset 12 + 4 + 4 + 4(flags) = 24, width 3bytes + height 3bytes
stream.Position = 24;
var buf = new byte[6];
if (stream.Read(buf, 0, 6) != 6)
{
return null;
}
var w = (buf[0] | (buf[1] << 8) | (buf[2] << 16)) + 1;
var h = (buf[3] | (buf[4] << 8) | (buf[5] << 16)) + 1;
return ToShortPair(w, h);
}
return null;
}
private static (short Width, short Height)? ToShortPair(int width, int height)
{
if (width <= 0 || height <= 0 || width > short.MaxValue || height > short.MaxValue)
{
return null;
}
return ((short)width, (short)height);
}
private static int ReadInt32BigEndian(byte[] buf, int offset)
{
return (buf[offset] << 24) | (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3];
}
private static int ReadUInt16LittleEndian(byte[] buf, int offset)
{
return buf[offset] | (buf[offset + 1] << 8);
}
private static int ReadInt32LittleEndian(byte[] buf, int offset)
{
return buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24);
}
}