From 5ddd54dd6d40f0bb81a8813955ed3c481e162f25 Mon Sep 17 00:00:00 2001 From: jmsgrogan Date: Wed, 25 Jan 2023 16:51:36 +0000 Subject: [PATCH] Add initial font metrics and equation rendering. --- src/base/core/encoding/UnicodeUtils.cpp | 50 ++++ src/base/core/encoding/UnicodeUtils.h | 9 + src/publishing/latex/LatexMathExpression.cpp | 245 ++++++++++++++++++ src/publishing/latex/LatexMathExpression.h | 81 ++++-- src/publishing/latex/LatexSymbols.cpp | 17 ++ src/publishing/latex/LatexSymbols.h | 33 ++- src/publishing/plotting/EquationNode.cpp | 76 ++++++ src/publishing/plotting/EquationNode.h | 26 ++ src/rendering/fonts/BasicFontEngine.h | 5 + src/rendering/fonts/FontsManager.cpp | 23 +- src/rendering/fonts/IFontEngine.h | 4 + .../fonts/directx/DirectWriteFontEngine.cpp | 118 +++++++++ .../fonts/directx/DirectWriteFontEngine.h | 18 ++ .../visual_elements/nodes/TextNode.cpp | 93 ++++++- .../visual_elements/nodes/TextNode.h | 13 +- .../visual_elements/svg/SvgPainter.cpp | 15 +- test/CMakeLists.txt | 2 +- test/fonts/CMakeLists.txt | 25 +- test/fonts/TestDirectWriteFontEngine.cpp | 14 + test/graphics/CMakeLists.txt | 1 + test/graphics/TestTextRendering.cpp | 22 ++ test/publishing/TestLatexConverter.cpp | 18 +- test/test_utils/TestCaseRunner.cpp | 15 +- test/test_utils/TestRenderUtils.h | 8 + 24 files changed, 868 insertions(+), 63 deletions(-) create mode 100644 test/graphics/TestTextRendering.cpp diff --git a/src/base/core/encoding/UnicodeUtils.cpp b/src/base/core/encoding/UnicodeUtils.cpp index a6e8937..6218571 100644 --- a/src/base/core/encoding/UnicodeUtils.cpp +++ b/src/base/core/encoding/UnicodeUtils.cpp @@ -39,3 +39,53 @@ std::wstring UnicodeUtils::utf8ToUtf16WString(const std::string& input) throw std::logic_error("Not implemented"); #endif } + +std::vector UnicodeUtils::utf8ToUtf32(const std::string& input) +{ + const auto utf_16 = utf8ToUtf16WString(input); + + std::vector output; + std::size_t pos = 0; + while (pos < utf_16.size()) + { + const auto c = utf_16[pos]; + pos++; + if (!isSurrogate(c)) + { + output.push_back(c); + } + else + { + if (isHighSurrogate(c) && pos < utf_16.size() && isLowSurrogate(utf_16[pos])) + { + output.push_back(surrogateToUtf32(c, utf_16[pos])); + pos++; + } + else + { + throw std::logic_error("Unexpected UTF16 content given for conversion to UTF32"); + } + } + } + return output; +} + +bool UnicodeUtils::isSurrogate(wchar_t c) +{ + return (c - 0xd800u) < 2048u; +} + +bool UnicodeUtils::isHighSurrogate(wchar_t c) +{ + return (c & 0xfffffc00) == 0xd800; +} + +bool UnicodeUtils::isLowSurrogate(wchar_t c) +{ + return (c & 0xfffffc00) == 0xdc00; +} + +uint32_t UnicodeUtils::surrogateToUtf32(wchar_t high, wchar_t low) +{ + return (high << 10) + low - 0x35fdc00; +} \ No newline at end of file diff --git a/src/base/core/encoding/UnicodeUtils.h b/src/base/core/encoding/UnicodeUtils.h index 48539be..9b4a684 100644 --- a/src/base/core/encoding/UnicodeUtils.h +++ b/src/base/core/encoding/UnicodeUtils.h @@ -1,6 +1,7 @@ #pragma once #include +#include class UnicodeUtils { @@ -8,4 +9,12 @@ public: static std::string utf16ToUtf8String(const std::wstring& input); static std::wstring utf8ToUtf16WString(const std::string& input); + + static std::vector utf8ToUtf32(const std::string& input); + +private: + static bool isSurrogate(wchar_t c); + static bool isHighSurrogate(wchar_t c); + static bool isLowSurrogate(wchar_t c); + static uint32_t surrogateToUtf32(wchar_t high, wchar_t low); }; \ No newline at end of file diff --git a/src/publishing/latex/LatexMathExpression.cpp b/src/publishing/latex/LatexMathExpression.cpp index e69de29..78e2bb9 100644 --- a/src/publishing/latex/LatexMathExpression.cpp +++ b/src/publishing/latex/LatexMathExpression.cpp @@ -0,0 +1,245 @@ +#include "LatexMathExpression.h" + +#include "FileLogger.h" + +LatexMathExpression::LatexMathExpression(const std::string& expression) + : mRawBody(expression), + mType(Type::LINEAR) +{ + MLOG_INFO("Adding expression with content: " << expression); + parse(); +} + +LatexMathExpression::LatexMathExpression(Type type) + : mType(type) +{ + +} + +LatexMathExpression::~LatexMathExpression() +{ + +} + +void LatexMathExpression::onTagBodyChar(char c) +{ + if (c == '}') + { + onRBrace(); + } + else + { + mWorkingString += c; + } +} + +void LatexMathExpression::onFreeChar(char c) +{ + if (c == '\\') + { + mLineState = LineState::IN_TAG_NAME; + } + else if (c == '{') + { + onLBrace(); + } + else if (c == '}') + { + onRBrace(); + } + else if (c == '^') + { + onSuperscript(); + } + else if (c == '_') + { + onSubscript(); + } + else if (std::isblank(c)) + { + onSpace(); + } + else + { + mWorkingString += c; + } +} + +void LatexMathExpression::parse() +{ + mLineState = LineState::NONE; + mWorkingString.clear(); + + for (auto c : mRawBody) + { + if (mLineState == LineState::IN_TAG_BODY) + { + onTagBodyChar(c); + } + else + { + onFreeChar(c); + } + } + onSpace(); + onCloseExpression(); +} + +void LatexMathExpression::onSpace() +{ + if (mLineState == LineState::IN_TAG_NAME) + { + onFinishedTagName(); + } + else + { + onLiteral(); + } + mWorkingString.clear(); +} + +void LatexMathExpression::onLBrace() +{ + if (mLineState == LineState::IN_TAG_NAME) + { + onFinishedTagName(); + } + else if (mLineState == LineState::AWAITING_LBRACE) + { + mLineState = LineState::IN_TAG_BODY; + } + else if (mLineState == LineState::IN_TAG_BODY) + { + mOpenBraceCount++; + } + else + { + mWorkingString += "{"; + } +} + +void LatexMathExpression::onRBrace() +{ + if (mLineState == LineState::IN_TAG_BODY) + { + if (mOpenBraceCount == 0) + { + onTagBody(); + } + else + { + mOpenBraceCount--; + mWorkingString += "}"; + } + } + else + { + mWorkingString += "}"; + } +} + +void LatexMathExpression::setRawContent(const std::string& content) +{ + auto expression = std::make_unique(content); + mExpressions.push_back(std::move(expression)); +} + +void LatexMathExpression::onTagBody() +{ + onCloseExpression(); +} + +void LatexMathExpression::onSuperscript() +{ + +} + +void LatexMathExpression::onSubscript() +{ + +} + +void LatexMathExpression::onLiteral() +{ + LatexMathSymbol symbol{ LatexMathSymbol::Type::RAW, mWorkingString, mWorkingString }; + mWorkingSymbols.push_back(symbol); +} + +void LatexMathExpression::onFinishedTagName() +{ + if (auto lookup = LatexSymbolLookup::getKnownTag(mWorkingString); lookup) + { + if (mLineState == LineState::AWAITING_LBRACE || mLineState == LineState::IN_TAG_NAME) + { + onCloseExpression(); + + if (*lookup == LatexMathSymbol::Tag::FRAC) + { + mWorkingType = Type::FRACTION; + auto expression = std::make_unique(mWorkingType); + mWorkingExpression = expression.get(); + mExpressions.push_back(std::move(expression)); + mWorkingString.clear(); + } + + mLineState = LineState::IN_TAG_BODY; + } + } + else if (auto lookup = LatexSymbolLookup::getSymbolUtf8(mWorkingString); lookup) + { + LatexMathSymbol symbol{ LatexMathSymbol::Type::TAG, mWorkingString, *lookup }; + mWorkingSymbols.push_back(symbol); + mLineState = LineState::NONE; + } +} + +void LatexMathExpression::onCloseExpression() +{ + if (mWorkingType == Type::LEAF) + { + if (!mWorkingSymbols.empty()) + { + auto expression = std::make_unique(mWorkingType); + expression->setContent(mWorkingSymbols); + mExpressions.push_back(std::move(expression)); + } + } + else if (mWorkingType == Type::FRACTION) + { + mWorkingExpression->setRawContent(mWorkingString); + + if (mWorkingExpression->getExpressions().size() == 2) + { + mWorkingType = Type::LEAF; + mLineState = LineState::NONE; + } + else + { + mLineState = LineState::AWAITING_LBRACE; + mOpenBraceCount = 0; + } + } + + mWorkingString.clear(); + mWorkingSymbols.clear(); +} + +void LatexMathExpression::setContent(std::vector& symbols) +{ + mSymbols = symbols; +} + +const std::vector& LatexMathExpression::getSymbols() const +{ + return mSymbols; +} + +const LatexMathExpression::Type LatexMathExpression::getType() const +{ + return mType; +} + +const std::vector& LatexMathExpression::getExpressions() const +{ + return mExpressions; +} \ No newline at end of file diff --git a/src/publishing/latex/LatexMathExpression.h b/src/publishing/latex/LatexMathExpression.h index bd01dca..9ed0bca 100644 --- a/src/publishing/latex/LatexMathExpression.h +++ b/src/publishing/latex/LatexMathExpression.h @@ -6,36 +6,81 @@ #include #include +class LatexMathExpression; +using LatexMathExpressionPtr = std::unique_ptr; + class LatexMathExpression { public: - LatexMathExpression(const std::string& expression = {}) - : mRawExpression(expression) + enum class Type { + LINEAR, + LEAF, + FRACTION, + SUBSCRIPT, + SUPERSCRIPT + }; - } - - void parse() + enum class LineState { - for (auto c : mRawExpression) - { + NONE, + IN_TAG_NAME, + AWAITING_LBRACE, + IN_TAG_BODY + }; - } - } + LatexMathExpression(const std::string& expression); - const std::vector& getSymbols() const - { - return mSymbols; - } + LatexMathExpression(Type type); + + ~LatexMathExpression(); + + const std::vector& getSymbols() const; + + const std::vector& getExpressions() const; + + const Type getType() const; + + void setContent(std::vector& symbols); + + void setRawContent(const std::string& content); + + void onTagBodyChar(char c); + + void onFreeChar(char c); + + void onSpace(); + + void onLBrace(); + + void onRBrace(); + + void onSuperscript(); + + void onSubscript(); + + void onLiteral(); + + void onTagBody(); + + void onFinishedTagName(); + + void onCloseExpression(); + + void parse(); private: + std::size_t mOpenBraceCount{ 0 }; - unsigned mOpenTagCount{ 0 }; + LineState mLineState{ LineState::NONE }; + std::string mWorkingString; + std::string mRawBody; - std::string mRawExpression; - std::unique_ptr mSuperScriptExpr; - std::unique_ptr mSubScriptExpr; - std::unique_ptr mEnclosedExpr; + Type mWorkingType{ Type::LEAF }; + LatexMathExpression* mWorkingExpression{ nullptr }; + Type mType{ Type::LEAF }; + std::vector mWorkingSymbols; std::vector mSymbols; + std::vector mExpressions; }; \ No newline at end of file diff --git a/src/publishing/latex/LatexSymbols.cpp b/src/publishing/latex/LatexSymbols.cpp index c12712f..fb7e8d1 100644 --- a/src/publishing/latex/LatexSymbols.cpp +++ b/src/publishing/latex/LatexSymbols.cpp @@ -10,6 +10,11 @@ std::unordered_map LatexSymbolLookup::mSymbols = { {"psi", L"\u03C8"} }; + +std::unordered_map LatexSymbolLookup::mTags = { + {"frac", LatexMathSymbol::Tag::FRAC}, +}; + std::optional LatexSymbolLookup::getSymbolUtf8(const std::string& tag) { if (auto entry = getSymbolUtf16(tag); entry) @@ -32,4 +37,16 @@ std::optional LatexSymbolLookup::getSymbolUtf16(const std::string& { return std::nullopt; } +} + +std::optional LatexSymbolLookup::getKnownTag(const std::string& tag) +{ + if (auto iter = mTags.find(tag); iter != mTags.end()) + { + return iter->second; + } + else + { + return std::nullopt; + } } \ No newline at end of file diff --git a/src/publishing/latex/LatexSymbols.h b/src/publishing/latex/LatexSymbols.h index e91b79e..a080dc3 100644 --- a/src/publishing/latex/LatexSymbols.h +++ b/src/publishing/latex/LatexSymbols.h @@ -15,8 +15,24 @@ struct LatexMathSymbol ENCLOSING, ENCLOSING_TAG }; + + enum class Tag + { + NONE, + FRAC + }; + + LatexMathSymbol(Type type, const std::string& name, const std::string& unicode) + : mType(type), + mName(name), + mUnicode(unicode) + { + + } + + Type mType{ Type::RAW }; std::string mName; - std::wstring mUnicode; + std::string mUnicode; }; class LatexSymbolLookup @@ -26,6 +42,21 @@ public: static std::optional getSymbolUtf16(const std::string& tag); + static std::optional getKnownTag(const std::string& tag); + + static std::size_t getNumTagBodies(LatexMathSymbol::Tag tag) + { + switch (tag) + { + case LatexMathSymbol::Tag::FRAC: + return 2; + default: + return 1; + } + } + private: static std::unordered_map mSymbols; + + static std::unordered_map mTags; }; \ No newline at end of file diff --git a/src/publishing/plotting/EquationNode.cpp b/src/publishing/plotting/EquationNode.cpp index 7060bbe..1f226c6 100644 --- a/src/publishing/plotting/EquationNode.cpp +++ b/src/publishing/plotting/EquationNode.cpp @@ -1,7 +1,83 @@ #include "EquationNode.h" +#include "LatexMathExpression.h" +#include "TextNode.h" + +#include "FileLogger.h" + EquationNode::EquationNode(const Transform& t) : AbstractVisualNode(t) { +} + +EquationNode::~EquationNode() +{ + +} + +void EquationNode::setContent(LatexMathExpression* content) +{ + mContent = content; + mContentDirty = true; +} + +void EquationNode::update(SceneInfo* sceneInfo) +{ + if (mContentDirty) + { + createOrUpdateGeometry(sceneInfo); + mContentDirty = false; + } +} + +void EquationNode::addExpression(const LatexMathExpression* expression) +{ + if (expression->getType() == LatexMathExpression::Type::LINEAR) + { + MLOG_INFO("Processing linear expr with : " << expression->getExpressions().size() << " children"); + for (const auto& expression : expression->getExpressions()) + { + addExpression(expression.get()); + } + } + else if(expression->getType() == LatexMathExpression::Type::LEAF) + { + std::string content = "_"; + for (const auto& symbol : expression->getSymbols()) + { + content += symbol.mUnicode; + } + MLOG_INFO("Processing leaf expr with content: " << content); + auto node = std::make_unique(content, Transform(mTextCursor)); + node->setWidth(100); + node->setFont(mDefaultFont); + addChild(node.get()); + mText.push_back(std::move(node)); + + mTextCursor.move(20.0, 0.0); + } + else if (expression->getType() == LatexMathExpression::Type::FRACTION) + { + MLOG_INFO("Processing frac expr"); + if (expression->getExpressions().size() == 2) + { + auto x_cache = mTextCursor.getX(); + mTextCursor.move(0.0, -10.0); + addExpression(expression->getExpressions()[0].get()); + + mTextCursor.move(mTextCursor.getX() - x_cache, 20.0); + addExpression(expression->getExpressions()[1].get()); + + mTextCursor.move(0.0, -10.0); + } + } +} + +void EquationNode::createOrUpdateGeometry(SceneInfo* sceneInfo) +{ + mChildren.clear(); + mText.clear(); + + addExpression(mContent); } \ No newline at end of file diff --git a/src/publishing/plotting/EquationNode.h b/src/publishing/plotting/EquationNode.h index 745526a..7b4a94f 100644 --- a/src/publishing/plotting/EquationNode.h +++ b/src/publishing/plotting/EquationNode.h @@ -1,9 +1,35 @@ #pragma once #include "AbstractVisualNode.h" +#include "FontItem.h" + +class TextNode; +class LatexMathExpression; class EquationNode : public AbstractVisualNode { public: EquationNode(const Transform& t = {}); + + virtual ~EquationNode(); + + void setContent(LatexMathExpression* content); + + void update(SceneInfo* sceneInfo) override; + +protected: + void createOrUpdateGeometry(SceneInfo* sceneInfo); + + void addExpression(const LatexMathExpression* expression); + + + std::vector > mText; + + FontItem mDefaultFont{ "Cambria Math", 14 }; + //FontItem mDefaultFont{ "Latin Modern Math", 14 }; + + Point mTextCursor; + + LatexMathExpression* mContent{ nullptr }; + bool mContentDirty{ true }; }; \ No newline at end of file diff --git a/src/rendering/fonts/BasicFontEngine.h b/src/rendering/fonts/BasicFontEngine.h index 4ae84e6..1064131 100644 --- a/src/rendering/fonts/BasicFontEngine.h +++ b/src/rendering/fonts/BasicFontEngine.h @@ -12,4 +12,9 @@ class BasicFontEngine : public IFontEngine virtual void loadFontFace(const std::filesystem::path&, float){}; virtual std::unique_ptr loadGlyph(unsigned){return nullptr;}; + + double getHorizontalAdvance(const FontItem& fontItem, const std::string& content) override + { + return 0.0; + } }; diff --git a/src/rendering/fonts/FontsManager.cpp b/src/rendering/fonts/FontsManager.cpp index 3adbcf7..5a0a354 100644 --- a/src/rendering/fonts/FontsManager.cpp +++ b/src/rendering/fonts/FontsManager.cpp @@ -1,20 +1,29 @@ #include "FontsManager.h" -#ifdef HAS_FREETYPE -#include "FreeTypeFontEngine.h" +#ifdef _WIN32 + #include "DirectWriteFontEngine.h" + #include "DirectWriteHelpers.h" #else -#include "BasicFontEngine.h" + #ifdef HAS_FREETYPE + #include "FreeTypeFontEngine.h" + #else + #include "BasicFontEngine.h" + #endif #endif #include "FontGlyph.h" FontsManager::FontsManager() { -#ifdef HAS_FREETYPE - mFontEngine = std::make_unique(); - mUsesGlyphs = true; +#ifdef _WIN32 + mFontEngine = std::make_unique(); #else - mFontEngine = std::make_unique(); + #ifdef HAS_FREETYPE + mFontEngine = std::make_unique(); + mUsesGlyphs = true; + #else + mFontEngine = std::make_unique(); + #endif #endif mFontEngine->initialize(); } diff --git a/src/rendering/fonts/IFontEngine.h b/src/rendering/fonts/IFontEngine.h index 644a92f..7bed912 100644 --- a/src/rendering/fonts/IFontEngine.h +++ b/src/rendering/fonts/IFontEngine.h @@ -1,5 +1,7 @@ #pragma once +#include "FontItem.h" + #include #include @@ -16,4 +18,6 @@ public: virtual void loadFontFace(const std::filesystem::path& fontFile, float penSize = 16) = 0; virtual std::unique_ptr loadGlyph(uint32_t charCode) = 0; + + virtual double getHorizontalAdvance(const FontItem& fontItem, const std::string& content) = 0; }; diff --git a/src/rendering/fonts/directx/DirectWriteFontEngine.cpp b/src/rendering/fonts/directx/DirectWriteFontEngine.cpp index 10e05bb..658cf39 100644 --- a/src/rendering/fonts/directx/DirectWriteFontEngine.cpp +++ b/src/rendering/fonts/directx/DirectWriteFontEngine.cpp @@ -4,8 +4,10 @@ #include "UnicodeUtils.h" #include "DirectWriteHelpers.h" +#include "FileLogger.h" #include +#include void DirectWriteFontEngine::initialize() { @@ -35,6 +37,83 @@ std::unique_ptr DirectWriteFontEngine::loadGlyph(uint32_t charCode) return nullptr; } +IDWriteFontFamily* DirectWriteFontEngine::getSystemFontFamily(const std::string& fontName) +{ + if (auto iter = mSystemFontFamilies.find(fontName); iter != mSystemFontFamilies.end()) + { + return iter->second.Get(); + } + else + { + loadSystemFontFamily(fontName); + if (auto iter = mSystemFontFamilies.find(fontName); iter != mSystemFontFamilies.end()) + { + return iter->second.Get(); + } + else + { + return nullptr; + } + } +} + +bool DirectWriteFontEngine::loadSystemFontFamily(const std::string& fontName) +{ + Microsoft::WRL::ComPtr system_collection; + if (SUCCEEDED(mDWriteFactory->GetSystemFontCollection(&system_collection, FALSE))) + { + UINT32 index{ 0 }; + BOOL exists{ false }; + system_collection->FindFamilyName(UnicodeUtils::utf8ToUtf16WString(fontName).c_str(), &index, &exists); + if (exists) + { + Microsoft::WRL::ComPtr font_family; + if (SUCCEEDED(system_collection->GetFontFamily(index, &font_family))) + { + mSystemFontFamilies[fontName] = font_family; + return true; + } + } + } + return false; +}; + +IDWriteFontFace* DirectWriteFontEngine::getDefaultFontFace(const std::string& fontName) +{ + if (auto iter = mDefaultFontFaces.find(fontName); iter != mDefaultFontFaces.end()) + { + return iter->second.Get(); + } + else + { + loadDefaultFontFace(fontName); + if (auto iter = mDefaultFontFaces.find(fontName); iter != mDefaultFontFaces.end()) + { + return iter->second.Get(); + } + else + { + return nullptr; + } + } +} + +bool DirectWriteFontEngine::loadDefaultFontFace(const std::string& fontName) +{ + if (auto family = getSystemFontFamily(fontName); family) + { + Microsoft::WRL::ComPtr font; + if (SUCCEEDED(family->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, &font))) + { + Microsoft::WRL::ComPtr font_face; + font->CreateFontFace(&font_face); + mDefaultFontFaces[fontName] = font_face; + return true; + } + }; + return false; +} + std::unique_ptr DirectWriteFontEngine::getGlyphRunOutlines(const std::vector& charCodes) { initializeOffScreenRenderer(); @@ -52,3 +131,42 @@ std::unique_ptr DirectWriteFontEngine::getGlyphRunOutlines(con return mOffScreenRenderer->getGlypyRunOutline(); } +double DirectWriteFontEngine::getHorizontalAdvance(const FontItem& fontItem, const std::string& content) +{ + auto font_face = getDefaultFontFace(fontItem.getFaceName()); + if (!font_face) + { + return 0; + } + + DWRITE_FONT_METRICS font_metrics; + font_face->GetMetrics(&font_metrics); + + const auto design_per_em = font_metrics.designUnitsPerEm; + MLOG_INFO("Design units: " << design_per_em); + + const auto utf32_content = UnicodeUtils::utf8ToUtf32(content); + + std::vector glyph_indices(utf32_content.size()); + if (SUCCEEDED(font_face->GetGlyphIndices(utf32_content.data(), static_cast(utf32_content.size()), &glyph_indices[0]))) + { + std::vector metrics(utf32_content.size()); + double total_advance{ 0 }; + if (SUCCEEDED(font_face->GetDesignGlyphMetrics(glyph_indices.data(), static_cast(glyph_indices.size()), &metrics[0], false))) + { + for (std::size_t idx = 0; idx < utf32_content.size(); idx++) + { + const auto design_width = metrics[idx].advanceWidth; + // https://learn.microsoft.com/en-us/windows/win32/gdi/device-vs--design-units + double device_width = (design_width / static_cast(design_per_em)) * (fontItem.getSize() / 72.0) * 96.0 * 0.75; + + MLOG_INFO("Got advance width: " << device_width); + //total_advance += std::floor(device_width); + total_advance += device_width; + } + return std::round(total_advance); + } + } + return 0; +} + diff --git a/src/rendering/fonts/directx/DirectWriteFontEngine.h b/src/rendering/fonts/directx/DirectWriteFontEngine.h index 6f01315..fff863d 100644 --- a/src/rendering/fonts/directx/DirectWriteFontEngine.h +++ b/src/rendering/fonts/directx/DirectWriteFontEngine.h @@ -2,10 +2,12 @@ #include "IFontEngine.h" #include "FontGlyph.h" +#include "FontItem.h" #include #include +#include class GlyphOutlineGeometrySink; class OffScreenTextRenderer; @@ -13,6 +15,8 @@ class OffScreenTextRenderer; struct IDWriteFactory; struct IDWriteTextFormat; struct IDWriteTextLayout; +struct IDWriteFontFace; +struct IDWriteFontFamily; class DirectWriteFontEngine : public IFontEngine { @@ -27,10 +31,24 @@ public: std::unique_ptr getGlyphRunOutlines(const std::vector& charCodes); + IDWriteFontFamily* getSystemFontFamily(const std::string& fontName); + + bool loadSystemFontFamily(const std::string& fontName); + + IDWriteFontFace* getDefaultFontFace(const std::string& fontName); + + bool loadDefaultFontFace(const std::string& fontName); + + double getHorizontalAdvance(const FontItem& fontItem, const std::string& content) override; + private: void initializeOffScreenRenderer(); bool mIsValid{ false }; + + std::unordered_map > mDefaultFontFaces; + std::unordered_map > mSystemFontFamilies; + Microsoft::WRL::ComPtr mDWriteFactory; Microsoft::WRL::ComPtr mTextFormat; Microsoft::WRL::ComPtr mTextLayout; diff --git a/src/rendering/visual_elements/nodes/TextNode.cpp b/src/rendering/visual_elements/nodes/TextNode.cpp index a123c00..3deba63 100644 --- a/src/rendering/visual_elements/nodes/TextNode.cpp +++ b/src/rendering/visual_elements/nodes/TextNode.cpp @@ -9,6 +9,8 @@ #include "SceneInfo.h" #include "SceneText.h" +#include "SceneModel.h" +#include "Rectangle.h" #include "Color.h" @@ -18,6 +20,8 @@ TextNode::TextNode(const std::string& content, const Transform& transform) : MaterialNode(transform) { mTextData.mContent= content; + setFillColor(Color(0, 0, 0)); + setHasStrokeColor(false); } TextNode::~TextNode() @@ -92,15 +96,39 @@ SceneItem* TextNode::getSceneItem(std::size_t idx) const { return mTextItem.get(); } - else + else if (idx == 1) { - return 0; + if (mNodeBounds) + { + return mNodeBounds.get(); + } + else + { + return mTextBounds.get(); + } } + else if (idx == 2) + { + if (mTextBounds) + { + return mTextBounds.get(); + } + } + return nullptr; } std::size_t TextNode::getNumSceneItems() const { - return 1; + auto count = 1; + if (mNodeBounds) + { + count++; + } + if (mTextBounds) + { + count++; + } + return count; } void TextNode::updateLines(FontsManager* fontsManager) @@ -145,12 +173,58 @@ void TextNode::updateLines(FontsManager* fontsManager) } } +void TextNode::setRenderNodeBounds(bool render) +{ + mRenderNodeBounds = render; +} + +void TextNode::setRenderTextBounds(bool render) +{ + mRenderTextBounds = render; +} + void TextNode::update(SceneInfo* sceneInfo) { if (!mTextItem) { mTextItem = std::make_unique(); mTextItem->setName(mName + "_SceneText"); + + mContentWidth = sceneInfo->mFontsManager->getFontEngine()->getHorizontalAdvance(mTextData.mFont, mTextData.mContent); + mContentHeight = mTextData.mFont.getSize(); + + if (mWidth == 1.0 && mHeight == 1.0) + { + mTextItem->setTextWidth(mContentWidth); + mTextItem->setTextHeight(mContentHeight); + } + else + { + mTextItem->setTextWidth(mWidth); + mTextItem->setTextHeight(mHeight); + } + + if (mRenderNodeBounds) + { + auto rect = std::make_unique(Point(0.0, 0.0), mWidth, mHeight); + mNodeBounds = std::make_unique(); + mNodeBounds->updateGeometry(std::move(rect)); + + BasicMaterial bounds_material; + bounds_material.setStrokeColor(Color(255, 0, 0)); + mNodeBounds->updateSolidMaterial(bounds_material); + } + + if (mRenderTextBounds) + { + auto rect = std::make_unique(Point(0.0, 0.0), mContentWidth, mContentHeight); + mTextBounds = std::make_unique(); + mTextBounds->updateGeometry(std::move(rect)); + + BasicMaterial bounds_material; + bounds_material.setStrokeColor(Color(0, 255, 0)); + mTextBounds->updateSolidMaterial(bounds_material); + } } if (mTransformIsDirty || mContentIsDirty) @@ -167,9 +241,16 @@ void TextNode::update(SceneInfo* sceneInfo) if (mTransformIsDirty) { - //mTextItem->updateTransform({mLocation}); - mTextItem->setTextWidth(mWidth); - mTextItem->setTextHeight(mHeight); + if (mWidth == 1.0 && mHeight == 1.0) + { + mTextItem->setTextWidth(mContentWidth); + mTextItem->setTextHeight(mContentHeight); + } + else + { + mTextItem->setTextWidth(mWidth); + mTextItem->setTextHeight(mHeight); + } mTransformIsDirty = false; } diff --git a/src/rendering/visual_elements/nodes/TextNode.h b/src/rendering/visual_elements/nodes/TextNode.h index 1c82adb..c0ffeb3 100644 --- a/src/rendering/visual_elements/nodes/TextNode.h +++ b/src/rendering/visual_elements/nodes/TextNode.h @@ -11,6 +11,7 @@ #include class FontsManager; +class SceneModel; class TextNode : public MaterialNode { @@ -34,9 +35,11 @@ public: void setHeight(double height); void setContent(const std::string& content); - void setFont(const FontItem& font); + void setRenderNodeBounds(bool render); + void setRenderTextBounds(bool render); + void update(SceneInfo* sceneInfo) override; private: @@ -49,7 +52,15 @@ private: double mWidth{1}; double mHeight{1}; + double mContentWidth{ 1 }; + double mContentHeight{ 1 }; + std::unique_ptr mTextItem; + + bool mRenderNodeBounds{ false }; + bool mRenderTextBounds{ false }; + std::unique_ptr mNodeBounds; + std::unique_ptr mTextBounds; }; using TextNodetr = std::unique_ptr; diff --git a/src/rendering/visual_elements/svg/SvgPainter.cpp b/src/rendering/visual_elements/svg/SvgPainter.cpp index f7597e8..b7ce451 100644 --- a/src/rendering/visual_elements/svg/SvgPainter.cpp +++ b/src/rendering/visual_elements/svg/SvgPainter.cpp @@ -235,11 +235,9 @@ void SvgPainter::paintText(SvgDocument* document, SceneText* text) const { auto svg_text = std::make_unique(); svg_text->setContent(text->getTextData().mContent); - auto loc = text->getTransform().getLocation(); - - loc.move(text->getTextWidth() / 2.0, text->getTextHeight()/2.0); - svg_text->setLocation(loc); - + + Point centre(text->getTextWidth() / 2.0, text->getTextHeight() / 2.0); + svg_text->setLocation(centre); svg_text->setFontFamily(text->getTextData().mFont.getFaceName()); svg_text->setFill(text->getSolidMaterial().getFillColor()); @@ -248,9 +246,14 @@ void SvgPainter::paintText(SvgDocument* document, SceneText* text) const { svg_text->setFillOpacity(opacity); } - svg_text->setFontSize(text->getTextData().mFont.getSize()); + const auto transform = text->getTransform(); + if (!transform.isDefaultTransform()) + { + svg_text->addAttribute(toTransform(transform)); + } + document->getRoot()->addChild(std::move(svg_text)); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 213374d..c8b9aa5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(test_utils) +add_subdirectory(fonts) add_subdirectory(geometry) add_subdirectory(graphics) add_subdirectory(publishing) @@ -13,7 +14,6 @@ set(TEST_MODULES compression core database - fonts image ipc network diff --git a/test/fonts/CMakeLists.txt b/test/fonts/CMakeLists.txt index 5267014..0188b06 100644 --- a/test/fonts/CMakeLists.txt +++ b/test/fonts/CMakeLists.txt @@ -4,23 +4,24 @@ if(UNIX) find_package(Freetype QUIET) if(Freetype_FOUND) set(PLATFORM_UNIT_TEST_FILES - fonts/TestFreeTypeFontEngine.cpp + TestFreeTypeFontEngine.cpp ) endif() else() set(PLATFORM_UNIT_TEST_FILES - fonts/TestDirectWriteFontEngine.cpp + TestDirectWriteFontEngine.cpp ) endif() + +set(MODULE_NAME fonts) +list(APPEND UNIT_TEST_FILES + TestFontReader.cpp + ${PLATFORM_UNIT_TEST_FILES} +) -set(FONTS_UNIT_TEST_FILES - fonts/TestFontReader.cpp - ${PLATFORM_UNIT_TEST_FILES} - PARENT_SCOPE - ) - -set(FONTS_UNIT_TEST_DEPENDENCIES - fonts - PARENT_SCOPE - ) \ No newline at end of file +set(UNIT_TEST_TARGET_NAME ${MODULE_NAME}_unit_tests) + +add_executable(${UNIT_TEST_TARGET_NAME} ${CMAKE_SOURCE_DIR}/test/test_runner.cpp ${UNIT_TEST_FILES}) +target_link_libraries(${UNIT_TEST_TARGET_NAME} PUBLIC test_utils fonts) +set_property(TARGET ${UNIT_TEST_TARGET_NAME} PROPERTY FOLDER test/${MODULE_NAME}) \ No newline at end of file diff --git a/test/fonts/TestDirectWriteFontEngine.cpp b/test/fonts/TestDirectWriteFontEngine.cpp index 73a7436..8a83ac2 100644 --- a/test/fonts/TestDirectWriteFontEngine.cpp +++ b/test/fonts/TestDirectWriteFontEngine.cpp @@ -9,6 +9,7 @@ #include "SvgWriter.h" #include "SvgShapeElements.h" #include "File.h" +#include "FontItem.h" TEST_CASE(TestDirectWriteFontEngine, "fonts") { @@ -33,4 +34,17 @@ TEST_CASE(TestDirectWriteFontEngine, "fonts") File file(TestUtils::getTestOutputDir(__FILE__) / "out.svg"); file.writeText(doc_string); +} + +TEST_CASE(TestDirectWriteFontEngine_GlyphMetrics, "fonts") +{ + DirectWriteFontEngine font_engine; + font_engine.initialize(); + + FontItem font("Verdana", 16); + + const auto advance = font_engine.getHorizontalAdvance(font, "abc"); + MLOG_INFO("Advance is: " << advance); + + REQUIRE(advance != 0.0); } \ No newline at end of file diff --git a/test/graphics/CMakeLists.txt b/test/graphics/CMakeLists.txt index 378c581..a779e8c 100644 --- a/test/graphics/CMakeLists.txt +++ b/test/graphics/CMakeLists.txt @@ -23,6 +23,7 @@ endif() set(UNIT_TEST_FILES TestRasterizer.cpp + TestTextRendering.cpp ${PLATFORM_UNIT_TEST_FILES} ) diff --git a/test/graphics/TestTextRendering.cpp b/test/graphics/TestTextRendering.cpp new file mode 100644 index 0000000..dc33780 --- /dev/null +++ b/test/graphics/TestTextRendering.cpp @@ -0,0 +1,22 @@ +#include "TestFramework.h" +#include "TestUtils.h" +#include "TestRenderUtils.h" + +#include "TextNode.h" + +TEST_CASE(TestTextRendeing_WithBoundingBoxes, "graphics") +{ + TestRenderer renderer(800, 800); + + auto loc = Point(10, 10); + + TextNode text_node("abcdefgh", Transform(loc)); + text_node.setRenderNodeBounds(true); + text_node.setRenderTextBounds(true); + + auto scene = renderer.getScene(); + scene->addNode(&text_node); + + renderer.writeSvg(TestUtils::getTestOutputDir(__FILE__) / "text_with_bounding_box.svg"); + renderer.write(TestUtils::getTestOutputDir(__FILE__) / "text_with_bounding_box.png"); +}; \ No newline at end of file diff --git a/test/publishing/TestLatexConverter.cpp b/test/publishing/TestLatexConverter.cpp index 46ffe53..37b288c 100644 --- a/test/publishing/TestLatexConverter.cpp +++ b/test/publishing/TestLatexConverter.cpp @@ -4,13 +4,16 @@ #include "TestRenderUtils.h" #include "StringUtils.h" -#include "FontItem.h" -#include "TextNode.h" -#include "LatexSymbols.h" +#include "EquationNode.h" +#include "LatexMathExpression.h" + TEST_CASE(TestLatexConverter, "publishing") { - FontItem font("Cambria Math", 14); + auto expression = std::make_unique("\\psi = \\frac{\\alpha + \\beta}{c}"); + + auto equation_node = std::make_unique(Point(10, 10)); + equation_node->setContent(expression.get()); auto psi = LatexSymbolLookup::getSymbolUtf8("psi"); auto alpha = LatexSymbolLookup::getSymbolUtf8("alpha"); @@ -20,10 +23,7 @@ TEST_CASE(TestLatexConverter, "publishing") TestRenderer renderer(800, 800); - auto text = std::make_unique(content, Point(10, 10)); - text->setFont(font); - - renderer.getScene()->addNode(text.get()); - + renderer.getScene()->addNode(equation_node.get()); + renderer.writeSvg(TestUtils::getTestOutputDir(__FILE__) / "out.svg"); renderer.write(TestUtils::getTestOutputDir(__FILE__) / "out.png"); }; \ No newline at end of file diff --git a/test/test_utils/TestCaseRunner.cpp b/test/test_utils/TestCaseRunner.cpp index 0f6b7b9..8d5ffc2 100644 --- a/test/test_utils/TestCaseRunner.cpp +++ b/test/test_utils/TestCaseRunner.cpp @@ -68,12 +68,23 @@ bool TestCaseRunner::run(const std::vector& args) sLastTestFailed = false; std::cout << "TestFramework: Running Test - " << test_case->getName() << std::endl; - try{ + try + { test_case->run(); } + catch (const std::runtime_error& re) + { + std::cout << "Failed with runtime error: " << re.what() << std::endl; + mFailingTests.push_back(test_case->getName()); + } + catch (const std::exception& e) + { + std::cout << "Failed with exception: " << e.what() << std::endl; + mFailingTests.push_back(test_case->getName()); + } catch(...) { - std::cout << "Failed with exception" << std::endl; + std::cout << "Failed with unknown exception" << std::endl; mFailingTests.push_back(test_case->getName()); }; diff --git a/test/test_utils/TestRenderUtils.h b/test/test_utils/TestRenderUtils.h index d998600..9b27bb4 100644 --- a/test/test_utils/TestRenderUtils.h +++ b/test/test_utils/TestRenderUtils.h @@ -12,6 +12,8 @@ #include "Image.h" #include "PngWriter.h" +#include "FontsManager.h" + #include "File.h" class TestRenderer @@ -19,10 +21,14 @@ class TestRenderer public: TestRenderer(unsigned width = 1000, unsigned height = 1000) { + mFontsManager = std::make_unique(); + mSurface = std::make_unique(); mSurface->setSize(width, height); mDrawingContext = std::make_unique(mSurface.get()); + + getScene()->setFontsManager(mFontsManager.get()); } Scene* getScene() const @@ -60,4 +66,6 @@ public: private: std::unique_ptr mSurface; std::unique_ptr mDrawingContext; + + std::unique_ptr mFontsManager; }; \ No newline at end of file