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); } }