Files
devilutionX/Source/utils/pcx_to_clx.cpp
T
Gleb Mazovetskiy 9db80ef5d7 Assets: Load files directly for UNPACKED_MPQS
When using `UNPACKED_MPQS`, avoid all the SDL machinery for reading
files.

This is beneficial not only due to reduced indirection but also because
we can test for the file's existence and get the file size without
opening it, which is much faster.
2022-11-22 02:44:25 +01:00

196 lines
5.8 KiB
C++

#include "utils/pcx_to_clx.hpp"
#include <cstdint>
#include <cstring>
#include <array>
#include <memory>
#include <vector>
#include <SDL_endian.h>
#include "appfat.h"
#include "utils/clx_write.hpp"
#include "utils/endian.hpp"
#include "utils/pcx.hpp"
#include "utils/stdcompat/cstddef.hpp"
#ifdef DEBUG_PCX_TO_CL2_SIZE
#include <iomanip>
#include <iostream>
#endif
namespace devilution {
namespace {
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;
}
}
bool LoadPcxMeta(AssetHandle &handle, int &width, int &height, uint8_t &bpp)
{
PCXHeader pcxhdr;
if (!handle.read(&pcxhdr, PcxHeaderSize)) {
return false;
}
width = SDL_SwapLE16(pcxhdr.Xmax) - SDL_SwapLE16(pcxhdr.Xmin) + 1;
height = SDL_SwapLE16(pcxhdr.Ymax) - SDL_SwapLE16(pcxhdr.Ymin) + 1;
bpp = pcxhdr.BitsPerPixel;
return true;
}
} // namespace
OptionalOwnedClxSpriteList PcxToClx(AssetHandle &handle, size_t fileSize, int numFramesOrFrameHeight, std::optional<uint8_t> transparentColor, SDL_Color *outPalette)
{
int width;
int height;
uint8_t bpp;
if (!LoadPcxMeta(handle, width, height, bpp)) {
return std::nullopt;
}
assert(bpp == 8);
unsigned numFrames;
unsigned frameHeight;
if (numFramesOrFrameHeight > 0) {
numFrames = numFramesOrFrameHeight;
frameHeight = height / numFrames;
} else {
frameHeight = -numFramesOrFrameHeight;
numFrames = height / frameHeight;
}
size_t pixelDataSize = fileSize;
if (pixelDataSize <= PcxHeaderSize) {
return std::nullopt;
}
pixelDataSize -= PcxHeaderSize;
std::unique_ptr<uint8_t[]> fileBuffer { new uint8_t[pixelDataSize] };
if (handle.read(fileBuffer.get(), pixelDataSize) == 0) {
return std::nullopt;
}
// CLX header: frame count, frame offset for each frame, file size
std::vector<uint8_t> cl2Data;
cl2Data.reserve(GetReservationSize(pixelDataSize));
cl2Data.resize(4 * (2 + static_cast<size_t>(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<uint8_t[]>(new uint8_t[static_cast<size_t>(frameHeight) * width]);
const unsigned srcSkip = width % 2;
uint8_t *dataPtr = fileBuffer.get();
for (unsigned frame = 1; frame <= numFrames; ++frame) {
WriteLE32(&cl2Data[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(cl2Data.size()));
// Frame header: 5 16-bit values:
// 1. Offset to start of the pixel data.
// 2. Width
// 3. Height
// 4..5. Unused (0)
const size_t frameHeaderPos = cl2Data.size();
constexpr size_t FrameHeaderSize = 10;
cl2Data.resize(cl2Data.size() + FrameHeaderSize);
// Frame header:
WriteLE16(&cl2Data[frameHeaderPos], FrameHeaderSize);
WriteLE16(&cl2Data[frameHeaderPos + 2], static_cast<uint16_t>(width));
WriteLE16(&cl2Data[frameHeaderPos + 4], static_cast<uint16_t>(frameHeight));
memset(&cl2Data[frameHeaderPos + 6], 0, 4);
for (unsigned j = 0; j < frameHeight; ++j) {
uint8_t *buffer = &frameBuffer[static_cast<size_t>(j) * width];
for (unsigned x = 0; x < static_cast<unsigned>(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);
}
++line;
}
AppendCl2TransparentRun(transparentRunWidth, cl2Data);
}
WriteLE32(&cl2Data[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(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;
}
}
// Release buffers before allocating the result array to reduce peak memory use.
frameBuffer = nullptr;
fileBuffer = nullptr;
auto out = std::unique_ptr<uint8_t[]>(new uint8_t[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<int>(cl2Data.size()) - static_cast<int>(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl;
#endif
return OwnedClxSpriteList { std::move(out) };
}
} // namespace devilution