From c83b4425be5befe2b2ea6386ff1cc6dc7876945e Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 29 May 2026 23:32:09 +0200 Subject: [PATCH] C++/Specification: introduce new format version 2 with some additions - We try to write format version 1 if possible to stay as compatible as possible (unless newer features are actually used) - Format version 2 has new section types: - MultiTimeBasedDrawn: with multiple timestamp ranges - ExpiryDate: to indicate this file has expired - An additional Box element type that fills a box is available - Also there are lots of flags to draw things in dark are now available - to draw dark pixels on a light background --- Specification.rst | 142 +++++- cpp/src/monoformat_parseonly.cpp | 337 ++++++++++++-- cpp/src/monoformat_parseonly.hpp | 2 + cpp/src/monoformat_schema.hpp | 46 +- cpp/src/monoformat_structured.cpp | 733 ++++++++++++++++++++++++++---- cpp/src/monoformat_structured.hpp | 99 +++- 6 files changed, 1239 insertions(+), 120 deletions(-) diff --git a/Specification.rst b/Specification.rst index 4c1c4f7..f31096b 100644 --- a/Specification.rst +++ b/Specification.rst @@ -77,9 +77,11 @@ The following possible values for the version field exist: +===========+============================================+ | ``0`` | Illegal | +-----------+--------------------------------------------+ -| ``1`` | The current version of this specification. | +| ``1`` | A previous version of this specification. | +-----------+--------------------------------------------+ -| ``2`` .. | Reserved for future use | +| ``2`` | The current version of this specification. | ++-----------+--------------------------------------------+ +| ``3`` .. | Reserved for future use | +-----------+--------------------------------------------+ A value of ``0`` is illegal for the version field and indivates a problem with the file. The current specification version is ``1``. Writers that conform to this specification **MUST** use this value. @@ -118,8 +120,12 @@ The section type specifies what kind of section this is. The following section t +-----------+---------------------------------------------------+ | ``2`` | List of drawn elements (timespan) | +-----------+---------------------------------------------------+ -| ``3`` .. | Reserved for future use | -| ``31`` | | +| ``3`` | List of drawn elements (list of timespans) | ++-----------+---------------------------------------------------+ +| ``4`` .. | Reserved for future use | +| ``30`` | | ++-----------+---------------------------------------------------+ +| ``31`` | Expiry Date | +-----------+---------------------------------------------------+ | ``32`` | Custom Font | +-----------+---------------------------------------------------+ @@ -190,6 +196,52 @@ This section is similar to the "List of drawn elements (always)" section, but th The additional fields are two 64bit POSIX timestamps containing the start and end time during which this section is to be drawn. If the current timestamp is larger than or equal to the start timestamp, and the current timestamp is smaller than the end timestamp, the section will be shown, otherwise it will be skipped. +List of drawn elements (list of timespans) +------------------------------------------ + +This section is similar to the "List of drawn elements (timespan)" section, but the header contains a list of timestamp ranges, not just a single range: + + 0 1 2 3 + +------------------------+------------------------+------------------------+------------------------+ + 0 | Flags | Number of Elements | + +------------------------+------------------------+------------------------+------------------------+ + 4 | Number of Timestamps | Reserved | + +------------------------+------------------------+------------------------+------------------------+ + 8 | First Start Timestamp (Low) | + +------------------------+------------------------+------------------------+------------------------+ + 12 | First Start Timestamp (High) | + +------------------------+------------------------+------------------------+------------------------+ + 16 | First End Timestamp (Low) | + +------------------------+------------------------+------------------------+------------------------+ + 20 | First End Timestamp (High) | + +------------------------+------------------------+------------------------+------------------------+ + 24 | Second Start Timestamp (Low) | + +------------------------+------------------------+------------------------+------------------------+ + 28 | Second Start Timestamp (High) | + +------------------------+------------------------+------------------------+------------------------+ + 32 | Second End Timestamp (Low) | + +------------------------+------------------------+------------------------+------------------------+ + 36 | Second End Timestamp (High) | + +------------------------+------------------------+------------------------+------------------------+ + 40.. | Additional timestamps .... | + +------------------------+------------------------+------------------------+------------------------+ + +After the last timestamp the list of elements follows (as with the "List of drawn elements (always)" and "List of drawn elements (timespan)" elements). + +File Global Expiry Date +----------------------- + +If this section is present it contains the timestamp that describes the date after which the file is no longer valid. This allows the user to generate +files that will be discarded if the current date is already past the expiry timestamp, and will automatically stop the file from being displayed +once the expiry timestamp is reached. + + 0 1 2 3 + +------------------------+------------------------+------------------------+------------------------+ + 0 | Expiry Date Timestamp (Low) | + +------------------------+------------------------+------------------------+------------------------+ + 4 | Expiry Date Timestamp (High) | + +------------------------+------------------------+------------------------+------------------------+ + Custom Font ----------- @@ -331,13 +383,61 @@ Line 8 | Y Target | Line Style | Flags | +------------------------+------------------------+-------------------------------------------------+ +Line Style +`````````` + ++-----------+---------------------------------------------------+ +| Bit | Description | ++===========+===================================================+ +| ``0`` | Solid line | ++-----------+---------------------------------------------------+ +| ``1`` .. | Reserved for future use | +| ``255`` | | ++-----------+---------------------------------------------------+ + +Flags +````` + ++-----------+---------------------------------------------------+ +| Bit | Description | ++===========+===================================================+ +| ``0`` | Line color is dark, not white | ++-----------+---------------------------------------------------+ +| ``1`` .. | Reserved for future use | +| ``7`` | | ++-----------+---------------------------------------------------+ + +Box +--- + + 0 1 2 3 + +------------------------+------------------------+------------------------+------------------------+ + 0 | Type: 6 | X | + +------------------------+------------------------+------------------------+------------------------+ + 4 | Y | Width | + +------------------------+------------------------+------------------------+------------------------+ + 8 | Height | Fill Pattern | Flags | + +------------------------+------------------------+-------------------------------------------------+ + +Fill Pattern +```````````` + ++-----------+---------------------------------------------------+ +| Bit | Description | ++===========+===================================================+ +| ``0`` | Solid | ++-----------+---------------------------------------------------+ +| ``1`` .. | Reserved for future use | +| ``255`` | | ++-----------+---------------------------------------------------+ + Flags ````` +-----------+---------------------------------------------------+ | Bit | Description | +===========+===================================================+ -| ``0`` | Invert Pixel Values | +| ``0`` | Fill color is dark, not white | +-----------+---------------------------------------------------+ | ``1`` .. | Reserved for future use | | ``7`` | | @@ -352,13 +452,25 @@ Clipped Text +------------------------+------------------------+------------------------+------------------------+ 4 | Y Offset | Width | +------------------------+------------------------+------------------------+------------------------+ - 8 | Height | Font Index | + 8 | Height | Text Flags | Reserved | +------------------------+------------------------+------------------------+------------------------+ - 12 | Text Length | Text ... | + 12 | Font Index | Text Length | +------------------------+------------------------+------------------------+------------------------+ - ... | ... Text | Padding (if required) | + 16.. | Text ... | Padding (if required) | +------------------------+------------------------+------------------------+------------------------+ +Text Flags +`````````` + ++-----------+---------------------------------------------------+ +| Bit | Description | ++===========+===================================================+ +| ``0`` | Text is dark, background is white | ++-----------+---------------------------------------------------+ +| ``1`` .. | Reserved for future use | +| ``7`` | | ++-----------+---------------------------------------------------+ + Font Index `````````` @@ -377,11 +489,13 @@ Horizontally Scrolling Text +------------------------+------------------------+------------------------+------------------------+ 4 | Y Offset | Width | +------------------------+------------------------+------------------------+------------------------+ - 8 | Height | Flags | Scroll Speed | + 8 | Height | Text Flags | Reserved | +------------------------+------------------------+------------------------+------------------------+ - 12 | Font Index | Text Length | + 12 | Flags | Scroll Speed | Reserved | +------------------------+------------------------+------------------------+------------------------+ - ... | ... Text | Padding (if required) | + 16 | Font Index | Text Length | + +------------------------+------------------------+------------------------+------------------------+ + 20.. | ... Text | Padding (if required) | +------------------------+------------------------+------------------------+------------------------+ Current Time @@ -393,9 +507,11 @@ Current Time +------------------------+------------------------+------------------------+------------------------+ 4 | Y Offset | Width | +------------------------+------------------------+------------------------+------------------------+ - 8 | Height | Font Index | + 8 | Height | Text Flags | Reserved | + +------------------------+------------------------+------------------------+------------------------+ + 12 | Font Index | UTC Offset (Minutes) | +------------------------+------------------------+------------------------+------------------------+ - 12 | UTC Offset (Minutes) | Flags | + 16 | Flags | Reserved | +------------------------+------------------------+------------------------+------------------------+ UTC Offset diff --git a/cpp/src/monoformat_parseonly.cpp b/cpp/src/monoformat_parseonly.cpp index 90d48e3..7e72b3b 100644 --- a/cpp/src/monoformat_parseonly.cpp +++ b/cpp/src/monoformat_parseonly.cpp @@ -23,11 +23,18 @@ struct TimeBasedDrawnSectionMetaData { std::int64_t endTimestamp{}; }; +struct MultiTimeBasedDrawnSectionMetaData { + AlwaysDrawnSectionMetaData base; + std::size_t rangeCount{}; + std::span rangeData; +}; + static std::expected handleImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleAnimation(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleHScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleVScrollImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleLine(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); +static std::expected handleBox(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleClippedText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleHScrollText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); static std::expected handleCurrentTime(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion); @@ -124,6 +131,62 @@ static std::expected parseTimeBasedDr return result; } +static std::expected parseMultiTimeBasedDrawnSection(std::span& data, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + auto type = readU8LE(data); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::MultiTimeBasedDrawn)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(data); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 12) { + 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 rangeCount = readU16LE(data); + if (!rangeCount) { + return std::unexpected(rangeCount.error()); + } + if (((*rangeCount) * 16u + 12u) > *size) { + return std::unexpected(ParseError::InvalidValue); + } + auto reserved1 = readU16LE(data); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + + auto sectionData = data.subspan(0, *size - 12 - 16 * (*rangeCount)); + auto rangeData = data.subspan(12, 16 * (*rangeCount)); + data = data.subspan(*size - 12 - 16 * (*rangeCount)); + + MultiTimeBasedDrawnSectionMetaData result; + result.base.drawOnFront = isBitSet(*flags, 0); + result.base.drawOnBack = isBitSet(*flags, 1); + result.base.clearBeforeDrawing = isBitSet(*flags, 2); + result.base.elementData = sectionData; + result.rangeCount = *rangeCount; + result.rangeData = rangeData; + +#if defined(DEBUG) + std::cerr << "[Debug] MultiTimeBasedDrawnSection, drawOnFront = " << result.base.drawOnFront << ", drawOnBack = " << result.base.drawOnBack << ", clearBeforeDrawing = " << result.base.clearBeforeDrawing << ", element data size = " << result.base.elementData.size() << " bytes, # timestamps = " << result.rangeCount << std::endl; +#endif + + return result; +} + static std::expected, ParseError> parseCustomFontSection(std::span& data, std::uint32_t formatVersion) { std::ignore = formatVersion; @@ -176,7 +239,7 @@ std::expected parseAndApply(std::span data, O if (!version) { return std::unexpected(version.error()); } - if (*version != 1) { + if (*version < 1 || *version > 2) { return std::unexpected(ParseError::InvalidValue); } auto sectionCount = readU16LE(data); @@ -211,6 +274,9 @@ std::expected parseAndApply(std::span data, O customFonts[customFontIndex++] = *fontData; } continue; + } else if (static_cast(*sectionType) == SectionType::ExpiryDate) { + // Ignore expiry date sections here + continue; } AlwaysDrawnSectionMetaData section; @@ -229,6 +295,30 @@ std::expected parseAndApply(std::span data, O continue; } section = metaData->base; + } else if (static_cast(*sectionType) == SectionType::MultiTimeBasedDrawn) { + auto metaData = parseMultiTimeBasedDrawnSection(*rawSectionData, *version); + if (!metaData) { + return std::unexpected(metaData.error()); + } + bool inRange = false; + for (std::size_t i = 0; i < metaData->rangeCount; ++i) { + auto startTimestamp = readU64LE(metaData->rangeData); + if (!startTimestamp) { + return std::unexpected(startTimestamp.error()); + } + auto endTimestamp = readU64LE(metaData->rangeData); + if (!endTimestamp) { + return std::unexpected(endTimestamp.error()); + } + if (currentTimestamp >= std::bit_cast(*startTimestamp) && currentTimestamp < std::bit_cast(*endTimestamp)) { + inRange = true; + break; + } + } + if (!inRange) { + continue; + } + section = metaData->base; } else { continue; } @@ -260,6 +350,7 @@ std::expected parseAndApply(std::span data, O case ElementType::HScrollImage: parseResult = handleHScrollImage(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; case ElementType::VScrollImage: parseResult = handleVScrollImage(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; case ElementType::Line: parseResult = handleLine(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; + case ElementType::Box: parseResult = handleBox(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; case ElementType::ClippedText: parseResult = handleClippedText(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; case ElementType::HScrollText: parseResult = handleHScrollText(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; case ElementType::CurrentTime: parseResult = handleCurrentTime(section.elementData, screen, animationTick, currentTimestamp, std::span{customFonts}.subspan(0, customFontIndex), *version); break; @@ -275,6 +366,70 @@ std::expected parseAndApply(std::span data, O return {}; } +std::expected parseAndCheckExpiry(std::span data, std::int64_t currentTimestamp) { + auto magic = readU32LE(data); + if (!magic) { + return std::unexpected(magic.error()); + } + if (*magic != UINT32_C(0x632B7EAF)) { + return std::unexpected(ParseError::InvalidValue); + } + auto version = readU32LE(data); + if (!version) { + return std::unexpected(version.error()); + } + if (*version == 1) { + // Version 1 doesn't ever expire + return false; + } + if (*version < 1 || *version > 2) { + return std::unexpected(ParseError::InvalidValue); + } + auto sectionCount = readU16LE(data); + if (!sectionCount) { + return std::unexpected(sectionCount.error()); + } + auto reserved_ = readU16LE(data); + if (!reserved_) { + return std::unexpected(reserved_.error()); + } + + 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::ExpiryDate) { + // Ignore expiry date sections here + continue; + } + + auto headerData = rawSectionData->subspan(4); + auto expiryDate = readU64LE(headerData); + if (!expiryDate) { + return std::unexpected(expiryDate.error()); + } + + if (currentTimestamp >= std::bit_cast(*expiryDate)) { + return true; + } + + // Don't abort here, there could be multiple expiry dates in the file + } + + // The file isn't expired + return false; +} + std::expected handleImage(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion) { std::ignore = formatVersion; @@ -785,11 +940,10 @@ std::expected handleLine(std::span& buffer, O #endif LineFlags lineFlags{*flags}; - std::ignore = lineFlags; std::int32_t dx = static_cast(*targetX) - static_cast(*originX); std::int32_t dy = static_cast(*targetY) - static_cast(*originY); - bool value = true; + bool value = !lineFlags.dark; if (dx == 0) { for (std::int32_t i = 0; i <= dy; ++i) { @@ -810,6 +964,66 @@ std::expected handleLine(std::span& buffer, O return {}; } +std::expected handleBox(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion) { + std::ignore = formatVersion; + 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::Box)) { + return std::unexpected(ParseError::InvalidValue); + } + auto x = readU16LE(buffer); + if (!x) { + return std::unexpected(x.error()); + } + auto y = readU16LE(buffer); + if (!y) { + return std::unexpected(y.error()); + } + auto width = readU16LE(buffer); + if (!width) { + return std::unexpected(width.error()); + } + auto height = readU16LE(buffer); + if (!height) { + return std::unexpected(height.error()); + } + auto fillPattern = readU8LE(buffer); + if (!fillPattern) { + return std::unexpected(fillPattern.error()); + } + if (*fillPattern != static_cast(FillPattern::Solid)) { + return std::unexpected(ParseError::InvalidValue); + } + auto flags = readU8LE(buffer); + if (!flags) { + return std::unexpected(flags.error()); + } + +#if defined(DEBUG) + std::cerr << "[Debug] - Box, x = " << (*x) << ", y = " << (*y) << ", width = " << (*width) << ", height = " << (*height) + << ", fillPattern = " << int(*fillPattern) + << std::endl; +#endif + + FillFlags fillFlags{*flags}; + + bool value = !fillFlags.dark; + + for (std::int32_t dy = 0; dy < *height; ++dy) { + for (std::int32_t dx = 0; dx < *width; ++dx) { + screen->setPixel(dx + *x, dy + *y, value); + } + } + + return {}; +} + std::expected handleClippedText(std::span& buffer, OneBitBufferInterface* screen, std::size_t animationTick, std::int64_t currentTimestamp, std::span> customFonts, std::uint32_t formatVersion) { std::ignore = formatVersion; std::ignore = animationTick; @@ -838,6 +1052,18 @@ std::expected handleClippedText(std::span& bu if (!height) { return std::unexpected(height.error()); } + std::expected textFlags_ = 0; + if (formatVersion >= 2) { + textFlags_ = readU8LE(buffer); + if (!textFlags_) { + return std::unexpected(textFlags_.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } + TextFlags textFlags{*textFlags_}; auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -847,6 +1073,9 @@ std::expected handleClippedText(std::span& bu return std::unexpected(textLength.error()); } std::size_t alignedRoundedUp = (*textLength + 2 + 4 - 1) / 4 * 4 - 2; + if (formatVersion >= 2) { + alignedRoundedUp = (*textLength + 4 - 1) / 4 * 4; + } auto textData = readBuffer(buffer, alignedRoundedUp); if (!textData) { return std::unexpected(textData.error()); @@ -873,9 +1102,14 @@ std::expected handleClippedText(std::span& bu if (fontData.size() < 23) { return {}; } + + std::optional bgColor; + bgColor = textFlags.dark; + bool fgColor = !textFlags.dark; + 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); + renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); return {}; } @@ -907,6 +1141,18 @@ std::expected handleHScrollText(std::span& bu if (!height) { return std::unexpected(height.error()); } + std::expected textFlags_ = 0; + if (formatVersion >= 2) { + textFlags_ = readU8LE(buffer); + if (!textFlags_) { + return std::unexpected(textFlags_.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } + TextFlags textFlags{*textFlags_}; auto flags = readU8LE(buffer); if (!flags) { return std::unexpected(flags.error()); @@ -915,6 +1161,12 @@ std::expected handleHScrollText(std::span& bu if (!scrollSpeed) { return std::unexpected(scrollSpeed.error()); } + if (formatVersion >= 2) { + auto reserved2 = readU16LE(buffer); + if (!reserved2) { + return std::unexpected(reserved2.error()); + } + } auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -967,6 +1219,10 @@ std::expected handleHScrollText(std::span& bu ScrollFlags scrollFlags{*flags}; + std::optional bgColor; + bgColor = textFlags.dark; + bool fgColor = !textFlags.dark; + bool restarting = !scrollFlags.endless; bool invert = scrollFlags.invertDirection; bool padBefore = scrollFlags.padBefore; @@ -974,7 +1230,7 @@ std::expected handleHScrollText(std::span& bu 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); + renderer.render(text, Point{offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); return {}; } @@ -998,45 +1254,51 @@ std::expected handleHScrollText(std::span& bu 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; + if (bgColor) { + 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, *bgColor); } - target.setPixel(ddx - offset, dy, false); } } partOffset += *width; } - renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); 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; + if (bgColor) { + 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, *bgColor); } - 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; + if (bgColor) { + 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, *bgColor); } - target.setPixel(ddx - offset, dy, false); } } partOffset += *width; } - renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } return {}; @@ -1069,6 +1331,18 @@ std::expected handleCurrentTime(std::span& bu if (!height) { return std::unexpected(height.error()); } + std::expected textFlags_ = 0; + if (formatVersion >= 2) { + textFlags_ = readU8LE(buffer); + if (!textFlags_) { + return std::unexpected(textFlags_.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } + TextFlags textFlags{*textFlags_}; auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -1081,6 +1355,12 @@ std::expected handleCurrentTime(std::span& bu if (!flags) { return std::unexpected(flags.error()); } + if (formatVersion >= 2) { + auto reserved2 = readU16LE(buffer); + if (!reserved2) { + return std::unexpected(reserved2.error()); + } + } #if defined(DEBUG) std::cerr << "[Debug] - CurrentTime, x = " << (*x) << ", y = " << (*y) << ", width = " << (*width) << ", height = " << (*height) @@ -1103,6 +1383,11 @@ std::expected handleCurrentTime(std::span& bu if (fontData.size() < 23) { return {}; } + + std::optional bgColor; + bgColor = textFlags.dark; + bool fgColor = !textFlags.dark; + ClippedImage target{screen, *x, *y, *width, *height}; TimeDisplayFlags timeDisplayFlags{*flags}; @@ -1144,7 +1429,7 @@ std::expected handleCurrentTime(std::span& bu return {}; } - renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); return {}; } diff --git a/cpp/src/monoformat_parseonly.hpp b/cpp/src/monoformat_parseonly.hpp index 56ee626..68bf9e3 100644 --- a/cpp/src/monoformat_parseonly.hpp +++ b/cpp/src/monoformat_parseonly.hpp @@ -10,6 +10,8 @@ std::expected parseAndApply(std::span data, O std::uint16_t screenWidth, std::uint16_t screenHeight, bool isFront, std::size_t animationTick, std::int64_t currentTimestamp); +std::expected parseAndCheckExpiry(std::span data, std::int64_t currentTimestamp); + } // namespace monoformat #endif // MONOFORMAT_PARSEONLY_HPP diff --git a/cpp/src/monoformat_schema.hpp b/cpp/src/monoformat_schema.hpp index 09f1136..5b4f9f7 100644 --- a/cpp/src/monoformat_schema.hpp +++ b/cpp/src/monoformat_schema.hpp @@ -13,6 +13,8 @@ namespace monoformat { enum class SectionType { AlwaysDrawn = 1, TimeBasedDrawn = 2, + MultiTimeBasedDrawn = 3, + ExpiryDate = 31, CustomFont = 32, }; @@ -22,6 +24,7 @@ enum class ElementType { HScrollImage = 3, VScrollImage = 4, Line = 5, + Box = 6, ClippedText = 16, HScrollText = 17, //VScrollText = 18, @@ -59,15 +62,56 @@ struct ScrollFlags { }; struct LineFlags { + bool dark{}; + LineFlags() = default; explicit LineFlags(std::uint8_t flags) noexcept + : dark{isBitSet(flags, 0)} + { + } + + explicit operator std::uint8_t() const noexcept { + std::uint8_t result{}; + maybeSetBit(result, 0, dark); + return result; + } +}; + +enum class FillPattern : std::uint8_t { + Solid = 0, +}; + +struct FillFlags { + bool dark{}; + + FillFlags() = default; + + explicit FillFlags(std::uint8_t flags) noexcept + : dark{isBitSet(flags, 0)} + { + } + + explicit operator std::uint8_t() const noexcept { + std::uint8_t result{}; + maybeSetBit(result, 0, dark); + return result; + } +}; + +struct TextFlags { + bool dark{}; + + TextFlags() = default; + + explicit TextFlags(std::uint8_t flags) noexcept + : dark{isBitSet(flags, 0)} { - std::ignore = flags; } explicit operator std::uint8_t() const noexcept { std::uint8_t result{}; + maybeSetBit(result, 0, dark); return result; } }; diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 2bfe01c..5db1097 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -14,6 +14,8 @@ static std::vector base64Decode(std::string_view data); NLOHMANN_JSON_SERIALIZE_ENUM(SectionType, { {SectionType::AlwaysDrawn, "AlwaysDrawn"}, {SectionType::TimeBasedDrawn, "TimeBasedDrawn"}, + {SectionType::MultiTimeBasedDrawn, "MultiTimeBasedDrawn"}, + {SectionType::ExpiryDate, "ExpiryDate"}, {SectionType::CustomFont, "CustomFont"}, }) @@ -23,6 +25,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(ElementType, { {ElementType::HScrollImage, "HScrollImage"}, {ElementType::VScrollImage, "VScrollImage"}, {ElementType::Line, "Line"}, + {ElementType::Box, "Box"}, {ElementType::ClippedText, "ClippedText"}, {ElementType::HScrollText, "HScrollText"}, {ElementType::CurrentTime, "CurrentTime"}, @@ -32,20 +35,14 @@ NLOHMANN_JSON_SERIALIZE_ENUM(LineStyle, { {LineStyle::Solid, "Solid"}, }) -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ScrollFlags, endless, invertDirection, padBefore, padAfter) - -// Don't use NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT here because our struct is empty -static void from_json(nlohmann::json const& j, LineFlags& v) { - if (!j.is_object() || j.size() > 0) { - throw std::runtime_error("Cannot deserialize a LineFlags that is not an empty object"); - } - v = {}; -} -static void to_json(nlohmann::json& j, LineFlags const& v) { - std::ignore = v; - j = nlohmann::json::object(); -} +NLOHMANN_JSON_SERIALIZE_ENUM(FillPattern, { + {FillPattern::Solid, "Solid"}, +}) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ScrollFlags, endless, invertDirection, padBefore, padAfter) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(LineFlags, dark) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(FillFlags, dark) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TextFlags, dark) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TimeDisplayFlags, use12h, showHours, showMinutes, showSeconds) ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) @@ -1104,7 +1101,11 @@ ElementType LineElement::elementType() const { } std::uint32_t LineElement::minimumFormatVersion() const { - return 1; + if (static_cast(m_flags) != 0) { + return 2; + } else { + return 1; + } } std::size_t LineElement::serializeTo(std::span target, std::uint32_t formatVersion) const { @@ -1216,11 +1217,150 @@ std::unique_ptr LineElement::parseJSON(nlohmann::json const& j, std 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) +BoxElement::BoxElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, FillPattern fillPattern, FillFlags flags) + : m_x{x} + , m_y{y} + , m_width{width} + , m_height{height} + , m_fillPattern{fillPattern} + , m_flags{flags} +{ +} + +std::uint16_t BoxElement::x() const noexcept { + return m_x; +} + +std::uint16_t BoxElement::y() const noexcept { + return m_y; +} + +std::uint16_t BoxElement::width() const noexcept { + return m_width; +} + +std::uint16_t BoxElement::height() const noexcept { + return m_height; +} + +FillPattern BoxElement::fillPattern() const noexcept { + return m_fillPattern; +} + +FillFlags BoxElement::flags() const noexcept { + return m_flags; +} + +BoxElement::~BoxElement() { +} + +ElementType BoxElement::elementType() const { + return ElementType::Box; +} + +std::uint32_t BoxElement::minimumFormatVersion() const { + return 2; +} + +std::size_t BoxElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + std::size_t pos = 0; + pos = writeU16LE(target, pos, static_cast(ElementType::Box)); + pos = writeU16LE(target, pos, m_x); + pos = writeU16LE(target, pos, m_y); + pos = writeU16LE(target, pos, m_width); + pos = writeU16LE(target, pos, m_height); + pos = writeU8LE(target, pos, static_cast(m_fillPattern)); + pos = writeU8LE(target, pos, static_cast(m_flags)); + return alignNextWrite(target, pos, 4); +} + +nlohmann::json BoxElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "Box"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["fillPattern"] = m_fillPattern; + result["flags"] = m_flags; + return result; +} + +void BoxElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { + std::ignore = animationTick; + std::ignore = currentTimestamp; + std::ignore = customFonts; + + bool color = !m_flags.dark; + for (std::uint16_t dy = 0; dy < m_height; ++dy) { + for (std::uint16_t dx = 0; dx < m_width; ++dx) { + imageBuffer->setPixel(m_x + dx, m_y + dy, color); + } + } +} + +std::expected, ParseError> BoxElement::parse(std::span& buffer, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + auto type = readU16LE(buffer); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(ElementType::Image)) { + return std::unexpected(ParseError::InvalidValue); + } + auto x = readU16LE(buffer); + if (!x) { + return std::unexpected(x.error()); + } + auto y = readU16LE(buffer); + if (!y) { + return std::unexpected(y.error()); + } + auto width = readU16LE(buffer); + if (!width) { + return std::unexpected(width.error()); + } + auto height = readU16LE(buffer); + if (!height) { + return std::unexpected(height.error()); + } + auto fillPattern = readU8LE(buffer); + if (!fillPattern) { + return std::unexpected(fillPattern.error()); + } + auto flags = readU8LE(buffer); + if (!flags) { + return std::unexpected(flags.error()); + } + + auto result = std::make_unique(*x, *y, *width, *height, static_cast(*fillPattern), FillFlags{*flags}); + return result; +} + +std::unique_ptr BoxElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t x = j.at("x"); + std::uint16_t y = j.at("y"); + std::uint16_t width = j.at("width"); + std::uint16_t height = j.at("height"); + FillPattern fillPattern = j.at("fillPattern"); + FillFlags flags = j.at("flags"); + auto result = std::make_unique(x, y, width, height, fillPattern, flags); + return result; +} + +ClippedTextElement::ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::string text) : m_x{x} , m_y{y} , m_width{width} , m_height{height} + , m_textFlags{textFlags} , m_fontIndex{fontIndex} , m_text{std::move(text)} { @@ -1242,6 +1382,10 @@ std::uint16_t ClippedTextElement::height() const noexcept { return m_height; } +TextFlags ClippedTextElement::textFlags() const noexcept { + return m_textFlags; +} + std::uint16_t ClippedTextElement::fontIndex() const noexcept { return m_fontIndex; } @@ -1258,18 +1402,24 @@ ElementType ClippedTextElement::elementType() const { } std::uint32_t ClippedTextElement::minimumFormatVersion() const { - return 1; + if (static_cast(m_textFlags) != 0) { + return 2; + } else { + return 1; + } } std::size_t ClippedTextElement::serializeTo(std::span target, std::uint32_t formatVersion) const { - std::ignore = formatVersion; - std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::ClippedText)); pos = writeU16LE(target, pos, m_x); pos = writeU16LE(target, pos, m_y); pos = writeU16LE(target, pos, m_width); pos = writeU16LE(target, pos, m_height); + if (formatVersion >= 2) { + pos = writeU8LE(target, pos, static_cast(m_textFlags)); + pos = writeU8LE(target, pos, 0); + } pos = writeU16LE(target, pos, m_fontIndex); pos = writeU16LE(target, pos, m_text.size()); pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text})); @@ -1277,14 +1427,15 @@ std::size_t ClippedTextElement::serializeTo(std::span target, std::ui } nlohmann::json ClippedTextElement::serializeJSON(std::uint32_t formatVersion) const { - std::ignore = formatVersion; - nlohmann::json result; result["type"] = "ClippedText"; result["x"] = m_x; result["y"] = m_y; result["width"] = m_width; result["height"] = m_height; + if (formatVersion >= 2) { + result["textFlags"] = m_textFlags; + } result["fontIndex"] = m_fontIndex; result["text"] = m_text; return result; @@ -1310,13 +1461,15 @@ void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height}; + std::optional bgColor; + bgColor = m_textFlags.dark; + bool fgColor = !m_textFlags.dark; + auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); - renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(m_text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } std::expected, ParseError> ClippedTextElement::parse(std::span& buffer, std::uint32_t formatVersion) { - std::ignore = formatVersion; - auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); @@ -1340,6 +1493,17 @@ std::expected, ParseError> ClippedTextElemen if (!height) { return std::unexpected(height.error()); } + std::expected textFlags = 0; + if (formatVersion >= 2) { + textFlags = readU8LE(buffer); + if (!textFlags) { + return std::unexpected(textFlags.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -1354,29 +1518,32 @@ std::expected, ParseError> ClippedTextElemen 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}); + auto result = std::make_unique(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, std::string{text}); return result; } std::unique_ptr ClippedTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { - std::ignore = formatVersion; - std::uint16_t x = j.at("x"); std::uint16_t y = j.at("y"); std::uint16_t width = j.at("width"); std::uint16_t height = j.at("height"); + TextFlags textFlags; + if (formatVersion >= 2) { + textFlags = j.at("textFlags"); + } std::uint16_t fontIndex = j.at("fontIndex"); std::string text = j.at("text"); - auto result = std::make_unique(x, y, width, height, fontIndex, std::move(text)); + auto result = std::make_unique(x, y, width, height, textFlags, fontIndex, std::move(text)); return result; } -HScrollTextElement::HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text) +HScrollTextElement::HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text) : m_x{x} , m_y{y} , m_width{width} , m_height{height} + , m_textFlags{textFlags} , m_flags{flags} , m_scrollSpeed{scrollSpeed} , m_fontIndex{fontIndex} @@ -1400,6 +1567,10 @@ std::uint16_t HScrollTextElement::height() const noexcept { return m_height; } +TextFlags HScrollTextElement::textFlags() const noexcept { + return m_textFlags; +} + ScrollFlags HScrollTextElement::flags() const noexcept { return m_flags; } @@ -1424,20 +1595,29 @@ ElementType HScrollTextElement::elementType() const { } std::uint32_t HScrollTextElement::minimumFormatVersion() const { - return 1; + if (static_cast(m_textFlags) != 0) { + return 2; + } else { + return 1; + } } std::size_t HScrollTextElement::serializeTo(std::span target, std::uint32_t formatVersion) const { - std::ignore = formatVersion; - std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::HScrollText)); pos = writeU16LE(target, pos, m_x); pos = writeU16LE(target, pos, m_y); pos = writeU16LE(target, pos, m_width); pos = writeU16LE(target, pos, m_height); + if (formatVersion >= 2) { + pos = writeU8LE(target, pos, static_cast(m_textFlags)); + pos = writeU8LE(target, pos, 0); + } pos = writeU8LE(target, pos, static_cast(m_flags)); pos = writeU8LE(target, pos, m_scrollSpeed); + if (formatVersion >= 2) { + pos = writeU16LE(target, pos, 0); + } pos = writeU16LE(target, pos, m_fontIndex); pos = writeU16LE(target, pos, m_text.size()); pos = writeBuffer(target, pos, std::as_bytes(std::span{m_text})); @@ -1453,6 +1633,9 @@ nlohmann::json HScrollTextElement::serializeJSON(std::uint32_t formatVersion) co result["y"] = m_y; result["width"] = m_width; result["height"] = m_height; + if (formatVersion >= 2) { + result["textFlags"] = m_textFlags; + } result["flags"] = m_flags; result["scrollSpeed"] = int(m_scrollSpeed); result["fontIndex"] = m_fontIndex; @@ -1491,6 +1674,10 @@ void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t return; } + std::optional bgColor; + bgColor = m_textFlags.dark; + bool fgColor = !m_textFlags.dark; + bool restarting = !m_flags.endless; bool invert = m_flags.invertDirection; bool padBefore = m_flags.padBefore; @@ -1498,7 +1685,7 @@ void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t 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); + renderer.render(m_text, Point{offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); return; } @@ -1522,51 +1709,55 @@ void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t 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; + if (bgColor) { + for (std::int32_t dy = 0; dy < m_height; ++dy) { + for (std::int32_t dx = 0; dx < m_width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, *bgColor); } - target.setPixel(ddx - offset, dy, false); } } partOffset += m_width; } - renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); 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; + if (bgColor) { + for (std::int32_t dy = 0; dy < m_height; ++dy) { + for (std::int32_t dx = 0; dx < m_width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, *bgColor); } - 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; + if (bgColor) { + for (std::int32_t dy = 0; dy < m_height; ++dy) { + for (std::int32_t dx = 0; dx < m_width; ++dx) { + std::int32_t ddx = dx + partOffset; + if ((ddx - offset) < 0) { + continue; + } + target.setPixel(ddx - offset, dy, *bgColor); } - target.setPixel(ddx - offset, dy, false); } } partOffset += m_width; } - renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(m_text, Point{partOffset - offset, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } } std::expected, ParseError> HScrollTextElement::parse(std::span& buffer, std::uint32_t formatVersion) { - std::ignore = formatVersion; - auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); @@ -1590,6 +1781,17 @@ std::expected, ParseError> HScrollTextElemen if (!height) { return std::unexpected(height.error()); } + std::expected textFlags = 0; + if (formatVersion >= 2) { + textFlags = readU8LE(buffer); + if (!textFlags) { + return std::unexpected(textFlags.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } auto flags = readU8LE(buffer); if (!flags) { return std::unexpected(flags.error()); @@ -1598,6 +1800,12 @@ std::expected, ParseError> HScrollTextElemen if (!scrollSpeed) { return std::unexpected(scrollSpeed.error()); } + if (formatVersion >= 2) { + auto reserved2 = readU16LE(buffer); + if (!reserved2) { + return std::unexpected(reserved2.error()); + } + } auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -1612,17 +1820,19 @@ std::expected, ParseError> HScrollTextElemen return std::unexpected(textData.error()); } std::string_view text{reinterpret_cast(textData->data()), *textLength}; - auto result = std::make_unique(*x, *y, *width, *height, ScrollFlags{*flags}, *scrollSpeed, *fontIndex, std::string{text}); + auto result = std::make_unique(*x, *y, *width, *height, TextFlags{*textFlags}, ScrollFlags{*flags}, *scrollSpeed, *fontIndex, std::string{text}); return result; } std::unique_ptr HScrollTextElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { - std::ignore = formatVersion; - std::uint16_t x = j.at("x"); std::uint16_t y = j.at("y"); std::uint16_t width = j.at("width"); std::uint16_t height = j.at("height"); + TextFlags textFlags; + if (formatVersion >= 2) { + textFlags = j.at("textFlags"); + } ScrollFlags flags = j.at("flags"); int scrollSpeed = j.at("scrollSpeed"); std::uint16_t fontIndex = j.at("fontIndex"); @@ -1632,16 +1842,17 @@ std::unique_ptr HScrollTextElement::parseJSON(nlohmann::json throw std::runtime_error(std::format("Error unserializing an HScrollText element: invalid scroll speed {:d} specified", scrollSpeed)); } - auto result = std::make_unique(x, y, width, height, flags, static_cast(scrollSpeed), fontIndex, std::move(text)); + auto result = std::make_unique(x, y, width, height, textFlags, flags, static_cast(scrollSpeed), fontIndex, std::move(text)); return result; } -CurrentTimeElement::CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t fontIndex, std::uint16_t utcOffset, TimeDisplayFlags flags) +CurrentTimeElement::CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::uint16_t utcOffset, TimeDisplayFlags flags) : m_x{x} , m_y{y} , m_width{width} , m_height{height} + , m_textFlags{textFlags} , m_fontIndex{fontIndex} , m_utcOffset{utcOffset} , m_flags{flags} @@ -1664,6 +1875,10 @@ std::uint16_t CurrentTimeElement::height() const noexcept { return m_height; } +TextFlags CurrentTimeElement::textFlags() const noexcept { + return m_textFlags; +} + std::uint16_t CurrentTimeElement::fontIndex() const noexcept { return m_fontIndex; } @@ -1688,29 +1903,35 @@ std::uint32_t CurrentTimeElement::minimumFormatVersion() const { } std::size_t CurrentTimeElement::serializeTo(std::span target, std::uint32_t formatVersion) const { - std::ignore = formatVersion; - std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::CurrentTime)); pos = writeU16LE(target, pos, m_x); pos = writeU16LE(target, pos, m_y); pos = writeU16LE(target, pos, m_width); pos = writeU16LE(target, pos, m_height); + if (formatVersion >= 2) { + pos = writeU8LE(target, pos, static_cast(m_textFlags)); + pos = writeU8LE(target, pos, 0); + } pos = writeU16LE(target, pos, m_fontIndex); pos = writeU16LE(target, pos, m_utcOffset); pos = writeU16LE(target, pos, static_cast(m_flags)); + if (formatVersion >= 2) { + pos = writeU16LE(target, pos, 0); + } return alignNextWrite(target, pos, 4); } nlohmann::json CurrentTimeElement::serializeJSON(std::uint32_t formatVersion) const { - std::ignore = formatVersion; - nlohmann::json result; result["type"] = "CurrentTime"; result["x"] = m_x; result["y"] = m_y; result["width"] = m_width; result["height"] = m_height; + if (formatVersion >= 2) { + result["textFlags"] = m_textFlags; + } result["fontIndex"] = m_fontIndex; result["utcOffset"] = m_utcOffset; result["flags"] = m_flags; @@ -1742,6 +1963,10 @@ void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t showMinutes = true; } + std::optional bgColor; + bgColor = m_textFlags.dark; + bool fgColor = !m_textFlags.dark; + ClippedImage target{imageBuffer, m_x, m_y, m_width, m_height}; auto renderer = FontRenderer{fontData}.withIgnoreUnknownChars(true); @@ -1771,12 +1996,10 @@ void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t return; } - renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, true, false); + renderer.render(text, Point{0, static_cast(renderer.lineHeight() - 1)}, target, fgColor, bgColor); } std::expected, ParseError> CurrentTimeElement::parse(std::span& buffer, std::uint32_t formatVersion) { - std::ignore = formatVersion; - auto type = readU16LE(buffer); if (!type) { return std::unexpected(type.error()); @@ -1800,6 +2023,17 @@ std::expected, ParseError> CurrentTimeElemen if (!height) { return std::unexpected(height.error()); } + std::expected textFlags = 0; + if (formatVersion >= 2) { + textFlags = readU8LE(buffer); + if (!textFlags) { + return std::unexpected(textFlags.error()); + } + auto reserved1 = readU8LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + } auto fontIndex = readU16LE(buffer); if (!fontIndex) { return std::unexpected(fontIndex.error()); @@ -1812,22 +2046,30 @@ std::expected, ParseError> CurrentTimeElemen if (!flags) { return std::unexpected(flags.error()); } - auto result = std::make_unique(*x, *y, *width, *height, *fontIndex, *utcOffset, TimeDisplayFlags{*flags}); + if (formatVersion >= 2) { + auto reserved2 = readU16LE(buffer); + if (!reserved2) { + return std::unexpected(reserved2.error()); + } + } + auto result = std::make_unique(*x, *y, *width, *height, TextFlags{*textFlags}, *fontIndex, *utcOffset, TimeDisplayFlags{*flags}); return result; } std::unique_ptr CurrentTimeElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { - std::ignore = formatVersion; - std::uint16_t x = j.at("x"); std::uint16_t y = j.at("y"); std::uint16_t width = j.at("width"); std::uint16_t height = j.at("height"); + TextFlags textFlags; + if (formatVersion >= 2) { + textFlags = j.at("textFlags"); + } std::uint16_t fontIndex = j.at("fontIndex"); std::uint16_t utcOffset = j.at("utcOffset"); TimeDisplayFlags flags = j.at("flags"); - auto result = std::make_unique(x, y, width, height, fontIndex, utcOffset, flags); + auto result = std::make_unique(x, y, width, height, textFlags, fontIndex, utcOffset, flags); return result; } @@ -1992,6 +2234,7 @@ std::expected, ParseError> AlwaysDrawnSectio case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break; case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break; case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break; + case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break; case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break; case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break; case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break; @@ -2027,6 +2270,7 @@ std::unique_ptr AlwaysDrawnSection::parseJSON(nlohmann::json case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break; case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break; case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break; + case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break; case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break; case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break; case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break; @@ -2229,6 +2473,7 @@ std::expected, ParseError> TimeBasedDrawn case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break; case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break; case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break; + case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break; case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break; case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break; case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break; @@ -2268,6 +2513,7 @@ std::unique_ptr TimeBasedDrawnSection::parseJSON(nlohmann case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break; case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break; case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break; + case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break; case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break; case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break; case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break; @@ -2279,6 +2525,332 @@ std::unique_ptr TimeBasedDrawnSection::parseJSON(nlohmann return section; } +MultiTimeBasedDrawnSection::~MultiTimeBasedDrawnSection() { +} + +bool MultiTimeBasedDrawnSection::doesDrawOnFront() const noexcept { + return m_drawOnFront; +} + +bool MultiTimeBasedDrawnSection::doesDrawOnBack() const noexcept { + return m_drawOnBack; +} + +bool MultiTimeBasedDrawnSection::doesClearBeforeDrawing() const noexcept { + return m_clearBeforeDrawing; +} + +void MultiTimeBasedDrawnSection::setDrawOnFront(bool value) noexcept { + m_drawOnFront = value; +} + +void MultiTimeBasedDrawnSection::setDrawOnBack(bool value) noexcept { + m_drawOnBack = value; +} + +void MultiTimeBasedDrawnSection::setClearBeforeDrawing(bool value) noexcept { + m_clearBeforeDrawing = value; +} + +std::size_t MultiTimeBasedDrawnSection::elementCount() const noexcept { + return m_elements.size(); +} + +Element* MultiTimeBasedDrawnSection::elementAt(std::size_t index) const { + if (index < m_elements.size()) { + return m_elements[index].get(); + } else { + return nullptr; + } +} + +void MultiTimeBasedDrawnSection::appendElement(std::unique_ptr element) { + m_elements.push_back(std::move(element)); +} + +void MultiTimeBasedDrawnSection::insertElement(std::size_t index, std::unique_ptr element) { + if (index < m_elements.size()) { + m_elements.insert(m_elements.begin() + index, std::move(element)); + } else { + m_elements.push_back(std::move(element)); + } +} + +std::unique_ptr MultiTimeBasedDrawnSection::replaceElement(std::size_t index, std::unique_ptr element) { + std::unique_ptr result; + if (index < m_elements.size()) { + result = std::move(m_elements[index]); + m_elements[index] = std::move(element); + } + return result; +} + +std::unique_ptr MultiTimeBasedDrawnSection::eraseElement(std::size_t index) { + std::unique_ptr result; + if (index < m_elements.size()) { + result = std::move(m_elements[index]); + m_elements.erase(m_elements.begin() + index); + } + return result; +} + +std::vector> MultiTimeBasedDrawnSection::timeRanges() const { + return m_timeRanges; +} + +void MultiTimeBasedDrawnSection::setTimeRanges(std::vector> ranges) { + m_timeRanges = std::move(ranges); +} + +SectionType MultiTimeBasedDrawnSection::sectionType() const { + return SectionType::MultiTimeBasedDrawn; +} + +std::uint32_t MultiTimeBasedDrawnSection::minimumFormatVersion() const { + std::uint32_t result = 2; + for (auto const& element : m_elements) { + result = std::max(result, element->minimumFormatVersion()); + } + return result; +} + +std::size_t MultiTimeBasedDrawnSection::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::size_t pos = 0; + pos = writeU8LE(target, pos, static_cast(SectionType::MultiTimeBasedDrawn)); + // Will be replaced later + pos = writeU24LE(target, pos, 0); + std::uint16_t flags = 0; + maybeSetBit(flags, 0, m_drawOnFront); + maybeSetBit(flags, 1, m_drawOnBack); + maybeSetBit(flags, 2, m_clearBeforeDrawing); + pos = writeU16LE(target, pos, flags); + pos = writeU16LE(target, pos, m_elements.size()); + pos = writeU16LE(target, pos, m_timeRanges.size()); + pos = writeU16LE(target, pos, 0); + for (std::size_t i = 0; i < m_timeRanges.size(); ++i) { + pos = writeU64LE(target, pos, std::bit_cast(m_timeRanges[i].first)); + pos = writeU64LE(target, pos, std::bit_cast(m_timeRanges[i].second)); + } + for (auto const& element : m_elements) { + if (pos < target.size()) { + pos += element->serializeTo(target.subspan(pos), formatVersion); + } else { + pos += element->serializeTo({}, formatVersion); + } + } + pos = alignNextWrite(target, pos, 4); + std::ignore = writeU24LE(target, 1, pos); + return pos; +} + +nlohmann::json MultiTimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const { + nlohmann::json result; + result["type"] = "MultiTimeBasedDrawn"; + result["flags"]["drawOnFront"] = m_drawOnFront; + result["flags"]["drawOnBack"] = m_drawOnBack; + result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing; + result["timeRanges"] = m_timeRanges; + result["elements"] = nlohmann::json::array(); + for (auto const& element : m_elements) { + result["elements"].push_back(element->serializeJSON(formatVersion)); + } + return result; +} + +std::expected, ParseError> MultiTimeBasedDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + auto type = readU8LE(buffer); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::TimeBasedDrawn)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(buffer); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 24) { + return std::unexpected(ParseError::InvalidValue); + } + auto flags = readU16LE(buffer); + if (!flags) { + return std::unexpected(flags.error()); + } + auto elementCount = readU16LE(buffer); + if (!elementCount) { + return std::unexpected(elementCount.error()); + } + auto rangeCount = readU16LE(buffer); + if (!rangeCount) { + return std::unexpected(rangeCount.error()); + } + auto reserved1 = readU16LE(buffer); + if (!reserved1) { + return std::unexpected(reserved1.error()); + } + std::vector> timeRanges; + timeRanges.reserve(*rangeCount); + for (std::size_t i = 0; i < *rangeCount; ++i) { + auto startTimestamp = readU64LE(buffer); + if (!startTimestamp) { + return std::unexpected(startTimestamp.error()); + } + auto endTimestamp = readU64LE(buffer); + if (!endTimestamp) { + return std::unexpected(endTimestamp.error()); + } + timeRanges.push_back(std::make_pair(*startTimestamp, *endTimestamp)); + } + auto sectionData = buffer.subspan(0, *size - 24); + buffer = buffer.subspan(*size - 24); + auto section = std::make_unique(); + section->setDrawOnFront(isBitSet(*flags, 0)); + section->setDrawOnBack(isBitSet(*flags, 1)); + section->setClearBeforeDrawing(isBitSet(*flags, 2)); + section->setTimeRanges(std::move(timeRanges)); + section->m_elements.reserve(*elementCount); + for (std::size_t i = 0; i < *elementCount; ++i) { + auto type = peekU16LE(sectionData); + if (!type) { + return std::unexpected(type.error()); + } + std::expected, ParseError> element; + switch (static_cast(*type)) { + case ElementType::Image: element = ImageElement::parse(sectionData, formatVersion); break; + case ElementType::Animation: element = AnimationElement::parse(sectionData, formatVersion); break; + case ElementType::HScrollImage: element = HScrollImageElement::parse(sectionData, formatVersion); break; + case ElementType::VScrollImage: element = VScrollImageElement::parse(sectionData, formatVersion); break; + case ElementType::Line: element = LineElement::parse(sectionData, formatVersion); break; + case ElementType::Box: element = BoxElement::parse(sectionData, formatVersion); break; + case ElementType::ClippedText: element = ClippedTextElement::parse(sectionData, formatVersion); break; + case ElementType::HScrollText: element = HScrollTextElement::parse(sectionData, formatVersion); break; + case ElementType::CurrentTime: element = CurrentTimeElement::parse(sectionData, formatVersion); break; + default: + return std::unexpected(ParseError::InvalidValue); + } + if (!element) { + return std::unexpected(element.error()); + } + section->m_elements.push_back(std::move(*element)); + } + return section; +} + +std::unique_ptr MultiTimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + bool drawOnFront = j.at("flags").at("drawOnFront"); + bool drawOnBack = j.at("flags").at("drawOnBack"); + bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing"); + std::vector> timeRanges = j.at("timeRanges"); + + auto section = std::make_unique(); + section->setDrawOnFront(drawOnFront); + section->setDrawOnBack(drawOnBack); + section->setClearBeforeDrawing(clearBeforeDrawing); + section->setTimeRanges(std::move(timeRanges)); + + std::size_t const elementCount = j.at("elements").size(); + for (std::size_t i = 0; i < elementCount; ++i) { + nlohmann::json const& jElement = j.at("elements").at(i); + ElementType type = jElement.at("type"); + std::unique_ptr element; + switch (type) { + case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break; + case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break; + case ElementType::Box: element = BoxElement::parseJSON(jElement, formatVersion); break; + case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break; + case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break; + case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break; + default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section", + jElement.at("type").get())); + } + section->m_elements.push_back(std::move(element)); + } + return section; +} + +ExpiryDateSection::~ExpiryDateSection() { +} + +std::int64_t ExpiryDateSection::expiryDate() const noexcept { + return m_expiryDate; +} + +void ExpiryDateSection::setExpiryDate(std::int64_t value) noexcept { + m_expiryDate = value; +} + +SectionType ExpiryDateSection::sectionType() const { + return SectionType::CustomFont; +} + +std::uint32_t ExpiryDateSection::minimumFormatVersion() const { + return 2; +} + +std::size_t ExpiryDateSection::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + std::size_t pos = 0; + pos = writeU8LE(target, pos, static_cast(SectionType::ExpiryDate)); + // Will be replaced later + pos = writeU24LE(target, pos, 0); + pos = writeU64LE(target, pos, std::bit_cast(m_expiryDate)); + pos = alignNextWrite(target, pos, 4); + std::ignore = writeU24LE(target, 1, pos); + return pos; +} + +nlohmann::json ExpiryDateSection::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "ExpiryDate"; + result["expiryDate"] = m_expiryDate; + return result; +} + +std::expected, ParseError> ExpiryDateSection::parse(std::span& buffer, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + auto type = readU8LE(buffer); + if (!type) { + return std::unexpected(type.error()); + } + if (*type != static_cast(SectionType::CustomFont)) { + return std::unexpected(ParseError::InvalidValue); + } + auto size = readU24LE(buffer); + if (!size) { + return std::unexpected(size.error()); + } + if (*size < 8) { + return std::unexpected(ParseError::InvalidValue); + } + auto expiryDate = readU64LE(buffer); + if (!expiryDate) { + return std::unexpected(expiryDate.error()); + } + + auto section = std::make_unique(); + section->setExpiryDate(std::bit_cast(*expiryDate)); + return section; +} + +std::unique_ptr ExpiryDateSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::int64_t expiryDate = j.at("expiryDate"); + + auto section = std::make_unique(); + section->setExpiryDate(expiryDate); + return section; +} + CustomFontSection::~CustomFontSection() { } @@ -2421,7 +2993,7 @@ std::expected parseFile(std::span data) { if (!version) { return std::unexpected(version.error()); } - if (*version != 1) { + if (*version < 1 || *version > 2) { return std::unexpected(ParseError::InvalidValue); } auto sectionCount = readU16LE(data); @@ -2445,9 +3017,11 @@ std::expected parseFile(std::span data) { std::expected, ParseError> section; switch (static_cast(*sectionType)) { - case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parse(data, *version); break; - case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parse(data, *version); break; - case SectionType::CustomFont: section = CustomFontSection::parse(data, *version); break; + case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parse(data, *version); break; + case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parse(data, *version); break; + case SectionType::MultiTimeBasedDrawn: section = MultiTimeBasedDrawnSection::parse(data, *version); break; + case SectionType::ExpiryDate: section = ExpiryDateSection::parse(data, *version); break; + case SectionType::CustomFont: section = CustomFontSection::parse(data, *version); break; default: return std::unexpected(ParseError::InvalidValue); } @@ -2465,15 +3039,20 @@ File parseFileJSON(nlohmann::json const& j) { File result; std::uint32_t version = j.at("version"); + if (version < 1 || version > 2) { + throw std::runtime_error(std::format("Unsupported file format version {:d} encountered while parsing a file", version)); + } std::size_t const sectionCount = j.at("sections").size(); for (std::size_t i = 0; i < sectionCount; ++i) { nlohmann::json const& jSection = j.at("sections").at(i); SectionType sectionType = jSection.at("type"); std::unique_ptr
section; switch (sectionType) { - case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parseJSON(jSection, version); break; - case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parseJSON(jSection, version); break; - case SectionType::CustomFont: section = CustomFontSection::parseJSON(jSection, version); break; + case SectionType::AlwaysDrawn: section = AlwaysDrawnSection::parseJSON(jSection, version); break; + case SectionType::TimeBasedDrawn: section = TimeBasedDrawnSection::parseJSON(jSection, version); break; + case SectionType::MultiTimeBasedDrawn: section = MultiTimeBasedDrawnSection::parseJSON(jSection, version); break; + case SectionType::ExpiryDate: section = ExpiryDateSection::parseJSON(jSection, version); break; + case SectionType::CustomFont: section = CustomFontSection::parseJSON(jSection, version); break; default: throw std::runtime_error(std::format("Unsupported section type {:s} encountered while parsing a file", jSection.at("type").get())); } diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index d9904ab..41847d5 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -210,13 +210,43 @@ private: LineFlags m_flags; }; +struct BoxElement : public Element { + BoxElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, FillPattern fillPattern, FillFlags flags); + + std::uint16_t x() const noexcept; + std::uint16_t y() const noexcept; + std::uint16_t width() const noexcept; + std::uint16_t height() const noexcept; + FillPattern fillPattern() const noexcept; + FillFlags flags() const noexcept; + + virtual ~BoxElement() override; + virtual ElementType elementType() const override; + virtual std::uint32_t minimumFormatVersion() const override; + virtual std::size_t serializeTo(std::span target, std::uint32_t formatVersion) const override; + virtual nlohmann::json serializeJSON(std::uint32_t formatVersion) const 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, std::uint32_t formatVersion); + static std::unique_ptr parseJSON(nlohmann::json const& j, std::uint32_t formatVersion); + +private: + std::uint16_t m_x{}; + std::uint16_t m_y{}; + std::uint16_t m_width{}; + std::uint16_t m_height{}; + FillPattern m_fillPattern{}; + FillFlags m_flags; +}; + struct ClippedTextElement : public Element { - ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t fontIndex, std::string text); + ClippedTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::string text); std::uint16_t x() const noexcept; std::uint16_t y() const noexcept; std::uint16_t width() const noexcept; std::uint16_t height() const noexcept; + TextFlags textFlags() const noexcept; std::uint16_t fontIndex() const noexcept; std::string text() const; @@ -235,17 +265,19 @@ private: std::uint16_t m_y{}; std::uint16_t m_width{}; std::uint16_t m_height{}; + TextFlags m_textFlags; std::uint16_t m_fontIndex{}; std::string m_text; }; struct HScrollTextElement : public Element { - HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text); + HScrollTextElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, ScrollFlags flags, std::uint8_t scrollSpeed, std::uint16_t fontIndex, std::string text); std::uint16_t x() const noexcept; std::uint16_t y() const noexcept; std::uint16_t width() const noexcept; std::uint16_t height() const noexcept; + TextFlags textFlags() const noexcept; ScrollFlags flags() const noexcept; std::uint8_t scrollSpeed() const noexcept; std::uint16_t fontIndex() const noexcept; @@ -266,6 +298,7 @@ private: std::uint16_t m_y{}; std::uint16_t m_width{}; std::uint16_t m_height{}; + TextFlags m_textFlags; ScrollFlags m_flags; std::uint8_t m_scrollSpeed{}; std::uint16_t m_fontIndex{}; @@ -273,12 +306,13 @@ private: }; struct CurrentTimeElement : public Element { - 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, TimeDisplayFlags flags); + CurrentTimeElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, TextFlags textFlags, std::uint16_t fontIndex, std::uint16_t utcOffset, TimeDisplayFlags flags); std::uint16_t x() const noexcept; std::uint16_t y() const noexcept; std::uint16_t width() const noexcept; std::uint16_t height() const noexcept; + TextFlags textFlags() const noexcept; std::uint16_t fontIndex() const noexcept; std::uint16_t utcOffset() const noexcept; TimeDisplayFlags flags() const noexcept; @@ -298,6 +332,7 @@ private: std::uint16_t m_y{}; std::uint16_t m_width{}; std::uint16_t m_height{}; + TextFlags m_textFlags{}; std::uint16_t m_fontIndex{}; std::uint16_t m_utcOffset{}; TimeDisplayFlags m_flags; @@ -380,6 +415,64 @@ private: bool m_clearBeforeDrawing{true}; }; +struct MultiTimeBasedDrawnSection : public Section { + MultiTimeBasedDrawnSection() = default; + virtual ~MultiTimeBasedDrawnSection() override; + + bool doesDrawOnFront() const noexcept; + bool doesDrawOnBack() const noexcept; + bool doesClearBeforeDrawing() const noexcept; + + void setDrawOnFront(bool value) noexcept; + void setDrawOnBack(bool value) noexcept; + void setClearBeforeDrawing(bool value) noexcept; + + std::size_t elementCount() const noexcept; + 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::vector> timeRanges() const; + void setTimeRanges(std::vector> ranges); + + virtual SectionType sectionType() const override; + virtual std::uint32_t minimumFormatVersion() const override; + virtual std::size_t serializeTo(std::span target, std::uint32_t formatVersion) const override; + virtual nlohmann::json serializeJSON(std::uint32_t formatVersion) const override; + + static std::expected, ParseError> parse(std::span& buffer, std::uint32_t formatVersion); + static std::unique_ptr parseJSON(nlohmann::json const& j, std::uint32_t formatVersion); + +private: + std::vector> m_elements; + std::vector> m_timeRanges; + bool m_drawOnFront{true}; + bool m_drawOnBack{true}; + bool m_clearBeforeDrawing{true}; +}; + +struct ExpiryDateSection : public Section { + ExpiryDateSection() = default; + virtual ~ExpiryDateSection() override; + + std::int64_t expiryDate() const noexcept; + void setExpiryDate(std::int64_t value) noexcept; + + virtual SectionType sectionType() const override; + virtual std::uint32_t minimumFormatVersion() const override; + virtual std::size_t serializeTo(std::span target, std::uint32_t formatVersion) const override; + virtual nlohmann::json serializeJSON(std::uint32_t formatVersion) const override; + + static std::expected, ParseError> parse(std::span& buffer, std::uint32_t formatVersion); + static std::unique_ptr parseJSON(nlohmann::json const& j, std::uint32_t formatVersion); + +private: + std::int64_t m_expiryDate{}; +}; + struct CustomFontSection : public Section { CustomFontSection() = default; virtual ~CustomFontSection() override;