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.
3156 lines
109 KiB
3156 lines
109 KiB
#include "monoformat_structured.hpp"
|
|
#include "monoformat_fontreader.hpp"
|
|
#include "monoformat_bithelpers.hpp"
|
|
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <format>
|
|
|
|
namespace monoformat {
|
|
|
|
static std::string base64Encode(std::span<std::byte const> buffer);
|
|
static std::vector<std::byte> base64Decode(std::string_view data);
|
|
|
|
NLOHMANN_JSON_SERIALIZE_ENUM(SectionType, {
|
|
{SectionType::AlwaysDrawn, "AlwaysDrawn"},
|
|
{SectionType::TimeBasedDrawn, "TimeBasedDrawn"},
|
|
{SectionType::MultiTimeBasedDrawn, "MultiTimeBasedDrawn"},
|
|
{SectionType::ExpiryDate, "ExpiryDate"},
|
|
{SectionType::CustomFont, "CustomFont"},
|
|
})
|
|
|
|
NLOHMANN_JSON_SERIALIZE_ENUM(ElementType, {
|
|
{ElementType::Image, "Image"},
|
|
{ElementType::Animation, "Animation"},
|
|
{ElementType::HScrollImage, "HScrollImage"},
|
|
{ElementType::VScrollImage, "VScrollImage"},
|
|
{ElementType::Line, "Line"},
|
|
{ElementType::Box, "Box"},
|
|
{ElementType::ClippedText, "ClippedText"},
|
|
{ElementType::HScrollText, "HScrollText"},
|
|
{ElementType::CurrentTime, "CurrentTime"},
|
|
})
|
|
|
|
NLOHMANN_JSON_SERIALIZE_ENUM(LineStyle, {
|
|
{LineStyle::Solid, "Solid"},
|
|
})
|
|
|
|
NLOHMANN_JSON_SERIALIZE_ENUM(FillPattern, {
|
|
{FillPattern::Solid, "Solid"},
|
|
})
|
|
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ScrollFlags, endless, invertDirection, padBefore, padAfter)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LineFlags, dark)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(FillFlags, dark)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TextFlags, dark)
|
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TimeDisplayFlags, use12h, showHours, showMinutes, showSeconds)
|
|
|
|
ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
{
|
|
std::size_t const bufferSize = (m_width * m_height + 8 - 1) / 8;\
|
|
m_buffer.resize(bufferSize);
|
|
}
|
|
|
|
std::uint16_t ImageElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t ImageElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t ImageElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t ImageElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
std::span<std::byte> ImageElement::buffer() noexcept {
|
|
return m_buffer;
|
|
}
|
|
|
|
std::span<std::byte const> ImageElement::buffer() const noexcept {
|
|
return m_buffer;
|
|
}
|
|
|
|
MemoryOneBitBuffer ImageElement::image() noexcept {
|
|
return MemoryOneBitBuffer{m_buffer, m_width, m_height};
|
|
}
|
|
|
|
ConstMemoryOneBitBuffer ImageElement::image() const noexcept {
|
|
return ConstMemoryOneBitBuffer{m_buffer, m_width, m_height};
|
|
}
|
|
|
|
void ImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
|
|
std::size_t n = std::min(m_buffer.size(), newBufferData.size());
|
|
std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
|
|
std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
|
|
}
|
|
|
|
ImageElement::~ImageElement() {
|
|
}
|
|
|
|
ElementType ImageElement::elementType() const {
|
|
return ElementType::Image;
|
|
}
|
|
|
|
std::uint32_t ImageElement::minimumFormatVersion() const {
|
|
return 1;
|
|
}
|
|
|
|
std::size_t ImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Image));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
pos = writeU16LE(target, pos, 0);
|
|
pos = writeBuffer(target, pos, m_buffer);
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json ImageElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "Image";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
result["image"] = nlohmann::json::array();
|
|
|
|
auto ownImage = image();
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void ImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = animationTick;
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
auto ownImage = image();
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
imageBuffer->setPixel(m_x + dx, m_y + dy, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<ImageElement>, ParseError> ImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::Image)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
auto reserved_ = readU16LE(buffer);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t imageSize = ((*width) * (*height) + 8 - 1) / 8;
|
|
auto imageData = readBuffer(buffer, imageSize);
|
|
if (!imageData) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
|
|
if (imageSize < rounded) {
|
|
buffer = buffer.subspan(rounded - imageSize);
|
|
}
|
|
|
|
auto result = std::make_unique<ImageElement>(*x, *y, *width, *height);
|
|
result->updateBuffer(*imageData);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<ImageElement> ImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
std::vector<int> pixels = j.at("image");
|
|
if (pixels.size() != width * height) {
|
|
throw std::runtime_error(std::format("Error unserializing an Image element: the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, height, pixels.size(), width * height));
|
|
}
|
|
auto result = std::make_unique<ImageElement>(x, y, width, height);
|
|
auto image = result->image();
|
|
for (std::uint16_t dy = 0; dy < height; ++dy) {
|
|
std::size_t base = static_cast<std::size_t>(dy) * width;
|
|
for (std::uint16_t dx = 0; dx < width; ++dx) {
|
|
image.setPixel(dx, dy, !!pixels[base + dx]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
AnimationElement::AnimationElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t numberOfFrames, std::uint16_t updateInterval)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_numberOfFrames{numberOfFrames}
|
|
, m_updateInterval{updateInterval}
|
|
{
|
|
std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
|
|
m_buffer.resize(imageBufferSize * numberOfFrames);
|
|
}
|
|
|
|
std::uint16_t AnimationElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t AnimationElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t AnimationElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t AnimationElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
std::uint16_t AnimationElement::numberOfFrames() const noexcept {
|
|
return m_numberOfFrames;
|
|
}
|
|
|
|
std::uint16_t AnimationElement::updateInterval() const noexcept {
|
|
return m_updateInterval;
|
|
}
|
|
|
|
std::span<std::byte> AnimationElement::buffers() noexcept {
|
|
return m_buffer;
|
|
}
|
|
|
|
std::span<std::byte const> AnimationElement::buffers() const noexcept {
|
|
return m_buffer;
|
|
}
|
|
|
|
std::span<std::byte> AnimationElement::buffer(std::uint16_t frameIndex) noexcept {
|
|
if (frameIndex >= m_numberOfFrames) {
|
|
return {};
|
|
}
|
|
std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
|
|
return std::span{m_buffer}.subspan(frameIndex * imageBufferSize, imageBufferSize);
|
|
}
|
|
|
|
std::span<std::byte const> AnimationElement::buffer(std::uint16_t frameIndex) const noexcept {
|
|
if (frameIndex >= m_numberOfFrames) {
|
|
return {};
|
|
}
|
|
std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;
|
|
return std::span{m_buffer}.subspan(frameIndex * imageBufferSize, imageBufferSize);
|
|
}
|
|
|
|
MemoryOneBitBuffer AnimationElement::image(std::uint16_t frameIndex) noexcept {
|
|
return MemoryOneBitBuffer{buffer(frameIndex), m_width, m_height};
|
|
}
|
|
|
|
ConstMemoryOneBitBuffer AnimationElement::image(std::uint16_t frameIndex) const noexcept {
|
|
return ConstMemoryOneBitBuffer{buffer(frameIndex), m_width, m_height};
|
|
}
|
|
|
|
void AnimationElement::updateBuffer(std::span<std::byte const> newBufferData) {
|
|
std::size_t n = std::min(m_buffer.size(), newBufferData.size());
|
|
std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
|
|
std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
|
|
}
|
|
|
|
AnimationElement::~AnimationElement() {
|
|
}
|
|
|
|
ElementType AnimationElement::elementType() const {
|
|
return ElementType::Animation;
|
|
}
|
|
|
|
std::uint32_t AnimationElement::minimumFormatVersion() const {
|
|
return 1;
|
|
}
|
|
|
|
std::size_t AnimationElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Animation));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
pos = writeU16LE(target, pos, m_numberOfFrames);
|
|
pos = writeU16LE(target, pos, m_updateInterval);
|
|
pos = writeU16LE(target, pos, 0);
|
|
pos = writeBuffer(target, pos, m_buffer);
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json AnimationElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "Animation";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
result["updateInterval"] = m_updateInterval;
|
|
result["frames"] = nlohmann::json::array();
|
|
|
|
for (std::uint16_t f = 0; f < m_numberOfFrames; ++f) {
|
|
nlohmann::json frame = nlohmann::json::array();
|
|
auto ownImage = image(f);
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
frame.push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
|
|
}
|
|
}
|
|
result["frames"].push_back(frame);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void AnimationElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
|
|
if (m_numberOfFrames == 0) {
|
|
return;
|
|
}
|
|
|
|
std::size_t const tick = animationTick / (static_cast<std::size_t>(m_updateInterval) + 1);
|
|
std::size_t const frameIndex = tick % m_numberOfFrames;
|
|
|
|
auto ownImage = image(frameIndex);
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
imageBuffer->setPixel(m_x + dx, m_y + dy, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<AnimationElement>, ParseError> AnimationElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::Animation)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
auto numberOfFrames = readU16LE(buffer);
|
|
if (!numberOfFrames) {
|
|
return std::unexpected(numberOfFrames.error());
|
|
}
|
|
auto updateInterval = readU16LE(buffer);
|
|
if (!updateInterval) {
|
|
return std::unexpected(updateInterval.error());
|
|
}
|
|
auto reserved_ = readU16LE(buffer);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t imageSize = ((*width) * (*height) + 8 - 1) / 8;
|
|
auto imageData = readBuffer(buffer, imageSize);
|
|
if (!imageData) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
|
|
if (imageSize < rounded) {
|
|
buffer = buffer.subspan(rounded - imageSize);
|
|
}
|
|
|
|
auto result = std::make_unique<AnimationElement>(*x, *y, *width, *height, *numberOfFrames, *updateInterval);
|
|
result->updateBuffer(*imageData);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<AnimationElement> AnimationElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
std::uint16_t updateInterval = j.at("updateInterval");
|
|
std::vector<std::vector<int>> frames = j.at("frames");
|
|
|
|
if (frames.size() > std::numeric_limits<std::uint16_t>::max()) {
|
|
throw std::runtime_error(std::format("Error unserializing an Animation element: the number of frames, {:d}, is too large", frames.size()));
|
|
}
|
|
for (std::size_t i = 0; i < frames.size(); ++i) {
|
|
if (frames[i].size() != width * height) {
|
|
throw std::runtime_error(std::format("Error unserializing an Animation element: in frame {:d}, the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", i, width, height, frames[i].size(), width * height));
|
|
}
|
|
}
|
|
|
|
auto result = std::make_unique<AnimationElement>(x, y, width, height, static_cast<std::uint16_t>(frames.size()), updateInterval);
|
|
for (std::size_t i = 0; i < frames.size(); ++i) {
|
|
auto image = result->image(i);
|
|
for (std::uint16_t dy = 0; dy < height; ++dy) {
|
|
std::size_t base = static_cast<std::size_t>(dy) * width;
|
|
for (std::uint16_t dx = 0; dx < width; ++dx) {
|
|
image.setPixel(dx, dy, !!frames[i][base + dx]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
HScrollImageElement::HScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentWidth, ScrollFlags flags, std::uint8_t scrollSpeed)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_contentWidth{contentWidth}
|
|
, m_flags{flags}
|
|
, m_scrollSpeed{scrollSpeed}
|
|
{
|
|
std::size_t const imageBufferSize = (m_contentWidth * m_height + 8 - 1) / 8;
|
|
m_buffer.resize(imageBufferSize);
|
|
}
|
|
|
|
std::uint16_t HScrollImageElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t HScrollImageElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t HScrollImageElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t HScrollImageElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
std::uint16_t HScrollImageElement::contentWidth() const noexcept {
|
|
return m_contentWidth;
|
|
}
|
|
|
|
ScrollFlags HScrollImageElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
std::uint8_t HScrollImageElement::scrollSpeed() const noexcept {
|
|
return m_scrollSpeed;
|
|
}
|
|
|
|
std::span<std::byte> HScrollImageElement::buffer() noexcept {
|
|
return std::span{m_buffer};
|
|
}
|
|
|
|
std::span<std::byte const> HScrollImageElement::buffer() const noexcept {
|
|
return std::span{m_buffer};
|
|
}
|
|
|
|
MemoryOneBitBuffer HScrollImageElement::image() noexcept {
|
|
return MemoryOneBitBuffer{m_buffer, m_contentWidth, m_height};
|
|
}
|
|
|
|
ConstMemoryOneBitBuffer HScrollImageElement::image() const noexcept {
|
|
return ConstMemoryOneBitBuffer{m_buffer, m_contentWidth, m_height};
|
|
}
|
|
|
|
void HScrollImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
|
|
std::size_t n = std::min(m_buffer.size(), newBufferData.size());
|
|
std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
|
|
std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
|
|
}
|
|
|
|
HScrollImageElement::~HScrollImageElement() {
|
|
}
|
|
|
|
ElementType HScrollImageElement::elementType() const {
|
|
return ElementType::HScrollImage;
|
|
}
|
|
|
|
std::uint32_t HScrollImageElement::minimumFormatVersion() const {
|
|
return 1;
|
|
}
|
|
|
|
std::size_t HScrollImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::HScrollImage));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
pos = writeU16LE(target, pos, m_contentWidth);
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
|
|
pos = writeU8LE(target, pos, m_scrollSpeed);
|
|
pos = writeU16LE(target, pos, 0);
|
|
pos = writeBuffer(target, pos, m_buffer);
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json HScrollImageElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "HScrollImage";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
result["contentWidth"] = m_contentWidth;
|
|
result["flags"] = m_flags;
|
|
result["scrollSpeed"] = int(m_scrollSpeed);
|
|
result["image"] = nlohmann::json::array();
|
|
|
|
auto ownImage = image();
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_contentWidth; ++dx) {
|
|
result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
|
|
auto ownImage = image();
|
|
ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};
|
|
|
|
if (m_contentWidth == 0) {
|
|
return;
|
|
}
|
|
|
|
bool restarting = !m_flags.endless;
|
|
bool invert = m_flags.invertDirection;
|
|
bool padBefore = m_flags.padBefore;
|
|
bool padAfter = m_flags.padAfter;
|
|
|
|
if (!padBefore && !padAfter && m_contentWidth < m_width) {
|
|
std::int16_t offset = invert ? (m_width - m_contentWidth) : 0;
|
|
for (std::size_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::size_t dx = 0; dx < m_contentWidth; ++dx) {
|
|
target.setPixel(dx + offset, dy, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
std::int32_t totalSize = m_contentWidth;
|
|
if (padBefore) {
|
|
totalSize += m_width;
|
|
}
|
|
if (padAfter) {
|
|
totalSize += m_width;
|
|
}
|
|
|
|
// FIXME: handle overflow here...
|
|
std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;
|
|
if (invert) {
|
|
offset = totalSize - offset - 1;
|
|
}
|
|
if (restarting) {
|
|
offset = std::min(offset, totalSize - m_width);
|
|
}
|
|
|
|
std::int32_t partOffset = 0;
|
|
if (padBefore) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, false);
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_contentWidth; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
partOffset += m_contentWidth;
|
|
}
|
|
if (padAfter) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, false);
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
if (!restarting) {
|
|
if (padBefore) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, false);
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_contentWidth; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
partOffset += m_contentWidth;
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<HScrollImageElement>, ParseError> HScrollImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::HScrollImage)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
auto contentWidth = readU16LE(buffer);
|
|
if (!contentWidth) {
|
|
return std::unexpected(contentWidth.error());
|
|
}
|
|
auto flags = readU8LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto scrollSpeed = readU8LE(buffer);
|
|
if (!scrollSpeed) {
|
|
return std::unexpected(scrollSpeed.error());
|
|
}
|
|
auto reserved_ = readU16LE(buffer);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t imageSize = ((*contentWidth) * (*height) + 8 - 1) / 8;
|
|
auto imageData = readBuffer(buffer, imageSize);
|
|
if (!imageData) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
|
|
if (imageSize < rounded) {
|
|
buffer = buffer.subspan(rounded - imageSize);
|
|
}
|
|
|
|
auto result = std::make_unique<HScrollImageElement>(*x, *y, *width, *height, *contentWidth, ScrollFlags{*flags}, *scrollSpeed);
|
|
result->updateBuffer(*imageData);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<HScrollImageElement> HScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
std::uint16_t contentWidth = j.at("contentWidth");
|
|
ScrollFlags flags = j.at("flags");
|
|
int scrollSpeed = j.at("scrollSpeed");
|
|
std::vector<int> pixels = j.at("image");
|
|
if (scrollSpeed < 0 || scrollSpeed > 255) {
|
|
throw std::runtime_error(std::format("Error unserializing an HScrollImage element: invalid scroll speed {:d} specified", scrollSpeed));
|
|
}
|
|
if (pixels.size() != contentWidth * height) {
|
|
throw std::runtime_error(std::format("Error unserializing an HScrollImage element: the content width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", contentWidth, height, pixels.size(), contentWidth * height));
|
|
}
|
|
|
|
auto result = std::make_unique<HScrollImageElement>(x, y, width, height, contentWidth, flags, static_cast<std::uint8_t>(scrollSpeed));
|
|
auto image = result->image();
|
|
for (std::uint16_t dy = 0; dy < height; ++dy) {
|
|
std::size_t base = static_cast<std::size_t>(dy) * contentWidth;
|
|
for (std::uint16_t dx = 0; dx < contentWidth; ++dx) {
|
|
image.setPixel(dx, dy, !!pixels[base + dx]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
VScrollImageElement::VScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentHeight, ScrollFlags flags, std::uint8_t scrollSpeed)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_contentHeight{contentHeight}
|
|
, m_flags{flags}
|
|
, m_scrollSpeed{scrollSpeed}
|
|
{
|
|
std::size_t const imageBufferSize = (m_width * m_contentHeight + 8 - 1) / 8;
|
|
m_buffer.resize(imageBufferSize);
|
|
}
|
|
|
|
std::uint16_t VScrollImageElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t VScrollImageElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t VScrollImageElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t VScrollImageElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
std::uint16_t VScrollImageElement::contentHeight() const noexcept {
|
|
return m_contentHeight;
|
|
}
|
|
|
|
ScrollFlags VScrollImageElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
std::uint8_t VScrollImageElement::scrollSpeed() const noexcept {
|
|
return m_scrollSpeed;
|
|
}
|
|
|
|
std::span<std::byte> VScrollImageElement::buffer() noexcept {
|
|
return std::span{m_buffer};
|
|
}
|
|
|
|
std::span<std::byte const> VScrollImageElement::buffer() const noexcept {
|
|
return std::span{m_buffer};
|
|
}
|
|
|
|
MemoryOneBitBuffer VScrollImageElement::image() noexcept {
|
|
return MemoryOneBitBuffer{m_buffer, m_width, m_contentHeight};
|
|
}
|
|
|
|
ConstMemoryOneBitBuffer VScrollImageElement::image() const noexcept {
|
|
return ConstMemoryOneBitBuffer{m_buffer, m_width, m_contentHeight};
|
|
}
|
|
|
|
void VScrollImageElement::updateBuffer(std::span<std::byte const> newBufferData) {
|
|
std::size_t n = std::min(m_buffer.size(), newBufferData.size());
|
|
std::copy(newBufferData.begin(), newBufferData.begin() + n, m_buffer.begin());
|
|
std::fill(m_buffer.begin() + n, m_buffer.end(), std::byte{});
|
|
}
|
|
|
|
VScrollImageElement::~VScrollImageElement() {
|
|
}
|
|
|
|
ElementType VScrollImageElement::elementType() const {
|
|
return ElementType::VScrollImage;
|
|
}
|
|
|
|
std::uint32_t VScrollImageElement::minimumFormatVersion() const {
|
|
return 1;
|
|
}
|
|
|
|
std::size_t VScrollImageElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::VScrollImage));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
pos = writeU16LE(target, pos, m_contentHeight);
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
|
|
pos = writeU8LE(target, pos, m_scrollSpeed);
|
|
pos = writeU16LE(target, pos, 0);
|
|
pos = writeBuffer(target, pos, m_buffer);
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json VScrollImageElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "VScrollImage";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
result["contentHeight"] = m_contentHeight;
|
|
result["flags"] = m_flags;
|
|
result["scrollSpeed"] = int(m_scrollSpeed);
|
|
result["image"] = nlohmann::json::array();
|
|
|
|
auto ownImage = image();
|
|
for (std::uint16_t dy = 0; dy < m_contentHeight; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
|
|
auto ownImage = image();
|
|
ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};
|
|
|
|
if (m_contentHeight == 0) {
|
|
return;
|
|
}
|
|
|
|
bool restarting = !m_flags.endless;
|
|
bool invert = m_flags.invertDirection;
|
|
bool padBefore = m_flags.padBefore;
|
|
bool padAfter = m_flags.padAfter;
|
|
|
|
if (!padBefore && !padAfter && m_contentHeight < m_height) {
|
|
std::int16_t offset = invert ? (m_height - m_contentHeight) : 0;
|
|
for (std::size_t dy = 0; dy < m_contentHeight; ++dy) {
|
|
for (std::size_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, dy + offset, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
std::int32_t totalSize = m_contentHeight;
|
|
if (padBefore) {
|
|
totalSize += m_height;
|
|
}
|
|
if (padAfter) {
|
|
totalSize += m_height;
|
|
}
|
|
|
|
// FIXME: handle overflow here...
|
|
std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;
|
|
if (invert) {
|
|
offset = totalSize - offset - 1;
|
|
}
|
|
if (restarting) {
|
|
offset = std::min(offset, totalSize - m_height);
|
|
}
|
|
|
|
std::int32_t partOffset = 0;
|
|
if (padBefore) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
std::int32_t ddy = dy + partOffset;
|
|
if ((ddy - offset) < 0) {
|
|
continue;
|
|
}
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, ddy - offset, false);
|
|
}
|
|
}
|
|
partOffset += m_height;
|
|
}
|
|
for (std::int32_t dy = 0; dy < m_contentHeight; ++dy) {
|
|
std::int32_t ddy = dy + partOffset;
|
|
if ((ddy - offset) < 0) {
|
|
continue;
|
|
}
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
partOffset += m_contentHeight;
|
|
}
|
|
if (padAfter) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
std::int32_t ddy = dy + partOffset;
|
|
if ((ddy - offset) < 0) {
|
|
continue;
|
|
}
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, ddy - offset, false);
|
|
}
|
|
}
|
|
partOffset += m_height;
|
|
}
|
|
if (!restarting) {
|
|
if (padBefore) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
std::int32_t ddy = dy + partOffset;
|
|
if ((ddy - offset) < 0) {
|
|
continue;
|
|
}
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, ddy - offset, false);
|
|
}
|
|
}
|
|
partOffset += m_height;
|
|
}
|
|
for (std::int32_t dy = 0; dy < m_contentHeight; ++dy) {
|
|
std::int32_t ddy = dy + partOffset;
|
|
if ((ddy - offset) < 0) {
|
|
continue;
|
|
}
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy));
|
|
}
|
|
}
|
|
partOffset += m_contentHeight;
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<VScrollImageElement>, ParseError> VScrollImageElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::VScrollImage)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
auto contentHeight = readU16LE(buffer);
|
|
if (!contentHeight) {
|
|
return std::unexpected(contentHeight.error());
|
|
}
|
|
auto flags = readU8LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto scrollSpeed = readU8LE(buffer);
|
|
if (!scrollSpeed) {
|
|
return std::unexpected(scrollSpeed.error());
|
|
}
|
|
auto reserved_ = readU16LE(buffer);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t imageSize = ((*width) * (*contentHeight) + 8 - 1) / 8;
|
|
auto imageData = readBuffer(buffer, imageSize);
|
|
if (!imageData) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
std::size_t rounded = (imageSize + 4 - 1) / 4 * 4;
|
|
if (imageSize < rounded) {
|
|
buffer = buffer.subspan(rounded - imageSize);
|
|
}
|
|
|
|
auto result = std::make_unique<VScrollImageElement>(*x, *y, *width, *height, *contentHeight, ScrollFlags{*flags}, *scrollSpeed);
|
|
result->updateBuffer(*imageData);
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<VScrollImageElement> VScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
std::uint16_t contentHeight = j.at("contentHeight");
|
|
ScrollFlags flags = j.at("flags");
|
|
int scrollSpeed = j.at("scrollSpeed");
|
|
std::vector<int> pixels = j.at("image");
|
|
if (scrollSpeed < 0 || scrollSpeed > 255) {
|
|
throw std::runtime_error(std::format("Error unserializing an VScrollImage element: invalid scroll speed {:d} specified", scrollSpeed));
|
|
}
|
|
if (pixels.size() != width * contentHeight) {
|
|
throw std::runtime_error(std::format("Error unserializing an VScrollImage element: the width, {:d}, and content height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, contentHeight, pixels.size(), width * contentHeight));
|
|
}
|
|
|
|
auto result = std::make_unique<VScrollImageElement>(x, y, width, height, contentHeight, flags, static_cast<std::uint8_t>(scrollSpeed));
|
|
auto image = result->image();
|
|
for (std::uint16_t dy = 0; dy < contentHeight; ++dy) {
|
|
std::size_t base = static_cast<std::size_t>(dy) * width;
|
|
for (std::uint16_t dx = 0; dx < width; ++dx) {
|
|
image.setPixel(dx, dy, !!pixels[base + dx]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
LineElement::LineElement(std::uint16_t originX, std::uint16_t originY, std::uint16_t targetX, std::uint16_t targetY, LineStyle lineStyle, LineFlags flags)
|
|
: m_originX{originX}
|
|
, m_originY{originY}
|
|
, m_targetX{targetX}
|
|
, m_targetY{targetY}
|
|
, m_lineStyle{lineStyle}
|
|
, m_flags{flags}
|
|
{
|
|
}
|
|
|
|
std::uint16_t LineElement::originX() const noexcept {
|
|
return m_originX;
|
|
}
|
|
|
|
std::uint16_t LineElement::originY() const noexcept {
|
|
return m_originY;
|
|
}
|
|
|
|
std::uint16_t LineElement::targetX() const noexcept {
|
|
return m_targetX;
|
|
}
|
|
|
|
std::uint16_t LineElement::targetY() const noexcept {
|
|
return m_targetY;
|
|
}
|
|
|
|
LineStyle LineElement::lineStyle() const noexcept {
|
|
return m_lineStyle;
|
|
}
|
|
|
|
LineFlags LineElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
LineElement::~LineElement() {
|
|
}
|
|
|
|
ElementType LineElement::elementType() const {
|
|
return ElementType::Line;
|
|
}
|
|
|
|
std::uint32_t LineElement::minimumFormatVersion() const {
|
|
if (static_cast<std::uint8_t>(m_flags) != 0) {
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
std::size_t LineElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Line));
|
|
pos = writeU16LE(target, pos, m_originX);
|
|
pos = writeU16LE(target, pos, m_originY);
|
|
pos = writeU16LE(target, pos, m_targetX);
|
|
pos = writeU16LE(target, pos, m_targetY);
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_lineStyle));
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json LineElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "Line";
|
|
result["originX"] = m_originX;
|
|
result["originY"] = m_originY;
|
|
result["targetX"] = m_targetX;
|
|
result["targetY"] = m_targetY;
|
|
result["lineStyle"] = m_lineStyle;
|
|
result["flags"] = m_flags;
|
|
return result;
|
|
}
|
|
|
|
void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = animationTick;
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
|
|
std::int32_t dx = static_cast<std::int32_t>(m_targetX) - static_cast<std::int32_t>(m_originX);
|
|
std::int32_t dy = static_cast<std::int32_t>(m_targetY) - static_cast<std::int32_t>(m_originY);
|
|
bool value = true;
|
|
|
|
if (dx == 0) {
|
|
for (std::int32_t i = 0; i <= dy; ++i) {
|
|
imageBuffer->setPixel(m_originX, m_originY + i, value);
|
|
}
|
|
} else if (dy == 0) {
|
|
for (std::int32_t i = 0; i <= dx; ++i) {
|
|
imageBuffer->setPixel(m_originX + i, m_originY, value);
|
|
}
|
|
} else {
|
|
for (std::int32_t i = 0; i <= dx; ++i) {
|
|
std::int32_t x = m_originX + i;
|
|
std::int32_t y = i * dy / dx + m_originY;
|
|
imageBuffer->setPixel(static_cast<std::uint16_t>(x), static_cast<std::uint16_t>(y), value);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<LineElement>, ParseError> LineElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::Line)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto originX = readU16LE(buffer);
|
|
if (!originX) {
|
|
return std::unexpected(originX.error());
|
|
}
|
|
auto originY = readU16LE(buffer);
|
|
if (!originY) {
|
|
return std::unexpected(originY.error());
|
|
}
|
|
auto targetX = readU16LE(buffer);
|
|
if (!targetX) {
|
|
return std::unexpected(targetX.error());
|
|
}
|
|
auto targetY = readU16LE(buffer);
|
|
if (!targetY) {
|
|
return std::unexpected(targetY.error());
|
|
}
|
|
auto lineStyle = readU8LE(buffer);
|
|
if (!lineStyle) {
|
|
return std::unexpected(lineStyle.error());
|
|
}
|
|
if (*lineStyle != static_cast<std::uint8_t>(LineStyle::Solid)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto flags = readU8LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto result = std::make_unique<LineElement>(*originX, *originY, *targetX, *targetY, static_cast<LineStyle>(*lineStyle), LineFlags{*flags});
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<LineElement> LineElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t originX = j.at("originX");
|
|
std::uint16_t originY = j.at("originY");
|
|
std::uint16_t targetX = j.at("targetX");
|
|
std::uint16_t targetY = j.at("targetY");
|
|
LineStyle lineStyle = j.at("lineStyle");
|
|
LineFlags flags = j.at("flags");
|
|
|
|
auto result = std::make_unique<LineElement>(originX, originY, targetX, targetY, lineStyle, flags);
|
|
return result;
|
|
}
|
|
|
|
BoxElement::BoxElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, FillPattern fillPattern, FillFlags flags)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_fillPattern{fillPattern}
|
|
, m_flags{flags}
|
|
{
|
|
}
|
|
|
|
std::uint16_t BoxElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t BoxElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t BoxElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t BoxElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
FillPattern BoxElement::fillPattern() const noexcept {
|
|
return m_fillPattern;
|
|
}
|
|
|
|
FillFlags BoxElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
BoxElement::~BoxElement() {
|
|
}
|
|
|
|
ElementType BoxElement::elementType() const {
|
|
return ElementType::Box;
|
|
}
|
|
|
|
std::uint32_t BoxElement::minimumFormatVersion() const {
|
|
return 2;
|
|
}
|
|
|
|
std::size_t BoxElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::Box));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_fillPattern));
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json BoxElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "Box";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
result["fillPattern"] = m_fillPattern;
|
|
result["flags"] = m_flags;
|
|
return result;
|
|
}
|
|
|
|
void BoxElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = animationTick;
|
|
std::ignore = currentTimestamp;
|
|
std::ignore = customFonts;
|
|
|
|
bool color = !m_flags.dark;
|
|
for (std::uint16_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::uint16_t dx = 0; dx < m_width; ++dx) {
|
|
imageBuffer->setPixel(m_x + dx, m_y + dy, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<BoxElement>, ParseError> BoxElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::Image)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
auto fillPattern = readU8LE(buffer);
|
|
if (!fillPattern) {
|
|
return std::unexpected(fillPattern.error());
|
|
}
|
|
auto flags = readU8LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
|
|
auto result = std::make_unique<BoxElement>(*x, *y, *width, *height, static_cast<FillPattern>(*fillPattern), FillFlags{*flags});
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<BoxElement> BoxElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
FillPattern fillPattern = j.at("fillPattern");
|
|
FillFlags flags = j.at("flags");
|
|
auto result = std::make_unique<BoxElement>(x, y, width, height, fillPattern, flags);
|
|
return result;
|
|
}
|
|
|
|
ClippedTextElement::ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::string text)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_textFlags{textFlags}
|
|
, m_fontIndex{fontIndex}
|
|
, m_text{std::move(text)}
|
|
{
|
|
}
|
|
|
|
std::uint16_t ClippedTextElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t ClippedTextElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t ClippedTextElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t ClippedTextElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
TextFlags ClippedTextElement::textFlags() const noexcept {
|
|
return m_textFlags;
|
|
}
|
|
|
|
std::uint16_t ClippedTextElement::fontIndex() const noexcept {
|
|
return m_fontIndex;
|
|
}
|
|
|
|
std::string ClippedTextElement::text() const {
|
|
return m_text;
|
|
}
|
|
|
|
ClippedTextElement::~ClippedTextElement() {
|
|
}
|
|
|
|
ElementType ClippedTextElement::elementType() const {
|
|
return ElementType::ClippedText;
|
|
}
|
|
|
|
std::uint32_t ClippedTextElement::minimumFormatVersion() const {
|
|
if (static_cast<std::uint8_t>(m_textFlags) != 0) {
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
std::size_t ClippedTextElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::ClippedText));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
if (formatVersion >= 2) {
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
|
|
pos = writeU8LE(target, pos, 0);
|
|
}
|
|
pos = writeU16LE(target, pos, m_fontIndex);
|
|
pos = writeU16LE(target, pos, m_text.size());
|
|
pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text}));
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json ClippedTextElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
nlohmann::json result;
|
|
result["type"] = "ClippedText";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
if (formatVersion >= 2) {
|
|
result["textFlags"] = m_textFlags;
|
|
}
|
|
result["fontIndex"] = m_fontIndex;
|
|
result["text"] = m_text;
|
|
return result;
|
|
}
|
|
|
|
void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = animationTick;
|
|
std::ignore = currentTimestamp;
|
|
|
|
std::span<std::byte const> fontData;
|
|
if (m_fontIndex & 0x8000u) {
|
|
std::size_t fontIndex = m_fontIndex & ~0x8000u;
|
|
if (fontIndex >= customFonts.size()) {
|
|
return;
|
|
}
|
|
fontData = customFonts[fontIndex]->fontData();
|
|
} else {
|
|
fontData = findEmbeddedFont(m_fontIndex);
|
|
}
|
|
if (fontData.size() < 23) {
|
|
return;
|
|
}
|
|
|
|
ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};
|
|
|
|
std::optional<bool> bgColor;
|
|
bgColor = m_textFlags.dark;
|
|
bool fgColor = !m_textFlags.dark;
|
|
|
|
auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);
|
|
renderer.render(m_text, Point{0, static_cast<std::int32_t>(renderer.lineHeight() - 1)}, target, fgColor, bgColor);
|
|
}
|
|
|
|
std::expected<std::unique_ptr<ClippedTextElement>, ParseError> ClippedTextElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::ClippedText)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
std::expected<std::uint8_t, ParseError> textFlags = 0;
|
|
if (formatVersion >= 2) {
|
|
textFlags = readU8LE(buffer);
|
|
if (!textFlags) {
|
|
return std::unexpected(textFlags.error());
|
|
}
|
|
auto reserved1 = readU8LE(buffer);
|
|
if (!reserved1) {
|
|
return std::unexpected(reserved1.error());
|
|
}
|
|
}
|
|
auto fontIndex = readU16LE(buffer);
|
|
if (!fontIndex) {
|
|
return std::unexpected(fontIndex.error());
|
|
}
|
|
auto textLength = readU16LE(buffer);
|
|
if (!textLength) {
|
|
return std::unexpected(textLength.error());
|
|
}
|
|
std::size_t alignedRoundedUp = (*textLength + 2 + 4 - 1) / 4 * 4 - 2;
|
|
auto textData = readBuffer(buffer, alignedRoundedUp);
|
|
if (!textData) {
|
|
return std::unexpected(textData.error());
|
|
}
|
|
std::string_view text{reinterpret_cast<char const*>(textData->data()), *textLength};
|
|
auto result = std::make_unique<ClippedTextElement>(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, std::string{text});
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<ClippedTextElement> ClippedTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
TextFlags textFlags;
|
|
if (formatVersion >= 2) {
|
|
textFlags = j.at("textFlags");
|
|
}
|
|
std::uint16_t fontIndex = j.at("fontIndex");
|
|
std::string text = j.at("text");
|
|
|
|
auto result = std::make_unique<ClippedTextElement>(x, y, width, height, textFlags, fontIndex, std::move(text));
|
|
return result;
|
|
}
|
|
|
|
HScrollTextElement::HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_textFlags{textFlags}
|
|
, m_flags{flags}
|
|
, m_scrollSpeed{scrollSpeed}
|
|
, m_fontIndex{fontIndex}
|
|
, m_text{std::move(text)}
|
|
{
|
|
}
|
|
|
|
std::uint16_t HScrollTextElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t HScrollTextElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t HScrollTextElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t HScrollTextElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
TextFlags HScrollTextElement::textFlags() const noexcept {
|
|
return m_textFlags;
|
|
}
|
|
|
|
ScrollFlags HScrollTextElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
std::uint8_t HScrollTextElement::scrollSpeed() const noexcept {
|
|
return m_scrollSpeed;
|
|
}
|
|
|
|
std::uint16_t HScrollTextElement::fontIndex() const noexcept {
|
|
return m_fontIndex;
|
|
}
|
|
|
|
std::string HScrollTextElement::text() const {
|
|
return m_text;
|
|
}
|
|
|
|
HScrollTextElement::~HScrollTextElement() {
|
|
}
|
|
|
|
ElementType HScrollTextElement::elementType() const {
|
|
return ElementType::HScrollText;
|
|
}
|
|
|
|
std::uint32_t HScrollTextElement::minimumFormatVersion() const {
|
|
if (static_cast<std::uint8_t>(m_textFlags) != 0) {
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
std::size_t HScrollTextElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::HScrollText));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
if (formatVersion >= 2) {
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
|
|
pos = writeU8LE(target, pos, 0);
|
|
}
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_flags));
|
|
pos = writeU8LE(target, pos, m_scrollSpeed);
|
|
if (formatVersion >= 2) {
|
|
pos = writeU16LE(target, pos, 0);
|
|
}
|
|
pos = writeU16LE(target, pos, m_fontIndex);
|
|
pos = writeU16LE(target, pos, m_text.size());
|
|
pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text}));
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json HScrollTextElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "HScrollText";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
if (formatVersion >= 2) {
|
|
result["textFlags"] = m_textFlags;
|
|
}
|
|
result["flags"] = m_flags;
|
|
result["scrollSpeed"] = int(m_scrollSpeed);
|
|
result["fontIndex"] = m_fontIndex;
|
|
result["text"] = m_text;
|
|
return result;
|
|
}
|
|
|
|
void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = currentTimestamp;
|
|
|
|
std::span<std::byte const> fontData;
|
|
if (m_fontIndex & 0x8000u) {
|
|
std::size_t fontIndex = m_fontIndex & ~0x8000u;
|
|
if (fontIndex >= customFonts.size()) {
|
|
return;
|
|
}
|
|
fontData = customFonts[fontIndex]->fontData();
|
|
} else {
|
|
fontData = findEmbeddedFont(m_fontIndex);
|
|
}
|
|
if (fontData.size() < 23) {
|
|
return;
|
|
}
|
|
|
|
auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);
|
|
auto dimensions = renderer.getRenderedDimensions(m_text, {0, static_cast<std::int32_t>(renderer.lineHeight() - 1)});
|
|
if (!dimensions || !dimensions->boundingBox) {
|
|
return;
|
|
}
|
|
|
|
std::uint16_t contentWidth = dimensions->boundingBox->size.width;
|
|
|
|
ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};
|
|
|
|
if (contentWidth == 0) {
|
|
return;
|
|
}
|
|
|
|
std::optional<bool> bgColor;
|
|
bgColor = m_textFlags.dark;
|
|
bool fgColor = !m_textFlags.dark;
|
|
|
|
bool restarting = !m_flags.endless;
|
|
bool invert = m_flags.invertDirection;
|
|
bool padBefore = m_flags.padBefore;
|
|
bool padAfter = m_flags.padAfter;
|
|
|
|
if (!padBefore && !padAfter && contentWidth < m_width) {
|
|
std::uint16_t offset = invert ? (m_width - contentWidth) : 0;
|
|
renderer.render(m_text, Point{offset, static_cast<std::int32_t>(renderer.lineHeight() - 1)}, target, fgColor, bgColor);
|
|
return;
|
|
}
|
|
|
|
std::int32_t totalSize = contentWidth;
|
|
if (padBefore) {
|
|
totalSize += m_width;
|
|
}
|
|
if (padAfter) {
|
|
totalSize += m_width;
|
|
}
|
|
|
|
// FIXME: handle overflow here...
|
|
std::int32_t offset = static_cast<std::int32_t>(animationTick * (m_scrollSpeed + 1) / 8) % totalSize;
|
|
|
|
if (invert) {
|
|
offset = totalSize - offset - 1;
|
|
}
|
|
if (restarting) {
|
|
offset = std::min(offset, totalSize - m_width);
|
|
}
|
|
|
|
std::int32_t partOffset = 0;
|
|
if (padBefore) {
|
|
if (bgColor) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, *bgColor);
|
|
}
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
renderer.render(m_text, Point{partOffset - offset, static_cast<std::int32_t>(renderer.lineHeight() - 1)}, target, fgColor, bgColor);
|
|
partOffset += contentWidth;
|
|
if (padAfter) {
|
|
if (bgColor) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, *bgColor);
|
|
}
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
if (!restarting) {
|
|
if (padBefore) {
|
|
if (bgColor) {
|
|
for (std::int32_t dy = 0; dy < m_height; ++dy) {
|
|
for (std::int32_t dx = 0; dx < m_width; ++dx) {
|
|
std::int32_t ddx = dx + partOffset;
|
|
if ((ddx - offset) < 0) {
|
|
continue;
|
|
}
|
|
target.setPixel(ddx - offset, dy, *bgColor);
|
|
}
|
|
}
|
|
}
|
|
partOffset += m_width;
|
|
}
|
|
renderer.render(m_text, Point{partOffset - offset, static_cast<std::int32_t>(renderer.lineHeight() - 1)}, target, fgColor, bgColor);
|
|
}
|
|
}
|
|
|
|
std::expected<std::unique_ptr<HScrollTextElement>, ParseError> HScrollTextElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::HScrollText)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
std::expected<std::uint8_t, ParseError> textFlags = 0;
|
|
if (formatVersion >= 2) {
|
|
textFlags = readU8LE(buffer);
|
|
if (!textFlags) {
|
|
return std::unexpected(textFlags.error());
|
|
}
|
|
auto reserved1 = readU8LE(buffer);
|
|
if (!reserved1) {
|
|
return std::unexpected(reserved1.error());
|
|
}
|
|
}
|
|
auto flags = readU8LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto scrollSpeed = readU8LE(buffer);
|
|
if (!scrollSpeed) {
|
|
return std::unexpected(scrollSpeed.error());
|
|
}
|
|
if (formatVersion >= 2) {
|
|
auto reserved2 = readU16LE(buffer);
|
|
if (!reserved2) {
|
|
return std::unexpected(reserved2.error());
|
|
}
|
|
}
|
|
auto fontIndex = readU16LE(buffer);
|
|
if (!fontIndex) {
|
|
return std::unexpected(fontIndex.error());
|
|
}
|
|
auto textLength = readU16LE(buffer);
|
|
if (!textLength) {
|
|
return std::unexpected(textLength.error());
|
|
}
|
|
std::size_t alignedRoundedUp = (*textLength + 4 - 1) / 4 * 4;
|
|
auto textData = readBuffer(buffer, alignedRoundedUp);
|
|
if (!textData) {
|
|
return std::unexpected(textData.error());
|
|
}
|
|
std::string_view text{reinterpret_cast<char const*>(textData->data()), *textLength};
|
|
auto result = std::make_unique<HScrollTextElement>(*x, *y, *width, *height, TextFlags{*textFlags}, ScrollFlags{*flags}, *scrollSpeed, *fontIndex, std::string{text});
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<HScrollTextElement> HScrollTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
TextFlags textFlags;
|
|
if (formatVersion >= 2) {
|
|
textFlags = j.at("textFlags");
|
|
}
|
|
ScrollFlags flags = j.at("flags");
|
|
int scrollSpeed = j.at("scrollSpeed");
|
|
std::uint16_t fontIndex = j.at("fontIndex");
|
|
std::string text = j.at("text");
|
|
|
|
if (scrollSpeed < 0 || scrollSpeed > 255) {
|
|
throw std::runtime_error(std::format("Error unserializing an HScrollText element: invalid scroll speed {:d} specified", scrollSpeed));
|
|
}
|
|
|
|
auto result = std::make_unique<HScrollTextElement>(x, y, width, height, textFlags, flags, static_cast<std::uint8_t>(scrollSpeed), fontIndex, std::move(text));
|
|
return result;
|
|
|
|
}
|
|
|
|
CurrentTimeElement::CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::uint16_t utcOffset, TimeDisplayFlags flags)
|
|
: m_x{x}
|
|
, m_y{y}
|
|
, m_width{width}
|
|
, m_height{height}
|
|
, m_textFlags{textFlags}
|
|
, m_fontIndex{fontIndex}
|
|
, m_utcOffset{utcOffset}
|
|
, m_flags{flags}
|
|
{
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::x() const noexcept {
|
|
return m_x;
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::y() const noexcept {
|
|
return m_y;
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::width() const noexcept {
|
|
return m_width;
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::height() const noexcept {
|
|
return m_height;
|
|
}
|
|
|
|
TextFlags CurrentTimeElement::textFlags() const noexcept {
|
|
return m_textFlags;
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::fontIndex() const noexcept {
|
|
return m_fontIndex;
|
|
}
|
|
|
|
std::uint16_t CurrentTimeElement::utcOffset() const noexcept {
|
|
return m_utcOffset;
|
|
}
|
|
|
|
TimeDisplayFlags CurrentTimeElement::flags() const noexcept {
|
|
return m_flags;
|
|
}
|
|
|
|
CurrentTimeElement::~CurrentTimeElement() {
|
|
}
|
|
|
|
ElementType CurrentTimeElement::elementType() const {
|
|
return ElementType::CurrentTime;
|
|
}
|
|
|
|
std::uint32_t CurrentTimeElement::minimumFormatVersion() const {
|
|
if (static_cast<std::uint8_t>(m_textFlags) != 0) {
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
std::size_t CurrentTimeElement::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(ElementType::CurrentTime));
|
|
pos = writeU16LE(target, pos, m_x);
|
|
pos = writeU16LE(target, pos, m_y);
|
|
pos = writeU16LE(target, pos, m_width);
|
|
pos = writeU16LE(target, pos, m_height);
|
|
if (formatVersion >= 2) {
|
|
pos = writeU8LE(target, pos, static_cast<std::uint8_t>(m_textFlags));
|
|
pos = writeU8LE(target, pos, 0);
|
|
}
|
|
pos = writeU16LE(target, pos, m_fontIndex);
|
|
pos = writeU16LE(target, pos, m_utcOffset);
|
|
pos = writeU16LE(target, pos, static_cast<std::uint16_t>(m_flags));
|
|
if (formatVersion >= 2) {
|
|
pos = writeU16LE(target, pos, 0);
|
|
}
|
|
return alignNextWrite(target, pos, 4);
|
|
}
|
|
|
|
nlohmann::json CurrentTimeElement::serializeJSON(std::uint32_t formatVersion) const {
|
|
nlohmann::json result;
|
|
result["type"] = "CurrentTime";
|
|
result["x"] = m_x;
|
|
result["y"] = m_y;
|
|
result["width"] = m_width;
|
|
result["height"] = m_height;
|
|
if (formatVersion >= 2) {
|
|
result["textFlags"] = m_textFlags;
|
|
}
|
|
result["fontIndex"] = m_fontIndex;
|
|
result["utcOffset"] = m_utcOffset;
|
|
result["flags"] = m_flags;
|
|
return result;
|
|
}
|
|
|
|
void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span<CustomFontSection const*> customFonts) {
|
|
std::ignore = animationTick;
|
|
|
|
std::span<std::byte const> fontData;
|
|
if (m_fontIndex & 0x8000u) {
|
|
std::size_t fontIndex = m_fontIndex & ~0x8000u;
|
|
if (fontIndex >= customFonts.size()) {
|
|
return;
|
|
}
|
|
fontData = customFonts[fontIndex]->fontData();
|
|
} else {
|
|
fontData = findEmbeddedFont(m_fontIndex);
|
|
}
|
|
if (fontData.size() < 23) {
|
|
return;
|
|
}
|
|
|
|
bool use12h = m_flags.use12h;
|
|
bool showHours = m_flags.showHours;
|
|
bool showMinutes = m_flags.showMinutes;
|
|
bool showSeconds = m_flags.showSeconds;
|
|
if (showHours && showSeconds) {
|
|
showMinutes = true;
|
|
}
|
|
|
|
std::optional<bool> bgColor;
|
|
bgColor = m_textFlags.dark;
|
|
bool fgColor = !m_textFlags.dark;
|
|
|
|
ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height};
|
|
|
|
auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true);
|
|
|
|
std::uint32_t seconds = (currentTimestamp + static_cast<std::int64_t>(m_utcOffset) * 60) % 86400;
|
|
std::uint32_t hours = seconds / 3600;
|
|
std::uint32_t minutes = (seconds - hours * 3600) / 60;
|
|
seconds = seconds % 60;
|
|
if (use12h) {
|
|
hours = hours % 12;
|
|
if (hours == 0) {
|
|
hours = 12;
|
|
}
|
|
}
|
|
std::string text;
|
|
if (showHours && showMinutes && showSeconds) {
|
|
text = std::format("{:02d}:{:02d}:{:02d}", hours, minutes, seconds);
|
|
} else if (showHours && showMinutes) {
|
|
text = std::format("{:02d}:{:02d}", hours, minutes);
|
|
} else if (showHours) {
|
|
text = std::format("{:02d}", hours);
|
|
} else if (showMinutes && showSeconds) {
|
|
text = std::format("{:02d}:{:02d}", minutes, seconds);
|
|
} else if (showSeconds) {
|
|
text = std::format("{:02d}", seconds);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
renderer.render(text, Point{0, static_cast<std::int32_t>(renderer.lineHeight() - 1)}, target, fgColor, bgColor);
|
|
}
|
|
|
|
std::expected<std::unique_ptr<CurrentTimeElement>, ParseError> CurrentTimeElement::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
auto type = readU16LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint16_t>(ElementType::CurrentTime)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto x = readU16LE(buffer);
|
|
if (!x) {
|
|
return std::unexpected(x.error());
|
|
}
|
|
auto y = readU16LE(buffer);
|
|
if (!y) {
|
|
return std::unexpected(y.error());
|
|
}
|
|
auto width = readU16LE(buffer);
|
|
if (!width) {
|
|
return std::unexpected(width.error());
|
|
}
|
|
auto height = readU16LE(buffer);
|
|
if (!height) {
|
|
return std::unexpected(height.error());
|
|
}
|
|
std::expected<std::uint8_t, ParseError> textFlags = 0;
|
|
if (formatVersion >= 2) {
|
|
textFlags = readU8LE(buffer);
|
|
if (!textFlags) {
|
|
return std::unexpected(textFlags.error());
|
|
}
|
|
auto reserved1 = readU8LE(buffer);
|
|
if (!reserved1) {
|
|
return std::unexpected(reserved1.error());
|
|
}
|
|
}
|
|
auto fontIndex = readU16LE(buffer);
|
|
if (!fontIndex) {
|
|
return std::unexpected(fontIndex.error());
|
|
}
|
|
auto utcOffset = readU16LE(buffer);
|
|
if (!utcOffset) {
|
|
return std::unexpected(utcOffset.error());
|
|
}
|
|
auto flags = readU16LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
if (formatVersion >= 2) {
|
|
auto reserved2 = readU16LE(buffer);
|
|
if (!reserved2) {
|
|
return std::unexpected(reserved2.error());
|
|
}
|
|
}
|
|
auto result = std::make_unique<CurrentTimeElement>(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, *utcOffset, TimeDisplayFlags{*flags});
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<CurrentTimeElement> CurrentTimeElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::uint16_t x = j.at("x");
|
|
std::uint16_t y = j.at("y");
|
|
std::uint16_t width = j.at("width");
|
|
std::uint16_t height = j.at("height");
|
|
TextFlags textFlags;
|
|
if (formatVersion >= 2) {
|
|
textFlags = j.at("textFlags");
|
|
}
|
|
std::uint16_t fontIndex = j.at("fontIndex");
|
|
std::uint16_t utcOffset = j.at("utcOffset");
|
|
TimeDisplayFlags flags = j.at("flags");
|
|
|
|
auto result = std::make_unique<CurrentTimeElement>(x, y, width, height, textFlags, fontIndex, utcOffset, flags);
|
|
return result;
|
|
}
|
|
|
|
AlwaysDrawnSection::~AlwaysDrawnSection() {
|
|
}
|
|
|
|
bool AlwaysDrawnSection::doesDrawOnFront() const noexcept {
|
|
return m_drawOnFront;
|
|
}
|
|
|
|
bool AlwaysDrawnSection::doesDrawOnBack() const noexcept {
|
|
return m_drawOnBack;
|
|
}
|
|
|
|
bool AlwaysDrawnSection::doesClearBeforeDrawing() const noexcept {
|
|
return m_clearBeforeDrawing;
|
|
}
|
|
|
|
void AlwaysDrawnSection::setDrawOnFront(bool value) noexcept {
|
|
m_drawOnFront = value;
|
|
}
|
|
|
|
void AlwaysDrawnSection::setDrawOnBack(bool value) noexcept {
|
|
m_drawOnBack = value;
|
|
}
|
|
|
|
void AlwaysDrawnSection::setClearBeforeDrawing(bool value) noexcept {
|
|
m_clearBeforeDrawing = value;
|
|
}
|
|
|
|
std::size_t AlwaysDrawnSection::elementCount() const noexcept {
|
|
return m_elements.size();
|
|
}
|
|
|
|
Element* AlwaysDrawnSection::elementAt(std::size_t index) const {
|
|
if (index < m_elements.size()) {
|
|
return m_elements[index].get();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
void AlwaysDrawnSection::appendElement(std::unique_ptr<Element> element) {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
|
|
void AlwaysDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
if (index < m_elements.size()) {
|
|
m_elements.insert(m_elements.begin() + index, std::move(element));
|
|
} else {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Element> AlwaysDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements[index] = std::move(element);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<Element> AlwaysDrawnSection::eraseElement(std::size_t index) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements.erase(m_elements.begin() + index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
SectionType AlwaysDrawnSection::sectionType() const {
|
|
return SectionType::AlwaysDrawn;
|
|
}
|
|
|
|
std::uint32_t AlwaysDrawnSection::minimumFormatVersion() const {
|
|
std::uint32_t result = 1;
|
|
for (auto const& element : m_elements) {
|
|
result = std::max(result, element->minimumFormatVersion());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::size_t AlwaysDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::AlwaysDrawn));
|
|
// Will be replaced later
|
|
pos = writeU24LE(target, pos, 0);
|
|
std::uint16_t flags = 0;
|
|
maybeSetBit(flags, 0, m_drawOnFront);
|
|
maybeSetBit(flags, 1, m_drawOnBack);
|
|
maybeSetBit(flags, 2, m_clearBeforeDrawing);
|
|
pos = writeU16LE(target, pos, flags);
|
|
pos = writeU16LE(target, pos, m_elements.size());
|
|
for (auto const& element : m_elements) {
|
|
if (pos < target.size()) {
|
|
pos += element->serializeTo(target.subspan(pos), formatVersion);
|
|
} else {
|
|
pos += element->serializeTo({}, formatVersion);
|
|
}
|
|
}
|
|
pos = alignNextWrite(target, pos, 4);
|
|
std::ignore = writeU24LE(target, 1, pos);
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json AlwaysDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "AlwaysDrawn";
|
|
result["flags"]["drawOnFront"] = m_drawOnFront;
|
|
result["flags"]["drawOnBack"] = m_drawOnBack;
|
|
result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
|
|
result["elements"] = nlohmann::json::array();
|
|
for (auto const& element : m_elements) {
|
|
result["elements"].push_back(element->serializeJSON(formatVersion));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::expected<std::unique_ptr<AlwaysDrawnSection>, ParseError> AlwaysDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
auto type = readU8LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint8_t>(SectionType::AlwaysDrawn)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto size = readU24LE(buffer);
|
|
if (!size) {
|
|
return std::unexpected(size.error());
|
|
}
|
|
if (*size < 8) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto flags = readU16LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto elementCount = readU16LE(buffer);
|
|
if (!elementCount) {
|
|
return std::unexpected(elementCount.error());
|
|
}
|
|
auto sectionData = buffer.subspan(0, *size - 8);
|
|
buffer = buffer.subspan(*size - 8);
|
|
auto section = std::make_unique<AlwaysDrawnSection>();
|
|
section->setDrawOnFront(isBitSet(*flags, 0));
|
|
section->setDrawOnBack(isBitSet(*flags, 1));
|
|
section->setClearBeforeDrawing(isBitSet(*flags, 2));
|
|
section->m_elements.reserve(*elementCount);
|
|
for (std::size_t i = 0; i < *elementCount; ++i) {
|
|
auto type = peekU16LE(sectionData);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
std::expected<std::unique_ptr<Element>, ParseError> element;
|
|
switch (static_cast<ElementType>(*type)) {
|
|
case ElementType::Image: element = ImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break;
|
|
default:
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
if (!element) {
|
|
return std::unexpected(element.error());
|
|
}
|
|
section->m_elements.push_back(std::move(*element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
std::unique_ptr<AlwaysDrawnSection> AlwaysDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
bool drawOnFront = j.at("flags").at("drawOnFront");
|
|
bool drawOnBack = j.at("flags").at("drawOnBack");
|
|
bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");
|
|
|
|
auto section = std::make_unique<AlwaysDrawnSection>();
|
|
section->setDrawOnFront(drawOnFront);
|
|
section->setDrawOnBack(drawOnBack);
|
|
section->setClearBeforeDrawing(clearBeforeDrawing);
|
|
|
|
std::size_t const elementCount = j.at("elements").size();
|
|
for (std::size_t i = 0; i < elementCount; ++i) {
|
|
nlohmann::json const& jElement = j.at("elements").at(i);
|
|
ElementType type = jElement.at("type");
|
|
std::unique_ptr<Element> element;
|
|
switch (type) {
|
|
case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break;
|
|
default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
|
|
jElement.at("type").get<std::string>()));
|
|
}
|
|
section->m_elements.push_back(std::move(element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
TimeBasedDrawnSection::~TimeBasedDrawnSection() {
|
|
}
|
|
|
|
bool TimeBasedDrawnSection::doesDrawOnFront() const noexcept {
|
|
return m_drawOnFront;
|
|
}
|
|
|
|
bool TimeBasedDrawnSection::doesDrawOnBack() const noexcept {
|
|
return m_drawOnBack;
|
|
}
|
|
|
|
bool TimeBasedDrawnSection::doesClearBeforeDrawing() const noexcept {
|
|
return m_clearBeforeDrawing;
|
|
}
|
|
|
|
void TimeBasedDrawnSection::setDrawOnFront(bool value) noexcept {
|
|
m_drawOnFront = value;
|
|
}
|
|
|
|
void TimeBasedDrawnSection::setDrawOnBack(bool value) noexcept {
|
|
m_drawOnBack = value;
|
|
}
|
|
|
|
void TimeBasedDrawnSection::setClearBeforeDrawing(bool value) noexcept {
|
|
m_clearBeforeDrawing = value;
|
|
}
|
|
|
|
std::size_t TimeBasedDrawnSection::elementCount() const noexcept {
|
|
return m_elements.size();
|
|
}
|
|
|
|
Element* TimeBasedDrawnSection::elementAt(std::size_t index) const {
|
|
if (index < m_elements.size()) {
|
|
return m_elements[index].get();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
void TimeBasedDrawnSection::appendElement(std::unique_ptr<Element> element) {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
|
|
void TimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
if (index < m_elements.size()) {
|
|
m_elements.insert(m_elements.begin() + index, std::move(element));
|
|
} else {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Element> TimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements[index] = std::move(element);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<Element> TimeBasedDrawnSection::eraseElement(std::size_t index) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements.erase(m_elements.begin() + index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::int64_t TimeBasedDrawnSection::startTimestamp() const noexcept {
|
|
return m_startTimestamp;
|
|
}
|
|
|
|
std::int64_t TimeBasedDrawnSection::endTimestamp() const noexcept {
|
|
return m_endTimestamp;
|
|
}
|
|
|
|
void TimeBasedDrawnSection::setStartTimestamp(std::int64_t value) {
|
|
m_startTimestamp = value;
|
|
}
|
|
|
|
void TimeBasedDrawnSection::setEndTimestamp(std::int64_t value) {
|
|
m_endTimestamp = value;
|
|
}
|
|
|
|
SectionType TimeBasedDrawnSection::sectionType() const {
|
|
return SectionType::TimeBasedDrawn;
|
|
}
|
|
|
|
std::uint32_t TimeBasedDrawnSection::minimumFormatVersion() const {
|
|
std::uint32_t result = 1;
|
|
for (auto const& element : m_elements) {
|
|
result = std::max(result, element->minimumFormatVersion());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::size_t TimeBasedDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::TimeBasedDrawn));
|
|
// Will be replaced later
|
|
pos = writeU24LE(target, pos, 0);
|
|
std::uint16_t flags = 0;
|
|
maybeSetBit(flags, 0, m_drawOnFront);
|
|
maybeSetBit(flags, 1, m_drawOnBack);
|
|
maybeSetBit(flags, 2, m_clearBeforeDrawing);
|
|
pos = writeU16LE(target, pos, flags);
|
|
pos = writeU16LE(target, pos, m_elements.size());
|
|
pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_startTimestamp));
|
|
pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_endTimestamp));
|
|
for (auto const& element : m_elements) {
|
|
if (pos < target.size()) {
|
|
pos += element->serializeTo(target.subspan(pos), formatVersion);
|
|
} else {
|
|
pos += element->serializeTo({}, formatVersion);
|
|
}
|
|
}
|
|
pos = alignNextWrite(target, pos, 4);
|
|
std::ignore = writeU24LE(target, 1, pos);
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json TimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
|
|
nlohmann::json result;
|
|
result["type"] = "TimeBasedDrawn";
|
|
result["flags"]["drawOnFront"] = m_drawOnFront;
|
|
result["flags"]["drawOnBack"] = m_drawOnBack;
|
|
result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
|
|
result["startTimestamp"] = m_startTimestamp;
|
|
result["endTimestamp"] = m_endTimestamp;
|
|
result["elements"] = nlohmann::json::array();
|
|
for (auto const& element : m_elements) {
|
|
result["elements"].push_back(element->serializeJSON(formatVersion));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::expected<std::unique_ptr<TimeBasedDrawnSection>, ParseError> TimeBasedDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU8LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint8_t>(SectionType::TimeBasedDrawn)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto size = readU24LE(buffer);
|
|
if (!size) {
|
|
return std::unexpected(size.error());
|
|
}
|
|
if (*size < 24) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto flags = readU16LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto elementCount = readU16LE(buffer);
|
|
if (!elementCount) {
|
|
return std::unexpected(elementCount.error());
|
|
}
|
|
auto startTimestamp = readU64LE(buffer);
|
|
if (!startTimestamp) {
|
|
return std::unexpected(startTimestamp.error());
|
|
}
|
|
auto endTimestamp = readU64LE(buffer);
|
|
if (!endTimestamp) {
|
|
return std::unexpected(endTimestamp.error());
|
|
}
|
|
auto sectionData = buffer.subspan(0, *size - 24);
|
|
buffer = buffer.subspan(*size - 24);
|
|
auto section = std::make_unique<TimeBasedDrawnSection>();
|
|
section->setDrawOnFront(isBitSet(*flags, 0));
|
|
section->setDrawOnBack(isBitSet(*flags, 1));
|
|
section->setClearBeforeDrawing(isBitSet(*flags, 2));
|
|
section->setStartTimestamp(std::bit_cast<std::int64_t>(*startTimestamp));
|
|
section->setEndTimestamp(std::bit_cast<std::int64_t>(*endTimestamp));
|
|
section->m_elements.reserve(*elementCount);
|
|
for (std::size_t i = 0; i < *elementCount; ++i) {
|
|
auto type = peekU16LE(sectionData);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
std::expected<std::unique_ptr<Element>, ParseError> element;
|
|
switch (static_cast<ElementType>(*type)) {
|
|
case ElementType::Image: element = ImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break;
|
|
default:
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
if (!element) {
|
|
return std::unexpected(element.error());
|
|
}
|
|
section->m_elements.push_back(std::move(*element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
std::unique_ptr<TimeBasedDrawnSection> TimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
bool drawOnFront = j.at("flags").at("drawOnFront");
|
|
bool drawOnBack = j.at("flags").at("drawOnBack");
|
|
bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");
|
|
std::uint64_t startTimestamp = j.at("startTimestamp");
|
|
std::uint64_t endTimestamp = j.at("endTimestamp");
|
|
|
|
auto section = std::make_unique<TimeBasedDrawnSection>();
|
|
section->setDrawOnFront(drawOnFront);
|
|
section->setDrawOnBack(drawOnBack);
|
|
section->setClearBeforeDrawing(clearBeforeDrawing);
|
|
section->setStartTimestamp(startTimestamp);
|
|
section->setEndTimestamp(endTimestamp);
|
|
|
|
std::size_t const elementCount = j.at("elements").size();
|
|
for (std::size_t i = 0; i < elementCount; ++i) {
|
|
nlohmann::json const& jElement = j.at("elements").at(i);
|
|
ElementType type = jElement.at("type");
|
|
std::unique_ptr<Element> element;
|
|
switch (type) {
|
|
case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break;
|
|
default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
|
|
jElement.at("type").get<std::string>()));
|
|
}
|
|
section->m_elements.push_back(std::move(element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
MultiTimeBasedDrawnSection::~MultiTimeBasedDrawnSection() {
|
|
}
|
|
|
|
bool MultiTimeBasedDrawnSection::doesDrawOnFront() const noexcept {
|
|
return m_drawOnFront;
|
|
}
|
|
|
|
bool MultiTimeBasedDrawnSection::doesDrawOnBack() const noexcept {
|
|
return m_drawOnBack;
|
|
}
|
|
|
|
bool MultiTimeBasedDrawnSection::doesClearBeforeDrawing() const noexcept {
|
|
return m_clearBeforeDrawing;
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::setDrawOnFront(bool value) noexcept {
|
|
m_drawOnFront = value;
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::setDrawOnBack(bool value) noexcept {
|
|
m_drawOnBack = value;
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::setClearBeforeDrawing(bool value) noexcept {
|
|
m_clearBeforeDrawing = value;
|
|
}
|
|
|
|
std::size_t MultiTimeBasedDrawnSection::elementCount() const noexcept {
|
|
return m_elements.size();
|
|
}
|
|
|
|
Element* MultiTimeBasedDrawnSection::elementAt(std::size_t index) const {
|
|
if (index < m_elements.size()) {
|
|
return m_elements[index].get();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::appendElement(std::unique_ptr<Element> element) {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
if (index < m_elements.size()) {
|
|
m_elements.insert(m_elements.begin() + index, std::move(element));
|
|
} else {
|
|
m_elements.push_back(std::move(element));
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Element> MultiTimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr<Element> element) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements[index] = std::move(element);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<Element> MultiTimeBasedDrawnSection::eraseElement(std::size_t index) {
|
|
std::unique_ptr<Element> result;
|
|
if (index < m_elements.size()) {
|
|
result = std::move(m_elements[index]);
|
|
m_elements.erase(m_elements.begin() + index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::pair<std::int64_t, std::int64_t>> MultiTimeBasedDrawnSection::timeRanges() const {
|
|
return m_timeRanges;
|
|
}
|
|
|
|
void MultiTimeBasedDrawnSection::setTimeRanges(std::vector<std::pair<std::int64_t, std::int64_t>> ranges) {
|
|
m_timeRanges = std::move(ranges);
|
|
}
|
|
|
|
SectionType MultiTimeBasedDrawnSection::sectionType() const {
|
|
return SectionType::MultiTimeBasedDrawn;
|
|
}
|
|
|
|
std::uint32_t MultiTimeBasedDrawnSection::minimumFormatVersion() const {
|
|
std::uint32_t result = 2;
|
|
for (auto const& element : m_elements) {
|
|
result = std::max(result, element->minimumFormatVersion());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::size_t MultiTimeBasedDrawnSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::size_t pos = 0;
|
|
pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::MultiTimeBasedDrawn));
|
|
// Will be replaced later
|
|
pos = writeU24LE(target, pos, 0);
|
|
std::uint16_t flags = 0;
|
|
maybeSetBit(flags, 0, m_drawOnFront);
|
|
maybeSetBit(flags, 1, m_drawOnBack);
|
|
maybeSetBit(flags, 2, m_clearBeforeDrawing);
|
|
pos = writeU16LE(target, pos, flags);
|
|
pos = writeU16LE(target, pos, m_elements.size());
|
|
pos = writeU16LE(target, pos, m_timeRanges.size());
|
|
pos = writeU16LE(target, pos, 0);
|
|
for (std::size_t i = 0; i < m_timeRanges.size(); ++i) {
|
|
pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_timeRanges[i].first));
|
|
pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_timeRanges[i].second));
|
|
}
|
|
for (auto const& element : m_elements) {
|
|
if (pos < target.size()) {
|
|
pos += element->serializeTo(target.subspan(pos), formatVersion);
|
|
} else {
|
|
pos += element->serializeTo({}, formatVersion);
|
|
}
|
|
}
|
|
pos = alignNextWrite(target, pos, 4);
|
|
std::ignore = writeU24LE(target, 1, pos);
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json MultiTimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const {
|
|
nlohmann::json result;
|
|
result["type"] = "MultiTimeBasedDrawn";
|
|
result["flags"]["drawOnFront"] = m_drawOnFront;
|
|
result["flags"]["drawOnBack"] = m_drawOnBack;
|
|
result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing;
|
|
result["timeRanges"] = m_timeRanges;
|
|
result["elements"] = nlohmann::json::array();
|
|
for (auto const& element : m_elements) {
|
|
result["elements"].push_back(element->serializeJSON(formatVersion));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::expected<std::unique_ptr<MultiTimeBasedDrawnSection>, ParseError> MultiTimeBasedDrawnSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU8LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint8_t>(SectionType::TimeBasedDrawn)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto size = readU24LE(buffer);
|
|
if (!size) {
|
|
return std::unexpected(size.error());
|
|
}
|
|
if (*size < 24) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto flags = readU16LE(buffer);
|
|
if (!flags) {
|
|
return std::unexpected(flags.error());
|
|
}
|
|
auto elementCount = readU16LE(buffer);
|
|
if (!elementCount) {
|
|
return std::unexpected(elementCount.error());
|
|
}
|
|
auto rangeCount = readU16LE(buffer);
|
|
if (!rangeCount) {
|
|
return std::unexpected(rangeCount.error());
|
|
}
|
|
auto reserved1 = readU16LE(buffer);
|
|
if (!reserved1) {
|
|
return std::unexpected(reserved1.error());
|
|
}
|
|
std::vector<std::pair<std::int64_t, std::int64_t>> timeRanges;
|
|
timeRanges.reserve(*rangeCount);
|
|
for (std::size_t i = 0; i < *rangeCount; ++i) {
|
|
auto startTimestamp = readU64LE(buffer);
|
|
if (!startTimestamp) {
|
|
return std::unexpected(startTimestamp.error());
|
|
}
|
|
auto endTimestamp = readU64LE(buffer);
|
|
if (!endTimestamp) {
|
|
return std::unexpected(endTimestamp.error());
|
|
}
|
|
timeRanges.push_back(std::make_pair(*startTimestamp, *endTimestamp));
|
|
}
|
|
auto sectionData = buffer.subspan(0, *size - 24);
|
|
buffer = buffer.subspan(*size - 24);
|
|
auto section = std::make_unique<MultiTimeBasedDrawnSection>();
|
|
section->setDrawOnFront(isBitSet(*flags, 0));
|
|
section->setDrawOnBack(isBitSet(*flags, 1));
|
|
section->setClearBeforeDrawing(isBitSet(*flags, 2));
|
|
section->setTimeRanges(std::move(timeRanges));
|
|
section->m_elements.reserve(*elementCount);
|
|
for (std::size_t i = 0; i < *elementCount; ++i) {
|
|
auto type = peekU16LE(sectionData);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
std::expected<std::unique_ptr<Element>, ParseError> element;
|
|
switch (static_cast<ElementType>(*type)) {
|
|
case ElementType::Image: element = ImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break;
|
|
default:
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
if (!element) {
|
|
return std::unexpected(element.error());
|
|
}
|
|
section->m_elements.push_back(std::move(*element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
std::unique_ptr<MultiTimeBasedDrawnSection> MultiTimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
bool drawOnFront = j.at("flags").at("drawOnFront");
|
|
bool drawOnBack = j.at("flags").at("drawOnBack");
|
|
bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing");
|
|
std::vector<std::pair<std::int64_t, std::int64_t>> timeRanges = j.at("timeRanges");
|
|
|
|
auto section = std::make_unique<MultiTimeBasedDrawnSection>();
|
|
section->setDrawOnFront(drawOnFront);
|
|
section->setDrawOnBack(drawOnBack);
|
|
section->setClearBeforeDrawing(clearBeforeDrawing);
|
|
section->setTimeRanges(std::move(timeRanges));
|
|
|
|
std::size_t const elementCount = j.at("elements").size();
|
|
for (std::size_t i = 0; i < elementCount; ++i) {
|
|
nlohmann::json const& jElement = j.at("elements").at(i);
|
|
ElementType type = jElement.at("type");
|
|
std::unique_ptr<Element> element;
|
|
switch (type) {
|
|
case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break;
|
|
case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break;
|
|
default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section",
|
|
jElement.at("type").get<std::string>()));
|
|
}
|
|
section->m_elements.push_back(std::move(element));
|
|
}
|
|
return section;
|
|
}
|
|
|
|
ExpiryDateSection::~ExpiryDateSection() {
|
|
}
|
|
|
|
std::int64_t ExpiryDateSection::expiryDate() const noexcept {
|
|
return m_expiryDate;
|
|
}
|
|
|
|
void ExpiryDateSection::setExpiryDate(std::int64_t value) noexcept {
|
|
m_expiryDate = value;
|
|
}
|
|
|
|
SectionType ExpiryDateSection::sectionType() const {
|
|
return SectionType::CustomFont;
|
|
}
|
|
|
|
std::uint32_t ExpiryDateSection::minimumFormatVersion() const {
|
|
return 2;
|
|
}
|
|
|
|
std::size_t ExpiryDateSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::ExpiryDate));
|
|
// Will be replaced later
|
|
pos = writeU24LE(target, pos, 0);
|
|
pos = writeU64LE(target, pos, std::bit_cast<std::uint64_t>(m_expiryDate));
|
|
pos = alignNextWrite(target, pos, 4);
|
|
std::ignore = writeU24LE(target, 1, pos);
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json ExpiryDateSection::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "ExpiryDate";
|
|
result["expiryDate"] = m_expiryDate;
|
|
return result;
|
|
}
|
|
|
|
std::expected<std::unique_ptr<ExpiryDateSection>, ParseError> ExpiryDateSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU8LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint8_t>(SectionType::CustomFont)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto size = readU24LE(buffer);
|
|
if (!size) {
|
|
return std::unexpected(size.error());
|
|
}
|
|
if (*size < 8) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto expiryDate = readU64LE(buffer);
|
|
if (!expiryDate) {
|
|
return std::unexpected(expiryDate.error());
|
|
}
|
|
|
|
auto section = std::make_unique<ExpiryDateSection>();
|
|
section->setExpiryDate(std::bit_cast<std::int64_t>(*expiryDate));
|
|
return section;
|
|
}
|
|
|
|
std::unique_ptr<ExpiryDateSection> ExpiryDateSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
std::int64_t expiryDate = j.at("expiryDate");
|
|
|
|
auto section = std::make_unique<ExpiryDateSection>();
|
|
section->setExpiryDate(expiryDate);
|
|
return section;
|
|
}
|
|
|
|
CustomFontSection::~CustomFontSection() {
|
|
}
|
|
|
|
std::span<std::byte const> CustomFontSection::fontData() const {
|
|
return std::span{m_fontData};
|
|
}
|
|
|
|
void CustomFontSection::setFontData(std::span<std::byte const> fontData) {
|
|
m_fontData.resize(fontData.size());
|
|
std::copy(fontData.begin(), fontData.end(), m_fontData.begin());
|
|
}
|
|
|
|
SectionType CustomFontSection::sectionType() const {
|
|
return SectionType::CustomFont;
|
|
}
|
|
|
|
std::uint32_t CustomFontSection::minimumFormatVersion() const {
|
|
return 1;
|
|
}
|
|
|
|
std::size_t CustomFontSection::serializeTo(std::span<std::byte> target, std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
std::size_t pos = 0;
|
|
pos = writeU8LE(target, pos, static_cast<std::uint16_t>(SectionType::CustomFont));
|
|
// Will be replaced later
|
|
pos = writeU24LE(target, pos, 0);
|
|
pos = writeU24LE(target, pos, m_fontData.size());
|
|
pos = writeU8LE(target, pos, 0);
|
|
pos = writeBuffer(target, pos, m_fontData);
|
|
pos = alignNextWrite(target, pos, 4);
|
|
std::ignore = writeU24LE(target, 1, pos);
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json CustomFontSection::serializeJSON(std::uint32_t formatVersion) const {
|
|
std::ignore = formatVersion;
|
|
|
|
nlohmann::json result;
|
|
result["type"] = "CustomFont";
|
|
result["data"] = base64Encode(m_fontData);
|
|
return result;
|
|
}
|
|
|
|
std::expected<std::unique_ptr<CustomFontSection>, ParseError> CustomFontSection::parse(std::span<std::byte const>& buffer, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto type = readU8LE(buffer);
|
|
if (!type) {
|
|
return std::unexpected(type.error());
|
|
}
|
|
if (*type != static_cast<std::uint8_t>(SectionType::CustomFont)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto size = readU24LE(buffer);
|
|
if (!size) {
|
|
return std::unexpected(size.error());
|
|
}
|
|
if (*size < 8) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto actualSize = readU24LE(buffer);
|
|
if (!actualSize) {
|
|
return std::unexpected(actualSize.error());
|
|
}
|
|
auto reserved_ = readU8LE(buffer);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
auto fontData = buffer.subspan(0, *size - 8);
|
|
buffer = buffer.subspan(*size - 8);
|
|
|
|
auto actualFontData = readBuffer(fontData, *actualSize);
|
|
if (!actualFontData) {
|
|
return std::unexpected(actualFontData.error());
|
|
}
|
|
|
|
auto section = std::make_unique<CustomFontSection>();
|
|
section->setFontData(*actualFontData);
|
|
return section;
|
|
}
|
|
|
|
std::unique_ptr<CustomFontSection> CustomFontSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) {
|
|
std::ignore = formatVersion;
|
|
|
|
auto fontData = base64Decode(j.at("data").get<std::string>());
|
|
|
|
auto section = std::make_unique<CustomFontSection>();
|
|
section->setFontData(fontData);
|
|
return section;
|
|
}
|
|
|
|
std::size_t serializeFile(std::span<std::byte>& buffer, File const& file) {
|
|
std::size_t pos = 0;
|
|
std::uint32_t formatVersion = 1;
|
|
for (auto const& section : file.sections) {
|
|
formatVersion = std::max(formatVersion, section->minimumFormatVersion());
|
|
}
|
|
pos = writeU8LE(buffer, pos, 0xAF);
|
|
pos = writeU8LE(buffer, pos, 0x7E);
|
|
pos = writeU8LE(buffer, pos, 0x2B);
|
|
pos = writeU8LE(buffer, pos, 0x63);
|
|
pos = writeU32LE(buffer, pos, formatVersion);
|
|
pos = writeU16LE(buffer, pos, file.sections.size());
|
|
pos = writeU16LE(buffer, pos, 0);
|
|
pos = alignNextWrite(buffer, pos, 4);
|
|
for (auto const& section : file.sections) {
|
|
if (pos < buffer.size()) {
|
|
pos += section->serializeTo(buffer.subspan(pos), formatVersion);
|
|
} else {
|
|
pos += section->serializeTo({}, formatVersion);
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
nlohmann::json serializeFileJSON(File const& file) {
|
|
std::uint32_t formatVersion = 1;
|
|
for (auto const& section : file.sections) {
|
|
formatVersion = std::max(formatVersion, section->minimumFormatVersion());
|
|
}
|
|
nlohmann::json result;
|
|
result["version"] = formatVersion;
|
|
result["sections"] = nlohmann::json::array();
|
|
for (auto const& section : file.sections) {
|
|
result["sections"].push_back(section->serializeJSON(formatVersion));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::expected<File, ParseError> parseFile(std::span<std::byte const> data) {
|
|
auto magic = readU32LE(data);
|
|
if (!magic) {
|
|
return std::unexpected(magic.error());
|
|
}
|
|
if (*magic != UINT32_C(0x632B7EAF)) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto version = readU32LE(data);
|
|
if (!version) {
|
|
return std::unexpected(version.error());
|
|
}
|
|
if (*version < 1 || *version > 2) {
|
|
return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
auto sectionCount = readU16LE(data);
|
|
if (!sectionCount) {
|
|
return std::unexpected(sectionCount.error());
|
|
}
|
|
auto reserved_ = readU16LE(data);
|
|
if (!reserved_) {
|
|
return std::unexpected(reserved_.error());
|
|
}
|
|
|
|
File result;
|
|
result.sections.reserve(*sectionCount);
|
|
|
|
for (std::size_t i = 0; i < *sectionCount; ++i) {
|
|
auto sectionType = peekU8LE(data);
|
|
if (!sectionType) {
|
|
return std::unexpected(sectionType.error());
|
|
}
|
|
|
|
std::expected<std::unique_ptr<Section>, ParseError> section;
|
|
|
|
switch (static_cast<SectionType>(*sectionType)) {
|
|
case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parse(data, *version); break;
|
|
case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parse(data, *version); break;
|
|
case SectionType::MultiTimeBasedDrawn: section = MultiTimeBasedDrawnSection::parse(data, *version); break;
|
|
case SectionType::ExpiryDate: section = ExpiryDateSection::parse(data, *version); break;
|
|
case SectionType::CustomFont: section = CustomFontSection::parse(data, *version); break;
|
|
default: return std::unexpected(ParseError::InvalidValue);
|
|
}
|
|
|
|
if (!section) {
|
|
return std::unexpected(section.error());
|
|
}
|
|
|
|
result.sections.push_back(std::move(*section));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
File parseFileJSON(nlohmann::json const& j) {
|
|
File result;
|
|
|
|
std::uint32_t version = j.at("version");
|
|
if (version < 1 || version > 2) {
|
|
throw std::runtime_error(std::format("Unsupported file format version {:d} encountered while parsing a file", version));
|
|
}
|
|
std::size_t const sectionCount = j.at("sections").size();
|
|
for (std::size_t i = 0; i < sectionCount; ++i) {
|
|
nlohmann::json const& jSection = j.at("sections").at(i);
|
|
SectionType sectionType = jSection.at("type");
|
|
std::unique_ptr<Section> section;
|
|
switch (sectionType) {
|
|
case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parseJSON(jSection, version); break;
|
|
case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parseJSON(jSection, version); break;
|
|
case SectionType::MultiTimeBasedDrawn: section = MultiTimeBasedDrawnSection::parseJSON(jSection, version); break;
|
|
case SectionType::ExpiryDate: section = ExpiryDateSection::parseJSON(jSection, version); break;
|
|
case SectionType::CustomFont: section = CustomFontSection::parseJSON(jSection, version); break;
|
|
default: throw std::runtime_error(std::format("Unsupported section type {:s} encountered while parsing a file",
|
|
jSection.at("type").get<std::string>()));
|
|
}
|
|
result.sections.push_back(std::move(section));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
constexpr static char const* const Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
constexpr static std::int8_t const Base64ParseLookup[256] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
|
|
|
|
std::string base64Encode(std::span<std::byte const> buffer) {
|
|
std::string result;
|
|
|
|
uint8_t const* data = reinterpret_cast<uint8_t const*>(buffer.data());
|
|
std::size_t numberOfBlocks = (buffer.size() + 2) / 3;
|
|
result.reserve(numberOfBlocks);
|
|
|
|
for (std::size_t i = 0; i < numberOfBlocks; ++i) {
|
|
std::size_t ii = i * 3;
|
|
std::size_t ni = std::min(ii + 3, buffer.size()) - ii;
|
|
|
|
int c0 = (((data[ii + 0] >> 2) & 0x3f) << 0);
|
|
if (ni == 3) {
|
|
int c1 = (((data[ii + 0] >> 0) & 0x03) << 4)
|
|
| (((data[ii + 1] >> 4) & 0x0f) << 0);
|
|
int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2)
|
|
| (((data[ii + 2] >> 6) & 0x03) << 0);
|
|
int c3 = (((data[ii + 2] >> 0) & 0x3f) << 0);
|
|
result.push_back(Base64Alphabet[c0]);
|
|
result.push_back(Base64Alphabet[c1]);
|
|
result.push_back(Base64Alphabet[c2]);
|
|
result.push_back(Base64Alphabet[c3]);
|
|
} else if (ni == 2) {
|
|
int c1 = (((data[ii + 0] >> 0) & 0x03) << 4)
|
|
| (((data[ii + 1] >> 4) & 0x0f) << 0);
|
|
int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2);
|
|
result.push_back(Base64Alphabet[c0]);
|
|
result.push_back(Base64Alphabet[c1]);
|
|
result.push_back(Base64Alphabet[c2]);
|
|
} else if (ni == 1) {
|
|
int c1 = (((data[ii + 0] >> 0) & 0x03) << 4);
|
|
result.push_back(Base64Alphabet[c0]);
|
|
result.push_back(Base64Alphabet[c1]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::byte> base64Decode(std::string_view data) {
|
|
std::vector<std::byte> result;
|
|
// This is just an initial estimate
|
|
std::size_t numberOfBlocks = (data.size() + 3) / 4;
|
|
result.reserve(numberOfBlocks * 3);
|
|
|
|
int state = 0;
|
|
int currentValue = 0;
|
|
for (std::size_t i = 0; i < data.size(); ++i) {
|
|
int value = Base64ParseLookup[static_cast<std::uint8_t>(data[i])];
|
|
if (value == -2) {
|
|
// '=': padding at the end
|
|
break;
|
|
} else if (value == -1) {
|
|
// any invalid character (especially whitespace): ignore
|
|
continue;
|
|
}
|
|
|
|
switch (state) {
|
|
case 0:
|
|
default:
|
|
currentValue = value << 2;
|
|
break;
|
|
case 1:
|
|
currentValue |= (value >> 4) & 0x03;
|
|
result.push_back(static_cast<std::byte>(currentValue));
|
|
currentValue = (value & 0x0f) << 4;
|
|
break;
|
|
case 2:
|
|
currentValue |= (value >> 2) & 0x0f;
|
|
result.push_back(static_cast<std::byte>(currentValue));
|
|
currentValue = (value & 0x03) << 6;
|
|
break;
|
|
case 3:
|
|
currentValue |= value & 0x3f;
|
|
result.push_back(static_cast<std::byte>(currentValue));
|
|
currentValue = 0;
|
|
break;
|
|
}
|
|
state = (state + 1) % 4;
|
|
}
|
|
|
|
// Ignore any errors here
|
|
return result;
|
|
}
|
|
|
|
} // namespace monoformat
|
|
|