13#include <unordered_map>
16#define STB_DXT_IMPLEMENTATION
22 constexpr std::uint8_t expand5(std::uint32_t v)
noexcept {
23 return static_cast<std::uint8_t
>((v << 3) | (v >> 2));
27 constexpr std::uint8_t expand6(std::uint32_t v)
noexcept {
28 return static_cast<std::uint8_t
>((v << 2) | (v >> 4));
32 std::uint16_t readU16(
const std::uint8_t* at)
noexcept {
33 return static_cast<std::uint16_t
>(at[0] | (at[1] << 8));
39 std::span<std::uint8_t> rgba,
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);
61 const std::uint16_t c0 = readU16(block);
62 const std::uint16_t c1 = readU16(block + 2);
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};
68 colors[0] = expand(c0);
69 colors[1] = expand(c1);
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);
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);
86 colors[3] = {0, 0, 0, 0};
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});
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);
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};
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);
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);
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];
124 std::uint32_t height,
126 std::uint32_t by)
noexcept {
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});
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;
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)]);
153 lo[0] = lo[1] = lo[2] = 0;
154 hi[0] = hi[1] = hi[2] = 0;
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));
160 std::uint16_t c0 = pack565(lo);
161 std::uint16_t c1 = pack565(hi);
162 if (c0 > c1) std::swap(c0, c1);
165 const std::array<std::array<std::int32_t, 3>, 3> palette = {
167 {expand5((c0 >> 11) & 0x1F), expand6((c0 >> 5) & 0x3F), expand5(c0 & 0x1F)},
168 {expand5((c1 >> 11) & 0x1F), expand6((c1 >> 5) & 0x3F), expand5(c1 & 0x1F)},
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
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;
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<
195 if (best < 0 || dist < best) {
201 bits |=
static_cast<std::uint8_t
>(index << (2 * col));
203 dest[4 + row] = bits;
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);
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;
222 std::array<std::uint8_t, 16> raw{};
223 std::memcpy(raw.data(), src.data() + at, stride);
236 std::array<std::uint8_t, 16> alpha{};
238 for (std::uint32_t texel = 0; texel < 16; ++texel) block[texel * 4 + 3] = alpha[texel];
242 std::array<std::uint8_t, 16> red{};
243 std::array<std::uint8_t, 16> 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;
256 std::format(
"BLP DXT decode: pixel format {} is not a block format",
257 std::to_underlying(_format)));
259 placeBlock(block, rgba, width, height, bx, by);
266 std::uint32_t height)
const {
267 const std::uint32_t blocksX = (width + 3) / 4;
268 const std::uint32_t blocksY = (height + 3) / 4;
270 FileBuffer out(std::size_t{blocksX} * blocksY * stride);
273 for (std::uint32_t by = 0; by < blocksY; ++by)
274 for (std::uint32_t bx = 0; bx < blocksX; ++bx, at += stride) {
276 std::array<std::uint8_t, 16> encoded{};
279 const bool transparent = std::ranges::any_of(std::views::iota(0u, 16u), [&](std::uint32_t texel) {
280 return block[texel * 4 + 3] < 128;
284 stb_compress_dxt_block(encoded.data(), block.data(), 0, STB_DXT_HIGHQUAL);
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)));
293 stb_compress_dxt_block(encoded.data() + 8, block.data(), 0, STB_DXT_HIGHQUAL);
297 stb_compress_dxt_block(encoded.data(), block.data(), 1, STB_DXT_HIGHQUAL);
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];
305 stb_compress_bc5_block(encoded.data(), rg.data());
310 std::format(
"BLP DXT encode: pixel format {} is not a block format",
311 std::to_underlying(_format)));
313 std::memcpy(out.data() + at, encoded.data(), stride);
321 switch (_alphaDepth) {
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;
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);
334 return index < plane.size() ? std::to_integer<std::uint8_t>(plane[index]) : 0;
341 switch (_alphaDepth) {
343 if (alpha >= 128) plane[index / 8] |=
static_cast<std::byte
>(1 << (index % 8));
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)));
351 plane[index] =
static_cast<std::byte
>(alpha);
361 std::uint8_t b)
noexcept {
362 std::uint32_t bestIndex = 0;
363 std::int64_t best = -1;
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) {
374 return static_cast<std::uint8_t
>(bestIndex);
378 std::span<const CImVector, BlpPaletteSize> palette,
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>{};
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;
396 std::span<const CImVector, BlpPaletteSize> palette,
398 std::uint32_t height)
const {
399 const std::size_t texels = std::size_t{width} * height;
401 const std::span<std::byte> plane = std::span{out}.subspan(texels);
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);
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];
427 std::array<CImVector, BlpPaletteSize> palette{};
428 const auto entry = [](std::uint32_t key) {
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),
439 for (
const auto& [key, count] : counts) palette[at++] = entry(key);
445 std::uint8_t r, g, b;
448 std::vector<Point> points;
449 points.reserve(counts.size());
450 for (
const auto& [key, count] : counts)
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),
459 std::size_t begin, end;
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;
468 std::size_t pick = boxes.size();
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) {
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));
480 if (hi - lo > pickExtent) {
481 pickExtent = hi - lo;
487 if (pick == boxes.size() || pickExtent <= 0)
break;
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);
495 const std::size_t split = box.begin + (box.end - box.begin) / 2;
496 const std::size_t oldEnd = box.end;
498 boxes.push_back({split, oldEnd});
502 for (std::size_t i = 0; i < boxes.size(); ++i) {
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;
513 if (total == 0)
continue;
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),
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]);
542 std::uint32_t height)
const {
543 const std::size_t texels = std::size_t{width} * height;
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]);
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);
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);
599 std::format(
"BLP file is {} bytes; the header region is {}", data.size(),
BlpHeaderBytes));
602 std::memcpy(&header, data.data(),
sizeof header);
605 std::format(
"not a BLP2 file (magic {:#010x})", header.
magic));
610 std::format(
"BLP preferredFormat {} is unknown", header.
preferredFormat));
620 std::memcpy(
palette.data(), data.data() +
sizeof header,
sizeof palette);
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);
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)
643 covered.emplace_back(header.
mipOffsets[level],
644 std::uint64_t{header.mipOffsets[level]} + header.
mipSizes[level]);
645 std::ranges::sort(covered);
647 std::uint64_t reached = 0;
648 for (
const auto& [begin, end] : covered) {
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)
657 reached = std::max(reached, end);
659 if (reached < data.size())
661 static_cast<std::uint32_t
>(reached),
662 FileBuffer{data.begin() +
static_cast<std::ptrdiff_t
>(reached), data.end()}
683 for (std::size_t level = 0; level <
BlpMaxMips; ++level) {
685 const bool present = level <
mips.size() && !
mips[level].empty();
686 if (stored != present || (stored &&
mips[level].size() !=
storedLayout.sizes[level])) {
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)
700 std::memcpy(out.data() + header.
mipOffsets[level],
mips[level].data(),
mips[level].size());
708 std::format(
"a BLP addresses at most {} mip levels, {} stored",
BlpMaxMips,
mips.size()));
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();
717 if (cursor > std::numeric_limits<std::uint32_t>::max())
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());
730 const auto data =
fs.readFile(key);
731 if (!data)
return std::unexpected{data.error()};
736 const FileKey resolved =
fs.resolve(key);
740 return fs.addFile(*resolved.
path, data).transform([](
auto&&) {});
747 if (level >=
mips.size() ||
mips[level].empty())
749 std::format(
"BLP mip level {} is not stored ({} levels present)", level,
mips.size()));
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]};
768 "JPEG-encoded BLPs never shipped in WoW clients and are not decodable "
769 "(the payload round-trips verbatim)");
773 if (!pixels)
return std::unexpected{pixels.error()};
774 return Image{.width = w, .height = h, .pixels = std::move(*pixels)};
784 if (image.
pixels.size() != std::size_t{image.width} * image.
height * 4)
787 std::size_t{image.width} * image.
height * 4));
793 std::format(
"alphaDepth must be 0, 1, 4 or 8, not {}", settings.
alphaDepth));
808 ? std::min<std::uint8_t>(settings.
alphaDepth, 1)
823 std::vector<std::uint8_t> levelPixels = image.
pixels;
824 std::uint32_t w =
width;
827 for (std::size_t level = 0; level <
BlpMaxMips; ++level) {
838 if (!payload)
return std::unexpected{payload.error()};
839 mips.push_back(std::move(*payload));
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);
850 if (level >=
mips.size() ||
mips[level].empty())
852 std::format(
"BLP mip level {} is not stored ({} levels present)", level,
mips.size()));
859 std::format(
"a BLP addresses at most {} mip levels",
BlpMaxMips));
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());
872 report.
addError(
"width", std::format(
"zero dimension ({}x{}): the client cannot size " "the texture",
width,
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");
885 report.
addError(
"mips", std::format(
"{} levels exceed the {} the header can address",
mips.size(),
887 if (
mips.empty() ||
mips[0].empty())
888 report.
addError(
"mips[0]",
"level 0 carries no payload; every texture needs its base level");
892 "JPEG-encoded BLPs never shipped in WoW clients; the payload "
893 "round-trips verbatim but cannot be decoded");
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();
909 std::size_t want = 0;
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");
930 report.
addWarning(path, what +
" — short levels decode as transparent black " "(a known client quirk)");
The BLP entity (namespace wowlib::formats::blp): Blizzard's texture format.
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ InvalidEntityState
An entity's members disagree (e.g.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
@ FormatVersionMismatch
The file's version chunk disagrees with the requested version.
@ NotSupported
Operation not supported by this backend/provider.
@ OffsetOutOfBounds
An offset array (M2Array) points outside its base buffer.
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
std::optional< std::string > path
The canonical client-internal path, if known.