#include "monoformat_structured.hpp" #include "monoformat_fontreader.hpp" #include "monoformat_bithelpers.hpp" #include #include #include namespace monoformat { static std::string base64Encode(std::span buffer); static std::vector 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 ImageElement::buffer() noexcept { return m_buffer; } std::span 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 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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 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, ParseError> ImageElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(*x, *y, *width, *height); result->updateBuffer(*imageData); return result; } std::unique_ptr 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 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(x, y, width, height); auto image = result->image(); for (std::uint16_t dy = 0; dy < height; ++dy) { std::size_t base = static_cast(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 AnimationElement::buffers() noexcept { return m_buffer; } std::span AnimationElement::buffers() const noexcept { return m_buffer; } std::span 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 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 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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 customFonts) { std::ignore = currentTimestamp; std::ignore = customFonts; if (m_numberOfFrames == 0) { return; } std::size_t const tick = animationTick / (static_cast(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, ParseError> AnimationElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(*x, *y, *width, *height, *numberOfFrames, *updateInterval); result->updateBuffer(*imageData); return result; } std::unique_ptr 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> frames = j.at("frames"); if (frames.size() > std::numeric_limits::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(x, y, width, height, static_cast(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(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 HScrollImageElement::buffer() noexcept { return std::span{m_buffer}; } std::span 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 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(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 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(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, ParseError> HScrollImageElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(*x, *y, *width, *height, *contentWidth, ScrollFlags{*flags}, *scrollSpeed); result->updateBuffer(*imageData); return result; } std::unique_ptr 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 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(x, y, width, height, contentWidth, flags, static_cast(scrollSpeed)); auto image = result->image(); for (std::uint16_t dy = 0; dy < height; ++dy) { std::size_t base = static_cast(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 VScrollImageElement::buffer() noexcept { return std::span{m_buffer}; } std::span 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 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(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 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(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, ParseError> VScrollImageElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(*x, *y, *width, *height, *contentHeight, ScrollFlags{*flags}, *scrollSpeed); result->updateBuffer(*imageData); return result; } std::unique_ptr 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 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(x, y, width, height, contentHeight, flags, static_cast(scrollSpeed)); auto image = result->image(); for (std::uint16_t dy = 0; dy < contentHeight; ++dy) { std::size_t base = static_cast(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(m_flags) != 0) { return 2; } else { return 1; } } std::size_t LineElement::serializeTo(std::span target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(m_lineStyle)); pos = writeU8LE(target, pos, static_cast(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 customFonts) { std::ignore = animationTick; std::ignore = currentTimestamp; std::ignore = customFonts; std::int32_t dx = static_cast(m_targetX) - static_cast(m_originX); std::int32_t dy = static_cast(m_targetY) - static_cast(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(x), static_cast(y), value); } } } std::expected, ParseError> LineElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(LineStyle::Solid)) { return std::unexpected(ParseError::InvalidValue); } auto flags = readU8LE(buffer); if (!flags) { return std::unexpected(flags.error()); } auto result = std::make_unique(*originX, *originY, *targetX, *targetY, static_cast(*lineStyle), LineFlags{*flags}); return result; } std::unique_ptr 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(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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(m_fillPattern)); pos = writeU8LE(target, pos, static_cast(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 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, ParseError> BoxElement::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(*x, *y, *width, *height, static_cast(*fillPattern), FillFlags{*flags}); return result; } std::unique_ptr 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(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(m_textFlags) != 0) { return 2; } else { return 1; } } std::size_t ClippedTextElement::serializeTo(std::span target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(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 customFonts) { std::ignore = animationTick; std::ignore = currentTimestamp; std::span 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 bgColor; bgColor = m_textFlags.dark; bool fgColor = !m_textFlags.dark; auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } std::expected, ParseError> ClippedTextElement::parse(std::span& buffer, std::uint32_t formatVersion) { auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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 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(textData->data()), *textLength}; auto result = std::make_unique(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, std::string{text}); return result; } std::unique_ptr 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(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(m_textFlags) != 0) { return 2; } else { return 1; } } std::size_t HScrollTextElement::serializeTo(std::span target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(m_textFlags)); pos = writeU8LE(target, pos, 0); } pos = writeU8LE(target, pos, static_cast(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 customFonts) { std::ignore = currentTimestamp; std::span 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(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 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(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(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(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(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } } std::expected, ParseError> HScrollTextElement::parse(std::span& buffer, std::uint32_t formatVersion) { auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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 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(textData->data()), *textLength}; auto result = std::make_unique(*x, *y, *width, *height, TextFlags{*textFlags}, ScrollFlags{*flags}, *scrollSpeed, *fontIndex, std::string{text}); return result; } std::unique_ptr 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(x, y, width, height, textFlags, flags, static_cast(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(m_textFlags) != 0) { return 2; } else { return 1; } } std::size_t CurrentTimeElement::serializeTo(std::span target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(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(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(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 customFonts) { std::ignore = animationTick; std::span 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 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(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(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } std::expected, ParseError> CurrentTimeElement::parse(std::span& buffer, std::uint32_t formatVersion) { auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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 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(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, *utcOffset, TimeDisplayFlags{*flags}); return result; } std::unique_ptr 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(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) { m_elements.push_back(std::move(element)); } void AlwaysDrawnSection::insertElement(std::size_t index, std::unique_ptr 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 AlwaysDrawnSection::replaceElement(std::size_t index, std::unique_ptr element) { std::unique_ptr result; if (index < m_elements.size()) { result = std::move(m_elements[index]); m_elements[index] = std::move(element); } return result; } std::unique_ptr AlwaysDrawnSection::eraseElement(std::size_t index) { std::unique_ptr 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 target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(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, ParseError> AlwaysDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(); 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, ParseError> element; switch (static_cast(*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::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(); 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; 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())); } 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) { m_elements.push_back(std::move(element)); } void TimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr 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 TimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr element) { std::unique_ptr result; if (index < m_elements.size()) { result = std::move(m_elements[index]); m_elements[index] = std::move(element); } return result; } std::unique_ptr TimeBasedDrawnSection::eraseElement(std::size_t index) { std::unique_ptr 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 target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(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(m_startTimestamp)); pos = writeU64LE(target, pos, std::bit_cast(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, ParseError> TimeBasedDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(); section->setDrawOnFront(isBitSet(*flags, 0)); section->setDrawOnBack(isBitSet(*flags, 1)); section->setClearBeforeDrawing(isBitSet(*flags, 2)); section->setStartTimestamp(std::bit_cast(*startTimestamp)); section->setEndTimestamp(std::bit_cast(*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, ParseError> element; switch (static_cast(*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::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(); 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; 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())); } 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) { m_elements.push_back(std::move(element)); } void MultiTimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr 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 MultiTimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr element) { std::unique_ptr result; if (index < m_elements.size()) { result = std::move(m_elements[index]); m_elements[index] = std::move(element); } return result; } std::unique_ptr MultiTimeBasedDrawnSection::eraseElement(std::size_t index) { std::unique_ptr result; if (index < m_elements.size()) { result = std::move(m_elements[index]); m_elements.erase(m_elements.begin() + index); } return result; } std::vector> MultiTimeBasedDrawnSection::timeRanges() const { return m_timeRanges; } void MultiTimeBasedDrawnSection::setTimeRanges(std::vector> 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 target, std::uint32_t formatVersion) const { std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(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(m_timeRanges[i].first)); pos = writeU64LE(target, pos, std::bit_cast(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, ParseError> MultiTimeBasedDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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> 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(); 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, ParseError> element; switch (static_cast(*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::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> timeRanges = j.at("timeRanges"); auto section = std::make_unique(); 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; 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())); } 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(SectionType::ExpiryDate)); // Will be replaced later pos = writeU24LE(target, pos, 0); pos = writeU64LE(target, pos, std::bit_cast(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, ParseError> ExpiryDateSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(); section->setExpiryDate(std::bit_cast(*expiryDate)); return section; } std::unique_ptr 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(); section->setExpiryDate(expiryDate); return section; } CustomFontSection::~CustomFontSection() { } std::span CustomFontSection::fontData() const { return std::span{m_fontData}; } void CustomFontSection::setFontData(std::span 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 target, std::uint32_t formatVersion) const { std::ignore = formatVersion; std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(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, ParseError> CustomFontSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); } if (*type != static_cast(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(); section->setFontData(*actualFontData); return section; } std::unique_ptr CustomFontSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { std::ignore = formatVersion; auto fontData = base64Decode(j.at("data").get()); auto section = std::make_unique(); section->setFontData(fontData); return section; } std::size_t serializeFile(std::span& 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 parseFile(std::span 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, ParseError> section; switch (static_cast(*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; 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())); } 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 buffer) { std::string result; uint8_t const* data = reinterpret_cast(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 base64Decode(std::string_view data) { std::vector 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(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(currentValue)); currentValue = (value & 0x0f) << 4; break; case 2: currentValue |= (value >> 2) & 0x0f; result.push_back(static_cast(currentValue)); currentValue = (value & 0x03) << 6; break; case 3: currentValue |= value & 0x3f; result.push_back(static_cast(currentValue)); currentValue = 0; break; } state = (state + 1) % 4; } // Ignore any errors here return result; } } // namespace monoformat