Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
LegacyBitmap.cpp
Go to the documentation of this file.
1#include "LegacyBitmap.hpp"
2
3#include <algorithm>
4#include <fstream>
5#include <limits>
6#include <utility>
7#include <vector>
8
9namespace monopoly::data
10{
11 namespace
12 {
13 [[nodiscard]] std::uint16_t readU16Le(
14 const std::byte* bytes) noexcept
15 {
16 return
17 static_cast<std::uint16_t>(
18 std::to_integer<std::uint8_t>(bytes[0])) |
19 static_cast<std::uint16_t>(
20 std::to_integer<std::uint8_t>(bytes[1]) << 8U);
21 }
22
23
24 [[nodiscard]] std::uint32_t readU32Le(
25 const std::byte* bytes) noexcept
26 {
27 return
28 static_cast<std::uint32_t>(
29 std::to_integer<std::uint8_t>(bytes[0])) |
30 (static_cast<std::uint32_t>(
31 std::to_integer<std::uint8_t>(bytes[1])) << 8U) |
32 (static_cast<std::uint32_t>(
33 std::to_integer<std::uint8_t>(bytes[2])) << 16U) |
34 (static_cast<std::uint32_t>(
35 std::to_integer<std::uint8_t>(bytes[3])) << 24U);
36 }
37
38
39 [[nodiscard]] BitmapError error(
40 BitmapErrorCode code,
41 std::string detail,
42 std::filesystem::path path = {})
43 {
44 return { code, std::move(path), std::move(detail) };
45 }
46
47
48 [[nodiscard]] constexpr bool isSupportedBiRgbBitDepth(
49 std::uint16_t bitsPerPixel) noexcept
50 {
51 switch (bitsPerPixel)
52 {
53 case 1:
54 case 4:
55 case 8:
56 case 16:
57 case 24:
58 case 32:
59 return true;
60
61 default:
62 return false;
63 }
64 }
65
66
67 [[nodiscard]] constexpr bool checkedAdd(
68 std::uint64_t left,
69 std::uint64_t right,
70 std::uint64_t& result) noexcept
71 {
72 if (left > std::numeric_limits<std::uint64_t>::max() - right)
73 {
74 return false;
75 }
76
77 result = left + right;
78 return true;
79 }
80
81
82 [[nodiscard]] constexpr bool checkedMultiply(
83 std::uint64_t left,
84 std::uint64_t right,
85 std::uint64_t& result) noexcept
86 {
87 if (left != 0 &&
88 right > std::numeric_limits<std::uint64_t>::max() / left)
89 {
90 return false;
91 }
92
93 result = left * right;
94 return true;
95 }
96 }
97
98
99 std::string_view bitmapErrorCodeName(BitmapErrorCode code) noexcept
100 {
101 switch (code)
102 {
103 case BitmapErrorCode::InvalidPalette: return "InvalidPalette";
104 case BitmapErrorCode::DecodeBudgetExceeded: return "DecodeBudgetExceeded";
105 case BitmapErrorCode::UnsupportedDataType: return "UnsupportedDataType";
106 case BitmapErrorCode::None: return "None";
107 case BitmapErrorCode::FileOpenFailed: return "FileOpenFailed";
108 case BitmapErrorCode::ReadFailed: return "ReadFailed";
109 case BitmapErrorCode::HeaderTruncated: return "HeaderTruncated";
110 case BitmapErrorCode::InvalidSignature: return "InvalidSignature";
112 return "UnsupportedDibHeader";
113 case BitmapErrorCode::InvalidDimensions: return "InvalidDimensions";
114 case BitmapErrorCode::InvalidPlanes: return "InvalidPlanes";
115 case BitmapErrorCode::InvalidBitDepth: return "InvalidBitDepth";
117 return "UnsupportedCompression";
119 return "PixelDataOutOfRange";
121 return "DeclaredSizeOutOfRange";
122 }
123
124 return "InvalidBitmapErrorCode";
125 }
126
127
128 std::expected<LegacyUapMetadata, BitmapError> inspectLegacyUap(
129 std::span<const std::byte> bytes)
130 {
131 constexpr std::size_t HeaderSize = 16;
132 constexpr std::size_t PaletteEntrySize = 8;
133 if (bytes.size() < HeaderSize)
134 return std::unexpected(error(BitmapErrorCode::HeaderTruncated,
135 "UAP requires a complete 16-byte NEWBITMAPHEADER"));
136
138 metadata.width = readU16Le(bytes.data());
139 metadata.height = readU16Le(bytes.data() + 2);
140 metadata.originX = static_cast<std::int16_t>(readU16Le(bytes.data() + 4));
141 metadata.originY = static_cast<std::int16_t>(readU16Le(bytes.data() + 6));
142 metadata.flags = readU32Le(bytes.data() + 8);
143 metadata.colourCount = readU16Le(bytes.data() + 12);
144 metadata.alphaCount = readU16Le(bytes.data() + 14);
145
146 if (metadata.width == 0 || metadata.height == 0)
147 return std::unexpected(error(BitmapErrorCode::InvalidDimensions,
148 "UAP dimensions must be non-zero"));
149 if (metadata.colourCount == 0 || metadata.colourCount > 256 ||
150 metadata.alphaCount > metadata.colourCount)
151 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
152 "UAP palette must contain 1..256 entries and bound nAlpha"));
153
154 std::uint64_t paletteBytes{};
155 std::uint64_t pixelOffset{};
158 pixelOffset > bytes.size())
159 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
160 "UAP palette exceeds immutable payload bounds"));
161
162 metadata.pixelDataOffset = static_cast<std::size_t>(pixelOffset);
163 metadata.rowStride = (static_cast<std::size_t>(metadata.width) + 3U) & ~std::size_t{3U};
164 std::uint64_t rasterBytes{};
165 std::uint64_t rasterEnd{};
168 return std::unexpected(error(BitmapErrorCode::PixelDataOutOfRange,
169 "UAP top-down DWORD-padded raster is truncated"));
170
171 constexpr std::uint32_t AlphaChannel = 0x02U;
172 if ((metadata.flags & AlphaChannel) != 0 && metadata.alphaCount == 0)
173 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
174 "UAP alpha-channel flag requires at least one alpha palette entry"));
175 return metadata;
176 }
177
178
179 std::expected<LegacyBitmapRGBA8, BitmapError> decodeLegacyUapRGBA8(
180 std::span<const std::byte> bytes, std::size_t maxPixels)
181 {
182 const auto inspected = inspectLegacyUap(bytes);
183 if (!inspected) return std::unexpected(inspected.error());
184 const auto& m = *inspected;
185 const std::uint64_t count =
186 static_cast<std::uint64_t>(m.width) * m.height;
187 if (count > maxPixels || count > std::numeric_limits<std::size_t>::max() / 4U)
188 return std::unexpected(error(BitmapErrorCode::DecodeBudgetExceeded,
189 "decoded UAP exceeds pixel allocation budget"));
190
191 constexpr std::uint32_t NoTransparency = 0x01U;
192 constexpr std::uint32_t AlphaChannel = 0x02U;
193 const bool solid = (m.flags & NoTransparency) != 0;
194 const bool alphaChannel = (m.flags & AlphaChannel) != 0;
195 constexpr std::size_t HeaderSize = 16;
196 constexpr std::size_t PaletteEntrySize = 8;
197 LegacyBitmapRGBA8 result{m.width, m.height, {}};
198 result.pixels.resize(static_cast<std::size_t>(count) * 4U);
199
200 for (std::uint32_t y = 0; y < m.height; ++y)
201 {
202 const auto* row = bytes.data() + m.pixelDataOffset +
203 static_cast<std::size_t>(y) * m.rowStride;
204 for (std::uint32_t x = 0; x < m.width; ++x)
205 {
206 const auto index = std::to_integer<std::uint8_t>(row[x]);
207 if (index >= m.colourCount)
208 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
209 "UAP raster references an absent palette entry"));
210 const auto* entry = bytes.data() + HeaderSize +
211 static_cast<std::size_t>(index) * PaletteEntrySize;
212 const auto storedBlue = std::to_integer<std::uint8_t>(entry[0]);
213 const auto storedGreen = std::to_integer<std::uint8_t>(entry[1]);
214 const auto storedRed = std::to_integer<std::uint8_t>(entry[2]);
215
216 std::uint32_t alpha = 255;
217 if (alphaChannel && index < m.alphaCount)
218 {
219 alpha = readU32Le(entry + 4);
220 if (alpha > 255)
221 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
222 "UAP alpha palette value exceeds 8-bit ArtLib range"));
223 }
224 else if (!alphaChannel && !solid && index == 0)
225 {
226 alpha = 0;
227 }
228
229 const auto straight = [alpha](std::uint8_t premultiplied) -> std::uint8_t
230 {
231 if (alpha == 0) return 0;
232 if (alpha >= 255) return premultiplied;
233 const auto value =
234 (static_cast<std::uint32_t>(premultiplied) * 255U + alpha / 2U) / alpha;
235 return static_cast<std::uint8_t>(std::min(value, 255U));
236 };
237 const bool premultiplied = alphaChannel && index < m.alphaCount;
238 const auto offset =
239 (static_cast<std::size_t>(y) * m.width + x) * 4U;
240 result.pixels[offset] = premultiplied ? straight(storedRed) : storedRed;
241 result.pixels[offset + 1] = premultiplied ? straight(storedGreen) : storedGreen;
242 result.pixels[offset + 2] = premultiplied ? straight(storedBlue) : storedBlue;
243 result.pixels[offset + 3] = static_cast<std::uint8_t>(alpha);
244 }
245 }
246 return result;
247 }
248
249
250 std::expected<LegacyBitmapMetadata, BitmapError> inspectLegacyBitmap(
251 std::span<const std::byte> bytes)
252 {
253 // BITMAPFILEHEADER (14) + les 40 octets communs de BITMAPINFOHEADER.
254 constexpr std::size_t MinimumHeaderSize = 54;
255
256 if (bytes.size() < MinimumHeaderSize)
257 {
258 return std::unexpected(error(
260 "BMP requires a 14-byte file header and 40-byte DIB header"));
261 }
262
263 if (std::to_integer<std::uint8_t>(bytes[0]) != 'B' ||
264 std::to_integer<std::uint8_t>(bytes[1]) != 'M')
265 {
266 return std::unexpected(error(
268 "legacy asset is not a Windows BM bitmap"));
269 }
270
272 {
273 readU32Le(bytes.data() + 2),
274 readU32Le(bytes.data() + 10),
275 readU32Le(bytes.data() + 14),
276 static_cast<std::int32_t>(readU32Le(bytes.data() + 18)),
277 static_cast<std::int32_t>(readU32Le(bytes.data() + 22)),
278 readU16Le(bytes.data() + 28),
279 readU32Le(bytes.data() + 30),
280 readU32Le(bytes.data() + 34)
281 };
282
283 if (metadata.dibHeaderSize < 40 ||
284 static_cast<std::uint64_t>(14) + metadata.dibHeaderSize >
285 bytes.size())
286 {
287 return std::unexpected(error(
289 "BMP DIB header must expose at least BITMAPINFOHEADER"));
290 }
291
292 if (metadata.width <= 0 || metadata.height == 0 ||
293 metadata.height == std::numeric_limits<std::int32_t>::min())
294 {
295 return std::unexpected(error(
297 "BMP width must be positive and height must be non-zero"));
298 }
299
300 if (readU16Le(bytes.data() + 26) != 1)
301 {
302 return std::unexpected(error(
304 "Windows BMP plane count must equal one"));
305 }
306
308 {
309 return std::unexpected(error(
311 "BI_RGB BMP bit depth must be 1, 4, 8, 16, 24 or 32"));
312 }
313
314 constexpr std::uint32_t BiRgb = 0;
315
317 {
318 return std::unexpected(error(
320 "legacy texture inspection supports only BI_RGB BMP data"));
321 }
322
323 const std::uint64_t dibEnd =
324 14U + static_cast<std::uint64_t>(metadata.dibHeaderSize);
325
327 static_cast<std::uint64_t>(metadata.pixelDataOffset) >=
328 bytes.size())
329 {
330 return std::unexpected(error(
332 "BMP pixel data offset is outside the file"));
333 }
334
335 const auto absoluteHeight = metadata.height < 0
336 ? static_cast<std::uint64_t>(-
337 static_cast<std::int64_t>(metadata.height))
338 : static_cast<std::uint64_t>(metadata.height);
339 const auto width = static_cast<std::uint64_t>(metadata.width);
340 std::uint64_t rowBits{};
341 std::uint64_t paddedRowBits{};
342 std::uint64_t rowStride{};
343 std::uint64_t minimumRasterSize{};
344 std::uint64_t minimumRasterEnd{};
345
346 // Chaque scanline BI_RGB est arrondie au DWORD suivant. Les calculs
347 // restent explicites sur 64 bits afin qu'un header hostile ne puisse
348 // pas ramener une taille immense dans la plage du fichier.
349 if (!checkedMultiply(
350 width,
351 static_cast<std::uint64_t>(metadata.bitsPerPixel),
352 rowBits) ||
354 !checkedMultiply(paddedRowBits / 32U, 4U, rowStride) ||
356 rowStride,
359 !checkedAdd(
363 {
364 return std::unexpected(error(
366 "BMP raster size overflows portable 64-bit bounds"));
367 }
368
369 if (minimumRasterEnd > static_cast<std::uint64_t>(bytes.size()))
370 {
371 return std::unexpected(error(
373 "BMP pixel array is truncated for its dimensions and bit depth"));
374 }
375
377 {
378 std::uint64_t declaredImageEnd{};
379
381 !checkedAdd(
385 declaredImageEnd > static_cast<std::uint64_t>(bytes.size()))
386 {
387 return std::unexpected(error(
389 "BMP declared image size does not contain a complete raster"));
390 }
391 }
392
393 if (metadata.declaredFileSize != 0 &&
394 (metadata.declaredFileSize > bytes.size() ||
397 static_cast<std::uint64_t>(metadata.declaredFileSize) <
398 static_cast<std::uint64_t>(
401 {
402 return std::unexpected(error(
404 "BMP declared file size does not contain its declared raster"));
405 }
406
407 return metadata;
408 }
409
410
411 std::expected<LegacyBitmapRGBA8, BitmapError> decodeLegacyBitmapRGBA8(
412 std::span<const std::byte> bytes, std::size_t maxPixels)
413 {
414 const auto inspected = inspectLegacyBitmap(bytes);
415 if (!inspected) return std::unexpected(inspected.error());
416 const auto& m = *inspected;
417 if (m.bitsPerPixel != 8 && m.bitsPerPixel != 24)
418 return std::unexpected(error(BitmapErrorCode::InvalidBitDepth,
419 "RGBA8 decoding supports only BI_RGB 8/24-bit BMP"));
420 const auto height = static_cast<std::uint32_t>(m.topDown() ? -m.height : m.height);
421 const auto width = static_cast<std::uint32_t>(m.width);
422 const auto count = static_cast<std::uint64_t>(width) * height;
423 if (count > maxPixels || count > std::numeric_limits<std::size_t>::max() / 4U)
424 return std::unexpected(error(BitmapErrorCode::DecodeBudgetExceeded,
425 "decoded BMP exceeds pixel allocation budget"));
426 const auto paletteStart = 14ULL + m.dibHeaderSize;
427 std::uint32_t paletteCount{};
428 if (m.bitsPerPixel == 8)
429 {
430 paletteCount = readU32Le(bytes.data() + 46);
431 if (!paletteCount) paletteCount = 256;
432 if (paletteCount > 256 || paletteStart + 4ULL * paletteCount > m.pixelDataOffset)
433 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
434 "BMP palette is truncated or overlaps raster"));
435 }
436 const auto stride = (static_cast<std::uint64_t>(width) * m.bitsPerPixel + 31U) / 32U * 4U;
437 LegacyBitmapRGBA8 result{width, height, {}};
438 result.pixels.resize(static_cast<std::size_t>(count) * 4U);
439 for (std::uint32_t y = 0; y < height; ++y)
440 {
441 const auto sourceY = m.topDown() ? y : height - 1U - y;
442 const auto* row = bytes.data() + m.pixelDataOffset + sourceY * stride;
443 for (std::uint32_t x = 0; x < width; ++x)
444 {
445 const std::byte* color{};
446 if (m.bitsPerPixel == 8)
447 {
448 const auto index = std::to_integer<std::uint8_t>(row[x]);
449 if (index >= paletteCount)
450 return std::unexpected(error(BitmapErrorCode::InvalidPalette,
451 "BMP raster references an absent palette entry"));
452 color = bytes.data() + paletteStart + 4U * index;
453 }
454 else color = row + 3ULL * x;
455 const auto offset = (static_cast<std::size_t>(y) * width + x) * 4U;
456 result.pixels[offset] = std::to_integer<std::uint8_t>(color[2]);
457 result.pixels[offset + 1] = std::to_integer<std::uint8_t>(color[1]);
458 result.pixels[offset + 2] = std::to_integer<std::uint8_t>(color[0]);
459 result.pixels[offset + 3] = 255;
460 }
461 }
462 return result;
463 }
464
465
466 std::expected<LegacyBitmapMetadata, BitmapError>
467 inspectLegacyBitmapFile(const std::filesystem::path& path)
468 {
469 std::ifstream input(path, std::ios::binary | std::ios::ate);
470
471 if (!input)
472 {
473 return std::unexpected(error(
475 "unable to open legacy BMP",
476 path));
477 }
478
479 const auto end = input.tellg();
480
481 if (end < 0 || static_cast<std::uint64_t>(end) >
482 std::numeric_limits<std::size_t>::max())
483 {
484 return std::unexpected(error(
486 "unable to determine a portable BMP size",
487 path));
488 }
489
490 std::vector<std::byte> bytes(static_cast<std::size_t>(end));
491 input.seekg(0, std::ios::beg);
492
493 if (!bytes.empty())
494 {
495 input.read(
496 reinterpret_cast<char*>(bytes.data()),
497 static_cast<std::streamsize>(bytes.size()));
498 }
499
500 if (!input)
501 {
502 return std::unexpected(error(
504 "unable to read complete legacy BMP",
505 path));
506 }
507
509
510 if (!metadata)
511 {
512 auto failure = metadata.error();
513 failure.path = path;
514 return std::unexpected(std::move(failure));
515 }
516
517 return metadata;
518 }
519}
std::size_t count
std::size_t offset
std::atomic< float > left
std::atomic< float > right
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
std::uint32_t height
std::uint32_t width
unsigned index
std::string path
std::string error
Bytes input
std::uint16_t value
Definition Timers.cpp:16
std::string_view bitmapErrorCodeName(BitmapErrorCode code) noexcept
std::expected< LegacyBitmapMetadata, BitmapError > inspectLegacyBitmapFile(const std::filesystem::path &path)
std::expected< LegacyUapMetadata, BitmapError > inspectLegacyUap(std::span< const std::byte > bytes)
std::expected< LegacyBitmapRGBA8, BitmapError > decodeLegacyUapRGBA8(std::span< const std::byte > bytes, std::size_t maxPixels)
std::expected< LegacyBitmapRGBA8, BitmapError > decodeLegacyBitmapRGBA8(std::span< const std::byte > bytes, std::size_t maxPixels)
std::expected< LegacyBitmapMetadata, BitmapError > inspectLegacyBitmap(std::span< const std::byte > bytes)
std::vector< std::uint8_t > pixels