From 87d36abe87c6d0fa68a1f73e9a89f3bd15862516 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Wed, 13 May 2026 19:50:41 +0200 Subject: [PATCH 1/9] Specification: fix id of custom font --- Specification.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Specification.rst b/Specification.rst index 1541275..460ebb9 100644 --- a/Specification.rst +++ b/Specification.rst @@ -389,7 +389,7 @@ Current Time 0 1 2 3 +------------------------+------------------------+------------------------+------------------------+ - 0 | Type: 16 | X Offset | + 0 | Type: 32 | X Offset | +------------------------+------------------------+------------------------+------------------------+ 4 | Y Offset | Width | +------------------------+------------------------+------------------------+------------------------+ From 305c039d8392b135deb085516682d6527505e505 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Wed, 13 May 2026 19:50:50 +0200 Subject: [PATCH 2/9] C++: create all stub methods for structured analysis --- cpp/src/monoformat_structured.cpp | 592 +++++++++++++++++++++++++++++- cpp/src/monoformat_structured.hpp | 21 +- 2 files changed, 603 insertions(+), 10 deletions(-) diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 99bd689..940d435 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -141,7 +141,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); } @@ -220,4 +220,594 @@ std::expected, ParseError> AnimationElement::p // FIXME: implement this } +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}; +} + +HScrollImageElement::~HScrollImageElement() { +} + +ElementType HScrollImageElement::elementType() const { + return ElementType::HScrollImage; +} + +std::size_t HScrollImageElement::serializeTo(std::span target) const { + // FIXME Serialize +} + +void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME draw +} + +std::expected, ParseError> HScrollImageElement::parse(std::span& buffer) { + // FIXME parse +} + +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}; +} + +VScrollImageElement::~VScrollImageElement() { +} + +ElementType VScrollImageElement::elementType() const { + return ElementType::VScrollImage; +} + +std::size_t VScrollImageElement::serializeTo(std::span target) const { + // FIXME Serialize +} + +void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME draw +} + +std::expected, ParseError> VScrollImageElement::parse(std::span& buffer) { + // FIXME parse +} + + +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 { + // FIXME: serialize +} + +void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME: draw +} + +std::expected, ParseError> LineElement::parse(std::span& buffer) { + // FIXME: parse +} + +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 { + // FIXME: serialize +} + +void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME: draw +} + +std::expected, ParseError> ClippedTextElement::parse(std::span& buffer) { + // FIXME: parse +} + +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 { + // FIXME serialize +} + +void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME: draw +} + +std::expected, ParseError> HScrollTextElement::parse(std::span& buffer) { + // FIXME: parse +} + +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 { + // FIXME: serialize +} + +void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { + // FIXME: draw +} + +std::expected, ParseError> CurrentTimeElement::parse(std::span& buffer) { + // FIXME: current time +} + +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 { + // FIXME: serialize +} + +std::expected, ParseError> AlwaysDrawnSection::parse(std::span& buffer) { + // FIXME: parse +} + +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 { + // FIXME: serialize +} + +std::expected, ParseError> TimeBasedDrawnSection::parse(std::span& buffer) { + // FIXME: parse +} + +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 { + // FIXME: serailize +} + +std::expected, ParseError> CustomFontSection::parse(std::span& buffer) { + // FIXME: parse +} + +std::size_t serializeFile(std::span& buffer, File const& file) { + // FIXME: serialize +} + +std::expected parseFile(std::span data) { + // FIXME: parse +} + } // namespace monoformat diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index 400ea43..e3a6436 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -331,7 +331,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 +363,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 +382,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 +393,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 +409,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 From 328f338a6ca5f4992ea247a6573ce0d7eb99ae27 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Wed, 13 May 2026 20:17:00 +0200 Subject: [PATCH 3/9] Fix bug in specification (text length missing) --- Specification.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Specification.rst b/Specification.rst index 460ebb9..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) | +------------------------+------------------------+------------------------+------------------------+ From 51f60eb817d679773282b202a6632300e3ac6370 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Wed, 13 May 2026 20:20:23 +0200 Subject: [PATCH 4/9] C++/Structured: implement serialization functions --- cpp/src/monoformat_parsehelpers.hpp | 120 +++++++++++++++++++ cpp/src/monoformat_structured.cpp | 178 ++++++++++++++++++++++++++-- 2 files changed, 286 insertions(+), 12 deletions(-) diff --git a/cpp/src/monoformat_parsehelpers.hpp b/cpp/src/monoformat_parsehelpers.hpp index 503159c..9e2ca4c 100644 --- a/cpp/src/monoformat_parsehelpers.hpp +++ b/cpp/src/monoformat_parsehelpers.hpp @@ -5,6 +5,7 @@ #include #include #include +#include // for std::ignore namespace monoformat { @@ -228,6 +229,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 < value.size()) { + std::size_t const n = value.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_structured.cpp b/cpp/src/monoformat_structured.cpp index 940d435..0ac5475 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -122,7 +122,15 @@ 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) { @@ -209,7 +217,17 @@ 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) { @@ -285,7 +303,18 @@ ElementType HScrollImageElement::elementType() const { } std::size_t HScrollImageElement::serializeTo(std::span target) const { - // FIXME Serialize + 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) { @@ -361,7 +390,18 @@ ElementType VScrollImageElement::elementType() const { } std::size_t VScrollImageElement::serializeTo(std::span target) const { - // FIXME Serialize + 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) { @@ -415,7 +455,15 @@ ElementType LineElement::elementType() const { } std::size_t LineElement::serializeTo(std::span target) const { - // FIXME: serialize + 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) { @@ -468,7 +516,16 @@ ElementType ClippedTextElement::elementType() const { } std::size_t ClippedTextElement::serializeTo(std::span target) const { - // FIXME: serialize + 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_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) { @@ -531,7 +588,18 @@ ElementType HScrollTextElement::elementType() const { } std::size_t HScrollTextElement::serializeTo(std::span target) const { - // FIXME serialize + 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 = 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) { @@ -589,7 +657,16 @@ ElementType CurrentTimeElement::elementType() const { } std::size_t CurrentTimeElement::serializeTo(std::span target) const { - // FIXME: serialize + 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_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) { @@ -674,7 +751,32 @@ SectionType AlwaysDrawnSection::sectionType() const { } std::size_t AlwaysDrawnSection::serializeTo(std::span target) const { - // FIXME: serialize + 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) { @@ -771,7 +873,34 @@ SectionType TimeBasedDrawnSection::sectionType() const { } std::size_t TimeBasedDrawnSection::serializeTo(std::span target) const { - // FIXME: serialize + 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) { @@ -795,7 +924,16 @@ SectionType CustomFontSection::sectionType() const { } std::size_t CustomFontSection::serializeTo(std::span target) const { - // FIXME: serailize + 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) { @@ -803,7 +941,23 @@ std::expected, ParseError> CustomFontSection: } std::size_t serializeFile(std::span& buffer, File const& file) { - // FIXME: serialize + std::size_t pos = 0; + pos = writeU8LE(buffer, pos, 0xAF); + pos = writeU8LE(buffer, pos, 0x7E); + pos = writeU8LE(buffer, pos, 0x2B); + pos = writeU8LE(buffer, pos, 0x64); + 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) { From 41738b4a3b07f9a84b319fa48319e12d21136857 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Wed, 13 May 2026 21:02:19 +0200 Subject: [PATCH 5/9] C++/Structured: implement parsing logic --- cpp/src/monoformat_parsehelpers.hpp | 1 + cpp/src/monoformat_structured.cpp | 581 +++++++++++++++++++++++++++- cpp/src/monoformat_structured.hpp | 4 + 3 files changed, 574 insertions(+), 12 deletions(-) diff --git a/cpp/src/monoformat_parsehelpers.hpp b/cpp/src/monoformat_parsehelpers.hpp index 9e2ca4c..5cd269c 100644 --- a/cpp/src/monoformat_parsehelpers.hpp +++ b/cpp/src/monoformat_parsehelpers.hpp @@ -11,6 +11,7 @@ namespace monoformat { enum class ParseError { BufferSizeMismatch = 1, + InvalidValue = 2, }; inline std::expected peekU8LE(std::span buffer) { diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 0ac5475..511d3e5 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -114,6 +114,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() { } @@ -138,7 +144,46 @@ void ImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animat } 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) @@ -209,6 +254,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() { } @@ -235,7 +286,54 @@ void AnimationElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t an } 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) @@ -295,6 +393,12 @@ 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() { } @@ -322,7 +426,58 @@ void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } std::expected, ParseError> HScrollImageElement::parse(std::span& buffer) { - // FIXME parse + 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) @@ -382,6 +537,12 @@ 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() { } @@ -409,7 +570,58 @@ void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } std::expected, ParseError> VScrollImageElement::parse(std::span& buffer) { - // FIXME parse + 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; } @@ -471,7 +683,42 @@ void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animati } std::expected, ParseError> LineElement::parse(std::span& buffer) { - // FIXME: parse + 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) @@ -533,7 +780,45 @@ void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } std::expected, ParseError> ClippedTextElement::parse(std::span& buffer) { - // FIXME: parse + 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) @@ -607,7 +892,53 @@ void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } std::expected, ParseError> HScrollTextElement::parse(std::span& buffer) { - // FIXME: parse + 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) @@ -674,7 +1005,43 @@ void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } std::expected, ParseError> CurrentTimeElement::parse(std::span& buffer) { - // FIXME: current time + 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() { @@ -780,7 +1147,59 @@ std::size_t AlwaysDrawnSection::serializeTo(std::span target) const { } std::expected, ParseError> AlwaysDrawnSection::parse(std::span& buffer) { - // FIXME: parse + 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() { @@ -904,7 +1323,69 @@ std::size_t TimeBasedDrawnSection::serializeTo(std::span target) cons } std::expected, ParseError> TimeBasedDrawnSection::parse(std::span& buffer) { - // FIXME: parse + 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() { @@ -937,7 +1418,39 @@ std::size_t CustomFontSection::serializeTo(std::span target) const { } std::expected, ParseError> CustomFontSection::parse(std::span& buffer) { - // FIXME: parse + 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) { @@ -961,7 +1474,51 @@ std::size_t serializeFile(std::span& buffer, File const& file) { } std::expected parseFile(std::span data) { - // FIXME: parse + 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); + } + + 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 e3a6436..6ade4a9 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -89,6 +89,7 @@ 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; @@ -120,6 +121,7 @@ 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; @@ -152,6 +154,7 @@ 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; @@ -185,6 +188,7 @@ 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; From 9237b2b1ac79bd1730b12bf1ec0cb1212c5183f3 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 15 May 2026 08:36:15 +0200 Subject: [PATCH 6/9] C++: implement drawing code for all elements --- cpp/README.txt | 5 - cpp/src/monoformat_fontreader.hpp | 2 +- cpp/src/monoformat_parsehelpers.hpp | 2 +- cpp/src/monoformat_structured.cpp | 521 ++++++++++++++++++++++++++-- cpp/src/monoformat_structured.hpp | 33 +- cpp/src/monoformat_utf8.hpp | 2 +- 6 files changed, 522 insertions(+), 43 deletions(-) diff --git a/cpp/README.txt b/cpp/README.txt index b3978b6..18830af 100644 --- a/cpp/README.txt +++ b/cpp/README.txt @@ -2,11 +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 diff --git a/cpp/src/monoformat_fontreader.hpp b/cpp/src/monoformat_fontreader.hpp index a10c495..96e3233 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{}; diff --git a/cpp/src/monoformat_parsehelpers.hpp b/cpp/src/monoformat_parsehelpers.hpp index 5cd269c..5901595 100644 --- a/cpp/src/monoformat_parsehelpers.hpp +++ b/cpp/src/monoformat_parsehelpers.hpp @@ -123,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; } diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 511d3e5..0bd3f36 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -1,4 +1,7 @@ #include "monoformat_structured.hpp" +#include "monoformat_fontreader.hpp" + +#include namespace monoformat { @@ -72,6 +75,32 @@ void ConstMemoryOneBitBuffer::setPixel(std::uint16_t x, std::uint16_t y, bool va 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); +} + ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) : m_x{x} , m_y{y} @@ -139,8 +168,16 @@ std::size_t ImageElement::serializeTo(std::span target) const { 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) { @@ -281,8 +318,23 @@ std::size_t AnimationElement::serializeTo(std::span target) const { 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) { @@ -421,8 +473,108 @@ std::size_t HScrollImageElement::serializeTo(std::span target) const return alignNextWrite(target, pos, 4); } -void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME draw +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) { @@ -565,8 +717,108 @@ std::size_t VScrollImageElement::serializeTo(std::span target) const return alignNextWrite(target, pos, 4); } -void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME draw +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) { @@ -678,8 +930,30 @@ std::size_t LineElement::serializeTo(std::span target) const { return alignNextWrite(target, pos, 4); } -void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: draw +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) { @@ -775,8 +1049,34 @@ std::size_t ClippedTextElement::serializeTo(std::span target) const { return alignNextWrite(target, pos, 4); } -void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: draw +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 { + if (m_fontIndex == 0) { + fontData = findEmbeddedFont("NokiaSmallPlain_tf"); + } else if (m_fontIndex == 1) { + fontData = findEmbeddedFont("5x7_mf"); + } else { + return; + } + } + 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) { @@ -887,8 +1187,114 @@ std::size_t HScrollTextElement::serializeTo(std::span target) const { return alignNextWrite(target, pos, 4); } -void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: draw +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 { + if (m_fontIndex == 0) { + fontData = findEmbeddedFont("NokiaSmallPlain_tf"); + } else if (m_fontIndex == 1) { + fontData = findEmbeddedFont("5x7_mf"); + } else { + return; + } + } + 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) { @@ -1000,8 +1406,67 @@ std::size_t CurrentTimeElement::serializeTo(std::span target) const { return alignNextWrite(target, pos, 4); } -void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp) { - // FIXME: draw +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 { + if (m_fontIndex == 0) { + fontData = findEmbeddedFont("NokiaSmallPlain_tf"); + } else if (m_fontIndex == 1) { + fontData = findEmbeddedFont("5x7_mf"); + } else { + return; + } + } + 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) { @@ -1172,9 +1637,9 @@ std::expected, ParseError> AlwaysDrawnSectio 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->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); @@ -1356,9 +1821,9 @@ std::expected, ParseError> TimeBasedDrawn 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->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); @@ -1509,10 +1974,14 @@ std::expected parseFile(std::span data) { 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); + 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)); diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index 6ade4a9..6de27b5 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -26,6 +26,8 @@ protected: Section() = default; }; +class CustomFontSection; + enum class ElementType { Image = 1, Animation = 2, @@ -50,7 +52,7 @@ 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; @@ -78,6 +80,19 @@ private: 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{}; +}; + struct ImageElement : public Element { ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height); @@ -94,7 +109,7 @@ struct ImageElement : public Element { 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); @@ -126,7 +141,7 @@ struct AnimationElement : public Element { 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); @@ -159,7 +174,7 @@ struct HScrollImageElement : public Element { 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); @@ -193,7 +208,7 @@ struct VScrollImageElement : public Element { 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); @@ -225,7 +240,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); @@ -251,7 +266,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); @@ -279,7 +294,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); @@ -308,7 +323,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); 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{}; } From b9ff412df5fa5ab64b86bbb6755fdb11f7a7f31d Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 15 May 2026 10:20:07 +0200 Subject: [PATCH 7/9] C++: implement (mostly) non-allocating parser that immediately renders --- cpp/clivis/main.cpp | 149 +++- cpp/src/CMakeLists.txt | 4 + cpp/src/monoformat_fontreader.cpp | 6 +- cpp/src/monoformat_fontreader.hpp | 2 +- cpp/src/monoformat_parsehelpers.hpp | 4 +- cpp/src/monoformat_parseonly.cpp | 1053 +++++++++++++++++++++++++++ cpp/src/monoformat_parseonly.hpp | 15 + cpp/src/monoformat_schema.cpp | 103 +++ cpp/src/monoformat_schema.hpp | 77 ++ cpp/src/monoformat_structured.cpp | 128 +--- cpp/src/monoformat_structured.hpp | 66 +- 11 files changed, 1384 insertions(+), 223 deletions(-) create mode 100644 cpp/src/monoformat_parseonly.cpp create mode 100644 cpp/src/monoformat_parseonly.hpp create mode 100644 cpp/src/monoformat_schema.cpp create mode 100644 cpp/src/monoformat_schema.hpp 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 96e3233..dcdf69a 100644 --- a/cpp/src/monoformat_fontreader.hpp +++ b/cpp/src/monoformat_fontreader.hpp @@ -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 5901595..06b2462 100644 --- a/cpp/src/monoformat_parsehelpers.hpp +++ b/cpp/src/monoformat_parsehelpers.hpp @@ -328,8 +328,8 @@ inline std::size_t writeU64BE(std::span& target, std::size_t pos, std inline std::size_t writeBuffer(std::span& target, std::size_t pos, std::span value) { if ((pos + value.size()) > target.size()) { - if (pos < value.size()) { - std::size_t const n = value.size() - pos; + if (pos < target.size()) { + std::size_t const n = target.size() - pos; std::copy(value.begin(), value.begin() + n, target.begin() + pos); } } else { 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 0bd3f36..c56a765 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -5,102 +5,6 @@ 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); -} - ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) : m_x{x} , m_y{y} @@ -1038,7 +942,7 @@ ElementType ClippedTextElement::elementType() const { std::size_t ClippedTextElement::serializeTo(std::span target) const { std::size_t pos = 0; - pos = writeU16LE(target, pos, static_cast(ElementType::HScrollImage)); + 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); @@ -1061,13 +965,7 @@ void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } fontData = customFonts[fontIndex]->fontData(); } else { - if (m_fontIndex == 0) { - fontData = findEmbeddedFont("NokiaSmallPlain_tf"); - } else if (m_fontIndex == 1) { - fontData = findEmbeddedFont("5x7_mf"); - } else { - return; - } + fontData = findEmbeddedFont(m_fontIndex); } if (fontData.size() < 23) { return; @@ -1174,7 +1072,7 @@ ElementType HScrollTextElement::elementType() const { std::size_t HScrollTextElement::serializeTo(std::span target) const { std::size_t pos = 0; - pos = writeU16LE(target, pos, static_cast(ElementType::HScrollImage)); + 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); @@ -1198,13 +1096,7 @@ void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } fontData = customFonts[fontIndex]->fontData(); } else { - if (m_fontIndex == 0) { - fontData = findEmbeddedFont("NokiaSmallPlain_tf"); - } else if (m_fontIndex == 1) { - fontData = findEmbeddedFont("5x7_mf"); - } else { - return; - } + fontData = findEmbeddedFont(m_fontIndex); } if (fontData.size() < 23) { return; @@ -1395,7 +1287,7 @@ ElementType CurrentTimeElement::elementType() const { std::size_t CurrentTimeElement::serializeTo(std::span target) const { std::size_t pos = 0; - pos = writeU16LE(target, pos, static_cast(ElementType::HScrollImage)); + 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); @@ -1417,13 +1309,7 @@ void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t } fontData = customFonts[fontIndex]->fontData(); } else { - if (m_fontIndex == 0) { - fontData = findEmbeddedFont("NokiaSmallPlain_tf"); - } else if (m_fontIndex == 1) { - fontData = findEmbeddedFont("5x7_mf"); - } else { - return; - } + fontData = findEmbeddedFont(m_fontIndex); } if (fontData.size() < 23) { return; @@ -1923,7 +1809,7 @@ std::size_t serializeFile(std::span& buffer, File const& file) { pos = writeU8LE(buffer, pos, 0xAF); pos = writeU8LE(buffer, pos, 0x7E); pos = writeU8LE(buffer, pos, 0x2B); - pos = writeU8LE(buffer, pos, 0x64); + pos = writeU8LE(buffer, pos, 0x63); pos = writeU32LE(buffer, pos, 1); pos = writeU16LE(buffer, pos, file.sections.size()); pos = writeU16LE(buffer, pos, 0); diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index 6de27b5..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; @@ -28,26 +23,6 @@ protected: class CustomFontSection; -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; -}; - struct Element { virtual ~Element() = default; virtual ElementType elementType() const = 0; @@ -58,41 +33,6 @@ 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 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{}; -}; - struct ImageElement : public Element { ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height); @@ -223,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); From 48f71fea6d7d1810c4302a7350c3e3c1ee10d829 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 15 May 2026 10:20:46 +0200 Subject: [PATCH 8/9] README: update README --- cpp/README.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/README.txt b/cpp/README.txt index 18830af..a4046c2 100644 --- a/cpp/README.txt +++ b/cpp/README.txt @@ -2,8 +2,6 @@ This is the C++ implementation of the mono display format library. TODO: - - 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 From 08932f4ff80332fdb358a13ade63f3bf78dfc2f3 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 15 May 2026 11:14:18 +0200 Subject: [PATCH 9/9] Add minimal PHP script --- php/monoformat.php | 201 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 php/monoformat.php 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 + +?>