Add files via upload
This commit is contained in:
35 files changed
+14482
No files matched your search
@@ -0,0 +1,596 @@
|
||||
#include "export.hpp"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <SDL_image.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace sppm {
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
|
||||
void be32(Bytes& out, std::uint32_t value) {
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 24));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 16));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 8));
|
||||
out.push_back(static_cast<std::uint8_t>(value));
|
||||
}
|
||||
void be16(Bytes& out, std::uint16_t value) {
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 8));
|
||||
out.push_back(static_cast<std::uint8_t>(value));
|
||||
}
|
||||
void type(Bytes& out, const char* value) {
|
||||
out.insert(out.end(), value, value + 4);
|
||||
}
|
||||
std::size_t boxBegin(Bytes& out, const char* name) {
|
||||
const std::size_t start = out.size();
|
||||
be32(out, 0); type(out, name);
|
||||
return start;
|
||||
}
|
||||
void boxEnd(Bytes& out, std::size_t start) {
|
||||
const std::size_t size = out.size() - start;
|
||||
out[start] = static_cast<std::uint8_t>(size >> 24);
|
||||
out[start + 1] = static_cast<std::uint8_t>(size >> 16);
|
||||
out[start + 2] = static_cast<std::uint8_t>(size >> 8);
|
||||
out[start + 3] = static_cast<std::uint8_t>(size);
|
||||
}
|
||||
std::size_t fullBoxBegin(Bytes& out, const char* name, std::uint8_t version = 0,
|
||||
std::uint32_t flags = 0) {
|
||||
const auto start = boxBegin(out, name);
|
||||
out.push_back(version);
|
||||
out.push_back(static_cast<std::uint8_t>(flags >> 16));
|
||||
out.push_back(static_cast<std::uint8_t>(flags >> 8));
|
||||
out.push_back(static_cast<std::uint8_t>(flags));
|
||||
return start;
|
||||
}
|
||||
|
||||
Bytes mp4MovieBox(const std::vector<std::uint32_t>& sizes,
|
||||
const std::vector<std::uint32_t>& offsets,
|
||||
double fps) {
|
||||
constexpr std::uint32_t timeScale = 1000000;
|
||||
const std::uint32_t frameDelta = static_cast<std::uint32_t>(std::lround(timeScale / fps));
|
||||
const std::uint32_t duration = static_cast<std::uint32_t>(std::llround(sizes.size() * static_cast<double>(timeScale) / fps));
|
||||
Bytes out;
|
||||
auto moov = boxBegin(out, "moov");
|
||||
auto mvhd = fullBoxBegin(out, "mvhd");
|
||||
be32(out, 0); be32(out, 0); be32(out, timeScale);
|
||||
be32(out, duration);
|
||||
be32(out, 0x00010000); be16(out, 0x0100); be16(out, 0);
|
||||
be32(out, 0); be32(out, 0);
|
||||
const std::uint32_t matrix[] = {0x00010000,0,0, 0,0x00010000,0, 0,0,0x40000000};
|
||||
for (auto value : matrix) be32(out, value);
|
||||
for (int i = 0; i < 6; ++i) be32(out, 0);
|
||||
be32(out, 2); boxEnd(out, mvhd);
|
||||
|
||||
auto trak = boxBegin(out, "trak");
|
||||
auto tkhd = fullBoxBegin(out, "tkhd", 0, 7);
|
||||
be32(out, 0); be32(out, 0); be32(out, 1); be32(out, 0);
|
||||
be32(out, duration);
|
||||
be32(out, 0); be32(out, 0); be16(out, 0); be16(out, 0); be16(out, 0); be16(out, 0);
|
||||
for (auto value : matrix) be32(out, value);
|
||||
be32(out, static_cast<std::uint32_t>(kWidth) << 16);
|
||||
be32(out, static_cast<std::uint32_t>(kHeight) << 16); boxEnd(out, tkhd);
|
||||
|
||||
auto mdia = boxBegin(out, "mdia");
|
||||
auto mdhd = fullBoxBegin(out, "mdhd");
|
||||
be32(out, 0); be32(out, 0); be32(out, timeScale);
|
||||
be32(out, duration); be16(out, 0x55c4); be16(out, 0);
|
||||
boxEnd(out, mdhd);
|
||||
auto hdlr = fullBoxBegin(out, "hdlr");
|
||||
be32(out, 0); type(out, "vide"); be32(out, 0); be32(out, 0); be32(out, 0);
|
||||
out.insert(out.end(), {'S','w','i','t','c','h','n','o','t','e',' ','V','i','d','e','o',0});
|
||||
boxEnd(out, hdlr);
|
||||
auto minf = boxBegin(out, "minf");
|
||||
auto vmhd = fullBoxBegin(out, "vmhd", 0, 1);
|
||||
be16(out, 0); be16(out, 0); be16(out, 0); be16(out, 0); boxEnd(out, vmhd);
|
||||
auto dinf = boxBegin(out, "dinf");
|
||||
auto dref = fullBoxBegin(out, "dref"); be32(out, 1);
|
||||
auto url = fullBoxBegin(out, "url ", 0, 1); boxEnd(out, url); boxEnd(out, dref); boxEnd(out, dinf);
|
||||
auto stbl = boxBegin(out, "stbl");
|
||||
auto stsd = fullBoxBegin(out, "stsd"); be32(out, 1);
|
||||
auto jpeg = boxBegin(out, "mjpg");
|
||||
be32(out, 0); be16(out, 0); be16(out, 1);
|
||||
be16(out, 0); be16(out, 0); be32(out, 0); be32(out, 0); be32(out, 0);
|
||||
be16(out, kWidth); be16(out, kHeight); be32(out, 0x00480000); be32(out, 0x00480000);
|
||||
be32(out, 0); be16(out, 1); out.insert(out.end(), 32, 0); be16(out, 0x0018); be16(out, 0xffff);
|
||||
boxEnd(out, jpeg); boxEnd(out, stsd);
|
||||
auto stts = fullBoxBegin(out, "stts"); be32(out, 1); be32(out, static_cast<std::uint32_t>(sizes.size())); be32(out, frameDelta); boxEnd(out, stts);
|
||||
auto stsc = fullBoxBegin(out, "stsc"); be32(out, 1); be32(out, 1); be32(out, 1); be32(out, 1); boxEnd(out, stsc);
|
||||
auto stsz = fullBoxBegin(out, "stsz"); be32(out, 0); be32(out, static_cast<std::uint32_t>(sizes.size()));
|
||||
for (auto size : sizes) be32(out, size);
|
||||
boxEnd(out, stsz);
|
||||
auto stco = fullBoxBegin(out, "stco"); be32(out, static_cast<std::uint32_t>(offsets.size()));
|
||||
for (auto offset : offsets) be32(out, offset);
|
||||
boxEnd(out, stco);
|
||||
auto stss = fullBoxBegin(out, "stss"); be32(out, static_cast<std::uint32_t>(sizes.size()));
|
||||
for (std::size_t i = 0; i < sizes.size(); ++i) be32(out, static_cast<std::uint32_t>(i + 1));
|
||||
boxEnd(out, stss);
|
||||
boxEnd(out, stbl); boxEnd(out, minf); boxEnd(out, mdia); boxEnd(out, trak); boxEnd(out, moov);
|
||||
return out;
|
||||
}
|
||||
|
||||
Color opaqueFramePixel(const Frame& frame, const Palette& palette, std::size_t pixel) {
|
||||
Color result{255, 255, 255, 255};
|
||||
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
|
||||
const auto index = frame.layers[layerIndex][pixel];
|
||||
if (!index) continue;
|
||||
const auto& source = palette[index];
|
||||
if (!source.a) continue;
|
||||
const unsigned alpha = source.a;
|
||||
result.r = static_cast<std::uint8_t>((source.r * alpha + result.r * (255 - alpha) + 127) / 255);
|
||||
result.g = static_cast<std::uint8_t>((source.g * alpha + result.g * (255 - alpha) + 127) / 255);
|
||||
result.b = static_cast<std::uint8_t>((source.b * alpha + result.b * (255 - alpha) + 127) / 255);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool checkDocument(const Document& document, bool cover, std::string& error) {
|
||||
if (document.frames.empty() || document.frames.size() > kMaxFrames) {
|
||||
error = "frame count must be between 1 and 9999";
|
||||
return false;
|
||||
}
|
||||
if (document.metadata.fps < 1 || document.metadata.fps > 30) {
|
||||
error = "frame rate must be between 1 and 30 fps";
|
||||
return false;
|
||||
}
|
||||
if (cover && document.metadata.coverFrame >= document.frames.size()) {
|
||||
error = "cover frame is out of range";
|
||||
return false;
|
||||
}
|
||||
for (const Frame& frame : document.frames) {
|
||||
for (const auto& layer : frame.layers) {
|
||||
if (layer.size() != kPixelsPerLayer) {
|
||||
error = "a layer has the wrong pixel count";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Straight-alpha source-over compositing, with A foremost and D rearmost.
|
||||
Color compositePixel(const Frame& frame, const Palette& palette,
|
||||
std::size_t pixel) {
|
||||
Color result{0, 0, 0, 0};
|
||||
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
|
||||
const auto& layer = frame.layers[layerIndex];
|
||||
const std::uint8_t index = layer[pixel];
|
||||
if (index == 0) continue;
|
||||
const Color& source = palette[index];
|
||||
if (source.a == 0) continue;
|
||||
if (source.a == 255) {
|
||||
result = source;
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t sourceWeight =
|
||||
static_cast<std::uint32_t>(source.a) * 255U;
|
||||
const std::uint32_t behindWeight =
|
||||
static_cast<std::uint32_t>(result.a) * (255U - source.a);
|
||||
const std::uint32_t total = sourceWeight + behindWeight;
|
||||
result.r = static_cast<std::uint8_t>(
|
||||
(source.r * sourceWeight + result.r * behindWeight + total / 2U) / total);
|
||||
result.g = static_cast<std::uint8_t>(
|
||||
(source.g * sourceWeight + result.g * behindWeight + total / 2U) / total);
|
||||
result.b = static_cast<std::uint8_t>(
|
||||
(source.b * sourceWeight + result.b * behindWeight + total / 2U) / total);
|
||||
result.a = static_cast<std::uint8_t>((total + 127U) / 255U);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint8_t nearestPaletteIndex(const Color& color, const Palette& palette) {
|
||||
std::uint32_t bestDistance = 3U * 255U * 255U + 1U;
|
||||
std::uint8_t best = 0;
|
||||
for (std::size_t i = 1; i < palette.size(); ++i) {
|
||||
const Color& candidate = palette[i];
|
||||
if (candidate.a == 0) continue;
|
||||
const int dr = static_cast<int>(color.r) - candidate.r;
|
||||
const int dg = static_cast<int>(color.g) - candidate.g;
|
||||
const int db = static_cast<int>(color.b) - candidate.b;
|
||||
const std::uint32_t distance =
|
||||
static_cast<std::uint32_t>(dr * dr + dg * dg + db * db);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = static_cast<std::uint8_t>(i);
|
||||
if (distance == 0) break;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// GIF has a single transparent index, so partially transparent colors are
|
||||
// composited first, then matched to the nearest RGB color in the palette.
|
||||
std::vector<std::uint8_t> gifPixels(const Frame& frame,
|
||||
const Palette& palette) {
|
||||
std::vector<std::uint8_t> pixels(kPixelsPerLayer, 0);
|
||||
bool hasPartialAlpha = false;
|
||||
for (std::size_t i = 1; i < palette.size(); ++i) {
|
||||
if (palette[i].a != 0 && palette[i].a != 255) {
|
||||
hasPartialAlpha = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasPartialAlpha) {
|
||||
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
|
||||
const auto& layer = frame.layers[layerIndex];
|
||||
for (std::size_t pixel = 0; pixel < pixels.size(); ++pixel) {
|
||||
const std::uint8_t index = layer[pixel];
|
||||
if (index != 0 && palette[index].a != 0) pixels[pixel] = index;
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
std::unordered_map<std::uint32_t, std::uint8_t> matchCache;
|
||||
matchCache.reserve(512);
|
||||
for (std::size_t pixel = 0; pixel < pixels.size(); ++pixel) {
|
||||
const Color color = compositePixel(frame, palette, pixel);
|
||||
if (color.a < 128) continue;
|
||||
const std::uint32_t key =
|
||||
(static_cast<std::uint32_t>(color.r) << 16U) |
|
||||
(static_cast<std::uint32_t>(color.g) << 8U) | color.b;
|
||||
const auto found = matchCache.find(key);
|
||||
if (found != matchCache.end()) {
|
||||
pixels[pixel] = found->second;
|
||||
} else {
|
||||
const std::uint8_t index = nearestPaletteIndex(color, palette);
|
||||
matchCache.emplace(key, index);
|
||||
pixels[pixel] = index;
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
struct BitPacker {
|
||||
std::vector<std::uint8_t> data;
|
||||
std::uint32_t pending = 0;
|
||||
unsigned pendingBits = 0;
|
||||
|
||||
void code(std::uint16_t value, unsigned width) {
|
||||
pending |= static_cast<std::uint32_t>(value) << pendingBits;
|
||||
pendingBits += width;
|
||||
while (pendingBits >= 8) {
|
||||
data.push_back(static_cast<std::uint8_t>(pending & 0xFFU));
|
||||
pending >>= 8U;
|
||||
pendingBits -= 8U;
|
||||
}
|
||||
}
|
||||
|
||||
void finish() {
|
||||
if (pendingBits != 0) data.push_back(static_cast<std::uint8_t>(pending));
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> gifLzw(const std::vector<std::uint8_t>& pixels) {
|
||||
constexpr std::uint16_t clearCode = 256;
|
||||
constexpr std::uint16_t endCode = 257;
|
||||
std::unordered_map<std::uint32_t, std::uint16_t> dictionary;
|
||||
dictionary.reserve(4096);
|
||||
std::uint16_t nextCode = 258;
|
||||
unsigned codeWidth = 9;
|
||||
BitPacker writer;
|
||||
writer.data.reserve(pixels.size() / 2);
|
||||
writer.code(clearCode, codeWidth);
|
||||
std::uint16_t prefix = pixels.front();
|
||||
|
||||
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||
const std::uint8_t suffix = pixels[i];
|
||||
const std::uint32_t key =
|
||||
(static_cast<std::uint32_t>(prefix) << 8U) | suffix;
|
||||
const auto found = dictionary.find(key);
|
||||
if (found != dictionary.end()) {
|
||||
prefix = found->second;
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.code(prefix, codeWidth);
|
||||
if (nextCode < 4096) {
|
||||
dictionary.emplace(key, nextCode++);
|
||||
// GIF's decoder learns each new phrase one code later.
|
||||
if (codeWidth < 12 && nextCode > (1U << codeWidth)) ++codeWidth;
|
||||
} else {
|
||||
writer.code(clearCode, codeWidth);
|
||||
dictionary.clear();
|
||||
nextCode = 258;
|
||||
codeWidth = 9;
|
||||
}
|
||||
prefix = suffix;
|
||||
}
|
||||
writer.code(prefix, codeWidth);
|
||||
writer.code(endCode, codeWidth);
|
||||
writer.finish();
|
||||
return writer.data;
|
||||
}
|
||||
|
||||
void byte(std::ostream& out, std::uint8_t value) {
|
||||
out.put(static_cast<char>(value));
|
||||
}
|
||||
|
||||
void le16(std::ostream& out, std::uint16_t value) {
|
||||
byte(out, static_cast<std::uint8_t>(value));
|
||||
byte(out, static_cast<std::uint8_t>(value >> 8U));
|
||||
}
|
||||
|
||||
void be32(std::ostream& out, std::uint32_t value) {
|
||||
byte(out, static_cast<std::uint8_t>(value >> 24U));
|
||||
byte(out, static_cast<std::uint8_t>(value >> 16U));
|
||||
byte(out, static_cast<std::uint8_t>(value >> 8U));
|
||||
byte(out, static_cast<std::uint8_t>(value));
|
||||
}
|
||||
|
||||
std::uint32_t crc32(const char* type, const std::vector<std::uint8_t>& data) {
|
||||
std::uint32_t crc = 0xFFFFFFFFU;
|
||||
auto update = [&crc](std::uint8_t value) {
|
||||
crc ^= value;
|
||||
for (unsigned bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc >> 1U) ^ ((crc & 1U) ? 0xEDB88320U : 0U);
|
||||
}
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) update(static_cast<std::uint8_t>(type[i]));
|
||||
for (std::uint8_t value : data) update(value);
|
||||
return crc ^ 0xFFFFFFFFU;
|
||||
}
|
||||
|
||||
void pngChunk(std::ostream& out, const char* type,
|
||||
const std::vector<std::uint8_t>& data) {
|
||||
be32(out, static_cast<std::uint32_t>(data.size()));
|
||||
out.write(type, 4);
|
||||
if (!data.empty()) {
|
||||
out.write(reinterpret_cast<const char*>(data.data()),
|
||||
static_cast<std::streamsize>(data.size()));
|
||||
}
|
||||
be32(out, crc32(type, data));
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> zlibStored(const std::vector<std::uint8_t>& raw) {
|
||||
std::vector<std::uint8_t> encoded;
|
||||
encoded.reserve(raw.size() + 6 + (raw.size() / 65535 + 1) * 5);
|
||||
encoded.push_back(0x78); // zlib CMF: deflate, 32 KiB window
|
||||
encoded.push_back(0x01); // zlib FLG: no compression
|
||||
std::size_t offset = 0;
|
||||
while (offset < raw.size()) {
|
||||
const std::size_t length = std::min<std::size_t>(65535, raw.size() - offset);
|
||||
const bool final = offset + length == raw.size();
|
||||
const std::uint16_t len = static_cast<std::uint16_t>(length);
|
||||
const std::uint16_t inverse = static_cast<std::uint16_t>(~len);
|
||||
encoded.push_back(final ? 0x01 : 0x00);
|
||||
encoded.push_back(static_cast<std::uint8_t>(len));
|
||||
encoded.push_back(static_cast<std::uint8_t>(len >> 8U));
|
||||
encoded.push_back(static_cast<std::uint8_t>(inverse));
|
||||
encoded.push_back(static_cast<std::uint8_t>(inverse >> 8U));
|
||||
encoded.insert(encoded.end(), raw.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
raw.begin() + static_cast<std::ptrdiff_t>(offset + length));
|
||||
offset += length;
|
||||
}
|
||||
std::uint32_t a = 1;
|
||||
std::uint32_t b = 0;
|
||||
for (std::uint8_t value : raw) {
|
||||
a = (a + value) % 65521U;
|
||||
b = (b + a) % 65521U;
|
||||
}
|
||||
const std::uint32_t adler = (b << 16U) | a;
|
||||
encoded.push_back(static_cast<std::uint8_t>(adler >> 24U));
|
||||
encoded.push_back(static_cast<std::uint8_t>(adler >> 16U));
|
||||
encoded.push_back(static_cast<std::uint8_t>(adler >> 8U));
|
||||
encoded.push_back(static_cast<std::uint8_t>(adler));
|
||||
return encoded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool exportGif(const Document& document, const std::string& path,
|
||||
std::string& error) {
|
||||
if (!checkDocument(document, false, error)) return false;
|
||||
if (path.empty()) {
|
||||
error = "GIF output path is empty";
|
||||
return false;
|
||||
}
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
if (!out) {
|
||||
error = "could not open GIF output";
|
||||
return false;
|
||||
}
|
||||
out.write("GIF89a", 6);
|
||||
le16(out, kWidth);
|
||||
le16(out, kHeight);
|
||||
byte(out, 0xF7); // global 256-color table, 8-bit color resolution
|
||||
byte(out, 1); // white in the default palette as the canvas background
|
||||
byte(out, 0);
|
||||
for (const Color& color : document.palette) {
|
||||
byte(out, color.r);
|
||||
byte(out, color.g);
|
||||
byte(out, color.b);
|
||||
}
|
||||
|
||||
// Netscape application extension: loop indefinitely.
|
||||
const std::array<std::uint8_t, 19> loop = {
|
||||
0x21, 0xFF, 0x0B, 'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0',
|
||||
0x03, 0x01, 0x00, 0x00, 0x00};
|
||||
out.write(reinterpret_cast<const char*>(loop.data()),
|
||||
static_cast<std::streamsize>(loop.size()));
|
||||
|
||||
for (std::size_t i = 0; i < document.frames.size(); ++i) {
|
||||
const double fps = frameRate(document.metadata);
|
||||
const unsigned start = static_cast<unsigned>(std::llround(i * 100.0 / fps));
|
||||
const unsigned end = static_cast<unsigned>(std::llround((i + 1U) * 100.0 / fps));
|
||||
const std::uint16_t delay =
|
||||
static_cast<std::uint16_t>(std::max(1U, end - start));
|
||||
byte(out, 0x21); byte(out, 0xF9); byte(out, 0x04);
|
||||
byte(out, 0x09); // restore background before next frame, index 0 transparent
|
||||
le16(out, delay);
|
||||
byte(out, 0); byte(out, 0);
|
||||
|
||||
byte(out, 0x2C); // full-canvas image descriptor
|
||||
le16(out, 0); le16(out, 0);
|
||||
le16(out, kWidth); le16(out, kHeight);
|
||||
byte(out, 0); // use global palette
|
||||
byte(out, 8); // LZW minimum code size
|
||||
const std::vector<std::uint8_t> pixels =
|
||||
gifPixels(document.frames[i], document.palette);
|
||||
const std::vector<std::uint8_t> compressed = gifLzw(pixels);
|
||||
for (std::size_t offset = 0; offset < compressed.size();) {
|
||||
const std::size_t length =
|
||||
std::min<std::size_t>(255, compressed.size() - offset);
|
||||
byte(out, static_cast<std::uint8_t>(length));
|
||||
out.write(reinterpret_cast<const char*>(compressed.data() + offset),
|
||||
static_cast<std::streamsize>(length));
|
||||
offset += length;
|
||||
}
|
||||
byte(out, 0); // terminate LZW sub-blocks
|
||||
if (!out) {
|
||||
error = "could not write GIF frame";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
byte(out, 0x3B);
|
||||
out.flush();
|
||||
if (!out) {
|
||||
error = "could not finish GIF output";
|
||||
return false;
|
||||
}
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool exportPngCover(const Document& document, const std::string& path,
|
||||
std::string& error) {
|
||||
if (!checkDocument(document, true, error)) return false;
|
||||
if (path.empty()) {
|
||||
error = "PNG output path is empty";
|
||||
return false;
|
||||
}
|
||||
const Frame& frame = document.frames[document.metadata.coverFrame];
|
||||
std::vector<std::uint8_t> raw;
|
||||
raw.reserve(static_cast<std::size_t>(kHeight) * (1U + 4U * kWidth));
|
||||
for (std::size_t y = 0; y < kHeight; ++y) {
|
||||
raw.push_back(0); // PNG filter type 0
|
||||
for (std::size_t x = 0; x < kWidth; ++x) {
|
||||
const Color pixel = compositePixel(frame, document.palette,
|
||||
y * kWidth + x);
|
||||
raw.push_back(pixel.r);
|
||||
raw.push_back(pixel.g);
|
||||
raw.push_back(pixel.b);
|
||||
raw.push_back(pixel.a);
|
||||
}
|
||||
}
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
if (!out) {
|
||||
error = "could not open PNG output";
|
||||
return false;
|
||||
}
|
||||
constexpr std::array<std::uint8_t, 8> signature =
|
||||
{137, 80, 78, 71, 13, 10, 26, 10};
|
||||
out.write(reinterpret_cast<const char*>(signature.data()),
|
||||
static_cast<std::streamsize>(signature.size()));
|
||||
const std::vector<std::uint8_t> header = {
|
||||
static_cast<std::uint8_t>(kWidth >> 24U),
|
||||
static_cast<std::uint8_t>(kWidth >> 16U),
|
||||
static_cast<std::uint8_t>(kWidth >> 8U),
|
||||
static_cast<std::uint8_t>(kWidth),
|
||||
static_cast<std::uint8_t>(kHeight >> 24U),
|
||||
static_cast<std::uint8_t>(kHeight >> 16U),
|
||||
static_cast<std::uint8_t>(kHeight >> 8U),
|
||||
static_cast<std::uint8_t>(kHeight),
|
||||
8, 6, 0, 0, 0}; // RGBA, 8 bits/channel, no interlace
|
||||
pngChunk(out, "IHDR", header);
|
||||
pngChunk(out, "IDAT", zlibStored(raw));
|
||||
pngChunk(out, "IEND", {});
|
||||
out.flush();
|
||||
if (!out) {
|
||||
error = "could not finish PNG output";
|
||||
return false;
|
||||
}
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool exportMp4(const Document& document, const std::string& path,
|
||||
std::string& error) {
|
||||
if (!checkDocument(document, false, error)) return false;
|
||||
if (path.empty()) { error = "MP4 output path is empty"; return false; }
|
||||
const double fps = frameRate(document.metadata);
|
||||
if (!(fps > 0.0 && fps <= 30.0)) { error = "frame rate must be between 0 and 30 fps"; return false; }
|
||||
|
||||
Bytes ftyp;
|
||||
auto ftypBox = boxBegin(ftyp, "ftyp");
|
||||
type(ftyp, "isom"); be32(ftyp, 0x00000200); type(ftyp, "isom"); type(ftyp, "iso2"); type(ftyp, "mp41"); type(ftyp, "mjpg");
|
||||
boxEnd(ftyp, ftypBox);
|
||||
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
if (!out) { error = "could not create MP4 file"; return false; }
|
||||
out.write(reinterpret_cast<const char*>(ftyp.data()), static_cast<std::streamsize>(ftyp.size()));
|
||||
const std::uint32_t mdatStart = static_cast<std::uint32_t>(ftyp.size());
|
||||
const std::uint8_t mdatHeader[8] = {0,0,0,0,'m','d','a','t'};
|
||||
out.write(reinterpret_cast<const char*>(mdatHeader), sizeof(mdatHeader));
|
||||
std::vector<std::uint32_t> sampleSizes;
|
||||
std::vector<std::uint32_t> sampleOffsets;
|
||||
sampleSizes.reserve(document.frames.size());
|
||||
sampleOffsets.reserve(document.frames.size());
|
||||
|
||||
constexpr int jpegCapacity = 2 * 1024 * 1024;
|
||||
std::vector<std::uint8_t> jpegBuffer(static_cast<std::size_t>(jpegCapacity));
|
||||
for (std::size_t frameIndex = 0; frameIndex < document.frames.size(); ++frameIndex) {
|
||||
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, kWidth, kHeight, 32, SDL_PIXELFORMAT_RGBA32);
|
||||
if (!surface) { error = "could not allocate frame image"; return false; }
|
||||
if (SDL_LockSurface(surface) != 0) {
|
||||
SDL_FreeSurface(surface); error = "could not prepare frame image"; return false;
|
||||
}
|
||||
auto* rows = static_cast<std::uint8_t*>(surface->pixels);
|
||||
for (std::size_t y = 0; y < kHeight; ++y) {
|
||||
auto* row = reinterpret_cast<std::uint32_t*>(rows + y * static_cast<std::size_t>(surface->pitch));
|
||||
for (std::size_t x = 0; x < kWidth; ++x) {
|
||||
const Color pixel = opaqueFramePixel(document.frames[frameIndex], document.palette, y * kWidth + x);
|
||||
row[x] = SDL_MapRGBA(surface->format, pixel.r, pixel.g, pixel.b, 255);
|
||||
}
|
||||
}
|
||||
SDL_UnlockSurface(surface);
|
||||
SDL_RWops* jpegStream = SDL_RWFromMem(jpegBuffer.data(), jpegCapacity);
|
||||
if (!jpegStream || IMG_SaveJPG_RW(surface, jpegStream, 0, 82) != 0) {
|
||||
if (jpegStream) SDL_RWclose(jpegStream);
|
||||
SDL_FreeSurface(surface); error = "could not encode an MP4 frame as JPEG"; return false;
|
||||
}
|
||||
const Sint64 jpegSize = SDL_RWtell(jpegStream);
|
||||
SDL_RWclose(jpegStream);
|
||||
SDL_FreeSurface(surface);
|
||||
if (jpegSize <= 0 || jpegSize > jpegCapacity) { error = "encoded frame is too large"; return false; }
|
||||
const auto position = out.tellp();
|
||||
if (position < 0 || static_cast<std::uint64_t>(position) > UINT32_MAX) {
|
||||
error = "MP4 exceeds the supported 4 GB file limit"; return false;
|
||||
}
|
||||
sampleOffsets.push_back(static_cast<std::uint32_t>(position));
|
||||
sampleSizes.push_back(static_cast<std::uint32_t>(jpegSize));
|
||||
out.write(reinterpret_cast<const char*>(jpegBuffer.data()), jpegSize);
|
||||
if (!out) { error = "could not write MP4 frame data"; return false; }
|
||||
}
|
||||
const auto endOfMedia = out.tellp();
|
||||
if (endOfMedia < 0 || static_cast<std::uint64_t>(endOfMedia) - mdatStart > UINT32_MAX) {
|
||||
error = "MP4 exceeds the supported 4 GB file limit"; return false;
|
||||
}
|
||||
const std::uint32_t mdatSize = static_cast<std::uint32_t>(static_cast<std::uint64_t>(endOfMedia) - mdatStart);
|
||||
out.seekp(mdatStart);
|
||||
const std::uint8_t encodedMdatSize[4] = {
|
||||
static_cast<std::uint8_t>(mdatSize >> 24), static_cast<std::uint8_t>(mdatSize >> 16),
|
||||
static_cast<std::uint8_t>(mdatSize >> 8), static_cast<std::uint8_t>(mdatSize)};
|
||||
out.write(reinterpret_cast<const char*>(encodedMdatSize), sizeof(encodedMdatSize));
|
||||
out.seekp(endOfMedia);
|
||||
const Bytes moov = mp4MovieBox(sampleSizes, sampleOffsets, fps);
|
||||
out.write(reinterpret_cast<const char*>(moov.data()), static_cast<std::streamsize>(moov.size()));
|
||||
out.flush();
|
||||
if (!out) { error = "could not finish MP4 index"; return false; }
|
||||
out.close();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sppm
|
||||
Reference in new issue
Block a user
