| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- namespace SharedKernel.Storage;
- public static class ImageDimensionHelper
- {
- /// <summary>
- /// 이미지 스트림에서 해상도(Width, Height)를 추출합니다.
- /// 지원 포맷: PNG, JPEG, GIF, BMP, WebP
- /// </summary>
- 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;
- }
- /// <summary>
- /// 바이트 배열에서 이미지 해상도(Width, Height)를 추출합니다.
- /// </summary>
- public static (short Width, short Height)? GetDimensions(ReadOnlyMemory<byte> 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);
- }
- }
|