diff --git a/addons/ofxSvg/addon_config.mk b/addons/ofxSvg/addon_config.mk index bbd68296e55..1774fa56a15 100644 --- a/addons/ofxSvg/addon_config.mk +++ b/addons/ofxSvg/addon_config.mk @@ -16,8 +16,8 @@ meta: ADDON_NAME = ofxSvg - ADDON_DESCRIPTION = Addon for parsing svg files into ofPaths - ADDON_AUTHOR = Joshua Noble, maintained by OF Team + ADDON_DESCRIPTION = Addon for parsing, manipulating and saving svg files. + ADDON_AUTHOR = Nick Hardeman, original by Joshua Noble, maintained by OF Team ADDON_TAGS = "svg" ADDON_URL = http://github.com/openframeworks/openFrameworks @@ -59,30 +59,3 @@ common: # when parsing the file system looking for libraries exclude this for all or # a specific platform # ADDON_LIBS_EXCLUDE = - -osx: - ADDON_LIBS = libs/svgtiny/lib/macos/svgtiny.xcframework/macos-arm64_x86_64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/macos/libxml2.xcframework/macos-arm64_x86_64/libxml2.a - -ios: - ADDON_LIBS = libs/svgtiny/lib/ios/svgtiny.a - ADDON_LIBS += libs/libxml2/lib/ios/xml2.a - -linux64: - ADDON_LIBS = libs/svgtiny/lib/linux64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linux64/libxml2.a - -linuxarmv6l: - ADDON_LIBS = libs/svgtiny/lib/linuxarmv6l/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxarmv6l/libxml2.a - -linuxarmv7l: - ADDON_LIBS = libs/svgtiny/lib/linuxarmv7l/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxarmv7l/libxml2.a - -linuxaarch64: - ADDON_LIBS = libs/svgtiny/lib/linuxaarch64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxaarch64/libxml2.a - -msys2: - ADDON_PKG_CONFIG_LIBRARIES = libxml-2.0 diff --git a/addons/ofxSvg/src/ofxSvg.cpp b/addons/ofxSvg/src/ofxSvg.cpp old mode 100644 new mode 100755 index bbbf54024f1..4a85953595b --- a/addons/ofxSvg/src/ofxSvg.cpp +++ b/addons/ofxSvg/src/ofxSvg.cpp @@ -1,245 +1,2715 @@ -#include "ofConstants.h" #include "ofxSvg.h" -#include +#include "ofUtils.h" +#include +#include "ofGraphics.h" +#include "ofxSvgUtils.h" using std::string; +using std::vector; +using std::shared_ptr; -extern "C" { -#include "svgtiny.h" +ofPath ofxSvg::sDummyPath; + +struct Measurement { + double value; + std::string unit; +}; + +//-------------------------------------------------------------- +Measurement parseMeasurement(const std::string& input) { + + if( input.empty() ) { + Measurement result; + result.value = 0.0; + result.unit = ""; + return result; + } + + size_t i = 0; + + // Extract numeric part + while (i < input.size() && (std::isdigit(input[i]) || input[i] == '.' || input[i] == '-')) { + i++; + } + + Measurement result; + result.value = std::stod(input.substr(0, i)); // Convert number part to double + result.unit = input.substr(i); // Extract the unit part + + return result; +} + +// Function to deep copy a vector of shared_ptrs +std::vector> ofxSvg::deepCopyVector(const std::vector>& original) { + std::vector> copy; + copy.reserve(original.size()); // Reserve space for efficiency + + for (auto ptr : original) { + if (ptr) { + copy.push_back(ptr->clone()); + } + } + return copy; +} + +void ofxSvg::deepCopyFrom( const ofxSvg & mom ) { + + ofLogVerbose("ofxSvg::deepCopyFrom"); + + if( mom.mChildren.size() > 0 ) { + mChildren = deepCopyVector(mom.mChildren); + for( auto& kid : mChildren ) { + kid->setParent(*this); + } + } + if( mom.mDefElements.size() > 0 ) { + mDefElements = deepCopyVector(mom.mDefElements); + } + + mViewbox = mom.mViewbox; + mBounds = mom.mBounds; + + fontsDirectory = mom.fontsDirectory; + folderPath = mom.folderPath; + svgPath = mom.svgPath; + + mCurrentLayer = mom.mCurrentLayer; + mUnitStr = mom.mUnitStr; + +// if(mom.mCurrentSvgCss) { +// mCurrentSvgCss = std::make_shared(*mom.mCurrentSvgCss); +// } + + mSvgCss = mom.mSvgCss; + mCurrentCss = mom.mCurrentCss; + mDocumentCss = mom.mDocumentCss; + mFillColor = mom.mFillColor; + mStrokeColor = mom.mStrokeColor; + + mCssClassStack = mom.mCssClassStack; + _buildCurrentSvgCssFromStack(); + + mModelMatrix = mom.mModelMatrix; + mModelMatrixStack = mom.mModelMatrixStack; + + mCircleResolution = mom.mCircleResolution; + mCurveResolution = mom.mCurveResolution; + + mPaths = mom.mPaths; +} + +//-------------------------------------------------------------- +void ofxSvg::moveFrom( ofxSvg&& mom ) { + ofLogVerbose("ofxSvg::moveFrom"); + + mChildren = std::move(mom.mChildren); + + for( auto& kid : mChildren ) { + kid->setParent(*this); + } + + mDefElements = std::move(mom.mDefElements); + + mViewbox = mom.mViewbox; + mBounds = mom.mBounds; + + fontsDirectory = mom.fontsDirectory; + folderPath = mom.folderPath; + svgPath = mom.svgPath; + + mCurrentLayer = mom.mCurrentLayer; + mUnitStr = mom.mUnitStr; + +// mCurrentSvgCss = mom.mCurrentSvgCss; + + mSvgCss = std::move(mom.mSvgCss); + mCurrentCss = std::move(mom.mCurrentCss); + mDocumentCss = std::move(mom.mDocumentCss); + mFillColor = mom.mFillColor; + mStrokeColor = mom.mStrokeColor; + + mCssClassStack = std::move(mom.mCssClassStack); + _buildCurrentSvgCssFromStack(); + + mModelMatrix = mom.mModelMatrix; + mModelMatrixStack = mom.mModelMatrixStack; + + mCircleResolution = mom.mCircleResolution; + mCurveResolution = mom.mCurveResolution; + + mPaths = std::move(mom.mPaths); +} + + +// Copy constructor (deep copy) +//-------------------------------------------------------------- +ofxSvg::ofxSvg(const ofxSvg & mom) { + clear(); + ofLogVerbose("ofxSvg") << "ofxSvg(const ofxSvg & mom)"; + deepCopyFrom(mom); +} + +// Copy assignment operator (deep copy) +//-------------------------------------------------------------- +ofxSvg& ofxSvg::operator=(const ofxSvg& mom) { + if (this != &mom) { + ofLogVerbose("ofxSvg") << "ofxSvg::operator=(const ofxSvg& mom)"; + clear(); + deepCopyFrom(mom); + } + return *this; +} + +// Move constructor +//-------------------------------------------------------------- +ofxSvg::ofxSvg(ofxSvg && mom) { + ofLogVerbose("ofxSvg") << "ofxSvg(ofxSvg && mom)"; + clear(); + moveFrom(std::move(mom)); +} + +// Move assignment operator +ofxSvg& ofxSvg::operator=(ofxSvg&& mom) { + if (this != &mom) { + ofLogVerbose("ofxSvg") << "ofxSvg::operator=(ofxSvg&& mom)"; + clear(); + moveFrom(std::move(mom)); + } + return *this; } +//-------------------------------------------------------------- ofxSvg::ofxSvg(const of::filesystem::path & fileName) { load(fileName); } -float ofxSvg::getWidth() const { - return width; +//-------------------------------------------------------------- +bool ofxSvg::load( const of::filesystem::path& fileName ) { + ofFile mainXmlFile( fileName, ofFile::ReadOnly ); + ofBuffer tMainXmlBuffer( mainXmlFile ); + + svgPath = fileName; + folderPath = ofFilePath::getEnclosingDirectory( fileName, false ); + + ofXml xml; + if (!xml.load(tMainXmlBuffer)) { + ofLogWarning("ofxSvg") << " unable to load svg from " << fileName << " mainXmlFile: " << mainXmlFile.getAbsolutePath(); + return false; + } + + return loadFromString(tMainXmlBuffer.getText()); } -float ofxSvg::getHeight() const { - return height; +//-------------------------------------------------------------- +bool ofxSvg::loadFromString(const std::string& data, std::string url) { + clear(); + + ofXml xml; + xml.parse(data); + + if( xml ) { + ofXml svgNode = xml.getFirstChild(); + + validateXmlSvgRoot( svgNode ); + ofXml::Attribute viewBoxAttr = svgNode.getAttribute("viewBox"); + if(svgNode) { + + std::vector values = { + parseMeasurement(svgNode.getAttribute("x").getValue()), + parseMeasurement(svgNode.getAttribute("y").getValue()), + parseMeasurement(svgNode.getAttribute("width").getValue()), + parseMeasurement(svgNode.getAttribute("height").getValue()) + }; + + for( auto& tv : values ) { + if( !tv.unit.empty() ) { + mUnitStr = tv.unit; + } + } + if( mUnitStr.empty() ) { + mUnitStr = "px"; + } + + mBounds.x = values[0].value; + mBounds.y = values[1].value; + mBounds.width = values[2].value; + mBounds.height = values[3].value; + +// mBounds.x = ofToFloat( cleanString( svgNode.getAttribute("x").getValue(), "px") ); +// mBounds.y = ofToFloat( cleanString( svgNode.getAttribute("y").getValue(), "px" )); +// mBounds.width = ofToFloat( cleanString( svgNode.getAttribute("width").getValue(), "px" )); +// mBounds.height = ofToFloat( cleanString( svgNode.getAttribute("height").getValue(), "px" )); + mViewbox = mBounds; + } + + if( viewBoxAttr ) { + string tboxstr = viewBoxAttr.getValue(); + vector< string > tvals = ofSplitString( tboxstr, " " ); + if( tvals.size() == 4 ) { + mViewbox.x = ofToFloat(tvals[0] ); + mViewbox.y = ofToFloat( tvals[1] ); + mViewbox.width = ofToFloat( tvals[2] ); + mViewbox.height = ofToFloat( tvals[3] ); + } + } + + if(svgNode) { + ofLogVerbose("ofxSvg") << svgNode.findFirst("style").toString() << " bounds: " << mBounds; + } else { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : NO svgNode: "; + } + + + ofXml styleXmlNode = svgNode.findFirst("//style"); + if( styleXmlNode ) { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : STYLE NODE" << styleXmlNode.getAttribute("type").getValue() << " string: " << styleXmlNode.getValue(); + + mSvgCss.parse(styleXmlNode.getValue()); + + ofLogVerbose("ofxSvg") << "-----------------------------"; + ofLogVerbose() << mSvgCss.toString(); + ofLogVerbose("ofxSvg") << "-----------------------------"; + } else { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : NO STYLE NODE"; + } + + + // the defs are added in the _parseXmlNode function // + _parseXmlNode( svgNode, mChildren ); + // then set the parent to be the document +// for( auto& child : mChildren ) { +// child->setParent(*this); +// } + + ofLogVerbose("ofxSvg") << " number of defs elements: " << mDefElements.size(); + } + + return true; } -int ofxSvg::getNumPath() { - return paths.size(); +//-------------------------------------------------------------- +bool ofxSvg::reload() { + if( svgPath.empty() ) { + ofLogError("ofxSvg") << __FUNCTION__ << " : svg path is empty, please call load with file path before calling reload"; + return false; + } + return load( svgPath ); +} + +//-------------------------------------------------------------- +bool ofxSvg::save( of::filesystem::path apath ) { + // https://www.w3.org/TR/SVG2/struct.html#NewDocument + ofXml svgXml; + ofXml svgXmlNode = svgXml.appendChild("svg"); + if( auto vattr = svgXmlNode.appendAttribute("version")) { + vattr.set("1.1"); + } + if( auto vattr = svgXmlNode.appendAttribute("xmlns")) { + vattr.set("http://www.w3.org/2000/svg"); + } + if( auto vattr = svgXmlNode.appendAttribute("xmlns:xlink")) { + vattr.set("http://www.w3.org/1999/xlink"); + } + // + + if( auto vattr = svgXmlNode.appendAttribute("x")) { + vattr.set(ofToString(mBounds.x,2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("y")) { + vattr.set(ofToString(mBounds.y,2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("width")) { + vattr.set(ofToString(mBounds.getWidth(),2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("height")) { + vattr.set(ofToString(mBounds.getHeight(),2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("viewBox")) { + vattr.set(ofToString(mViewbox.x,0)+" "+ofToString(mViewbox.y,0)+" "+ofToString(mViewbox.getWidth(),0)+" "+ofToString(mViewbox.getHeight(),0)); + } + + ofXml cssNode = svgXmlNode.appendChild("style"); + +// if( mCurrentCss.properties.size() > 0 ) { +// mSvgCss.getAddClass(mCurrentCss); +// } + + // now we need to save out the children // + for( auto& kid : mChildren ) { + _toXml( svgXmlNode, kid ); + } + + if( mSvgCss.classes.size() > 0 && cssNode) { + if( auto vattr = cssNode.appendAttribute("type")) { + vattr.set("text/css"); + } + cssNode.set(mSvgCss.toString(false)); + } else { + svgXmlNode.removeChild(cssNode); + } + +// ofLogNotice("Parser::CSS") << mSvgCss.toString(); + + return svgXml.save(apath); } -ofPath & ofxSvg::getPathAt(int n) { - return paths[n]; + +//-------------------------------------------------------------- +void ofxSvg::clear() { + mChildren.clear(); + mDefElements.clear(); + mCurrentLayer = 0; +// mCurrentSvgCss.reset(); + mSvgCss.clear(); + mCPoints.clear(); + mCenterPoints.clear(); + + mCurrentCss.clear(); + mCssClassStack.clear(); + + mGroupStack.clear(); + mModelMatrix = glm::mat4(1.f); + mModelMatrixStack = std::stack(); +// loadIdentityMatrix(); + + mFillColor = ofColor(0); + mStrokeColor = ofColor(0); + + mPaths.clear(); } -void ofxSvg::load(const of::filesystem::path & fileName) { - of::filesystem::path file = ofToDataPath(fileName); - if (!of::filesystem::exists(file)) { - ofLogError("ofxSVG") << "load(): path does not exist: " << file ; - return; +//-------------------------------------------------------------- +const int ofxSvg::getTotalLayers(){ + return mCurrentLayer; +} + +//-------------------------------------------------------------- +void ofxSvg::recalculateLayers() { + mCurrentLayer = 0; + auto allKids = getAllElements(true); + for( auto& kid : allKids ) { + kid->layer = mCurrentLayer += 1.0; } +} - ofBuffer buffer = ofBufferFromFile(fileName); - loadFromString(buffer.getText(), file.string()); +// including these for legacy considerations // +//-------------------------------------------------------------- +int ofxSvg::getNumPath() { + if( mPaths.size() < 1 ) { + getPaths(); + } + return mPaths.size(); } -void ofxSvg::loadFromString(std::string stringdata, std::string urlstring) { +//-------------------------------------------------------------- +ofPath& ofxSvg::getPathAt(int n) { + if( mPaths.size() < 1 ) { + getPaths(); + } + if( n < 0 || n >= mPaths.size() ) { + ofLogWarning("ofxSvg") << "getPathAt: " << n << " out of bounds for number of paths: " << mPaths.size(); + return sDummyPath; + } + return mPaths[n]; +} - // goes some way to improving SVG compatibility - fixSvgString(stringdata); +//-------------------------------------------------------------- +const std::vector& ofxSvg::getPaths() const { +// auto spaths = const_cast(this)->getAllElementsForType(); +// std::size_t num = spaths.size(); + if( mPaths.size() < 1 ) { + // previous ofxSvg also included, circles, ellipses, rects, paths, etc. + auto spaths = const_cast(this)->getAllElementsWithPath(); + std::size_t num = spaths.size(); + mPaths.resize(num); + for( std::size_t i = 0; i < num; i++ ) { + mPaths[i] = spaths[i]->getPath(); + } + } + return mPaths; +} - const char * data = stringdata.c_str(); - int size = stringdata.size(); - const char * url = urlstring.c_str(); +//-------------------------------------------------------------- +void ofxSvg::setFontsDirectory( string aDir ) { + auto fontsDir = aDir; + if( fontsDir.size() > 1 && fontsDir.back() != '/' ) { + fontsDir += '/'; + } + fontsDirectory = fontsDir; + ofxSvgFontBook::setFontDirectory(fontsDir); +} - struct svgtiny_diagram * diagram = svgtiny_create(); - // Switch to "C" locale as svgtiny expect it to parse floating points (issue 6644) - std::locale prev_locale = std::locale::global(std::locale::classic()); - svgtiny_code code = svgtiny_parse(diagram, data, size, url, 0, 0); - // Restore locale - std::locale::global(prev_locale); +//-------------------------------------------------------------- +string ofxSvg::toString(int nlevel) { + string tstr = ""; + if( mChildren.size() ) { + for( std::size_t i = 0; i < mChildren.size(); i++ ) { + tstr += mChildren[i]->toString( nlevel ); + } + } + return tstr; +} - if (code != svgtiny_OK) { - std::string msg; - switch (code) { - case svgtiny_OUT_OF_MEMORY: - msg = "svgtiny_OUT_OF_MEMORY"; - break; +//-------------------------------------------------------------- +void ofxSvg::validateXmlSvgRoot( ofXml& aRootSvgNode ) { + // if there is no width and height set in the svg base node, svg tiny no likey // + if(aRootSvgNode) { + // check for x, y, width and height // + { + auto xattr = aRootSvgNode.getAttribute("x"); + if( !xattr ) { + auto nxattr = aRootSvgNode.appendAttribute("x"); + if(nxattr) nxattr.set("0px"); + } + } + { + auto yattr = aRootSvgNode.getAttribute("y"); + if( !yattr ) { + auto yattr = aRootSvgNode.appendAttribute("y"); + if( yattr ) yattr.set("0px"); + } + } + + ofXml::Attribute viewBoxAttr = aRootSvgNode.getAttribute("viewBox"); + + ofRectangle vrect; + if( viewBoxAttr ) { + string tboxstr = viewBoxAttr.getValue(); + vector< string > tvals = ofSplitString( tboxstr, " " ); + if( tvals.size() == 4 ) { + vrect.x = ofToFloat( cleanString( tvals[0], "px") ); + vrect.y = ofToFloat( cleanString( tvals[1], "px") ); + vrect.width = ofToFloat( cleanString( tvals[2], "px") ); + vrect.height = ofToFloat( cleanString( tvals[3], "px") ); + } + } + + auto wattr = aRootSvgNode.getAttribute("width"); + auto hattr = aRootSvgNode.getAttribute("height"); + + if( !wattr || !hattr ) { +// ofXml::Attribute viewBoxAttr = aRootSvgNode.getAttribute("viewBox"); + if( vrect.getWidth() > 0.0f && vrect.getHeight() > 0.0f ) { +// string tboxstr = viewBoxAttr.getValue(); +// vector< string > tvals = ofSplitString( tboxstr, " " ); +// if( tvals.size() >= 4 ) { + if( !wattr ) { + auto nwattr = aRootSvgNode.appendAttribute("width"); + if(nwattr) nwattr.set( ofToString(vrect.getWidth())+"px" ); + } + + if( !hattr ) { + auto nhattr = aRootSvgNode.appendAttribute("height"); + if(nhattr) nhattr.set( ofToString(vrect.getHeight())+"px" ); + } +// } + } + } + // from previous version of ofxSvg + // Affinity Designer does not set width/height as pixels but as a percentage + // and relies on the "viewBox" to convey the size of things. this applies the + // viewBox to the width and height. + if( vrect.getWidth() > 0.0f ) { + if(wattr) { + if( ofIsStringInString( wattr.getValue(), "%")) { + float wpct = ofToFloat( cleanString(wattr.getValue(), "%" )) / 100.0f; + wattr.set( ofToString(wpct * vrect.getWidth())+"px" ); + } + } + } + if( vrect.getHeight() > 0.0f ) { + if(hattr) { + if( ofIsStringInString( hattr.getValue(), "%")) { + float hpct = ofToFloat( cleanString(hattr.getValue(), "%" )) / 100.0f; + hattr.set( ofToString(hpct * vrect.getHeight())+"px" ); + } + } + } + } +} - /*case svgtiny_LIBXML_ERROR: - msg = "svgtiny_LIBXML_ERROR"; - break;*/ +//-------------------------------------------------------------- +string ofxSvg::cleanString( string aStr, string aReplace ) { + ofStringReplace( aStr, aReplace, ""); + return aStr; +} - case svgtiny_NOT_SVG: - msg = "svgtiny_NOT_SVG"; - break; +//-------------------------------------------------------------- +void ofxSvg::_parseXmlNode( ofXml& aParentNode, vector< shared_ptr >& aElements ) { + + auto kids = aParentNode.getChildren(); + for( auto& kid : kids ) { + if( kid.getName() == "g" ) { + auto fkid = kid.getFirstChild(); + if( fkid ) { + auto tgroup = std::make_shared(); + tgroup->layer = mCurrentLayer += 1.0; + auto idattr = kid.getAttribute("id"); + if( idattr ) { + tgroup->name = idattr.getValue(); + } + + auto kidStyle = _parseStyle(kid); + _pushCssClass(kidStyle); + + auto transAttr = kid.getAttribute("transform"); + if( transAttr ) { + setTransformFromSvgMatrixString( transAttr.getValue(), tgroup ); + } + +// // set the parent +// if( mGroupStack.size() > 0 ) { +// auto pgroup = mGroupStack.back(); +// tgroup->setParent(*pgroup.get()); +// } + tgroup->setParent(*_getPushedGroup(), false); + + pushGroup(tgroup); + aElements.push_back( tgroup ); + _parseXmlNode( kid, tgroup->getChildren() ); + popGroup(); + + _popCssClass(); + } + } else if( kid.getName() == "defs") { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " found a defs node."; + _parseXmlNode(kid, mDefElements ); + } else { + + auto ele = _addElementFromXmlNode( kid, aElements ); +// cout << "----------------------------------" << endl; +// cout << kid.getName() << " kid: " << kid.getAttribute("id").getValue() << " out xml: " << txml.toString() << endl; + } + } +} - case svgtiny_SVG_ERROR: - msg = "svgtiny_SVG_ERROR: line " + ofToString(diagram->error_line) + ": " + diagram->error_message; - break; +//-------------------------------------------------------------- +shared_ptr ofxSvg::_addElementFromXmlNode( ofXml& tnode, vector< shared_ptr >& aElements ) { + shared_ptr telement; + + bool bHasMatOrTrans = false; + + if(auto transAttr = tnode.getAttribute("transform") ) { + bHasMatOrTrans = true; + } + + if( tnode.getName() == "use") { + if( auto hrefAtt = tnode.getAttribute("xlink:href")) { + ofLogVerbose("ofxSvg") << "found a use node with href " << hrefAtt.getValue(); + std::string href = hrefAtt.getValue(); + if( href.size() > 1 && href[0] == '#' ) { + // try to find by id + href = href.substr(1, std::string::npos); + ofLogVerbose("ofxSvg") << "going to look for href " << href; + for( auto & def : mDefElements ) { + if( def->name == href ) { +// ofLogNotice("ofxSvg") << "Found a mDefElement with href: " << def->getName(); +// telement = clone(def); + telement = def->clone(); + if( !telement ) { + ofLogWarning("ofxSvg") << "could not find type for def : " << def->name; + } + break; + } + } + } else { + ofLogWarning("ofxSvg") << "could not parse use node with href : " << href; + } + } else { + ofLogWarning("ofxSvg") << "found a use node but no href!"; + } + } else if( tnode.getName() == "image" ) { + auto image = std::make_shared(); + auto wattr = tnode.getAttribute("width"); + if(wattr) image->width = wattr.getFloatValue(); + auto hattr = tnode.getAttribute("height"); + if(hattr) image->height = hattr.getFloatValue(); + auto xlinkAttr = tnode.getAttribute("xlink:href"); + if( xlinkAttr ) { + // determine if this is an embedded image // + if( ofIsStringInString(xlinkAttr.getValue(), "image/png;base64")) { + auto decodedPix = ofxSvgUtils::base64_decode(xlinkAttr.getValue() ); + if(decodedPix.isAllocated() && decodedPix.getWidth() > 0 && decodedPix.getHeight() > 0 ) { + image->img.setFromPixels(decodedPix); + } + } else { + image->filepath = folderPath; + image->filepath.append(xlinkAttr.getValue()); + telement = image; + } + } + + + } else if( tnode.getName() == "ellipse" ) { + auto ellipse = std::make_shared(); + + auto tpos = glm::vec2(0.f, 0.f); + auto cxAttr = tnode.getAttribute("cx"); + if(cxAttr) {tpos.x = cxAttr.getFloatValue();} + auto cyAttr = tnode.getAttribute("cy"); + if(cyAttr) {tpos.y = cyAttr.getFloatValue();} + + if( bHasMatOrTrans ) { + ellipse->setOffsetPathPosition(tpos.x,tpos.y); + } else { + ellipse->setPosition( tpos.x, tpos.y, 0.0f); + } + + glm::vec2 radii(0.f, 0.f); + + if(auto rxAttr = tnode.getAttribute( "rx" )) { + radii.x = rxAttr.getFloatValue(); + } + if(auto ryAttr = tnode.getAttribute( "ry" )) { + radii.y = ryAttr.getFloatValue(); + } + + ellipse->path.setCircleResolution(mCircleResolution); + ellipse->path.setCurveResolution(mCurveResolution); + // make local so we can apply transform later in the function +// ellipse->path.ellipse({0.f,0.f}, ellipse->radiusX * 2.0f, ellipse->radiusY * 2.0f ); + ellipse->setRadius(radii.x, radii.y); + + _applyStyleToPath( tnode, ellipse ); + + telement = ellipse; + } else if( tnode.getName() == "circle" ) { + auto circle = std::make_shared(); + auto tpos = glm::vec2(0.f, 0.f); + + if(auto cxAttr = tnode.getAttribute("cx")) { + tpos.x = cxAttr.getFloatValue(); + } + if(auto cyAttr = tnode.getAttribute("cy")) { + tpos.y = cyAttr.getFloatValue(); + } + + if( bHasMatOrTrans ) { + circle->setOffsetPathPosition(tpos.x,tpos.y); + } else { + circle->setPosition(tpos.x, tpos.y, 0.f); + } + + // make local so we can apply transform later in the function + // position is from the top left + circle->path.setCircleResolution(mCircleResolution); + circle->path.setCurveResolution(mCurveResolution); +// circle->path.circle({0.f,0.f}, circle->radius ); + if(auto rAttr = tnode.getAttribute( "r" )) { + circle->setRadius(rAttr.getFloatValue()); + } + + _applyStyleToPath( tnode, circle ); + + telement = circle; + + } else if( tnode.getName() == "line" ) { + auto telePath = std::make_shared(); + + glm::vec3 p1 = {0.f, 0.f, 0.f}; + glm::vec3 p2 = {0.f, 0.f, 0.f}; + auto x1Attr = tnode.getAttribute("x1"); + if(x1Attr) p1.x = x1Attr.getFloatValue(); + auto y1Attr = tnode.getAttribute("y1"); + if(y1Attr) p1.y = y1Attr.getFloatValue(); + + auto x2Attr = tnode.getAttribute("x2"); + if(x2Attr) p2.x = x2Attr.getFloatValue(); + auto y2Attr = tnode.getAttribute("y2"); + if(y2Attr) p2.y = y2Attr.getFloatValue(); + + // set the colors and stroke width, etc. + telePath->path.clear(); + telePath->path.moveTo(p1); + telePath->path.lineTo(p2); + + _applyStyleToPath( tnode, telePath ); + + telement = telePath; + + } else if(tnode.getName() == "polyline" || tnode.getName() == "polygon") { + auto tpath = std::make_shared(); + _parsePolylinePolygon(tnode, tpath); + _applyStyleToPath( tnode, tpath ); + telement = tpath; + } else if( tnode.getName() == "path" ) { + auto tpath = std::make_shared(); + _parsePath( tnode, tpath ); + _applyStyleToPath( tnode, tpath ); + telement = tpath; + } else if( tnode.getName() == "rect" ) { + auto rect = std::make_shared(); + + auto tpos = glm::vec2(0.f, 0.f); + if(auto xattr = tnode.getAttribute("x")) { + tpos.x = xattr.getFloatValue(); + } + if(auto yattr = tnode.getAttribute("y")) { + tpos.y = yattr.getFloatValue(); + } + if(auto wattr = tnode.getAttribute("width")) { + rect->width = wattr.getFloatValue(); + } + if(auto hattr = tnode.getAttribute("height")) { + rect->height = hattr.getFloatValue(); + } + + if( bHasMatOrTrans ) { + rect->setOffsetPathPosition(tpos.x,tpos.y); + } else { + rect->setPosition(tpos.x, tpos.y, 0.0f); + } + + auto rxAttr = tnode.getAttribute("rx"); + auto ryAttr = tnode.getAttribute("ry"); + + rect->path.setCircleResolution(mCircleResolution); + rect->path.setCurveResolution(mCurveResolution); + + float rRadius = 0.0f; + + if( !ofxSvgCssClass::sIsNone(rxAttr.getValue()) || !ofxSvgCssClass::sIsNone(ryAttr.getValue())) { + rRadius = std::max(ofxSvgCssClass::sGetFloat(rxAttr.getValue()), + ofxSvgCssClass::sGetFloat(ryAttr.getValue())); + } + rect->roundRadius = -1.f; + rect->setRoundRadius(rRadius); + +// // make local so we can apply transform later in the function +// if( !ofxSvgCssClass::sIsNone(rxAttr.getValue()) || !ofxSvgCssClass::sIsNone(ryAttr.getValue())) { +// rect->roundRadius = -1.f; // force an update in setRoundRadius +// rect->setRoundRadius(std::max(ofxSvgCssClass::sGetFloat(rxAttr.getValue()), +// ofxSvgCssClass::sGetFloat(ryAttr.getValue())) +// ); +//// rect->roundRadius = std::max(ofxSvgCssClass::sGetFloat(rxAttr.getValue()), +//// ofxSvgCssClass::sGetFloat(ryAttr.getValue())); +//// +//// +//// rect->path.rectRounded(tpos.x, tpos.y, rect->width, rect->height, rect->roundRadius); +// +// } else { +// rect->path.rectangle(tpos.x, tpos.y, rect->getWidth(), rect->getHeight()); +// } + + telement = rect; + + _applyStyleToPath( tnode, rect ); + + // this shouldn't be drawn at all, may be a rect that for some reason is generated + // by text blocks // + if( !rect->isFilled() && !rect->hasStroke() ) { + telement->setVisible(false); + } + + } else if( tnode.getName() == "text" ) { + auto text = std::make_shared(); + telement = text; + + auto textCss = _parseStyle( tnode ); + _pushCssClass(textCss); +// auto tempCss = mCurrentCss; +// textCss.addMissingClassProperties(tempCss); +// mCurrentCss = textCss; + +// ofLogNotice("ofxSvg") << "_addElementFromXmlNode :: text: " << "has kids: " << tnode.getFirstChild() << " node value: " << tnode.getValue() << std::endl; +// ofLogNotice("ofxSvg") << "_addElementFromXmlNode :: text: " << "name:" << tnode.getName() << " to string: " << tnode.toString() << std::endl; +// if( tnode.getAttribute("id")) { +// ofLogNotice("ofxSvg") << "_addElementFromXmlNode :: text: " << tnode.getAttribute("id").getValue(); +// } + + if( tnode.getFirstChild() ) { + + auto kids = tnode.getChildren(); + for( auto& kid : kids ) { + if(kid) { + if( kid.getName() == "tspan" ) { +// text->textSpans.push_back( _getTextSpanFromXmlNode( kid ) ); + _getTextSpanFromXmlNode( kid, text->textSpans ); + } + } + } + + // this may not be a text block or it may have no text // + if( text->textSpans.size() == 0 ) { + // ok lets see if the node has a value / text + if( !tnode.getValue().empty() ) { + _getTextSpanFromXmlNode( tnode, text->textSpans ); + } + } + } + _popCssClass(); + + string tempFolderPath = ofFilePath::addTrailingSlash(folderPath); + if( ofDirectory::doesDirectoryExist( tempFolderPath+"fonts/" )) { + text->setFontDirectory( tempFolderPath+"fonts/" ); + } + if( fontsDirectory != "" ) { + if( ofDirectory::doesDirectoryExist(fontsDirectory)) { + text->setFontDirectory(fontsDirectory); + } + } + + } else if( tnode.getName() == "g" ) { + + } + + if( !telement ) { + return shared_ptr(); + } + + auto idAttr = tnode.getAttribute("id"); + if( idAttr ) { + telement->name = idAttr.getValue(); + } + +// if( telement->getType() == OFXSVG_TYPE_RECTANGLE || telement->getType() == OFXSVG_TYPE_IMAGE || telement->getType() == OFXSVG_TYPE_TEXT || telement->getType() == OFXSVG_TYPE_CIRCLE || telement->getType() == OFXSVG_TYPE_ELLIPSE ) { + if( telement->getType() != OFXSVG_TYPE_DOCUMENT ) { + auto transAttr = tnode.getAttribute("transform"); + if( transAttr ) { +// getTransformFromSvgMatrix( transAttr.getValue(), telement->pos, telement->scale.x, telement->scale.y, telement->rotation ); + setTransformFromSvgMatrixString( transAttr.getValue(), telement ); + } + } + + if( telement->getType() == OFXSVG_TYPE_TEXT ) { + auto text = std::dynamic_pointer_cast( telement ); + text->create(); + } + + _applyStyleToElement(tnode, telement); + + telement->layer = mCurrentLayer += 1.0; + aElements.push_back( telement ); + + if( telement->getType() == OFXSVG_TYPE_RECTANGLE ) { + auto rect = std::dynamic_pointer_cast( telement ); + ofLogVerbose("ofxSvg::_addElementFromXmlNode") << "rect->pos: " << rect->getGlobalPosition() << " shape: " << rect->getOffsetPathPosition(); + } + + if( mGroupStack.size() > 0 ) { + auto pgroup = mGroupStack.back(); + ofLogVerbose("ofxSvg::_addElementFromXmlNode") << "element: " << telement->getTypeAsString() << " -" << telement->getCleanName() << "- pos: " << telement->getPosition() << "- parent: " << pgroup->getCleanName(); + telement->setParent(*_getPushedGroup(), false); + } + + return telement; +} - default: - msg = "unknown svgtiny_code " + ofToString(code); - break; +std::vector parseToFloats(const std::string& input) { + std::vector points; + std::regex regex("[-]?\\d*\\.?\\d+"); // Matches positive/negative floats + std::sregex_iterator begin(input.begin(), input.end(), regex), end; + + std::vector values; + + // Extract all floating-point values using regex + for (std::sregex_iterator i = begin; i != end; ++i) { + try { + values.push_back(std::stof((*i).str())); + } catch (const std::invalid_argument&) { + std::cerr << "Invalid number found: " << (*i).str() << std::endl; } - ofLogError("ofxSVG") << "load(): couldn't parse \"" << urlstring << "\": " << msg; } + + return values; +} + +std::vector parsePoints(const std::string& input) { + std::vector points; + auto values = parseToFloats( input ); + + // Create vec3 pairs from the values + for (size_t i = 0; i < values.size(); i += 2) { + if (i + 1 < values.size()) { + glm::vec3 point(values[i], values[i + 1], 0.f); + points.push_back(point); + } + } + + if( values.size() == 1 && points.size() < 1) { + glm::vec3 point(values[0], values[0], 0.f); + points.push_back(point); + } + + return points; +} - setupDiagram(diagram); - svgtiny_free(diagram); +std::vector parsePointsDefaultY(const std::string& input, float aYpos ) { + std::vector points; + auto values = parseToFloats( input ); + + // Create vec3 pairs from the values + for (size_t i = 0; i < values.size(); i++) { + glm::vec3 point(values[i], aYpos, 0.f); + points.push_back(point); + } + + if( values.size() == 1 && points.size() < 1) { + glm::vec3 point(values[0], aYpos, 0.f); + points.push_back(point); + } + + return points; } -void ofxSvg::fixSvgString(std::string & xmlstring) { +std::vector parsePointsDefaultX(const std::string& input, float aXpos ) { + std::vector points; + auto values = parseToFloats( input ); + + // Create vec3 pairs from the values + for (size_t i = 0; i < values.size(); i++) { + glm::vec3 point(aXpos, values[i], 0.f); + points.push_back(point); + } + + if( values.size() == 1 && points.size() < 1) { + glm::vec3 point(aXpos, values[0], 0.f); + points.push_back(point); + } + + return points; +} - ofXml xml; - xml.parse(xmlstring); - // so it turns out that if the stroke width is <1 it rounds it down to 0, - // and makes it disappear because svgtiny stores strokewidth as an integer! - ofXml::Search strokeWidthElements = xml.find("//*[@stroke-width]"); - if (!strokeWidthElements.empty()) { - for (ofXml & element : strokeWidthElements) { - //cout << element.toString() << endl; - float strokewidth = element.getAttribute("stroke-width").getFloatValue(); - strokewidth = std::fmax(1.0, std::round(strokewidth)); - element.getAttribute("stroke-width").set(strokewidth); +//---------------------------------------------------- +std::vector _parseSvgArc(const std::string& arcStr) { + std::vector result; + std::regex numberRegex(R"([-+]?[0-9]*\.?[0-9]+)"); // Improved regex for better compatibility + std::sregex_iterator iter(arcStr.begin(), arcStr.end(), numberRegex); + std::sregex_iterator end; + + while (iter != end) { + try { + result.push_back(std::stod(iter->str())); + } catch (const std::exception& e) { + std::cerr << "Error parsing number: " << iter->str() << " - " << e.what() << std::endl; } + ++iter; } + + return result; +} - // Affinity Designer does not set width/height as pixels but as a percentage - // and relies on the "viewBox" to convey the size of things. this applies the - // viewBox to the width and height. +//---------------------------------------------------- +int _getWindingOrderOnArc( glm::vec3& aStartPos, glm::vec3& aCenterPos, glm::vec3& aendPos ) { + glm::vec3 sdiff = (aStartPos - aCenterPos); + if( glm::length2(sdiff) > 0.0f ) { + sdiff = glm::normalize(sdiff); + } + glm::vec3 ediff = (aendPos - aCenterPos); + if( glm::length2(ediff) > 0.0f ) { + ediff = glm::normalize(ediff); + } + float tcross = sdiff.x * ediff.y - sdiff.y * ediff.x; +// ofLogNotice("_getWindingOrderOnArc") << "tcross is " << tcross; + if( tcross > 0.0f ) { + // clockwise + return 1; + } else if( tcross < 0.0f ) { + // counter clockwise + return -1; + } + // co-linear + return 0; + +} - std::vector rect; - for (ofXml & element : xml.find("//*[@viewBox]")) { - rect = ofSplitString(element.getAttribute("viewBox").getValue(), " "); +//---------------------------------------------------- +// Function to find the center of the elliptical arc from SVG arc parameters +glm::vec2 findArcCenter(glm::vec2 start, glm::vec2 end, double rx, double ry, double x_axis_rotation, bool large_arc_flag, bool sweep_flag) { + // Convert the rotation to radians + double phi = glm::radians(x_axis_rotation); + double cos_phi = cos(phi); + double sin_phi = sin(phi); + + // Step 1: Compute (x1', y1') - the coordinates of the start point in the transformed coordinate system + glm::vec2 diff = (start - end) / 2.0f; + glm::vec2 p1_prime(cos_phi * diff.x + sin_phi * diff.y, -sin_phi * diff.x + cos_phi * diff.y); + + // Step 2: Correct radii if necessary + double p1_prime_x_sq = p1_prime.x * p1_prime.x; + double p1_prime_y_sq = p1_prime.y * p1_prime.y; + double rx_sq = rx * rx; + double ry_sq = ry * ry; + double radii_check = p1_prime_x_sq / rx_sq + p1_prime_y_sq / ry_sq; + if (radii_check > 1) { + // Scale radii to ensure the arc can fit between the two points + double scale = std::sqrt(radii_check); + rx *= scale; + ry *= scale; + rx_sq = rx * rx; + ry_sq = ry * ry; + } + + // Step 3: Compute (cx', cy') - the center point in the transformed coordinate system + double factor_numerator = rx_sq * ry_sq - rx_sq * p1_prime_y_sq - ry_sq * p1_prime_x_sq; + double factor_denominator = rx_sq * p1_prime_y_sq + ry_sq * p1_prime_x_sq; + if (factor_numerator < 0) { + factor_numerator = 0; // Precision error correction to avoid sqrt of negative numbers } + + double factor = std::sqrt(factor_numerator / factor_denominator); + if (large_arc_flag == sweep_flag) { + factor = -factor; + } + + glm::vec2 center_prime(factor * rx * p1_prime.y / ry, factor * -ry * p1_prime.x / rx); + + // Step 4: Compute the center point in the original coordinate system + glm::vec2 center( + cos_phi * center_prime.x - sin_phi * center_prime.y + (start.x + end.x) / 2.0, + sin_phi * center_prime.x + cos_phi * center_prime.y + (start.y + end.y) / 2.0 + ); + + return center; +} - if (rect.size() == 4) { - for (ofXml & element : xml.find("//*[@width]")) { - if (element.getAttribute("width").getValue() == "100%") { - auto w = ofToFloat(rect.at(2)); - ofLogWarning("ofxSvg::fixSvgString()") << "the SVG size is provided as percentage, which svgtiny translates to 0. The width is corrected from the viewBox width: " << w; - element.getAttribute("width").set(w); +//-------------------------------------------------------------- +void ofxSvg::_parsePolylinePolygon( ofXml& tnode, std::shared_ptr aSvgPath ) { + auto pointsAttr = tnode.getAttribute("points"); + if( !pointsAttr ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " polyline or polygon does not have a points attriubute."; + return; + } + + if( pointsAttr.getValue().empty() ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " polyline or polygon does not have points."; + return; + } + + auto points = parsePoints(pointsAttr.getValue()); + std::size_t numPoints = points.size(); + for( std::size_t i = 0; i < numPoints; i++ ) { + if( i == 0 ) { + aSvgPath->path.moveTo(points[i]); + } else { + aSvgPath->path.lineTo(points[i]); + } + } + if( numPoints > 2 ) { + if(tnode.getName() == "polygon" ) { + aSvgPath->path.close(); + } + } +} +// reference: https://www.w3.org/TR/SVG2/paths.html#PathData +//-------------------------------------------------------------- +void ofxSvg::_parsePath( ofXml& tnode, std::shared_ptr aSvgPath ) { + + aSvgPath->path.clear(); + + auto dattr = tnode.getAttribute("d"); + if( !dattr ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " path node does not have d attriubute."; + return; + } + +// OF_POLY_WINDING_ODD + /// OF_POLY_WINDING_NONZERO + /// OF_POLY_WINDING_POSITIVE + /// OF_POLY_WINDING_NEGATIVE + /// OF_POLY_WINDING_ABS_GEQ_TWO +// aSvgPath->path.setPolyWindingMode(OF_POLY_WINDING_POSITIVE); +// aSvgPath->path.setPolyWindingMode(mDefaultPathWindingMode); + + + aSvgPath->path.setCircleResolution(mCircleResolution); + aSvgPath->path.setCurveResolution(mCurveResolution); + + std::vector splitChars = { + 'M', 'm', // move to + 'V', 'v', // vertical line + 'H', 'h', // horizontal line + 'L','l', // line + 'z','Z', // close path + 'c','C','s','S', // cubic bézier + 'Q', 'q', 'T', 't', // quadratic bézier + 'A', 'a' // elliptical arc + }; + std::string ostring = dattr.getValue(); +// ofLogNotice("ofxSvg") << __FUNCTION__ << " dattr: " << ostring; + + if( ostring.empty() ) { + ofLogError("ofxSvg") << __FUNCTION__ << " there is no data in the d string."; + return; + } + + std::size_t index = 0; + if( ostring[index] != 'm' && ostring[index] != 'M' ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " first char is not a m or M, ostring[index]: " << ostring[index]; + return; + } + + glm::vec3 currentPos = {0.f, 0.f, 0.f}; + glm::vec3 secondControlPoint = currentPos; + glm::vec3 qControlPoint = currentPos; + + int numSubPathsClosed = 0; + int numSubPaths = 0; + + auto convertToAbsolute = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for(auto& apos : aposes ) { + if( aBRelative ) { + apos += aCurrentPos; + } + } + if( aposes.size() > 0 ) { + aCurrentPos = aposes.back(); + } + return aCurrentPos; + }; + + auto convertToAbsolute2 = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for( std::size_t k = 0; k < aposes.size(); k+= 1 ) { + if( aBRelative ) { + aposes[k] += aCurrentPos; + } + if( k > 0 && k % 2 == 1 ) { + aCurrentPos = aposes[k]; + } + } + return aCurrentPos; + }; + + auto convertToAbsolute3 = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for( std::size_t k = 0; k < aposes.size(); k+= 1 ) { + if( aBRelative ) { + aposes[k] += aCurrentPos; + } + + if( k > 0 && k % 3 == 2 ) { + aCurrentPos = aposes[k]; } + + } + return aCurrentPos; + }; + + auto lineToRelativeFromAbsoluteRecursive = [](glm::vec3& aStartPos, glm::vec3& acurrentPos, std::vector& aposes, std::shared_ptr aPath ) { + // int ncounter = 0; + auto cp = aStartPos; + for( auto& np : aposes ) { + auto relativePos = np-aStartPos; +// auto relativePos = np; + auto newPos = relativePos + cp; + aPath->path.lineTo(newPos); + cp = newPos;//relativePos+prevPos; +// ncounter++; } + acurrentPos = cp; + }; + + std::string tname; + if( auto tattr = tnode.getAttribute("id")) { + tname = tattr.getValue(); + } + + ofLogVerbose("ofxSvg::_parsePath") << " ------ PARSE-" << tname << "-----------------------" ; + + aSvgPath->path.clear(); + + unsigned int justInCase = 0; +// std::vector commands; + bool breakMe = false; + while( index < ostring.size() && !breakMe && justInCase < 999999) { + // figure out what we have here . + auto cchar = ostring[index]; + // check for valid character // + bool bFoundValidChar = false; + for( auto& sc : splitChars ) { + if( sc == cchar ) { + bFoundValidChar = true; + break; + } + } + if( !bFoundValidChar ) { + ofLogWarning("svgParser") << "Did not find valid character: " << cchar; + breakMe = true; + break; + } + + + ofLogVerbose("ofxSvg") << tname << "- o : ["<< ostring[index] <<"]"; + + // up to next valid character // + std::string currentString; + bool bFoundValidNextChar = false; + auto pos = index+1; + if( pos >= ostring.size() ) { +// ofLogVerbose("ofxSvg") << "pos is greater than string size: " << pos << " / " << ostring.size(); +// break; + breakMe = true; + } + + bFoundValidChar = false; + for( pos = index+1; pos < ostring.size(); pos++ ) { + for( auto& sc : splitChars ) { + if( sc == ostring[pos] ) { + bFoundValidChar = true; + break; + } + } + if( bFoundValidChar ) { + break; + } + currentString.push_back(ostring[pos]); + } + + + int cindex = index; + index += currentString.size()+1; + + + if( currentString.empty() ) { + ofLogVerbose("ofxSvg") << "currentString is empty: " << cchar; +// break; + } + + + ofLogVerbose("ofxSvg") <<"["< npositions= {glm::vec3(0.f, 0.f, 0.f)}; + /// \note: ofxSvgOptional is declared in ofxSvgUtils + /// Using a custom class because older versions of OF did not include std::optional. + ofxSvgOptional ctype; + + // check if we are looking for a position + if( cchar == 'm' || cchar == 'M' ) { + /* ------------------------------------------------ +// Reference: https://www.w3.org/TR/SVG/paths.html + "Start a new sub-path at the given (x,y) coordinates. M (uppercase) indicates that absolute coordinates will follow; + m (lowercase) indicates that relative coordinates will follow. + If a moveto is followed by multiple pairs of coordinates, the subsequent pairs are treated as implicit lineto commands. + Hence, implicit lineto commands will be relative if the moveto is relative, and absolute if the moveto is absolute. + If a relative moveto (m) appears as the first element of the path, then it is treated as a pair of absolute coordinates. + In this case, subsequent pairs of coordinates are treated as relative even though the initial moveto is interpreted as an absolute moveto." + ------------------------------------------------ */ + + if( cchar == 'm' && cindex > 0 ) { + bRelative = true; + } + npositions = parsePoints(currentString); + for( int ni = 0; ni < npositions.size(); ni++ ) { + ofLogVerbose("ofxSvg::_parsePath") << ni << "->" << npositions[ni]; + } + ctype = ofPath::Command::moveTo; + numSubPaths++; + } else if( cchar == 'v' || cchar == 'V' ) { + + float xvalue = 0.f; + if( cchar == 'v' ) { + bRelative = true; + } else { + xvalue = currentPos.x; + } + npositions = parsePointsDefaultX(currentString,xvalue); + //ofLogVerbose("ofxSvg") << cchar << " line to: " << npositions[0] << " current pos: " << currentPos; + ctype = ofPath::Command::lineTo; + } else if( cchar == 'H' || cchar == 'h' ) { + float yvalue = 0.f; + if( cchar == 'h' ) { + bRelative = true; + } else { + yvalue = currentPos.y; + } + npositions = parsePointsDefaultY(currentString,yvalue); + ctype = ofPath::Command::lineTo; + } else if( cchar == 'L' || cchar == 'l' ) { + if( cchar == 'l' ) { + bRelative = true; + } + npositions = parsePoints(currentString); +// for( auto& np : npositions ) { +// ofLogVerbose("ofxSvg") << cchar << " line to: " << np; +// } + ctype = ofPath::Command::lineTo; + } else if( cchar == 'z' || cchar == 'Z' ) { + ctype = ofPath::Command::close; + npositions.clear(); + } else if( cchar == 'c' || cchar == 'C' || cchar == 'S' || cchar == 's' ) { + if( cchar == 'c' || cchar == 's') { + bRelative = true; + } + ctype = ofPath::Command::bezierTo; + npositions = parsePoints(currentString); +// for( auto& np : npositions ) { +// ofLogVerbose("ofxSvg") << cchar << " bezier to: " << np; +// } + } else if( cchar == 'Q' || cchar == 'q' || cchar == 'T' || cchar == 't' ) { + if( cchar == 'q' ) { + bRelative = true; + } + + ctype = ofPath::Command::quadBezierTo; + npositions = parsePoints(currentString); + +// for( auto& np : npositions ) { +// ofLogNotice("ofxSvg") << " Quad bezier to: " << np; +// } + } else if(cchar == 'a' || cchar == 'A' ) { + if( cchar == 'a' ) { + bRelative = true; + } + ctype = ofPath::Command::arc; +// npositions = _parseStrCoordsFunc(currentString); + auto arcValues = _parseSvgArc(currentString); + if( arcValues.size() == 7 ) { + npositions = { + glm::vec3(arcValues[0], arcValues[1], 0.0f), + glm::vec3(arcValues[2], 0.0f, 0.0f), + glm::vec3(arcValues[3], arcValues[4], 0.0f), + glm::vec3(arcValues[5], arcValues[6], 0.0f) + }; + } else { + ofLogError("ofxSvg") << "unable to parse arc command, incorrect number of parameters detected: " << arcValues.size(); + ofLogError("ofxSvg") << "-- Arc values ---------------------- "; + for( std::size_t n = 0; n < arcValues.size(); n++ ) { + ofLogError("ofxSvg") << n << ": " << arcValues[n]; + } + + } +// for( auto& np : npositions ) { +// ofLogNotice("ofxSvg") << " arc parsed positions: " << np; +// } + } + + if( ctype.has_value() ) { + + auto prevPos = currentPos; + auto commandT = ctype.value(); + + if( commandT == ofPath::Command::arc ) { + if( npositions.size() == 4 ) { + std::vector tpositions = {npositions[3]}; + currentPos = convertToAbsolute(bRelative, currentPos, tpositions ); + npositions[3] = tpositions[0]; + } else { + ofLogWarning("ofxSvg") << "invalid number of arc commands."; + } + } else if( commandT == ofPath::Command::bezierTo ) { + if( cchar == 'S' || cchar == 's' ) { + currentPos = convertToAbsolute2(bRelative, currentPos, npositions ); + } else { + currentPos = convertToAbsolute3(bRelative, currentPos, npositions ); + } +// } else if( commandT == ofPath::Command::quadBezierTo ) { + // TODO: Check quad bezier for poly bezier like cubic bezier + + } else { + if( npositions.size() > 0 && commandT != ofPath::Command::close ) { + if( commandT == ofPath::Command::moveTo ) { + // going to handle this below; + // inside the if( commandT == ofPath::Command::moveTo ) { check. + } else { + currentPos = convertToAbsolute(bRelative, currentPos, npositions ); + } + } + } + + if( commandT != ofPath::Command::bezierTo ) { + secondControlPoint = currentPos; + } + if( commandT != ofPath::Command::quadBezierTo ) { + qControlPoint = currentPos; + } + + if( commandT == ofPath::Command::moveTo ) { + + if( cchar == 'm' ) { + if(npositions.size() > 0 ) { + if(cindex == 0 ) { + // this is the first m, so the moveTo is absolute but the subsequent points are relative + currentPos = npositions[0]; + } else { + currentPos += npositions[0]; + } + } + } else { + if(npositions.size() > 0 ) { + currentPos = npositions[0]; + } + } + + if( npositions.size() > 0 ) { + ofLogVerbose("ofxSvg::moveTo") << npositions[0] << " currentPos: " << currentPos;// << " path pos: " << aSvgPath->pos; + aSvgPath->path.moveTo(currentPos); + } + + if(npositions.size() > 1 ) { + bool bLineToRelative = bRelative; + // determine if these points started with m and is the first character + if( cchar == 'm') { + bLineToRelative = true; + } + + if( bLineToRelative ) { + for( int ki = 1; ki < npositions.size(); ki++ ) { + currentPos += npositions[ki]; + aSvgPath->path.lineTo(currentPos); + } + } else { + for( int ki = 1; ki < npositions.size(); ki++ ) { + aSvgPath->path.lineTo(npositions[ki]); + } + if(npositions.size() > 0 ) { + currentPos = npositions.back(); + } + } + } + + secondControlPoint = currentPos; + qControlPoint = currentPos; + + } else if( commandT == ofPath::Command::lineTo ) { + if( npositions.size() > 0 ) { + // current pos is already set above + // so just worry about adding paths + if( bRelative ) { + lineToRelativeFromAbsoluteRecursive(prevPos, currentPos, npositions, aSvgPath ); + } else { + int ncounter = 0; + for( auto& np : npositions ) { + ofLogVerbose("ofxSvg::lineTo") << ncounter << "--->"<< np << " prevPos:" << prevPos; + aSvgPath->path.lineTo(np); + ncounter++; + } + } + } + } else if( commandT == ofPath::Command::close ) { +// ofLogNotice("ofxSvg") << "Closing the path"; + // TODO: Not sure if we need to draw a line to the start point here + aSvgPath->path.close(); + numSubPathsClosed++; + } else if( commandT == ofPath::Command::bezierTo ) { + + if( cchar == 'S' || cchar == 's' ) { + // these can come in as multiple sets of points // + std::vector ppositions;// = npositions; + auto tppos = prevPos; + for( std::size_t i = 0; i < npositions.size(); i += 2 ) { + auto cp2 = (secondControlPoint - tppos) * -1.f; + cp2 += tppos; + ppositions.push_back( cp2 ); + ppositions.push_back(npositions[i+0]); + ppositions.push_back(npositions[i+1]); + tppos = npositions[i+1]; + secondControlPoint = npositions[i+0]; + } + + npositions = ppositions; + } + + auto tcpos = prevPos; + + for( std::size_t k = 0; k < npositions.size(); k +=3 ) { + aSvgPath->path.bezierTo(npositions[k+0], npositions[k+1], npositions[k+2]); + secondControlPoint = npositions[k+1]; + + mCPoints.push_back(prevPos); + mCPoints.push_back(npositions[k+0]); + mCPoints.push_back(npositions[k+1]); + tcpos = npositions[k+2]; + } + } else if( commandT == ofPath::Command::quadBezierTo ) { + if( cchar == 'T' || cchar == 't' ) { + if( npositions.size() == 1 ) { + auto cp2 = (qControlPoint - prevPos) * -1.f; + cp2 += prevPos; + npositions.insert(npositions.begin(), cp2 ); + } + } + + if( npositions.size() == 2 ) { + aSvgPath->path.quadBezierTo(prevPos, npositions[0], npositions[1] ); + } + qControlPoint = npositions[0]; + } else if( commandT == ofPath::Command::arc ) { + if( npositions.size() == 4 ) { + // first point is rx, ry + // second point x value is x-axis rotation + // third point x value is large-arc-flag, y value is sweep-flag + // fourth point is x and y: When a relative a command is used, the end point of the arc is (cpx + x, cpy + y). + glm::vec3 radii = npositions[0]; + float xAxisRotation = npositions[1].x; + float largeArcFlag = std::clamp( npositions[2].x, 0.f, 1.f ); + float sweepFlag = std::clamp( npositions[2].y, 0.f, 1.f ); + + glm::vec3 spt = prevPos; + glm::vec3 ept = npositions[3]; + + auto cpt = glm::vec3(findArcCenter(spt, ept, radii.x, radii.y, xAxisRotation, largeArcFlag, sweepFlag ), 0.f); + auto windingOrder = _getWindingOrderOnArc( spt, cpt, ept ); + + if( largeArcFlag < 1 ) { + if( sweepFlag > 0 ) { + if( windingOrder < 0 ) { + windingOrder *= -1.f; + } + } else { + if( windingOrder > 0 ) { + windingOrder *= -1.f; + } + } + } else { + if( sweepFlag > 0 ) { + if( windingOrder < 1 ) { + windingOrder *= -1.f; + } + } else { + if( windingOrder > -1 ) { + windingOrder *= -1.f; + } else { + + } + } + } + + + auto startDiff = (spt - cpt); + if( glm::length2(startDiff) > 0.0f ) { + startDiff = glm::normalize(startDiff); + } else { + startDiff = glm::vec3(1.f, 0.f, 0.f ); + } + auto endDiff = (ept - cpt); + if( glm::length2(endDiff) > 0.0f ) { + endDiff = glm::normalize(endDiff); + } else { + endDiff = glm::vec3(1.f, 0.f, 0.f ); + } + + float startAngle = atan2f( startDiff.y, startDiff.x );// - glm::radians(40.f); + float endAngle = atan2f( endDiff.y, endDiff.x ); + + float xrotRad = glm::radians(xAxisRotation); + + startAngle = ofWrapRadians(startAngle); + endAngle = ofWrapRadians(endAngle); + + + std::string worderS = "co linear"; + if( windingOrder > 0 ) { + worderS = "clockwise"; + } else if( windingOrder < 0 ) { + worderS = "counter clockwise"; + } + + ofLogVerbose("ofxSvg") << "Arc winding order is: " << worderS << " order: " << windingOrder << " startDiff: " << startDiff << " endDiff: " << endDiff << " xAxisRotation: " << xAxisRotation; + + ofPolyline tline; + + if( windingOrder < 0 ) { + tline.arcNegative(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad), mCircleResolution ); + } else { + tline.arc(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad), mCircleResolution ); + } + + // rotate based on x-axis rotation // +// aSvgPath->path.rotateRad(xrotRad, glm::vec3(0.0f, 0.0f, 1.f)); + + for( auto& pv : tline.getVertices() ) { + auto nv = pv - cpt; + if( glm::length2(nv) > 0.0f ) { + nv = glm::vec3( glm::rotate(glm::vec2(nv.x, nv.y), xrotRad), 0.f); + } + nv += cpt; + pv.x = nv.x; + pv.y = nv.y; + } +//// +// // I guess we have to copy the line via commands + if( tline.size() > 0 ) { +// aSvgPath->path.moveTo(spt); + for( std::size_t i = 0; i < tline.size(); i++ ) { +// if( i == 0 ) { +// aSvgPath->path.moveTo(tline[0]); +// } else { + aSvgPath->path.lineTo(tline[i]); +// } + } + } + + mCenterPoints.push_back(cpt); +// mCenterPoints.push_back(cpt); + npositions.clear(); + npositions.push_back(ept); + } else { + ofLogWarning("ofxSvg") << "unable to parse arc segment."; + } + } + } + +// ofLogNotice("ofxSvg") << "["<path.setPolyWindingMode(mDefaultClosedPathWindingMode); + } + + justInCase++; + } +} - for (ofXml & element : xml.find("//*[@height]")) { - if (element.getAttribute("height").getValue() == "100%") { - auto w = ofToFloat(rect.at(3)); - ofLogWarning("ofxSvg::fixSvgString()") << "the SVG size is provided as percentage, which svgtiny translates to 0. The height is corrected from the viewBox height: " << w; - element.getAttribute("height").set(w); +//-------------------------------------------------------------- +ofxSvgCssClass ofxSvg::_parseStyle( ofXml& anode ) { + ofxSvgCssClass css; + + // apply first if we have a global style // + for( auto& tprop : mCurrentCss.properties ) { + if( tprop.first.empty() ) { + ofLogNotice("ofxSvg") << "First prop is empty"; + } + css.addProperty(tprop.first, tprop.second); + } + + // now apply all of the other via css classes // + // now lets figure out if there is any css applied // + if( auto classAttr = anode.getAttribute("class") ) { + // get a list of classes, is this separated by commas? + auto classList = ofSplitString(classAttr.getValue(), ","); +// ofLogNotice("ofxSvg") << " going to try and parse style classes string: " << classAttr.getValue(); + for( auto& className : classList ) { + if( mSvgCss.hasClass(className) ) { +// ofLogNotice("ofxSvg") << " has class " << className; + // now lets try to apply it to the path + auto& tCss = mSvgCss.getClass(className); + for( auto& tprop : tCss.properties ) { +// ofLogNotice("ofxSvg") << " adding property " << tprop.first << " value: " << tprop.second.srcString; + css.addProperty(tprop.first, tprop.second); + } + } + } + } + + // locally set on node overrides the class listing + // are there any properties on the node? + + // avoid the following + std::vector reservedAtts = { + "d", "id", "xlink:href", "width", "height", "rx", "ry", "cx", "cy", "r", "style", "font-family", + "x","y","x1","y1","x2","y2","transform" + }; + + // lets try to do this a better way + for( auto& att : anode.getAttributes() ) { + auto atName = ofToLower(att.getName()); + bool bFileIt = true; + for( auto& rattName : reservedAtts ) { + if( atName == rattName ) { + bFileIt=false; + break; } } + if( bFileIt ) { + css.addProperty(att.getName(), att.getValue()); + } + } + + if( auto ffattr = anode.getAttribute("font-family") ) { + std::string tFontFam = ffattr.getValue(); + ofStringReplace( tFontFam, "'", "" ); + css.addProperty("font-family", tFontFam); + } + + // and lastly style + if( auto styleAttr = anode.getAttribute("style") ) { + css.addProperties(styleAttr.getValue()); + } + + return css; +} + +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToElement( ofXml& tnode, std::shared_ptr aEle ) { + auto css = _parseStyle(tnode); + if( css.hasAndIsNone("display")) { + ofLogVerbose("ofxSvg") << "setting element to invisible: " << aEle->name; + aEle->setVisible(false); } +} + +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToPath( ofXml& tnode, std::shared_ptr aSvgPath ) { + auto css = _parseStyle(tnode); + aSvgPath->applyStyle(css); +} - //lib svgtiny doesn't remove elements with display = none, so this code fixes that +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToText( ofXml& anode, std::shared_ptr aTextSpan ) { + auto css = _parseStyle(anode); + aTextSpan->applyStyle(css); +} - bool finished = false; - while (!finished) { - ofXml::Search invisibleElements = xml.find("//*[@display=\"none\"]"); +//-------------------------------------------------------------- +glm::vec3 ofxSvg::_parseMatrixString(const std::string& input, const std::string& aprefix, bool abDefaultZero ) { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " input: " << input;; + std::string searchStr = aprefix + "("; + size_t startPos = input.find(searchStr); + + if (startPos != std::string::npos) { + startPos += searchStr.size(); + size_t endPos = input.find(")", startPos); + + if (endPos != std::string::npos) { + // Extract the part inside the parentheses + std::string inside = input.substr(startPos, endPos - startPos); + + std::replace(inside.begin(), inside.end(), ',', ' '); + + // Ensure numbers like ".5" are correctly handled by adding a leading zero if needed + if (inside[0] == '.') { + inside = "0" + inside; + } + + float tx = 0.f, ty = 0.f, tz = 0.f; + std::stringstream ss(inside); + if (ss >> tx) { + if (!(ss >> ty)) { + if(abDefaultZero) { + ty = 0.0f; + } else { + ty = tx; // If only one value is provided, duplicate it + } + } + if (!(ss >> tz)) { + if( abDefaultZero) { + tz = 0.0f; + } else { + tz = ty; // If only two values are provided, duplicate the second one + } + } + return glm::vec3(tx, ty, tz); + } + } + } + return glm::vec3(0.f, 0.f, 0.0f); +} + +//-------------------------------------------------------------- +glm::mat4 ofxSvg::setTransformFromSvgMatrixString( string aStr, std::shared_ptr aele ) { + ofLogVerbose("-----------ofxSvg::setTransformFromSvgMatrixString") << aele->getTypeAsString() << " name: " << aele->getName() +"----------------"; +// aele->rotation = 0.0; + aele->setScale(1.f); + aele->mModelRotationPoint = glm::vec2(0.0f, 0.0f); + // TODO: Should a matrix push and pop structure, similar to renderers, be implemented? + ofLogVerbose("ofxSvg") << __FUNCTION__ << " name: " << aele->getName() << " going to parse string: " << aStr << " pos: " << aele->getPosition(); + + float trotation = 0.f; + glm::mat4 mat = glm::mat4(1.f); + + if( ofIsStringInString(aStr, "translate")) { + auto transStr = aStr; + auto tp = _parseMatrixString(transStr, "translate", false ); + tp.z = 0.f; + ofLogVerbose("ofxSvg::setTransformFromSvgMatrixString") << aele->getTypeAsString() << " name: " << aele->getName() << " translate: " << tp; +// apos += tp; + mat = glm::translate(glm::mat4(1.0f), glm::vec3(tp.x, tp.y, 0.0f)); +// gmat = glm::translate(gmat, glm::vec3(tp.x, tp.y, 0.0f)); + aele->setPosition(tp.x, tp.y, 0.0f); + } else { + mat = glm::translate(glm::mat4(1.0f), glm::vec3(0.f, 0.f, 0.0f)); +// gmat = glm::translate(gmat, glm::vec3(0.f, 0.f, 0.0f)); + } + + if( ofIsStringInString(aStr, "rotate")) { + auto transStr = aStr; + auto tr = _parseMatrixString(transStr, "rotate", true ); + trotation = tr.x; + if( trotation != 0.f ) { + glm::vec2 rcenter(0.f, 0.f); + if( tr.y != 0.0f || tr.z != 0.0f ) { + rcenter.x = tr.y; + rcenter.y = tr.z; + + aele->mModelRotationPoint = rcenter; + + glm::vec3 pivot(rcenter.x, rcenter.y, 0.f); + // Step 1: Translate to pivot (move pivot to origin) + glm::mat4 toOrigin = glm::translate(glm::mat4(1.0f), -pivot ); + + // Step 2: Apply rotation + glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f) ); + + // Step 3: Translate back to original position + glm::mat4 backToPivot = glm::translate(glm::mat4(1.0f), pivot); + + // Apply transformations in the correct order: T_back * R * T_origin * Original_Transform + mat = backToPivot * rotation * toOrigin * mat; +// gmat = backToPivot * rotation * toOrigin * gmat; + } else { +// mat = mat * glm::toMat4((const glm::quat&)glm::angleAxis(glm::radians(aele->rotation), glm::vec3(0.f, 0.f, 1.f))); + mat = glm::rotate(mat, glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f)); +// gmat = glm::rotate(gmat, glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f)); + } + + // now we need to apply the rotation + aele->setOrientation(glm::angleAxis(glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f) )); +// ofLogNotice("ofxSvg") << "rcenter: " << rcenter.x << ", " << rcenter.y; + } + ofLogVerbose("ofxSvg") << __FUNCTION__ << " name: " << aele->getName() << " arotation: " << trotation << " trot: " << tr; + } + + if( ofIsStringInString(aStr, "scale")) { + auto transStr = aStr; + auto ts = _parseMatrixString(transStr, "scale", false ); +// aele->scale.x = ts.x; +// aele->scale.y = ts.y; + aele->setScale(ts.x, ts.y, 1.f); + ofLogVerbose("ofxSvg") << __FUNCTION__ << " name: " << aele->getName() << " scale: " << ts; + + mat = glm::scale(mat, glm::vec3(aele->getScale().x, aele->getScale().y, 1.f)); +// gmat = glm::scale(gmat, glm::vec3(aele->getScale().x, aele->getScale().y, 1.f)); + } + +// glm::vec3 pos3 = mat * glm::vec4( aele->getPosition().x, aele->getPosition().y, 0.0f, 1.f ); +// pos3 = gmat * glm::vec4( aele->pos.x, aele->pos.y, 0.0f, 1.f ); +// aele->pos.x = pos3.x; +// aele->pos.y = pos3.y; +// aele->setPosition( pos3.x, pos3.y, 0.0f); + + if( ofIsStringInString(aStr, "matrix")) { + + // example transform string for matrix form. + // transform="matrix(-1,0,0,-1,358.9498,1564.4744)" + + auto matrix = aStr; + ofStringReplace(matrix, "matrix(", ""); + ofStringReplace(matrix, ")", ""); + ofStringReplace(matrix, ",", " "); + vector matrixNum = ofSplitString(matrix, " ", false, true); + vector matrixF; + for(std::size_t i = 0; i < matrixNum.size(); i++){ + matrixF.push_back(ofToFloat(matrixNum[i])); + ofLogVerbose("ofxSvg::setTransformFromSvgMatrixString") << aele->getCleanName() << " matrix[" << i << "] = " << matrixF[i] << " string version is " << matrixNum[i]; + } + + if( matrixNum.size() == 6 ) { + + mat = glm::translate(glm::mat4(1.0f), glm::vec3(matrixF[4], matrixF[5], 0.0f)); + + aele->setPosition(matrixF[4], matrixF[5], 0.f); + + float trotation = glm::degrees( atan2f(matrixF[1],matrixF[0]) ); + +// aele->scale.x = glm::sqrt(matrixF[0] * matrixF[0] + matrixF[1] * matrixF[1]); +// aele->scale.y = glm::sqrt(matrixF[2] * matrixF[2] + matrixF[3] * matrixF[3]); + float sx = glm::sqrt(matrixF[0] * matrixF[0] + matrixF[1] * matrixF[1]); + float sy = glm::sqrt(matrixF[2] * matrixF[2] + matrixF[3] * matrixF[3]); + + if (matrixF[0] < 0) sx *= -1.f; + if (matrixF[3] < 0) sy *= -1.f; + + aele->setScale(sx, sy, 1.f); + + // Avoid double-rotating when both scale = -1 and rotation = 180 + if (sx < 0 && sy < 0 && glm::abs(trotation - 180.0f) < 0.01f) { + trotation = 0.f; + } + + if( trotation != 0.f ) { +// mat = mat * glm::toMat4((const glm::quat&)glm::angleAxis(glm::radians(aele->rotation), glm::vec3(0.f, 0.f, 1.f))); + mat = glm::rotate(mat, glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f)); +// aele->setOrientation(glm::angleAxis(glm::radians(trotation), glm::vec3(0.f, 0.f, 1.f) )); + aele->setRotationDeg(trotation); + } + + mat = glm::scale(mat, glm::vec3(aele->getScale().x, aele->getScale().y, 1.f)); + + ofLogVerbose("ofxSvg::setTransformFromSvgMatrixString") << "pos: " << aele->getPosition() << " rotation: " << trotation << " scale: " << aele->getScale(); + } + } + + return mat; +} - if (invisibleElements.empty()) { - finished = true; +//-------------------------------------------------------------- +std::string ofxSvg::getSvgMatrixStringFromElement( std::shared_ptr aele ) { + std::ostringstream matrixStream; + matrixStream << std::fixed << std::setprecision(1); + bool bFirst = true; + + if( aele->getPosition().x != 0.f || aele->getPosition().y != 0.f ) { + bFirst = false; + matrixStream << "translate(" << aele->getPosition().x << "," << aele->getPosition().y << ")"; + } + + if( aele->getRotationDeg() != 0.f ) { + if(!bFirst) { + matrixStream << " "; + } + bFirst = false; + if( aele->mModelRotationPoint.x != 0.0f || aele->mModelRotationPoint.y != 0.0f ) { + matrixStream << "rotate(" << aele->getRotationDeg() << " " << aele->mModelRotationPoint.x << " " << aele->mModelRotationPoint.y <<")"; } else { - const ofXml & element = invisibleElements[0]; - ofXml parent = element.getParent(); - if (parent && element) parent.removeChild(element); + matrixStream << "rotate(" << aele->getRotationDeg() <<")"; } } + + if( aele->getScale().x != 1.f || aele->getScale().y != 1.f ) { + if(!bFirst) { + matrixStream << " "; + } + bFirst = false; + matrixStream << "scale(" << aele->getScale().x << " " << aele->getScale().y <<")"; + } + + if( matrixStream.str().size() > 3 ) { + return matrixStream.str(); + } + + return ""; + +} - // implement the SVG "use" element by expanding out those elements into - // XML that svgtiny will parse correctly. - ofXml::Search useElements = xml.find("//use"); - if (!useElements.empty()) { +//-------------------------------------------------------------- +void ofxSvg::_getTextSpanFromXmlNode( ofXml& anode, std::vector< std::shared_ptr >& aspans ) { +// if( anode.getName() != "tspan") { +// return; +// } + auto tspan = std::make_shared(); + + string tText = anode.getValue(); + float tx = 0; + auto txattr = anode.getAttribute("x"); + if( txattr) { + tx = txattr.getFloatValue(); + } + float ty = 0; + auto tyattr = anode.getAttribute("y"); + if( tyattr ) { + ty = tyattr.getFloatValue(); + } + + tspan->setText(tText); + tspan->rect.x = tx; + tspan->rect.y = ty; + + _applyStyleToText(anode, tspan); + +// ofLogNotice("ofxSvg::_getTextSpanFromXmlNode") << anode.getValue() << " anode string: " << anode.toString(); + + aspans.push_back(tspan); + + _pushCssClass(tspan->getCssClass()); + for( auto& kid : anode.getChildren() ) { + if( kid ) { + if( kid.getName() == "tspan") { +// ofLogNotice("ofxSvg::_getTextSpanFromXmlNode") << anode.getValue() << " anode string: " << anode.toString(); + _getTextSpanFromXmlNode( kid, aspans ); + } + } + } + _popCssClass(); + +} - for (ofXml & element : useElements) { +//-------------------------------------------------------------- +float ofxSvg::getWidth() const { + return mViewbox.getWidth(); +} - // get the id attribute - string id = element.getAttribute("xlink:href").getValue(); - // remove the leading "#" from the id - id.erase(id.begin()); +//-------------------------------------------------------------- +float ofxSvg::getHeight() const { + return mViewbox.getHeight(); +} - // find the original definition of that element - TODO add defs into path? - string searchstring = "//*[@id='" + id + "']"; - ofXml idelement = xml.findFirst(searchstring); +//-------------------------------------------------------------- +ofRectangle ofxSvg::getViewbox() const { + return mViewbox; +} - // if we found one then use it! (find first returns an empty xml on failure) - if (idelement.getAttribute("id").getValue() != "") { +//-------------------------------------------------------------- +float ofxSvg::getBoundsWidth() const { + return mBounds.getWidth(); +} - // make a copy of that element - element.appendChild(idelement); +//-------------------------------------------------------------- +float ofxSvg::getBoundsHeight() const { + return mBounds.getHeight(); +} - // then turn the use element into a g element - element.setName("g"); - } - } +//-------------------------------------------------------------- +ofRectangle ofxSvg::getBounds() const { + return mBounds; +} + +//-------------------------------------------------------------- +void ofxSvg::setWidth(float aw) { + mViewbox.width = aw; + if( mBounds.width < 1 ) { + mBounds.width = aw; + } +} + +//-------------------------------------------------------------- +void ofxSvg::setHeight(float ah) { + mViewbox.height = ah; + if( mBounds.height < 1 ) { + mBounds.height = ah; + } +} + +//-------------------------------------------------------------- +void ofxSvg::setViewBox( const ofRectangle& arect ) { + mViewbox = arect; +} + +//-------------------------------------------------------------- +void ofxSvg::setBoundsWidth( float aw ) { + mBounds.width = aw; +} + +//-------------------------------------------------------------- +void ofxSvg::setBoundsHeight( float ah ) { + mBounds.height = ah; +} + +//-------------------------------------------------------------- +void ofxSvg::setBounds( const ofRectangle& arect ) { + mBounds = arect; +} + +//-------------------------------------------------------------- +void ofxSvg::pushGroup( const std::string& apath ) { + std::shared_ptr cgroup; + if( mGroupStack.size() > 0 ) { + mGroupStack.back()->get( apath ); + } else { + cgroup = get( apath ); + } + + if( cgroup ) { + pushGroup(cgroup); + } else { + ofLogWarning("ofxSvg") << "could not find group with path " << apath; + } +} + +//-------------------------------------------------------------- +void ofxSvg::pushGroup( const std::shared_ptr& agroup ) { + if( agroup ) { + mGroupStack.push_back(agroup); + } +} + +//-------------------------------------------------------------- +void ofxSvg::popGroup() { + if( mGroupStack.size() > 0 ) { + mGroupStack.pop_back(); } +} + +//-------------------------------------------------------------- +void ofxSvg::setFillColor(ofColor acolor) { + mFillColor = acolor; + mDocumentCss.setFillColor(acolor); + mCurrentCss.setFillColor(acolor); +} + +//-------------------------------------------------------------- +void ofxSvg::setFilled(bool abFilled) { + if( abFilled ) { + mDocumentCss.setFillColor(mFillColor); + mCurrentCss.setFillColor(mFillColor); + } else { + mDocumentCss.setNoFill(); + mCurrentCss.setNoFill(); + } +} - xmlstring = xml.toString(); +//-------------------------------------------------------------- +void ofxSvg::setStrokeColor(ofColor acolor) { + mStrokeColor = acolor; + mCurrentCss.setStrokeColor(acolor); + mDocumentCss.setStrokeColor(acolor); } -void ofxSvg::draw() { - for (int i = 0; i < (int)paths.size(); i++) { - paths[i].draw(); +//-------------------------------------------------------------- +void ofxSvg::setStrokeWidth(float aLineWidth) { + mCurrentCss.setStrokeWidth(aLineWidth); + mDocumentCss.setStrokeWidth(aLineWidth); +} + +//-------------------------------------------------------------- +void ofxSvg::setHasStroke(bool abStroke) { + if( abStroke ) { + mCurrentCss.setStrokeColor(mStrokeColor); + mDocumentCss.setStrokeColor(mStrokeColor); + } else { + mCurrentCss.setNoStroke(); + mDocumentCss.setNoStroke(); } } -void ofxSvg::setupDiagram(struct svgtiny_diagram * diagram) { +//-------------------------------------------------------------- +void ofxSvg::setDefaultClosedPathWindingMode( ofPolyWindingMode aWindingMode ) { + mDefaultClosedPathWindingMode = aWindingMode; +} - width = diagram->width; - height = diagram->height; +//-------------------------------------------------------------- +ofPolyWindingMode ofxSvg::getDefaultClosedPathWindingMode() { + return mDefaultClosedPathWindingMode; +} - paths.clear(); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addGroup(std::string aname) { + auto tgroup = std::make_shared(); + tgroup->name = aname; + _getPushedGroup()->add(tgroup); + recalculateLayers(); + return tgroup; +} - for (int i = 0; i < (int)diagram->shape_count; i++) { - if (diagram->shape[i].path) { - paths.push_back(ofPath()); - setupShape(&diagram->shape[i], paths.back()); - } else if (diagram->shape[i].text) { - ofLogWarning("ofxSVG") << "setupDiagram(): text: not implemented yet"; +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofPath& apath ) { + auto path = std::make_shared(); + path->path = apath; + path->applyStyle(mCurrentCss); + _getPushedGroup()->add(path); + recalculateLayers(); + mPaths.clear(); + return path; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvg::add( const std::vector& apaths ) { + std::vector< std::shared_ptr > rpaths; + for( auto& path : apaths ) { + rpaths.push_back( add(path) ); + } + return rpaths; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofPolyline& apoly ) { + if( apoly.size() < 2 ) { + ofLogWarning("ofxSvg::add") << "polyline has less than 2 vertices."; + } + + ofPath opath; + const auto& verts = apoly.getVertices(); + for( std::size_t i = 0; i < verts.size(); i++ ) { + if( i == 0 ) { + opath.moveTo(verts[i]); + } else { + opath.lineTo(verts[i]); } } + if( apoly.isClosed() ) { + opath.close(); + } + return add( opath ); } -void ofxSvg::setupShape(struct svgtiny_shape * shape, ofPath & path) { - float * p = shape->path; +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvg::add( const std::vector& apolys ) { + std::vector< std::shared_ptr > rpaths; + for( auto& poly : apolys ) { + rpaths.push_back( add(poly) ); + } + return rpaths; +} - path.setFilled(false); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofRectangle& arect ) { + return add( arect, 0.0f); +} - if (shape->fill != svgtiny_TRANSPARENT) { - path.setFilled(true); - path.setFillHexColor(shape->fill); - path.setPolyWindingMode(OF_POLY_WINDING_NONZERO); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofRectangle& arect, float aRoundRadius ) { + auto rect = std::make_shared(); + rect->setPosition(arect.x, arect.y, 0.0f); + rect->width = arect.getWidth(); + rect->height = arect.getHeight(); + rect->roundRadius = -1; // force setting round + rect->setRoundRadius(std::max(0.f,aRoundRadius)); + rect->applyStyle(mCurrentCss); + _getPushedGroup()->add(rect); + recalculateLayers(); + mPaths.clear(); + return rect; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( float aradius ) { + return addCircle(glm::vec2(0.f, 0.f), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const glm::vec2& apos, float aradius ) { + auto circle = std::make_shared(); + circle->setPosition( apos.x, apos.y, 0.f); + circle->path.setCircleResolution(mCircleResolution); + circle->radius = -1.f; + circle->setRadius(std::max(0.f,aradius)); + circle->setPosition( apos.x, apos.y, 0.0f); + circle->applyStyle(mCurrentCss); + _getPushedGroup()->add(circle); + recalculateLayers(); + mPaths.clear(); + return circle; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const glm::vec3& apos, float aradius ) { + return addCircle( glm::vec2(apos.x, apos.y), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const float& ax, const float& ay, float aradius ) { + return addCircle( glm::vec2(ax, ay), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(0.f, 0.f), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const glm::vec2& apos, float aradiusX, float aradiusY ) { + auto ellipse = std::make_shared(); + ellipse->setPosition(apos.x, apos.y, 0.f); + + ellipse->radiusX = aradiusX; + ellipse->radiusY = aradiusY; + ellipse->path.setCircleResolution(mCircleResolution); + ellipse->path.ellipse(apos, aradiusX, aradiusY); + + ellipse->applyStyle(mCurrentCss); + _getPushedGroup()->add(ellipse); + recalculateLayers(); + mPaths.clear(); + return ellipse; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const glm::vec3& apos, float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(apos.x, apos.y), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const float& ax, const float& ay, float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(ax, ay), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const of::filesystem::path& apath, const ofTexture& atex ) { + return addImage(glm::vec2(0.f, 0.f), apath, atex.getWidth(), atex.getHeight() ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const glm::vec2& apos, const of::filesystem::path& apath, const ofTexture& atex ) { + return addImage( apos, apath, atex.getWidth(), atex.getHeight() ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const of::filesystem::path& apath, const float& awidth, const float& aheight ) { + return addImage(glm::vec2(0.f, 0.f), apath, awidth, aheight ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const glm::vec2& apos, const of::filesystem::path& apath, const float& awidth, const float& aheight ) { + auto img = std::make_shared(); + img->filepath = apath; + img->width = awidth; + img->height = aheight; + img->setPosition(apos.x, apos.y, 0.0f); + _getPushedGroup()->add(img); + recalculateLayers(); + return img; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEmbeddedImage(const glm::vec2& apos, const ofPixels& apixels ) { + auto img = std::make_shared(); + img->img.setFromPixels(apixels); + img->width = apixels.getWidth(); + img->height = apixels.getHeight(); +// _applyModelMatrixToElement( img, glm::vec2(0.f, 0.f) ); + img->setPosition(apos.x, apos.y, 0.0f); + _getPushedGroup()->add(img); + recalculateLayers(); + return img; +} + +//-------------------------------------------------------------- +bool ofxSvg::remove( std::shared_ptr aelement ) { + bool bRemoved = ofxSvgGroup::remove(aelement); + if( bRemoved ) { + recalculateLayers(); } + return bRemoved; +} + +////-------------------------------------------------------------- +//bool ofxSvg::remove( std::vector > aelements ) { +// bool bAllRemoved = ofxSvgGroup::remove(aelements); +// // we should just recalculate the layers if there was more than one element +// // since the function only returns true if all of the elements were found and removed. +// if( aelements.size() > 0 ) { +// recalculateLayers(); +// } +// return bAllRemoved; +//} - if (shape->stroke != svgtiny_TRANSPARENT) { - path.setStrokeWidth(shape->stroke_width); - path.setStrokeHexColor(shape->stroke); +//-------------------------------------------------------------- +void ofxSvg::drawDebug() { +// Group::draw(); + ofSetColor( ofColor::limeGreen ); + ofNoFill(); + +// int cindex = 0; +// for( auto& cp : mCPoints ) { +// ofSetColor( (float)(cindex % 2) * 255, 200, 60 ); +//// ofDrawCircle( cp, (cindex+1) * 1.0f ); +// ofDrawCircle( cp, 3. ); +// cindex ++; +// } +// ofFill(); + + for( std::size_t k = 0; k < mCPoints.size(); k += 3 ) { + ofSetColor( ofColor::orange ); + ofDrawCircle( mCPoints[k+0], 6.f ); + ofSetColor( ofColor::white ); + ofDrawCircle( mCPoints[k+1], 3.f ); + ofDrawCircle( mCPoints[k+2], 3.f ); + ofDrawLine( mCPoints[k+0], mCPoints[k+1] ); + ofDrawLine( mCPoints[k+0], mCPoints[k+2] ); } + + ofFill(); + + + int tcounter = 0; + for( auto& cp : mCenterPoints ) { + if(tcounter == 0) { + ofSetColor( ofColor::cyan ); + } else { + ofSetColor( ofColor::orange ); + } + ofDrawCircle(cp, 4.f); + tcounter++; + } + +} - for (int i = 0; i < (int)shape->path_length;) { - if (p[i] == svgtiny_PATH_MOVE) { - path.moveTo(p[i + 1], p[i + 2]); - i += 3; - } else if (p[i] == svgtiny_PATH_CLOSE) { - path.close(); +//-------------------------------------------------------------- +ofxSvgGroup* ofxSvg::_getPushedGroup() { + if( mGroupStack.size() > 0 ) { + return mGroupStack.back().get(); + } + return this; +} - i += 1; - } else if (p[i] == svgtiny_PATH_LINE) { - path.lineTo(p[i + 1], p[i + 2]); - i += 3; - } else if (p[i] == svgtiny_PATH_BEZIER) { - path.bezierTo(p[i + 1], p[i + 2], - p[i + 3], p[i + 4], - p[i + 5], p[i + 6]); - i += 7; +//-------------------------------------------------------------- +ofxSvgCssClass& ofxSvg::_addCssClassFromPath( std::shared_ptr aSvgPath ) { + ofxSvgCssClass tcss; +// tcss.name = aSvgPath->name+"st"; + tcss.name = "st"; + if( aSvgPath->path.isFilled() ) { + tcss.setFillColor(aSvgPath->path.getFillColor()); + } else { + tcss.setNoFill(); + } + + if( aSvgPath->path.hasOutline() ) { + tcss.setStrokeColor(aSvgPath->path.getStrokeColor()); + tcss.setStrokeWidth(aSvgPath->path.getStrokeWidth()); + } else { + tcss.setNoStroke(); + } + if( !aSvgPath->isVisible() ) { + tcss.addProperty("display", "none" ); + } + + return mSvgCss.getAddClass(tcss); +} + +//-------------------------------------------------------------- +void ofxSvg::_addCssClassFromPath( std::shared_ptr aSvgPath, ofXml& anode ) { + auto& css = _addCssClassFromPath(aSvgPath); + if( auto xattr = anode.appendAttribute("class") ) { + xattr.set(css.name); + } +} + +//-------------------------------------------------------------- +void ofxSvg::_addCssClassFromImage( std::shared_ptr aSvgImage, ofXml& anode ) { + + if( !aSvgImage->isVisible() ) { + ofxSvgCssClass tcss; + tcss.name = "st"; + tcss.addProperty("display", "none" ); + + auto& addedClass = mSvgCss.getAddClass(tcss); + + if( auto xattr = anode.appendAttribute("class") ) { + xattr.set(addedClass.name); + } + } +} + +//-------------------------------------------------------------- +void ofxSvg::_addCssClassFromTextSpan( std::shared_ptr aSvgTextSpan, ofXml& anode ) { + + auto textCss = aSvgTextSpan->getCssClass(); + + if( textCss.name.empty() ) { + textCss.name = "ts"; + } + + auto& tcss = mSvgCss.getAddClass(textCss); + + if( auto xattr = anode.appendAttribute("class") ) { + xattr.set(tcss.name); + } + +// if( !aSvgTextSpan->isVisible() ) { +// ofxSvgCssClass tcss; +// tcss.name = "st"; +// tcss.addProperty("display", "none" ); +// +// auto& addedClass = mSvgCss.getAddClass(tcss); +// +// if( auto xattr = anode.appendAttribute("class") ) { +// xattr.set(addedClass.name); +// } +// } +} + +//-------------------------------------------------------------- +bool ofxSvg::_toXml( ofXml& aParentNode, std::shared_ptr aele ) { + ofXml txml = aParentNode.appendChild( ofxSvgElement::sGetSvgXmlName(aele->getType())); + if( !aele->getName().empty() ) { + if( auto iattr = txml.appendAttribute("id")) { + iattr.set(aele->getName()); + } + } + if( aele->getType() == OFXSVG_TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast(aele); + if( tgroup ) { + if( tgroup->getNumChildren() > 0 ) { + for( auto& kid : tgroup->getChildren() ) { + _toXml( txml, kid ); + } + } + } + } else if( aele->getType() == OFXSVG_TYPE_RECTANGLE ) { + auto trect = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( trect, txml ); + + if( auto xattr = txml.appendAttribute("x")) { +// xattr.set(trect->getPosition().x); + xattr.set(trect->getOffsetPathPosition().x); + } + if( auto xattr = txml.appendAttribute("y")) { +// xattr.set(trect->getPosition().y); + xattr.set(trect->getOffsetPathPosition().y); + } + if( auto xattr = txml.appendAttribute("width")) { + xattr.set(trect->getWidth()); + } + if( auto xattr = txml.appendAttribute("height")) { + xattr.set(trect->getHeight()); + } + if( trect->getRoundRadius() > 0.0f ) { + if( auto xattr = txml.appendAttribute("rx")) { + xattr.set(trect->getRoundRadius()); + } + if( auto xattr = txml.appendAttribute("ry")) { + xattr.set(trect->getRoundRadius()); + } + } + + } else if( aele->getType() == OFXSVG_TYPE_IMAGE ) { + auto timage = std::dynamic_pointer_cast(aele); + + _addCssClassFromImage( timage, txml ); + + if( auto xattr = txml.appendAttribute("width")) { + xattr.set(timage->width); + } + if( auto xattr = txml.appendAttribute("height")) { + xattr.set(timage->height); + } + if( !timage->getFilePath().empty() ) { + if( auto xattr = txml.appendAttribute("xlink:href")) { + xattr.set(timage->getFilePath().string()); + } } else { - ofLogError("ofxSVG") << "setupShape(): SVG parse error"; - i += 1; + // check for embedded image // + if( timage->img.getPixels().isAllocated() ) { + // embed the pixels // + if( auto xattr = txml.appendAttribute("xlink:href")) { + auto base64String = ofxSvgUtils::base64_encode( timage->img.getPixels() ); + std::string encString = "data:image/png;base64,"+base64String; + xattr.set(encString); + } + } + } + + + } else if( aele->getType() == OFXSVG_TYPE_ELLIPSE ) { + auto tellipse = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( tellipse, txml ); + + if( auto xattr = txml.appendAttribute("cx")) { + xattr.set(tellipse->getOffsetPathPosition().x); + } + if( auto xattr = txml.appendAttribute("cy")) { + xattr.set(tellipse->getOffsetPathPosition().y); + } + if( auto xattr = txml.appendAttribute("rx")) { + xattr.set(tellipse->radiusX); + } + if( auto xattr = txml.appendAttribute("ry")) { + xattr.set(tellipse->radiusY); + } + + } else if( aele->getType() == OFXSVG_TYPE_CIRCLE ) { + auto tcircle = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( tcircle, txml ); + + if( auto xattr = txml.appendAttribute("cx")) { + xattr.set(tcircle->getOffsetPathPosition().x); + } + if( auto xattr = txml.appendAttribute("cy")) { + xattr.set(tcircle->getOffsetPathPosition().y); + } + if( auto xattr = txml.appendAttribute("r")) { + xattr.set(tcircle->getRadius()); + } + + } else if( aele->getType() == OFXSVG_TYPE_PATH ) { + auto tpath = std::dynamic_pointer_cast(aele); + + _addCssClassFromPath( tpath, txml ); + + std::stringstream vstr; + + if( tpath->path.getMode() == ofPath::Mode::POLYLINES ) { + + auto outlines = tpath->path.getOutline(); + for( auto& polyline : outlines ) { + const auto& pverts = polyline.getVertices(); + if( pverts.size() > 1 ) { + for( std::size_t i = 0; i < pverts.size(); i++ ) { + if( i == 0 ) { + vstr << "M"<path.getCommands(); + if( commands.size() > 1 ) { +// std::stringstream vstr; + for( auto& command : commands ) { + if( command.type == ofPath::Command::moveTo ) { + vstr << "M" << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::lineTo ) { + vstr << "L" << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::curveTo ) { + // hmm, not sure how to handle this at the moment + } else if( command.type == ofPath::Command::bezierTo ) { + vstr << "C" << command.cp1.x << "," << command.cp1.y << " " << command.cp2.x << "," << command.cp2.y << " " << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::quadBezierTo ) { + vstr << "Q" << command.cp2.x << "," << command.cp2.y << " " << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::arc ) { + // TODO: Not so sure about these + glm::vec2 ept = glm::vec2(command.to.x + cosf( command.radiusX ), command.to.y + sinf(command.radiusY )); + vstr << "A" << command.radiusX << "," << command.radiusY << " 0 " << "0,1 " << ept.x << "," <getType() == OFXSVG_TYPE_TEXT ) { + auto ttext = std::dynamic_pointer_cast(aele); + for( auto tspan : ttext->textSpans ) { + if( auto spanXml = txml.appendChild("tspan")) { + if( auto xattr = spanXml.appendAttribute("x")) { + xattr.set(tspan->rect.x); + } + if( auto yattr = spanXml.appendAttribute("y")) { + yattr.set(tspan->rect.y); + } + spanXml.set(tspan->getText()); + _addCssClassFromTextSpan( tspan, spanXml ); + } + } + } + + // figure out if we need a transform attribute + auto matrixString = getSvgMatrixStringFromElement(aele); + if( !matrixString.empty() ) { + if( auto xattr = txml.appendAttribute("transform")) { + xattr.set(matrixString); } } + return txml; } -const std::vector & ofxSvg::getPaths() const { - return paths; +//-------------------------------------------------------------- +void ofxSvg::_pushCssClass( const ofxSvgCssClass& acss ) { + mCssClassStack.push_back(acss); + _buildCurrentSvgCssFromStack(); +} + +//-------------------------------------------------------------- +void ofxSvg::_popCssClass() { + if( mCssClassStack.size() > 0 ) { + mCssClassStack.pop_back(); + _buildCurrentSvgCssFromStack(); + } } + +//-------------------------------------------------------------- +void ofxSvg::_buildCurrentSvgCssFromStack() { + // maybe not efficient, but should account for removing / adding + mCurrentCss.clear(); + mCurrentCss.setClassProperties(mDocumentCss); + for( auto& css : mCssClassStack ) { + mCurrentCss.setClassProperties(css); + } +} + + diff --git a/addons/ofxSvg/src/ofxSvg.h b/addons/ofxSvg/src/ofxSvg.h old mode 100644 new mode 100755 index c2c678deb45..c58438bc3b9 --- a/addons/ofxSvg/src/ofxSvg.h +++ b/addons/ofxSvg/src/ofxSvg.h @@ -1,61 +1,382 @@ #pragma once - -//#include "ofMain.h" -#include "ofPath.h" -#include "ofTypes.h" +#include "ofxSvgGroup.h" #include "ofXml.h" +#include "ofxSvgCss.h" +#include /// \file -/// ofxSVG is used for loading and rendering SVG files. It's a wrapper -/// for the open source C library [Libsvgtiny](https://www.netsurf-browser.org/projects/libsvgtiny/ "Libsvgtiny website"), -/// and it supports files in the [SVG Tiny format](https://www.w3.org/TR/SVGMobile/ "SVG Tiny 1.2 -/// format specification at the W3C"). -/// -/// Libsvgtiny supports a subset of SVG elements, (for a full list, see the Libsvgtiny readme file) -/// but we have gone some way to improving this by manually implementing some extra features (such as the -/// SVG "use" element). - -class ofxSvg { -public: +/// ofxSvg is used for loading, manipulating, rendering and saving of SVG files. +/// Based on this spec: https://www.w3.org/TR/SVG/Overview.html +class ofxSvg : public ofxSvgGroup { +protected: + std::vector> deepCopyVector(const std::vector>& original); + void deepCopyFrom( const ofxSvg & mom ); + void moveFrom( ofxSvg&& mom ); + +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_DOCUMENT;} + + // Default constructor ofxSvg() = default; + // Copy constructor (deep copy) + ofxSvg(const ofxSvg & mom); + // Copy assignment operator (deep copy) + ofxSvg& operator=(const ofxSvg& mom); + // Move constructor + ofxSvg(ofxSvg&& mom); + // Move assignment operator + ofxSvg& operator=(ofxSvg&& mom); + ~ofxSvg() = default; - ofxSvg(const ofxSvg & a) = default; - + ofxSvg(const of::filesystem::path & fileName); + /// \brief Loads an SVG file from the provided filename. + /// \return true if the load was successful. + bool load( const of::filesystem::path & fileName ); + /// \brief provided for legacy support. + /// \return true if the load was successful. + bool loadFromString( const std::string& data, std::string url = "local"); + /// \brief Reload from filepath saved from a previouly called load(). + /// \return true if the reload was successful. + bool reload(); + /// \brief Save the svg to the path. + /// Use the add functions to provide data to be saved or load data using the load() function. + /// \return true if the save was successful. + bool save( of::filesystem::path apath ); + /// \brief Remove all of the data from the document. + void clear(); + /// \brief Set the directory to search for fonts if using text elements. + /// Needs to be set before calling load(); + /// \param aDir string representation of the directory. + void setFontsDirectory( std::string aDir ); + + /// \brief A string of the element hierarchy. + /// Helpful for visualizing structure. + std::string toString(int nlevel = 0) override; + /// \brief Get the units, ie. px, cm, in, etc. + /// \return string description of the units used. + std::string getUnitString() { return mUnitStr; } + /// \brief Set the units using a string, ie. px, cm, in, etc. + void setUnitString(std::string astring ) { mUnitStr = astring; } + /// \brief The total layers in the svg, should also be the number of groups + elements. + /// \return int that is the total layers. + const int getTotalLayers(); + /// \brief If an element has been added or removed, the function should be called internally. + /// The function updates the 'layer' property on each element. + /// If the layers appear to be incorrect, call this function. + void recalculateLayers(); + /// \brief Provided for legacy support. + /// Includes circles, ellipses, rectangles and paths as ofPaths. + /// \return int as the number of paths in the document. + int getNumPath(); + /// \brief Provided for legacy support. Use getNumPath() to acquire the total number of paths detected. + /// \param n the index of the ofPath to return. + /// \return An ofPath using the provided index. + ofPath & getPathAt(int n); + /// \brief Provided for legacy support. + /// Includes circles, ellipses, rectangles and paths as ofPaths. + /// \return A vector of ofPaths in the entire document. + const std::vector & getPaths() const; + /// \brief Parse the svg transform string into global position, scale and rotation. + /// The elements pos, scale and rotation is set. + /// \param aStr svg transform string. + /// \param aele ofx::svg::Element to be updated. + glm::mat4 setTransformFromSvgMatrixString( std::string aStr, std::shared_ptr aele ); + /// \brief Return a string used to represent matrix transforms in svg + /// The matrix can be represented as an array of values like : + /// Or using individual components like tranform(translateX translateY), scale(scaleX scaleY) and rotate(degrees ptx pty ) + /// Skew is currently not supported. + /// Reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform + /// \param aele the element to extract the svg matrix from using its pos, scale and rotation properties. + /// \return string that represents the svg matrix for using in a svg file. + std::string getSvgMatrixStringFromElement( std::shared_ptr aele ); + /// \brief The viewbox width. + /// \return float width of the viewbox. float getWidth() const; + /// \brief The viewbox height. + /// \return float height of the viewbox. float getHeight() const; + /// \brief The viewbox rectangle. The viewbox attribute on the svg node. + /// \return ofRectangle with the dimenstions of the viewbox. + ofRectangle getViewbox() const; + /// \brief The bounds width. + /// \return float width of the bounds. + float getBoundsWidth() const; + /// \brief The bounds height. + /// \return float height of the bounds. + float getBoundsHeight() const; + /// \brief The bounds rectangle. The width and height attribute on the svg node. + /// \return ofRectangle with the dimenstions of the bounds. + ofRectangle getBounds() const; + + /// \brief Set the width of the viewbox. + /// If the bounds width is less than 1, set the bounds to the same width. + /// \param aw float width. + void setWidth( float aw ); + /// \brief Set the height of the viewbox. + /// If the bounds height is less than 1, set the bounds to the same height. + /// \param ah float height. + void setHeight( float ah ); + /// \brief Set the dimensions of the viewbox. + /// \param arect an ofRectangle with the dimensions of the viewbox. + void setViewBox( const ofRectangle& arect ); + /// \brief Set the width of the bounds. + /// \param aw float width. + void setBoundsWidth( float aw ); + /// \brief Set the height of the viewbox. + /// \param ah float height. + void setBoundsHeight( float ah ); + /// \brief Set the dimensions of the bounds. + /// \param arect an ofRectangle with the dimensions of the bounds. + void setBounds( const ofRectangle& arect ); + + /// \brief Push a svg group to be active for adding elements to. + /// \param aname The path to the svg group in the document, if a group is already pushed, search in that group. + void pushGroup( const std::string& aname ); + /// \brief Push a svg group to be active for adding elements to. + /// \param aname The group to push and make active. + void pushGroup( const std::shared_ptr& agroup ); + /// \brief Remove the most recent pushed group if any. + void popGroup(); + /// \brief Set the current fill color. Any subsequent items using a fill color will adopt this color. + /// \param acolor is the color to set. + void setFillColor(ofColor acolor); + /// \brief Set if items should be filled or not. Any subsequent added items will use this value. + /// \param abFilled should the items be filled or not. + void setFilled(bool abFilled); + /// \brief Set the current stroke color. Any subsequent items using a stroke color will adopt this color. + /// \param acolor is the color to set. + void setStrokeColor(ofColor acolor); + /// \brief Set the current stroke width. Any subsequent items using a stroke width will adopt this value. + /// \param aLineWidth is the width of the lines. + void setStrokeWidth(float aLineWidth); + /// \brief Set if items should have a stroke or not. Any subsequent items using a stroke will adopt this value. + /// \param abStroke activates or deactivates strokes. + void setHasStroke(bool abStroke); + /// \brief Set the circle resolution for rendering. + /// Set this value before calling load. + /// \param ac (int) the resolution to use. + void setCircleResolution( int ac ) { mCircleResolution = ac; } + /// \brief Set the curve resolution for rendering. + /// Set this value before calling load. + /// \param ac (int) the resolution to use. + void setCurveResolution( int ac ) { mCurveResolution = ac; } + /// \brief Get the circle resolution for rendering. + /// \return int of the circle resolution. + int getCircleResolution() { return mCircleResolution; } + /// \brief Get the curve resolution for rendering. + /// \return int of the circle resolution. + int getCurveResolution() { return mCurveResolution; }; + /// \brief Get the current css used for items. + /// \return ofxSvgCssClass. + ofxSvgCssClass& getCurrentCss() { return mCurrentCss;} + /// \brief Set the default winding mode of imported paths with all sub paths that are closed. The default is OF_POLY_WINDING_NONZERO. + /// If not all of the sub paths are closed, the winding mode is not set. + /// Must be called before load() to apply to newly imported paths. + /// \param ofPolyWindingMode to be used as default. + void setDefaultClosedPathWindingMode( ofPolyWindingMode aWindingMode ); + /// \brief Get the default winding mode of imported paths. + /// \return ofPolyWindingMode. + ofPolyWindingMode getDefaultClosedPathWindingMode(); + + ofxSvgCssStyleSheet& getCssStyleSheet() {return mSvgCss; } + /// \brief Add a ofxSvgGroup to the document. This will also push back the group as current. + /// \param aname is the name of the group. + /// \return std::shared_ptr as the group that was created. + std::shared_ptr addGroup(std::string aname); + + /// \brief Add a ofxSvgPath to the document. The ofxSvgPath will be added to the current group. + /// \param ofPath apath is the path to be added. + /// \return std::shared_ptr as the path that was created and added to the document. + std::shared_ptr add( const ofPath& apath ); + /// \brief Add multiple ofxSvgPaths to the document. The ofxSvgPaths will be added to the current group. + /// \param std::vector apaths are the paths to be added. + /// \return std::vector< std::shared_ptr > the paths that were created and added to the document. + std::vector< std::shared_ptr > add( const std::vector& apaths ); + /// \brief Add a ofxSvgPath to the document. The ofxSvgPath will be added to the current group. + /// \param ofPolyline apoly is the polyline that will create an ofxSvgPath and added. + /// \return std::shared_ptr as the path that was created and added to the document. + std::shared_ptr add( const ofPolyline& apoly ); + /// \brief Add multiple ofxSvgPaths to the document. The ofxSvgPaths will be added to the current group. + /// \param std::vector apolys are the polylines used to create and add ofxSvgPaths to the document. + /// \return std::vector< std::shared_ptr > the paths that were created and added to the document. + std::vector< std::shared_ptr > add( const std::vector& apolys ); + + /// \brief Add a ofxSvgRectangle to the document. The ofxSvgRectangle will be added to the current group. + /// \param ofRectangle arect is the rectangle that will create an ofxSvgRectangle. + /// \return std::shared_ptr as the rectangle that was created and added to the document. + std::shared_ptr add( const ofRectangle& arect ); + /// \brief Add a ofxSvgRectangle to the document. The ofxSvgRectangle will be added to the current group. + /// \param ofRectangle arect is the rectangle that will create an ofxSvgRectangle. + /// \param float aRoundRadius is the corner radius used to round the corners. + /// \return std::shared_ptr as the rectangle that was created and added to the document. + std::shared_ptr add( const ofRectangle& arect, float aRoundRadius ); + + /// \brief Add a ofxSvgCircle to the document. The ofxSvgCircle will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgCircle to set properties directly. + /// \param float aradius is the radius used to create an ofxSvgCircle. + /// \return std::shared_ptr as the circle that was created and added to the document. + std::shared_ptr addCircle( float aradius ); + /// \brief Add a ofxSvgCircle to the document. The ofxSvgCircle will be added to the current group. + /// \param glm::vec2 apos is the center position of the circle. + /// \param float aradius is the radius used to create an ofxSvgCircle. + /// \return std::shared_ptr as the circle that was created and added to the document. + std::shared_ptr addCircle( const glm::vec2& apos, float aradius ); + /// \brief Add a ofxSvgCircle to the document. The ofxSvgCircle will be added to the current group. + /// \param glm::vec3 apos is the center position of the circle. Note: z is not used. + /// \param float aradius is the radius used to create an ofxSvgCircle. + /// \return std::shared_ptr as the circle that was created and added to the document. + std::shared_ptr addCircle( const glm::vec3& apos, float aradius ); + /// \brief Add a ofxSvgCircle to the document. The ofxSvgCircle will be added to the current group. + /// \param float ax is the x center position of the circle. + /// \param float ay is the y center position of the circle. + /// \param float aradius is the radius used to create an ofxSvgCircle. + /// \return std::shared_ptr as the circle that was created and added to the document. + std::shared_ptr addCircle( const float& ax, const float& ay, float aradius ); + + /// \brief Add a ofxSvgEllipse to the document. The ofxSvgEllipse will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgEllipse to set properties directly. + /// \param float aradiusX is the horizontal radius used to create an ofxSvgEllipse. + /// \param float aradiusY is the vertical radius used to create an ofxSvgEllipse. + /// \return std::shared_ptr as the ellipse that was created and added to the document. + std::shared_ptr addEllipse( float aradiusX, float aradiusY ); + /// \brief Add a ofxSvgEllipse to the document. The ofxSvgEllipse will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgEllipse to set properties directly. + /// \param glm::vec2 apos is the center position of the ellipse. + /// \param float aradiusX is the horizontal radius used to create an ofxSvgEllipse. + /// \param float aradiusY is the vertical radius used to create an ofxSvgEllipse. + /// \return std::shared_ptr as the ellipse that was created and added to the document. + std::shared_ptr addEllipse( const glm::vec2& apos, float aradiusX, float aradiusY ); + /// \brief Add a ofxSvgEllipse to the document. The ofxSvgEllipse will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgEllipse to set properties directly. + /// \param glm::vec3 apos is the center position of the ellipse. Note: z is not used. + /// \param float aradiusX is the horizontal radius used to create an ofxSvgEllipse. + /// \param float aradiusY is the vertical radius used to create an ofxSvgEllipse. + /// \return std::shared_ptr as the ellipse that was created and added to the document. + std::shared_ptr addEllipse( const glm::vec3& apos, float aradiusX, float aradiusY ); + /// \brief Add a ofxSvgEllipse to the document. The ofxSvgEllipse will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgEllipse to set properties directly. + /// \param float ax is the x center position of the ellipse. + /// \param float ay is the y center position of the ellipse. + /// \param float aradiusX is the horizontal radius used to create an ofxSvgEllipse. + /// \param float aradiusY is the vertical radius used to create an ofxSvgEllipse. + /// \return std::shared_ptr as the ellipse that was created and added to the document. + std::shared_ptr addEllipse( const float& ax, const float& ay, float aradiusX, float aradiusY ); + + /// \brief Add a ofxSvgImage to the document. The ofxSvgImage will be added to the current group. + /// The default position is 0,0. Use the returned ofxSvgImage to set properties directly. + /// \param of::filesystem::path apath is the path to the linked image file. + /// \param ofTexture atex provides the width and height of the image. + /// \return std::shared_ptr as the image that was created and added to the document. + std::shared_ptr addImage( const of::filesystem::path& apath, const ofTexture& atex ); + /// \brief Add a ofxSvgImage to the document. The ofxSvgImage will be added to the current group. + /// \param glm::vec2 apos is the position; anchored to the top left. + /// \param of::filesystem::path apath is the path to the linked image file. + /// \param ofTexture atex provides the width and height of the image. + /// \return std::shared_ptr as the image that was created and added to the document. + std::shared_ptr addImage( const glm::vec2& apos, const of::filesystem::path& apath, const ofTexture& atex ); + /// \brief Add a ofxSvgImage to the document. The ofxSvgImage will be added to the current group. + /// \param of::filesystem::path apath is the path to the linked image file. + /// \param float awidth provides the width of the image. + /// \param float aheight provides the height of the image. + /// \return std::shared_ptr as the image that was created and added to the document. + std::shared_ptr addImage( const of::filesystem::path& apath, const float& awidth, const float& aheight ); + /// \brief Add a ofxSvgImage to the document. The ofxSvgImage will be added to the current group. + /// \param glm::vec2 apos is the position; anchored to the top left. + /// \param of::filesystem::path apath is the path to the linked image file. + /// \param float awidth provides the width of the image. + /// \param float aheight provides the height of the image. + /// \return std::shared_ptr as the image that was created and added to the document. + std::shared_ptr addImage( const glm::vec2& apos, const of::filesystem::path& apath, const float& awidth, const float& aheight ); + /// \brief Add a ofxSvgImage to the document. The ofxSvgImage will be added to the current group. + /// \param glm::vec2 apos is the position; anchored to the top left. + /// \param ofPixels apixels is the pixels to embed in the svg document. Note: This can increase file size dramatically. + /// \return std::shared_ptr as the image that was created and added to the document. + std::shared_ptr addEmbeddedImage( const glm::vec2& apos, const ofPixels& apixels ); + + /// \brief Remove an element from this document or child groups. + /// \param shared_ptr aelement to be removed. + /// \return bool true if element was found and removed. + bool remove( std::shared_ptr aelement ) override; + + /// \brief Used for development to provide insight into anchor point / control point placements. + virtual void drawDebug(); + +protected: + std::string fontsDirectory = ""; + of::filesystem::path folderPath, svgPath; + ofRectangle mViewbox; + ofRectangle mBounds; + void validateXmlSvgRoot( ofXml& aRootSvgNode ); + std::string cleanString( std::string aStr, std::string aReplace ); + void _parseXmlNode( ofXml& aParentNode, std::vector< std::shared_ptr >& aElements ); + std::shared_ptr _addElementFromXmlNode( ofXml& tnode, std::vector< std::shared_ptr >& aElements ); + + void _parsePolylinePolygon( ofXml& tnode, std::shared_ptr aSvgPath ); + // reference: https://www.w3.org/TR/SVG/paths.html + void _parsePath( ofXml& tnode, std::shared_ptr aSvgPath ); + + ofxSvgCssClass _parseStyle( ofXml& tnode ); + void _applyStyleToElement( ofXml& tnode, std::shared_ptr aEle ); + void _applyStyleToPath( ofXml& tnode, std::shared_ptr aSvgPath ); + void _applyStyleToText( ofXml& tnode, std::shared_ptr aTextSpan ); + + glm::vec3 _parseMatrixString(const std::string& input, const std::string& aprefix, bool abDefaultZero ); + + void _getTextSpanFromXmlNode( ofXml& anode, std::vector< std::shared_ptr >& aspans ); + + void _setNodeParentGroupStack( std::shared_ptr aele ); + + ofxSvgGroup* _getPushedGroup(); + + ofxSvgCssClass& _addCssClassFromPath( std::shared_ptr aSvgPath ); + void _addCssClassFromPath( std::shared_ptr aSvgPath, ofXml& anode ); + void _addCssClassFromImage( std::shared_ptr aSvgImage, ofXml& anode ); + void _addCssClassFromTextSpan( std::shared_ptr aSvgTextSpan, ofXml& anode ); + bool _toXml( ofXml& aParentNode, std::shared_ptr aele ); + + void _pushCssClass( const ofxSvgCssClass& acss ); + void _popCssClass(); + void _buildCurrentSvgCssFromStack(); + + unsigned int mCurrentLayer = 0; + + std::string mUnitStr = "px"; + + ofxSvgCssStyleSheet mSvgCss; + ofxSvgCssClass mCurrentCss, mDocumentCss; + std::vector mCssClassStack; + + + ofColor mFillColor, mStrokeColor; + + std::vector< std::shared_ptr > mGroupStack; + std::vector< std::shared_ptr > mDefElements; + + // just used for debugging + std::vector mCPoints; + std::vector mCenterPoints; + + glm::mat4 mModelMatrix = glm::mat4(1.f); + std::stack mModelMatrixStack; + + int mCircleResolution = 64; + int mCurveResolution = 24; + + // for legacy purposes // + static ofPath sDummyPath; + mutable std::vector mPaths; + + ofPolyWindingMode mDefaultClosedPathWindingMode = OF_POLY_WINDING_NONZERO; + +}; - /// \brief Loads an SVG file from the provided filename. - /// - /// ~~~~ - void load(const of::filesystem::path & fileName); - - /// \brief Loads an SVG from a text string. - /// - /// Useful for parsing SVG text from sources other than a file. As the - /// underlying SVG parsing library requires a url, this method gives - /// you the option of providing one. - /// - /// ~~~~ - void loadFromString(std::string data, std::string url = "local"); - void draw(); - int getNumPath(); - ofPath & getPathAt(int n); - const std::vector & getPaths() const; - static void fixSvgString(std::string & xmlstring); -private: - float width, height; - std::vector paths; - void setupDiagram(struct svgtiny_diagram * diagram); - void setupShape(struct svgtiny_shape * shape, ofPath & path); -}; -typedef ofxSvg ofxSVG; diff --git a/addons/ofxSvg/src/ofxSvgCss.cpp b/addons/ofxSvg/src/ofxSvgCss.cpp new file mode 100644 index 00000000000..10705d39791 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgCss.cpp @@ -0,0 +1,614 @@ +#include "ofxSvgCss.h" +#include "ofUtils.h" +#include "ofLog.h" +#include +#include +//#include + + +std::map sCommonColors = { + {"white", ofColor(255, 255, 255)}, + {"black", ofColor(0, 0, 0)}, + {"red", ofColor(255, 0, 0)}, + {"green", ofColor(0, 255, 0)}, + {"blue", ofColor(0, 0, 255)}, + {"yellow", ofColor(255, 255, 0)}, + {"cyan", ofColor(0, 255, 255)}, + {"magenta", ofColor(255, 0, 255)}, + {"gray", ofColor(128, 128, 128)}, + {"orange", ofColor(255, 165, 0)}, + {"brown", ofColor(165, 42, 42)}, + {"pink", ofColor(255, 192, 203)}, + {"purple", ofColor(128, 0, 128)}, + {"lime", ofColor(0, 255, 0)}, + {"maroon", ofColor(128, 0, 0)}, + {"navy", ofColor(0, 0, 128)}, + {"olive", ofColor(128, 128, 0)}, + {"teal", ofColor(0, 128, 128)}, + {"violet", ofColor(238, 130, 238)}, + {"indigo", ofColor(75, 0, 130)}, + {"gold", ofColor(255, 215, 0)}, + {"silver", ofColor(192, 192, 192)}, + {"beige", ofColor(245, 245, 220)}, + {"lavender", ofColor(230, 230, 250)}, + {"turquoise", ofColor(64, 224, 208)}, + {"sky blue", ofColor(135, 206, 235)}, + {"mint", ofColor(189, 252, 201)}, + {"coral", ofColor(255, 127, 80)}, + {"salmon", ofColor(250, 128, 114)}, + {"khaki", ofColor(240, 230, 140)}, + {"ivory", ofColor(255, 255, 240)}, + {"peach", ofColor(255, 218, 185)}, + {"aquamarine", ofColor(127, 255, 212)}, + {"chartreuse", ofColor(127, 255, 0)}, + {"plum", ofColor(221, 160, 221)}, + {"chocolate", ofColor(210, 105, 30)}, + {"orchid", ofColor(218, 112, 214)}, + {"tan", ofColor(210, 180, 140)}, + {"slate gray", ofColor(112, 128, 144)}, + {"periwinkle", ofColor(204, 204, 255)}, + {"sea green", ofColor(46, 139, 87)}, + {"mauve", ofColor(224, 176, 255)}, + {"rose", ofColor(255, 0, 127)}, + {"rust", ofColor(183, 65, 14)}, + {"amber", ofColor(255, 191, 0)}, + {"crimson", ofColor(220, 20, 60)}, + {"sand", ofColor(194, 178, 128)}, + {"jade", ofColor(0, 168, 107)}, + {"denim", ofColor(21, 96, 189)}, + {"copper", ofColor(184, 115, 51)} +}; + +//-------------------------------------------------------------- +void ofxSvgCssClass::clear() { + properties.clear(); + name = "default"; +} + +//-------------------------------------------------------------- +void ofxSvgCssClass::scaleNumericalValues( float ascale ) { + for( auto& piter : properties ) { + if(piter.second.fvalue.has_value()) { + piter.second.fvalue = piter.second.fvalue.value() * ascale; + } + if( piter.second.ivalue.has_value() ) { + piter.second.ivalue = piter.second.ivalue.value() * ascale; + } + } +} + +//-------------------------------------------------------------- +std::string ofxSvgCssClass::sRgbaToHexString(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::uppercase; + ss << std::setw(2) << static_cast(r); + ss << std::setw(2) << static_cast(g); + ss << std::setw(2) << static_cast(b); + ss << std::setw(2) << static_cast(a); + return "#"+ss.str(); +} + +//-------------------------------------------------------------- +std::string ofxSvgCssClass::sRgbToHexString(unsigned char r, unsigned char g, unsigned char b) { + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::uppercase; + ss << std::setw(2) << static_cast(r); + ss << std::setw(2) << static_cast(g); + ss << std::setw(2) << static_cast(b); + return "#"+ss.str(); +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::sIsNone( const std::string& astr ) { + if( astr.empty() ) { + return true; + } + if( ofToLower(astr) == "none" ) { + return true; + } + return false; +} + +//-------------------------------------------------------------- +ofColor ofxSvgCssClass::sGetColor(const std::string& astr ) { + bool bHasHash = false; + std::string cstr = astr; + if( ofIsStringInString(cstr, "#")) { + ofStringReplace(cstr, "#", ""); + bHasHash = true; + } + cstr = ofToLower(cstr); + // if this is only 3 chars, it is short hand and needs to be expanded to 6 characters + if( cstr.size() == 3 ) { + std::string fullHex; + for( char c : cstr ) { + fullHex += c; + fullHex += c; + } + cstr = fullHex; + } + + if( bHasHash ) { + ofColor tcolor(255); + if( cstr.size() == 8 ) { + ofLogVerbose("SvgCss") << "going to try to get the hex from: " << cstr; + unsigned long hexValue = std::stoul(cstr, nullptr, 16); + // Manually extract components from the 8-digit hex value (RRGGBBAA) + int r = (hexValue >> 24) & 0xFF; // Extract the red component + int g = (hexValue >> 16) & 0xFF; // Extract the green component + int b = (hexValue >> 8) & 0xFF; // Extract the blue component + int a = hexValue & 0xFF; // Extract the alpha component + + // Create the color using the extracted components + tcolor.set(r, g, b, a); + } else { + int hint = ofHexToInt(cstr); + tcolor.setHex(hint); + } +// ofLogNotice("ofxSvgCssClass") << "color: " << cstr << " ofColor: " << tcolor; + return tcolor; + } else if( !astr.empty() ) { + if( sCommonColors.count(cstr)) { + return sCommonColors[cstr]; + } + } + return ofColor(255); +} + +//-------------------------------------------------------------- +float ofxSvgCssClass::sGetFloat(const std::string& astr) { + if( astr.empty() ) return 0.f; + +// bool bHasPix = false; + std::string cstr = astr; + if( ofIsStringInString(cstr, "px")) { +// bHasPix = true; + ofStringReplace(cstr, "px", ""); + } + return ofToFloat( cstr ); +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addMissingClassProperties( const ofxSvgCssClass& aclass ) { + for( auto& propI : aclass.properties ) { + if( properties.count(propI.first) < 1 ) { + properties[propI.first] = propI.second; + } + } + return properties.size() > 0; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::setClassProperties( const ofxSvgCssClass& aclass ) { + for( auto& propI : aclass.properties ) { + properties[propI.first] = propI.second; + } + return properties.size() > 0; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperties( std::string aPropertiesString ) { + if( aPropertiesString.size() > 0 ) { + auto propertiesStr = ofSplitString(aPropertiesString, ";", true, true); +// int pindex = 0; + for( auto& propStr : propertiesStr ) { +// std::cout << " " << pindex << " - property: " << propStr << std::endl; + addProperty(propStr); +// pindex++; + } + } + return properties.size() > 0; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperty( std::string aPropString ) { + auto splitProps = ofSplitString(aPropString, ":", true ); + if( splitProps.size() == 2 ) { + return addProperty(splitProps[0], splitProps[1]); + } + return false; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperty( std::string aName, std::string avalue ) { + if( !aName.empty() && !avalue.empty() ) { + Property newProp; + newProp.srcString = avalue; + ofStringReplace(newProp.srcString, ";", ""); + ofStringReplace(newProp.srcString, "'", ""); + properties[aName] = newProp; + return true; + } + return false; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const Property& aprop ) { + return addProperty(aName, aprop.srcString); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const float& avalue ) { + ofxSvgCssClass::Property prop; + prop.fvalue = avalue; + prop.srcString = ofToString(avalue); + return addProperty(aName, prop ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const ofColor& acolor ) { + ofxSvgCssClass::Property prop; + prop.cvalue = acolor; + prop.srcString = sRgbToHexString(acolor.r, acolor.g, acolor.b); +// ofLogNotice(" CssClass::addProperty") << prop.srcString << " color: " << acolor; + return addProperty(aName, prop ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::removeProperty( std::string aPropString ) { + bool bHas = hasProperty(aPropString); + properties.erase(aPropString); + return bHas; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setColor(const ofColor& acolor) { + return addProperty("color", acolor); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFillColor(const ofColor& acolor) { + return addProperty("fill", acolor); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setNoFill() { + return addProperty("fill", "none" ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isFilled() { + return !isNone("fill"); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setStrokeColor(const ofColor& acolor) { + return addProperty("stroke", acolor); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setStrokeWidth( const float& awidth ) { + return addProperty("stroke-width", awidth); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setNoStroke() { +// return addProperty("stroke", "none" ); + bool bstroke = removeProperty("stroke"); + bool bstrokeW = removeProperty("stroke-width"); + return bstroke || bstrokeW; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasStroke() { + return !isNone("stroke"); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFontSize( int asize ) { + return addProperty("font-size", asize); +} + +//-------------------------------------------------- +int ofxSvgCssClass::getFontSize(int adefault) { + return getIntValue("font-size", adefault); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFontFamily( std::string aFontFamily ) { + return addProperty("font-family", aFontFamily); +} + +//-------------------------------------------------- +std::string ofxSvgCssClass::getFontFamily( std::string aDefaultFontFamily ) { + return getValue("font-family", aDefaultFontFamily); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFontBold( bool ab ) { + return addProperty("font-weight", ab ? "bold" : "regular" ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isFontBold() { + bool bold = false; + if( ofIsStringInString(getValue("font-weight", "" ), "bold")) { + bold = true; + } + return bold; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFontItalic(bool ab) { + return addProperty("font-style", ab ? "italic" : "regular" ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isFontItalic() { + bool italic = false; + if( ofIsStringInString(getValue("font-style", "" ), "italic")) { + italic = true; + } + return italic; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasProperty( const std::string& akey ) { + return (properties.count(akey) > 0); +} + +//-------------------------------------------------- +ofxSvgCssClass::Property& ofxSvgCssClass::getProperty( const std::string& akey ) { + if( properties.count(akey) < 1 ) { + return dummyProp; + } + return properties[akey]; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isNone(const std::string& akey) { + if( properties.count(akey) < 1 ) { + return true; + } + return sIsNone( properties[akey].srcString ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasAndIsNone(const std::string& akey) { + if( hasProperty(akey)) { + return sIsNone( properties[akey].srcString ); + } + return false; +} + +//-------------------------------------------------- +std::string ofxSvgCssClass::getValue(const std::string& akey, const std::string& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if(!prop.svalue.has_value()) { + prop.svalue = prop.srcString; + } + return prop.svalue.value(); +} + +//-------------------------------------------------- +int ofxSvgCssClass::getIntValue(const std::string& akey, const int& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if(!prop.ivalue.has_value()) { + prop.ivalue = ofToInt(prop.srcString); + } + return prop.ivalue.value(); +} + +//-------------------------------------------------- +float ofxSvgCssClass::getFloatValue(const std::string& akey, const float& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if( !prop.fvalue.has_value() ) { + prop.fvalue = sGetFloat(prop.srcString); + } + return prop.fvalue.value(); +} + +//-------------------------------------------------- +ofColor ofxSvgCssClass::getColor(const std::string& akey) { + return getColor(akey, ofColor(0)); +} + +//-------------------------------------------------- +ofColor ofxSvgCssClass::getColor(const std::string& akey, const ofColor& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if( !prop.cvalue.has_value() ) { + prop.cvalue = sGetColor(prop.srcString); + } + return prop.cvalue.value(); +} + +//-------------------------------------------------- +std::string ofxSvgCssClass::toString(bool aBPrettyPrint) { + std::stringstream ss; + for( auto& piter : properties ) { + if(aBPrettyPrint) { + ss << std::endl << " "; + } + ss << piter.first << ":" << piter.second.srcString << ";"; + } + return ss.str(); +} + +//-------------------------------------------------- +bool ofxSvgCssStyleSheet::parse( std::string aCssString ) { + if( aCssString.empty() ) { + return false; + } + + classes.clear(); + + ofStringReplace(aCssString, "", ""); + + // Regular expression to match class names (e.g., .cls-1, .cls-2, etc.) + std::regex class_regex(R"((\.[\w\-]+(?:,\s*\.[\w\-]+)*))"); + // Regular expression to match properties within curly braces (e.g., fill: none; stroke-miterlimit: 10;) + std::regex property_regex(R"(\{([^}]+)\})"); + + std::smatch class_match; + std::smatch property_match; + + // Search for each class rule block + auto search_start = aCssString.cbegin(); + while (std::regex_search(search_start, aCssString.cend(), class_match, class_regex)) { + std::string class_list = class_match[1]; // Extract the class list (e.g., .cls-1, .cls-2) + + // Move search start forward to find the corresponding property block + std::string::const_iterator prop_search_start = class_match.suffix().first; + + // Find the corresponding properties block + if (std::regex_search(prop_search_start, aCssString.cend(), property_match, property_regex)) { + std::string properties_block = property_match[1]; // Extract properties + std::map properties; + + // Split properties into key-value pairs + std::regex property_pair_regex(R"(([\w\-]+)\s*:\s*([^;]+);?)"); + std::smatch property_pair_match; + + auto prop_search_start = properties_block.cbegin(); + while (std::regex_search(prop_search_start, properties_block.cend(), property_pair_match, property_pair_regex)) { + std::string key = property_pair_match[1]; // e.g., "fill" + std::string value = property_pair_match[2]; // e.g., "none" + properties[key] = value; // Add the property to the map + prop_search_start = property_pair_match.suffix().first; // Continue searching for more properties + } + + // Process the list of classes (comma-separated classes) + std::regex individual_class_regex(R"(\.[\w\-]+)"); + auto class_search_start = class_list.cbegin(); + std::smatch individual_class_match; + while (std::regex_search(class_search_start, class_list.cend(), individual_class_match, individual_class_regex)) { + std::string class_name = individual_class_match[0]; // Extract the individual class (e.g., .cls-1) + + if( class_name.size() > 0 && class_name[0] == '.' ) { + class_name = class_name.substr(1, std::string::npos); + } + + // Merge properties for the class (with priority for the latest properties) + for (const auto& prop : properties) { + auto& svgCssClass = addClass(class_name); + svgCssClass.addProperty(prop.first, prop.second); +// cssClasses[class_name][prop.first] = prop.second; + } + + class_search_start = individual_class_match.suffix().first; // Move to the next class + } + } + + search_start = property_match.suffix().first; // Move to the next block + } + return classes.size() > 0; +} + +//-------------------------------------------------- +void ofxSvgCssStyleSheet::clear() { + classes.clear(); +} + +//-------------------------------------------------- +void ofxSvgCssStyleSheet::scaleNumericalValues( float ascale ) { + for( auto& tclass : classes ) { + tclass.second.scaleNumericalValues(ascale); + } +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::addClass(std::string aname) { + if( hasClass(aname) ) { + return classes[aname]; + } + ofxSvgCssClass tclass; + tclass.name = aname; + classes[aname] = tclass; + return classes[aname]; +} + +//-------------------------------------------------- +bool ofxSvgCssStyleSheet::hasClass(const std::string& aname) { + return classes.count(aname) > 0; +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::getClass( const std::string& aname ) { + if( hasClass(aname)) { + return classes[aname]; + } + ofLogWarning("ofxSvgCssStyleSheet") << "could not find class " << aname; + return dummyClass; +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::getAddClass( ofxSvgCssClass& aclass ) { + + for( auto& tclass : classes ) { + bool bFoundAll = true; + + // check for the same number of properties + if( tclass.second.properties.size() != aclass.properties.size() ) { + continue; + } + + for( auto& aprop : aclass.properties ) { + bool bFound = false; + for( auto& tprop : tclass.second.properties ) { + if( tprop.first == aprop.first && tprop.second.srcString == aprop.second.srcString) { + bFound = true; + break; + } + } + if( !bFound ) { + bFoundAll = false; + } + } + + if( bFoundAll ) { + // we found another class that has all the things +// ofLogNotice("CssStyleSheet::getAddClass") << "found matching class: " << tclass.second.name; + return classes[tclass.second.name]; + } + } + + // check if name already exists + std::string className = aclass.name; + int ccounter = 0; + bool bKeepGoing = true; + while( bKeepGoing ) { + bool bFound = false; + for( auto& tclass : classes ) { + if( className == tclass.first ) { + bFound = true; + break; + } + } + if( bFound ) { + ccounter ++; + className = aclass.name + ofToString(ccounter); + } else { + bKeepGoing = false; + } + } + + aclass.name = className; + classes[className] = aclass; + return classes[className]; +} + +//-------------------------------------------------- +std::string ofxSvgCssStyleSheet::toString(bool aBPrettyPrint) { + std::stringstream ss; + for( auto& citer : classes ) { + ss << std::endl; + ss << "." << citer.first << " { "; + ss << citer.second.toString(aBPrettyPrint); + ss << "}" << std::endl; + } + + return ss.str(); +} diff --git a/addons/ofxSvg/src/ofxSvgCss.h b/addons/ofxSvg/src/ofxSvgCss.h new file mode 100644 index 00000000000..af3b597a392 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgCss.h @@ -0,0 +1,105 @@ +#pragma once +#include +#include "ofColor.h" +#include "ofLog.h" +#include "ofXml.h" +#include "ofxSvgUtils.h" + +class ofxSvgCssClass { +public: + + /// \note: ofxSvgOptional is declared in ofxSvgUtils + class Property { + public: + std::string srcString; + ofxSvgOptional fvalue; + ofxSvgOptional ivalue; + ofxSvgOptional svalue; + ofxSvgOptional cvalue; + }; + + std::unordered_map properties; + std::string name = "default"; + + void clear(); + void scaleNumericalValues( float ascale ); + + static std::string sRgbaToHexString(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + static std::string sRgbToHexString(unsigned char r, unsigned char g, unsigned char b); + static bool sIsNone( const std::string& astr ); + static ofColor sGetColor(const std::string& astr); + static float sGetFloat(const std::string& astr); + + bool addMissingClassProperties( const ofxSvgCssClass& aclass ); + bool setClassProperties( const ofxSvgCssClass& aclass ); + + bool addProperties( std::string aPropertiesString ); + bool addProperty( std::string aPropString ); + bool addProperty( std::string aName, std::string avalue ); + bool addProperty( const std::string& aName, const Property& aprop ); + bool addProperty( const std::string& aName, const float& avalue ); + bool addProperty( const std::string& aName, const ofColor& acolor ); + + bool removeProperty( std::string aPropString ); + + bool setColor(const ofColor& acolor); + + bool setFillColor(const ofColor& acolor); + bool setNoFill(); + bool isFilled(); + + bool setStrokeColor(const ofColor& acolor); + bool setStrokeWidth( const float& awidth ); + bool setNoStroke(); + bool hasStroke(); + + bool setFontSize( int asize ); + int getFontSize(int adefault); + + bool setFontFamily( std::string aFontFamily ); + std::string getFontFamily( std::string aDefaultFontFamily ); + + bool setFontBold( bool ab ); + bool isFontBold(); + + bool setFontItalic( bool ab ); + bool isFontItalic(); + + bool hasProperty( const std::string& akey ); + Property& getProperty( const std::string& akey ); + bool isNone(const std::string& akey); + bool hasAndIsNone(const std::string& akey); + + std::string getValue(const std::string& akey, const std::string& adefault); + int getIntValue(const std::string& akey, const int& adefault); + float getFloatValue(const std::string& akey, const float& adefault); + ofColor getColor(const std::string& akey); + ofColor getColor(const std::string& akey, const ofColor& adefault); + + std::string toString(bool aBPrettyPrint=true); + +protected: + Property dummyProp; +}; + +class ofxSvgCssStyleSheet { +public: + + bool parse( std::string aCssString ); + void clear(); + + void scaleNumericalValues( float ascale ); + + ofxSvgCssClass& addClass( std::string aname ); + bool hasClass( const std::string& aname ); + ofxSvgCssClass& getClass( const std::string& aname ); + + ofxSvgCssClass& getAddClass( ofxSvgCssClass& aclass ); + + std::unordered_map classes; + + std::string toString(bool aBPrettyPrint=true); + +protected: + ofxSvgCssClass dummyClass; +}; diff --git a/addons/ofxSvg/src/ofxSvgElements.cpp b/addons/ofxSvg/src/ofxSvgElements.cpp new file mode 100755 index 00000000000..2d9128aca26 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgElements.cpp @@ -0,0 +1,879 @@ +#include "ofxSvgElements.h" +#include "ofGraphics.h" +#include + +using std::vector; +using std::string; + +//-------------------------------------------------------------- +std::string ofxSvgElement::sGetTypeAsString(ofxSvgType atype) { + switch (atype) { + case OFXSVG_TYPE_GROUP: + return "Group"; + break; + case OFXSVG_TYPE_RECTANGLE: + return "Rectangle"; + break; + case OFXSVG_TYPE_IMAGE: + return "Image"; + break; + case OFXSVG_TYPE_ELLIPSE: + return "Ellipse"; + break; + case OFXSVG_TYPE_CIRCLE: + return "Circle"; + break; + case OFXSVG_TYPE_PATH: + return "Path"; + break; + case OFXSVG_TYPE_TEXT: + return "Text"; + break; + case OFXSVG_TYPE_DOCUMENT: + return "Document"; + break; + case OFXSVG_TYPE_ELEMENT: + return "Element"; + break; + default: + break; + } + return "Unknown"; +} + +//-------------------------------------------------------------- +std::string ofxSvgElement::sGetSvgXmlName(ofxSvgType atype) { + switch (atype) { + case OFXSVG_TYPE_GROUP: + return "g"; + break; + case OFXSVG_TYPE_RECTANGLE: + return "rect"; + break; + case OFXSVG_TYPE_IMAGE: + return "image"; + break; + case OFXSVG_TYPE_ELLIPSE: + return "ellipse"; + break; + case OFXSVG_TYPE_CIRCLE: + return "circle"; + break; + case OFXSVG_TYPE_PATH: + return "path"; + break; + case OFXSVG_TYPE_TEXT: + return "text"; + break; + case OFXSVG_TYPE_DOCUMENT: + return "svg"; + break; + case OFXSVG_TYPE_ELEMENT: + return "element"; + break; + default: + break; + } + return "unknown"; +} + +//-------------------------------------------------------------- +string ofxSvgElement::getTypeAsString() { + return sGetTypeAsString(getType()); +} + +//-------------------------------------------------------------- +string ofxSvgElement::getCleanName() { + if( name.empty() ) { + return name; + } + + string nstr = name; + + // Step 2: Replace known patterns from Illustrator + const std::vector> tregs = { + {std::regex("_x5F_"), "_"}, + {std::regex("_x28_"), "("}, + {std::regex("_x29_"), ")"}, + {std::regex("_x3B_"), ";"}, + {std::regex("_x2C_"), ","} + }; + + for (const auto& [regexPattern, replacement] : tregs) { + nstr = std::regex_replace(nstr, regexPattern, replacement); + } + + // this regex pattern removes the added numbers added by illustrator + // ie. lelbow_00000070086365269320197030000010368508730034196876_ becomes lelbow + // Define a regular expression pattern to match '__' + std::regex pattern("_\\d+_"); + // Use regex_replace to remove the pattern from the input string + nstr = std::regex_replace(nstr, pattern, ""); + + return nstr; +} + +//-------------------------------------------------------------- +string ofxSvgElement::toString( int nlevel ) { + + string tstr = ""; + for( int k = 0; k < nlevel; k++ ) { + tstr += " "; + } + tstr += ofToString(layer)+": " + getTypeAsString() + " - " + getName() + "\n"; + + return tstr; +} + +//-------------------------------------------------------------- +void ofxSvgPath::applyStyle(ofxSvgCssClass& aclass) { + if( aclass.hasProperty("fill")) { + if( !aclass.isNone("fill")) { + path.setFillColor(aclass.getColor("fill")); + } else { + path.setFilled(false); + } + } else { + // aSvgPath->path.setFilled(false); + path.setFillColor(ofColor(0)); + } + + if( aclass.hasProperty("fill-opacity")) { + if( aclass.isNone("fill-opacity")) { + path.setFilled(false); + } else { + float val = aclass.getFloatValue("fill-opacity", 1.0f); + if( val <= 0.0001f ) { + path.setFilled(false); + } else { + auto pcolor = path.getFillColor(); + pcolor.a = val; + path.setFillColor(pcolor); + } + } + } + + if( !aclass.isNone("stroke") ) { + path.setStrokeColor(aclass.getColor("stroke")); + } + + if( aclass.hasProperty("stroke-width")) { + if( aclass.isNone("stroke-width")) { + path.setStrokeWidth(0.f); + } else { + path.setStrokeWidth( aclass.getFloatValue("stroke-width", 0.f)); + } + } else { + // default with no value is 1.f + // aSvgPath->path.setStrokeWidth(1.f); + } + + // if the color is not set and the width is not set, then it should be 0 + if( !aclass.isNone("stroke") ) { + if( !aclass.hasProperty("stroke-width")) { + path.setStrokeWidth(1.f); + } + } + + if( aclass.hasProperty("opacity")) { + if( path.isFilled() ) { + auto fcolor = path.getFillColor(); + fcolor.a *= aclass.getFloatValue("opacity", 1.f); + path.setFillColor( fcolor ); + } + if( path.hasOutline() ) { + auto scolor = path.getStrokeColor(); + scolor.a *= aclass.getFloatValue("opacity", 1.f); + path.setStrokeColor( scolor ); + } + } + +} + +//-------------------------------------------------------------- +void ofxSvgPath::customDraw() { + + bool bHasOffset = mOffsetPos.x != 0.f || mOffsetPos.y != 0.f; + if(bHasOffset) { + ofPushMatrix(); + ofTranslate(mOffsetPos.x, mOffsetPos.y); + } + + if(isVisible()) { + ofColor fillColor = getFillColor(); + ofColor strokeColor = getStrokeColor(); + if( alpha != 1.f ) { + if( isFilled() ) { + path.setFillColor(ofColor(fillColor.r, fillColor.g, fillColor.b, alpha * (float)fillColor.a )); + } + if( hasStroke() ) { + path.setStrokeColor(ofColor(strokeColor.r, strokeColor.g, strokeColor.b, alpha * (float)strokeColor.a )); + } + } + path.draw(); + + if( alpha != 1.f ) { + if( isFilled() ) { + path.setFillColor(fillColor); + } + if( hasStroke() ) { + path.setStrokeColor(strokeColor); + } + } + } + + if(bHasOffset) { + ofPopMatrix(); + } +} + +#pragma mark - Image +//-------------------------------------------------------------- +void ofxSvgImage::load() { + if( !bTryLoad ) { + if( !getFilePath().empty() ) { + img.load( getFilePath() ); + bTryLoad = true; + } + } +} + +//-------------------------------------------------------------- +void ofxSvgImage::customDraw() { + if( !bTryLoad ) { + load(); + } + + if( isVisible() ) { + if( img.isAllocated() ) { + if(bUseShapeColor) { + ofSetColor( getColor() ); + } + img.draw( 0, 0 ); + } + } +} + +////-------------------------------------------------------------- +//glm::vec2 ofxSvgImage::getAnchorPointForPercent( float ax, float ay ) { +// glm::vec2 ap = glm::vec2( width * ax * getScale().x, height * ay * getScale().y ); +//// ap = glm::rotate(ap, glm::radians(rotation)); +// ap = glm::rotate(ap, atan2f(getSideDir().y, getSideDir().x)); +// return ap; +//} + + +#pragma mark - Text +//-------------------------------------------------------------- +std::vector ofxSvgText::splitBySpanTags(const std::string& input) { + std::vector result; + std::regex span_regex(R"(<[^>]+>[\s\S]*?]+>)"); // Match tags with content including newlines + std::sregex_iterator begin(input.begin(), input.end(), span_regex); + std::sregex_iterator end; + + size_t last_pos = 0; + for (auto it = begin; it != end; ++it) { + std::smatch match = *it; + size_t match_start = match.position(); + size_t match_end = match_start + match.length(); + + // Add text before match + if (match_start > last_pos) { + result.push_back(input.substr(last_pos, match_start - last_pos)); + } + + // Add the matched tag + result.push_back(match.str()); + + last_pos = match_end; + } + + // Add any remaining text after the last match + if (last_pos < input.length()) { + result.push_back(input.substr(last_pos)); + } + + return result; +} + + +//-------------------------------------------------------------- +ofxSvgText::SpanData ofxSvgText::extractSpanData(const std::string& spanTag) { + SpanData data; + + // Extract style + std::regex style_regex(R"(style\s*=\s*["']([^"']*)["'])"); + std::smatch style_match; + if (std::regex_search(spanTag, style_match, style_regex)) { + data.style = style_match[1].str(); + } + + // Extract inner content (even across newlines) + // std::regex inner_text_regex(R"(]*>([\s\S]*?))"); + std::regex inner_text_regex(R"(]*>([\s\S]*?)<\/span>)"); + std::smatch content_match; + if (std::regex_search(spanTag, content_match, inner_text_regex)) { + data.content = content_match[1].str(); + } + + return data; +} + +//-------------------------------------------------------------- +bool ofxSvgText::endsWithLineEnding(const std::string& astr) { + if (astr.size() >= 2 && astr.substr(astr.size() - 2) == "\r\n") { + // Windows line ending + return true; + } else if (!astr.empty() && astr.back() == '\n') { + // Unix line ending + return true; + } + return false; +} + +//-------------------------------------------------------------- +std::vector ofxSvgText::splitWordsAndLineEndings(const std::string& input) { + std::vector result; + + // Match a single \r or \n or \r\n, or a word + std::regex token_regex(R"(\r\n|[\r\n]|[^\s\r\n]+)"); + std::sregex_iterator begin(input.begin(), input.end(), token_regex); + std::sregex_iterator end; + + for (auto it = begin; it != end; ++it) { + result.push_back(it->str()); + } + + return result; +} + +// build the text spans from a string and not from xml / svg file structure // +//-------------------------------------------------------------- +void ofxSvgText::setText( const std::string& astring, std::string aFontFamily, int aFontSize, float aMaxWidth ) { + ofxSvgCssClass css; + css.addProperty("font-family", aFontFamily); + css.addProperty("font-size", aFontSize); + css.addProperty("color", getColor() ); + setText( astring, css, aMaxWidth ); +} + +//-------------------------------------------------------------- +void ofxSvgText::setText( const std::string& astring, const ofxSvgCssClass& aSvgCssClass, float aMaxWidth ) { + meshes.clear(); + textSpans.clear(); + + if( astring.empty() ) { + ofLogWarning("ofxSvgText::setText") << "string argument is empty. "; + return; + } + + auto spanStrings = splitBySpanTags(astring); + // ofLogNotice("ofxSvgText") << "number of strings: " << spanStrings.size(); + float ex = 0.f; + float ey = 0.f; + int spanCounter = 0; + for( auto& spanString : spanStrings ) { + std::vector twords;// = ofSplitString(spanString, " ", true, true); + bool bLastCharIsSpace = false; + + // ofLogNotice("ofxSvgText") << "spanString: |" < tag.\n"; +// std::cout << " Style: [" << data.style << "]\n"; +// std::cout << " Content: [" << data.content << "]\n"; + css.addProperties(data.style); + if (!data.content.empty() && data.content.back() == ' ') { + bLastCharIsSpace = true; + data.content.pop_back(); + } + // twords = ofSplitString(data.content, " " , false, false ); + twords = splitWordsAndLineEndings(data.content); + } else { +// std::cout << "Regular text: [" << spanString << "]\n"; + + if (!spanString.empty() && spanString.back() == ' ') { + bLastCharIsSpace = true; + spanString.pop_back(); + } + + // twords = ofSplitString(spanString, " " , false, false ); + twords = splitWordsAndLineEndings(spanString); + } + // cspan->applyStyle(css); + + auto cspan = std::make_shared(); + cspan->applyStyle(css); + + ofLogVerbose("ofxSvgText" ) << "going to try and load: " << spanString << " bold: " << cspan->isBold() << " italic: " << cspan->isItalic(); + if(!ofxSvgFontBook::loadFont(fdirectory, cspan->getFontFamily(), cspan->getFontSize(), cspan->isBold(), cspan->isItalic() )) { + continue; + } + + auto& font = cspan->getFont(); + + // if( spanString=="\n") { + // ex = 0.f; + // ey += font.getLineHeight(); + // continue; + // } + +// if( spanCounter == 0 ) { +// // shift everything down on the first line so it's within the desired bounds +// ey = font.stringHeight("M"); +// } + + cspan->rect.x = ex; + cspan->rect.y = ey; + + + std::stringstream ss; + std::string tCurrentString = ""; + + // ofRectangle addedBounds(ex, ey,1, 1); + + bool bFirstBreak = true; + + bool bWasAStringLineBreak = false; + + // std::cout << "_____________________________________________________" << std::endl; + // for( std::size_t i = 0; i < twords.size(); i++ ) { + // auto& token = twords[i]; + // if (token == "\n") { + // std::cout << "[\\n]" << std::endl; + // } else if (token == "\r\n") { + // std::cout << "[\\r\\n]" << std::endl; + // } else if (token == "\r") { + // std::cout << "[\\r]" << std::endl; + // } else { + // std::cout << "[" << token << "]" << std::endl; + // } + // } + // std::cout << "_____________________________________________________" << std::endl; + + for( std::size_t i = 0; i < twords.size(); i++ ) { + + bool bFinalWord = (i == twords.size()-1); + + bool bAddedBreak = false; + tCurrentString += twords[i];//+ " "; + if( i < twords.size()-1 ) { //|| (bFinalWord && bLastCharIsSpace )) { + tCurrentString += " "; + } else { + if(bFinalWord && bLastCharIsSpace) { + tCurrentString += "f"; + } + } + + bool bIsAStringLineBreak = false; + if( twords[i] == "\r\n" || twords[i] == "\n" ) { + bIsAStringLineBreak = true; + } + + // ofLogNotice("textSpan word string") << "|"<rect.x > aMaxWidth || bIsAStringLineBreak > 0 ) { + // if( ex > aMaxWidth ) { + + tCurrentString = twords[i]; + if( bLastCharIsSpace ) { + tCurrentString += "f"; + } + // ofLogNotice("ofxSvgText") << "we have a break, taking ex from string: |" << tCurrentString << "|"; + ex = font.getStringBoundingBox(tCurrentString,0,0,true).getRight(); + // if( bLastCharIsSpace ) { + // ex += 60;//font.getCharWidth(' '); + // } + + if(bIsAStringLineBreak) { + ex = 0.f; + tCurrentString = ""; + } + + if( bFirstBreak && spanCounter > 0 ) { + bFirstBreak = false; + // if( spanCounter > 0 ) { + cspan->text = ss.str(); + // cspan->rect.height = font.stringHeight(cspan->text+"M"); + //ey += cspan->rect.height; + ey += font.getLineHeight(); + + if( ss.str().size() > 0 ) { + // ofLogNotice("ofxSvgText") << "LINE Break: adding cspan: " << textSpans.size() << " x: " << cspan->rect.x << " text: |" << cspan->text << "|"; + textSpans.push_back(cspan); + } + + cspan = std::make_shared(); + cspan->applyStyle(css); + ss.str(""); + ss.clear(); + // ofLogNotice("Adding a new line I think") << "ss: " <rect.x = ex; + cspan->rect.y = ey; + bAddedBreak = true; + // } + // } + } else { + //ey += font.stringHeight("M"); + bAddedBreak = true; + if( spanCounter == 0 && i == 0 ) { + + } else { + ey += font.getLineHeight(); + ss << std::endl; + } + } + + bFirstBreak = false; + + // addedBounds.growToInclude(font.getStringBoundingBox(cspan->text+"M", 0, 0)); + // ey += addedBounds.height; + // ey += font.getLineHeight(); + + // if(!bFirstBreak) { + // cspan->rect.x = ex; + // } + } + + if( !bAddedBreak && i > 0 && !bWasAStringLineBreak) { + // if( !bFinalWord ) { + ss << " "; + // } + } + if( !bIsAStringLineBreak ) { + ss << twords[i]; + } + + bWasAStringLineBreak = bIsAStringLineBreak; + } + + // ey += font.stringHeight(ss.str()); + // if( !bFirstBreak ) { + // ey += font.stringHeight(ss.str()+"M"); + // } + + ex += cspan->rect.x; + + if( ss.str().size() > 0 ) { + if( endsWithLineEnding(ss.str())) { + ex = 0.f; + } + // if(!bLastCharIsSpace && bFirstBreak) { + // ss << " "; + //ex += 30.f;//font.getCharWidth(' ') * 4.0f;//font.getStringBoundingBox(" ",0.f, 0.f).getRight(); + // ex += font.getCharWidth('f'); + // } + cspan->text = ss.str(); + // ofLogNotice("ofxSvgText") << "adding cspan: " << textSpans.size() << " x: " << cspan->rect.x << " text: |" << cspan->text << "|"; + cspan->rect.height = font.getStringBoundingBox(cspan->text, 0.f, 0.f ).getHeight();//font.stringHeight(cspan->text); + if( bFirstBreak ) { + cspan->rect.height = font.getLineHeight(); + // ey += font.getLineHeight(); + } else { + // cspan->rect.height += font.getLineHeight(); + } + // ey = cspan->rect.y + cspan->rect.height; + //ey += cspan->rect.height; + textSpans.push_back(cspan); + } + + spanCounter++; + } + + // for( std::size_t i = 0; i < textSpans.size(); i++ ) { + // ofLogNotice("TextSpan") << i << " - " << "|"<text<<"|"; + // } + + create(); +} + +//-------------------------------------------------------------- +void ofxSvgText::create() { + meshes.clear(); + + // now lets sort the text based on meshes that we need to create // + vector< std::shared_ptr > tspans = textSpans; + + for( auto& tspan : textSpans ) { + + // lets add any missing properties for the text spans from the ofxSvgText class + tspan->mSvgCssClass.addMissingClassProperties(mSvgCssClass); + +// auto tkey = ofxSvgFontBook::getFontKey(tspan->getFontFamily(), tspan->isBold(), tspan->isItalic() ); + if( !ofxSvgFontBook::hasFont(tspan->getFontFamily(), tspan->getFontSize(), tspan->isBold(), tspan->isItalic() )) { + ofLogVerbose("ofxSvgText") << "Trying to load font " << tspan->getFontFamily() << " bold: " << tspan->isBold() << " italic: " << tspan->isItalic() << " | " << ofGetFrameNum(); + // loadFont( const std::string& aDirectory, const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ) + ofxSvgFontBook::loadFont(fdirectory, tspan->getFontFamily(), tspan->getFontSize(), tspan->isBold(), tspan->isItalic() ); + } + } + + std::map< string, std::map< int, vector > > > tspanFonts; + for( std::size_t i = 0; i < tspans.size(); i++ ) { + if( tspanFonts.count( tspans[i]->getFontKey() ) == 0 ) { + std::map< int, vector< std::shared_ptr> > tmapap; + tspanFonts[ tspans[i]->getFontKey() ] = tmapap; + } + std::map< int, vector< std::shared_ptr> >& spanMap = tspanFonts[ tspans[i]->getFontKey() ]; + if( spanMap.count(tspans[i]->getFontSize()) == 0 ) { + vector< std::shared_ptr > tvec; + spanMap[ tspans[i]->getFontSize() ] = tvec; + } + spanMap[ tspans[i]->getFontSize() ].push_back( tspans[i] ); + } + + std::map< string, std::map< int, vector< std::shared_ptr> > >::iterator mainIt; + for( mainIt = tspanFonts.begin(); mainIt != tspanFonts.end(); ++mainIt ) { + + // now create a mesh for the family // + // map< string, map > meshes; + if( meshes.count(mainIt->first) == 0 ) { + std::map< int, ofMesh > tempMeshMap; + meshes[ mainIt->first ] = tempMeshMap; + } + + // Font& tfont = fonts[ mainIt->first ]; + std::map< int, ofMesh >& meshMap = meshes[ mainIt->first ]; + + std::map< int, vector> >::iterator vIt; + for( vIt = mainIt->second.begin(); vIt != mainIt->second.end(); ++vIt ) { + vector>& spanSpans = vIt->second; + + if( !ofxSvgFontBook::hasFontForKey(mainIt->first, vIt->first )) { + ofLogError("ofxSvgText") << __FUNCTION__ << " : Could not find that font size in the map: " << vIt->first; + continue; + } + + if( meshMap.count(vIt->first) == 0 ) { + meshMap[ vIt->first ] = ofMesh(); + } + ofMesh& tmesh = meshMap[ vIt->first ]; + + // ofTrueTypeFont& ttfont = tfont.sizes[ vIt->first ]; + auto& ttfont = ofxSvgFontBook::getFontForKey(mainIt->first, vIt->first); + for( std::size_t i = 0; i < spanSpans.size(); i++ ) { + // create a mesh here // + std::shared_ptr& cspan = spanSpans[i]; + if( cspan->text == "" ) continue; +// cout << "font family: " << cspan.fontFamily << " size: " << cspan.fontSize << " text: " << cspan.text << endl; + +// const ofMesh& stringMesh = ttfont.getStringMesh( "please work", 20, 20 ); + + ofRectangle tempBounds = ttfont.getStringBoundingBox( cspan->text, 0, 0 ); + float tffontx = bCentered ? cspan->rect.x - tempBounds.width/2 : cspan->rect.x; +// const ofMesh& stringMesh = ttfont.getStringMesh( cspan.text, tffontx-ogPos.x, cspan.rect.y-ogPos.y ); + const ofMesh& stringMesh = ttfont.getStringMesh( cspan->text, tffontx, cspan->rect.y ); + int offsetIndex = tmesh.getNumVertices(); + + vector tsIndices = stringMesh.getIndices(); + for( std::size_t k = 0; k < tsIndices.size(); k++ ) { + tsIndices[k] = tsIndices[k] + offsetIndex; + } + + ofFloatColor tcolor = cspan->getColor(); + vector< ofFloatColor > tcolors; + tcolors.assign( stringMesh.getVertices().size(), tcolor ); + + tmesh.addIndices( tsIndices ); + tmesh.addVertices( stringMesh.getVertices() ); + tmesh.addTexCoords( stringMesh.getTexCoords() ); + tmesh.addColors( tcolors ); + } + } + } + + // now loop through and set the width and height of the text spans // + for( std::size_t i = 0; i < textSpans.size(); i++ ) { + auto& tempSpan = textSpans[i]; + + ofTrueTypeFont& tfont = tempSpan->getFont(); + if( tfont.isLoaded() ) { + tempSpan->mBTextDirty = false; + tempSpan->fontRect = tfont.getStringBoundingBox( tempSpan->text, 0, 0 ); +// ofLogNotice("ofxSvgText::create") << getCleanName() << "-Updating text span with text: " << tempSpan->text << " width: " << tempSpan->fontRect.getWidth() << " | " << ofGetFrameNum(); + // rect is used for drawing the font. tempSpan->fontRect is used for calculating bounding box. + tempSpan->rect.width = tempSpan->fontRect.width; + tempSpan->rect.height = tempSpan->fontRect.height; +// ofRectangle() +// tempSpan->lineHeight = tfont.getStringBoundingBox("M", 0, 0).height; + tempSpan->lineHeight = tfont.getLineHeight(); + } + } +} + +//-------------------------------------------------------------- +void ofxSvgText::customDraw() { + if( !isVisible() ) return; + + if(bUseShapeColor) { + ofSetColor( 255, 255, 255, 255.f * alpha ); + } + std::map< string, std::map >::iterator mainIt; + + if(areTextSpansDirty()) { + // sets textSpan->mBTextDirty to false; + create(); + } + + ofTexture* tex = NULL; + for( mainIt = meshes.begin(); mainIt != meshes.end(); ++mainIt ) { + string fontKey = mainIt->first; + std::map< int, ofMesh >::iterator mIt; + for( mIt = meshes[ fontKey ].begin(); mIt != meshes[ fontKey ].end(); ++mIt ) { + int fontSize = mIt->first; + // let's check to make sure that the texture is there, so that we can bind it // + bool bHasTexture = false; + // if( fonts.count( fontKey ) ) { + if( ofxSvgFontBook::hasBookFont(fontKey)) { + auto& fbook = ofxSvgFontBook::getBookFont(fontKey); + if( fbook.textures.count( fontSize ) ) { + bHasTexture = true; + tex = &fbook.textures[ fontSize ]; + } + } + + if( bHasTexture ) tex->bind(); + ofMesh& tMeshMesh = mIt->second; + if( bUseShapeColor ) { + vector< ofFloatColor >& tcolors = tMeshMesh.getColors(); + for( auto& tc : tcolors ) { + tc.a = alpha; + } + } else { + tMeshMesh.disableColors(); + } + tMeshMesh.draw(); + if( bHasTexture ) tex->unbind(); + tMeshMesh.enableColors(); + } + } +} + +////-------------------------------------------------------------- +//void ofxSvgText::draw(const std::string &astring, bool abCentered ) { +// if( textSpans.size() > 0 ) { +// transformGL(); { +// textSpans[0]->draw(astring, abCentered ); +// } restoreTransformGL(); +// } else { +// ofLogVerbose("ofxSvgText") << __FUNCTION__ << " : no text spans to draw with."; +// } +//} +// +////-------------------------------------------------------------- +//void ofxSvgText::draw(const std::string& astring, const ofColor& acolor, bool abCentered ) { +// if( textSpans.size() > 0 ) { +// transformGL(); { +// textSpans[0]->draw(astring, acolor, abCentered ); +// } restoreTransformGL(); +// } else { +// ofLogVerbose("ofxSvgText") << __FUNCTION__ << " : no text spans to draw with."; +// } +//} + +//-------------------------------------------------------------- +void ofxSvgText::TextSpan::applyStyle(ofxSvgCssClass& aclass) { + mSvgCssClass = aclass; + + if( !aclass.hasProperty("color") ) { + if( aclass.hasProperty("fill")) { + mSvgCssClass.addProperty("color", aclass.getColor("fill") ); + } else { + mSvgCssClass.addProperty("color", ofColor(0)); + } + } + + alpha = 1.f; + if( mSvgCssClass.hasProperty("opacity")) { + alpha = mSvgCssClass.getFloatValue("opacity", 1.f); + } + + ofLogVerbose("ofxSvgText::TextSpan::applyStyle") << "text: " << text << " has fill: " << mSvgCssClass.hasProperty("fill") << " color: " << getColor(); + ofLogVerbose("ofxSvgText::TextSpan::applyStyle") << " css class: " << mSvgCssClass.toString() << std::endl;// << " color: " << color; + +} + +// must return a reference for some reason here // +//-------------------------------------------------------------- +ofTrueTypeFont& ofxSvgText::TextSpan::getFont() { + return ofxSvgFontBook::getFontForKey( getFontKey(), getFontSize() ); +} + +//-------------------------------------------------------------- +void ofxSvgText::TextSpan::draw(const std::string &astring, bool abCentered ) { + draw( astring, getColor(), abCentered ); +} + +//-------------------------------------------------------------- +void ofxSvgText::TextSpan::draw(const std::string &astring, const ofColor& acolor, bool abCentered ) { + ofSetColor( acolor ); + auto& cfont = getFont(); + ofRectangle tempBounds = cfont.getStringBoundingBox( astring, 0, 0 ); + float tffontx = abCentered ? rect.getCenter().x - tempBounds.width/2 : rect.x; + // const ofMesh& stringMesh = cfont.getStringMesh( astring, tffontx, rect.y ); + // cfont.drawString(astring, tffontx, rect.y + (tempBounds.getHeight() - lineHeight) ); + cfont.drawString(astring, tffontx, rect.y ); +} + +//-------------------------------------------------------------- +ofTrueTypeFont& ofxSvgText::getFont() { + if( textSpans.size() > 0 ) { + return textSpans[0]->getFont(); + } + ofLogWarning("ofxSvgText") << __FUNCTION__ << " : no font detected from text spans, returning default font."; + return ofxSvgFontBook::defaultFont; +} + +// get the bounding rect for all of the text spans in this text element +// should be called after create // +//-------------------------------------------------------------- +ofRectangle ofxSvgText::getBoundingBox() { + if(areTextSpansDirty()) { + create(); + } + + ofRectangle temp( 0, 0, 1, 1 ); + for( std::size_t i = 0; i < textSpans.size(); i++ ) { + ofRectangle trect = textSpans[i]->fontRect; + trect.x += textSpans[i]->rect.x; + trect.y += textSpans[i]->rect.y; + if( i == 0 ) { + temp = trect; + } else { + temp.growToInclude( trect ); + } + } + + // we want a local rect + return temp; +} + +//-------------------------------------------------------------- +bool ofxSvgText::areTextSpansDirty() { + bool bDirty = false; + for( auto& tspan : textSpans ) { + if( tspan->mBTextDirty ) { + bDirty=true; + break; + } + } + return bDirty; +} + + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgElements.h b/addons/ofxSvg/src/ofxSvgElements.h new file mode 100755 index 00000000000..3b97a908826 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgElements.h @@ -0,0 +1,667 @@ +#pragma once +#include "ofNode.h" +#include "ofImage.h" +#include "ofPath.h" +#include +#include "ofTrueTypeFont.h" +#include "ofxSvgCss.h" +#include "ofxSvgFontBook.h" + +enum ofxSvgType { + OFXSVG_TYPE_ELEMENT = 0, + OFXSVG_TYPE_GROUP, + OFXSVG_TYPE_RECTANGLE, + OFXSVG_TYPE_IMAGE, + OFXSVG_TYPE_ELLIPSE, + OFXSVG_TYPE_CIRCLE, + OFXSVG_TYPE_PATH, + OFXSVG_TYPE_TEXT, + OFXSVG_TYPE_DOCUMENT, + OFXSVG_TYPE_TOTAL +}; + +class ofxSvgElement : public ofNode { + // adding some friend classes so we can set protected properties from other classes. + friend class ofxSvg; + friend class ofxSvgGroup; +public: + + /// \brief Get the ofxSvgType as a string. + /// \param ofxSvgType atype. + /// \return std::string as readable type. + static std::string sGetTypeAsString(ofxSvgType atype); + /// \brief Get the xml name from the ofxSvgType. + /// \param ofxSvgType atype. + /// \return std::string as readable type. + static std::string sGetSvgXmlName(ofxSvgType atype); + /// \brief Get the ofxSvgType for this element. + /// \return ofxSvgType. + virtual ofxSvgType getType() {return OFXSVG_TYPE_ELEMENT;} + /// \brief Get the ofxSvgType as a string. + /// \return std::string as readable type. + std::string getTypeAsString(); + /// \brief Get the name of the element; or the id attribute from the xml node. + /// \return std::string name of element. + std::string getName() { return name; } + /// \brief Set the name of the element; or the id attribute of the xml node. + /// \param aname string; name to be used for the element + void setName( const std::string& aname ) { + name = aname; + } + /// \brief Get name with escaped characters and attempts to remove added naming patterns. + /// Removes the numbers added to the name by illustrator + /// ie. lelbow_00000070086365269320197030000010368508730034196876_ becomes lelbow + std::string getCleanName(); + /// \return bool if this element is a group or not. + bool isGroup() { + return (getType() == OFXSVG_TYPE_GROUP); + } + + /// \brief Set the rotation around the z-axis. + /// \param adegrees rotation in degrees. + void setRotationDeg( float adegrees ) { + setRotationRad(glm::radians(adegrees)); + } + + /// \brief Set the rotation around the z-axis. + /// \param aradians rotation in radians. + void setRotationRad( float aradians ) { + setOrientation(glm::angleAxis(aradians, glm::vec3(0.f, 0.f, 1.f) )); + } + + /// \brief Convenience function that wraps getRollDeg() from ofNode. + /// \return Rotation around the z-axis in degrees. + float getRotationDeg() { + return getRollDeg(); + } + + /// \brief Convenience function that wraps getRollRad() from ofNode. + /// \return Rotation around the z-axis in radians. + float getRotationRad() { + return getRollRad(); + } + + /// \brief Set the visibility of the element, will not draw if not visible. + /// \param bool aBVisible set to true for visible. + virtual void setVisible( bool ab ) { bVisible = ab; } + /// \return bool if this element is visible. + bool isVisible() const { return bVisible; } + /// \brief Set the drawing alpha value of the element. + /// \param float aAlpha from 0-1; transparent to full opacity. + virtual void setAlpha( float aAlpha ) { + alpha = aAlpha; + } + /// \return float alpha value of the element. + float getAlpha() { + return alpha; + } + /// \brief The layer order of the element in the document hierarchy. + /// \return float layer of the element. + float getLayer() { + return layer; + } + /// \brief Enable or disable using colors from paths or text spans. + /// Set to false so ofSetColor() outside of this class will have effect/ + /// \param bool Enable or disable the shape colors. + virtual void setUseColors( bool ab ) { + bUseShapeColor = ab; + } + + + /// \brief Output a string description + /// \param nlevel (optional) is the indentation amount. + /// \return string with type and name. + virtual std::string toString(int nlevel = 0); + + /// \brief Override the function in ofNode meant for drawing + virtual void customDraw() override {}; + /// \brief Apply css class to the element. Meant to be overridden in subsequent classes. + virtual void applyStyle(ofxSvgCssClass& aclass) {}; + + /// \brief Get the local axis aligned bounding box of the element without transforms applied. + virtual ofRectangle getBoundingBox() { return ofRectangle( 0.0f, 0.0f, 1.f, 1.f ); }; + + /// \brief Get the axis aligned bounding box of the element, taking into account + /// global position, rotation and scale to determine the extents of the element aligned with x and y axes. + virtual ofRectangle getGlobalBoundingBox() { + auto localRect = getBoundingBox(); + std::vector rverts = { localRect.getTopLeft(), localRect.getTopRight(), localRect.getBottomRight(), localRect.getBottomLeft()}; + auto gtrans = getGlobalTransformMatrix(); + + for( auto& rv : rverts ) { + rv = glm::vec3(gtrans * glm::vec4(rv, 1.f)); + } + return _calculateExtents( rverts ); + }; + + virtual ofPolyline getFirstPolyline() { + ofLogWarning("ofxSvgElement") << __FUNCTION__ << " : Element " << getTypeAsString() << " does not have a path."; + return ofPolyline(); + } +protected: + ofRectangle _calculateExtents( const std::vector& averts ) { + glm::vec2 min = {std::numeric_limits::max(), std::numeric_limits::max()}; + glm::vec2 max = {std::numeric_limits::min(), std::numeric_limits::min()}; + + for( auto& rv : averts ) { + min.x = std::min( min.x, rv.x ); + min.y = std::min( min.y, rv.y ); + + max.x = std::max( max.x, rv.x ); + max.y = std::max( max.y, rv.y ); + } + + return ofRectangle( min.x, min.y, max.x-min.x, max.y-min.y ); + } + + bool bUseShapeColor = true; + + float alpha = 1.f; + std::string name = ""; + float layer = -1.f; + bool bVisible=true; + + // used for storing the offset model rotation point // + glm::vec2 mModelRotationPoint = glm::vec2(0.f, 0.f); + + virtual std::shared_ptr clone() { + return std::make_shared(*this); + }; +}; + +class ofxSvgPath : public ofxSvgElement { + friend class ofxSvg; +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_PATH;} + + virtual void setUseColors( bool ab ) override { + ofxSvgElement::setUseColors(ab); + path.setUseShapeColor(ab); + } + + virtual void applyStyle(ofxSvgCssClass& aclass) override; + + virtual void customDraw() override; + + bool isFilled() { return path.isFilled(); } + ofColor getFillColor() { return path.getFillColor(); } + void setFillColor( const ofColor& acolor ) { + path.setFillColor(acolor); + } + + bool hasStroke() { return path.hasOutline(); } + float getStrokeWidth() { return path.getStrokeWidth(); } + void setStrokeWidth( const float& awidth ) { + path.setStrokeWidth(awidth); + } + ofColor getStrokeColor() { return path.getStrokeColor(); } + void setStrokeColor( const ofColor& acolor ) { + path.setStrokeColor( acolor ); + } + + /// \brief Get the first polyline if available. + /// \return ofPolyline returned from path.getOutline()[0]; + ofPolyline getFirstPolyline() override { + if( path.getOutline().size() > 0 ) { + return path.getOutline()[0]; + } + ofLogWarning("ofxSvgPath") << __FUNCTION__ << " : path does not have an outline."; + return ofPolyline(); + } + + /// \brief Get the local axis aligned bounding box of the path, no transforms applied. + virtual ofRectangle getBoundingBox() override { + if(mBBoxNeedsRecalc) { + mBBoxNeedsRecalc = false; +// ofRectangle brect; + const auto& outlines = path.getOutline(); + if( outlines.size() > 0 ) { + bool bFirst = true; + for( auto& outline : outlines ) { + auto bbox = outline.getBoundingBox(); + bbox.x += mOffsetPos.x; + bbox.y += mOffsetPos.y; + + if( bFirst ) { + bFirst = false; + mBounds = bbox; +// mBounds = outline.getBoundingBox(); + } else { + mBounds.growToInclude( bbox ); +// mBounds.growToInclude( outline.getBoundingBox() ); + } + } + } + } + return mBounds; + }; + + /// \brief Offset the local shape position. Helpful with offset rotations. + void setOffsetPathPosition( float ax, float ay ) { + if( ax != mOffsetPos.x || ay != mOffsetPos.y ) { + mBBoxNeedsRecalc=true; + } + mOffsetPos = {ax, ay, 0.f}; + } + glm::vec3& getOffsetPathPosition() { + return mOffsetPos; + } + + ofPath& getPath() { + mBBoxNeedsRecalc = true; + return path; + }; + +protected: + ofPath path; + bool mBBoxNeedsRecalc = true; + ofRectangle mBounds; + + glm::vec3 mOffsetPos = {0.f, 0.f, 0.f}; + + virtual std::shared_ptr clone() override { + return std::make_shared(*this); + }; +}; + +class ofxSvgRectangle : public ofxSvgPath { + friend class ofxSvg; +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_RECTANGLE;} + + float getWidth() { return width;} + float getHeight() { return height;} + + float getGlobalWidth() { return width * getGlobalScale().x;} + float getGlobalHeight() { return height * getGlobalScale().y;} + + /// \brief Get the local bounding box of the rectangle, no transforms applied. + virtual ofRectangle getBoundingBox() override { + return ofRectangle(mOffsetPos.x, mOffsetPos.y, getWidth(), getHeight()); + }; + + /// \brief Set the radius for rounded corners. + /// \param aRoundAmount the curve radius + void setRoundRadius( float aRoundAmount ) { + if( aRoundAmount != roundRadius ) { + roundRadius = aRoundAmount; + path.clear(); + if( roundRadius > 0.0f ) { + path.rectRounded(0.f, 0.f, width, height, roundRadius); + } else { + path.rectangle(0.f, 0.f, width, height ); + } + } + } + + float getRoundRadius() { + return roundRadius; + } + +protected: + float roundRadius = 0.f; + float width = 0.f; + float height = 0.f; + + virtual std::shared_ptr clone() override { + return std::make_shared(*this); + }; +}; + + +class ofxSvgCircle : public ofxSvgPath { + friend class ofxSvg; +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_CIRCLE;} + float getRadius() {return radius;} + float getGlobalRadius() {return radius * getGlobalScale().x;} + + /// \brief Get the local bounding box of the circle, no transforms applied. + virtual ofRectangle getBoundingBox() override { + return ofRectangle(-getRadius()+mOffsetPos.x, -getRadius()+mOffsetPos.y, getRadius()*2.f, getRadius()*2.f ); + }; + + /// \brief Set the radius of the circle. Only updates if the stored radius differs from the radius argument. + /// \param float desired radius of the circle. + void setRadius( float aradius ) { + if( aradius != radius ) { + radius = aradius; + path.clear(); + path.circle(0.f, 0.f, radius); + } + } + +protected: + float radius = 0.0; + + virtual std::shared_ptr clone() override { + return std::make_shared(*this); + }; +}; + +class ofxSvgEllipse : public ofxSvgPath { + friend class ofxSvg; +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_ELLIPSE;} + float getRadiusX() {return radiusX;} + float getRadiusY() {return radiusY;} + + float getGlobalRadiusX() {return radiusX * getGlobalScale().x;} + float getGlobalRadiusY() {return radiusY * getGlobalScale().x;} + + /// \brief Get the local bounding box of the circle, no transforms applied. + virtual ofRectangle getBoundingBox() override { + return ofRectangle(-getRadiusX()+mOffsetPos.x, -getRadiusY()+mOffsetPos.y, getRadiusX()*2.f, getRadiusY()*2.f ); + }; + + /// \brief Set the radius X and radius Y of the ellipse. Only updates if the stored radius X or radius Y differs from the radius arguments. + /// \param float aRadiusX desired radius X of the ellipse. + /// \param float aRadiusY desired radius Y of the ellipse. + void setRadius( float aRadiusX, float aRadiusY ) { + if( aRadiusX != radiusX || aRadiusY != radiusY ) { + path.clear(); + radiusX = aRadiusX; + radiusY = aRadiusY; + path.ellipse({0.f,0.f}, radiusX * 2.0f, radiusY * 2.0f ); + } + } + +protected: + float radiusX, radiusY = 10.0f; + + virtual std::shared_ptr clone() override { + return std::make_shared(*this); + }; +}; + + +class ofxSvgImage : public ofxSvgElement { + friend class ofxSvg; +public: + virtual ofxSvgType getType() override {return OFXSVG_TYPE_IMAGE;} + + float getWidth() const { return width;} + float getHeight() const { return height;} + + float getGlobalWidth() { return width * getGlobalScale().x;} + float getGlobalHeight() { return height * getGlobalScale().y;} + + /// \brief Get the local bounding box of the image, no transforms applied. + virtual ofRectangle getBoundingBox() override { + return ofRectangle(0.f, 0.f, getWidth(), getHeight()); + }; + + void load(); + bool isLoaded() { + return (img.isAllocated() && img.getWidth() > 0 && img.getHeight() > 0); + } + + virtual void customDraw() override; + // glm::vec2 getAnchorPointForPercent( float ax, float ay ); + + const std::filesystem::path& getFilePath() { return filepath; } + ofImage& getImage() { return img; } + + void setColor( ofColor aColor ) { + color = aColor; + } + ofColor getColor() { + return ofColor( color.r, color.g, color.b, alpha * (float)color.a ); + } + +protected: + of::filesystem::path filepath; + + ofColor color; + ofImage img; + + float width = 0.f; + float height = 0.f; + bool bTryLoad = false; + + virtual std::shared_ptr clone() override { + return std::make_shared(*this); + }; +}; + + +class ofxSvgText : public ofxSvgElement { + friend class ofxSvg; +public: + class TextSpan { + friend class ofxSvgText; + public: + TextSpan() { + text = ""; + lineHeight = 0; + } + + std::string getFontKey() { + return ofxSvgFontBook::getFontKey(getFontFamily(), isBold(), isItalic()); + } + + std::string getFontFamily() { + return mSvgCssClass.getFontFamily("Arial"); + } + void setFontFamily(std::string aFontFamily) { + mSvgCssClass.setFontFamily(aFontFamily); + } + + int getFontSize() { + return mSvgCssClass.getFontSize(12); + } + void setFontSize( int asize ) { + mSvgCssClass.setFontSize(asize); + } + + ofColor getColor() { + auto tcolor = mSvgCssClass.getColor("color"); + tcolor.a *= alpha; + return tcolor; + } + + void setColor( const ofColor& acolor ) { + mSvgCssClass.setColor(acolor); + } + + bool isBold() { + return mSvgCssClass.isFontBold(); + } + void setBold( bool ab ) { + mSvgCssClass.setFontBold(ab); + } + + bool isItalic() { + return mSvgCssClass.isFontItalic(); + } + void setItalic( bool ab ) { + mSvgCssClass.setFontItalic(ab); + } + + std::string getText() { + return text; + } + + void setText( const std::string& atext ) { + mBTextDirty = true; + text = atext; + } + + ofRectangle rect; + float lineHeight = 0; + float alpha = 1.f; + + void applyStyle(ofxSvgCssClass& aclass); + ofxSvgCssClass& getCssClass() { return mSvgCssClass; } + + ofTrueTypeFont& getFont(); + void draw( const std::string& astring, bool abCentered ); + void draw(const std::string &astring, const ofColor& acolor, bool abCentered ); + + protected: + ofRectangle fontRect; + std::string text; + ofxSvgCssClass mSvgCssClass; + bool mBTextDirty = true; + }; + + + ofxSvgText() = default; + + // Deep-copy constructor + ofxSvgText(const ofxSvgText& other) { + setPosition(other.getPosition()); + setOrientation(other.getOrientationQuat()); + setScale(other.getScale()); + + layer = other.layer; + bVisible = other.bVisible; + alpha = other.alpha; + name = other.name; + bUseShapeColor = other.bUseShapeColor; + + fdirectory = other.fdirectory; + bCentered = other.bCentered; + mSvgCssClass = other.mSvgCssClass; + + textSpans.reserve(other.textSpans.size()); + for (const auto& ptr : other.textSpans) { + // Create a new shared_ptr to a new Item copy or nullptr + textSpans.push_back(ptr ? std::make_shared(*ptr) : nullptr); + } + + create(); + } + + // Deep-copy assignment + ofxSvgText& operator=(const ofxSvgText& other) { + if (this != &other) { + setPosition(other.getPosition()); + setOrientation(other.getOrientationQuat()); + setScale(other.getScale()); + + layer = other.layer; + bVisible = other.bVisible; + alpha = other.alpha; + name = other.name; + bUseShapeColor = other.bUseShapeColor; + + fdirectory = other.fdirectory; + bCentered = other.bCentered; + mSvgCssClass = other.mSvgCssClass; + + textSpans.clear(); + textSpans.reserve(other.textSpans.size()); + for (const auto& ptr : other.textSpans) { + // Create a new shared_ptr to a new Item copy or nullptr + textSpans.push_back(ptr ? std::make_shared(*ptr) : nullptr); + } + create(); + } + return *this; + } + + + + virtual ofxSvgType getType() override {return OFXSVG_TYPE_TEXT;} + + ofTrueTypeFont& getFont(); + + void setText( const std::string& astring, std::string aFontFamily, int aFontSize, float aMaxWidth ); + void setText( const std::string& astring, const ofxSvgCssClass& aSvgCssClass, float aMaxWidth ); + void create(); + void customDraw() override; + // going to override +// void draw(const std::string &astring, bool abCentered ); +// void draw(const std::string &astring, const ofColor& acolor, bool abCentered ); + + void setFontDirectory( const of::filesystem::path& aPath ) { + fdirectory = aPath; + } + of::filesystem::path getFontDirectory() { + return fdirectory; + } + + std::string getText() { + std::string ttext; + for( auto& tspan : textSpans ) { + ttext += tspan->getText(); + } + return ttext; + } + + void setColor( ofColor acolor ) { + mSvgCssClass.setColor(acolor); + } + + /// \brief Apply css class to the element. Text spans will use this style unless overridden by their css. + virtual void applyStyle(ofxSvgCssClass& aclass) override { + mSvgCssClass = aclass; + } + + ofColor getColor() { + auto tcolor = mSvgCssClass.getColor("color", ofColor(0)); + tcolor.a *= alpha; + return tcolor; + } + + void setCentered(bool ab) { + if( ab != bCentered ) { + bCentered = ab; + create(); + } + } + +/// \brief Get the local bounding box of all of the text spans. + virtual ofRectangle getBoundingBox() override; + + std::map< std::string, std::map > meshes; + std::vector< std::shared_ptr > textSpans; + + bool areTextSpansDirty(); + +protected: + of::filesystem::path fdirectory; + + bool bCentered = false; + + ofxSvgCssClass mSvgCssClass; + + virtual std::shared_ptr clone() override { + auto newEle = std::make_shared(*this); + newEle->textSpans.clear(); + newEle->textSpans.reserve(textSpans.size()); + for (const auto& ptr : textSpans) { + // Create a new shared_ptr to a new Item copy or nullptr + // lets not copy over the nullptrs + if( ptr ) { + newEle->textSpans.push_back(std::make_shared(*ptr)); + } + } + return newEle; + }; + + struct SpanData { + std::string style; + std::string content; + }; + + std::vectorsplitBySpanTags(const std::string& input); + SpanData extractSpanData(const std::string& spanTag); + bool endsWithLineEnding(const std::string& astr); + std::vector splitWordsAndLineEndings(const std::string& input); + +}; + + + + + + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgFontBook.cpp b/addons/ofxSvg/src/ofxSvgFontBook.cpp new file mode 100644 index 00000000000..f9514a48e10 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgFontBook.cpp @@ -0,0 +1,305 @@ +#include "ofxSvgFontBook.h" + +using std::string; + +std::string ofxSvgFontBook::mFontDirectory; +ofTrueTypeFont ofxSvgFontBook::defaultFont; +std::map< string, ofxSvgFontBook::Font > ofxSvgFontBook::fonts; + +ofxSvgFontBook::Font ofxSvgFontBook::defaultBookFont; + +//-------------------------------------------------------------- +bool ofxSvgFontBook::loadFont(const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ) { + return loadFont(mFontDirectory, aFontFamily, aFontSize, aBBold, aBItalic ); +} + +//-------------------------------------------------------------- +bool ofxSvgFontBook::loadFont(const of::filesystem::path& aDirectory, const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ) { + ofxSvgCssClass css; + css.setFontFamily(aFontFamily); + css.setFontSize(aFontSize); + css.setFontBold(aBBold); + css.setFontItalic(aBItalic); + return loadFont( aDirectory, css ); +} + +//-------------------------------------------------------------- +bool ofxSvgFontBook::loadFont(const of::filesystem::path& aDirectory, ofxSvgCssClass& aCssClass ) { + auto fontFamily = aCssClass.getFontFamily("Arial"); + auto fontSize = aCssClass.getFontSize(12); + bool bBold = aCssClass.isFontBold(); + bool bItalic = aCssClass.isFontItalic(); + + ofLogVerbose("ofxFontBook") << "checking font: " << fontFamily << " bold: " << bBold << " italic: " << bItalic << " fkey: "; + auto fkey = ofxSvgFontBook::getFontKey(fontFamily, bBold, bItalic ); + + ofLogVerbose("ofxFontBook") << "checking font: " << fontFamily << " bold: " << bBold << " italic: " << bItalic; + + if( fonts.count(fkey) == 0 ) { + Font tafont; + tafont.fontFamily = fontFamily; + tafont.bold = bBold; + tafont.italic = bItalic; + fonts[fkey] = tafont; + } + bool bFontLoadOk = true; + + Font& tfont = fonts[ fkey ]; + if (tfont.sizes.count(fontSize) == 0) { + bool bHasFontDirectory = false; + // cout << "checking directory: " << fdirectory+"/fonts/" << endl; + std::string fontsDirectory = "";// = ofToDataPath("", true); + if( !aDirectory.empty() ) { + fontsDirectory = aDirectory.string(); + } + + if( !ofFile::doesFileExist(fontsDirectory)) { + if( ofFile::doesFileExist(mFontDirectory)) { + fontsDirectory = mFontDirectory; + } + } + +// if( !ofFile::doesFileExist(fontsDirectory)) { +//// fs::path fontPath = fs::path(getenv("HOME")) / "Library" / "Fonts"; +// #if defined(TARGET_OSX) +// std::filesystem::path fontPath = std::filesystem::path(getenv("HOME")) / "Library" / "Fonts"; +// if( ofDirectory::doesDirectoryExist(fontPath) ) { +// fontsDirectory = ofToDataPath(fontPath, true); +// } +// #endif +// } + if( !ofFile::doesFileExist( fontsDirectory )) { + fontsDirectory = ofToDataPath("", true); + } + + if( ofFile::doesFileExist( fontsDirectory )) { + bHasFontDirectory = true; + } + + std::vector fontNamesToSearch = {fontFamily}; + // sometimes there are fallback fonts included with a comma separator + if( ofIsStringInString(fontFamily, ",")) { + std::vector splitNames = ofSplitString(fontFamily, ",", true, true); + for( auto& sname : splitNames ) { + // remove spaces + ofStringReplace(sname, " ", "" ); + } +// fontNamesToSearch.insert(fontNamesToSearch.end(), splitNames.begin(), splitNames.end()); + fontNamesToSearch = splitNames; + } + + + +// string _filename, int _fontSize, bool _bAntiAliased, bool _bFullCharacterSet, bool _makeContours, float _simplifyAmt, int _dpi + // first let's see if the fonts are provided. Some system fonts are .dfont that have several of the faces + // in them, but OF isn't setup to parse them, so we need each bold, regular, italic, etc to be a .ttf font // + string tfontPath = tfont.fontFamily; + if (bHasFontDirectory) { + std::stringstream fs; + bool bf = true; + for( auto& fname : fontNamesToSearch ) { + if( !bf ) { + fs << ", "; + } + fs << fname; + bf = false; + } + + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " : " << fs.str() << " : starting off searching directory : " << fontsDirectory; + string tNewFontPath = ""; + + std::vector subStrs; + std::vector excludeStrs; + if( bBold ) { + subStrs.push_back("bold"); + } else { + excludeStrs.push_back("bold"); + } + if( bItalic ) { + subStrs.push_back("italic"); + } else { + excludeStrs.push_back("italic"); + } + + bool bMightHaveFoundTheFont = false; + ofLogVerbose("ofxSvgFontBook") << "trying to load font: " << tfont.fontFamily << " bold: " << bBold << " italic: " << bItalic; +// bool bFoundTheFont = _recursiveFontDirSearch(fontsDirectory, tfont.fontFamily, tNewFontPath, subStrs, excludeStrs, 0); + for( auto& fontFam : fontNamesToSearch ) { + bool bFoundTheFont = _recursiveFontDirSearch(fontsDirectory, fontFam, tNewFontPath, subStrs, excludeStrs, 0); + if (bFoundTheFont) { + tfontPath = tNewFontPath; + bMightHaveFoundTheFont = true; + ofLogVerbose("ofxSvgFontBook") << "Found the font at " << tfontPath; + break; + } + } + + if( !bMightHaveFoundTheFont ) { + // search the system level // + #if defined(TARGET_OSX) + std::filesystem::path fontPath = std::filesystem::path(getenv("HOME")) / "Library" / "Fonts"; + if( ofDirectory::doesDirectoryExist(fontPath) ) { + fontsDirectory = ofToDataPath(fontPath, true); + + for( auto& fontFam : fontNamesToSearch ) { + bool bFoundTheFont = _recursiveFontDirSearch(fontsDirectory, fontFam, tNewFontPath, subStrs, excludeStrs, 0); + if (bFoundTheFont) { + tfontPath = tNewFontPath; + bMightHaveFoundTheFont = true; + ofLogVerbose("ofxSvgFontBook") << "Found the font at " << tfontPath; + break; + } + } + + } + #endif + } + + if( !bMightHaveFoundTheFont ) { + // search the system level // + #if defined(TARGET_OSX) + std::filesystem::path fontPath = "/Library/Fonts"; + if( ofDirectory::doesDirectoryExist(fontPath) ) { + fontsDirectory = ofToDataPath(fontPath, true); + + for( auto& fontFam : fontNamesToSearch ) { + bool bFoundTheFont = _recursiveFontDirSearch(fontsDirectory, fontFam, tNewFontPath, subStrs, excludeStrs, 0); + if (bFoundTheFont) { + tfontPath = tNewFontPath; + bMightHaveFoundTheFont = true; + ofLogVerbose("ofxSvgFontBook") << "Found the font at " << tfontPath; + break; + } + } + + } + #endif + } + + /*ofDirectory tfDir; + tfDir.listDir( fontsDirectory ); + for( int ff = 0; ff < tfDir.size(); ff++ ) { + ofFile tfFile = tfDir.getFile(ff); + if( tfFile.getExtension() == "ttf" || tfFile.getExtension() == "otf" ) { + cout << ff << " - font family: " << tfont.fontFamily << " file name: " << tfFile.getBaseName() << endl; + if( ofToLower(tfFile.getBaseName()) == ofToLower(tfont.fontFamily) ) { + ofLogNotice(" >> ofxSvgText found font file for " ) << tfont.fontFamily; + tfontPath = tfFile.getAbsolutePath(); + break; + } + } + }*/ + } + + + + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " : Trying to load font from: " << tfontPath; + + if (tfontPath == "") { + bFontLoadOk = false; + } else { + // load(const std::string& _filename, int _fontSize, bool _bAntiAliased, bool _bFullCharacterSet, bool _makeContours, float _simplifyAmt, int _dpi) + bFontLoadOk = tfont.sizes[fontSize].load(tfontPath, fontSize, true, true, false, 0.5, 72); + if( bFontLoadOk && tfont.pathToFont.empty() ) { + tfont.pathToFont = tfontPath; + } + } + if(bFontLoadOk) { +// tfont.sizes[ vIt->first ].setSpaceSize( 0.57 ); +// tfont.sizes[ vIt->first ] = datFontTho; + tfont.textures[ fontSize ] = tfont.sizes[ fontSize ].getFontTexture(); + } else { + ofLogError("ofxSvgFontBook") << __FUNCTION__ << " : error loading font family: " << tfont.fontFamily << " size: " << fontSize; + tfont.sizes.erase(fontSize); + } + } + return bFontLoadOk; +} + +//-------------------------------------------------------------- +bool ofxSvgFontBook::_recursiveFontDirSearch(const string& afile, const string& aFontFamToLookFor, string& fontpath, + const std::vector& aAddNames, + const std::vector& aExcludeNames, + int aNumRecursions) { + if (fontpath != "") { + return true; + } + int numRecursions = aNumRecursions+1; + if( numRecursions > 20 ) { + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " too many recursions, aborting."; + return false; + } + ofFile tfFile( afile, ofFile::Reference ); + if (tfFile.isDirectory()) { + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " : searching in directory : " << afile << " | " << ofGetFrameNum(); + ofDirectory tdir; + tdir.listDir(afile); + tdir.sort(); + for (std::size_t i = 0; i < tdir.size(); i++) { + bool bFontFound = _recursiveFontDirSearch(tdir.getPath(i), aFontFamToLookFor, fontpath, aAddNames, aExcludeNames, numRecursions); + if( bFontFound ) { + return true; + } + } + tdir.close(); + } else { + if ( tfFile.getExtension() == "ttf" || tfFile.getExtension() == "otf") { + auto tfbase = ofToLower(tfFile.getBaseName()); + auto fontFamLower = ofToLower(aFontFamToLookFor); + + if( aAddNames.size() > 0 || aExcludeNames.size() > 0 ) { + + if(ofIsStringInString(tfbase, fontFamLower)) { + bool bAllFound = true; + for( auto& subName : aAddNames ) { + if( !ofIsStringInString(tfbase, ofToLower(subName))) { +// ofLogNotice("ofxSvgFontBook") << __FUNCTION__ << " add name not found: " << subName << " font: " << fontFamLower; + bAllFound = false; + break; + } + } + if(aExcludeNames.size() > 0 ) { + for( auto& subName : aExcludeNames ) { + if( ofIsStringInString(tfbase, ofToLower(subName))) { +// ofLogNotice("ofxSvgFontBook") << __FUNCTION__ << " exclude name not found: " << subName << " font: " << fontFamLower; + bAllFound = false; + break; + } + } + } + +// ofLogNotice("ofxSvgFontBook") << __FUNCTION__ << " checking font fam: " << fontFamLower << " file: " << tfbase << " allfound: " << bAllFound; + + if(bAllFound) { + fontpath = tfFile.getAbsolutePath(); + return true; + } + } + } else { + if (ofToLower( tfFile.getBaseName() ) == fontFamLower) { + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " : found font file for " << aFontFamToLookFor; + fontpath = tfFile.getAbsolutePath(); + return true; + } + string tAltFileName = ofToLower(tfFile.getBaseName()); + ofStringReplace(tAltFileName, " ", "-"); + if (tAltFileName == fontFamLower) { + ofLogVerbose("ofxSvgFontBook") << __FUNCTION__ << " : found font file for " << aFontFamToLookFor; + fontpath = tfFile.getAbsolutePath(); + return true; + } + } + } + } + return false; +} + +//-------------------------------------------------------------- +ofTrueTypeFont& ofxSvgFontBook::getFontForKey( const std::string& aFontKey, int aFontSize ) { + if( fonts.count(aFontKey) > 0 ) { + if( fonts[aFontKey].sizes.count(aFontSize) > 0 ) { + return fonts[aFontKey].sizes[aFontSize]; + } + } + return defaultFont; +} diff --git a/addons/ofxSvg/src/ofxSvgFontBook.h b/addons/ofxSvg/src/ofxSvgFontBook.h new file mode 100644 index 00000000000..9e3c1e70f3b --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgFontBook.h @@ -0,0 +1,74 @@ +#pragma once +#include "ofTrueTypeFont.h" +#include +#include "ofxSvgCss.h" + +/// @brief This class is mostly for internal use. +/// Used by the ofxSvgText elements for managing fonts +class ofxSvgFontBook { +public: + class Font { + public: + std::string fontFamily; + std::map< int, ofTrueTypeFont > sizes; + std::map< int, ofTexture > textures; + bool bold = false; + bool italic = false; + of::filesystem::path pathToFont; + }; + + static void setFontDirectory( std::string adir ) { + mFontDirectory = adir; + } + + static bool loadFont(const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ); + static bool loadFont(const of::filesystem::path& aDirectory, ofxSvgCssClass& aCssClass ); + static bool loadFont(const of::filesystem::path& aDirectory, const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ); + + static std::string getFontKey( const std::string& aFontFamily, bool aBBold, bool aBItalic ) { + auto fkey = aFontFamily; + if( aBBold ) {fkey += "_bold";} + if( aBItalic) {fkey += "_italic";} + return fkey; + } + + static bool hasFont(const std::string& aFontFamily, int aFontSize, bool aBBold, bool aBItalic ) { + auto fkey = getFontKey(aFontFamily, aBBold, aBItalic); + return hasFontForKey(fkey, aFontSize); + } + + static bool hasFontForKey(const std::string& aFontKey, int aFontSize ) { + if( fonts.count(aFontKey) > 0 ) { + auto& font = fonts[aFontKey]; + if( font.sizes.count(aFontSize) > 0 ) { + return true; + } + } + return false; + } + + static bool hasBookFont( const std::string& aFontKey ) { + return ( fonts.count(aFontKey) > 0 ); + } + + static Font& getBookFont( const std::string& aFontKey ) { + if( fonts.count(aFontKey) > 0 ) { + return fonts[aFontKey]; + } + return defaultBookFont; + } + + static ofTrueTypeFont& getFontForKey( const std::string& aFontKey, int aFontSize ); + + static ofTrueTypeFont defaultFont; + +protected: + static Font defaultBookFont; + static std::string mFontDirectory; + static std::map< std::string, Font > fonts; + static bool _recursiveFontDirSearch(const std::string& afile, const std::string& aFontFamToLookFor, + std::string& fontpath, + const std::vector& aAddNames, + const std::vector& aExcludeNames, + int aNumRecursions); +}; diff --git a/addons/ofxSvg/src/ofxSvgGroup.cpp b/addons/ofxSvg/src/ofxSvgGroup.cpp new file mode 100755 index 00000000000..acd0bdea745 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgGroup.cpp @@ -0,0 +1,352 @@ +#include "ofxSvgGroup.h" +#include "ofGraphics.h" + +using std::vector; +using std::shared_ptr; +using std::string; + +//-------------------------------------------------------------- +void ofxSvgGroup::draw() const { + if( !isVisible() ) return; + + std::size_t numElements = mChildren.size(); + for( std::size_t i = 0; i < numElements; i++ ) { + mChildren[i]->draw(); + } +} + +//-------------------------------------------------------------- +std::size_t ofxSvgGroup::getNumChildren() { + return mChildren.size(); +} + +//-------------------------------------------------------------- +vector< shared_ptr >& ofxSvgGroup::getChildren() { + return mChildren; +} + +//-------------------------------------------------------------- +vector< shared_ptr > ofxSvgGroup::getAllElements(bool aBIncludeGroups) { + vector< shared_ptr > retElements; + + for( auto ele : mChildren ) { + _getAllElementsRecursive( retElements, ele, aBIncludeGroups ); + } + + return retElements; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getAllGroups() { + vector< shared_ptr > retGroups; + for( auto ele : mChildren ) { + _getAllGroupsRecursive( retGroups, ele ); + } + return retGroups; +} + +// flattens out hierarchy // +//-------------------------------------------------------------- +void ofxSvgGroup::_getAllElementsRecursive( vector< shared_ptr >& aElesToReturn, shared_ptr aele, bool aBIncludeGroups ) { + if( aele ) { + if( aele->isGroup() ) { + auto tgroup = std::dynamic_pointer_cast(aele); + if(aBIncludeGroups) {aElesToReturn.push_back(tgroup);} + for( auto ele : tgroup->getChildren() ) { + _getAllElementsRecursive( aElesToReturn, ele, aBIncludeGroups ); + } + } else { + aElesToReturn.push_back( aele ); + } + } +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_getAllGroupsRecursive( std::vector< std::shared_ptr >& aGroupsToReturn, std::shared_ptr aele ) { + if( aele ) { + if( aele->isGroup() ) { + auto tgroup = std::dynamic_pointer_cast(aele); + aGroupsToReturn.push_back(tgroup); + for( auto ele : tgroup->getChildren() ) { + if( ele->isGroup() ) { + _getAllGroupsRecursive( aGroupsToReturn, ele ); + } + } + } + } +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getAllElementsWithPath() { + auto allKids = getAllElements(false); + std::vector< std::shared_ptr > rpaths; + for( auto kid : allKids ) { + if( kid->getType() == OFXSVG_TYPE_RECTANGLE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == OFXSVG_TYPE_PATH ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == OFXSVG_TYPE_CIRCLE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == OFXSVG_TYPE_ELLIPSE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } + } + + return rpaths; +} + +//-------------------------------------------------------------- +shared_ptr ofxSvgGroup::getElement( std::string aPath, bool bStrict ) { + + vector< std::string > tsearches; + if( ofIsStringInString( aPath, ":" ) ) { + tsearches = ofSplitString( aPath, ":" ); + } else { + tsearches.push_back( aPath ); + } + + shared_ptr temp; + _getElementForNameRecursive( tsearches, temp, mChildren, bStrict ); + return temp; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getAllElementsForName( const std::string& aname, bool bStrict ) { + std::vector< std::shared_ptr > relements; + auto allElements = getAllElements(true); + for( auto& ele : allElements ) { + if( bStrict ) { + if( ele->getCleanName() == aname ) { + relements.push_back(ele); + } + } else { + if( ofIsStringInString( ele->getCleanName(), aname )) { + relements.push_back(ele); + } + } + } + return relements; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getChildrenForName( const std::string& aname, bool bStrict ) { + std::vector< std::shared_ptr > relements; + for( auto& kid : mChildren ) { + if( bStrict ) { + if( kid->getCleanName() == aname ) { + relements.push_back(kid); + } + } else { + if( ofIsStringInString( kid->getCleanName(), aname )) { + relements.push_back(kid); + } + } + } + return relements; +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_getElementForNameRecursive( vector& aNamesToFind, shared_ptr& aTarget, vector< shared_ptr >& aElements, bool bStrict ) { + + if( aNamesToFind.size() < 1 ) { + return; + } + if(aTarget) { + return; + } + + bool bKeepGoing = false; + std::string nameToFind = aNamesToFind[0]; + if( aNamesToFind.size() > 1 ) { + if( aNamesToFind[0] == "*" ) { + bKeepGoing = true; + nameToFind = aNamesToFind[1]; + } + } + for( std::size_t i = 0; i < aElements.size(); i++ ) { + bool bFound = false; + if(bStrict) { + if( aElements[i]->getName() == nameToFind ) { + bFound = true; + } + } else { +// std::cout << "Group::_getElementForNameRecursive: ele name: " << aElements[i]->getName() << " nameToFind: " << nameToFind << " keep going: " << bKeepGoing << std::endl; + if( ofIsStringInString( aElements[i]->getName(), nameToFind )) { + bFound = true; + } + + if (!bFound && aElements[i]->getType() == OFXSVG_TYPE_TEXT) { + + if (aElements[i]->getName() == "No Name") { + // the ids for text block in illustrator are weird, + // so try to grab the name from the text contents // + auto etext = std::dynamic_pointer_cast(aElements[i]); + if (etext) { + if (etext->textSpans.size()) { + if(ofIsStringInString( etext->textSpans.front()->getText(), aNamesToFind[0] )) { + bFound = true; + } + } + } + } + } + } + + if( bFound && !bKeepGoing ) { + if( !bKeepGoing && aNamesToFind.size() > 0 ) { + aNamesToFind.erase( aNamesToFind.begin() ); + } + if(aNamesToFind.size() == 0 ) { + aTarget = aElements[i]; + break; + } else { + if( aElements[i]->getType() == OFXSVG_TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _getElementForNameRecursive( aNamesToFind, aTarget, tgroup->getChildren(), bStrict ); + break; + } + } + } + + if( bKeepGoing ) { + if( bFound ) { +// std::cout << "Group::_getElementForNameRecursive: SETTING TARGET: " << aElements[i]->getName() << " keep going: " << bKeepGoing << std::endl; + aTarget = aElements[i]; + break; + } else { + if( aElements[i]->getType() == OFXSVG_TYPE_GROUP ) { +// std::cout << "Group::_getElementForNameRecursive: FOUND A GROUP, But still going: " << aElements[i]->getName() << " keep going: " << bKeepGoing << std::endl; + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _getElementForNameRecursive( aNamesToFind, aTarget, tgroup->getChildren(), bStrict ); + } + } + } + } +} + +//-------------------------------------------------------------- +bool ofxSvgGroup::replace( shared_ptr aOriginal, shared_ptr aNew ) { + bool bReplaced = false; + _replaceElementRecursive( aOriginal, aNew, mChildren, bReplaced ); + return bReplaced; +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_replaceElementRecursive( shared_ptr aTarget, shared_ptr aNew, vector< shared_ptr >& aElements, bool& aBSuccessful ) { + for( std::size_t i = 0; i < aElements.size(); i++ ) { + bool bFound = false; + if( aTarget == aElements[i] ) { + bFound = true; + aBSuccessful = true; + aElements[i] = aNew; + aNew->layer = aTarget->layer; + if( aTarget->getParent() ) { + aNew->setParent(*aTarget->getParent(), true); + aTarget->clearParent(); + } + break; + } + if( !bFound ) { + if( aElements[i]->getType() == OFXSVG_TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _replaceElementRecursive(aTarget, aNew, tgroup->mChildren, aBSuccessful ); + } + } + } +} + +//-------------------------------------------------------------- +bool ofxSvgGroup::remove( std::shared_ptr aelement ) { + if( !aelement ) { + ofLogWarning("ofxSvgGroup::remove") << "element is invalid."; + return false; + } + bool bRemoved = false; + _removeElementRecursive( aelement, mChildren, bRemoved ); + return bRemoved; +} + +////-------------------------------------------------------------- +//bool ofxSvgGroup::remove( std::vector > aelements ) { +// if( aelements.size() < 1 ) { +// return false; +// } +// +// bool bAllRemoved = true; +// for( auto& aele : aelements ) { +// if( !aele ) { +// ofLogWarning("ofxSvgGroup::remove") << "element is invalid."; +// bAllRemoved = false; +// continue; +// } +// bool bEleRemoved = remove( aele ); +// if( !bEleRemoved ) { +// bAllRemoved = false; +// } +// } +// return bAllRemoved; +//} + +//-------------------------------------------------------------- +void ofxSvgGroup::_removeElementRecursive( shared_ptr aTarget, vector< shared_ptr >& aElements, bool& aBSuccessful ) { + + for( std::size_t i = 0; i < aElements.size(); i++ ) { + bool bFound = false; + if( aTarget == aElements[i] ) { + bFound = true; + aBSuccessful = true; + aElements.erase(aElements.begin() + i); + // make sure to break here since the size() will change + break; + } + if( !bFound ) { + if( aElements[i]->getType() == OFXSVG_TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _removeElementRecursive(aTarget, tgroup->mChildren, aBSuccessful ); + } + } + } +} + +//-------------------------------------------------------------- +string ofxSvgGroup::toString(int nlevel) { + + string tstr = ""; + for( int k = 0; k < nlevel; k++ ) { + tstr += " "; + } + tstr += getTypeAsString() + " - " + getName() + "\n"; + + if( mChildren.size() ) { + for( std::size_t i = 0; i < mChildren.size(); i++ ) { + tstr += mChildren[i]->toString( nlevel+1); + } + } + + return tstr; +} + + +//-------------------------------------------------------------- +void ofxSvgGroup::disableColors() { + auto telements = getAllElements(false); + for( auto& ele : telements ) { + ele->setUseColors(false); + } +} + +//-------------------------------------------------------------- +void ofxSvgGroup::enableColors() { + auto telements = getAllElements(false); + for( auto& ele : telements ) { + ele->setUseColors(true); + } +} + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgGroup.h b/addons/ofxSvg/src/ofxSvgGroup.h new file mode 100755 index 00000000000..753d11b239d --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgGroup.h @@ -0,0 +1,427 @@ +#pragma once +#include "ofxSvgElements.h" + +class ofxSvgGroup : public ofxSvgElement { +public: + + virtual ofxSvgType getType() override {return OFXSVG_TYPE_GROUP;} + + ofxSvgGroup() = default; + + /// \brief Deep-copy constructor. Clone the children so that the new group does not have references to the children in this class. + ofxSvgGroup(const ofxSvgGroup& other) { + ofLogVerbose("ofxSvgGroup") << "ofxSvgGroup(const ofxSvgGroup& other)"; + mChildren.reserve(other.mChildren.size()); + + setPosition(other.getPosition()); + setOrientation(other.getOrientationQuat()); + setScale(other.getScale()); + + bVisible = other.bVisible; + alpha = other.alpha; + layer = other.layer; + name = other.name; + for (const auto& ptr : other.mChildren) { + // Create a new shared_ptr to a new item copy. + if( ptr ) { + auto newKid = ptr->clone(); + newKid->setParent(*this); + mChildren.push_back(newKid); + } + } + } + + /// \brief Deep-copy assignment. Clone / create copies of the children from other. + ofxSvgGroup& operator=(const ofxSvgGroup& other) { + if (this != &other) { + ofLogVerbose("ofxSvgGroup") << "operator=(const ofxSvgGroup& other)"; + + setPosition(other.getPosition()); + setOrientation(other.getOrientationQuat()); + setScale(other.getScale()); + + bVisible = other.bVisible; + alpha = other.alpha; + layer = other.layer; + name = other.name; + mChildren.clear(); + mChildren.reserve(other.mChildren.size()); + for (const auto& ptr : other.mChildren) { + // Create a new shared_ptr to a new item copy. + if( ptr ) { + auto newKid = ptr->clone(); + newKid->setParent(*this); + mChildren.push_back(newKid); + } + } + } + return *this; + } + + // we need to override this so that we can draw the children correctly + // without this transform getting applied to the children + virtual void draw() const override; + + /// \brief Set the visibility of the group. Does not set visibility of each child. The group only draws its children if the group is visible. + /// \param bool aBVisible set to true for visible. +// virtual void setVisible( bool aBVisible ) override { +// ofxSvgElement::setVisible(aBVisible); +// } + + /// \brief Set the alpha of the group and call setAlpha(aAlpha) on its children. + /// \param float aAlpha in range from 0-1 where 0 is transparent and 1 is full opacity. + virtual void setAlpha( float aAlpha ) override { + alpha = aAlpha; + for( auto& kid : mChildren ) { + kid->setAlpha( aAlpha ); + } + } + + /// \brief Get the bounding box of all of the elements in this group. + /// \return ofRectangle encapsulating all of the elements in the group. + virtual ofRectangle getBoundingBox() override { + ofRectangle rrect; + bool bFirstRect = true; + auto allEs = getAllElements(false); + for( auto& ele : allEs ) { + auto erect = ele->getBoundingBox(); + if( erect.width > 0 && erect.height > 0 ) { + if(bFirstRect) { + rrect = erect; + } else { + rrect.growToInclude(erect); + } + bFirstRect = false; + } + } + return rrect; + }; + + /// \brief Get the number of immediate children in this group. + /// \return std::size_t number of chilren. + std::size_t getNumChildren(); + /// \brief Get immediate children in this group. + /// \return std::vector< std::shared_ptr > all of the children in this group as a reference. + std::vector< std::shared_ptr >& getChildren(); + /// \brief Get all of the elements under this group recursively creating a flattened vector. If groups are included, + /// the returned groups will point to shared_ptrs in their children but also in the returned vector. + /// \param bool aBIncludeGroups true if groups should be included in the returning vector. + /// \return std::vector< std::shared_ptr > as a flat vector. + std::vector< std::shared_ptr > getAllElements(bool aBIncludeGroups); + /// \brief Get all of the groups under this group recursively creating a flattened vector. + /// \return std::vector< std::shared_ptr > of all the groups. + std::vector< std::shared_ptr > getAllGroups(); + + + + /* + /// -- Search Functions with aPath argument for searching and retrieving elements. -- + + /// \param std::string aPath to the element separated by semi colons and ending with the name of the desired element. + /// If empty, will search children. + /// For example: "MyGroup:MyElement" will attempt to retrieve an element with name "MyElement" from inside the group "MyGroup" + + /// Wildcards are also acceptable and will recursively search until the element with the name is found. + /// For example: "*:MyElement" will recursively search for the element "MyElement". + + /// \param bool bStrict if true, will include elements with a name that matches the final component of the apath. + /// If bStrict is false, will include elements that contain the final component of the aPath. + /// -- For example: Argument aPath = "MyElement" -- + /// Argument bStrict = true; Will NOT return an element with name "MyElement2". + /// Argument bStrict = false; Will return an element with name "MyElement2". + */ + + + + + /// \brief Retrieve an element at a path, casted to a type. See notes above about search functions. + /// Example call auto myEle = group->get("MyRectangle", false); + /// \param std::string aPath to the element separated by semi colons and ending with the name of the desired element. If empty, will search children. + /// \param bool bStrict if true, search only elements with a name that matches the final element of the aPath component. + /// \return std::shared_ptr< ofxSvg_T > and invalid if not found. + template + std::shared_ptr< ofxSvg_T > get( std::string aPath, bool bStrict = false ) { + auto stemp = getElement( aPath, bStrict ); + if( !stemp ) { + return std::shared_ptr(); + } + return std::dynamic_pointer_cast( stemp ); + } + + /// \brief Retrieve an element from the vector of children. + /// \param int aIndex the index to the element in the vector of children. + /// \return std::shared_ptr< ofxSvg_T > and invalid if not found. + template + std::shared_ptr< ofxSvg_T > get( int aIndex ) { + if( aIndex > -1 && aIndex < mChildren.size() ) { + return std::dynamic_pointer_cast( mChildren[ aIndex ] ); + } + return std::shared_ptr< ofxSvg_T >(); + } + + /// \brief Retrieve elements in a group of a specific type. See notes above about search functions. + /// \param std::string aPathToGroup path to the group to be searched separated by semi colons and ending with the name of the desired group. + /// If empty, will search children. + /// \param bool bStrict if true, search only elements with a name that matches the final element of the aPath component. + /// \return std::vector< std::shared_ptr > casted to type. + template + std::vector< std::shared_ptr > getElementsForType( std::string aPathToGroup, bool bStrict= false ) { + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > telements; + std::vector< std::shared_ptr > elementsToSearch; + if( aPathToGroup == "" ) { + elementsToSearch = mChildren; + } else { + auto temp = getElement( aPathToGroup, bStrict ); + if( temp ) { + if( temp->isGroup() ) { + std::shared_ptr< ofxSvgGroup > tgroup = std::dynamic_pointer_cast( temp ); + elementsToSearch = tgroup->mChildren; + } + } + } + + if( !elementsToSearch.size() && mChildren.size() ) { + ofLogNotice("ofx::svg::Group") << __FUNCTION__ << " did not find group with name: " << aPathToGroup; + elementsToSearch = mChildren; + } + + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( elementsToSearch[i]->getType() == sType ) { + telements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + return telements; + } + + /// \brief Retrieve the first element in a group of a specific type. See notes above about search functions. + /// \param std::string aPathToGroup path to group to be searched separated by semi colons and ending with the name of the desired group. + /// If empty, will search children. + /// \param bool bStrict if true, search only elements with a name that matches the final element of the aPath component. + /// \return std::shared_ptr casted to type and invalid if not found. + template + std::shared_ptr getFirstElementForType( std::string aPathToGroup, bool bStrict= false ) { + auto eles = getElementsForType(aPathToGroup, bStrict ); + if( eles.size() > 0 ) { + return eles[0]; + } + return std::shared_ptr(); + } + + + /// \brief Retrieve all of the elements recursively in the group of a specific type. + /// \return std::vector< std::shared_ptr > casted to type. + template + std::vector< std::shared_ptr > getAllElementsForType() { + + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > telements; + auto elementsToSearch = getAllElements(true); + + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( elementsToSearch[i]->getType() == sType ) { + telements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + return telements; + } + + /// \brief Retrieve all of the elements recursively in the group of a specific type filtered by the name provided. + /// \param std::string aname Name to be matched against the elements name. + /// \param bool bStrict if true, the element name mush match aname, if false, the element name must be contained in aname. + /// \return std::vector< std::shared_ptr > casted to type. + template + std::vector< std::shared_ptr > getAllElementsForTypeForName(std::string aname, bool bStrict = false) { + std::vector< std::shared_ptr > relements; + auto elementsToSearch = getAllElementsForType(); + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( bStrict ) { + if( elementsToSearch[i]->getCleanName() == aname ) { + relements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } else { + if( ofIsStringInString(elementsToSearch[i]->getCleanName(), aname) ) { + relements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + } + + return relements; + } + + /// \brief Retrieve all of the elements recursively in the group that have a path. + /// Including ofxSvgPath, ofxSvgRectangle, ofxSvgCircle, ofxSvgEllipse + /// \return std::vector< std::shared_ptr >. + std::vector< std::shared_ptr > getAllElementsWithPath(); + + /// \brief Retrieve an element in this group for a path. See notes above about search functions. + /// \param aPath to the element separated by semi colons and ending with the name of the desired element. + /// \param bool bStrict if true, search only elements with a name that matches the final element of the aPath component. + /// \return std::shared_ptr. Valid if found. + std::shared_ptr getElement( std::string aPath, bool bStrict = false ); + + /// \brief Retrieve all of the elements recursively in the group filtered by the name provided. + /// \param std::string aname Name to be matched against the elements name. + /// \param bool bStrict if true, the element name mush match aname, if false, the element name must be contained in aname. + /// \return std::vector< std::shared_ptr >. + std::vector< std::shared_ptr > getAllElementsForName( const std::string& aname, bool bStrict = false ); + + /// \brief Retrieve child elements in the group filtered by the name provided. + /// \param std::string aname Name to be matched against the elements name. + /// \param bool bStrict if true, the element name mush match aname, if false, the element name must be contained in aname. + /// \return std::vector< std::shared_ptr >. + std::vector< std::shared_ptr > getChildrenForName( const std::string& aname, bool bStrict = false ); + + /// \brief Retrieve child elements in the group of a specific type filtered by the name provided. + /// \param std::string aname Name to be matched against the elements name. + /// \param bool bStrict if true, the element name mush match aname, if false, the element name must be contained in aname. + /// \return std::vector< std::shared_ptr > casted to type. + template + std::vector< std::shared_ptr > getChildrenForTypeForName( const std::string& aname, bool bStrict = false ) { + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > relements; + auto namedKids = getChildrenForName(aname); + for( auto& namedKid : namedKids ) { + if( namedKid->getType() != sType ) continue; + relements.push_back(std::dynamic_pointer_cast(namedKid)); + } + return relements; + } + + /// \brief Retrieve child elements in the group of a specific type. + /// \return std::vector< std::shared_ptr > casted to type. + template + std::vector< std::shared_ptr > getChildrenForType() { + return getElementsForType(""); + } + + /// \brief Retrieve the first child element of a specific type. + /// \return std::shared_ptr casted to type; Invalid if not found. + template + std::shared_ptr getFirstChildForType() { + return getFirstElementForType(""); + } + + /// \brief Replace an item with a new item. + /// \param shared_ptr aOriginal Element to be replaced. + /// \param shared_ptr aNew Element used to replace aOriginal. + /// \return bool true if aOriginal was replaced. + bool replace( std::shared_ptr aOriginal, std::shared_ptr aNew ); + + /// \brief Remove an element from this group or child groups. + /// \param shared_ptr aelement to be removed. + /// \return bool true if element was found and removed. + virtual bool remove( std::shared_ptr aelement ); + /// \brief Remove elements in a vector from this group or child groups. Example: svg.remove(); + /// \param vector > aelements to be removed. + /// \return bool true if all of the elements were found and removed. + template + bool removeElements( std::vector > aelements ) { + if( aelements.size() < 1 ) { + return false; + } + + bool bAllRemoved = true; + for( auto& aele : aelements ) { + if( !aele ) { + ofLogWarning("ofxSvgGroup::remove") << "element is invalid."; + bAllRemoved = false; + continue; + } + bool bEleRemoved = remove( aele ); + if( !bEleRemoved ) { + bAllRemoved = false; + } + } + return bAllRemoved; + } + + /// \brief Add a child element of provided type. + /// \param std::string aname Name to give the created element. + /// \return std::shared_ptr. + template + std::shared_ptr add(std::string aname) { + auto element = std::make_shared(); + element->name = aname; + mChildren.push_back(element); + return element; + }; + + /// \brief Add a child element. + /// \param std::shared_ptr aele Element to add as a child of this group. + void add( std::shared_ptr aele ) { mChildren.push_back(aele); } + + /// \brief Create a string representation of the group hierarchy. + /// \param int nlevel (optional) used for indentation. + virtual std::string toString(int nlevel = 0) override; + + /// \brief Disable shape colors on children. + void disableColors(); + + /// \brief Disable shape colors on children. + void enableColors(); + +protected: + void _getElementForNameRecursive(std::vector< std::string >& aNamesToFind, + std::shared_ptr& aTarget, + std::vector< std::shared_ptr >& aElements, + bool bStrict + ); + void _getAllElementsRecursive(std::vector< std::shared_ptr >& aElesToReturn, + std::shared_ptr aele, + bool aBIncludeGroups + ); + + void _getAllGroupsRecursive( std::vector< std::shared_ptr >& aGroupsToReturn, std::shared_ptr aele ); + + void _replaceElementRecursive(std::shared_ptr aTarget, + std::shared_ptr aNew, + std::vector< std::shared_ptr >& aElements, + bool& aBSuccessful + ); + + void _removeElementRecursive(std::shared_ptr aTarget, + std::vector >& aElements, + bool& aBSuccessful + ); + + std::vector< std::shared_ptr > mChildren; + + + virtual std::shared_ptr clone() override { + //ofLogVerbose("ofxSvgGroup") << "std::shared_ptr clone():"; + auto newEle = std::make_shared(*this); + newEle->mChildren.clear(); + newEle->mChildren.reserve(mChildren.size()); + for( const auto& ptr : mChildren ) { + if( ptr ) { + auto newKid = ptr->clone(); + newKid->setParent(*this); + newEle->mChildren.push_back(newKid); + } + } + return newEle; + }; +}; + + + + + + + + + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgUtils.cpp b/addons/ofxSvg/src/ofxSvgUtils.cpp new file mode 100644 index 00000000000..9220932f369 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgUtils.cpp @@ -0,0 +1,263 @@ +#include "ofxSvgUtils.h" +#include +#include "ofImage.h" + +/*----------------------------------------------------------------------*/ +/* + + base64 encoding and decoding with C++. + More information at + https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp + + Version: 2.rc.09 (release candidate) + + Copyright (C) 2004-2017, 2020-2022 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + + */ + + +/* + https://github.com/ReneNyffenegger/cpp-base64/blob/master/base64.cpp + This code has been adapted to work in only a .cpp file. + // certain functions have been ommitted. + */ + +static const char* base64_chars[2] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-_"}; + + +static unsigned int pos_of_char(const unsigned char chr) { + // + // Return the position of chr within base64_encode() + // + + if (chr >= 'A' && chr <= 'Z') return chr - 'A'; + else if (chr >= 'a' && chr <= 'z') return chr - 'a' + ('Z' - 'A') + 1; + else if (chr >= '0' && chr <= '9') return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2; + else if (chr == '+' || chr == '-') return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters ( + else if (chr == '/' || chr == '_') return 63; // Ditto for '/' and '_' + else + // + // 2020-10-23: Throw std::exception rather than const char* + //(Pablo Martin-Gomez, https://github.com/Bouska) + // + throw std::runtime_error("Input is not valid base64-encoded data."); +} + + +std::string ofxSvgUtils::base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url) { + + size_t len_encoded = (in_len +2) / 3 * 4; + + unsigned char trailing_char = url ? '.' : '='; + + // + // Choose set of base64 characters. They differ + // for the last two positions, depending on the url + // parameter. + // A bool (as is the parameter url) is guaranteed + // to evaluate to either 0 or 1 in C++ therefore, + // the correct character set is chosen by subscripting + // base64_chars with url. + // + const char* base64_chars_ = base64_chars[url]; + + std::string ret; + ret.reserve(len_encoded); + + unsigned int pos = 0; + + while (pos < in_len) { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]); + + if (pos+1 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]); + + if (pos+2 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]); + ret.push_back(base64_chars_[ bytes_to_encode[pos + 2] & 0x3f]); + } + else { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); + ret.push_back(trailing_char); + } + } + else { + + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); + ret.push_back(trailing_char); + ret.push_back(trailing_char); + } + + pos += 3; + } + + + return ret; +} + +//template // nh removed to only work with string +std::string ofxSvgUtils::base64_decode(std::string const& encoded_string, bool remove_linebreaks) { + // + // decode(…) is templated so that it can be used with String = const std::string& + // or std::string_view (requires at least C++17) + // + + if (encoded_string.empty()) return std::string(); + + if (remove_linebreaks) { + + std::string copy(encoded_string); + + copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end()); + + return base64_decode(copy, false); // nh modified to work without .cpp and .h + // return base64_decode(copy, false); + } + + size_t length_of_string = encoded_string.length(); + size_t pos = 0; + + // + // The approximate length (bytes) of the decoded string might be one or + // two bytes smaller, depending on the amount of trailing equal signs + // in the encoded string. This approximation is needed to reserve + // enough space in the string to be returned. + // + size_t approx_length_of_decoded_string = length_of_string / 4 * 3; + std::string ret; + ret.reserve(approx_length_of_decoded_string); + + while (pos < length_of_string) { + // + // Iterate over encoded input string in chunks. The size of all + // chunks except the last one is 4 bytes. + // + // The last chunk might be padded with equal signs or dots + // in order to make it 4 bytes in size as well, but this + // is not required as per RFC 2045. + // + // All chunks except the last one produce three output bytes. + // + // The last chunk produces at least one and up to three bytes. + // + + size_t pos_of_char_1 = pos_of_char(encoded_string.at(pos+1) ); + + // + // Emit the first output byte that is produced in each chunk: + // + ret.push_back(static_cast( ( (pos_of_char(encoded_string.at(pos+0)) ) << 2 ) + ( (pos_of_char_1 & 0x30 ) >> 4))); + + if ( ( pos + 2 < length_of_string ) && // Check for data that is not padded with equal signs (which is allowed by RFC 2045) + encoded_string.at(pos+2) != '=' && + encoded_string.at(pos+2) != '.' // accept URL-safe base 64 strings, too, so check for '.' also. + ) + { + // + // Emit a chunk's second byte (which might not be produced in the last chunk). + // + unsigned int pos_of_char_2 = pos_of_char(encoded_string.at(pos+2) ); + ret.push_back(static_cast( (( pos_of_char_1 & 0x0f) << 4) + (( pos_of_char_2 & 0x3c) >> 2))); + + if ( ( pos + 3 < length_of_string ) && + encoded_string.at(pos+3) != '=' && + encoded_string.at(pos+3) != '.' + ) + { + // + // Emit a chunk's third byte (which might not be produced in the last chunk). + // + ret.push_back(static_cast( ( (pos_of_char_2 & 0x03 ) << 6 ) + pos_of_char(encoded_string.at(pos+3)) )); + } + } + + pos += 4; + } + + return ret; +} + +/* end base64 encoding / decoding */ +/*----------------------------------------------------------------------*/ + + +//--------------------------------------------------------------- +std::string ofxSvgUtils::base64_encode( const ofPixels& apixels ) { + ofBuffer tbuffer; + ofSaveImage(apixels, tbuffer); + auto buffStr = tbuffer.getText(); + const unsigned char* data = reinterpret_cast(buffStr.data()); + return ofxSvgUtils::base64_encode( data, tbuffer.size(), false ); +} + +//--------------------------------------------------------------- +ofPixels ofxSvgUtils::base64_decode(std::string const& encoded_string ) { + ofBuffer tbuffer; + + ofPixels rpix; + + std::string estring = encoded_string; + ofStringReplace(estring, "data:image/png;base64,", "" ); + + if( estring.empty() ) { + return rpix; + } + + try { + std::string decoded_string = base64_decode(estring, false ); + + if( decoded_string.size() < 1 ) { + return rpix; + } + tbuffer.set(decoded_string); + + bool bok = ofLoadImage(rpix, tbuffer); + ofLogNotice("ofxSvgUtils::base64_decode") << "pixels ok: " << bok << " pixels: " << rpix.getWidth() << " x " << rpix.getHeight(); + + } catch (const std::runtime_error& e) { + if (std::string(e.what()) == "Input is not valid base64-encoded data.") { + ofLogError("ofxSvgUtils::base64_decode") << "Caught specific base64 error: " << e.what() << "\n"; + } else { + ofLogError("ofxSvgUtils::base64_decode") << "Caught runtime_error: " << e.what() << "\n"; + } + } + catch (...) { + ofLogError("ofxSvgUtils::base64_decode") << "Caught unknown exception.\n"; + } + + + +// bool bok = ofLoadImage(rpix, tbuffer); +// ofLogNotice("ofxSvgUtils::base64_decode") << "pixels ok: " << bok << " pixels: " << rpix.getWidth() << " x " << rpix.getHeight(); + return rpix; +} + diff --git a/addons/ofxSvg/src/ofxSvgUtils.h b/addons/ofxSvg/src/ofxSvgUtils.h new file mode 100644 index 00000000000..a63759876e3 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgUtils.h @@ -0,0 +1,69 @@ +#pragma once +#include +#include "ofPixels.h" + +// adding this Optional class since std::optional is not a part of all std:: distributions at the moment, looking at you gcc < 10 nh +// and not included in older versions of OF on Windows, ie. 12.0. +template +class ofxSvgOptional { +public: + ofxSvgOptional() : hasValue(false) {} // Default constructor, no value + ofxSvgOptional(const T& value) : hasValue(true), data(value) {} // Construct with a value + ofxSvgOptional(T&& value) : hasValue(true), data(std::move(value)) {} // Move constructor + + // Copy and move constructors + ofxSvgOptional(const ofxSvgOptional& other) = default; + ofxSvgOptional(ofxSvgOptional&& other) noexcept = default; + + // Assignment operators + ofxSvgOptional& operator=(const ofxSvgOptional& other) = default; + ofxSvgOptional& operator=(ofxSvgOptional&& other) noexcept = default; + + // Destructor + ~ofxSvgOptional() = default; + + // Check if there's a value + bool has_value() const { return hasValue; } + + // Accessors for the value + T& value() { + // if (!hasValue) throw std::runtime_error("No value present"); + if (!hasValue) { + ofLogError("ofxSvgCssClass") << "No value present"; + } + return data; + } + + const T& value() const { + // if (!hasValue) throw std::runtime_error("No value present"); + if (!hasValue) { + ofLogError("ofxSvgCssClass") << "No value present"; + } + return data; + } + + // Reset to an empty state + void reset() { hasValue = false; } + +private: + bool hasValue; + T data; +}; + + +class ofxSvgUtils { +public: + +// Adapted from code by René Nyffenegger. +// base64 encoding and decoding with C++. +// More information at +// https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp + static std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url); + static std::string base64_decode(std::string const& encoded_string, bool remove_linebreaks); + + static std::string base64_encode( const ofPixels& apixels ); + static ofPixels base64_decode(std::string const& encoded_string ); + + + +}; diff --git a/examples/input_output/svgParseExample/bin/data/ofLogoDesserts.svg b/examples/input_output/svgParseExample/bin/data/ofLogoDesserts.svg new file mode 100644 index 00000000000..0dedffaa165 --- /dev/null +++ b/examples/input_output/svgParseExample/bin/data/ofLogoDesserts.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/input_output/svgParseExample/src/main.cpp b/examples/input_output/svgParseExample/src/main.cpp new file mode 100755 index 00000000000..e2a9df057c8 --- /dev/null +++ b/examples/input_output/svgParseExample/src/main.cpp @@ -0,0 +1,18 @@ +#include "ofMain.h" +#include "ofApp.h" + +//======================================================================== +int main( ){ + + //Use ofGLFWWindowSettings for more options like multi-monitor fullscreen + ofGLWindowSettings settings; + settings.setSize(1024, 768); + settings.setGLVersion(3,2); + settings.windowMode = OF_WINDOW; //can also be OF_FULLSCREEN + + auto window = ofCreateWindow(settings); + + ofRunApp(window, std::make_shared()); + ofRunMainLoop(); + +} diff --git a/examples/input_output/svgParseExample/src/ofApp.cpp b/examples/input_output/svgParseExample/src/ofApp.cpp new file mode 100755 index 00000000000..f8254ff24d5 --- /dev/null +++ b/examples/input_output/svgParseExample/src/ofApp.cpp @@ -0,0 +1,146 @@ +#include "ofApp.h" + +//-------------------------------------------------------------- +void ofApp::setup(){ + + ofSetBackgroundColor(250); + + ofSetFrameRate( 60 ); + svg.load("ofLogoDesserts.svg"); + // Print the svg structure to the console. + ofLogNotice("Svg Structure") << std::endl << svg.toString(); + + // get all the paths in the Donut -> Sprinkles group + auto sprinklePaths = svg.getElementsForType("Donut:Sprinkles"); + // A wildcard (*) is also acceptable. Allowing traversal of groups until the group with the name after the colon is found. + // In this case "Sprinkles" +// auto sprinklePaths = svg.getElementsForType("*:Sprinkles"); + + for( auto& sprinklePath : sprinklePaths ) { + // Grab and store the first polyline in the path so we can set it on the svg element later. + auto spoly = sprinklePath->getFirstPolyline(); + if( spoly.size() > 2 ) { + // get the center point from the bounding box + auto centerPos = spoly.getBoundingBox().getCenter(); + sprinklePath->setPosition(centerPos); + + sprinklePath->getPath().clear(); + int counter = 0; + + // now lets convert the polyline to be local around the center point. + for( auto& vert : spoly.getVertices() ) { + vert -= centerPos; + if( counter < 1 ) { + sprinklePath->getPath().moveTo(vert); + } else { + sprinklePath->getPath().lineTo(vert); + } + counter++; + } + + if( spoly.isClosed() ) { + sprinklePath->getPath().close(); + } + // Store in a vector so that we can manipulate these later. + mSprinkles.push_back(sprinklePath); + } + } +} + +//-------------------------------------------------------------- +void ofApp::update(){ + + // store the elapsed time in a variable since it can be an expensive operation in a loop. + float etimef = ofGetElapsedTimef(); + int counter = 0; + for( auto& sprinkle : mSprinkles ) { + if( ofGetMousePressed() ) { + // Store the difference from the sprinkle position to the mouse position. + auto diff = glm::normalize(glm::vec2( ofGetMouseX(), ofGetMouseY() ) - glm::vec2( sprinkle->getPosition().x, sprinkle->getPosition().y )); + // Convert to a angle rotation in degrees. + float targetRotation = glm::degrees(atan2f( diff.y, diff.x )); + // Lerp to the target rotation. + // Calling ofLerpDegrees handles wrapping and edge cases when using degrees. + sprinkle->setRotationDeg(ofLerpDegrees(sprinkle->getRotationDeg(), targetRotation, 0.1f )); + sprinkle->setFillColor(ofColor(255)); + } else { + // Store the current rotation. + float sprinkleRotation = sprinkle->getRotationDeg(); + // Rotate a small amount based on cos and the sprinkle x position. + float rotationAmount = 2.f * ofClamp( cosf( sprinkle->getPosition().x * 0.1f + etimef ), -0.1f, 1.f); + sprinkle->setRotationDeg(sprinkleRotation+rotationAmount); + ofColor tcolor( 101,163,253 ); + // Change the hue a bit for a color changing effect. + tcolor.setHue( tcolor.getHue() + (sin(etimef*0.5f + counter * 2.f)) * 50.f ); + sprinkle->setFillColor(tcolor); + } + counter++; + } + +} + +//-------------------------------------------------------------- +void ofApp::draw(){ + + auto wrect = ofRectangle(0,0,ofGetWidth(), ofGetHeight()); + auto srect = svg.getBounds(); + // scale the bounds of the svg to fit within the window rectangle + srect.scaleTo(wrect, OF_SCALEMODE_FIT); + + ofPushMatrix(); { + ofTranslate(srect.x, srect.y); + ofScale( srect.getWidth() / svg.getBounds().getWidth() ); + svg.draw(); + + ofSetColor( 120 ); + ofNoFill(); + ofDrawRectangle( svg.getBounds() ); + ofFill(); + } ofPopMatrix(); + +} + +//-------------------------------------------------------------- +void ofApp::keyPressed(int key){ + +} + +//-------------------------------------------------------------- +void ofApp::keyReleased(int key){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseMoved(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseDragged(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::mousePressed(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseReleased(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::windowResized(int w, int h){ + +} + +//-------------------------------------------------------------- +void ofApp::gotMessage(ofMessage msg){ + +} + +//-------------------------------------------------------------- +void ofApp::dragEvent(ofDragInfo dragInfo){ + +} diff --git a/examples/input_output/svgParseExample/src/ofApp.h b/examples/input_output/svgParseExample/src/ofApp.h new file mode 100755 index 00000000000..99d6fda2ba4 --- /dev/null +++ b/examples/input_output/svgParseExample/src/ofApp.h @@ -0,0 +1,25 @@ +#pragma once + +#include "ofMain.h" +#include "ofxSvg.h" + +class ofApp : public ofBaseApp{ + public: + void setup(); + void update(); + void draw(); + + void keyPressed(int key); + void keyReleased(int key); + void mouseMoved(int x, int y); + void mouseDragged(int x, int y, int button); + void mousePressed(int x, int y, int button); + void mouseReleased(int x, int y, int button); + void windowResized(int w, int h); + void dragEvent(ofDragInfo dragInfo); + void gotMessage(ofMessage msg); + + ofxSvg svg; + std::vector< std::shared_ptr > mSprinkles; + +}; diff --git a/examples/input_output/svgParseExample/svgParseExample.png b/examples/input_output/svgParseExample/svgParseExample.png new file mode 100644 index 00000000000..545ffd5b62c Binary files /dev/null and b/examples/input_output/svgParseExample/svgParseExample.png differ diff --git a/examples/input_output/svgSaveExample/bin/data/ofxSvg.svg b/examples/input_output/svgSaveExample/bin/data/ofxSvg.svg new file mode 100644 index 00000000000..440b665153d --- /dev/null +++ b/examples/input_output/svgSaveExample/bin/data/ofxSvg.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/input_output/svgSaveExample/src/main.cpp b/examples/input_output/svgSaveExample/src/main.cpp new file mode 100644 index 00000000000..5ce9270df19 --- /dev/null +++ b/examples/input_output/svgSaveExample/src/main.cpp @@ -0,0 +1,18 @@ +#include "ofMain.h" +#include "ofApp.h" + +//======================================================================== +int main( ){ + + //Use ofGLFWWindowSettings for more options like multi-monitor fullscreen + ofGLWindowSettings settings; + settings.setSize(1024, 768); + settings.setGLVersion(3,2); + settings.windowMode = OF_WINDOW; //can also be OF_FULLSCREEN + + auto window = ofCreateWindow(settings); + + ofRunApp(window, std::make_shared()); + ofRunMainLoop(); + +} diff --git a/examples/input_output/svgSaveExample/src/ofApp.cpp b/examples/input_output/svgSaveExample/src/ofApp.cpp new file mode 100644 index 00000000000..43dd197b8ee --- /dev/null +++ b/examples/input_output/svgSaveExample/src/ofApp.cpp @@ -0,0 +1,183 @@ +#include "ofApp.h" + +//-------------------------------------------------------------- +void ofApp::setup(){ + ofSetBackgroundColor( 230 ); + + svgAddTypes = { + OFXSVG_TYPE_RECTANGLE, + OFXSVG_TYPE_CIRCLE, + OFXSVG_TYPE_PATH + }; + + svg.load("ofxSvg.svg"); + +} + +//-------------------------------------------------------------- +void ofApp::update(){ + svg.setBoundsWidth(ofGetWidth()); + svg.setBoundsHeight(ofGetHeight()); +} + +//-------------------------------------------------------------- +void ofApp::draw(){ + svg.draw(); + ofSetColor( ofColor::cyan ); + polyline.draw(); + + std::stringstream ss; + ss << "Add Type (left/right): " << (svgTypeIndex+1) << " / " << svgAddTypes.size() << " - " << ofxSvgElement::sGetTypeAsString(svgAddTypes[svgTypeIndex]); + if( svgAddTypes[svgTypeIndex] == OFXSVG_TYPE_PATH ) { + ss << std::endl << "Click and drag mouse to add points."; + } + ss << std::endl << "Size (up/down): " << size; + ss << std::endl << "Remove circles (c)"; + ss << std::endl << "Remove rectangles (r)"; + ss << std::endl << "Save (s)"; + ss << std::endl << "Clear (delete)"; + ofDrawBitmapStringHighlight(ss.str(), 40, 40); +} + +//-------------------------------------------------------------- +void ofApp::exit(){ + +} + +//-------------------------------------------------------------- +void ofApp::keyPressed(int key){ + if( key == OF_KEY_DEL || key == OF_KEY_BACKSPACE ) { + svg.clear(); + } + if( key == 's' ) { + std::string filename = ofGetTimestampString()+".svg"; + ofLogNotice("ofApp") << "saving svg to file: " << filename; + svg.save(filename); + } + + if( key == 'c' ) { + // get all of the circles + auto circles = svg.getAllElementsForType(); + // now lets remove all of them. + svg.removeElements( circles ); + // another option would be to loop through the circles and call remove on each one. +// for( auto& circle : circles ) { +// svg.remove(circle); +// } + } + if( key == 'r' ) { + // our original document contains rectangles + // or maybe paths since Illustrator sometimes converts them. + // lets try grabbing them by name. + // The second argument of this function determines if the name is strictly matched or not. + // We pass in false because we name our rectangles with myrect + frame num. + // So we want to get all of the rects that contain the string "myrect" in their name. + auto myRects = svg.getAllElementsForTypeForName("myrect", false); + svg.removeElements(myRects); + } + + if( key == OF_KEY_RIGHT ) { + svgTypeIndex++; + svgTypeIndex %= svgAddTypes.size(); + } + if( key == OF_KEY_LEFT ) { + svgTypeIndex--; + if( svgTypeIndex < 0 ) { + svgTypeIndex = svgAddTypes.size()-1; + } + } + if( key == OF_KEY_UP ) { + size += 2; + } + if( key == OF_KEY_DOWN ) { + size -= 2; + } + + size = ofClamp( size, 2, 2000 ); +} + +//-------------------------------------------------------------- +void ofApp::keyReleased(int key){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseMoved(int x, int y ){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseDragged(int x, int y, int button){ + if(polyline.size() > 0 ) { + polyline.addVertex(glm::vec3(x,y,0.f)); + } +} + +//-------------------------------------------------------------- +void ofApp::mousePressed(int x, int y, int button){ + polyline.clear(); + svg.setFilled(true); + svg.setHasStroke(false); +// svg.setStrokeWidth(0); + if( svgAddTypes[svgTypeIndex] == OFXSVG_TYPE_RECTANGLE ) { + ofRectangle rect; + rect.setFromCenter(x, y, size, size); + svg.setFillColor(ofColor(255, x%255, (y*10) % 255)); + auto svgRect = svg.add( rect ); + svgRect->setName("myrect-"+ofToString(ofGetFrameNum())); + } else if( svgAddTypes[svgTypeIndex] == OFXSVG_TYPE_CIRCLE ) { + svg.setFillColor(ofColor(x%255, 205, (y*10) % 255)); + auto svgCircle = svg.addCircle( glm::vec2(x,y), size ); + svgCircle->setName("mycircle-"+ofToString(ofGetFrameNum())); + } else if( svgAddTypes[svgTypeIndex] == OFXSVG_TYPE_PATH ) { + svg.setFilled(false); + svg.setStrokeColor(ofColor::magenta); + svg.setStrokeWidth(3); + polyline.addVertex(glm::vec3(x,y,0.f)); + } + + std::cout << "------------------------------------" << std::endl; + std::cout << svg.toString() << std::endl; + std::cout << "------------------------------------" << std::endl; +} + +//-------------------------------------------------------------- +void ofApp::mouseReleased(int x, int y, int button){ + if( svgAddTypes[svgTypeIndex] == OFXSVG_TYPE_PATH ) { + if( polyline.size() > 2 ) { + auto svgPath = svg.add(polyline); + svgPath->setName("p-"+ofToString(ofGetFrameNum())); + polyline.clear(); + } + } +} + +//-------------------------------------------------------------- +void ofApp::mouseScrolled(int x, int y, float scrollX, float scrollY){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseEntered(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseExited(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::windowResized(int w, int h){ + +} + +//-------------------------------------------------------------- +void ofApp::gotMessage(ofMessage msg){ + +} + +//-------------------------------------------------------------- +void ofApp::dragEvent(ofDragInfo dragInfo){ + +} diff --git a/examples/input_output/svgSaveExample/src/ofApp.h b/examples/input_output/svgSaveExample/src/ofApp.h new file mode 100644 index 00000000000..7aa3b9fefb0 --- /dev/null +++ b/examples/input_output/svgSaveExample/src/ofApp.h @@ -0,0 +1,34 @@ +#pragma once + +#include "ofMain.h" +#include "ofxSvg.h" + +class ofApp : public ofBaseApp{ + + public: + void setup() override; + void update() override; + void draw() override; + void exit() override; + + void keyPressed(int key) override; + void keyReleased(int key) override; + void mouseMoved(int x, int y ) override; + void mouseDragged(int x, int y, int button) override; + void mousePressed(int x, int y, int button) override; + void mouseReleased(int x, int y, int button) override; + void mouseScrolled(int x, int y, float scrollX, float scrollY) override; + void mouseEntered(int x, int y) override; + void mouseExited(int x, int y) override; + void windowResized(int w, int h) override; + void dragEvent(ofDragInfo dragInfo) override; + void gotMessage(ofMessage msg) override; + + ofxSvg svg; + + std::vector svgAddTypes; + int svgTypeIndex = 0; + int size = 20; + ofPolyline polyline; + +}; diff --git a/examples/input_output/svgSaveExample/svgSaveExample.png b/examples/input_output/svgSaveExample/svgSaveExample.png new file mode 100644 index 00000000000..0ea5180f117 Binary files /dev/null and b/examples/input_output/svgSaveExample/svgSaveExample.png differ