#include <whiteout/textures/png/writer.h>
#include "deflate.h"
#include "png_internal.h"
#include "../io_helpers.h"
#include "../issue_sink.h"
#include <cstring>
#include <memory>
#include <span>
#include <utility>
#include <vector>
namespace whiteout::textures::png {
class Writer::Impl : public IssueSink {
public:
std::vector<u8> write(const Texture& texture);
std::vector<u8> writeAnimated(const std::vector<ApngFrame>& frames,
const ApngSaveOptions& opts);
private:
static void writeChunk(std::vector<u8>& out, u32 chunkType, const u8* data, u32 length);
static std::vector<u8> filterScanlines(const u8* rgba, u32 width, u32 height);
};
void Writer::Impl::writeChunk(std::vector<u8>& out, u32 chunkType, const u8* data, u32 length) {
size_t const startPos = out.size();
out.resize(startPos + 12 + length);
u8* p = out.data() + startPos;
writeU32BE(p, length);
writeU32BE(p + 4, chunkType);
if (length > 0 && data) {
std::memcpy(p + 8, data, length);
}
u32 const c = crc32(p + 4, 4 + length);
writeU32BE(p + 8 + length, c);
}
std::vector<u8> Writer::Impl::filterScanlines(const u8* rgba, u32 width, u32 height) {
const u32 bpp = 4;
const u32 stride = width * bpp;
const size_t totalSize = static_cast<size_t>(height) * (1 + stride);
std::vector<u8> filtered(totalSize);
for (u32 y = 0; y < height; ++y) {
const u8* cur = rgba + static_cast<size_t>(y) * stride;
const u8* prev = (y > 0) ? (rgba + static_cast<size_t>(y - 1) * stride) : nullptr;
u8* out = filtered.data() + static_cast<size_t>(y) * (1 + stride);
u64 sumNone = 0;
u64 sumSub = 0;
u64 sumUp = 0;
for (u32 x = 0; x < stride; ++x) {
sumNone += cur[x] > 127 ? (256 - cur[x]) : cur[x];
u8 const a = (x >= bpp) ? cur[x - bpp] : 0;
u8 const sub = static_cast<u8>(cur[x] - a);
sumSub += sub > 127 ? (256 - sub) : sub;
u8 const b = prev ? prev[x] : 0;
u8 const up = static_cast<u8>(cur[x] - b);
sumUp += up > 127 ? (256 - up) : up;
}
u8 bestFilter = FILTER_NONE;
u64 bestSum = sumNone;
if (sumSub < bestSum) {
bestFilter = FILTER_SUB;
bestSum = sumSub;
}
if (sumUp < bestSum) {
bestFilter = FILTER_UP;
}
out[0] = bestFilter;
switch (bestFilter) {
case FILTER_NONE:
std::memcpy(out + 1, cur, stride);
break;
case FILTER_SUB:
for (u32 x = 0; x < stride; ++x) {
u8 const a = (x >= bpp) ? cur[x - bpp] : 0;
out[1 + x] = static_cast<u8>(cur[x] - a);
}
break;
case FILTER_UP:
for (u32 x = 0; x < stride; ++x) {
u8 const b = prev ? prev[x] : 0;
out[1 + x] = static_cast<u8>(cur[x] - b);
}
break;
}
}
return filtered;
}
std::vector<u8> Writer::Impl::write(const Texture& texture) {
issues.clear();
if (texture.width() == 0 || texture.height() == 0) {
fail("Cannot save an empty texture");
return {};
}
Texture rgba_texture = texture.copyAsFormat(PixelFormat::RGBA8);
const u32 width = rgba_texture.width();
const u32 height = rgba_texture.height();
const u8* pixels = rgba_texture.dataPtr();
std::vector<u8> output;
output.reserve(64 + static_cast<size_t>(width) * height * 4);
output.insert(output.end(), PNG_SIGNATURE.begin(), PNG_SIGNATURE.end());
u8 ihdr[13];
writeU32BE(ihdr + 0, width);
writeU32BE(ihdr + 4, height);
ihdr[8] = 8; ihdr[9] = COLOR_TRUECOLOR_ALPHA; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; writeChunk(output, CHUNK_IHDR, ihdr, 13);
std::vector<u8> filtered = filterScanlines(pixels, width, height);
std::string compressError;
auto compressed =
zlib_compress(std::span<const u8>(filtered.data(), filtered.size()), &compressError);
if (compressed.empty()) {
fail("Failed to compress PNG data: " + compressError);
return {};
}
writeChunk(output, CHUNK_IDAT, compressed.data(), static_cast<u32>(compressed.size()));
writeChunk(output, CHUNK_IEND, nullptr, 0);
return output;
}
std::vector<u8> Writer::Impl::writeAnimated(const std::vector<ApngFrame>& frames,
const ApngSaveOptions& opts) {
issues.clear();
if (frames.empty()) {
fail("Cannot write an APNG with no frames");
return {};
}
Texture canvas0 = frames[0].image.copyAsFormat(PixelFormat::RGBA8);
const u32 canvasW = canvas0.width();
const u32 canvasH = canvas0.height();
if (canvasW == 0 || canvasH == 0) {
fail("Cannot write an APNG frame with zero dimensions");
return {};
}
std::vector<u8> output;
output.reserve(64 + static_cast<size_t>(canvasW) * canvasH * 4 * frames.size());
output.insert(output.end(), PNG_SIGNATURE.begin(), PNG_SIGNATURE.end());
u8 ihdr[13];
writeU32BE(ihdr + 0, canvasW);
writeU32BE(ihdr + 4, canvasH);
ihdr[8] = 8; ihdr[9] = COLOR_TRUECOLOR_ALPHA; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; writeChunk(output, CHUNK_IHDR, ihdr, 13);
u8 actl[8];
writeU32BE(actl + 0, static_cast<u32>(frames.size()));
writeU32BE(actl + 4, opts.loopCount);
writeChunk(output, CHUNK_acTL, actl, 8);
u32 seq = 0;
for (size_t i = 0; i < frames.size(); ++i) {
Texture rgba =
(i == 0) ? std::move(canvas0) : frames[i].image.copyAsFormat(PixelFormat::RGBA8);
if (rgba.width() != canvasW || rgba.height() != canvasH) {
fail("All APNG frames must share the same dimensions");
return {};
}
u32 const dm = frames[i].delayMs;
u16 const delayNum = (dm > 0xFFFF) ? 0xFFFF : static_cast<u16>(dm);
u8 fctl[26];
writeU32BE(fctl + 0, seq++);
writeU32BE(fctl + 4, canvasW);
writeU32BE(fctl + 8, canvasH);
writeU32BE(fctl + 12, 0); writeU32BE(fctl + 16, 0); writeU16BE(fctl + 20, delayNum);
writeU16BE(fctl + 22, 1000); fctl[24] = DISPOSE_NONE;
fctl[25] = BLEND_SOURCE;
writeChunk(output, CHUNK_fcTL, fctl, 26);
std::vector<u8> filtered = filterScanlines(rgba.dataPtr(), canvasW, canvasH);
std::string compressError;
auto compressed =
zlib_compress(std::span<const u8>(filtered.data(), filtered.size()), &compressError);
if (compressed.empty()) {
fail("Failed to compress APNG frame data: " + compressError);
return {};
}
if (i == 0) {
writeChunk(output, CHUNK_IDAT, compressed.data(), static_cast<u32>(compressed.size()));
} else {
std::vector<u8> fdat(4 + compressed.size());
writeU32BE(fdat.data(), seq++);
std::memcpy(fdat.data() + 4, compressed.data(), compressed.size());
writeChunk(output, CHUNK_fdAT, fdat.data(), static_cast<u32>(fdat.size()));
}
}
writeChunk(output, CHUNK_IEND, nullptr, 0);
return output;
}
Writer::Writer() : pImpl(std::make_unique<Impl>()) {}
Writer::~Writer() = default;
void Writer::write(const std::string& filePath, const Texture& texture) {
auto data = pImpl->write(texture);
if (data.empty()) {
return;
}
if (!write_file_bytes(filePath, data, *pImpl)) {
return;
}
}
std::vector<u8> Writer::write(const Texture& texture) {
return pImpl->write(texture);
}
std::vector<u8> Writer::writeAnimated(const std::vector<ApngFrame>& frames,
const ApngSaveOptions& opts) {
return pImpl->writeAnimated(frames, opts);
}
void Writer::writeAnimated(const std::string& filePath, const std::vector<ApngFrame>& frames,
const ApngSaveOptions& opts) {
auto data = pImpl->writeAnimated(frames, opts);
if (data.empty()) {
return;
}
write_file_bytes(filePath, data, *pImpl);
}
bool Writer::hasIssues() const {
return !pImpl->issues.empty();
}
const std::vector<std::string>& Writer::getIssues() const {
return pImpl->issues;
}
}