#include "pch.h" #include "PathView.h" #if __has_include("PathView.g.cpp") #include "PathView.g.cpp" #endif #include #include "JSValueXaml.h" #include "Utils.h" using namespace winrt; using namespace Microsoft::Graphics::Canvas; using namespace Microsoft::ReactNative; namespace winrt::RNSVG::implementation { void PathView::UpdateProperties(IJSValueReader const &reader, bool forceUpdate, bool invalidate) { const JSValueObject &propertyMap{JSValue::ReadObjectFrom(reader)}; for (auto const &pair : propertyMap) { auto const &propertyName{pair.first}; auto const &propertyValue{pair.second}; if (propertyName == "d") { m_commands.clear(); m_segmentData.clear(); if (propertyValue.IsNull()) { m_d.clear(); } else { m_d = propertyValue.AsString(); ParsePath(); } } } __super::UpdateProperties(reader, forceUpdate, invalidate); } void PathView::CreateGeometry(UI::Xaml::CanvasControl const &canvas) { auto const &resourceCreator{canvas.try_as()}; Svg::CanvasSvgDocument doc{resourceCreator}; auto const &path{doc.CreatePathAttribute(m_segmentData, m_commands)}; Geometry(path.CreatePathGeometry(FillRule())); } void PathView::ParsePath() { char prev_cmd = ' '; size_t i{0}; auto length{m_d.length()}; while (i < length) { SkipSpaces(i); if (i > length) { break; } bool has_prev_cmd{prev_cmd != ' '}; char first_char = m_d.at(i); if (!has_prev_cmd && first_char != 'M' && first_char != 'm') { throw hresult_invalid_argument(L"First segment must be a MoveTo."); } bool is_implicit_move_to{false}; char cmd = ' '; if (IsCommand(first_char)) { cmd = first_char; m_commands.push_back(m_cmds[cmd]); ++i; } else if (has_prev_cmd && IsNumberStart(first_char)) { if (prev_cmd == 'Z' || prev_cmd == 'z') { throw hresult_invalid_argument(L"ClosePath cannot be followed by a number."); } if (prev_cmd == 'M' || prev_cmd == 'm') { // If a MoveTo is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit LineTo commands. is_implicit_move_to = true; if (IsUpper(prev_cmd)) { cmd = 'L'; m_commands.push_back(m_cmds[cmd]); } else { cmd = 'l'; m_commands.push_back(m_cmds[cmd]); } } else { cmd = prev_cmd; m_commands.push_back(m_cmds[cmd]); } } else { throw hresult_invalid_argument(L"Unexpected character: " + first_char); } bool absolute{IsUpper(cmd)}; switch (cmd) { case 'm': case 'M': case 'l': case 'L': case 't': case 'T': m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); break; case 'h': case 'H': case 'v': case 'V': m_segmentData.push_back(ParseListNumber(i)); break; case 's': case 'S': case 'q': case 'Q': m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); break; case 'c': case 'C': m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); break; case 'a': case 'A': m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseFlag(i)); m_segmentData.push_back(ParseFlag(i)); m_segmentData.push_back(ParseListNumber(i)); m_segmentData.push_back(ParseListNumber(i)); break; case 'z': case 'Z': break; default: throw hresult_invalid_argument(L"Unexpected command."); } if (is_implicit_move_to) { if (absolute) { prev_cmd = 'M'; } else { prev_cmd = 'm'; } } else { prev_cmd = cmd; } } } void PathView::SkipSpaces(size_t &index) { while (index < m_d.length() && IsSpace(m_d.at(index))) { ++index; } } void PathView::SkipDigits(size_t& index) { while (index < m_d.length() && IsDigit(m_d.at(index))) { ++index; } } void PathView::SkipListSeparator(size_t& index) { if (index < m_d.length() && m_d.at(index) == ',') { ++index; } } bool PathView::IsCommand(char const &cmd) { return m_cmds.find(cmd) != m_cmds.end(); } bool PathView::IsNumberStart(char const& c) { return IsDigit(c) || c == '.' || c == '-' || c == '+'; } bool PathView::IsDigit(char const &c) { return std::isdigit(static_cast(c)); } bool PathView::IsUpper(char const &c) { return std::isupper(static_cast(c)); } bool PathView::IsSpace(char const& c) { return std::isspace(static_cast(c)); } float PathView::ParseListNumber(size_t &index) { if (index == m_d.length()) { throw hresult_invalid_argument(L"Unexpected end."); } float result{ParseNumber(index)}; SkipSpaces(index); SkipListSeparator(index); return result; } float PathView::ParseNumber(size_t &index) { SkipSpaces(index); if (index == m_d.length()) { throw hresult_invalid_argument(L"Unexpected end."); } size_t start = index; char c = m_d.at(start); // Consume sign. if (c == '-' || c == '+') { ++index; c = m_d.at(index); } // Consume integer. if (IsDigit(c)) { SkipDigits(index); if (index < m_d.length()) { c = m_d.at(index); } } else if (c != '.') { throw hresult_invalid_argument(L"Invalid number formating character."); } // Consume fraction. if (c == '.') { ++index; SkipDigits(index); if (index < m_d.length()) { c = m_d.at(index); } } if ((c == 'e' || c == 'E') && ((index + 1) < m_d.length())) { char c2 = m_d.at(index + 1); // Check for 'em'/'ex' if (c2 != 'm' && c2 != 'x') { ++index; c = m_d.at(index); if (c == '+' || c == '-') { ++index; SkipDigits(index); } else if (IsDigit(c)) { SkipDigits(index); } else { throw hresult_invalid_argument(L"Invalid number formating character."); } } } auto num{m_d.substr(start, index)}; auto result{std::stof(num, nullptr)}; if (std::isinf(result) || std::isnan(result)) { throw hresult_invalid_argument(L"Invalid number."); } return result; } float PathView::ParseFlag(size_t& index) { SkipSpaces(index); char c = m_d.at(index); switch (c) { case '0': case '1': { ++index; if (index < m_d.length() && m_d.at(index) == ',') { ++index; } SkipSpaces(index); break; } default: throw hresult_invalid_argument(L"Unexpected flag."); } return static_cast(c - '0'); } } // namespace winrt::RNSVG::implementation