diff --git a/README.md b/README.md index 5302785..d989556 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The input can be specified as one of: - **-font \ \** – to load a glyph from a font file. Character code can be expressed as either a decimal (63) or hexadecimal (0x3F) Unicode value, or an ASCII character in single quotes ('?'). - - **-svg \** – to load an SVG file. Note that only the first vector path in the file will be used. + - **-svg \** – to load an SVG file. Note that only the last vector path in the file will be used. - **-shapedesc \**, -defineshape \, -stdin – to load a text description of the shape from either a file, the next argument, or the standard input, respectively. Its syntax is documented further down. diff --git a/ext/import-svg.cpp b/ext/import-svg.cpp index 6ae3134..1d5467b 100644 --- a/ext/import-svg.cpp +++ b/ext/import-svg.cpp @@ -1,183 +1,316 @@ - -#include "import-svg.h" - -#include -#include - -#ifdef _WIN32 - #pragma warning(disable:4996) -#endif - -namespace msdfgen { - -#define REQUIRE(cond) { if (!(cond)) return false; } - -static bool readNodeType(char &output, const char *&pathDef) { - int shift; - char nodeType; - if (sscanf(pathDef, " %c%n", &nodeType, &shift) == 1 && nodeType != '+' && nodeType != '-' && nodeType != '.' && nodeType != ',' && (nodeType < '0' || nodeType > '9')) { - pathDef += shift; - output = nodeType; - return true; - } - return false; -} - -static bool readCoord(Point2 &output, const char *&pathDef) { - int shift; - double x, y; - if (sscanf(pathDef, "%lf%lf%n", &x, &y, &shift) == 2) { - output.x = x; - output.y = y; - pathDef += shift; - return true; - } - if (sscanf(pathDef, "%lf,%lf%n", &x, &y, &shift) == 2) { - output.x = x; - output.y = y; - pathDef += shift; - return true; - } - return false; -} - -static bool readDouble(double &output, const char *&pathDef) { - int shift; - double v; - if (sscanf(pathDef, "%lf%n", &v, &shift) == 1) { - pathDef += shift; - output = v; - return true; - } - return false; -} - -static void consumeOptionalComma(const char *&pathDef) { - while (*pathDef == ' ') - ++pathDef; - if (*pathDef == ',') - ++pathDef; -} - -static bool buildFromPath(Shape &shape, const char *pathDef) { - char nodeType; - Point2 prevNode(0, 0); - while (readNodeType(nodeType, pathDef)) { - Contour &contour = shape.addContour(); - bool contourStart = true; - - Point2 startPoint; - Point2 controlPoint[2]; - Point2 node; - - while (true) { - switch (nodeType) { - case 'M': case 'm': - REQUIRE(contourStart); - REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'm') - node += prevNode; - startPoint = node; - --nodeType; // to 'L' or 'l' - break; - case 'Z': case 'z': - if (prevNode != startPoint) - contour.addEdge(new LinearSegment(prevNode, startPoint)); - prevNode = startPoint; - goto NEXT_CONTOUR; - case 'L': case 'l': - REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'l') - node += prevNode; - contour.addEdge(new LinearSegment(prevNode, node)); - break; - case 'H': case 'h': - REQUIRE(readDouble(node.x, pathDef)); - if (nodeType == 'h') - node.x += prevNode.x; - contour.addEdge(new LinearSegment(prevNode, node)); - break; - case 'V': case 'v': - REQUIRE(readDouble(node.y, pathDef)); - if (nodeType == 'v') - node.y += prevNode.y; - contour.addEdge(new LinearSegment(prevNode, node)); - break; - case 'Q': case 'q': - REQUIRE(readCoord(controlPoint[0], pathDef)); - consumeOptionalComma(pathDef); - REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'q') { - controlPoint[0] += prevNode; - node += prevNode; - } - contour.addEdge(new QuadraticSegment(prevNode, controlPoint[0], node)); - break; - // TODO T, t - case 'C': case 'c': - REQUIRE(readCoord(controlPoint[0], pathDef)); - consumeOptionalComma(pathDef); - REQUIRE(readCoord(controlPoint[1], pathDef)); - consumeOptionalComma(pathDef); - REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'c') { - controlPoint[0] += prevNode; - controlPoint[1] += prevNode; - node += prevNode; - } - contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); - break; - case 'S': case 's': - controlPoint[0] = node+node-controlPoint[1]; - REQUIRE(readCoord(controlPoint[1], pathDef)); - consumeOptionalComma(pathDef); - REQUIRE(readCoord(node, pathDef)); - if (nodeType == 's') { - controlPoint[1] += prevNode; - node += prevNode; - } - contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); - break; - // TODO A, a - default: - REQUIRE(false); - } - contourStart &= nodeType == 'M' || nodeType == 'm'; - prevNode = node; - readNodeType(nodeType, pathDef); - } - NEXT_CONTOUR:; - } - return true; -} - -bool loadSvgShape(Shape &output, const char *filename, Vector2 *dimensions) { - tinyxml2::XMLDocument doc; - if (doc.LoadFile(filename)) - return false; - tinyxml2::XMLElement *root = doc.FirstChildElement("svg"); - if (!root) - return false; - - tinyxml2::XMLElement *path = root->FirstChildElement("path"); - if (!path) { - tinyxml2::XMLElement *g = root->FirstChildElement("g"); - if (g) - path = g->FirstChildElement("path"); - } - if (!path) - return false; - const char *pd = path->Attribute("d"); - if (!pd) - return false; - - output.contours.clear(); - output.inverseYAxis = true; - if (dimensions) { - dimensions->x = root->DoubleAttribute("width"); - dimensions->y = root->DoubleAttribute("height"); - } - return buildFromPath(output, pd); -} - -} + +#define _USE_MATH_DEFINES +#include "import-svg.h" + +#include +#include +#include "../core/arithmetics.hpp" + +#ifdef _WIN32 + #pragma warning(disable:4996) +#endif + +#define ARC_SEGMENTS_PER_PI 2 +#define ENDPOINT_SNAP_RANGE_PROPORTION (1/16384.) + +namespace msdfgen { + +#if defined(_DEBUG) || !NDEBUG +#define REQUIRE(cond) { if (!(cond)) { fprintf(stderr, "SVG Parse Error (%s:%d): " #cond "\n", __FILE__, __LINE__); return false; } } +#else +#define REQUIRE(cond) { if (!(cond)) return false; } +#endif + +static bool readNodeType(char &output, const char *&pathDef) { + int shift; + char nodeType; + if (sscanf(pathDef, " %c%n", &nodeType, &shift) == 1 && nodeType != '+' && nodeType != '-' && nodeType != '.' && nodeType != ',' && (nodeType < '0' || nodeType > '9')) { + pathDef += shift; + output = nodeType; + return true; + } + return false; +} + +static bool readCoord(Point2 &output, const char *&pathDef) { + int shift; + double x, y; + if (sscanf(pathDef, " %lf%lf%n", &x, &y, &shift) == 2 || sscanf(pathDef, " %lf , %lf%n", &x, &y, &shift) == 2) { + output.x = x; + output.y = y; + pathDef += shift; + return true; + } + return false; +} + +static bool readDouble(double &output, const char *&pathDef) { + int shift; + double v; + if (sscanf(pathDef, " %lf%n", &v, &shift) == 1) { + pathDef += shift; + output = v; + return true; + } + return false; +} + +static bool readBool(bool &output, const char *&pathDef) { + int shift; + int v; + if (sscanf(pathDef, " %d%n", &v, &shift) == 1) { + pathDef += shift; + output = v != 0; + return true; + } + return false; +} + +static void consumeWhitespace(const char *&pathDef) { + while (*pathDef == ' ' || *pathDef == '\t' || *pathDef == '\r' || *pathDef == '\n') + ++pathDef; +} + +static void consumeOptionalComma(const char *&pathDef) { + consumeWhitespace(pathDef); + if (*pathDef == ',') + ++pathDef; +} + +static double arcAngle(Vector2 u, Vector2 v) { + return nonZeroSign(crossProduct(u, v))*acos(clamp(dotProduct(u, v)/(u.length()*v.length()), -1., +1.)); +} + +static Vector2 rotateVector(Vector2 v, Vector2 direction) { + return Vector2(direction.x*v.x-direction.y*v.y, direction.y*v.x+direction.x*v.y); +} + +static void addArcApproximate(Contour &contour, Point2 startPoint, Point2 endPoint, Vector2 radius, double rotation, bool largeArc, bool sweep) { + if (endPoint == startPoint) + return; + if (radius.x == 0 || radius.y == 0) + return contour.addEdge(new LinearSegment(startPoint, endPoint)); + + radius.x = fabs(radius.x); + radius.y = fabs(radius.y); + Vector2 axis(cos(rotation), sin(rotation)); + + Vector2 rm = rotateVector(.5*(startPoint-endPoint), Vector2(axis.x, -axis.y)); + Vector2 rm2 = rm*rm; + Vector2 radius2 = radius*radius; + double radiusGap = rm2.x/radius2.x+rm2.y/radius2.y; + if (radiusGap > 1) { + radius *= sqrt(radiusGap); + radius2 = radius*radius; + } + double dq = (radius2.x*rm2.y+radius2.y*rm2.x); + double pq = radius2.x*radius2.y/dq-1; + double q = (largeArc == sweep ? -1 : +1)*sqrt(max(pq, 0.)); + Vector2 rc(q*radius.x*rm.y/radius.y, -q*radius.y*rm.x/radius.x); + Point2 center = .5*(startPoint+endPoint)+rotateVector(rc, axis); + + double angleStart = arcAngle(Vector2(1, 0), (rm-rc)/radius); + double angleExtent = arcAngle((rm-rc)/radius, (-rm-rc)/radius); + if (!sweep && angleExtent > 0) + angleExtent -= 2*M_PI; + else if (sweep && angleExtent < 0) + angleExtent += 2*M_PI; + + int segments = (int) ceil(ARC_SEGMENTS_PER_PI/M_PI*fabs(angleExtent)); + double angleIncrement = angleExtent/segments; + double cl = 4/3.*sin(.5*angleIncrement)/(1+cos(.5*angleIncrement)); + + Point2 prevNode = startPoint; + double angle = angleStart; + for (int i = 0; i < segments; ++i) { + Point2 controlPoint[2]; + Vector2 d(cos(angle), sin(angle)); + controlPoint[0] = center+rotateVector(Vector2(d.x-cl*d.y, d.y+cl*d.x)*radius, axis); + angle += angleIncrement; + d.set(cos(angle), sin(angle)); + controlPoint[1] = center+rotateVector(Vector2(d.x+cl*d.y, d.y-cl*d.x)*radius, axis); + Point2 node = i == segments-1 ? endPoint : center+rotateVector(d*radius, axis); + contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); + prevNode = node; + } +} + +static bool buildFromPath(Shape &shape, const char *pathDef, double size) { + char nodeType; + Point2 prevNode(0, 0); + bool nodeTypePreread = false; + while (nodeTypePreread || readNodeType(nodeType, pathDef)) { + nodeTypePreread = false; + Contour &contour = shape.addContour(); + bool contourStart = true; + + Point2 startPoint; + Point2 controlPoint[2]; + Point2 node; + + while (*pathDef) { + switch (nodeType) { + case 'M': case 'm': + if (!contourStart) { + nodeTypePreread = true; + goto NEXT_CONTOUR; + } + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 'm') + node += prevNode; + startPoint = node; + --nodeType; // to 'L' or 'l' + break; + case 'Z': case 'z': + REQUIRE(!contourStart); + goto NEXT_CONTOUR; + case 'L': case 'l': + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 'l') + node += prevNode; + contour.addEdge(new LinearSegment(prevNode, node)); + break; + case 'H': case 'h': + REQUIRE(readDouble(node.x, pathDef)); + if (nodeType == 'h') + node.x += prevNode.x; + contour.addEdge(new LinearSegment(prevNode, node)); + break; + case 'V': case 'v': + REQUIRE(readDouble(node.y, pathDef)); + if (nodeType == 'v') + node.y += prevNode.y; + contour.addEdge(new LinearSegment(prevNode, node)); + break; + case 'Q': case 'q': + REQUIRE(readCoord(controlPoint[0], pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 'q') { + controlPoint[0] += prevNode; + node += prevNode; + } + contour.addEdge(new QuadraticSegment(prevNode, controlPoint[0], node)); + break; + case 'T': case 't': + controlPoint[0] = node+node-controlPoint[0]; + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 't') + node += prevNode; + contour.addEdge(new QuadraticSegment(prevNode, controlPoint[0], node)); + break; + case 'C': case 'c': + REQUIRE(readCoord(controlPoint[0], pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readCoord(controlPoint[1], pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 'c') { + controlPoint[0] += prevNode; + controlPoint[1] += prevNode; + node += prevNode; + } + contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); + break; + case 'S': case 's': + controlPoint[0] = node+node-controlPoint[1]; + REQUIRE(readCoord(controlPoint[1], pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 's') { + controlPoint[1] += prevNode; + node += prevNode; + } + contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); + break; + case 'A': case 'a': + { + Vector2 radius; + double angle; + bool largeArg; + bool sweep; + REQUIRE(readCoord(radius, pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readDouble(angle, pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readBool(largeArg, pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readBool(sweep, pathDef)); + consumeOptionalComma(pathDef); + REQUIRE(readCoord(node, pathDef)); + if (nodeType == 'a') + node += prevNode; + angle *= M_PI/180.0; + addArcApproximate(contour, prevNode, node, radius, angle, largeArg, sweep); + } + break; + default: + REQUIRE(!"Unknown node type"); + } + contourStart &= nodeType == 'M' || nodeType == 'm'; + prevNode = node; + readNodeType(nodeType, pathDef); + consumeWhitespace(pathDef); + } + NEXT_CONTOUR: + // Fix contour if it isn't properly closed + if (!contour.edges.empty() && prevNode != startPoint) { + if ((contour.edges[contour.edges.size()-1]->point(1)-contour.edges[0]->point(0)).length() < ENDPOINT_SNAP_RANGE_PROPORTION*size) + contour.edges[contour.edges.size()-1]->moveEndPoint(contour.edges[0]->point(0)); + else + contour.addEdge(new LinearSegment(prevNode, startPoint)); + } + prevNode = startPoint; + } + return true; +} + +bool loadSvgShape(Shape &output, const char *filename, int pathIndex, Vector2 *dimensions) { + tinyxml2::XMLDocument doc; + if (doc.LoadFile(filename)) + return false; + tinyxml2::XMLElement *root = doc.FirstChildElement("svg"); + if (!root) + return false; + + tinyxml2::XMLElement *path = NULL; + if (pathIndex > 0) { + path = root->FirstChildElement("path"); + if (!path) { + tinyxml2::XMLElement *g = root->FirstChildElement("g"); + if (g) + path = g->FirstChildElement("path"); + } + while (path && --pathIndex > 0) + path = path->NextSiblingElement("path"); + } else { + path = root->LastChildElement("path"); + if (!path) { + tinyxml2::XMLElement *g = root->LastChildElement("g"); + if (g) + path = g->LastChildElement("path"); + } + while (path && ++pathIndex < 0) + path = path->PreviousSiblingElement("path"); + } + if (!path) + return false; + const char *pd = path->Attribute("d"); + if (!pd) + return false; + + output.contours.clear(); + output.inverseYAxis = true; + Vector2 dims(root->DoubleAttribute("width"), root->DoubleAttribute("height")); + if (!dims) { + double left, top; + const char *viewBox = root->Attribute("viewBox"); + if (viewBox) + sscanf(viewBox, "%lf %lf %lf %lf", &left, &top, &dims.x, &dims.y); + } + if (dimensions) + *dimensions = dims; + return buildFromPath(output, pd, dims.length()); +} + +} diff --git a/ext/import-svg.h b/ext/import-svg.h index 405ab69..cd69d2f 100644 --- a/ext/import-svg.h +++ b/ext/import-svg.h @@ -7,6 +7,6 @@ namespace msdfgen { /// Reads the first path found in the specified SVG file and stores it as a Shape in output. -bool loadSvgShape(Shape &output, const char *filename, Vector2 *dimensions = NULL); +bool loadSvgShape(Shape &output, const char *filename, int pathIndex = 0, Vector2 *dimensions = NULL); } diff --git a/main.cpp b/main.cpp index 93452d3..ef4a4c8 100644 --- a/main.cpp +++ b/main.cpp @@ -281,7 +281,7 @@ static const char *helpText = " -stdin\n" "\tReads text shape description from the standard input.\n" " -svg \n" - "\tLoads the first vector path encountered in the specified SVG file.\n" + "\tLoads the last vector path found in the specified SVG file.\n" "\n" "OPTIONS\n" " -angle \n" @@ -359,6 +359,7 @@ int main(int argc, const char * const *argv) { const char *testRenderMulti = NULL; bool outputSpecified = false; int unicode = 0; + int svgPathIndex = 0; int width = 64, height = 64; int testWidth = 0, testHeight = 0; @@ -618,7 +619,7 @@ int main(int argc, const char * const *argv) { Shape shape; switch (inputType) { case SVG: { - if (!loadSvgShape(shape, input, &svgDims)) + if (!loadSvgShape(shape, input, svgPathIndex, &svgDims)) ABORT("Failed to load shape from SVG file."); break; }