#include "utils/pcx_to_cl2.hpp" #include #include #include #include #include #include "appfat.h" #include "utils/endian.hpp" #include "utils/pcx.hpp" #include "utils/stdcompat/cstddef.hpp" #ifdef DEBUG_PCX_TO_CL2_SIZE #include #include #endif #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { namespace { void AppendCl2TransparentRun(unsigned width, std::vector &out) { while (width >= 0x7F) { out.push_back(0x7F); width -= 0x7F; } if (width == 0) return; out.push_back(width); } void AppendCl2FillRun(uint8_t color, unsigned width, std::vector &out) { while (width >= 0x3F) { out.push_back(0x80); out.push_back(color); width -= 0x3F; } if (width == 0) return; out.push_back(0xBF - width); out.push_back(color); } void AppendCl2PixelsRun(const uint8_t *src, unsigned width, std::vector &out) { while (width >= 0x41) { out.push_back(0xBF); for (size_t i = 0; i < 0x41; ++i) out.push_back(src[i]); width -= 0x41; src += 0x41; } if (width == 0) return; out.push_back(256 - width); for (size_t i = 0; i < width; ++i) out.push_back(src[i]); } void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::vector &out) { const uint8_t *begin = src; const uint8_t *prevColorBegin = src; unsigned prevColorRunLength = 1; uint8_t prevColor = *src++; while (--length > 0) { const uint8_t color = *src; if (prevColor == color) { ++prevColorRunLength; } else { // A tunable parameter that decides at which minimum length we encode a fill run. // 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4). constexpr unsigned MinFillRunLength = 3; if (prevColorRunLength >= MinFillRunLength) { AppendCl2PixelsRun(begin, prevColorBegin - begin, out); AppendCl2FillRun(prevColor, prevColorRunLength, out); begin = src; } prevColorBegin = src; prevColorRunLength = 1; prevColor = color; } ++src; } AppendCl2PixelsRun(begin, prevColorBegin - begin, out); AppendCl2FillRun(prevColor, prevColorRunLength, out); } size_t GetReservationSize(size_t pcxSize) { // For the most part, CL2 is smaller than PCX, with a few exceptions. switch (pcxSize) { case 2352187: // ui_art\hf_logo1.pcx return 2464867; case 172347: // ui_art\creditsw.pcx return 172347; case 157275: // ui_art\credits.pcx return 173367; default: return pcxSize; } } } // namespace std::optional PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette) { int width; int height; uint8_t bpp; if (!LoadPcxMeta(handle, width, height, bpp)) { SDL_RWclose(handle); return std::nullopt; } assert(bpp == 8); unsigned numFrames; unsigned frameHeight; if (numFramesOrFrameHeight > 0) { numFrames = numFramesOrFrameHeight; frameHeight = height / numFrames; } else { frameHeight = -numFramesOrFrameHeight; numFrames = height / frameHeight; } ptrdiff_t pixelDataSize = SDL_RWsize(handle); if (pixelDataSize < 0) { SDL_RWclose(handle); return std::nullopt; } pixelDataSize -= PcxHeaderSize; std::unique_ptr fileBuffer { new uint8_t[pixelDataSize] }; if (SDL_RWread(handle, fileBuffer.get(), pixelDataSize, 1) == 0) { SDL_RWclose(handle); return std::nullopt; } // CEL header: frame count, frame offset for each frame, file size std::vector cl2Data; cl2Data.reserve(GetReservationSize(pixelDataSize)); cl2Data.resize(4 * (2 + static_cast(numFrames))); WriteLE32(cl2Data.data(), numFrames); // We process the PCX a whole frame at a time because the lines are reversed in CEL. auto frameBuffer = std::unique_ptr(new uint8_t[static_cast(frameHeight) * width]); const unsigned srcSkip = width % 2; uint8_t *dataPtr = fileBuffer.get(); for (unsigned frame = 1; frame <= numFrames; ++frame) { WriteLE32(&cl2Data[4 * static_cast(frame)], static_cast(cl2Data.size())); // Frame header: 5 16-bit offsets to 32-pixel height blocks. const size_t frameHeaderPos = cl2Data.size(); constexpr size_t FrameHeaderSize = 10; cl2Data.resize(cl2Data.size() + FrameHeaderSize); // Frame header offset (first of five): WriteLE16(&cl2Data[frameHeaderPos], FrameHeaderSize); for (unsigned j = 0; j < frameHeight; ++j) { uint8_t *buffer = &frameBuffer[static_cast(j) * width]; for (unsigned x = 0; x < static_cast(width);) { constexpr uint8_t PcxMaxSinglePixel = 0xBF; const uint8_t byte = *dataPtr++; if (byte <= PcxMaxSinglePixel) { *buffer++ = byte; ++x; continue; } constexpr uint8_t PcxRunLengthMask = 0x3F; const uint8_t runLength = (byte & PcxRunLengthMask); std::memset(buffer, *dataPtr++, runLength); buffer += runLength; x += runLength; } dataPtr += srcSkip; } unsigned transparentRunWidth = 0; size_t line = 0; while (line != frameHeight) { // Process line: const uint8_t *src = &frameBuffer[(frameHeight - (line + 1)) * width]; if (transparentColor) { unsigned solidRunWidth = 0; for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { if (*src == *transparentColor) { if (solidRunWidth != 0) { AppendCl2PixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data); solidRunWidth = 0; } ++transparentRunWidth; } else { AppendCl2TransparentRun(transparentRunWidth, cl2Data); transparentRunWidth = 0; ++solidRunWidth; } } if (solidRunWidth != 0) { AppendCl2PixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data); } } else { AppendCl2PixelsOrFillRun(src, width, cl2Data); } // Frame header offset: switch (++line) { case 32: case 64: case 96: case 128: // Finish any active transparent run to not allow it to go over an offset line boundary. AppendCl2TransparentRun(transparentRunWidth, cl2Data); transparentRunWidth = 0; WriteLE16(&cl2Data[frameHeaderPos + line / 16], static_cast(cl2Data.size() - frameHeaderPos)); break; } } AppendCl2TransparentRun(transparentRunWidth, cl2Data); } WriteLE32(&cl2Data[4 * (1 + static_cast(numFrames))], static_cast(cl2Data.size())); if (outPalette != nullptr) { [[maybe_unused]] constexpr unsigned PcxPaletteSeparator = 0x0C; assert(*dataPtr == PcxPaletteSeparator); // PCX may not have a palette ++dataPtr; for (unsigned i = 0; i < 256; ++i) { outPalette->r = *dataPtr++; outPalette->g = *dataPtr++; outPalette->b = *dataPtr++; #ifndef USE_SDL1 outPalette->a = SDL_ALPHA_OPAQUE; #endif ++outPalette; } } SDL_RWclose(handle); // Release buffers before allocating the result array to reduce peak memory use. frameBuffer = nullptr; fileBuffer = nullptr; auto out = std::unique_ptr(new byte[cl2Data.size()]); memcpy(&out[0], cl2Data.data(), cl2Data.size()); #ifdef DEBUG_PCX_TO_CL2_SIZE std::cout << "\t" << pixelDataSize << "\t" << cl2Data.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast(cl2Data.size()) - static_cast(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl; #endif return OwnedCelSpriteWithFrameHeight { OwnedCelSprite { std::move(out), static_cast(width) }, static_cast(frameHeight) }; } } // namespace devilution