diff --git a/Specification.rst b/Specification.rst index 1541275..4c1c4f7 100644 --- a/Specification.rst +++ b/Specification.rst @@ -379,7 +379,7 @@ Horizontally Scrolling Text +------------------------+------------------------+------------------------+------------------------+ 8 | Height | Flags | Scroll Speed | +------------------------+------------------------+------------------------+------------------------+ - 12 | Font Index | Text ... | + 12 | Font Index | Text Length | +------------------------+------------------------+------------------------+------------------------+ ... | ... Text | Padding (if required) | +------------------------+------------------------+------------------------+------------------------+ @@ -389,7 +389,7 @@ Current Time 0 1 2 3 +------------------------+------------------------+------------------------+------------------------+ - 0 | Type: 16 | X Offset | + 0 | Type: 32 | X Offset | +------------------------+------------------------+------------------------+------------------------+ 4 | Y Offset | Width | +------------------------+------------------------+------------------------+------------------------+ diff --git a/cpp/README.txt b/cpp/README.txt index b3978b6..a4046c2 100644 --- a/cpp/README.txt +++ b/cpp/README.txt @@ -2,13 +2,6 @@ This is the C++ implementation of the mono display format library. TODO: - - Structured: fully implement the current spec (including changes to - spec since the initial implementation) - - Line support - - Text support - - Current time support - - Write low-RAM parser / renderer for the format (the structured renderer - exists for high-level software) - CMake: automatically convert standard fonts into importable data - Properly define a list of standard fonts and their indexes - Properly declare license information for font parser diff --git a/cpp/clivis/main.cpp b/cpp/clivis/main.cpp index 224c86b..1ed8838 100644 --- a/cpp/clivis/main.cpp +++ b/cpp/clivis/main.cpp @@ -1,47 +1,52 @@ #include #include +#include -#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include using namespace monoformat; constexpr static std::uint16_t const ScreenWidth = 120; constexpr static std::uint16_t const ScreenHeight = 60; -int main() { - auto fontData = findEmbeddedFont("NokiaSmallPlain_tf"); - if (!fontData.size()) { - std::cerr << "Error loading font!\n" << std::flush; - return 1; - } - auto fontData2 = findEmbeddedFont("5x7_mf"); - if (!fontData2.size()) { - std::cerr << "Error loading font!\n" << std::flush; - return 1; - } +static std::atomic g_ctrlCPressed; - std::vector memory; - memory.resize((ScreenWidth * ScreenHeight + 7) / 8); - MemoryOneBitBuffer screen{memory, ScreenWidth, ScreenHeight}; +static void handleCtrlC(int) { + g_ctrlCPressed.store(true, std::memory_order_relaxed); +} - auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); - auto ret = renderer.render("Hallo Welt! ggg äöüß é è ê", {0, renderer.lineHeight() - 1}, screen, true, false); - if (!ret) { - std::cerr << "Error rendering.\n" << std::flush; - return 2; - } - - auto renderer2 = FontRenderer{fontData2}.withIgnoreUnknownChars(true); - ret = renderer2.render("Hallo2", {0, renderer2.lineHeight() - 1 + renderer.lineHeight()}, screen, true, false); - if (!ret) { - std::cerr << "Error rendering.\n" << std::flush; - return 2; +template +static Container readEntireFile(std::filesystem::path const& fileName) { + std::ifstream fileReader(fileName, std::ios::binary | std::ios::ate | std::ios::in); + if (!fileReader) { + std::ostringstream buf; + buf << "Could not open \"" << fileName << "\" for reading"; + throw std::runtime_error(buf.str()); } + auto fileSize = fileReader.tellg(); + fileReader.seekg(std::ios::beg); + typename Container::size_type scalarSize = sizeof(typename Container::value_type); + Container content((static_cast(fileSize) + scalarSize - 1) / scalarSize, + static_cast(0)); + fileReader.read(reinterpret_cast(&content[0]), static_cast(fileSize)); + return content; +} - std::cerr << "Bbox height: " << ret->boundingBox->size.height << std::endl; +static void clearScreen() { + std::cout << "\x1b[1J\x1b[1;1H" << std::flush; +} +static void renderScreeen(monoformat::OneBitBufferInterface const& imageBuffer) { std::uint16_t const w = ScreenWidth; std::uint16_t const h = ScreenHeight / 2; @@ -54,8 +59,8 @@ int main() { for (std::uint16_t y = 0; y < h; ++y) { buf << "\xE2\x94\x82"; for (std::uint16_t x = 0; x < w; ++x) { - bool const pxUpperSet = screen.isPixelSet(x, 2 * y + 0); - bool const pxLowerSet = screen.isPixelSet(x, 2 * y + 1); + bool const pxUpperSet = imageBuffer.isPixelSet(x, 2 * y + 0); + bool const pxLowerSet = imageBuffer.isPixelSet(x, 2 * y + 1); if (pxUpperSet && pxLowerSet) { buf << "\xE2\x96\x88"; } else if (pxUpperSet) { @@ -71,7 +76,7 @@ int main() { if (ScreenHeight % 1) { buf << "\xE2\x94\x82"; for (std::uint16_t x = 0; x < w; ++x) { - bool const pxUpperSet = screen.isPixelSet(x, ScreenHeight - 1); + bool const pxUpperSet = imageBuffer.isPixelSet(x, ScreenHeight - 1); if (pxUpperSet) { buf << "\xF0\x9F\xAC\x8E"; } else { @@ -86,6 +91,88 @@ int main() { } buf << "\xE2\x94\x98\n"; std::cout << buf.str(); +} + +static void showContents(monoformat::File const& file, std::size_t tick, std::int64_t timestamp, std::span customFonts) { + monoformat::ImageElement imageBufferElement{0, 0, ScreenWidth, ScreenHeight}; + auto imageBuffer = imageBufferElement.image(); + + for (auto const& section : file.sections) { + if (auto sec = dynamic_cast(section.get())) { + for (std::size_t i = 0; i < sec->elementCount(); ++i) { + sec->elementAt(i)->drawTo(&imageBuffer, tick, timestamp, customFonts); + } + } else if (auto sec = dynamic_cast(section.get())) { + for (std::size_t i = 0; i < sec->elementCount(); ++i) { + sec->elementAt(i)->drawTo(&imageBuffer, tick, timestamp, customFonts); + } + } + } + + renderScreeen(imageBuffer); +} +static void showContents2(std::span data, std::size_t tick, std::int64_t timestamp) { + std::vector buffer; + buffer.resize((ScreenWidth * ScreenHeight + 8 - 1) / 8); + MemoryOneBitBuffer imageBuffer{buffer, ScreenWidth, ScreenHeight}; + + auto ret = parseAndApply(data, &imageBuffer, ScreenWidth, ScreenHeight, true, tick, timestamp); + if (!ret) { + std::cerr << "Parse & apply error\n"; + } + + renderScreeen(imageBuffer); +} + +int main(int argc, char** argv) { + if (argc != 2 || !argv[1]) { + std::ostringstream buf; + buf << "Usage: " << argv[0] << " filename\n"; + std::cerr << buf.str(); + return 1; + } + if (std::string_view{argv[1]} == "--help") { + std::ostringstream buf; + buf << "Usage: " << argv[0] << " filename\n"; + std::cout << buf.str() << std::flush; + return 0; + } + + try { + auto contents = readEntireFile>(argv[1]); + auto file = monoformat::parseFile(contents); + if (!file) { + std::ostringstream buf; + buf << "Parse error while processing the file \"" << argv[1] << "\""; + throw std::runtime_error(buf.str()); + } + + std::vector customFonts; + for (auto const& section : file->sections) { + if (auto fs = dynamic_cast(section.get())) { + customFonts.push_back(fs); + } + } + + std::signal(SIGINT, &handleCtrlC); + std::signal(SIGTERM, &handleCtrlC); + std::size_t tick = 0; + while (!g_ctrlCPressed.load(std::memory_order_relaxed)) { + clearScreen(); + //showContents(*file, tick, time(nullptr), customFonts); + showContents2(contents, tick, time(nullptr)); + std::ostringstream buf; + buf << "Animation Tick: " << tick << "\n"; + std::cout << buf.str() << std::flush; + std::this_thread::sleep_for(std::chrono::milliseconds{40}); + ++tick; + } + } catch (std::exception& e) { + std::ostringstream buf; + buf << argv[0] << ": " << e.what() << "\n"; + std::cerr << buf.str(); + return 2; + } return 0; } diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index b26967e..b0ebe6c 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -1,10 +1,14 @@ set(monoformat_SOURCES + monoformat_schema.cpp monoformat_structured.cpp + monoformat_parseonly.cpp monoformat_fontreader.cpp ) set(monoformat_HEADERS + monoformat_schema.hpp monoformat_structured.hpp monoformat_parsehelpers.hpp + monoformat_parseonly.hpp monoformat_fontreader.hpp monoformat_gfx.hpp monoformat_utf8.hpp diff --git a/cpp/src/monoformat_fontreader.cpp b/cpp/src/monoformat_fontreader.cpp index fa4883b..47921ff 100644 --- a/cpp/src/monoformat_fontreader.cpp +++ b/cpp/src/monoformat_fontreader.cpp @@ -5,10 +5,10 @@ namespace monoformat { #include "u8g2_font_NokiaSmallPlain_tf.inc.cpp" #include "u8g2_font_5x7_mf.inc.cpp" -std::span findEmbeddedFont(std::string_view name) { - if (name == "NokiaSmallPlain_tf") { +std::span findEmbeddedFont(std::size_t fontIndex) { + if (fontIndex == 0) { return std::as_bytes(std::span{u8g2_font_NokiaSmallPlain_tf_u8g2font, u8g2_font_NokiaSmallPlain_tf_u8g2font_len}); - } else if (name == "5x7_mf") { + } else if (fontIndex == 1) { return std::as_bytes(std::span{u8g2_font_5x7_mf_u8g2font, u8g2_font_5x7_mf_u8g2font_len}); } else { return {}; diff --git a/cpp/src/monoformat_fontreader.hpp b/cpp/src/monoformat_fontreader.hpp index a10c495..dcdf69a 100644 --- a/cpp/src/monoformat_fontreader.hpp +++ b/cpp/src/monoformat_fontreader.hpp @@ -122,8 +122,8 @@ private: friend class GlyphSearcher; std::span m_data; - bool m_supportsBackgroundColor{}; std::uint8_t m_glyphCount{}; + bool m_supportsBackgroundColor{}; std::uint8_t m_m0{}; std::uint8_t m_m1{}; std::uint8_t m_bitCountW{}; @@ -652,7 +652,7 @@ inline FontRenderer::FontRenderer(std::span fontData) { } -extern std::span findEmbeddedFont(std::string_view name); +extern std::span findEmbeddedFont(std::size_t fontIndex); } // namespace monoformat diff --git a/cpp/src/monoformat_parsehelpers.hpp b/cpp/src/monoformat_parsehelpers.hpp index 503159c..06b2462 100644 --- a/cpp/src/monoformat_parsehelpers.hpp +++ b/cpp/src/monoformat_parsehelpers.hpp @@ -5,11 +5,13 @@ #include #include #include +#include // for std::ignore namespace monoformat { enum class ParseError { BufferSizeMismatch = 1, + InvalidValue = 2, }; inline std::expected peekU8LE(std::span buffer) { @@ -121,7 +123,7 @@ inline std::expected peekU32LE(std::span readU32LE(std::span& buffer) { auto result = peekU32LE(buffer); if (result) { - buffer = buffer.subspan(8); + buffer = buffer.subspan(4); } return result; } @@ -228,6 +230,125 @@ inline std::pair overflowingShl(std::uint8_t value, std::uin return {value << bits, overflow}; } +inline std::size_t writeU8LE(std::span& target, std::size_t pos, std::uint8_t value) { + if (pos < target.size()) { + target[pos] = static_cast(value); + } + return pos + 1; +} + +inline std::size_t writeU8BE(std::span& target, std::size_t pos, std::uint8_t value) { + if (pos < target.size()) { + target[pos] = static_cast(value); + } + return pos + 1; +} + +inline std::size_t writeU16LE(std::span& target, std::size_t pos, std::uint16_t value) { + if ((pos + 1) < target.size()) { + target[pos + 0] = static_cast((value >> 0u) & 0xffu); + target[pos + 1] = static_cast((value >> 8u) & 0xffu); + } + return pos + 2; +} + +inline std::size_t writeU16BE(std::span& target, std::size_t pos, std::uint16_t value) { + if ((pos + 1) < target.size()) { + target[pos + 0] = static_cast((value >> 8u) & 0xffu); + target[pos + 1] = static_cast((value >> 0u) & 0xffu); + } + return pos + 2; +} + +inline std::size_t writeU24LE(std::span& target, std::size_t pos, std::uint32_t value) { + if ((pos + 2) < target.size()) { + target[pos + 0] = static_cast((value >> 0u) & 0xffu); + target[pos + 1] = static_cast((value >> 8u) & 0xffu); + target[pos + 2] = static_cast((value >> 16u) & 0xffu); + } + return pos + 3; +} + +inline std::size_t writeU24BE(std::span& target, std::size_t pos, std::uint32_t value) { + if ((pos + 2) < target.size()) { + target[pos + 0] = static_cast((value >> 16u) & 0xffu); + target[pos + 1] = static_cast((value >> 8u) & 0xffu); + target[pos + 2] = static_cast((value >> 0u) & 0xffu); + } + return pos + 3; +} + +inline std::size_t writeU32LE(std::span& target, std::size_t pos, std::uint32_t value) { + if ((pos + 3) < target.size()) { + target[pos + 0] = static_cast((value >> 0u) & 0xffu); + target[pos + 1] = static_cast((value >> 8u) & 0xffu); + target[pos + 2] = static_cast((value >> 16u) & 0xffu); + target[pos + 3] = static_cast((value >> 24u) & 0xffu); + } + return pos + 4; +} + +inline std::size_t writeU32BE(std::span& target, std::size_t pos, std::uint32_t value) { + if ((pos + 3) < target.size()) { + target[pos + 0] = static_cast((value >> 24u) & 0xffu); + target[pos + 1] = static_cast((value >> 16u) & 0xffu); + target[pos + 2] = static_cast((value >> 8u) & 0xffu); + target[pos + 3] = static_cast((value >> 0u) & 0xffu); + } + return pos + 4; +} + +inline std::size_t writeU64LE(std::span& target, std::size_t pos, std::uint64_t value) { + if ((pos + 7) < target.size()) { + target[pos + 0] = static_cast((value >> 0u) & 0xffu); + target[pos + 1] = static_cast((value >> 8u) & 0xffu); + target[pos + 2] = static_cast((value >> 16u) & 0xffu); + target[pos + 3] = static_cast((value >> 24u) & 0xffu); + target[pos + 4] = static_cast((value >> 32u) & 0xffu); + target[pos + 5] = static_cast((value >> 40u) & 0xffu); + target[pos + 6] = static_cast((value >> 48u) & 0xffu); + target[pos + 7] = static_cast((value >> 56u) & 0xffu); + } + return pos + 8; +} + +inline std::size_t writeU64BE(std::span& target, std::size_t pos, std::uint64_t value) { + if ((pos + 3) < target.size()) { + target[pos + 0] = static_cast((value >> 56u) & 0xffu); + target[pos + 1] = static_cast((value >> 48u) & 0xffu); + target[pos + 2] = static_cast((value >> 40u) & 0xffu); + target[pos + 3] = static_cast((value >> 32u) & 0xffu); + target[pos + 4] = static_cast((value >> 24u) & 0xffu); + target[pos + 5] = static_cast((value >> 16u) & 0xffu); + target[pos + 6] = static_cast((value >> 8u) & 0xffu); + target[pos + 7] = static_cast((value >> 0u) & 0xffu); + } + return pos + 8; +} + +inline std::size_t writeBuffer(std::span& target, std::size_t pos, std::span value) { + if ((pos + value.size()) > target.size()) { + if (pos < target.size()) { + std::size_t const n = target.size() - pos; + std::copy(value.begin(), value.begin() + n, target.begin() + pos); + } + } else { + std::copy(value.begin(), value.end(), target.begin() + pos); + } + return pos + value.size(); +} + +inline std::size_t alignNextWrite(std::span& target, std::size_t pos, std::size_t alignment) { + std::ignore = target; + if (alignment <= 1) { + return pos; + } + if (pos % alignment == 0) { + return pos; + } + return ((pos + alignment - 1) / alignment) * alignment; +} + } // namespace monoformat #endif // MONOFORMAT_PARSEHELPERS_HPP diff --git a/cpp/src/monoformat_parseonly.cpp b/cpp/src/monoformat_parseonly.cpp new file mode 100644 index 0000000..c50c6d3 --- /dev/null +++ b/cpp/src/monoformat_parseonly.cpp @@ -0,0 +1,1053 @@ +#include "monoformat_parseonly.hpp" +#include "monoformat_parsehelpers.hpp" +#include "monoformat_fontreader.hpp" + +#include +#include +#include +#include + +namespace monoformat { + +struct AlwaysDrawnSectionMetaData { + bool drawOnFront{}; + bool drawOnBack{}; + bool clearBeforeDrawing{}; + std::span elementData; +}; + +struct TimeBasedDrawnSectionMetaData { + AlwaysDrawnSectionMetaData base; + std::int64_t startTimestamp{}; + std::int64_t endTimestamp{}; +}; + +static std::expected handleImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleAnimation(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleHScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleVScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleLine(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleClippedText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleHScrollText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); +static std::expected handleCurrentTime(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts); + +static std::expected parseAlwaysDrawnSection(std::span& data) { + auto type = readU8LE(data); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::AlwaysDrawn)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(data); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 8) { + return std::unexpected(ParseError::InvalidValue); + } + auto flags = readU16LE(data); + if (!flags) { + return std::unexpected(flags.error()); + } + auto elementCount = readU16LE(data); + if (!elementCount) { + return std::unexpected(elementCount.error()); + } + auto sectionData = data.subspan(0, *size - 8); + data = data.subspan(*size - 8); + + AlwaysDrawnSectionMetaData result; + result.drawOnFront = (((*flags) >> 0u) & 0x01u) == 0x01u; + result.drawOnBack = (((*flags) >> 0u) & 0x02u) == 0x02u; + result.clearBeforeDrawing = (((*flags) >> 0u) & 0x04u) == 0x04u; + result.elementData = sectionData; + return result; +} + +static std::expected parseTimeBasedDrawnSection(std::span& data) { + auto type = readU8LE(data); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::AlwaysDrawn)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(data); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 8) { + return std::unexpected(ParseError::InvalidValue); + } + auto flags = readU16LE(data); + if (!flags) { + return std::unexpected(flags.error()); + } + auto elementCount = readU16LE(data); + if (!elementCount) { + return std::unexpected(elementCount.error()); + } + auto startTimestamp = readU64LE(data); + if (!startTimestamp) { + return std::unexpected(startTimestamp.error()); + } + auto endTimestamp = readU64LE(data); + if (!endTimestamp) { + return std::unexpected(endTimestamp.error()); + } + auto sectionData = data.subspan(0, *size - 24); + data = data.subspan(*size - 24); + + TimeBasedDrawnSectionMetaData result; + result.base.drawOnFront = (((*flags) >> 0u) & 0x01u) == 0x01u; + result.base.drawOnBack = (((*flags) >> 0u) & 0x02u) == 0x02u; + result.base.clearBeforeDrawing = (((*flags) >> 0u) & 0x04u) == 0x04u; + result.base.elementData = sectionData; + result.startTimestamp = std::bit_cast(*startTimestamp); + result.endTimestamp = std::bit_cast(*endTimestamp); + return result; +} + +static std::expected, ParseError> parseCustomFontSection(std::span& data) { + auto type = readU8LE(data); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::CustomFont)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(data); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 8) { + return std::unexpected(ParseError::InvalidValue); + } + auto actualSize = readU24LE(data); + if (!actualSize) { + return std::unexpected(actualSize.error()); + } + auto reserved_ = readU8LE(data); + if (!reserved_) { + return std::unexpected(reserved_.error()); + } + auto fontData = data.subspan(0, *size - 8); + data = data.subspan(*size - 8); + + return readBuffer(fontData, *actualSize); +} + +std::expected parseAndApply(std::span data, OneBitBufferInterface* screen, + std::uint16_t screenWidth, std::uint16_t screenHeight, + bool isFront, std::size_t animationTick, std::int64_t currentTimestamp) { + std::array, 16> customFonts; + std::size_t customFontIndex{}; + + 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) { + 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()); + } + + for (std::size_t i = 0; i < *sectionCount; ++i) { + auto sectionType = peekU8LE(data); + if (!sectionType) { + return std::unexpected(sectionType.error()); + } + auto sectionSize = peekU24LE(data.subspan(1)); + if (!sectionSize) { + return std::unexpected(sectionSize.error()); + } + auto rawSectionData = readBuffer(data, *sectionSize); + if (!rawSectionData) { + return std::unexpected(rawSectionData.error()); + } + + if (static_cast(*sectionType) == SectionType::CustomFont) { + auto fontData = parseCustomFontSection(*rawSectionData); + if (!fontData) { + return std::unexpected(fontData.error()); + } + if (customFontIndex < 16) { + customFonts[customFontIndex++] = *fontData; + } + continue; + } + + AlwaysDrawnSectionMetaData section; + if (static_cast(*sectionType) == SectionType::AlwaysDrawn) { + auto metaData = parseAlwaysDrawnSection(*rawSectionData); + if (!metaData) { + return std::unexpected(metaData.error()); + } + section = *metaData; + } else if (static_cast(*sectionType) == SectionType::TimeBasedDrawn) { + auto metaData = parseTimeBasedDrawnSection(*rawSectionData); + if (!metaData) { + return std::unexpected(metaData.error()); + } + if (currentTimestamp < metaData->startTimestamp || currentTimestamp >= metaData->endTimestamp) { + continue; + } + section = metaData->base; + } else { + continue; + } + + if ((!section.drawOnFront && isFront) + || (!section.drawOnBack && !isFront)) { + continue; + } + + if (section.clearBeforeDrawing) { + for (std::uint16_t y = 0; y < screenHeight; ++y) { + for (std::uint16_t x = 0; x < screenWidth; ++x) { + screen->setPixel(x, y, false); + } + } + } + + while (section.elementData.size() > 0) { + auto type = peekU16LE(section.elementData); + if (!type) { + return std::unexpected(type.error()); + } + + auto eType = static_cast(*type); + std::expected parseResult; + switch (eType) { + case ElementType::Image: parseResult = handleImage(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::Animation: parseResult = handleAnimation(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::HScrollImage: parseResult = handleHScrollImage(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::VScrollImage: parseResult = handleVScrollImage(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::Line: parseResult = handleLine(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::ClippedText: parseResult = handleClippedText(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::HScrollText: parseResult = handleHScrollText(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + case ElementType::CurrentTime: parseResult = handleCurrentTime(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex)); break; + default: parseResult = std::unexpected(ParseError::InvalidValue); + } + + if (!parseResult) { + return std::unexpected(parseResult.error()); + } + } + } + + return {}; +} + +std::expected handleImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + 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); + } + + std::ignore = animationTick; + std::ignore = currentTimestamp; + std::ignore = customFonts; + auto ownImage = ConstMemoryOneBitBuffer{*imageData, *width, *height}; + for (std::uint16_t dy = 0; dy < *height; ++dy) { + for (std::uint16_t dx = 0; dx < *width; ++dx) { + screen->setPixel(*x + dx, *y + dy, ownImage.isPixelSet(dx, dy)); + } + } + + return {}; +} + +std::expected handleAnimation(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = currentTimestamp; + std::ignore = customFonts; + + 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); + } + + if (*numberOfFrames == 0) { + return {}; + } + + std::size_t const tick = animationTick / (static_cast(*updateInterval) + 1); + std::size_t const frameIndex = tick % *numberOfFrames; + auto ownImage = ConstMemoryOneBitBuffer{std::span{*imageData}.subspan(frameIndex * imageSize, imageSize), *width, *height}; + for (std::uint16_t dy = 0; dy < *height; ++dy) { + for (std::uint16_t dx = 0; dx < *width; ++dx) { + screen->setPixel(*x + dx, *y + dy, ownImage.isPixelSet(dx, dy)); + } + } + + return {}; +} + +std::expected handleHScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = currentTimestamp; + std::ignore = customFonts; + + 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 ownImage = ConstMemoryOneBitBuffer{*imageData, *contentWidth, *height}; + ClippedImage target{screen, *x, *y, *width, *height}; + + if (*contentWidth == 0) { + return {}; + } + + bool restarting = ((*flags) & 0x01) == 0x00; + bool invert = ((*flags) & 0x02) == 0x02; + bool padBefore = ((*flags) & 0x04) == 0x04; + bool padAfter = ((*flags) & 0x08) == 0x08; + + if (!padBefore && !padAfter && *contentWidth < *width) { + std::int16_t offset = invert ? (*width - *contentWidth) : 0; + for (std::size_t dy = 0; dy < *height; ++dy) { + for (std::size_t dx = 0; dx < *contentWidth; ++dx) { + target.setPixel(dx + offset, dy, ownImage.isPixelSet(dx, dy)); + } + } + return {}; + } + + std::int32_t totalSize = *contentWidth; + if (padBefore) { + totalSize += *width; + } + if (padAfter) { + totalSize += *width; + } + + // FIXME: handle overflow here... + std::int32_t offset = static_cast(animationTick * (*scrollSpeed + 1) / 8) % totalSize; + if (invert) { + offset = totalSize - offset - 1; + } + if (restarting) { + offset = std::min(offset, totalSize - *width); + } + + std::int32_t partOffset = 0; + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *contentWidth; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy)); + } + partOffset += *contentWidth; + } + if (padAfter) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + if (!restarting) { + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *contentWidth; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, ownImage.isPixelSet(dx, dy)); + } + } + partOffset += *contentWidth; + } + + return {}; +} + +std::expected handleVScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = currentTimestamp; + std::ignore = customFonts; + + 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 ownImage = ConstMemoryOneBitBuffer{*imageData, *width, *contentHeight}; + ClippedImage target{screen, *x, *y, *width, *height}; + + if (*contentHeight == 0) { + return {}; + } + + bool restarting = ((*flags) & 0x01) == 0x00; + bool invert = ((*flags) & 0x02) == 0x02; + bool padBefore = ((*flags) & 0x04) == 0x04; + bool padAfter = ((*flags) & 0x08) == 0x08; + + if (!padBefore && !padAfter && *contentHeight < *height) { + std::int16_t offset = invert ? (*height - *contentHeight) : 0; + for (std::size_t dy = 0; dy < *contentHeight; ++dy) { + for (std::size_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, dy + offset, ownImage.isPixelSet(dx, dy)); + } + } + return {}; + } + + std::int32_t totalSize = *contentHeight; + if (padBefore) { + totalSize += *height; + } + if (padAfter) { + totalSize += *height; + } + + // FIXME: handle overflow here... + std::int32_t offset = static_cast(animationTick * (*scrollSpeed + 1) / 8) % totalSize; + if (invert) { + offset = totalSize - offset - 1; + } + if (restarting) { + offset = std::min(offset, totalSize - *height); + } + + std::int32_t partOffset = 0; + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + std::int32_t ddy = dy + partOffset; + if ((ddy - offset) < 0) { + continue; + } + for (std::int32_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, ddy - offset, false); + } + } + partOffset += *height; + } + for (std::int32_t dy = 0; dy < *contentHeight; ++dy) { + std::int32_t ddy = dy + partOffset; + if ((ddy - offset) < 0) { + continue; + } + for (std::int32_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy)); + } + partOffset += *contentHeight; + } + if (padAfter) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + std::int32_t ddy = dy + partOffset; + if ((ddy - offset) < 0) { + continue; + } + for (std::int32_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, ddy - offset, false); + } + } + partOffset += *height; + } + if (!restarting) { + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + std::int32_t ddy = dy + partOffset; + if ((ddy - offset) < 0) { + continue; + } + for (std::int32_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, ddy - offset, false); + } + } + partOffset += *height; + } + for (std::int32_t dy = 0; dy < *contentHeight; ++dy) { + std::int32_t ddy = dy + partOffset; + if ((ddy - offset) < 0) { + continue; + } + for (std::int32_t dx = 0; dx < *width; ++dx) { + target.setPixel(dx, ddy - offset, ownImage.isPixelSet(dx, dy)); + } + } + partOffset += *contentHeight; + } + + return {}; +} + +std::expected handleLine(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = animationTick; + std::ignore = currentTimestamp; + std::ignore = customFonts; + + 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()); + } + + std::int32_t dx = static_cast(*targetX) - static_cast(*originX); + std::int32_t dy = static_cast(*targetY) - static_cast(*originY); + bool value = true; + + if (dx == 0) { + for (std::int32_t i = 0; i <= dy; ++i) { + screen->setPixel(*originX, *originY + i, value); + } + } else if (dy == 0) { + for (std::int32_t i = 0; i <= dx; ++i) { + screen->setPixel(*originX + i, *originY, value); + } + } else { + for (std::int32_t i = 0; i <= dx; ++i) { + std::int32_t x = *originX + i; + std::int32_t y = i * dy / dx + *originY; + screen->setPixel(static_cast(x), static_cast(y), value); + } + } + + return {}; +} + +std::expected handleClippedText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = animationTick; + std::ignore = currentTimestamp; + + 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()); + } + 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}; + + std::span fontData; + if ((*fontIndex) & 0x8000u) { + fontIndex = (*fontIndex) & ~0x8000u; + if (*fontIndex >= customFonts.size()) { + return {}; + } + fontData = customFonts[*fontIndex]; + } else { + fontData = findEmbeddedFont(*fontIndex); + } + if (fontData.size() < 23) { + return {}; + } + ClippedImage target{screen, *x, *y, *width, *height}; + auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); + renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, true, false); + + return {}; +} + +std::expected handleHScrollText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = currentTimestamp; + + 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()); + } + auto flags = readU8LE(buffer); + if (!flags) { + return std::unexpected(flags.error()); + } + auto scrollSpeed = readU8LE(buffer); + if (!scrollSpeed) { + return std::unexpected(scrollSpeed.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}; + + std::span fontData; + if ((*fontIndex) & 0x8000u) { + fontIndex = (*fontIndex) & ~0x8000u; + if (*fontIndex >= customFonts.size()) { + return {}; + } + fontData = customFonts[*fontIndex]; + } else { + fontData = findEmbeddedFont(*fontIndex); + } + if (fontData.size() < 23) { + return {}; + } + ClippedImage target{screen, *x, *y, *width, *height}; + auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); + auto dimensions = renderer.getRenderedDimensions(text, {0, static_cast(renderer.lineHeight() - 1)}); + if (!dimensions || !dimensions->boundingBox) { + return {}; + } + + std::uint16_t contentWidth = dimensions->boundingBox->size.width; + + if (contentWidth == 0) { + return {}; + } + + bool restarting = ((*flags) & 0x01) == 0x00; + bool invert = ((*flags) & 0x02) == 0x02; + bool padBefore = ((*flags) & 0x04) == 0x04; + bool padAfter = ((*flags) & 0x08) == 0x08; + + if (!padBefore && !padAfter && contentWidth < *width) { + std::uint16_t offset = invert ? (*width - contentWidth) : 0; + renderer.render(text, Point{offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + return {}; + } + + std::int32_t totalSize = contentWidth; + if (padBefore) { + totalSize += *width; + } + if (padAfter) { + totalSize += *width; + } + + // FIXME: handle overflow here... + std::int32_t offset = static_cast(animationTick * (*scrollSpeed + 1) / 8) % totalSize; + + if (invert) { + offset = totalSize - offset - 1; + } + if (restarting) { + offset = std::min(offset, totalSize - *width); + } + + std::int32_t partOffset = 0; + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + partOffset += contentWidth; + if (padAfter) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + if (!restarting) { + if (padBefore) { + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, false); + } + } + partOffset += *width; + } + renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + } + + return {}; +} + +std::expected handleCurrentTime(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts) { + std::ignore = animationTick; + + 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()); + } + 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()); + } + + std::span fontData; + if ((*fontIndex) & 0x8000u) { + fontIndex = (*fontIndex) & ~0x8000u; + if (*fontIndex >= customFonts.size()) { + return {}; + } + fontData = customFonts[*fontIndex]; + } else { + fontData = findEmbeddedFont(*fontIndex); + } + if (fontData.size() < 23) { + return {}; + } + ClippedImage target{screen, *x, *y, *width, *height}; + + bool use12h = ((*flags) & 0x01u) == 0x01u; + bool showHours = ((*flags) & 0x02u) == 0x02u; + bool showMinutes = ((*flags) & 0x04u) == 0x04u; + bool showSeconds = ((*flags) & 0x08u) == 0x08u; + if (showHours && showSeconds) { + showMinutes = true; + } + + auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); + std::uint32_t seconds = (currentTimestamp + static_cast(*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, true, false); + + return {}; +} + +} // namespace monoformat + diff --git a/cpp/src/monoformat_parseonly.hpp b/cpp/src/monoformat_parseonly.hpp new file mode 100644 index 0000000..56ee626 --- /dev/null +++ b/cpp/src/monoformat_parseonly.hpp @@ -0,0 +1,15 @@ +#ifndef MONOFORMAT_PARSEONLY_HPP +#define MONOFORMAT_PARSEONLY_HPP + +#include "monoformat_parsehelpers.hpp" +#include "monoformat_schema.hpp" + +namespace monoformat { + +std::expected parseAndApply(std::span data, OneBitBufferInterface* buffer, + std::uint16_t screenWidth, std::uint16_t screenHeight, + bool isFront, std::size_t animationTick, std::int64_t currentTimestamp); + +} // namespace monoformat + +#endif // MONOFORMAT_PARSEONLY_HPP diff --git a/cpp/src/monoformat_schema.cpp b/cpp/src/monoformat_schema.cpp new file mode 100644 index 0000000..d1f6f42 --- /dev/null +++ b/cpp/src/monoformat_schema.cpp @@ -0,0 +1,103 @@ +#include "monoformat_schema.hpp" + +#include + +namespace monoformat { + +MemoryOneBitBuffer::MemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height) + : m_width{width} + , m_height{height} + , m_buffer{buffer} +{ + std::size_t expectedSize = (m_width * m_height + 8 - 1) / 8; + if (m_buffer.size() < expectedSize) { + m_width = 0; + m_height = 0; + } +} + +MemoryOneBitBuffer::~MemoryOneBitBuffer() { +} + +bool MemoryOneBitBuffer::isPixelSet(std::uint16_t x, std::uint16_t y) const { + if (x >= m_width || y >= m_height) { + return false; + } + std::size_t pxIndex = static_cast(y) * m_width + x; + std::size_t byteIndex = pxIndex / 8; + std::size_t bitIndex = pxIndex % 8; + return ((static_cast(m_buffer[byteIndex]) >> bitIndex) & 0x01) == 0x01; +} + +void MemoryOneBitBuffer::setPixel(std::uint16_t x, std::uint16_t y, bool value) { + if (x >= m_width || y >= m_height) { + return; + } + std::size_t pxIndex = static_cast(y) * m_width + x; + std::size_t byteIndex = pxIndex / 8; + std::size_t bitIndex = pxIndex % 8; + if (value) { + m_buffer[byteIndex] = static_cast(static_cast(m_buffer[byteIndex]) | (1u << bitIndex)); + } else { + m_buffer[byteIndex] = static_cast(static_cast(m_buffer[byteIndex]) & ~(1u << bitIndex)); + } +} + +ConstMemoryOneBitBuffer::ConstMemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height) + : m_width{width} + , m_height{height} + , m_buffer{buffer} +{ + std::size_t expectedSize = (m_width * m_height + 8 - 1) / 8; + if (m_buffer.size() < expectedSize) { + m_width = 0; + m_height = 0; + } +} + +ConstMemoryOneBitBuffer::~ConstMemoryOneBitBuffer() { +} + +bool ConstMemoryOneBitBuffer::isPixelSet(std::uint16_t x, std::uint16_t y) const { + if (x >= m_width || y >= m_height) { + return false; + } + std::size_t pxIndex = static_cast(y) * m_width + x; + std::size_t byteIndex = pxIndex / 8; + std::size_t bitIndex = pxIndex % 8; + return ((static_cast(m_buffer[byteIndex]) >> bitIndex) & 0x01) == 0x01; +} + +void ConstMemoryOneBitBuffer::setPixel(std::uint16_t x, std::uint16_t y, bool value) { + std::ignore = x; + std::ignore = y; + std::ignore = value; +} + +ClippedImage::ClippedImage(OneBitBufferInterface* underlying, std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) + : m_underlying{underlying} + , m_x{x} + , m_y{y} + , m_width{width} + , m_height{height} +{ +} + +ClippedImage::~ClippedImage() { +} + +bool ClippedImage::isPixelSet(std::uint16_t x, std::uint16_t y) const { + if (x >= m_width || y >= m_height) { + return false; + } + return m_underlying->isPixelSet(x + m_x, y + m_y); +} + +void ClippedImage::setPixel(std::uint16_t x, std::uint16_t y, bool value) { + if (x >= m_width || y >= m_height) { + return; + } + m_underlying->setPixel(x + m_x, y + m_y, value); +} + +} // namespace monoformat diff --git a/cpp/src/monoformat_schema.hpp b/cpp/src/monoformat_schema.hpp new file mode 100644 index 0000000..42aba69 --- /dev/null +++ b/cpp/src/monoformat_schema.hpp @@ -0,0 +1,77 @@ +#ifndef MONOFORMAT_SCHEMA_HPP +#define MONOFORMAT_SCHEMA_HPP + +#include +#include +#include + +namespace monoformat { + +enum class SectionType { + AlwaysDrawn = 1, + TimeBasedDrawn = 2, + CustomFont = 32, +}; + +enum class ElementType { + Image = 1, + Animation = 2, + HScrollImage = 3, + VScrollImage = 4, + Line = 5, + ClippedText = 16, + HScrollText = 17, + //VScrollText = 18, + CurrentTime = 32, +}; + +enum class LineStyle : std::uint8_t { + Solid = 0, +}; + +struct OneBitBufferInterface { + using Color = bool; + + virtual ~OneBitBufferInterface() = default; + virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const = 0; + virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) = 0; +}; + +struct MemoryOneBitBuffer : public OneBitBufferInterface { + MemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height); + virtual ~MemoryOneBitBuffer() override; + virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const override; + virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) override; +private: + std::uint16_t m_width{}; + std::uint16_t m_height{}; + std::span m_buffer; +}; + +struct ConstMemoryOneBitBuffer : public OneBitBufferInterface { + ConstMemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height); + virtual ~ConstMemoryOneBitBuffer() override; + virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const override; + virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) override; +private: + std::uint16_t m_width{}; + std::uint16_t m_height{}; + std::span m_buffer; +}; + +struct ClippedImage : public OneBitBufferInterface { + ClippedImage(OneBitBufferInterface* underlying, std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height); + virtual ~ClippedImage() override; + virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const override; + virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) override; +private: + OneBitBufferInterface* m_underlying{}; + std::uint16_t m_x{}; + std::uint16_t m_y{}; + std::uint16_t m_width{}; + std::uint16_t m_height{}; +}; + +} // namespace monoformat + +#endif // MONOFORMAT_SCHEMA_HPP diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 99bd689..c56a765 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -1,76 +1,9 @@ #include "monoformat_structured.hpp" +#include "monoformat_fontreader.hpp" -namespace monoformat { - -MemoryOneBitBuffer::MemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height) - : m_width{width} - , m_height{height} - , m_buffer{buffer} -{ - std::size_t expectedSize = (m_width * m_height + 8 - 1) / 8; - if (m_buffer.size() < expectedSize) { - m_width = 0; - m_height = 0; - } -} - -MemoryOneBitBuffer::~MemoryOneBitBuffer() { -} - -bool MemoryOneBitBuffer::isPixelSet(std::uint16_t x, std::uint16_t y) const { - if (x >= m_width || y >= m_height) { - return false; - } - std::size_t pxIndex = static_cast(y) * m_width + x; - std::size_t byteIndex = pxIndex / 8; - std::size_t bitIndex = pxIndex % 8; - return ((static_cast(m_buffer[byteIndex]) >> bitIndex) & 0x01) == 0x01; -} - -void MemoryOneBitBuffer::setPixel(std::uint16_t x, std::uint16_t y, bool value) { - if (x >= m_width || y >= m_height) { - return; - } - std::size_t pxIndex = static_cast(y) * m_width + x; - std::size_t byteIndex = pxIndex / 8; - std::size_t bitIndex = pxIndex % 8; - if (value) { - m_buffer[byteIndex] = static_cast(static_cast(m_buffer[byteIndex]) | (1u << bitIndex)); - } else { - m_buffer[byteIndex] = static_cast(static_cast(m_buffer[byteIndex]) & ~(1u << bitIndex)); - } -} - -ConstMemoryOneBitBuffer::ConstMemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height) - : m_width{width} - , m_height{height} - , m_buffer{buffer} -{ - std::size_t expectedSize = (m_width * m_height + 8 - 1) / 8; - if (m_buffer.size() < expectedSize) { - m_width = 0; - m_height = 0; - } -} - -ConstMemoryOneBitBuffer::~ConstMemoryOneBitBuffer() { -} +#include -bool ConstMemoryOneBitBuffer::isPixelSet(std::uint16_t x, std::uint16_t y) const { - if (x >= m_width || y >= m_height) { - return false; - } - std::size_t pxIndex = static_cast(y) * m_width + x; - std::size_t byteIndex = pxIndex / 8; - std::size_t bitIndex = pxIndex % 8; - return ((static_cast(m_buffer[byteIndex]) >> bitIndex) & 0x01) == 0x01; -} - -void ConstMemoryOneBitBuffer::setPixel(std::uint16_t x, std::uint16_t y, bool value) { - std::ignore = x; - std::ignore = y; - std::ignore = value; -} +namespace monoformat { ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) : m_x{x} @@ -114,6 +47,12 @@ 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() { } @@ -122,15 +61,70 @@ ElementType ImageElement::elementType() const { } std::size_t ImageElement::serializeTo(std::span target) const { - // FIXME: serialize + 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); } -void ImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: draw +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) { - // FIXME: parse + 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; } 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) @@ -141,7 +135,7 @@ AnimationElement::AnimationElement(std::uint16_t x, std::uint16_t y, std::uint16 , m_numberOfFrames{numberOfFrames} , m_updateInterval{updateInterval} { - std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8;\ + std::size_t const imageBufferSize = (m_width * m_height + 8 - 1) / 8; m_buffer.resize(imageBufferSize * numberOfFrames); } @@ -201,6 +195,12 @@ ConstMemoryOneBitBuffer AnimationElement::image(std::uint16_t frameIndex) const 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() { } @@ -209,15 +209,1671 @@ ElementType AnimationElement::elementType() const { } std::size_t AnimationElement::serializeTo(std::span target) const { - // FIXME: implement this + 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); } -void AnimationElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: implement this +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) { - // FIXME: implement this + 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; +} + +HScrollImageElement::HScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentWidth, std::uint8_t 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; +} + +std::uint8_t 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::size_t HScrollImageElement::serializeTo(std::span target) const { + 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, m_flags); + pos = writeU8LE(target, pos, m_scrollSpeed); + pos = writeU16LE(target, pos, 0); + pos = writeBuffer(target, pos, m_buffer); + return alignNextWrite(target, pos, 4); +} + +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 & 0x01) == 0x00; + bool invert = (m_flags & 0x02) == 0x02; + bool padBefore = (m_flags & 0x04) == 0x04; + bool padAfter = (m_flags & 0x08) == 0x08; + + 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) { + 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, *flags, *scrollSpeed); + result->updateBuffer(*imageData); + return result; +} + +VScrollImageElement::VScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentHeight, std::uint8_t 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; +} + +std::uint8_t 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::size_t VScrollImageElement::serializeTo(std::span target) const { + 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, m_flags); + pos = writeU8LE(target, pos, m_scrollSpeed); + pos = writeU16LE(target, pos, 0); + pos = writeBuffer(target, pos, m_buffer); + return alignNextWrite(target, pos, 4); +} + +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 & 0x01) == 0x00; + bool invert = (m_flags & 0x02) == 0x02; + bool padBefore = (m_flags & 0x04) == 0x04; + bool padAfter = (m_flags & 0x08) == 0x08; + + 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) { + 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, *flags, *scrollSpeed); + result->updateBuffer(*imageData); + return result; +} + + +LineElement::LineElement(std::uint16_t originX, std::uint16_t originY, std::uint16_t targetX, std::uint16_t targetY, LineStyle lineStyle, std::uint8_t 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; +} + +std::uint8_t LineElement::flags() const noexcept { + return m_flags; +} + +LineElement::~LineElement() { +} + +ElementType LineElement::elementType() const { + return ElementType::Line; +} + +std::size_t LineElement::serializeTo(std::span target) const { + 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, m_flags); + return alignNextWrite(target, pos, 4); +} + +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) { + 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), *flags); + return result; +} + +ClippedTextElement::ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t fontIndex, std::string text) + : m_x{x} + , m_y{y} + , m_width{width} + , m_height{height} + , 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; +} + +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::size_t ClippedTextElement::serializeTo(std::span target) 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); + 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); +} + +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}; + + auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); + renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, true, false); +} + +std::expected, ParseError> ClippedTextElement::parse(std::span& buffer) { + 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()); + } + 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, *fontIndex, std::string{text}); + return result; +} + +HScrollTextElement::HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint8_t 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_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; +} + +std::uint8_t 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::size_t HScrollTextElement::serializeTo(std::span target) 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); + pos = writeU8LE(target, pos, m_flags); + pos = writeU8LE(target, pos, m_scrollSpeed); + 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); +} + +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; + } + + bool restarting = (m_flags & 0x01) == 0x00; + bool invert = (m_flags & 0x02) == 0x02; + bool padBefore = (m_flags & 0x04) == 0x04; + bool padAfter = (m_flags & 0x08) == 0x08; + + 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, true, false); + 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) { + 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; + } + renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + partOffset += 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; + } + renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + } +} + +std::expected, ParseError> HScrollTextElement::parse(std::span& buffer) { + 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()); + } + auto flags = readU8LE(buffer); + if (!flags) { + return std::unexpected(flags.error()); + } + auto scrollSpeed = readU8LE(buffer); + if (!scrollSpeed) { + return std::unexpected(scrollSpeed.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, *flags, *scrollSpeed, *fontIndex, std::string{text}); + return result; +} + +CurrentTimeElement::CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t fontIndex, std::uint16_t utcOffset, std::uint16_t flags) + : m_x{x} + , m_y{y} + , m_width{width} + , m_height{height} + , 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; +} + +std::uint16_t CurrentTimeElement::fontIndex() const noexcept { + return m_fontIndex; +} + +std::uint16_t CurrentTimeElement::utcOffset() const noexcept { + return m_utcOffset; +} + +std::uint16_t CurrentTimeElement::flags() const noexcept { + return m_flags; +} + +CurrentTimeElement::~CurrentTimeElement() { +} + +ElementType CurrentTimeElement::elementType() const { + return ElementType::CurrentTime; +} + +std::size_t CurrentTimeElement::serializeTo(std::span target) 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); + pos = writeU16LE(target, pos, m_fontIndex); + pos = writeU16LE(target, pos, m_utcOffset); + pos = writeU16LE(target, pos, m_flags); + return alignNextWrite(target, pos, 4); +} + +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 & 0x01u) == 0x01u; + bool showHours = (m_flags & 0x02u) == 0x02u; + bool showMinutes = (m_flags & 0x04u) == 0x04u; + bool showSeconds = (m_flags & 0x08u) == 0x08u; + if (showHours && showSeconds) { + showMinutes = true; + } + + 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, true, false); +} + +std::expected, ParseError> CurrentTimeElement::parse(std::span& buffer) { + 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()); + } + 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()); + } + auto result = std::make_unique(*x, *y, *width, *height, *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::size_t AlwaysDrawnSection::serializeTo(std::span target) 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; + if (m_drawOnFront) { + flags |= std::uint16_t{1}; + } + if (m_drawOnBack) { + flags |= std::uint16_t{2}; + } + if (m_clearBeforeDrawing) { + flags |= std::uint16_t{4}; + } + 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)); + } else { + pos += element->serializeTo({}); + } + } + pos = alignNextWrite(target, pos, 4); + std::ignore = writeU24LE(target, 1, pos); + return pos; +} + +std::expected, ParseError> AlwaysDrawnSection::parse(std::span& buffer) { + 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((((*flags) >> 0u) & 0x01u) == 0x01u); + section->setDrawOnBack((((*flags) >> 1u) & 0x01u) == 0x01u); + section->setClearBeforeDrawing((((*flags) >> 2u) & 0x01u) == 0x01u); + 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); break; + case ElementType::Animation: element = AnimationElement::parse(sectionData); break; + case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData); break; + case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData); break; + case ElementType::Line: element = LineElement::parse(sectionData); break; + case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData); break; + case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData); break; + case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData); break; + default: + return std::unexpected(ParseError::InvalidValue); + } + if (!element) { + return std::unexpected(element.error()); + } + 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::size_t TimeBasedDrawnSection::serializeTo(std::span target) 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; + if (m_drawOnFront) { + flags |= std::uint16_t{1}; + } + if (m_drawOnBack) { + flags |= std::uint16_t{2}; + } + if (m_clearBeforeDrawing) { + flags |= std::uint16_t{4}; + } + 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)); + } else { + pos += element->serializeTo({}); + } + } + pos = alignNextWrite(target, pos, 4); + std::ignore = writeU24LE(target, 1, pos); + return pos; +} + +std::expected, ParseError> TimeBasedDrawnSection::parse(std::span& buffer) { + 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((((*flags) >> 0u) & 0x01u) == 0x01u); + section->setDrawOnBack((((*flags) >> 1u) & 0x01u) == 0x01u); + section->setClearBeforeDrawing((((*flags) >> 2u) & 0x01u) == 0x01u); + 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); break; + case ElementType::Animation: element = AnimationElement::parse(sectionData); break; + case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData); break; + case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData); break; + case ElementType::Line: element = LineElement::parse(sectionData); break; + case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData); break; + case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData); break; + case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData); break; + default: + return std::unexpected(ParseError::InvalidValue); + } + if (!element) { + return std::unexpected(element.error()); + } + section->m_elements.push_back(std::move(*element)); + } + 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::size_t CustomFontSection::serializeTo(std::span target) const { + std::size_t pos = 0; + pos = writeU8LE(target, pos, static_cast(SectionType::AlwaysDrawn)); + // 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; +} + +std::expected, ParseError> CustomFontSection::parse(std::span& buffer) { + 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::size_t serializeFile(std::span& buffer, File const& file) { + std::size_t pos = 0; + pos = writeU8LE(buffer, pos, 0xAF); + pos = writeU8LE(buffer, pos, 0x7E); + pos = writeU8LE(buffer, pos, 0x2B); + pos = writeU8LE(buffer, pos, 0x63); + pos = writeU32LE(buffer, pos, 1); + 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)); + } else { + pos += section->serializeTo({}); + } + } + return pos; +} + +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) { + 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); break; + case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parse(data); break; + case SectionType::CustomFont: section = CustomFontSection::parse(data); break; + default: return std::unexpected(ParseError::InvalidValue); + } + + if (!section) { + return std::unexpected(section.error()); + } + + result.sections.push_back(std::move(*section)); + } + + return result; } } // namespace monoformat diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index 400ea43..7d593be 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -2,6 +2,7 @@ #define MONOFORMAT_STRUCTURED_HPP #include "monoformat_parsehelpers.hpp" +#include "monoformat_schema.hpp" #include #include @@ -11,12 +12,6 @@ namespace monoformat { -enum class SectionType { - AlwaysDrawn = 1, - TimeBasedDrawn = 2, - CustomFont = 32, -}; - struct Section { virtual ~Section() = default; virtual SectionType sectionType() const = 0; @@ -26,58 +21,18 @@ protected: Section() = default; }; -enum class ElementType { - Image = 1, - Animation = 2, - HScrollImage = 3, - VScrollImage = 4, - Line = 5, - ClippedText = 16, - HScrollText = 17, - //VScrollText = 18, - CurrentTime = 32, -}; - -struct OneBitBufferInterface { - using Color = bool; - - virtual ~OneBitBufferInterface() = default; - virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const = 0; - virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) = 0; -}; +class CustomFontSection; struct Element { virtual ~Element() = default; virtual ElementType elementType() const = 0; virtual std::size_t serializeTo(std::span target) const = 0; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) = 0; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) = 0; protected: Element() = default; }; -struct MemoryOneBitBuffer : public OneBitBufferInterface { - MemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height); - virtual ~MemoryOneBitBuffer() override; - virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const override; - virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) override; -private: - std::uint16_t m_width{}; - std::uint16_t m_height{}; - std::span m_buffer; -}; - -struct ConstMemoryOneBitBuffer : public OneBitBufferInterface { - ConstMemoryOneBitBuffer(std::span buffer, std::uint16_t width, std::uint16_t height); - virtual ~ConstMemoryOneBitBuffer() override; - virtual bool isPixelSet(std::uint16_t x, std::uint16_t y) const override; - virtual void setPixel(std::uint16_t x, std::uint16_t y, bool value) override; -private: - std::uint16_t m_width{}; - std::uint16_t m_height{}; - std::span m_buffer; -}; - struct ImageElement : public Element { ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height); @@ -89,11 +44,12 @@ struct ImageElement : public Element { std::span buffer() const noexcept; MemoryOneBitBuffer image() noexcept; ConstMemoryOneBitBuffer image() const noexcept; + void updateBuffer(std::span newBufferData); virtual ~ImageElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -120,11 +76,12 @@ struct AnimationElement : public Element { std::span buffer(std::uint16_t frameIndex) const noexcept; MemoryOneBitBuffer image(std::uint16_t frameIndex) noexcept; ConstMemoryOneBitBuffer image(std::uint16_t frameIndex) const noexcept; + void updateBuffer(std::span newBufferData); virtual ~AnimationElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -152,11 +109,12 @@ struct HScrollImageElement : public Element { std::span buffer() const noexcept; MemoryOneBitBuffer image() noexcept; ConstMemoryOneBitBuffer image() const noexcept; + void updateBuffer(std::span newBufferData); virtual ~HScrollImageElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -185,11 +143,12 @@ struct VScrollImageElement : public Element { std::span buffer() const noexcept; MemoryOneBitBuffer image() noexcept; ConstMemoryOneBitBuffer image() const noexcept; + void updateBuffer(std::span newBufferData); virtual ~VScrollImageElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -204,10 +163,6 @@ private: std::vector m_buffer; }; -enum class LineStyle : std::uint8_t { - Solid = 0, -}; - struct LineElement : public Element { LineElement(std::uint16_t originX, std::uint16_t originY, std::uint16_t targetX, std::uint16_t targetY, LineStyle lineStyle, std::uint8_t flags); @@ -221,7 +176,7 @@ struct LineElement : public Element { virtual ~LineElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -247,7 +202,7 @@ struct ClippedTextElement : public Element { virtual ~ClippedTextElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -275,7 +230,7 @@ struct HScrollTextElement : public Element { virtual ~HScrollTextElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -304,7 +259,7 @@ struct CurrentTimeElement : public Element { virtual ~CurrentTimeElement() override; virtual ElementType elementType() const override; virtual std::size_t serializeTo(std::span target) const override; - virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) override; + virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) override; static std::expected, ParseError> parse(std::span& buffer); @@ -331,7 +286,7 @@ struct AlwaysDrawnSection : public Section { void setClearBeforeDrawing(bool value) noexcept; std::size_t elementCount() const noexcept; - std::unique_ptr elementAt(std::size_t index) const; + Element* elementAt(std::size_t index) const; void appendElement(std::unique_ptr element); void insertElement(std::size_t index, std::unique_ptr element); @@ -363,17 +318,17 @@ struct TimeBasedDrawnSection : public Section { void setClearBeforeDrawing(bool value) noexcept; std::size_t elementCount() const noexcept; - std::unique_ptr elementAt(std::size_t index) const; + Element* elementAt(std::size_t index) const; void appendElement(std::unique_ptr element); void insertElement(std::size_t index, std::unique_ptr element); std::unique_ptr replaceElement(std::size_t index, std::unique_ptr element); std::unique_ptr eraseElement(std::size_t index); - std::uint64_t startTimestamp() const noexcept; - std::uint64_t endTimestamp() const noexcept; - void setStartTimestamp(std::uint64_t value); - void setEndTimestamp(std::uint64_t value); + std::int64_t startTimestamp() const noexcept; + std::int64_t endTimestamp() const noexcept; + void setStartTimestamp(std::int64_t value); + void setEndTimestamp(std::int64_t value); virtual SectionType sectionType() const override; virtual std::size_t serializeTo(std::span target) const override; @@ -382,8 +337,8 @@ struct TimeBasedDrawnSection : public Section { private: std::vector> m_elements; - std::uint64_t m_startTimestamp{}; - std::uint64_t m_endTimestamp{}; + std::int64_t m_startTimestamp{}; + std::int64_t m_endTimestamp{}; bool m_drawOnFront{true}; bool m_drawOnBack{true}; bool m_clearBeforeDrawing{true}; @@ -393,6 +348,9 @@ struct CustomFontSection : public Section { CustomFontSection() = default; virtual ~CustomFontSection() override; + std::span fontData() const; + void setFontData(std::span fontData); + virtual SectionType sectionType() const override; virtual std::size_t serializeTo(std::span target) const override; @@ -406,8 +364,8 @@ struct File { std::vector> sections; }; +extern std::size_t serializeFile(std::span& buffer, File const& file); extern std::expected parseFile(std::span data); -extern std::size_t serializeFile(std::span buffer); } // namespace monoformat diff --git a/cpp/src/monoformat_utf8.hpp b/cpp/src/monoformat_utf8.hpp index 25eff58..2ceb11d 100644 --- a/cpp/src/monoformat_utf8.hpp +++ b/cpp/src/monoformat_utf8.hpp @@ -200,7 +200,7 @@ inline UTF8Iterator begin(UTF8Iterator const& a) noexcept { return a; } -inline UTF8Iterator end(UTF8Iterator const& b) noexcept { +inline UTF8Iterator end(UTF8Iterator const&) noexcept { return UTF8Iterator{}; } diff --git a/php/monoformat.php b/php/monoformat.php new file mode 100644 index 0000000..777ed64 --- /dev/null +++ b/php/monoformat.php @@ -0,0 +1,201 @@ +text); + $result = pack("vvvvvCCvv", + 17, + $this->x, + $this->y, + $this->width, + $this->height, + $this->flags, + $this->scrollSpeed, + $this->fontIndex, + $len); + $result .= (string) $this->text; + if ($len % 4 != 0) { + $n = 4 - ($len % 4); + for ($i = 0; $i < $n; ++$i) { + $result .= chr(0); + } + } + return $result; + } +} + +class CurrentTimeElement extends Element { + public $x = 0; + public $y = 0; + public $width = 0; + public $height = 0; + public $fontIndex = 0; + public $utcOffset = 0; + public $flags = 0; + + public function serialize() { + $result = pack("vvvvvvvv", + 32, + $this->x, + $this->y, + $this->width, + $this->height, + $this->fontIndex, + $this->utcOffset, + $this->flags); + return $result; + } +} + +class Section { + public function serialize() { + return ""; + } +} + +class AlwaysDrawnSection extends Section { + public $drawOnFront = false; + public $drawOnBack = false; + public $clearBeforeDrawing = false; + public $elements = []; + + public function serialize() { + $flags = 0; + if ($this->drawOnFront) { + $flags |= 0x01; + } + if ($this->drawOnBack) { + $flags |= 0x02; + } + if ($this->clearBeforeDrawing) { + $flags |= 0x04; + } + $inner = pack("vv", $flags, count($this->elements)); + foreach ($this->elements as $element) { + $inner .= $element->serialize(); + } + $len = strlen($inner) + 4; + $len = substr(pack("V", $len), 0, 3); + return pack("C", 1) . $len . $inner; + } +} + +class File { + public $sections = []; + + public function serialize() { + $nSections = count($this->sections); + $result = "\xAF\x7E\x2B\x63"; + $result .= pack("Vvv", 1, $nSections, 0); + foreach ($this->sections as $section) { + $result .= $section->serialize(); + } + return $result; + } +} + +} // namespace monoformat + +namespace { + +$roomName= "BOOL"; + +$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true); +$talks = []; +$now = new DateTimeImmutable("now"); +foreach ($data["schedule"]["conference"]["days"] as $day) { + foreach ($day["rooms"][$roomName] as $t) { + [$h, $m] = explode(":", $t["duration"]); + $duration = $h * 3600 + $m * 60; + $duration = DateInterval::createFromDateString("$duration sec"); + $talk = [ + "date" => new DateTimeImmutable($t["date"]), + "duration" => $duration, + "title" => $t["title"], + ]; + $talkEnd = $talk["date"]->add($talk["duration"]); + if ($talkEnd < $now) { + continue; + } + if ($talk["date"] > $now && count($talks) > 2) { + break; + } + array_push($talks, $talk); + } +} + +$elements = []; + +function putText($x, $y, $width, $height, $text) { + global $elements; + + $e = new monoformat\HScrollTextElement(); + $e->x = $x; + $e->y = $y; + $e->width = $width; + $e->height = $height; + $e->flags = 0; + $e->scrollSpeed = 15; + $e->fontIndex = 0; + $e->text = $text; + array_push($elements, $e); +} + +$i = 0; +foreach ($talks as $talk) { + putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i")); + putText(35, $i * 20, 85, 20, $talk["title"]); + ++$i; +} + +$section = new monoformat\AlwaysDrawnSection(); +$section->drawOnFront = true; +$section->drawOnBack = true; +$section->clearBeforeDrawing = true; +$section->elements = $elements; + +$file = new monoformat\File(); +array_push($file->sections, $section); + +print($file->serialize()); +} // global + +?>