ImageDimensionHelper.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. namespace SharedKernel.Storage;
  2. public static class ImageDimensionHelper
  3. {
  4. /// <summary>
  5. /// 이미지 스트림에서 해상도(Width, Height)를 추출합니다.
  6. /// 지원 포맷: PNG, JPEG, GIF, BMP, WebP
  7. /// </summary>
  8. public static (short Width, short Height)? GetDimensions(Stream stream)
  9. {
  10. if (stream is null || !stream.CanRead)
  11. {
  12. return null;
  13. }
  14. var buffer = new byte[32];
  15. var bytesRead = stream.Read(buffer, 0, buffer.Length);
  16. if (bytesRead < 10)
  17. {
  18. return null;
  19. }
  20. // PNG: 89 50 4E 47 0D 0A 1A 0A
  21. if (bytesRead >= 24 && buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47)
  22. {
  23. var w = ReadInt32BigEndian(buffer, 16);
  24. var h = ReadInt32BigEndian(buffer, 20);
  25. return ToShortPair(w, h);
  26. }
  27. // GIF: 47 49 46 38 (GIF8)
  28. if (bytesRead >= 10 && buffer[0] == 0x47 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x38)
  29. {
  30. var w = ReadUInt16LittleEndian(buffer, 6);
  31. var h = ReadUInt16LittleEndian(buffer, 8);
  32. return ToShortPair(w, h);
  33. }
  34. // BMP: 42 4D (BM)
  35. if (bytesRead >= 26 && buffer[0] == 0x42 && buffer[1] == 0x4D)
  36. {
  37. var w = ReadInt32LittleEndian(buffer, 18);
  38. var h = Math.Abs(ReadInt32LittleEndian(buffer, 22));
  39. return ToShortPair(w, h);
  40. }
  41. // WebP: RIFF....WEBP
  42. if (bytesRead >= 30 && buffer[0] == 0x52 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x46
  43. && buffer[8] == 0x57 && buffer[9] == 0x45 && buffer[10] == 0x42 && buffer[11] == 0x50)
  44. {
  45. return GetWebPDimensions(stream, buffer, bytesRead);
  46. }
  47. // JPEG: FF D8
  48. if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xD8)
  49. {
  50. return GetJpegDimensions(stream);
  51. }
  52. return null;
  53. }
  54. /// <summary>
  55. /// 바이트 배열에서 이미지 해상도(Width, Height)를 추출합니다.
  56. /// </summary>
  57. public static (short Width, short Height)? GetDimensions(ReadOnlyMemory<byte> bytes)
  58. {
  59. if (bytes.Length < 10)
  60. {
  61. return null;
  62. }
  63. using var stream = new MemoryStream(bytes.ToArray(), writable: false);
  64. return GetDimensions(stream);
  65. }
  66. private static (short Width, short Height)? GetJpegDimensions(Stream stream)
  67. {
  68. // 스트림 위치를 2로 이동 (SOI 마커 이후)
  69. stream.Position = 2;
  70. var marker = new byte[2];
  71. var sizeBuffer = new byte[2];
  72. while (stream.Position < stream.Length - 1)
  73. {
  74. // 마커 읽기 (0xFF xx)
  75. if (stream.Read(marker, 0, 2) != 2)
  76. {
  77. break;
  78. }
  79. if (marker[0] != 0xFF)
  80. {
  81. break;
  82. }
  83. // 패딩 0xFF 건너뛰기
  84. while (marker[1] == 0xFF && stream.Position < stream.Length)
  85. {
  86. marker[1] = (byte)stream.ReadByte();
  87. }
  88. var markerType = marker[1];
  89. // SOF 마커 (0xC0 ~ 0xCF, 0xC4/0xC8/0xCC 제외)
  90. if (markerType is >= 0xC0 and <= 0xCF && markerType is not (0xC4 or 0xC8 or 0xCC))
  91. {
  92. var header = new byte[7];
  93. if (stream.Read(header, 0, 7) != 7)
  94. {
  95. break;
  96. }
  97. // header[0..1] = segment length
  98. // header[2] = precision
  99. // header[3..4] = height (big-endian)
  100. // header[5..6] = width (big-endian)
  101. var h = (header[3] << 8) | header[4];
  102. var w = (header[5] << 8) | header[6];
  103. return ToShortPair(w, h);
  104. }
  105. // SOS(0xDA) 또는 EOI(0xD9) → 이미지 데이터 시작/끝
  106. if (markerType is 0xDA or 0xD9)
  107. {
  108. break;
  109. }
  110. // 나머지 세그먼트는 길이만큼 건너뛰기
  111. if (stream.Read(sizeBuffer, 0, 2) != 2)
  112. {
  113. break;
  114. }
  115. var segmentLength = (sizeBuffer[0] << 8) | sizeBuffer[1];
  116. if (segmentLength < 2)
  117. {
  118. break;
  119. }
  120. stream.Position += segmentLength - 2;
  121. }
  122. return null;
  123. }
  124. private static (short Width, short Height)? GetWebPDimensions(Stream stream, byte[] initialBuffer, int initialBytesRead)
  125. {
  126. // VP8 청크 시작: offset 12
  127. if (initialBytesRead < 16)
  128. {
  129. return null;
  130. }
  131. var chunkType = new string(new[] { (char)initialBuffer[12], (char)initialBuffer[13], (char)initialBuffer[14], (char)initialBuffer[15] });
  132. // VP8 (lossy)
  133. if (chunkType == "VP8 ")
  134. {
  135. // VP8 bitstream: offset 12 + 4(chunk header) + 4(chunk size) + 10(frame header)
  136. stream.Position = 26;
  137. var buf = new byte[4];
  138. if (stream.Read(buf, 0, 4) != 4)
  139. {
  140. return null;
  141. }
  142. var w = (buf[0] | (buf[1] << 8)) & 0x3FFF;
  143. var h = (buf[2] | (buf[3] << 8)) & 0x3FFF;
  144. return ToShortPair(w, h);
  145. }
  146. // VP8L (lossless)
  147. if (chunkType == "VP8L")
  148. {
  149. // VP8L: offset 12 + 4 + 4 + 1(signature) = 21
  150. stream.Position = 21;
  151. var buf = new byte[4];
  152. if (stream.Read(buf, 0, 4) != 4)
  153. {
  154. return null;
  155. }
  156. var bits = (uint)(buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
  157. var w = (int)((bits & 0x3FFF) + 1);
  158. var h = (int)(((bits >> 14) & 0x3FFF) + 1);
  159. return ToShortPair(w, h);
  160. }
  161. // VP8X (extended)
  162. if (chunkType == "VP8X")
  163. {
  164. // VP8X: offset 12 + 4 + 4 + 4(flags) = 24, width 3bytes + height 3bytes
  165. stream.Position = 24;
  166. var buf = new byte[6];
  167. if (stream.Read(buf, 0, 6) != 6)
  168. {
  169. return null;
  170. }
  171. var w = (buf[0] | (buf[1] << 8) | (buf[2] << 16)) + 1;
  172. var h = (buf[3] | (buf[4] << 8) | (buf[5] << 16)) + 1;
  173. return ToShortPair(w, h);
  174. }
  175. return null;
  176. }
  177. private static (short Width, short Height)? ToShortPair(int width, int height)
  178. {
  179. if (width <= 0 || height <= 0 || width > short.MaxValue || height > short.MaxValue)
  180. {
  181. return null;
  182. }
  183. return ((short)width, (short)height);
  184. }
  185. private static int ReadInt32BigEndian(byte[] buf, int offset)
  186. {
  187. return (buf[offset] << 24) | (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3];
  188. }
  189. private static int ReadUInt16LittleEndian(byte[] buf, int offset)
  190. {
  191. return buf[offset] | (buf[offset + 1] << 8);
  192. }
  193. private static int ReadInt32LittleEndian(byte[] buf, int offset)
  194. {
  195. return buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24);
  196. }
  197. }