wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
blp.cpp
Go to the documentation of this file.
1
4
7
8#include <algorithm>
9#include <cstring>
10#include <format>
11#include <limits>
12#include <ranges>
13#include <unordered_map>
14#include <utility>
15
16#define STB_DXT_IMPLEMENTATION
17#include <stb_dxt.h>
18
20 namespace {
22 constexpr std::uint8_t expand5(std::uint32_t v) noexcept {
23 return static_cast<std::uint8_t>((v << 3) | (v >> 2));
24 }
25
27 constexpr std::uint8_t expand6(std::uint32_t v) noexcept {
28 return static_cast<std::uint8_t>((v << 2) | (v >> 4));
29 }
30
32 std::uint16_t readU16(const std::uint8_t* at) noexcept {
33 return static_cast<std::uint16_t>(at[0] | (at[1] << 8));
34 }
35
38 void placeBlock(const BlockPixels& block,
39 std::span<std::uint8_t> rgba,
40 std::uint32_t width,
41 std::uint32_t height,
42 std::uint32_t bx,
43 std::uint32_t by) noexcept {
44 for (std::uint32_t py = 0; py < 4; ++py) {
45 const std::uint32_t y = by * 4 + py;
46 if (y >= height) break;
47 for (std::uint32_t px = 0; px < 4; ++px) {
48 const std::uint32_t x = bx * 4 + px;
49 if (x >= width) break;
50 const std::size_t src = (py * 4 + px) * std::size_t{4};
51 const std::size_t dst = (std::size_t{y} * width + x) * 4;
52 std::copy_n(block.data() + src, 4, rgba.data() + dst);
53 }
54 }
55 }
56 }
57
58 // --- DxtCodec ---------------------------------------------------------------
59
60 void DxtCodec::_decodeColorBlock(const std::uint8_t* block, BlockPixels& out, bool punchThrough) noexcept {
61 const std::uint16_t c0 = readU16(block);
62 const std::uint16_t c1 = readU16(block + 2);
63
64 std::array<std::array<std::uint8_t, 4>, 4> colors{};
65 const auto expand = [](std::uint16_t c) -> std::array<std::uint8_t, 4> {
66 return {expand5((c >> 11) & 0x1F), expand6((c >> 5) & 0x3F), expand5(c & 0x1F), 255};
67 };
68 colors[0] = expand(c0);
69 colors[1] = expand(c1);
70
71 if (!punchThrough || c0 > c1) {
72 for (int ch = 0; ch < 3; ++ch) {
73 colors[2][static_cast<std::size_t>(ch)] = static_cast<std::uint8_t>((2 * colors[0][static_cast<std::size_t>(ch)]
74 + colors[1][static_cast<std::size_t>(ch)]) / 3);
75 colors[3][static_cast<std::size_t>(ch)] = static_cast<std::uint8_t>((colors[0][static_cast<std::size_t>(ch)] + 2
76 * colors[1][static_cast<std::size_t>(ch)]) / 3);
77 }
78 colors[2][3] = 255;
79 colors[3][3] = 255;
80 }
81 else {
82 for (int ch = 0; ch < 3; ++ch)
83 colors[2][static_cast<std::size_t>(ch)] = static_cast<std::uint8_t>((colors[0][static_cast<std::size_t>(ch)] +
84 colors[1][static_cast<std::size_t>(ch)]) / 2);
85 colors[2][3] = 255;
86 colors[3] = {0, 0, 0, 0}; // the punch-through texel
87 }
88
89 for (std::uint32_t texel = 0; texel < 16; ++texel) {
90 const std::uint32_t bits = block[4 + texel / 4];
91 const std::uint32_t index = (bits >> (2 * (texel % 4))) & 0x3;
92 std::copy_n(colors[index].data(), 4, out.data() + texel * std::size_t{4});
93 }
94 }
95
96 void DxtCodec::_decodeExplicitAlpha(const std::uint8_t* block, BlockPixels& out) noexcept {
97 for (std::uint32_t texel = 0; texel < 16; ++texel) {
98 const std::uint32_t nibble = (block[texel / 2] >> (4 * (texel % 2))) & 0xF;
99 out[texel * 4 + 3] = static_cast<std::uint8_t>(nibble * 0x11);
100 }
101 }
102
103 void DxtCodec::_decodeInterpolatedAlpha(const std::uint8_t* block, std::span<std::uint8_t, 16> out) noexcept {
104 const std::uint8_t a0 = block[0];
105 const std::uint8_t a1 = block[1];
106 std::array<std::uint8_t, 8> values{a0, a1};
107 if (a0 > a1)
108 for (int i = 1; i <= 6; ++i)
109 values[static_cast<std::size_t>(i) + 1] = static_cast<std::uint8_t>(((7 - i) * a0 + i * a1) / 7);
110 else {
111 for (int i = 1; i <= 4; ++i)
112 values[static_cast<std::size_t>(i) + 1] = static_cast<std::uint8_t>(((5 - i) * a0 + i * a1) / 5);
113 values[6] = 0;
114 values[7] = 255;
115 }
116
117 std::uint64_t bits = 0;
118 for (int i = 0; i < 6; ++i) bits |= static_cast<std::uint64_t>(block[2 + i]) << (8 * i);
119 for (std::uint32_t texel = 0; texel < 16; ++texel) out[texel] = values[(bits >> (3 * texel)) & 0x7];
120 }
121
122 BlockPixels DxtCodec::_extractBlock(std::span<const std::uint8_t> rgba,
123 std::uint32_t width,
124 std::uint32_t height,
125 std::uint32_t bx,
126 std::uint32_t by) noexcept {
127 BlockPixels out{};
128 for (std::uint32_t py = 0; py < 4; ++py) {
129 const std::uint32_t y = std::min(by * 4 + py, height - 1);
130 for (std::uint32_t px = 0; px < 4; ++px) {
131 const std::uint32_t x = std::min(bx * 4 + px, width - 1);
132 const std::size_t src = (std::size_t{y} * width + x) * 4;
133 std::copy_n(rgba.data() + src, 4, out.data() + (py * 4 + px) * std::size_t{4});
134 }
135 }
136 return out;
137 }
138
139 void DxtCodec::_encodePunchThroughBlock(const BlockPixels& pixels, std::uint8_t* dest) noexcept {
140 // bounding-box endpoints over the opaque texels
141 std::uint8_t lo[3] = {255, 255, 255};
142 std::uint8_t hi[3] = {0, 0, 0};
143 bool anyOpaque = false;
144 for (std::uint32_t texel = 0; texel < 16; ++texel) {
145 if (pixels[texel * 4 + 3] < 128) continue;
146 anyOpaque = true;
147 for (int ch = 0; ch < 3; ++ch) {
148 lo[ch] = std::min(lo[ch], pixels[texel * 4 + static_cast<std::size_t>(ch)]);
149 hi[ch] = std::max(hi[ch], pixels[texel * 4 + static_cast<std::size_t>(ch)]);
150 }
151 }
152 if (!anyOpaque) {
153 lo[0] = lo[1] = lo[2] = 0;
154 hi[0] = hi[1] = hi[2] = 0;
155 }
156
157 const auto pack565 = [](const std::uint8_t* c) -> std::uint16_t {
158 return static_cast<std::uint16_t>(((c[0] >> 3) << 11) | ((c[1] >> 2) << 5) | (c[2] >> 3));
159 };
160 std::uint16_t c0 = pack565(lo);
161 std::uint16_t c1 = pack565(hi);
162 if (c0 > c1) std::swap(c0, c1);
163 // color0 <= color1 selects the 3-color + transparent mode
164
165 const std::array<std::array<std::int32_t, 3>, 3> palette = {
166 {
167 {expand5((c0 >> 11) & 0x1F), expand6((c0 >> 5) & 0x3F), expand5(c0 & 0x1F)},
168 {expand5((c1 >> 11) & 0x1F), expand6((c1 >> 5) & 0x3F), expand5(c1 & 0x1F)},
169 {
170 (expand5((c0 >> 11) & 0x1F) + expand5((c1 >> 11) & 0x1F)) / 2,
171 (expand6((c0 >> 5) & 0x3F) + expand6((c1 >> 5) & 0x3F)) / 2,
172 (expand5(c0 & 0x1F) + expand5(c1 & 0x1F)) / 2
173 },
174 }
175 };
176
177 dest[0] = static_cast<std::uint8_t>(c0 & 0xFF);
178 dest[1] = static_cast<std::uint8_t>(c0 >> 8);
179 dest[2] = static_cast<std::uint8_t>(c1 & 0xFF);
180 dest[3] = static_cast<std::uint8_t>(c1 >> 8);
181 for (std::uint32_t row = 0; row < 4; ++row) {
182 std::uint8_t bits = 0;
183 for (std::uint32_t col = 0; col < 4; ++col) {
184 const std::uint32_t texel = row * 4 + col;
185 std::uint32_t index = 3; // transparent
186 if (pixels[texel * 4 + 3] >= 128) {
187 std::int64_t best = -1;
188 for (std::uint32_t candidate = 0; candidate < 3; ++candidate) {
189 std::int64_t dist = 0;
190 for (int ch = 0; ch < 3; ++ch) {
191 const std::int64_t d = palette[candidate][static_cast<std::size_t>(ch)] - pixels[texel * 4 + static_cast<
192 std::size_t>(ch)];
193 dist += d * d;
194 }
195 if (best < 0 || dist < best) {
196 best = dist;
197 index = candidate;
198 }
199 }
200 }
201 bits |= static_cast<std::uint8_t>(index << (2 * col));
202 }
203 dest[4 + row] = bits;
204 }
205 }
206
207 Result<std::vector<std::uint8_t>> DxtCodec::decode(std::span<const std::byte> src,
208 std::uint32_t width,
209 std::uint32_t height) const {
210 const std::uint32_t blocksX = (width + 3) / 4;
211 const std::uint32_t blocksY = (height + 3) / 4;
212 std::vector<std::uint8_t> rgba(std::size_t{width} * height * 4, 0);
213
214 const std::size_t stride = blockBytes();
215 std::size_t at = 0;
216 for (std::uint32_t by = 0; by < blocksY; ++by)
217 for (std::uint32_t bx = 0; bx < blocksX; ++bx, at += stride) {
218 if (at + stride > src.size()) return rgba;
219 // understated mip size (known client quirk): pad transparent black
220
221 // std::byte -> the byte values the block decoders read
222 std::array<std::uint8_t, 16> raw{};
223 std::memcpy(raw.data(), src.data() + at, stride);
224
225 BlockPixels block{};
226 switch (_format) {
228 _decodeColorBlock(raw.data(), block, true);
229 break;
231 _decodeColorBlock(raw.data() + 8, block, false);
232 _decodeExplicitAlpha(raw.data(), block);
233 break;
234 case PixelFormat::Dxt5: {
235 _decodeColorBlock(raw.data() + 8, block, false);
236 std::array<std::uint8_t, 16> alpha{};
237 _decodeInterpolatedAlpha(raw.data(), alpha);
238 for (std::uint32_t texel = 0; texel < 16; ++texel) block[texel * 4 + 3] = alpha[texel];
239 break;
240 }
241 case PixelFormat::Bc5: {
242 std::array<std::uint8_t, 16> red{};
243 std::array<std::uint8_t, 16> green{};
244 _decodeInterpolatedAlpha(raw.data(), red);
245 _decodeInterpolatedAlpha(raw.data() + 8, green);
246 for (std::uint32_t texel = 0; texel < 16; ++texel) {
247 block[texel * 4 + 0] = red[texel];
248 block[texel * 4 + 1] = green[texel];
249 block[texel * 4 + 2] = 0;
250 block[texel * 4 + 3] = 255;
251 }
252 break;
253 }
254 default:
256 std::format("BLP DXT decode: pixel format {} is not a block format",
257 std::to_underlying(_format)));
258 }
259 placeBlock(block, rgba, width, height, bx, by);
260 }
261 return rgba;
262 }
263
264 Result<FileBuffer> DxtCodec::encode(std::span<const std::uint8_t> rgba,
265 std::uint32_t width,
266 std::uint32_t height) const {
267 const std::uint32_t blocksX = (width + 3) / 4;
268 const std::uint32_t blocksY = (height + 3) / 4;
269 const std::size_t stride = blockBytes();
270 FileBuffer out(std::size_t{blocksX} * blocksY * stride);
271
272 std::size_t at = 0;
273 for (std::uint32_t by = 0; by < blocksY; ++by)
274 for (std::uint32_t bx = 0; bx < blocksX; ++bx, at += stride) {
275 const BlockPixels block = _extractBlock(rgba, width, height, bx, by);
276 std::array<std::uint8_t, 16> encoded{};
277 switch (_format) {
278 case PixelFormat::Dxt1: {
279 const bool transparent = std::ranges::any_of(std::views::iota(0u, 16u), [&](std::uint32_t texel) {
280 return block[texel * 4 + 3] < 128;
281 });
282 if (transparent) _encodePunchThroughBlock(block, encoded.data());
283 else
284 stb_compress_dxt_block(encoded.data(), block.data(), 0, STB_DXT_HIGHQUAL);
285 break;
286 }
287 case PixelFormat::Dxt3: {
288 // explicit 4-bit alpha block, then the (always 4-color) color block
289 for (std::uint32_t texel = 0; texel < 16; ++texel) {
290 const std::uint32_t nibble = std::min<std::uint32_t>(15, (block[texel * 4 + 3] + std::uint32_t{8}) / 17);
291 encoded[texel / 2] |= static_cast<std::uint8_t>(nibble << (4 * (texel % 2)));
292 }
293 stb_compress_dxt_block(encoded.data() + 8, block.data(), 0, STB_DXT_HIGHQUAL);
294 break;
295 }
297 stb_compress_dxt_block(encoded.data(), block.data(), 1, STB_DXT_HIGHQUAL);
298 break;
299 case PixelFormat::Bc5: {
300 std::array<std::uint8_t, 16 * 2> rg{};
301 for (std::uint32_t texel = 0; texel < 16; ++texel) {
302 rg[texel * 2 + 0] = block[texel * 4 + 0];
303 rg[texel * 2 + 1] = block[texel * 4 + 1];
304 }
305 stb_compress_bc5_block(encoded.data(), rg.data());
306 break;
307 }
308 default:
310 std::format("BLP DXT encode: pixel format {} is not a block format",
311 std::to_underlying(_format)));
312 }
313 std::memcpy(out.data() + at, encoded.data(), stride);
314 }
315 return out;
316 }
317
318 // --- PaletteCodec -----------------------------------------------------------
319
320 std::uint8_t PaletteCodec::_unpackAlpha(std::span<const std::byte> plane, std::size_t index) const noexcept {
321 switch (_alphaDepth) {
322 case 1: {
323 if (index / 8 >= plane.size()) return 0;
324 const auto bits = std::to_integer<std::uint8_t>(plane[index / 8]);
325 return ((bits >> (index % 8)) & 1) ? 255 : 0;
326 }
327 case 4: {
328 if (index / 2 >= plane.size()) return 0;
329 const auto bits = std::to_integer<std::uint8_t>(plane[index / 2]);
330 const std::uint8_t nibble = (bits >> (4 * (index % 2))) & 0xF;
331 return static_cast<std::uint8_t>(nibble * 0x11);
332 }
333 case 8:
334 return index < plane.size() ? std::to_integer<std::uint8_t>(plane[index]) : 0;
335 default:
336 return 255; // depth 0: fully opaque
337 }
338 }
339
340 void PaletteCodec::_packAlpha(std::uint8_t alpha, std::span<std::byte> plane, std::size_t index) const noexcept {
341 switch (_alphaDepth) {
342 case 1:
343 if (alpha >= 128) plane[index / 8] |= static_cast<std::byte>(1 << (index % 8));
344 break;
345 case 4: {
346 const std::uint32_t nibble = std::min<std::uint32_t>(15, (alpha + std::uint32_t{8}) / 17);
347 plane[index / 2] |= static_cast<std::byte>(nibble << (4 * (index % 2)));
348 break;
349 }
350 case 8:
351 plane[index] = static_cast<std::byte>(alpha);
352 break;
353 default:
354 break; // depth 0: no plane
355 }
356 }
357
358 std::uint8_t PaletteCodec::_nearestIndex(std::span<const CImVector, BlpPaletteSize> palette,
359 std::uint8_t r,
360 std::uint8_t g,
361 std::uint8_t b) noexcept {
362 std::uint32_t bestIndex = 0;
363 std::int64_t best = -1;
364 for (std::uint32_t i = 0; i < BlpPaletteSize; ++i) {
365 const std::int64_t dr = std::int64_t{palette[i].r} - r;
366 const std::int64_t dg = std::int64_t{palette[i].g} - g;
367 const std::int64_t db = std::int64_t{palette[i].b} - b;
368 const std::int64_t dist = dr * dr + dg * dg + db * db;
369 if (best < 0 || dist < best) {
370 best = dist;
371 bestIndex = i;
372 }
373 }
374 return static_cast<std::uint8_t>(bestIndex);
375 }
376
378 std::span<const CImVector, BlpPaletteSize> palette,
379 std::uint32_t width,
380 std::uint32_t height) const {
381 const std::size_t texels = std::size_t{width} * height;
382 std::vector<std::uint8_t> rgba(texels * 4, 0);
383 const std::span<const std::byte> plane = src.size() > texels ? src.subspan(texels) : std::span<const std::byte>{};
384
385 for (std::size_t i = 0; i < texels && i < src.size(); ++i) {
386 const CImVector& color = palette[std::to_integer<std::uint8_t>(src[i])];
387 rgba[i * 4 + 0] = color.r;
388 rgba[i * 4 + 1] = color.g;
389 rgba[i * 4 + 2] = color.b;
390 rgba[i * 4 + 3] = _unpackAlpha(plane, i);
391 }
392 return rgba;
393 }
394
395 Result<FileBuffer> PaletteCodec::encode(std::span<const std::uint8_t> rgba,
396 std::span<const CImVector, BlpPaletteSize> palette,
397 std::uint32_t width,
398 std::uint32_t height) const {
399 const std::size_t texels = std::size_t{width} * height;
400 FileBuffer out(encodedSize(width, height));
401 const std::span<std::byte> plane = std::span{out}.subspan(texels);
402
403 // memoize distinct colors: _nearestIndex is a 256-entry scan, and images
404 // repeat colors heavily
405 std::unordered_map<std::uint32_t, std::uint8_t> memo;
406 for (std::size_t i = 0; i < texels; ++i) {
407 const std::uint8_t r = rgba[i * 4 + 0];
408 const std::uint8_t g = rgba[i * 4 + 1];
409 const std::uint8_t b = rgba[i * 4 + 2];
410 const std::uint32_t key = (std::uint32_t{r} << 16) | (std::uint32_t{g} << 8) | b;
411 auto found = memo.find(key);
412 if (found == memo.end()) found = memo.emplace(key, _nearestIndex(palette, r, g, b)).first;
413 out[i] = static_cast<std::byte>(found->second);
414 _packAlpha(rgba[i * 4 + 3], plane, i);
415 }
416 return out;
417 }
418
419 std::array<CImVector, BlpPaletteSize> PaletteCodec::buildPalette(std::span<const std::uint8_t> rgba) const {
420 // collect distinct opaque RGB values with occurrence counts
421 std::unordered_map<std::uint32_t, std::uint32_t> counts;
422 for (std::size_t i = 0; i + 3 < rgba.size(); i += 4) {
423 const std::uint32_t key = (std::uint32_t{rgba[i]} << 16) | (std::uint32_t{rgba[i + 1]} << 8) | rgba[i + 2];
424 ++counts[key];
425 }
426
427 std::array<CImVector, BlpPaletteSize> palette{};
428 const auto entry = [](std::uint32_t key) {
429 return CImVector{
430 .b = static_cast<std::uint8_t>(key & 0xFF),
431 .g = static_cast<std::uint8_t>((key >> 8) & 0xFF),
432 .r = static_cast<std::uint8_t>((key >> 16) & 0xFF),
433 .a = 0
434 };
435 };
436
437 if (counts.size() <= BlpPaletteSize) {
438 std::size_t at = 0;
439 for (const auto& [key, count] : counts) palette[at++] = entry(key);
440 return palette;
441 }
442
443 // median cut: split the box with the longest axis at its median until 256
444 struct Point {
445 std::uint8_t r, g, b;
446 std::uint32_t count;
447 };
448 std::vector<Point> points;
449 points.reserve(counts.size());
450 for (const auto& [key, count] : counts)
451 points.push_back({
452 static_cast<std::uint8_t>((key >> 16) & 0xFF),
453 static_cast<std::uint8_t>((key >> 8) & 0xFF),
454 static_cast<std::uint8_t>(key & 0xFF),
455 count
456 });
457
458 struct Box {
459 std::size_t begin, end; // the points range
460 };
461 std::vector<Box> boxes{{0, points.size()}};
462 const auto channel = [](const Point& p, int ch) {
463 return ch == 0 ? p.r : ch == 1 ? p.g : p.b;
464 };
465
466 while (boxes.size() < BlpPaletteSize) {
467 // the box with the longest color axis; stop when nothing is splittable
468 std::size_t pick = boxes.size();
469 int pickAxis = 0;
470 int pickExtent = -1;
471 for (std::size_t i = 0; i < boxes.size(); ++i) {
472 if (boxes[i].end - boxes[i].begin < 2) continue;
473 for (int axis = 0; axis < 3; ++axis) {
474 int lo = 256;
475 int hi = -1;
476 for (std::size_t p = boxes[i].begin; p < boxes[i].end; ++p) {
477 lo = std::min<int>(lo, channel(points[p], axis));
478 hi = std::max<int>(hi, channel(points[p], axis));
479 }
480 if (hi - lo > pickExtent) {
481 pickExtent = hi - lo;
482 pick = i;
483 pickAxis = axis;
484 }
485 }
486 }
487 if (pick == boxes.size() || pickExtent <= 0) break;
488
489 Box& box = boxes[pick];
490 const auto mid = points.begin() + static_cast<std::ptrdiff_t>(box.begin + (box.end - box.begin) / 2);
491 std::nth_element(points.begin() + static_cast<std::ptrdiff_t>(box.begin), mid,
492 points.begin() + static_cast<std::ptrdiff_t>(box.end), [&](const Point& a, const Point& other) {
493 return channel(a, pickAxis) < channel(other, pickAxis);
494 });
495 const std::size_t split = box.begin + (box.end - box.begin) / 2;
496 const std::size_t oldEnd = box.end;
497 box.end = split;
498 boxes.push_back({split, oldEnd});
499 }
500
501 // each box contributes its count-weighted average color
502 for (std::size_t i = 0; i < boxes.size(); ++i) {
503 std::uint64_t r = 0;
504 std::uint64_t g = 0;
505 std::uint64_t b = 0;
506 std::uint64_t total = 0;
507 for (std::size_t p = boxes[i].begin; p < boxes[i].end; ++p) {
508 r += std::uint64_t{points[p].r} * points[p].count;
509 g += std::uint64_t{points[p].g} * points[p].count;
510 b += std::uint64_t{points[p].b} * points[p].count;
511 total += points[p].count;
512 }
513 if (total == 0) continue;
514 palette[i] = CImVector{
515 .b = static_cast<std::uint8_t>(b / total),
516 .g = static_cast<std::uint8_t>(g / total),
517 .r = static_cast<std::uint8_t>(r / total),
518 .a = 0
519 };
520 }
521 return palette;
522 }
523
524 // --- RawCodec ---------------------------------------------------------------
525
526 Result<std::vector<std::uint8_t>> RawCodec::decode(std::span<const std::byte> src,
527 std::uint32_t width,
528 std::uint32_t height) const {
529 const std::size_t texels = std::size_t{width} * height;
530 std::vector<std::uint8_t> rgba(texels * 4, 0);
531 for (std::size_t i = 0; i < texels && i * 4 + 3 < src.size(); ++i) {
532 rgba[i * 4 + 0] = std::to_integer<std::uint8_t>(src[i * 4 + 2]);
533 rgba[i * 4 + 1] = std::to_integer<std::uint8_t>(src[i * 4 + 1]);
534 rgba[i * 4 + 2] = std::to_integer<std::uint8_t>(src[i * 4 + 0]);
535 rgba[i * 4 + 3] = std::to_integer<std::uint8_t>(src[i * 4 + 3]);
536 }
537 return rgba;
538 }
539
540 Result<FileBuffer> RawCodec::encode(std::span<const std::uint8_t> rgba,
541 std::uint32_t width,
542 std::uint32_t height) const {
543 const std::size_t texels = std::size_t{width} * height;
544 FileBuffer out(texels * 4);
545 for (std::size_t i = 0; i < texels; ++i) {
546 out[i * 4 + 0] = static_cast<std::byte>(rgba[i * 4 + 2]);
547 out[i * 4 + 1] = static_cast<std::byte>(rgba[i * 4 + 1]);
548 out[i * 4 + 2] = static_cast<std::byte>(rgba[i * 4 + 0]);
549 out[i * 4 + 3] = static_cast<std::byte>(rgba[i * 4 + 3]);
550 }
551 return out;
552 }
553
554 // --- MipmapScaler -----------------------------------------------------------
555
556 std::vector<std::uint8_t> MipmapScaler::downscale(std::span<const std::uint8_t> rgba,
557 std::uint32_t width,
558 std::uint32_t height) const {
559 const std::uint32_t outW = std::max<std::uint32_t>(1, width / 2);
560 const std::uint32_t outH = std::max<std::uint32_t>(1, height / 2);
561 std::vector<std::uint8_t> out(std::size_t{outW} * outH * 4);
562
563 for (std::uint32_t y = 0; y < outH; ++y)
564 for (std::uint32_t x = 0; x < outW; ++x) {
565 const std::uint32_t x0 = std::min(2 * x, width - 1);
566 const std::uint32_t x1 = std::min(2 * x + 1, width - 1);
567 const std::uint32_t y0 = std::min(2 * y, height - 1);
568 const std::uint32_t y1 = std::min(2 * y + 1, height - 1);
569 for (std::uint32_t ch = 0; ch < 4; ++ch) {
570 const int sum = rgba[(std::size_t{y0} * width + x0) * 4 + ch] + rgba[(std::size_t{y0} * width + x1) * 4 + ch]
571 + rgba[(std::size_t{y1} * width + x0) * 4 + ch] + rgba[(std::size_t{y1} * width + x1) * 4 + ch];
572 out[(std::size_t{y} * outW + x) * 4 + ch] = static_cast<std::uint8_t>((sum + 2) / 4);
573 }
574 }
575 return out;
576 }
577}
578
579namespace wowlib::formats::blp {
580 namespace {
583 PixelFormat resolveDxtFormat(PixelFormat preferred, std::uint8_t alphaDepth) noexcept {
584 switch (preferred) {
588 case PixelFormat::Bc5:
589 return preferred;
590 default:
591 return alphaDepth <= 1 ? PixelFormat::Dxt1 : alphaDepth == 4 ? PixelFormat::Dxt3 : PixelFormat::Dxt5;
592 }
593 }
594 }
595
596 Result<void> BLP::read(std::span<const std::byte> data) {
597 if (data.size() < BlpHeaderBytes)
599 std::format("BLP file is {} bytes; the header region is {}", data.size(), BlpHeaderBytes));
600
601 detail::BLPHeader header{};
602 std::memcpy(&header, data.data(), sizeof header);
603 if (header.magic != BlpMagic)
605 std::format("not a BLP2 file (magic {:#010x})", header.magic));
606 if (header.colorEncoding > std::to_underlying(ColorEncoding::BgraAlt))
607 return makeError(ErrorCode::NotSupported, std::format("BLP colorEncoding {} is unknown", header.colorEncoding));
608 if (header.preferredFormat > std::to_underlying(PixelFormat::Bc5) || header.preferredFormat == 10)
610 std::format("BLP preferredFormat {} is unknown", header.preferredFormat));
611
612 *this = BLP{};
613 version = header.version;
614 colorEncoding = static_cast<ColorEncoding>(header.colorEncoding);
615 alphaDepth = header.alphaDepth;
616 preferredFormat = static_cast<PixelFormat>(header.preferredFormat);
617 mipFlags = header.mipFlags;
618 width = header.width;
619 height = header.height;
620 std::memcpy(palette.data(), data.data() + sizeof header, sizeof palette);
621
622 mips.clear();
623 for (std::size_t level = 0; level < BlpMaxMips; ++level) {
624 const std::uint32_t offset = header.mipOffsets[level];
625 const std::uint32_t size = header.mipSizes[level];
626 if (offset == 0 || size == 0) continue;
627 if (std::uint64_t{offset} + size > data.size())
629 std::format("BLP mip {} spans [{}, {}) beyond the {}-byte file", level, offset,
630 std::uint64_t{offset} + size, data.size()));
631 mips.resize(std::max(mips.size(), level + 1));
632 mips[level].assign(data.begin() + offset, data.begin() + offset + size);
633 }
634
635 // record the on-disk placement + every byte no region claims, so write()
636 // can replay unusual layouts byte-perfectly
637 storedLayout.offsets = header.mipOffsets;
638 storedLayout.sizes = header.mipSizes;
639 storedLayout.fileSize = static_cast<std::uint32_t>(data.size());
640 std::vector<std::pair<std::uint64_t, std::uint64_t>> covered{{0, BlpHeaderBytes}};
641 for (std::size_t level = 0; level < BlpMaxMips; ++level)
642 if (header.mipOffsets[level] != 0 && header.mipSizes[level] != 0)
643 covered.emplace_back(header.mipOffsets[level],
644 std::uint64_t{header.mipOffsets[level]} + header.mipSizes[level]);
645 std::ranges::sort(covered);
646 storedLayout.gaps.clear();
647 std::uint64_t reached = 0;
648 for (const auto& [begin, end] : covered) {
649 if (begin > reached)
650 storedLayout.gaps.push_back({
651 static_cast<std::uint32_t>(reached),
653 data.begin() + static_cast<std::ptrdiff_t>(reached),
654 data.begin() + static_cast<std::ptrdiff_t>(begin)
655 }
656 });
657 reached = std::max(reached, end);
658 }
659 if (reached < data.size())
660 storedLayout.gaps.push_back({
661 static_cast<std::uint32_t>(reached),
662 FileBuffer{data.begin() + static_cast<std::ptrdiff_t>(reached), data.end()}
663 });
664 storedLayout.engaged = true;
665 return {};
666 }
667
669 detail::BLPHeader header{};
670 header.magic = BlpMagic;
671 header.version = version;
672 header.colorEncoding = std::to_underlying(colorEncoding);
673 header.alphaDepth = alphaDepth;
674 header.preferredFormat = std::to_underlying(preferredFormat);
675 header.mipFlags = mipFlags;
676 header.width = width;
677 header.height = height;
678
679 // replay the recorded layout while every payload still matches its
680 // recorded size; otherwise lay the file out canonically
681 bool replay = storedLayout.engaged;
682 if (replay)
683 for (std::size_t level = 0; level < BlpMaxMips; ++level) {
684 const bool stored = storedLayout.offsets[level] != 0 && storedLayout.sizes[level] != 0;
685 const bool present = level < mips.size() && !mips[level].empty();
686 if (stored != present || (stored && mips[level].size() != storedLayout.sizes[level])) {
687 replay = false;
688 break;
689 }
690 }
691
692 if (replay) {
693 header.mipOffsets = storedLayout.offsets;
694 header.mipSizes = storedLayout.sizes;
695 FileBuffer out(storedLayout.fileSize);
696 std::memcpy(out.data(), &header, sizeof header);
697 std::memcpy(out.data() + sizeof header, palette.data(), sizeof palette);
698 for (std::size_t level = 0; level < BlpMaxMips; ++level)
699 if (header.mipOffsets[level] != 0 && header.mipSizes[level] != 0)
700 std::memcpy(out.data() + header.mipOffsets[level], mips[level].data(), mips[level].size());
701 for (const detail::StoredLayout::Run& gap : storedLayout.gaps)
702 std::memcpy(out.data() + gap.offset, gap.bytes.data(), gap.bytes.size());
703 return out;
704 }
705
706 if (mips.size() > BlpMaxMips)
708 std::format("a BLP addresses at most {} mip levels, {} stored", BlpMaxMips, mips.size()));
709
710 std::uint64_t cursor = BlpHeaderBytes;
711 for (std::size_t level = 0; level < mips.size(); ++level) {
712 if (mips[level].empty()) continue;
713 header.mipOffsets[level] = static_cast<std::uint32_t>(cursor);
714 header.mipSizes[level] = static_cast<std::uint32_t>(mips[level].size());
715 cursor += mips[level].size();
716 }
717 if (cursor > std::numeric_limits<std::uint32_t>::max())
718 return makeError(ErrorCode::InvalidEntityState, "BLP mip payloads exceed the u32 offset space");
719
720 FileBuffer out(cursor);
721 std::memcpy(out.data(), &header, sizeof header);
722 std::memcpy(out.data() + sizeof header, palette.data(), sizeof palette);
723 for (std::size_t level = 0; level < mips.size(); ++level)
724 if (!mips[level].empty())
725 std::memcpy(out.data() + header.mipOffsets[level], mips[level].data(), mips[level].size());
726 return out;
727 }
728
730 const auto data = fs.readFile(key);
731 if (!data) return std::unexpected{data.error()};
732 return read(*data);
733 }
734
736 const FileKey resolved = fs.resolve(key);
737 if (!resolved.path)
738 return makeError(ErrorCode::PathNotResolvable, "saving a BLP needs a path for the file key");
739 return write().and_then([&](FileBuffer data) {
740 return fs.addFile(*resolved.path, data).transform([](auto&&) {});
741 });
742 }
743
744 Result<Image> BLP::decode() const { return decode(0); }
745
746 Result<Image> BLP::decode(std::uint32_t level) const {
747 if (level >= mips.size() || mips[level].empty())
749 std::format("BLP mip level {} is not stored ({} levels present)", level, mips.size()));
750 if (width == 0 || height == 0)
751 return makeError(ErrorCode::InvalidEntityState, "BLP has zero width or height");
752
753 const std::uint32_t w = mipWidth(level);
754 const std::uint32_t h = mipHeight(level);
755 const std::span<const std::byte> src{mips[level]};
756
757 Result<std::vector<std::uint8_t>> pixels = [& ]() -> Result<std::vector<std::uint8_t>> {
758 switch (colorEncoding) {
760 return detail::PaletteCodec{alphaDepth}.decode(src, palette, w, h);
762 return detail::DxtCodec{resolveDxtFormat(preferredFormat, alphaDepth)}.decode(src, w, h);
765 return detail::RawCodec{}.decode(src, w, h);
768 "JPEG-encoded BLPs never shipped in WoW clients and are not decodable "
769 "(the payload round-trips verbatim)");
770 }
771 return makeError(ErrorCode::NotSupported, "unreachable: validated on read");
772 }();
773 if (!pixels) return std::unexpected{pixels.error()};
774 return Image{.width = w, .height = h, .pixels = std::move(*pixels)};
775 }
776
778 return encode(image, EncodeSettings{});
779 }
780
781 Result<void> BLP::encode(const Image& image, const EncodeSettings& settings) {
782 if (image.width == 0 || image.height == 0)
783 return makeError(ErrorCode::InvalidEntityState, "encoding needs a non-empty image");
784 if (image.pixels.size() != std::size_t{image.width} * image.height * 4)
785 return makeError(ErrorCode::InvalidEntityState, std::format("image holds {} pixel bytes; {}x{} RGBA needs {}",
786 image.pixels.size(), image.width, image.height,
787 std::size_t{image.width} * image.height * 4));
788 if (settings.encoding == ColorEncoding::Jpeg)
789 return makeError(ErrorCode::NotSupported, "wowlib does not produce JPEG-encoded BLPs");
790 if (settings.alphaDepth != 0 && settings.alphaDepth != 1 && settings.alphaDepth != 4 && settings.alphaDepth !=
791 8)
793 std::format("alphaDepth must be 0, 1, 4 or 8, not {}", settings.alphaDepth));
794
795 *this = BLP{};
798 width = image.width;
799 height = image.height;
800 mipFlags = settings.mipmaps ? 1 : 0;
801
803 switch (colorEncoding) {
805 dxtFormat = resolveDxtFormat(settings.format, settings.alphaDepth);
806 preferredFormat = dxtFormat;
807 alphaDepth = dxtFormat == PixelFormat::Dxt1
808 ? std::min<std::uint8_t>(settings.alphaDepth, 1)
809 : settings.alphaDepth;
810 break;
813 alphaDepth = settings.alphaDepth;
815 break;
816 default: // Bgra
818 alphaDepth = 8;
819 break;
820 }
821
822 // level 0, then box-filtered halvings down to 1x1 (16-level table cap)
823 std::vector<std::uint8_t> levelPixels = image.pixels;
824 std::uint32_t w = width;
825 std::uint32_t h = height;
826 const detail::MipmapScaler scaler;
827 for (std::size_t level = 0; level < BlpMaxMips; ++level) {
828 Result<FileBuffer> payload = [&]() -> Result<FileBuffer> {
829 switch (colorEncoding) {
831 return detail::PaletteCodec{alphaDepth}.encode(levelPixels, palette, w, h);
833 return detail::DxtCodec{dxtFormat}.encode(levelPixels, w, h);
834 default:
835 return detail::RawCodec{}.encode(levelPixels, w, h);
836 }
837 }();
838 if (!payload) return std::unexpected{payload.error()};
839 mips.push_back(std::move(*payload));
840
841 if (!settings.mipmaps || (w == 1 && h == 1)) break;
842 levelPixels = scaler.downscale(levelPixels, w, h);
843 w = std::max<std::uint32_t>(1, w / 2);
844 h = std::max<std::uint32_t>(1, h / 2);
845 }
846 return {};
847 }
848
849 Result<FileBuffer> BLP::mip(std::uint32_t level) const {
850 if (level >= mips.size() || mips[level].empty())
852 std::format("BLP mip level {} is not stored ({} levels present)", level, mips.size()));
853 return mips[level];
854 }
855
856 Result<void> BLP::setMip(std::uint32_t level, std::span<const std::byte> data) {
857 if (level >= BlpMaxMips)
859 std::format("a BLP addresses at most {} mip levels", BlpMaxMips));
860 if (data.empty())
862 "an empty payload cannot be stored (drop trailing levels by resizing " "instead)");
863 mips.resize(std::max<std::size_t>(mips.size(), level + 1));
864 mips[level].assign(data.begin(), data.end());
865 return {};
866 }
867
869 ValidationReport report;
870
871 if (width == 0 || height == 0)
872 report.addError("width", std::format("zero dimension ({}x{}): the client cannot size " "the texture", width,
873 height));
874 // alphaDepth SIZES the separate alpha plane of a palettized file, but is
875 // only a block-format hint elsewhere — and shipped files carry junk in it
876 // there (3.3.5a Textures/SunGlare.blp: 136 on a DXT texture), so a bad
877 // value is only load-bearing when it decides a payload size.
878 if (alphaDepth != 0 && alphaDepth != 1 && alphaDepth != 4 && alphaDepth != 8) {
879 const std::string what = std::format("{} is not one of the 0/1/4/8 depths the client reads", alphaDepth);
881 report.addError("alphaDepth", what + " and sizes this file's alpha plane");
882 else report.addWarning("alphaDepth", what + "; ignored for this encoding");
883 }
884 if (mips.size() > BlpMaxMips)
885 report.addError("mips", std::format("{} levels exceed the {} the header can address", mips.size(),
886 BlpMaxMips));
887 if (mips.empty() || mips[0].empty())
888 report.addError("mips[0]", "level 0 carries no payload; every texture needs its base level");
889
891 report.addWarning("colorEncoding",
892 "JPEG-encoded BLPs never shipped in WoW clients; the payload "
893 "round-trips verbatim but cannot be decoded");
894 return report;
895 }
896
897 // Each level's payload must cover the pixels its dimensions imply. A SHORT
898 // payload is a documented client quirk for the block/raw encodings (the
899 // decoders pad the missing tail with transparent black), so it only warns;
900 // for palettized data the client would index past the buffer, so it errors.
901 for (std::size_t level = 0; level < mips.size() && !report.full(); ++level) {
902 if (mips[level].empty()) continue;
903 const auto index = static_cast<std::uint32_t>(level);
904 const std::uint32_t w = mipWidth(index);
905 const std::uint32_t h = mipHeight(index);
906 const std::size_t pixels = std::size_t{w} * h;
907 const std::size_t have = mips[level].size();
908
909 std::size_t want = 0;
910 switch (colorEncoding) {
912 want = pixels + pixels * alphaDepth / 8;
913 break;
915 want = detail::DxtCodec{resolveDxtFormat(preferredFormat, alphaDepth)}.encodedSize(w, h);
916 break;
919 want = pixels * 4;
920 break;
922 break;
923 }
924 if (have >= want) continue;
925 const std::string path = std::format("mips[{}]", level);
926 const std::string what = std::format("{} bytes for a {}x{} level, {} needed", have, w, h, want);
928 report.addError(path, what + " — the client would read past the payload");
929 else
930 report.addWarning(path, what + " — short levels decode as transparent black " "(a known client quirk)");
931 }
932 return report;
933 }
934}
The BLP entity (namespace wowlib::formats::blp): Blizzard's texture format.
void addWarning(std::string path, std::string message)
Record a warning finding (see add()).
void addError(std::string path, std::string message)
Record an error finding (see add()).
The DXT/BC block codec.
Definition codec.hpp:40
static void _decodeInterpolatedAlpha(const std::uint8_t *block, std::span< std::uint8_t, 16 > out) noexcept
Decode one BC3/BC4 interpolated-alpha block (8 bytes) into 16 values.
Definition blp.cpp:103
static void _decodeColorBlock(const std::uint8_t *block, BlockPixels &out, bool punchThrough) noexcept
Decode one BC1 color block (8 bytes) into 16 RGBA pixels.
Definition blp.cpp:60
static void _encodePunchThroughBlock(const BlockPixels &pixels, std::uint8_t *dest) noexcept
Encode one BC1 punch-through block (color0 <= color1 3-color mode, alpha < 128 texels as index 3) — t...
Definition blp.cpp:139
std::size_t encodedSize(std::uint32_t width, std::uint32_t height) const noexcept
The exact payload size of a level per the block formula ceil(w/4) * ceil(h/4) * blockBytes().
Definition codec.hpp:58
Result< std::vector< std::uint8_t > > decode(std::span< const std::byte > src, std::uint32_t width, std::uint32_t height) const
Decode one level's block stream to RGBA8 pixels.
Definition blp.cpp:207
Result< FileBuffer > encode(std::span< const std::uint8_t > rgba, std::uint32_t width, std::uint32_t height) const
Encode RGBA8 pixels to one level's block stream, exactly encodedSize() bytes.
Definition blp.cpp:264
static void _decodeExplicitAlpha(const std::uint8_t *block, BlockPixels &out) noexcept
Decode one BC2 explicit-alpha block (8 bytes, 4 bits per texel) onto the alpha channel of out.
Definition blp.cpp:96
static BlockPixels _extractBlock(std::span< const std::uint8_t > rgba, std::uint32_t width, std::uint32_t height, std::uint32_t bx, std::uint32_t by) noexcept
Extract one 4x4 block from a level, clamping reads at the right/bottom edges (partial blocks repeat t...
Definition blp.cpp:122
std::size_t blockBytes() const noexcept
The byte size of one compressed block (8 for BC1, 16 otherwise).
Definition codec.hpp:48
The mip-chain downscaler: one box-filter halving step per call, the standard non-sRGB-aware average e...
Definition codec.hpp:241
std::vector< std::uint8_t > downscale(std::span< const std::uint8_t > rgba, std::uint32_t width, std::uint32_t height) const
Downscale a level to the next one (dimensions halve, floored at 1); each destination texel averages i...
Definition blp.cpp:556
Result< FileBuffer > encode(std::span< const std::uint8_t > rgba, std::span< const CImVector, BlpPaletteSize > palette, std::uint32_t width, std::uint32_t height) const
Encode RGBA8 pixels against an existing palette: nearest-entry indices plus the packed alpha plane,...
Definition blp.cpp:395
void _packAlpha(std::uint8_t alpha, std::span< std::byte > plane, std::size_t index) const noexcept
Pack one texel's alpha into the plane at this codec's bit depth.
Definition blp.cpp:340
std::size_t encodedSize(std::uint32_t width, std::uint32_t height) const noexcept
The exact payload size of a level: one index byte per pixel plus the packed alpha plane.
Definition codec.hpp:146
std::uint8_t _unpackAlpha(std::span< const std::byte > plane, std::size_t index) const noexcept
The alpha value of texel index unpacked from the plane at this codec's bit depth (255 when the depth ...
Definition blp.cpp:320
std::array< CImVector, BlpPaletteSize > buildPalette(std::span< const std::uint8_t > rgba) const
Build a 256-entry palette for an image (median-cut over the opaque RGB values; an image with <= 256 d...
Definition blp.cpp:419
static std::uint8_t _nearestIndex(std::span< const CImVector, BlpPaletteSize > palette, std::uint8_t r, std::uint8_t g, std::uint8_t b) noexcept
The palette entry nearest to (r, g, b) by squared RGB distance.
Definition blp.cpp:358
Result< std::vector< std::uint8_t > > decode(std::span< const std::byte > src, std::span< const CImVector, BlpPaletteSize > palette, std::uint32_t width, std::uint32_t height) const
Decode one level's index + alpha-plane payload to RGBA8 pixels.
Definition blp.cpp:377
The uncompressed-BGRA codec (ColorEncoding::Bgra / BgraAlt): a pure channel swizzle.
Definition codec.hpp:217
Result< std::vector< std::uint8_t > > decode(std::span< const std::byte > src, std::uint32_t width, std::uint32_t height) const
Decode one level's BGRA bytes to RGBA8 pixels.
Definition blp.cpp:526
Result< FileBuffer > encode(std::span< const std::uint8_t > rgba, std::uint32_t width, std::uint32_t height) const
Encode RGBA8 pixels to one level's BGRA bytes.
Definition blp.cpp:540
The BLP pixel codecs (namespace wowlib::formats::blp::detail): DxtCodec, PaletteCodec,...
std::array< std::uint8_t, 16 *4 > BlockPixels
One decoded 4x4 texel block: 16 RGBA pixels, row-major.
Definition codec.hpp:33
constexpr std::size_t BlpHeaderBytes
The full header region: 148-byte header + 1024-byte palette.
Definition blp.hpp:53
constexpr std::uint32_t BlpVersion1
The BLP2 header version field; always 1 in every shipped client.
Definition blp.hpp:47
constexpr std::size_t BlpPaletteSize
The palette entry count (one byte-sized index space).
Definition blp.hpp:51
constexpr std::uint32_t BlpMagic
The BLP2 magic: the literal bytes "BLP2" (a forward FourCC, unlike the reversed chunk ids).
Definition blp.hpp:45
PixelFormat
The client-side pixel format hint (the header's preferredFormat byte).
Definition blp.hpp:65
@ Dxt1
BC1: 8-byte blocks, optional 1-bit punch-through alpha.
Definition blp.hpp:66
@ Argb8888
Raw 32-bit upload hint (used by Bgra-encoded files).
Definition blp.hpp:68
@ Unspecified
No preference recorded (typical for palettized files).
Definition blp.hpp:74
@ Dxt5
BC3: 16-byte blocks, interpolated 8-bit alpha.
Definition blp.hpp:73
@ Dxt3
BC2: 16-byte blocks, explicit 4-bit alpha.
Definition blp.hpp:67
@ Bc5
BC5: two interpolated channels (normal maps, later clients).
Definition blp.hpp:76
constexpr std::size_t BlpMaxMips
The mip table capacity: a BLP addresses at most 16 levels.
Definition blp.hpp:49
ColorEncoding
How a BLP's pixel payload is encoded (the header's colorEncoding byte).
Definition blp.hpp:56
@ Jpeg
JPEG-compressed content (Warcraft III heritage; never shipped in WoW clients — wowlib preserves but c...
Definition blp.hpp:57
@ Bgra
Raw 32-bit BGRA pixels (Cataclysm+; terrain cube maps).
Definition blp.hpp:60
@ Dxt
DXT/S3TC block compression; the variant (BC1/BC2/BC3/BC5) follows from preferred_format and alphaDept...
Definition blp.hpp:59
@ Palettized
256-color palette indices, one byte per pixel, followed by a separate alpha plane of alphaDepth bits ...
Definition blp.hpp:58
@ BgraAlt
Raw 32-bit BGRA under a different client-side PIXEL_FORMAT; identical file content to Bgra.
Definition blp.hpp:61
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ InvalidEntityState
An entity's members disagree (e.g.
Definition error.hpp:40
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
Definition error.hpp:23
@ FormatVersionMismatch
The file's version chunk disagrees with the requested version.
Definition error.hpp:41
@ NotSupported
Operation not supported by this backend/provider.
Definition error.hpp:32
@ OffsetOutOfBounds
An offset array (M2Array) points outside its base buffer.
Definition error.hpp:39
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
Definition error.hpp:36
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
std::optional< std::string > path
The canonical client-internal path, if known.
Definition file_key.hpp:31
std::array< CImVector, BlpPaletteSize > palette
The 256-entry color table of Palettized files (b, g, r + a padding byte, preserved verbatim).
Definition blp.hpp:181
ColorEncoding colorEncoding
How the pixel payload is encoded.
Definition blp.hpp:163
std::vector< FileBuffer > mips
The raw mip payloads, indexed by level (empty vector = level absent).
Definition blp.hpp:186
PixelFormat preferredFormat
The client-side pixel format hint; selects the DXT block format for Dxt-encoded files.
Definition blp.hpp:169
Result< Image > decode() const
Decode mip level 0 to an RGBA8 Image.
Definition blp.cpp:744
detail::StoredLayout storedLayout
The read-recorded on-disk placement (see detail::StoredLayout).
Definition blp.hpp:189
ValidationReport validate() const
Definition blp.cpp:868
std::uint32_t height
The level-0 height in pixels.
Definition blp.hpp:178
std::uint32_t width
The level-0 width in pixels.
Definition blp.hpp:175
std::uint8_t alphaDepth
Alpha bits per pixel (0, 1, 4 or 8): the alpha plane depth for Palettized files, and a selection hint...
Definition blp.hpp:166
Result< void > read(std::span< const std::byte > data)
Parse a BLP2 file from memory, replacing this entity's contents.
Definition blp.cpp:596
std::uint32_t version
The header version field; 1 in every shipped file.
Definition blp.hpp:160
std::uint8_t mipFlags
The header's mip byte: 0 = level 0 only, 1 = generated mips, 2 = handmade mips (plus rare high flag b...
Definition blp.hpp:172
std::uint32_t mipHeight(std::uint32_t level) const
The pixel height of a mip level (level 0 halves per step, floored at 1).
Definition blp.hpp:261
Result< void > encode(const Image &image)
Rebuild the whole texture from an RGBA8 image with the default settings (DXT, alpha depth 8 -> BC3,...
Definition blp.cpp:777
std::uint32_t mipWidth(std::uint32_t level) const
The pixel width of a mip level (level 0 halves per step, floored at 1).
Definition blp.hpp:255
Result< FileBuffer > mip(std::uint32_t level) const
One level's raw payload bytes (palette indices + alpha plane, DXT blocks, or BGRA pixels,...
Definition blp.cpp:849
Result< void > setMip(std::uint32_t level, std::span< const std::byte > data)
Replace one level's raw payload bytes verbatim.
Definition blp.cpp:856
Result< FileBuffer > write() const
Serialize this entity.
Definition blp.cpp:668
PixelFormat format
The DXT block format for Dxt encoding (Dxt1, Dxt3, Dxt5 or Bc5).
Definition blp.hpp:147
ColorEncoding encoding
The payload encoding to produce (Palettized, Dxt or Bgra; Jpeg is not supported).
Definition blp.hpp:144
bool mipmaps
Whether to generate the full mip chain down to 1x1 (box-filtered).
Definition blp.hpp:153
std::uint8_t alphaDepth
Alpha bits per pixel: 0, 1, 4 or 8.
Definition blp.hpp:150
std::vector< std::uint8_t > pixels
The RGBA8 pixel bytes, width * height * 4, row-major from the top-left.
Definition blp.hpp:137
std::uint32_t width
The width in pixels.
Definition blp.hpp:131
std::uint32_t height
The height in pixels.
Definition blp.hpp:134
The 148-byte BLP2 file header, exactly as on disk (the 1024-byte palette follows it).
Definition blp.hpp:83
std::array< std::uint32_t, BlpMaxMips > mipSizes
Payload byte sizes, 0 = unused.
Definition blp.hpp:94
std::uint8_t alphaDepth
Alpha bits per pixel: 0/1/4/8.
Definition blp.hpp:87
std::uint32_t version
Always 1.
Definition blp.hpp:85
std::uint32_t width
Level-0 width in pixels.
Definition blp.hpp:90
std::uint32_t height
Level-0 height in pixels.
Definition blp.hpp:91
std::array< std::uint32_t, BlpMaxMips > mipOffsets
Absolute file offsets, 0 = unused.
Definition blp.hpp:92
std::uint8_t colorEncoding
ColorEncoding byte.
Definition blp.hpp:86
std::uint8_t preferredFormat
PixelFormat byte.
Definition blp.hpp:88
std::uint8_t mipFlags
0 = no mips, 1 = generated, 2 = handmade.
Definition blp.hpp:89
One run of file bytes covered by neither the header region nor any mip payload (an inter-mip gap or a...
Definition blp.hpp:110
std::uint32_t offset
Absolute file offset of the run.
Definition blp.hpp:111
FileBuffer bytes
The run's verbatim bytes.
Definition blp.hpp:112
A color stored as b, g, r, a bytes — the client's immediate-mode vertex color layout (WMO MOCV,...
Definition types.hpp:178
std::uint8_t g
The green component.
Definition types.hpp:182
std::uint8_t b
The blue component.
Definition types.hpp:180
std::uint8_t r
The red component.
Definition types.hpp:184