diff --git a/.gitignore b/.gitignore index a696745..9cdcff2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ Release/ *.VC.opendb output.png render.png +out/ +build_xcode/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..98342cc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 2.8.12) + +project(msdfgen) + +find_package(Freetype REQUIRED) + +include(CheckCXXCompilerFlag) +CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11) +if (COMPILER_SUPPORTS_CXX11) + add_definitions(-DMSDFGEN_USE_CPP11) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +endif() + +# Make release mode default (turn on optimizations) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + + +# Note: Clang doesn't support openMP by default... +#find_package(OpenMP) +#if (OPENMP_FOUND) +# set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") +# set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") +#endif() + +#---------------------------------------------------------------- +# Support Functions +#---------------------------------------------------------------- + +# Mirror the folder structure for sources inside the IDE... +function(folderize_sources sources prefix) + foreach(FILE ${${sources}}) + get_filename_component(PARENT_DIR "${FILE}" PATH) + + # skip src or include and changes /'s to \\'s + string(REPLACE "${prefix}" "" GROUP "${PARENT_DIR}") + string(REPLACE "/" "\\" GROUP "${GROUP}") + + # If it's got a path, then append a "\\" separator (otherwise leave it blank) + if ("${GROUP}" MATCHES ".+") + set(GROUP "\\${GROUP}") + endif() + + source_group("${GROUP}" FILES "${FILE}") + endforeach() +endfunction(folderize_sources) + + + +file(GLOB_RECURSE msdfgen_HEADERS + "core/*.h" + "lib/*.h" + "ext/*.h" + "include/*.h" +) + +file(GLOB_RECURSE msdfgen_SOURCES + "core/*.cpp" + "lib/*.cpp" + "ext/*.cpp" +) + +include_directories(${FREETYPE_INCLUDE_DIRS}) +include_directories("include") + +# Build the library (aliased name because it's the same target name the exe) +folderize_sources(msdfgen_HEADERS ${CMAKE_SOURCE_DIR}) +folderize_sources(msdfgen_SOURCES ${CMAKE_SOURCE_DIR}) + +add_library(lib_msdfgen ${msdfgen_SOURCES} ${msdfgen_HEADERS}) +set_target_properties(lib_msdfgen PROPERTIES OUTPUT_NAME msdfgen) +target_link_libraries(lib_msdfgen ${FREETYPE_LIBRARIES}) + +# Build the executable + +add_executable(msdfgen main.cpp) +target_compile_definitions(msdfgen PRIVATE MSDFGEN_STANDALONE) +target_link_libraries(msdfgen lib_msdfgen) diff --git a/Makefile b/Makefile deleted file mode 100644 index ebdf199..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ - -all: - g++ -I include -D MSDFGEN_STANDALONE -O2 -o msdfgen core/*.cpp lib/*.cpp ext/*.cpp main.cpp -lfreetype diff --git a/README.md b/README.md index 5302785..ce800e6 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. @@ -151,6 +151,7 @@ The following is an example GLSL fragment shader including anti-aliasing: in vec2 pos; out vec4 color; uniform sampler2D msdf; +uniform float pxRange; uniform vec4 bgColor; uniform vec4 fgColor; @@ -159,9 +160,11 @@ float median(float r, float g, float b) { } void main() { + vec2 msdfUnit = pxRange/vec2(textureSize(msdf, 0)); vec3 sample = texture(msdf, pos).rgb; float sigDist = median(sample.r, sample.g, sample.b) - 0.5; - float opacity = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0); + sigDist *= dot(msdfUnit, 0.5/fwidth(pos)); + float opacity = clamp(sigDist + 0.5, 0.0, 1.0); color = mix(bgColor, fgColor, opacity); } ``` diff --git a/ext/import-svg.cpp b/ext/import-svg.cpp index 6ae3134..dd3462f 100644 --- a/ext/import-svg.cpp +++ b/ext/import-svg.cpp @@ -1,183 +1,313 @@ - -#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 void skipExtraChars(const char *&pathDef) { + while (*pathDef == ',' || *pathDef == ' ' || *pathDef == '\t' || *pathDef == '\r' || *pathDef == '\n') + ++pathDef; +} + +static bool readNodeType(char &output, const char *&pathDef) { + skipExtraChars(pathDef); + char nodeType = *pathDef; + if (nodeType && nodeType != '+' && nodeType != '-' && nodeType != '.' && nodeType != ',' && (nodeType < '0' || nodeType > '9')) { + ++pathDef; + output = nodeType; + return true; + } + return false; +} + +static bool readCoord(Point2 &output, const char *&pathDef) { + skipExtraChars(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) { + skipExtraChars(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) { + skipExtraChars(pathDef); + int shift; + int v; + if (sscanf(pathDef, "%d%n", &v, &shift) == 1) { + pathDef += shift; + output = v != 0; + return true; + } + return false; +} + +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 = '\0'; + char prevNodeType = '\0'; + 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)); + 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': + if (prevNodeType == 'Q' || prevNodeType == 'q' || prevNodeType == 'T' || prevNodeType == 't') + controlPoint[0] = node+node-controlPoint[0]; + else + controlPoint[0] = node; + 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)); + REQUIRE(readCoord(controlPoint[1], 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': + if (prevNodeType == 'C' || prevNodeType == 'c' || prevNodeType == 'S' || prevNodeType == 's') + controlPoint[0] = node+node-controlPoint[1]; + else + controlPoint[0] = node; + REQUIRE(readCoord(controlPoint[1], 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)); + REQUIRE(readDouble(angle, pathDef)); + REQUIRE(readBool(largeArg, pathDef)); + REQUIRE(readBool(sweep, 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; + prevNodeType = nodeType; + readNodeType(nodeType, 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; + prevNodeType = '\0'; + } + 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 bb8e1e4..498fc22 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,6 @@ /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) - standalone console program + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.5 (2017-07-23) - standalone console program * -------------------------------------------------------------------------------------------- * A utility by Viktor Chlumsky, (c) 2014 - 2017 * @@ -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" @@ -333,7 +333,7 @@ static const char *helpText = "\n"; int main(int argc, const char * const *argv) { - #define ABORT(msg) { puts(msg); return 0; } + #define ABORT(msg) { puts(msg); return 1; } // Parse command line arguments enum { @@ -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; } diff --git a/msdfgen-ext.h b/msdfgen-ext.h index 3b1f82c..8cb4c9a 100644 --- a/msdfgen-ext.h +++ b/msdfgen-ext.h @@ -2,7 +2,7 @@ #pragma once /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) - extensions + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.5 (2017-07-23) - extensions * ---------------------------------------------------------------------------- * A utility by Viktor Chlumsky, (c) 2014 - 2017 * diff --git a/msdfgen.h b/msdfgen.h index a80dde3..b09392b 100644 --- a/msdfgen.h +++ b/msdfgen.h @@ -2,7 +2,7 @@ #pragma once /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.5 (2017-07-23) * --------------------------------------------------------------- * A utility by Viktor Chlumsky, (c) 2014 - 2017 * @@ -24,7 +24,7 @@ #include "core/save-bmp.h" #include "core/shape-description.h" -#define MSDFGEN_VERSION "1.4" +#define MSDFGEN_VERSION "1.5" namespace msdfgen {