#include <whiteout/textures/jpeg/writer.h>
#include "../io_helpers.h"
#include "../issue_sink.h"
#include "../utils/color_convert.h"
#include "jpeg_common.h"
#include "jpeg_encode.h"
#include <algorithm>
#include <cstring>
#include <memory>
#include <stdexcept>
namespace whiteout::textures::jpeg {
class Writer::Impl : public IssueSink {
public:
i32 quality = 75;
interfaces::WorkerPool* pool = nullptr;
bool progressive = false;
std::vector<u8> write(const Texture& texture);
};
static std::vector<u8> build_jfif_app0() {
std::vector<u8> seg;
seg.reserve(20);
seg.push_back(0xFF);
seg.push_back(0xE0);
seg.push_back(0x00);
seg.push_back(0x10);
seg.push_back('J');
seg.push_back('F');
seg.push_back('I');
seg.push_back('F');
seg.push_back(0x00);
seg.push_back(0x01);
seg.push_back(0x01);
seg.push_back(0x00);
seg.push_back(0x00);
seg.push_back(0x01);
seg.push_back(0x00);
seg.push_back(0x01);
seg.push_back(0x00);
seg.push_back(0x00);
return seg;
}
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.copyAsFormat(PixelFormat::RGBA8);
const u32 width = rgba.width();
const u32 height = rgba.height();
const u8* src = rgba.dataPtr();
const u32 pixelCount = width * height;
whiteout::textures::jpeg::Image image;
image.width = width;
image.height = height;
image.components = 3;
image.pixels.resize(static_cast<size_t>(width) * height * 3);
std::unique_ptr<interfaces::TimelineSemaphore> sem;
JpegContext jctx;
JpegContext* ctxPtr = nullptr;
if (pool && pool->threadCount() > 1) {
sem = pool->createTimelineSemaphore();
if (sem) {
jctx.pool = pool;
jctx.sem = sem.get();
jctx.currentValue = sem->value();
ctxPtr = &jctx;
}
}
if (ctxPtr) {
parallel_for_blocks(pixelCount, ctxPtr, [src, &image](u32 begin, u32 end) {
for (u32 i = begin; i < end; ++i) {
rgb_to_ycbcr(src[i * 4 + 0], src[i * 4 + 1], src[i * 4 + 2],
image.pixels[i * 3 + 0], image.pixels[i * 3 + 1],
image.pixels[i * 3 + 2]);
}
});
std::vector<u8> encodedOutput;
std::string encodeError;
whiteout::textures::jpeg::encode_raw(image, quality, &encodeError, progressive, ctxPtr,
&encodedOutput);
jctx.sem->wait(jctx.currentValue);
if (encodedOutput.empty()) {
fail("JPEG encode failed: " + encodeError);
return {};
}
auto app0 = build_jfif_app0();
std::vector<u8> output;
output.reserve(encodedOutput.size() + app0.size());
output.insert(output.end(), encodedOutput.begin(), encodedOutput.begin() + 2);
output.insert(output.end(), app0.begin(), app0.end());
output.insert(output.end(), encodedOutput.begin() + 2, encodedOutput.end());
return output;
}
for (u32 i = 0; i < pixelCount; ++i) {
rgb_to_ycbcr(src[i * 4 + 0], src[i * 4 + 1], src[i * 4 + 2], image.pixels[i * 3 + 0],
image.pixels[i * 3 + 1], image.pixels[i * 3 + 2]);
}
std::string encodeError;
auto encoded = whiteout::textures::jpeg::encode_raw(image, quality, &encodeError, progressive);
if (encoded.empty()) {
fail("JPEG encode failed: " + encodeError);
return {};
}
auto app0 = build_jfif_app0();
std::vector<u8> output;
output.reserve(encoded.size() + app0.size());
output.insert(output.end(), encoded.begin(), encoded.begin() + 2);
output.insert(output.end(), app0.begin(), app0.end());
output.insert(output.end(), encoded.begin() + 2, encoded.end());
return output;
}
Writer::Writer(i32 quality, interfaces::WorkerPool* pool, bool progressive)
: pImpl(std::make_unique<Impl>()) {
pImpl->quality = std::clamp(quality, 1, 100);
pImpl->pool = pool;
pImpl->progressive = progressive;
}
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);
}
bool Writer::hasIssues() const {
return !pImpl->issues.empty();
}
const std::vector<std::string>& Writer::getIssues() const {
return pImpl->issues;
}
}