DdsImagePlugin.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. """
  2. A Pillow loader for .dds files (S3TC-compressed aka DXTC)
  3. Jerome Leclanche <jerome@leclan.ch>
  4. Documentation:
  5. https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
  6. The contents of this file are hereby released in the public domain (CC0)
  7. Full text of the CC0 license:
  8. https://creativecommons.org/publicdomain/zero/1.0/
  9. """
  10. from __future__ import annotations
  11. import io
  12. import struct
  13. import sys
  14. from enum import IntEnum, IntFlag
  15. from typing import IO
  16. from . import Image, ImageFile, ImagePalette
  17. from ._binary import i32le as i32
  18. from ._binary import o8
  19. from ._binary import o32le as o32
  20. # Magic ("DDS ")
  21. DDS_MAGIC = 0x20534444
  22. # DDS flags
  23. class DDSD(IntFlag):
  24. CAPS = 0x1
  25. HEIGHT = 0x2
  26. WIDTH = 0x4
  27. PITCH = 0x8
  28. PIXELFORMAT = 0x1000
  29. MIPMAPCOUNT = 0x20000
  30. LINEARSIZE = 0x80000
  31. DEPTH = 0x800000
  32. # DDS caps
  33. class DDSCAPS(IntFlag):
  34. COMPLEX = 0x8
  35. TEXTURE = 0x1000
  36. MIPMAP = 0x400000
  37. class DDSCAPS2(IntFlag):
  38. CUBEMAP = 0x200
  39. CUBEMAP_POSITIVEX = 0x400
  40. CUBEMAP_NEGATIVEX = 0x800
  41. CUBEMAP_POSITIVEY = 0x1000
  42. CUBEMAP_NEGATIVEY = 0x2000
  43. CUBEMAP_POSITIVEZ = 0x4000
  44. CUBEMAP_NEGATIVEZ = 0x8000
  45. VOLUME = 0x200000
  46. # Pixel Format
  47. class DDPF(IntFlag):
  48. ALPHAPIXELS = 0x1
  49. ALPHA = 0x2
  50. FOURCC = 0x4
  51. PALETTEINDEXED8 = 0x20
  52. RGB = 0x40
  53. LUMINANCE = 0x20000
  54. # dxgiformat.h
  55. class DXGI_FORMAT(IntEnum):
  56. UNKNOWN = 0
  57. R32G32B32A32_TYPELESS = 1
  58. R32G32B32A32_FLOAT = 2
  59. R32G32B32A32_UINT = 3
  60. R32G32B32A32_SINT = 4
  61. R32G32B32_TYPELESS = 5
  62. R32G32B32_FLOAT = 6
  63. R32G32B32_UINT = 7
  64. R32G32B32_SINT = 8
  65. R16G16B16A16_TYPELESS = 9
  66. R16G16B16A16_FLOAT = 10
  67. R16G16B16A16_UNORM = 11
  68. R16G16B16A16_UINT = 12
  69. R16G16B16A16_SNORM = 13
  70. R16G16B16A16_SINT = 14
  71. R32G32_TYPELESS = 15
  72. R32G32_FLOAT = 16
  73. R32G32_UINT = 17
  74. R32G32_SINT = 18
  75. R32G8X24_TYPELESS = 19
  76. D32_FLOAT_S8X24_UINT = 20
  77. R32_FLOAT_X8X24_TYPELESS = 21
  78. X32_TYPELESS_G8X24_UINT = 22
  79. R10G10B10A2_TYPELESS = 23
  80. R10G10B10A2_UNORM = 24
  81. R10G10B10A2_UINT = 25
  82. R11G11B10_FLOAT = 26
  83. R8G8B8A8_TYPELESS = 27
  84. R8G8B8A8_UNORM = 28
  85. R8G8B8A8_UNORM_SRGB = 29
  86. R8G8B8A8_UINT = 30
  87. R8G8B8A8_SNORM = 31
  88. R8G8B8A8_SINT = 32
  89. R16G16_TYPELESS = 33
  90. R16G16_FLOAT = 34
  91. R16G16_UNORM = 35
  92. R16G16_UINT = 36
  93. R16G16_SNORM = 37
  94. R16G16_SINT = 38
  95. R32_TYPELESS = 39
  96. D32_FLOAT = 40
  97. R32_FLOAT = 41
  98. R32_UINT = 42
  99. R32_SINT = 43
  100. R24G8_TYPELESS = 44
  101. D24_UNORM_S8_UINT = 45
  102. R24_UNORM_X8_TYPELESS = 46
  103. X24_TYPELESS_G8_UINT = 47
  104. R8G8_TYPELESS = 48
  105. R8G8_UNORM = 49
  106. R8G8_UINT = 50
  107. R8G8_SNORM = 51
  108. R8G8_SINT = 52
  109. R16_TYPELESS = 53
  110. R16_FLOAT = 54
  111. D16_UNORM = 55
  112. R16_UNORM = 56
  113. R16_UINT = 57
  114. R16_SNORM = 58
  115. R16_SINT = 59
  116. R8_TYPELESS = 60
  117. R8_UNORM = 61
  118. R8_UINT = 62
  119. R8_SNORM = 63
  120. R8_SINT = 64
  121. A8_UNORM = 65
  122. R1_UNORM = 66
  123. R9G9B9E5_SHAREDEXP = 67
  124. R8G8_B8G8_UNORM = 68
  125. G8R8_G8B8_UNORM = 69
  126. BC1_TYPELESS = 70
  127. BC1_UNORM = 71
  128. BC1_UNORM_SRGB = 72
  129. BC2_TYPELESS = 73
  130. BC2_UNORM = 74
  131. BC2_UNORM_SRGB = 75
  132. BC3_TYPELESS = 76
  133. BC3_UNORM = 77
  134. BC3_UNORM_SRGB = 78
  135. BC4_TYPELESS = 79
  136. BC4_UNORM = 80
  137. BC4_SNORM = 81
  138. BC5_TYPELESS = 82
  139. BC5_UNORM = 83
  140. BC5_SNORM = 84
  141. B5G6R5_UNORM = 85
  142. B5G5R5A1_UNORM = 86
  143. B8G8R8A8_UNORM = 87
  144. B8G8R8X8_UNORM = 88
  145. R10G10B10_XR_BIAS_A2_UNORM = 89
  146. B8G8R8A8_TYPELESS = 90
  147. B8G8R8A8_UNORM_SRGB = 91
  148. B8G8R8X8_TYPELESS = 92
  149. B8G8R8X8_UNORM_SRGB = 93
  150. BC6H_TYPELESS = 94
  151. BC6H_UF16 = 95
  152. BC6H_SF16 = 96
  153. BC7_TYPELESS = 97
  154. BC7_UNORM = 98
  155. BC7_UNORM_SRGB = 99
  156. AYUV = 100
  157. Y410 = 101
  158. Y416 = 102
  159. NV12 = 103
  160. P010 = 104
  161. P016 = 105
  162. OPAQUE_420 = 106
  163. YUY2 = 107
  164. Y210 = 108
  165. Y216 = 109
  166. NV11 = 110
  167. AI44 = 111
  168. IA44 = 112
  169. P8 = 113
  170. A8P8 = 114
  171. B4G4R4A4_UNORM = 115
  172. P208 = 130
  173. V208 = 131
  174. V408 = 132
  175. SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
  176. SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
  177. class D3DFMT(IntEnum):
  178. UNKNOWN = 0
  179. R8G8B8 = 20
  180. A8R8G8B8 = 21
  181. X8R8G8B8 = 22
  182. R5G6B5 = 23
  183. X1R5G5B5 = 24
  184. A1R5G5B5 = 25
  185. A4R4G4B4 = 26
  186. R3G3B2 = 27
  187. A8 = 28
  188. A8R3G3B2 = 29
  189. X4R4G4B4 = 30
  190. A2B10G10R10 = 31
  191. A8B8G8R8 = 32
  192. X8B8G8R8 = 33
  193. G16R16 = 34
  194. A2R10G10B10 = 35
  195. A16B16G16R16 = 36
  196. A8P8 = 40
  197. P8 = 41
  198. L8 = 50
  199. A8L8 = 51
  200. A4L4 = 52
  201. V8U8 = 60
  202. L6V5U5 = 61
  203. X8L8V8U8 = 62
  204. Q8W8V8U8 = 63
  205. V16U16 = 64
  206. A2W10V10U10 = 67
  207. D16_LOCKABLE = 70
  208. D32 = 71
  209. D15S1 = 73
  210. D24S8 = 75
  211. D24X8 = 77
  212. D24X4S4 = 79
  213. D16 = 80
  214. D32F_LOCKABLE = 82
  215. D24FS8 = 83
  216. D32_LOCKABLE = 84
  217. S8_LOCKABLE = 85
  218. L16 = 81
  219. VERTEXDATA = 100
  220. INDEX16 = 101
  221. INDEX32 = 102
  222. Q16W16V16U16 = 110
  223. R16F = 111
  224. G16R16F = 112
  225. A16B16G16R16F = 113
  226. R32F = 114
  227. G32R32F = 115
  228. A32B32G32R32F = 116
  229. CxV8U8 = 117
  230. A1 = 118
  231. A2B10G10R10_XR_BIAS = 119
  232. BINARYBUFFER = 199
  233. UYVY = i32(b"UYVY")
  234. R8G8_B8G8 = i32(b"RGBG")
  235. YUY2 = i32(b"YUY2")
  236. G8R8_G8B8 = i32(b"GRGB")
  237. DXT1 = i32(b"DXT1")
  238. DXT2 = i32(b"DXT2")
  239. DXT3 = i32(b"DXT3")
  240. DXT4 = i32(b"DXT4")
  241. DXT5 = i32(b"DXT5")
  242. DX10 = i32(b"DX10")
  243. BC4S = i32(b"BC4S")
  244. BC4U = i32(b"BC4U")
  245. BC5S = i32(b"BC5S")
  246. BC5U = i32(b"BC5U")
  247. ATI1 = i32(b"ATI1")
  248. ATI2 = i32(b"ATI2")
  249. MULTI2_ARGB8 = i32(b"MET1")
  250. # Backward compatibility layer
  251. module = sys.modules[__name__]
  252. for item in DDSD:
  253. assert item.name is not None
  254. setattr(module, f"DDSD_{item.name}", item.value)
  255. for item1 in DDSCAPS:
  256. assert item1.name is not None
  257. setattr(module, f"DDSCAPS_{item1.name}", item1.value)
  258. for item2 in DDSCAPS2:
  259. assert item2.name is not None
  260. setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
  261. for item3 in DDPF:
  262. assert item3.name is not None
  263. setattr(module, f"DDPF_{item3.name}", item3.value)
  264. DDS_FOURCC = DDPF.FOURCC
  265. DDS_RGB = DDPF.RGB
  266. DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
  267. DDS_LUMINANCE = DDPF.LUMINANCE
  268. DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
  269. DDS_ALPHA = DDPF.ALPHA
  270. DDS_PAL8 = DDPF.PALETTEINDEXED8
  271. DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
  272. DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
  273. DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
  274. DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
  275. DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
  276. DDS_HEIGHT = DDSD.HEIGHT
  277. DDS_WIDTH = DDSD.WIDTH
  278. DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
  279. DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
  280. DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
  281. DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
  282. DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
  283. DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
  284. DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
  285. DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
  286. DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
  287. DXT1_FOURCC = D3DFMT.DXT1
  288. DXT3_FOURCC = D3DFMT.DXT3
  289. DXT5_FOURCC = D3DFMT.DXT5
  290. DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
  291. DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
  292. DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
  293. DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
  294. DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
  295. DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
  296. DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
  297. DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
  298. DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
  299. DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
  300. DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
  301. class DdsImageFile(ImageFile.ImageFile):
  302. format = "DDS"
  303. format_description = "DirectDraw Surface"
  304. def _open(self) -> None:
  305. if not _accept(self.fp.read(4)):
  306. msg = "not a DDS file"
  307. raise SyntaxError(msg)
  308. (header_size,) = struct.unpack("<I", self.fp.read(4))
  309. if header_size != 124:
  310. msg = f"Unsupported header size {repr(header_size)}"
  311. raise OSError(msg)
  312. header_bytes = self.fp.read(header_size - 4)
  313. if len(header_bytes) != 120:
  314. msg = f"Incomplete header: {len(header_bytes)} bytes"
  315. raise OSError(msg)
  316. header = io.BytesIO(header_bytes)
  317. flags, height, width = struct.unpack("<3I", header.read(12))
  318. self._size = (width, height)
  319. extents = (0, 0) + self.size
  320. pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
  321. struct.unpack("<11I", header.read(44)) # reserved
  322. # pixel format
  323. pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16))
  324. n = 0
  325. rawmode = None
  326. if pfflags & DDPF.RGB:
  327. # Texture contains uncompressed RGB data
  328. if pfflags & DDPF.ALPHAPIXELS:
  329. self._mode = "RGBA"
  330. mask_count = 4
  331. else:
  332. self._mode = "RGB"
  333. mask_count = 3
  334. masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
  335. self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
  336. return
  337. elif pfflags & DDPF.LUMINANCE:
  338. if bitcount == 8:
  339. self._mode = "L"
  340. elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
  341. self._mode = "LA"
  342. else:
  343. msg = f"Unsupported bitcount {bitcount} for {pfflags}"
  344. raise OSError(msg)
  345. elif pfflags & DDPF.PALETTEINDEXED8:
  346. self._mode = "P"
  347. self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
  348. self.palette.mode = "RGBA"
  349. elif pfflags & DDPF.FOURCC:
  350. offset = header_size + 4
  351. if fourcc == D3DFMT.DXT1:
  352. self._mode = "RGBA"
  353. self.pixel_format = "DXT1"
  354. n = 1
  355. elif fourcc == D3DFMT.DXT3:
  356. self._mode = "RGBA"
  357. self.pixel_format = "DXT3"
  358. n = 2
  359. elif fourcc == D3DFMT.DXT5:
  360. self._mode = "RGBA"
  361. self.pixel_format = "DXT5"
  362. n = 3
  363. elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
  364. self._mode = "L"
  365. self.pixel_format = "BC4"
  366. n = 4
  367. elif fourcc == D3DFMT.BC5S:
  368. self._mode = "RGB"
  369. self.pixel_format = "BC5S"
  370. n = 5
  371. elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
  372. self._mode = "RGB"
  373. self.pixel_format = "BC5"
  374. n = 5
  375. elif fourcc == D3DFMT.DX10:
  376. offset += 20
  377. # ignoring flags which pertain to volume textures and cubemaps
  378. (dxgi_format,) = struct.unpack("<I", self.fp.read(4))
  379. self.fp.read(16)
  380. if dxgi_format in (
  381. DXGI_FORMAT.BC1_UNORM,
  382. DXGI_FORMAT.BC1_TYPELESS,
  383. ):
  384. self._mode = "RGBA"
  385. self.pixel_format = "BC1"
  386. n = 1
  387. elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
  388. self._mode = "L"
  389. self.pixel_format = "BC4"
  390. n = 4
  391. elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
  392. self._mode = "RGB"
  393. self.pixel_format = "BC5"
  394. n = 5
  395. elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
  396. self._mode = "RGB"
  397. self.pixel_format = "BC5S"
  398. n = 5
  399. elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
  400. self._mode = "RGB"
  401. self.pixel_format = "BC6H"
  402. n = 6
  403. elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
  404. self._mode = "RGB"
  405. self.pixel_format = "BC6HS"
  406. n = 6
  407. elif dxgi_format in (
  408. DXGI_FORMAT.BC7_TYPELESS,
  409. DXGI_FORMAT.BC7_UNORM,
  410. DXGI_FORMAT.BC7_UNORM_SRGB,
  411. ):
  412. self._mode = "RGBA"
  413. self.pixel_format = "BC7"
  414. n = 7
  415. if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
  416. self.info["gamma"] = 1 / 2.2
  417. elif dxgi_format in (
  418. DXGI_FORMAT.R8G8B8A8_TYPELESS,
  419. DXGI_FORMAT.R8G8B8A8_UNORM,
  420. DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
  421. ):
  422. self._mode = "RGBA"
  423. if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
  424. self.info["gamma"] = 1 / 2.2
  425. else:
  426. msg = f"Unimplemented DXGI format {dxgi_format}"
  427. raise NotImplementedError(msg)
  428. else:
  429. msg = f"Unimplemented pixel format {repr(fourcc)}"
  430. raise NotImplementedError(msg)
  431. else:
  432. msg = f"Unknown pixel format flags {pfflags}"
  433. raise NotImplementedError(msg)
  434. if n:
  435. self.tile = [
  436. ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
  437. ]
  438. else:
  439. self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
  440. def load_seek(self, pos: int) -> None:
  441. pass
  442. class DdsRgbDecoder(ImageFile.PyDecoder):
  443. _pulls_fd = True
  444. def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
  445. assert self.fd is not None
  446. bitcount, masks = self.args
  447. # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
  448. # Calculate how many zeros each mask is padded with
  449. mask_offsets = []
  450. # And the maximum value of each channel without the padding
  451. mask_totals = []
  452. for mask in masks:
  453. offset = 0
  454. if mask != 0:
  455. while mask >> (offset + 1) << (offset + 1) == mask:
  456. offset += 1
  457. mask_offsets.append(offset)
  458. mask_totals.append(mask >> offset)
  459. data = bytearray()
  460. bytecount = bitcount // 8
  461. dest_length = self.state.xsize * self.state.ysize * len(masks)
  462. while len(data) < dest_length:
  463. value = int.from_bytes(self.fd.read(bytecount), "little")
  464. for i, mask in enumerate(masks):
  465. masked_value = value & mask
  466. # Remove the zero padding, and scale it to 8 bits
  467. data += o8(
  468. int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
  469. )
  470. self.set_as_raw(data)
  471. return -1, 0
  472. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  473. if im.mode not in ("RGB", "RGBA", "L", "LA"):
  474. msg = f"cannot write mode {im.mode} as DDS"
  475. raise OSError(msg)
  476. alpha = im.mode[-1] == "A"
  477. if im.mode[0] == "L":
  478. pixel_flags = DDPF.LUMINANCE
  479. rawmode = im.mode
  480. if alpha:
  481. rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
  482. else:
  483. rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
  484. else:
  485. pixel_flags = DDPF.RGB
  486. rawmode = im.mode[::-1]
  487. rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
  488. if alpha:
  489. r, g, b, a = im.split()
  490. im = Image.merge("RGBA", (a, r, g, b))
  491. if alpha:
  492. pixel_flags |= DDPF.ALPHAPIXELS
  493. rgba_mask.append(0xFF000000 if alpha else 0)
  494. flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
  495. bitcount = len(im.getbands()) * 8
  496. pitch = (im.width * bitcount + 7) // 8
  497. fp.write(
  498. o32(DDS_MAGIC)
  499. + struct.pack(
  500. "<7I",
  501. 124, # header size
  502. flags, # flags
  503. im.height,
  504. im.width,
  505. pitch,
  506. 0, # depth
  507. 0, # mipmaps
  508. )
  509. + struct.pack("11I", *((0,) * 11)) # reserved
  510. # pfsize, pfflags, fourcc, bitcount
  511. + struct.pack("<4I", 32, pixel_flags, 0, bitcount)
  512. + struct.pack("<4I", *rgba_mask) # dwRGBABitMask
  513. + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
  514. )
  515. ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
  516. def _accept(prefix: bytes) -> bool:
  517. return prefix[:4] == b"DDS "
  518. Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
  519. Image.register_decoder("dds_rgb", DdsRgbDecoder)
  520. Image.register_save(DdsImageFile.format, _save)
  521. Image.register_extension(DdsImageFile.format, ".dds")