From 5e66961854a74240d41e64af45486b5b85519482 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Fri, 29 May 2026 21:47:51 +0200 Subject: [PATCH] C++: add JSON serialization and parsing support --- cpp/src/CMakeLists.txt | 5 + cpp/src/monoformat_bithelpers.hpp | 8 +- cpp/src/monoformat_schema.hpp | 2 + cpp/src/monoformat_structured.cpp | 629 +++++++++++++++++++++++++++++- cpp/src/monoformat_structured.hpp | 28 ++ 5 files changed, 667 insertions(+), 5 deletions(-) diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index c6076bb..2b9500f 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -15,6 +15,8 @@ set(monoformat_HEADERS option(MONOFORMAT_EMBEDDED "Only build for embedded devices" FALSE) if(NOT MONOFORMAT_EMBEDDED) + find_package(nlohmann_json REQUIRED) + list(APPEND monoformat_SOURCES monoformat_structured.cpp ) @@ -28,6 +30,9 @@ add_library(monoformat ${monoformat_SOURCES} ${monoformat_HEADERS}) target_compile_features(monoformat PUBLIC cxx_std_23) target_include_directories(monoformat PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +if(NOT MONOFORMAT_EMBEDDED) + target_link_libraries(monoformat PUBLIC nlohmann_json::nlohmann_json) +endif() install(TARGETS monoformat RUNTIME DESTINATION bin diff --git a/cpp/src/monoformat_bithelpers.hpp b/cpp/src/monoformat_bithelpers.hpp index 1af043b..ce1b58f 100644 --- a/cpp/src/monoformat_bithelpers.hpp +++ b/cpp/src/monoformat_bithelpers.hpp @@ -10,7 +10,7 @@ namespace monoformat { template constexpr inline bool isBitSet(Int value, OtherInt bit) { - if (bit < 0 || bit >= (sizeof(Int) * CHAR_BIT)) [[unlikely]] { + if (bit < 0 || bit >= static_cast(sizeof(Int) * CHAR_BIT)) [[unlikely]] { return false; } @@ -25,7 +25,7 @@ constexpr inline bool isBitSet(std::byte value, OtherInt bit) { template constexpr inline void setBit(Int& value, OtherInt bit) { - if (bit < 0 || bit >= (sizeof(Int) * CHAR_BIT)) [[unlikely]] { + if (bit < 0 || bit >= static_cast(sizeof(Int) * CHAR_BIT)) [[unlikely]] { return; } @@ -45,7 +45,7 @@ constexpr inline void setBit(std::byte& value, OtherInt bit) { template constexpr inline void unsetBit(Int& value, OtherInt bit) { - if (bit < 0 || bit >= (sizeof(Int) * CHAR_BIT)) [[unlikely]] { + if (bit < 0 || bit >= static_cast(sizeof(Int) * CHAR_BIT)) [[unlikely]] { return; } @@ -65,7 +65,7 @@ constexpr inline void unsetBit(std::byte& value, OtherInt bit) { template constexpr inline void maybeSetBit(Int& value, OtherInt bit, bool set) { - if (bit < 0 || bit >= (sizeof(Int) * CHAR_BIT)) [[unlikely]] { + if (bit < 0 || bit >= static_cast(sizeof(Int) * CHAR_BIT)) [[unlikely]] { return; } diff --git a/cpp/src/monoformat_schema.hpp b/cpp/src/monoformat_schema.hpp index 161c2dd..09f1136 100644 --- a/cpp/src/monoformat_schema.hpp +++ b/cpp/src/monoformat_schema.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace monoformat { @@ -62,6 +63,7 @@ struct LineFlags { explicit LineFlags(std::uint8_t flags) noexcept { + std::ignore = flags; } explicit operator std::uint8_t() const noexcept { diff --git a/cpp/src/monoformat_structured.cpp b/cpp/src/monoformat_structured.cpp index 585252c..ff1bee6 100644 --- a/cpp/src/monoformat_structured.cpp +++ b/cpp/src/monoformat_structured.cpp @@ -3,9 +3,51 @@ #include "monoformat_bithelpers.hpp" #include +#include +#include namespace monoformat { +static std::string base64Encode(std::span buffer); +static std::vector base64Decode(std::string_view data); + +NLOHMANN_JSON_SERIALIZE_ENUM(SectionType, { + {SectionType::AlwaysDrawn, "AlwaysDrawn"}, + {SectionType::TimeBasedDrawn, "TimeBasedDrawn"}, + {SectionType::CustomFont, "CustomFont"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ElementType, { + {ElementType::Image, "Image"}, + {ElementType::Animation, "Animation"}, + {ElementType::HScrollImage, "HScrollImage"}, + {ElementType::VScrollImage, "VScrollImage"}, + {ElementType::Line, "Line"}, + {ElementType::ClippedText, "ClippedText"}, + {ElementType::HScrollText, "HScrollText"}, + {ElementType::CurrentTime, "CurrentTime"}, +}) + +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_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(TimeDisplayFlags, use12h, showHours, showMinutes, showSeconds) + ImageElement::ImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height) : m_x{x} , m_y{y} @@ -66,6 +108,8 @@ std::uint32_t ImageElement::minimumFormatVersion() const { } std::size_t ImageElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::Image)); pos = writeU16LE(target, pos, m_x); @@ -77,6 +121,27 @@ std::size_t ImageElement::serializeTo(std::span target, std::uint32_t return alignNextWrite(target, pos, 4); } +nlohmann::json ImageElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "Image"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["image"] = nlohmann::json::array(); + + auto ownImage = image(); + for (std::uint16_t dy = 0; dy < m_height; ++dy) { + for (std::uint16_t dx = 0; dx < m_width; ++dx) { + result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0); + } + } + + return result; +} + void ImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = animationTick; std::ignore = currentTimestamp; @@ -134,6 +199,28 @@ std::expected, ParseError> ImageElement::parse(std return result; } +std::unique_ptr ImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t x = j.at("x"); + std::uint16_t y = j.at("y"); + std::uint16_t width = j.at("width"); + std::uint16_t height = j.at("height"); + std::vector pixels = j.at("image"); + if (pixels.size() != width * height) { + throw std::runtime_error(std::format("Error unserializing an Image element: the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, height, pixels.size(), width * height)); + } + auto result = std::make_unique(x, y, width, height); + auto image = result->image(); + for (std::uint16_t dy = 0; dy < height; ++dy) { + std::size_t base = static_cast(dy) * width; + for (std::uint16_t dx = 0; dx < width; ++dx) { + image.setPixel(dx, dy, !!pixels[base + dx]); + } + } + return result; +} + AnimationElement::AnimationElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t numberOfFrames, std::uint16_t updateInterval) : m_x{x} , m_y{y} @@ -220,6 +307,8 @@ std::uint32_t AnimationElement::minimumFormatVersion() const { } std::size_t AnimationElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::Animation)); pos = writeU16LE(target, pos, m_x); @@ -233,6 +322,32 @@ std::size_t AnimationElement::serializeTo(std::span target, std::uint return alignNextWrite(target, pos, 4); } +nlohmann::json AnimationElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "Animation"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["updateInterval"] = m_updateInterval; + result["frames"] = nlohmann::json::array(); + + for (std::uint16_t f = 0; f < m_numberOfFrames; ++f) { + nlohmann::json frame = nlohmann::json::array(); + auto ownImage = image(f); + for (std::uint16_t dy = 0; dy < m_height; ++dy) { + for (std::uint16_t dx = 0; dx < m_width; ++dx) { + frame.push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0); + } + } + result["frames"].push_back(frame); + } + + return result; +} + void AnimationElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = currentTimestamp; std::ignore = customFonts; @@ -305,6 +420,38 @@ std::expected, ParseError> AnimationElement::p return result; } +std::unique_ptr AnimationElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t x = j.at("x"); + std::uint16_t y = j.at("y"); + std::uint16_t width = j.at("width"); + std::uint16_t height = j.at("height"); + std::uint16_t updateInterval = j.at("updateInterval"); + std::vector> frames = j.at("frames"); + + if (frames.size() > std::numeric_limits::max()) { + throw std::runtime_error(std::format("Error unserializing an Animation element: the number of frames, {:d}, is too large", frames.size())); + } + for (std::size_t i = 0; i < frames.size(); ++i) { + if (frames[i].size() != width * height) { + throw std::runtime_error(std::format("Error unserializing an Animation element: in frame {:d}, the width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", i, width, height, frames[i].size(), width * height)); + } + } + + auto result = std::make_unique(x, y, width, height, static_cast(frames.size()), updateInterval); + for (std::size_t i = 0; i < frames.size(); ++i) { + auto image = result->image(i); + for (std::uint16_t dy = 0; dy < height; ++dy) { + std::size_t base = static_cast(dy) * width; + for (std::uint16_t dx = 0; dx < width; ++dx) { + image.setPixel(dx, dy, !!frames[i][base + dx]); + } + } + } + return result; +} + HScrollImageElement::HScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentWidth, ScrollFlags flags, std::uint8_t scrollSpeed) : m_x{x} , m_y{y} @@ -380,6 +527,8 @@ std::uint32_t HScrollImageElement::minimumFormatVersion() const { } std::size_t HScrollImageElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::HScrollImage)); pos = writeU16LE(target, pos, m_x); @@ -394,6 +543,30 @@ std::size_t HScrollImageElement::serializeTo(std::span target, std::u return alignNextWrite(target, pos, 4); } +nlohmann::json HScrollImageElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "HScrollImage"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["contentWidth"] = m_contentWidth; + result["flags"] = m_flags; + result["scrollSpeed"] = int(m_scrollSpeed); + result["image"] = nlohmann::json::array(); + + auto ownImage = image(); + for (std::uint16_t dy = 0; dy < m_height; ++dy) { + for (std::uint16_t dx = 0; dx < m_contentWidth; ++dx) { + result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0); + } + } + + return result; +} + void HScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = currentTimestamp; std::ignore = customFonts; @@ -555,6 +728,35 @@ std::expected, ParseError> HScrollImageElem return result; } +std::unique_ptr HScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t x = j.at("x"); + std::uint16_t y = j.at("y"); + std::uint16_t width = j.at("width"); + std::uint16_t height = j.at("height"); + std::uint16_t contentWidth = j.at("contentWidth"); + ScrollFlags flags = j.at("flags"); + int scrollSpeed = j.at("scrollSpeed"); + std::vector pixels = j.at("image"); + if (scrollSpeed < 0 || scrollSpeed > 255) { + throw std::runtime_error(std::format("Error unserializing an HScrollImage element: invalid scroll speed {:d} specified", scrollSpeed)); + } + if (pixels.size() != contentWidth * height) { + throw std::runtime_error(std::format("Error unserializing an HScrollImage element: the content width, {:d}, and height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", contentWidth, height, pixels.size(), contentWidth * height)); + } + + auto result = std::make_unique(x, y, width, height, contentWidth, flags, static_cast(scrollSpeed)); + auto image = result->image(); + for (std::uint16_t dy = 0; dy < height; ++dy) { + std::size_t base = static_cast(dy) * contentWidth; + for (std::uint16_t dx = 0; dx < contentWidth; ++dx) { + image.setPixel(dx, dy, !!pixels[base + dx]); + } + } + return result; +} + VScrollImageElement::VScrollImageElement(std::uint16_t x, std::uint16_t y, std::uint16_t width, std::uint16_t height, std::uint16_t contentHeight, ScrollFlags flags, std::uint8_t scrollSpeed) : m_x{x} , m_y{y} @@ -630,6 +832,8 @@ std::uint32_t VScrollImageElement::minimumFormatVersion() const { } std::size_t VScrollImageElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::VScrollImage)); pos = writeU16LE(target, pos, m_x); @@ -644,6 +848,30 @@ std::size_t VScrollImageElement::serializeTo(std::span target, std::u return alignNextWrite(target, pos, 4); } +nlohmann::json VScrollImageElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "VScrollImage"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["contentHeight"] = m_contentHeight; + result["flags"] = m_flags; + result["scrollSpeed"] = int(m_scrollSpeed); + result["image"] = nlohmann::json::array(); + + auto ownImage = image(); + for (std::uint16_t dy = 0; dy < m_contentHeight; ++dy) { + for (std::uint16_t dx = 0; dx < m_width; ++dx) { + result["image"].push_back(ownImage.isPixelSet(dx, dy) ? 1 : 0); + } + } + + return result; +} + void VScrollImageElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = currentTimestamp; std::ignore = customFonts; @@ -805,6 +1033,34 @@ std::expected, ParseError> VScrollImageElem return result; } +std::unique_ptr VScrollImageElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t x = j.at("x"); + std::uint16_t y = j.at("y"); + std::uint16_t width = j.at("width"); + std::uint16_t height = j.at("height"); + std::uint16_t contentHeight = j.at("contentHeight"); + ScrollFlags flags = j.at("flags"); + int scrollSpeed = j.at("scrollSpeed"); + std::vector pixels = j.at("image"); + if (scrollSpeed < 0 || scrollSpeed > 255) { + throw std::runtime_error(std::format("Error unserializing an VScrollImage element: invalid scroll speed {:d} specified", scrollSpeed)); + } + if (pixels.size() != width * contentHeight) { + throw std::runtime_error(std::format("Error unserializing an VScrollImage element: the width, {:d}, and content height {:d}, of the image don't match the number of pixels in the image data, {:d}, expected {:d} instead", width, contentHeight, pixels.size(), width * contentHeight)); + } + + auto result = std::make_unique(x, y, width, height, contentHeight, flags, static_cast(scrollSpeed)); + auto image = result->image(); + for (std::uint16_t dy = 0; dy < contentHeight; ++dy) { + std::size_t base = static_cast(dy) * width; + for (std::uint16_t dx = 0; dx < width; ++dx) { + image.setPixel(dx, dy, !!pixels[base + dx]); + } + } + return result; +} LineElement::LineElement(std::uint16_t originX, std::uint16_t originY, std::uint16_t targetX, std::uint16_t targetY, LineStyle lineStyle, LineFlags flags) : m_originX{originX} @@ -852,6 +1108,8 @@ std::uint32_t LineElement::minimumFormatVersion() const { } std::size_t LineElement::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU16LE(target, pos, static_cast(ElementType::Line)); pos = writeU16LE(target, pos, m_originX); @@ -863,6 +1121,20 @@ std::size_t LineElement::serializeTo(std::span target, std::uint32_t return alignNextWrite(target, pos, 4); } +nlohmann::json LineElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "Line"; + result["originX"] = m_originX; + result["originY"] = m_originY; + result["targetX"] = m_targetX; + result["targetY"] = m_targetY; + result["lineStyle"] = m_lineStyle; + result["flags"] = m_flags; + return result; +} + void LineElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = animationTick; std::ignore = currentTimestamp; @@ -930,6 +1202,20 @@ std::expected, ParseError> LineElement::parse(std:: return result; } +std::unique_ptr LineElement::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + std::uint16_t originX = j.at("originX"); + std::uint16_t originY = j.at("originY"); + std::uint16_t targetX = j.at("targetX"); + std::uint16_t targetY = j.at("targetY"); + LineStyle lineStyle = j.at("lineStyle"); + LineFlags flags = j.at("flags"); + + auto result = std::make_unique(originX, originY, targetX, targetY, lineStyle, flags); + return result; +} + 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} @@ -976,6 +1262,8 @@ std::uint32_t ClippedTextElement::minimumFormatVersion() const { } 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); @@ -988,6 +1276,20 @@ std::size_t ClippedTextElement::serializeTo(std::span target, std::ui return alignNextWrite(target, pos, 4); } +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; + result["fontIndex"] = m_fontIndex; + result["text"] = m_text; + return result; +} + void ClippedTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = animationTick; std::ignore = currentTimestamp; @@ -1056,6 +1358,20 @@ std::expected, ParseError> ClippedTextElemen 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"); + 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)); + 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) : m_x{x} , m_y{y} @@ -1112,6 +1428,8 @@ std::uint32_t HScrollTextElement::minimumFormatVersion() const { } 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); @@ -1126,6 +1444,22 @@ std::size_t HScrollTextElement::serializeTo(std::span target, std::ui return alignNextWrite(target, pos, 4); } +nlohmann::json HScrollTextElement::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "HScrollText"; + result["x"] = m_x; + result["y"] = m_y; + result["width"] = m_width; + result["height"] = m_height; + result["flags"] = m_flags; + result["scrollSpeed"] = int(m_scrollSpeed); + result["fontIndex"] = m_fontIndex; + result["text"] = m_text; + return result; +} + void HScrollTextElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = currentTimestamp; @@ -1282,6 +1616,27 @@ std::expected, ParseError> HScrollTextElemen 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"); + ScrollFlags flags = j.at("flags"); + int scrollSpeed = j.at("scrollSpeed"); + std::uint16_t fontIndex = j.at("fontIndex"); + std::string text = j.at("text"); + + if (scrollSpeed < 0 || scrollSpeed > 255) { + throw std::runtime_error(std::format("Error unserializing an HScrollText element: invalid scroll speed {:d} specified", scrollSpeed)); + } + + auto result = std::make_unique(x, y, width, height, 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) : m_x{x} , m_y{y} @@ -1333,6 +1688,8 @@ 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); @@ -1345,6 +1702,21 @@ std::size_t CurrentTimeElement::serializeTo(std::span target, std::ui 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; + result["fontIndex"] = m_fontIndex; + result["utcOffset"] = m_utcOffset; + result["flags"] = m_flags; + return result; +} + void CurrentTimeElement::drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) { std::ignore = animationTick; @@ -1444,6 +1816,21 @@ std::expected, ParseError> CurrentTimeElemen 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"); + 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); + return result; +} + AlwaysDrawnSection::~AlwaysDrawnSection() { } @@ -1548,9 +1935,22 @@ std::size_t AlwaysDrawnSection::serializeTo(std::span target, std::ui return pos; } -std::expected, ParseError> AlwaysDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { +nlohmann::json AlwaysDrawnSection::serializeJSON(std::uint32_t formatVersion) const { std::ignore = formatVersion; + nlohmann::json result; + result["type"] = "AlwaysDrawn"; + result["flags"]["drawOnFront"] = m_drawOnFront; + result["flags"]["drawOnBack"] = m_drawOnBack; + result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing; + result["elements"] = nlohmann::json::array(); + for (auto const& element : m_elements) { + result["elements"].push_back(element->serializeJSON(formatVersion)); + } + return result; +} + +std::expected, ParseError> AlwaysDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { auto type = readU8LE(buffer); if (!type) { return std::unexpected(type.error()); @@ -1606,6 +2006,38 @@ std::expected, ParseError> AlwaysDrawnSectio return section; } +std::unique_ptr AlwaysDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + bool drawOnFront = j.at("flags").at("drawOnFront"); + bool drawOnBack = j.at("flags").at("drawOnBack"); + bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing"); + + auto section = std::make_unique(); + section->setDrawOnFront(drawOnFront); + section->setDrawOnBack(drawOnBack); + section->setClearBeforeDrawing(clearBeforeDrawing); + + std::size_t const elementCount = j.at("elements").size(); + for (std::size_t i = 0; i < elementCount; ++i) { + nlohmann::json const& jElement = j.at("elements").at(i); + ElementType type = jElement.at("type"); + std::unique_ptr element; + switch (type) { + case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break; + case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break; + case ElementType::ClippedText: element = ClippedTextElement::parseJSON(jElement, formatVersion); break; + case ElementType::HScrollText: element = HScrollTextElement::parseJSON(jElement, formatVersion); break; + case ElementType::CurrentTime: element = CurrentTimeElement::parseJSON(jElement, formatVersion); break; + default: throw std::runtime_error(std::format("Unsupported element type {:s} encountered while parsing a section", + jElement.at("type").get())); + } + section->m_elements.push_back(std::move(element)); + } + return section; +} + TimeBasedDrawnSection::~TimeBasedDrawnSection() { } @@ -1728,6 +2160,21 @@ std::size_t TimeBasedDrawnSection::serializeTo(std::span target, std: return pos; } +nlohmann::json TimeBasedDrawnSection::serializeJSON(std::uint32_t formatVersion) const { + nlohmann::json result; + result["type"] = "TimeBasedDrawn"; + result["flags"]["drawOnFront"] = m_drawOnFront; + result["flags"]["drawOnBack"] = m_drawOnBack; + result["flags"]["clearBeforeDrawing"] = m_clearBeforeDrawing; + result["startTimestamp"] = m_startTimestamp; + result["endTimestamp"] = m_endTimestamp; + result["elements"] = nlohmann::json::array(); + for (auto const& element : m_elements) { + result["elements"].push_back(element->serializeJSON(formatVersion)); + } + return result; +} + std::expected, ParseError> TimeBasedDrawnSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; @@ -1796,6 +2243,42 @@ std::expected, ParseError> TimeBasedDrawn return section; } +std::unique_ptr TimeBasedDrawnSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + bool drawOnFront = j.at("flags").at("drawOnFront"); + bool drawOnBack = j.at("flags").at("drawOnBack"); + bool clearBeforeDrawing = j.at("flags").at("clearBeforeDrawing"); + std::uint64_t startTimestamp = j.at("startTimestamp"); + std::uint64_t endTimestamp = j.at("endTimestamp"); + + auto section = std::make_unique(); + section->setDrawOnFront(drawOnFront); + section->setDrawOnBack(drawOnBack); + section->setClearBeforeDrawing(clearBeforeDrawing); + section->setStartTimestamp(startTimestamp); + section->setEndTimestamp(endTimestamp); + + std::size_t const elementCount = j.at("elements").size(); + for (std::size_t i = 0; i < elementCount; ++i) { + nlohmann::json const& jElement = j.at("elements").at(i); + ElementType type = jElement.at("type"); + std::unique_ptr element; + switch (type) { + case ElementType::Image: element = ImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Animation: element = AnimationElement::parseJSON(jElement, formatVersion); break; + case ElementType::HScrollImage: element = HScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::VScrollImage: element = VScrollImageElement::parseJSON(jElement, formatVersion); break; + case ElementType::Line: element = LineElement::parseJSON(jElement, formatVersion); break; + case ElementType::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; +} + CustomFontSection::~CustomFontSection() { } @@ -1817,6 +2300,8 @@ std::uint32_t CustomFontSection::minimumFormatVersion() const { } std::size_t CustomFontSection::serializeTo(std::span target, std::uint32_t formatVersion) const { + std::ignore = formatVersion; + std::size_t pos = 0; pos = writeU8LE(target, pos, static_cast(SectionType::AlwaysDrawn)); // Will be replaced later @@ -1829,6 +2314,15 @@ std::size_t CustomFontSection::serializeTo(std::span target, std::uin return pos; } +nlohmann::json CustomFontSection::serializeJSON(std::uint32_t formatVersion) const { + std::ignore = formatVersion; + + nlohmann::json result; + result["type"] = "CustomFont"; + result["data"] = base64Encode(m_fontData); + return result; +} + std::expected, ParseError> CustomFontSection::parse(std::span& buffer, std::uint32_t formatVersion) { std::ignore = formatVersion; @@ -1867,6 +2361,16 @@ std::expected, ParseError> CustomFontSection: return section; } +std::unique_ptr CustomFontSection::parseJSON(nlohmann::json const& j, std::uint32_t formatVersion) { + std::ignore = formatVersion; + + auto fontData = base64Decode(j.at("data").get()); + + auto section = std::make_unique(); + section->setFontData(fontData); + return section; +} + std::size_t serializeFile(std::span& buffer, File const& file) { std::size_t pos = 0; std::uint32_t formatVersion = 1; @@ -1891,6 +2395,20 @@ std::size_t serializeFile(std::span& buffer, File const& file) { return pos; } +nlohmann::json serializeFileJSON(File const& file) { + std::uint32_t formatVersion = 1; + for (auto const& section : file.sections) { + formatVersion = std::max(formatVersion, section->minimumFormatVersion()); + } + nlohmann::json result; + result["version"] = formatVersion; + result["sections"] = nlohmann::json::array(); + for (auto const& section : file.sections) { + result["sections"].push_back(section->serializeJSON(formatVersion)); + } + return result; +} + std::expected parseFile(std::span data) { auto magic = readU32LE(data); if (!magic) { @@ -1943,4 +2461,113 @@ std::expected parseFile(std::span data) { return result; } +File parseFileJSON(nlohmann::json const& j) { + File result; + + std::uint32_t version = j.at("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; + default: throw std::runtime_error(std::format("Unsupported section type {:s} encountered while parsing a file", + jSection.at("type").get())); + } + result.sections.push_back(std::move(section)); + } + + return result; +} + +constexpr static char const* const Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +constexpr static std::int8_t const Base64ParseLookup[256] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + +std::string base64Encode(std::span buffer) { + std::string result; + + uint8_t const* data = reinterpret_cast(buffer.data()); + std::size_t numberOfBlocks = (buffer.size() + 2) / 3; + result.reserve(numberOfBlocks); + + for (std::size_t i = 0; i < numberOfBlocks; ++i) { + std::size_t ii = i * 3; + std::size_t ni = std::min(ii + 3, buffer.size()) - ii; + + int c0 = (((data[ii + 0] >> 2) & 0x3f) << 0); + if (ni == 3) { + int c1 = (((data[ii + 0] >> 0) & 0x03) << 4) + | (((data[ii + 1] >> 4) & 0x0f) << 0); + int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2) + | (((data[ii + 2] >> 6) & 0x03) << 0); + int c3 = (((data[ii + 2] >> 0) & 0x3f) << 0); + result.push_back(Base64Alphabet[c0]); + result.push_back(Base64Alphabet[c1]); + result.push_back(Base64Alphabet[c2]); + result.push_back(Base64Alphabet[c3]); + } else if (ni == 2) { + int c1 = (((data[ii + 0] >> 0) & 0x03) << 4) + | (((data[ii + 1] >> 4) & 0x0f) << 0); + int c2 = (((data[ii + 1] >> 0) & 0x0f) << 2); + result.push_back(Base64Alphabet[c0]); + result.push_back(Base64Alphabet[c1]); + result.push_back(Base64Alphabet[c2]); + } else if (ni == 1) { + int c1 = (((data[ii + 0] >> 0) & 0x03) << 4); + result.push_back(Base64Alphabet[c0]); + result.push_back(Base64Alphabet[c1]); + } + } + return result; +} + +std::vector base64Decode(std::string_view data) { + std::vector result; + // This is just an initial estimate + std::size_t numberOfBlocks = (data.size() + 3) / 4; + result.reserve(numberOfBlocks * 3); + + int state = 0; + int currentValue = 0; + for (std::size_t i = 0; i < data.size(); ++i) { + int value = Base64ParseLookup[static_cast(data[i])]; + if (value == -2) { + // '=': padding at the end + break; + } else if (value == -1) { + // any invalid character (especially whitespace): ignore + continue; + } + + switch (state) { + case 0: + default: + currentValue = value << 2; + break; + case 1: + currentValue |= (value >> 4) & 0x03; + result.push_back(static_cast(currentValue)); + currentValue = (value & 0x0f) << 4; + break; + case 2: + currentValue |= (value >> 2) & 0x0f; + result.push_back(static_cast(currentValue)); + currentValue = (value & 0x03) << 6; + break; + case 3: + currentValue |= value & 0x3f; + result.push_back(static_cast(currentValue)); + currentValue = 0; + break; + } + state = (state + 1) % 4; + } + + // Ignore any errors here + return result; +} + } // namespace monoformat diff --git a/cpp/src/monoformat_structured.hpp b/cpp/src/monoformat_structured.hpp index ff30d72..d9904ab 100644 --- a/cpp/src/monoformat_structured.hpp +++ b/cpp/src/monoformat_structured.hpp @@ -4,6 +4,8 @@ #include "monoformat_parsehelpers.hpp" #include "monoformat_schema.hpp" +#include + #include #include #include @@ -17,6 +19,7 @@ struct Section { virtual SectionType sectionType() const = 0; virtual std::uint32_t minimumFormatVersion() const = 0; virtual std::size_t serializeTo(std::span target, std::uint32_t formatVersion) const = 0; + virtual nlohmann::json serializeJSON(std::uint32_t formatVersion) const = 0; protected: Section() = default; @@ -29,6 +32,7 @@ struct Element { virtual ElementType elementType() const = 0; virtual std::uint32_t minimumFormatVersion() const = 0; virtual std::size_t serializeTo(std::span target, std::uint32_t formatVersion) const = 0; + virtual nlohmann::json serializeJSON(std::uint32_t formatVersion) const = 0; virtual void drawTo(OneBitBufferInterface* imageBuffer, std::size_t animationTick, std::int64_t currentTimestamp, std::span customFonts) = 0; protected: @@ -52,9 +56,11 @@ struct ImageElement : public Element { 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{}; @@ -85,9 +91,11 @@ struct AnimationElement : public Element { 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{}; @@ -119,9 +127,11 @@ struct HScrollImageElement : public Element { 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{}; @@ -154,9 +164,11 @@ struct VScrollImageElement : public Element { 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{}; @@ -183,9 +195,11 @@ struct LineElement : public Element { 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_originX{}; @@ -210,9 +224,11 @@ struct ClippedTextElement : public Element { 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{}; @@ -239,9 +255,11 @@ struct HScrollTextElement : public Element { 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{}; @@ -269,9 +287,11 @@ struct CurrentTimeElement : public Element { 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{}; @@ -306,8 +326,10 @@ struct AlwaysDrawnSection : public Section { 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; @@ -344,8 +366,10 @@ struct TimeBasedDrawnSection : public Section { 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; @@ -366,8 +390,10 @@ struct CustomFontSection : public Section { 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_fontData; @@ -378,7 +404,9 @@ struct File { }; extern std::size_t serializeFile(std::span& buffer, File const& file); +extern nlohmann::json serializeFileJSON(File const& file); extern std::expected parseFile(std::span data); +extern File parseFileJSON(nlohmann::json const& j); } // namespace monoformat