You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
5.8 KiB

#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