/* * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR - standalone console program * -------------------------------------------------------------------------- * A utility by Viktor Chlumsky, (c) 2014 - 2023 * */ #ifdef MSDFGEN_STANDALONE #define _USE_MATH_DEFINES #define _CRT_SECURE_NO_WARNINGS #include #include #include #include #include "msdfgen.h" #include "msdfgen-ext.h" #include "core/ShapeDistanceFinder.h" #define SDF_ERROR_ESTIMATE_PRECISION 19 #define DEFAULT_ANGLE_THRESHOLD 3. using namespace msdfgen; enum Format { AUTO, PNG, BMP, TIFF, TEXT, TEXT_FLOAT, BINARY, BINARY_FLOAT, BINARY_FLOAT_BE }; static bool is8bitFormat(Format format) { return format == PNG || format == BMP || format == TEXT || format == BINARY; } static char toupper(char c) { return c >= 'a' && c <= 'z' ? c-'a'+'A' : c; } static bool parseUnsigned(unsigned &value, const char *arg) { char c; return sscanf(arg, "%u%c", &value, &c) == 1; } static bool parseUnsignedDecOrHex(unsigned &value, const char *arg) { if (arg[0] == '0' && (arg[1] == 'x' || arg[1] == 'X')) { char c; return sscanf(arg+2, "%x%c", &value, &c) == 1; } return parseUnsigned(value, arg); } static bool parseUnsignedLL(unsigned long long &value, const char *arg) { char c; return sscanf(arg, "%llu%c", &value, &c) == 1; } static bool parseDouble(double &value, const char *arg) { char c; return sscanf(arg, "%lf%c", &value, &c) == 1; } static bool parseUnicode(unicode_t &unicode, const char *arg) { unsigned uuc; if (parseUnsignedDecOrHex(uuc, arg)) { unicode = uuc; return true; } if (arg[0] == '\'' && arg[1] && arg[2] == '\'' && !arg[3]) { unicode = (unicode_t) (unsigned char) arg[1]; return true; } return false; } static bool parseAngle(double &value, const char *arg) { char c1, c2; int result = sscanf(arg, "%lf%c%c", &value, &c1, &c2); if (result == 1) return true; if (result == 2 && (c1 == 'd' || c1 == 'D')) { value *= M_PI/180; return true; } return false; } static void parseColoring(Shape &shape, const char *edgeAssignment) { unsigned c = 0, e = 0; if (shape.contours.size() < c) return; Contour *contour = &shape.contours[c]; bool change = false; bool clear = true; for (const char *in = edgeAssignment; *in; ++in) { switch (*in) { case ',': if (change) ++e; if (clear) while (e < contour->edges.size()) { contour->edges[e]->color = WHITE; ++e; } ++c, e = 0; if (shape.contours.size() <= c) return; contour = &shape.contours[c]; change = false; clear = true; break; case '?': clear = false; break; case 'C': case 'M': case 'W': case 'Y': case 'c': case 'm': case 'w': case 'y': if (change) { ++e; change = false; } if (e < contour->edges.size()) { contour->edges[e]->color = EdgeColor( (*in == 'C' || *in == 'c')*CYAN| (*in == 'M' || *in == 'm')*MAGENTA| (*in == 'Y' || *in == 'y')*YELLOW| (*in == 'W' || *in == 'w')*WHITE); change = true; } break; } } } static FontHandle * loadVarFont(FreetypeHandle *library, const char *filename) { std::string buffer; while (*filename && *filename != '?') buffer.push_back(*filename++); FontHandle *font = loadFont(library, buffer.c_str()); if (font && *filename++ == '?') { do { buffer.clear(); while (*filename && *filename != '=') buffer.push_back(*filename++); if (*filename == '=') { double value = 0; int skip = 0; if (sscanf(++filename, "%lf%n", &value, &skip) == 1) { setFontVariationAxis(library, font, buffer.c_str(), value); filename += skip; } } } while (*filename++ == '&'); } return font; } template static void invertColor(const BitmapRef &bitmap) { const float *end = bitmap.pixels+N*bitmap.width*bitmap.height; for (float *p = bitmap.pixels; p < end; ++p) *p = 1.f-*p; } static bool writeTextBitmap(FILE *file, const float *values, int cols, int rows) { for (int row = 0; row < rows; ++row) { for (int col = 0; col < cols; ++col) { int v = clamp(int((*values++)*0x100), 0xff); fprintf(file, col ? " %02X" : "%02X", v); } fprintf(file, "\n"); } return true; } static bool writeTextBitmapFloat(FILE *file, const float *values, int cols, int rows) { for (int row = 0; row < rows; ++row) { for (int col = 0; col < cols; ++col) { fprintf(file, col ? " %.9g" : "%.9g", *values++); } fprintf(file, "\n"); } return true; } static bool writeBinBitmap(FILE *file, const float *values, int count) { for (int pos = 0; pos < count; ++pos) { unsigned char v = clamp(int((*values++)*0x100), 0xff); fwrite(&v, 1, 1, file); } return true; } #ifdef __BIG_ENDIAN__ static bool writeBinBitmapFloatBE(FILE *file, const float *values, int count) #else static bool writeBinBitmapFloat(FILE *file, const float *values, int count) #endif { return (int) fwrite(values, sizeof(float), count, file) == count; } #ifdef __BIG_ENDIAN__ static bool writeBinBitmapFloat(FILE *file, const float *values, int count) #else static bool writeBinBitmapFloatBE(FILE *file, const float *values, int count) #endif { for (int pos = 0; pos < count; ++pos) { const unsigned char *b = reinterpret_cast(values++); for (int i = sizeof(float)-1; i >= 0; --i) fwrite(b+i, 1, 1, file); } return true; } static bool cmpExtension(const char *path, const char *ext) { for (const char *a = path+strlen(path)-1, *b = ext+strlen(ext)-1; b >= ext; --a, --b) if (a < path || toupper(*a) != toupper(*b)) return false; return true; } template static const char * writeOutput(const BitmapConstRef &bitmap, const char *filename, Format &format) { if (filename) { if (format == AUTO) { if (cmpExtension(filename, ".png")) format = PNG; else if (cmpExtension(filename, ".bmp")) format = BMP; else if (cmpExtension(filename, ".tif") || cmpExtension(filename, ".tiff")) format = TIFF; else if (cmpExtension(filename, ".txt")) format = TEXT; else if (cmpExtension(filename, ".bin")) format = BINARY; else return "Could not deduce format from output file name."; } switch (format) { case PNG: return savePng(bitmap, filename) ? NULL : "Failed to write output PNG image."; case BMP: return saveBmp(bitmap, filename) ? NULL : "Failed to write output BMP image."; case TIFF: return saveTiff(bitmap, filename) ? NULL : "Failed to write output TIFF image."; case TEXT: case TEXT_FLOAT: { FILE *file = fopen(filename, "w"); if (!file) return "Failed to write output text file."; if (format == TEXT) writeTextBitmap(file, bitmap.pixels, N*bitmap.width, bitmap.height); else if (format == TEXT_FLOAT) writeTextBitmapFloat(file, bitmap.pixels, N*bitmap.width, bitmap.height); fclose(file); return NULL; } case BINARY: case BINARY_FLOAT: case BINARY_FLOAT_BE: { FILE *file = fopen(filename, "wb"); if (!file) return "Failed to write output binary file."; if (format == BINARY) writeBinBitmap(file, bitmap.pixels, N*bitmap.width*bitmap.height); else if (format == BINARY_FLOAT) writeBinBitmapFloat(file, bitmap.pixels, N*bitmap.width*bitmap.height); else if (format == BINARY_FLOAT_BE) writeBinBitmapFloatBE(file, bitmap.pixels, N*bitmap.width*bitmap.height); fclose(file); return NULL; } default:; } } else { if (format == AUTO || format == TEXT) writeTextBitmap(stdout, bitmap.pixels, N*bitmap.width, bitmap.height); else if (format == TEXT_FLOAT) writeTextBitmapFloat(stdout, bitmap.pixels, N*bitmap.width, bitmap.height); else return "Unsupported format for standard output."; } return NULL; } #define STRINGIZE_(x) #x #define STRINGIZE(x) STRINGIZE_(x) #define MSDFGEN_VERSION_STRING STRINGIZE(MSDFGEN_VERSION) #ifdef MSDFGEN_VERSION_UNDERLINE #define VERSION_UNDERLINE STRINGIZE(MSDFGEN_VERSION_UNDERLINE) #else #define VERSION_UNDERLINE "--------" #endif #if defined(MSDFGEN_USE_SKIA) && defined(MSDFGEN_USE_OPENMP) #define TITLE_SUFFIX " with Skia & OpenMP" #define SUFFIX_UNDERLINE "-------------------" #elif defined(MSDFGEN_USE_SKIA) #define TITLE_SUFFIX " with Skia" #define SUFFIX_UNDERLINE "----------" #elif defined(MSDFGEN_USE_OPENMP) #define TITLE_SUFFIX " with OpenMP" #define SUFFIX_UNDERLINE "------------" #else #define TITLE_SUFFIX #define SUFFIX_UNDERLINE #endif static const char * const versionText = "MSDFgen v" MSDFGEN_VERSION_STRING TITLE_SUFFIX "\n" "(c) 2016 - " STRINGIZE(MSDFGEN_COPYRIGHT_YEAR) " Viktor Chlumsky"; static const char * const helpText = "\n" "Multi-channel signed distance field generator by Viktor Chlumsky v" MSDFGEN_VERSION_STRING TITLE_SUFFIX "\n" "------------------------------------------------------------------" VERSION_UNDERLINE SUFFIX_UNDERLINE "\n" " Usage: msdfgen" #ifdef _WIN32 ".exe" #endif " \n" "\n" "MODES\n" " sdf - Generate conventional monochrome (true) signed distance field.\n" " psdf - Generate monochrome signed pseudo-distance field.\n" " msdf - Generate multi-channel signed distance field. This is used by default if no mode is specified.\n" " mtsdf - Generate combined multi-channel and true signed distance field in the alpha channel.\n" " metrics - Report shape metrics only.\n" "\n" "INPUT SPECIFICATION\n" " -defineshape \n" "\tDefines input shape using the ad-hoc text definition.\n" " -font \n" "\tLoads a single glyph from the specified font file.\n" "\tFormat of character code is '?', 63, 0x3F (Unicode value), or g34 (glyph index).\n" " -shapedesc \n" "\tLoads text shape description from a file.\n" " -stdin\n" "\tReads text shape description from the standard input.\n" " -svg \n" "\tLoads the last vector path found in the specified SVG file.\n" " -varfont \n" "\tLoads a single glyph from a variable font. Specify variable values as x.ttf?var1=0.5&var2=1\n" "\n" // Keep alphabetical order! "OPTIONS\n" " -angle \n" "\tSpecifies the minimum angle between adjacent edges to be considered a corner. Append D for degrees.\n" " -ascale \n" "\tSets the scale used to convert shape units to pixels asymmetrically.\n" " -autoframe\n" "\tAutomatically scales (unless specified) and translates the shape to fit.\n" " -coloringstrategy \n" "\tSelects the strategy of the edge coloring heuristic.\n" " -distanceshift \n" "\tShifts all normalized distances in the output distance field by this value.\n" " -edgecolors \n" "\tOverrides automatic edge coloring with the specified color sequence.\n" " -errorcorrection \n" "\tChanges the MSDF/MTSDF error correction mode. Use -errorcorrection help for a list of valid modes.\n" " -errordeviationratio \n" "\tSets the minimum ratio between the actual and maximum expected distance delta to be considered an error.\n" " -errorimproveratio \n" "\tSets the minimum ratio between the pre-correction distance error and the post-correction distance error.\n" " -estimateerror\n" "\tComputes and prints the distance field's estimated fill error to the standard output.\n" " -exportshape \n" "\tSaves the shape description into a text file that can be edited and loaded using -shapedesc.\n" " -fillrule \n" "\tSets the fill rule for the scanline pass. Default is nonzero.\n" " -format \n" "\tSpecifies the output format of the distance field. Otherwise it is chosen based on output file extension.\n" " -guessorder\n" "\tAttempts to detect if shape contours have the wrong winding and generates the SDF with the right one.\n" " -help\n" "\tDisplays this help.\n" " -legacy\n" "\tUses the original (legacy) distance field algorithms.\n" #ifdef MSDFGEN_USE_SKIA " -nopreprocess\n" "\tDisables path preprocessing which resolves self-intersections and overlapping contours.\n" #else " -nooverlap\n" "\tDisables resolution of overlapping contours.\n" " -noscanline\n" "\tDisables the scanline pass, which corrects the distance field's signs according to the selected fill rule.\n" #endif " -o \n" "\tSets the output file name. The default value is \"output.png\".\n" #ifdef MSDFGEN_USE_SKIA " -overlap\n" "\tSwitches to distance field generator with support for overlapping contours.\n" #endif " -printmetrics\n" "\tPrints relevant metrics of the shape to the standard output.\n" " -pxrange \n" "\tSets the width of the range between the lowest and highest signed distance in pixels.\n" " -range \n" "\tSets the width of the range between the lowest and highest signed distance in shape units.\n" " -reverseorder\n" "\tGenerates the distance field as if the shape's vertices were in reverse order.\n" " -scale \n" "\tSets the scale used to convert shape units to pixels.\n" #ifdef MSDFGEN_USE_SKIA " -scanline\n" "\tPerforms an additional scanline pass to fix the signs of the distances.\n" #endif " -seed \n" "\tSets the random seed for edge coloring heuristic.\n" " -size \n" "\tSets the dimensions of the output image.\n" " -stdout\n" "\tPrints the output instead of storing it in a file. Only text formats are supported.\n" " -testrender \n" "\tRenders an image preview using the generated distance field and saves it as a PNG file.\n" " -testrendermulti \n" "\tRenders an image preview without flattening the color channels.\n" " -translate \n" "\tSets the translation of the shape in shape units.\n" " -version\n" "\tPrints the version of the program.\n" " -windingpreprocess\n" "\tAttempts to fix only the contour windings assuming no self-intersections and even-odd fill rule.\n" " -yflip\n" "\tInverts the Y axis in the output distance field. The default order is bottom to top.\n" "\n"; static const char *errorCorrectionHelpText = "\n" "ERROR CORRECTION MODES\n" " auto-fast\n" "\tDetects inversion artifacts and distance errors that do not affect edges by range testing.\n" " auto-full\n" "\tDetects inversion artifacts and distance errors that do not affect edges by exact distance evaluation.\n" " auto-mixed (default)\n" "\tDetects inversions by distance evaluation and distance errors that do not affect edges by range testing.\n" " disabled\n" "\tDisables error correction.\n" " distance-fast\n" "\tDetects distance errors by range testing. Does not care if edges and corners are affected.\n" " distance-full\n" "\tDetects distance errors by exact distance evaluation. Does not care if edges and corners are affected, slow.\n" " edge-fast\n" "\tDetects inversion artifacts only by range testing.\n" " edge-full\n" "\tDetects inversion artifacts only by exact distance evaluation.\n" " help\n" "\tDisplays this help.\n" "\n"; int main(int argc, const char * const *argv) { #define ABORT(msg) { puts(msg); return 1; } // Parse command line arguments enum { NONE, SVG, FONT, VAR_FONT, DESCRIPTION_ARG, DESCRIPTION_STDIN, DESCRIPTION_FILE } inputType = NONE; enum { SINGLE, PSEUDO, MULTI, MULTI_AND_TRUE, METRICS } mode = MULTI; enum { NO_PREPROCESS, WINDING_PREPROCESS, FULL_PREPROCESS } geometryPreproc = ( #ifdef MSDFGEN_USE_SKIA FULL_PREPROCESS #else NO_PREPROCESS #endif ); bool legacyMode = false; MSDFGeneratorConfig generatorConfig; generatorConfig.overlapSupport = geometryPreproc == NO_PREPROCESS; bool scanlinePass = geometryPreproc == NO_PREPROCESS; FillRule fillRule = FILL_NONZERO; Format format = AUTO; const char *input = NULL; const char *output = "output.png"; const char *shapeExport = NULL; const char *testRender = NULL; const char *testRenderMulti = NULL; bool outputSpecified = false; bool glyphIndexSpecified = false; GlyphIndex glyphIndex; unicode_t unicode = 0; int svgPathIndex = 0; int width = 64, height = 64; int testWidth = 0, testHeight = 0; int testWidthM = 0, testHeightM = 0; bool autoFrame = false; enum { RANGE_UNIT, RANGE_PX } rangeMode = RANGE_PX; double range = 1; double pxRange = 2; Vector2 translate; Vector2 scale = 1; bool scaleSpecified = false; double angleThreshold = DEFAULT_ANGLE_THRESHOLD; float outputDistanceShift = 0.f; const char *edgeAssignment = NULL; bool yFlip = false; bool printMetrics = false; bool estimateError = false; bool skipColoring = false; enum { KEEP, REVERSE, GUESS } orientation = KEEP; unsigned long long coloringSeed = 0; void (*edgeColoring)(Shape &, double, unsigned long long) = edgeColoringSimple; bool explicitErrorCorrectionMode = false; int argPos = 1; bool suggestHelp = false; while (argPos < argc) { const char *arg = argv[argPos]; #define ARG_CASE(s, p) if (!strcmp(arg, s) && argPos+(p) < argc) #define ARG_MODE(s, m) if (!strcmp(arg, s)) { mode = m; ++argPos; continue; } #define SET_FORMAT(fmt, ext) do { format = fmt; if (!outputSpecified) output = "output." ext; } while (false) // Accept arguments prefixed with -- instead of - if (arg[0] == '-' && arg[1] == '-') ++arg; ARG_MODE("sdf", SINGLE) ARG_MODE("psdf", PSEUDO) ARG_MODE("msdf", MULTI) ARG_MODE("mtsdf", MULTI_AND_TRUE) ARG_MODE("metrics", METRICS) ARG_CASE("-svg", 1) { inputType = SVG; input = argv[argPos+1]; argPos += 2; continue; } //ARG_CASE -font, -varfont if (argPos+2 < argc && ((!strcmp(arg, "-font") && (inputType = FONT)) || (!strcmp(arg, "-varfont") && (inputType = VAR_FONT)))) { input = argv[argPos+1]; const char *charArg = argv[argPos+2]; unsigned gi; switch (charArg[0]) { case 'G': case 'g': if (parseUnsignedDecOrHex(gi, charArg+1)) { glyphIndex = GlyphIndex(gi); glyphIndexSpecified = true; } break; case 'U': case 'u': ++charArg; default: parseUnicode(unicode, charArg); } argPos += 3; continue; } ARG_CASE("-defineshape", 1) { inputType = DESCRIPTION_ARG; input = argv[argPos+1]; argPos += 2; continue; } ARG_CASE("-stdin", 0) { inputType = DESCRIPTION_STDIN; input = "stdin"; argPos += 1; continue; } ARG_CASE("-shapedesc", 1) { inputType = DESCRIPTION_FILE; input = argv[argPos+1]; argPos += 2; continue; } ARG_CASE("-o", 1) { output = argv[argPos+1]; outputSpecified = true; argPos += 2; continue; } ARG_CASE("-stdout", 0) { output = NULL; argPos += 1; continue; } ARG_CASE("-legacy", 0) { legacyMode = true; argPos += 1; continue; } ARG_CASE("-nopreprocess", 0) { geometryPreproc = NO_PREPROCESS; argPos += 1; continue; } ARG_CASE("-windingpreprocess", 0) { geometryPreproc = WINDING_PREPROCESS; argPos += 1; continue; } ARG_CASE("-preprocess", 0) { geometryPreproc = FULL_PREPROCESS; argPos += 1; continue; } ARG_CASE("-nooverlap", 0) { generatorConfig.overlapSupport = false; argPos += 1; continue; } ARG_CASE("-overlap", 0) { generatorConfig.overlapSupport = true; argPos += 1; continue; } ARG_CASE("-noscanline", 0) { scanlinePass = false; argPos += 1; continue; } ARG_CASE("-scanline", 0) { scanlinePass = true; argPos += 1; continue; } ARG_CASE("-fillrule", 1) { scanlinePass = true; if (!strcmp(argv[argPos+1], "nonzero")) fillRule = FILL_NONZERO; else if (!strcmp(argv[argPos+1], "evenodd") || !strcmp(argv[argPos+1], "odd")) fillRule = FILL_ODD; else if (!strcmp(argv[argPos+1], "positive")) fillRule = FILL_POSITIVE; else if (!strcmp(argv[argPos+1], "negative")) fillRule = FILL_NEGATIVE; else puts("Unknown fill rule specified."); argPos += 2; continue; } ARG_CASE("-format", 1) { if (!strcmp(argv[argPos+1], "auto")) format = AUTO; else if (!strcmp(argv[argPos+1], "png")) SET_FORMAT(PNG, "png"); else if (!strcmp(argv[argPos+1], "bmp")) SET_FORMAT(BMP, "bmp"); else if (!strcmp(argv[argPos+1], "tiff")) SET_FORMAT(TIFF, "tif"); else if (!strcmp(argv[argPos+1], "text") || !strcmp(argv[argPos+1], "txt")) SET_FORMAT(TEXT, "txt"); else if (!strcmp(argv[argPos+1], "textfloat") || !strcmp(argv[argPos+1], "txtfloat")) SET_FORMAT(TEXT_FLOAT, "txt"); else if (!strcmp(argv[argPos+1], "bin") || !strcmp(argv[argPos+1], "binary")) SET_FORMAT(BINARY, "bin"); else if (!strcmp(argv[argPos+1], "binfloat") || !strcmp(argv[argPos+1], "binfloatle")) SET_FORMAT(BINARY_FLOAT, "bin"); else if (!strcmp(argv[argPos+1], "binfloatbe")) SET_FORMAT(BINARY_FLOAT_BE, "bin"); else puts("Unknown format specified."); argPos += 2; continue; } ARG_CASE("-size", 2) { unsigned w, h; if (!(parseUnsigned(w, argv[argPos+1]) && parseUnsigned(h, argv[argPos+2]) && w && h)) ABORT("Invalid size arguments. Use -size with two positive integers."); width = w, height = h; argPos += 3; continue; } ARG_CASE("-autoframe", 0) { autoFrame = true; argPos += 1; continue; } ARG_CASE("-range", 1) { double r; if (!(parseDouble(r, argv[argPos+1]) && r > 0)) ABORT("Invalid range argument. Use -range with a positive real number."); rangeMode = RANGE_UNIT; range = r; argPos += 2; continue; } ARG_CASE("-pxrange", 1) { double r; if (!(parseDouble(r, argv[argPos+1]) && r > 0)) ABORT("Invalid range argument. Use -pxrange with a positive real number."); rangeMode = RANGE_PX; pxRange = r; argPos += 2; continue; } ARG_CASE("-scale", 1) { double s; if (!(parseDouble(s, argv[argPos+1]) && s > 0)) ABORT("Invalid scale argument. Use -scale with a positive real number."); scale = s; scaleSpecified = true; argPos += 2; continue; } ARG_CASE("-ascale", 2) { double sx, sy; if (!(parseDouble(sx, argv[argPos+1]) && parseDouble(sy, argv[argPos+2]) && sx > 0 && sy > 0)) ABORT("Invalid scale arguments. Use -ascale with two positive real numbers."); scale.set(sx, sy); scaleSpecified = true; argPos += 3; continue; } ARG_CASE("-translate", 2) { double tx, ty; if (!(parseDouble(tx, argv[argPos+1]) && parseDouble(ty, argv[argPos+2]))) ABORT("Invalid translate arguments. Use -translate with two real numbers."); translate.set(tx, ty); argPos += 3; continue; } ARG_CASE("-angle", 1) { double at; if (!parseAngle(at, argv[argPos+1])) ABORT("Invalid angle threshold. Use -angle with a positive real number less than PI or a value in degrees followed by 'd' below 180d."); angleThreshold = at; argPos += 2; continue; } ARG_CASE("-errorcorrection", 1) { if (!strcmp(argv[argPos+1], "disabled") || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "none")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::DISABLED; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "default") || !strcmp(argv[argPos+1], "auto") || !strcmp(argv[argPos+1], "auto-mixed") || !strcmp(argv[argPos+1], "mixed")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::EDGE_PRIORITY; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::CHECK_DISTANCE_AT_EDGE; } else if (!strcmp(argv[argPos+1], "auto-fast") || !strcmp(argv[argPos+1], "fast")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::EDGE_PRIORITY; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "auto-full") || !strcmp(argv[argPos+1], "full")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::EDGE_PRIORITY; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::ALWAYS_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "distance") || !strcmp(argv[argPos+1], "distance-fast") || !strcmp(argv[argPos+1], "indiscriminate") || !strcmp(argv[argPos+1], "indiscriminate-fast")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::INDISCRIMINATE; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "distance-full") || !strcmp(argv[argPos+1], "indiscriminate-full")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::INDISCRIMINATE; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::ALWAYS_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "edge-fast")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::EDGE_ONLY; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "edge") || !strcmp(argv[argPos+1], "edge-full")) { generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::EDGE_ONLY; generatorConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::ALWAYS_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "help")) { puts(errorCorrectionHelpText); return 0; } else puts("Unknown error correction mode. Use -errorcorrection help for more information."); explicitErrorCorrectionMode = true; argPos += 2; continue; } ARG_CASE("-errordeviationratio", 1) { double edr; if (!(parseDouble(edr, argv[argPos+1]) && edr > 0)) ABORT("Invalid error deviation ratio. Use -errordeviationratio with a positive real number."); generatorConfig.errorCorrection.minDeviationRatio = edr; argPos += 2; continue; } ARG_CASE("-errorimproveratio", 1) { double eir; if (!(parseDouble(eir, argv[argPos+1]) && eir > 0)) ABORT("Invalid error improvement ratio. Use -errorimproveratio with a positive real number."); generatorConfig.errorCorrection.minImproveRatio = eir; argPos += 2; continue; } ARG_CASE("-coloringstrategy", 1) { if (!strcmp(argv[argPos+1], "simple")) edgeColoring = edgeColoringSimple; else if (!strcmp(argv[argPos+1], "inktrap")) edgeColoring = edgeColoringInkTrap; else if (!strcmp(argv[argPos+1], "distance")) edgeColoring = edgeColoringByDistance; else puts("Unknown coloring strategy specified."); argPos += 2; continue; } ARG_CASE("-edgecolors", 1) { static const char *allowed = " ?,cmwyCMWY"; for (int i = 0; argv[argPos+1][i]; ++i) { for (int j = 0; allowed[j]; ++j) if (argv[argPos+1][i] == allowed[j]) goto EDGE_COLOR_VERIFIED; ABORT("Invalid edge coloring sequence. Use -edgecolors with only the colors C, M, Y, and W. Separate contours by commas and use ? to keep the default assigment for a contour."); EDGE_COLOR_VERIFIED:; } edgeAssignment = argv[argPos+1]; argPos += 2; continue; } ARG_CASE("-distanceshift", 1) { double ds; if (!parseDouble(ds, argv[argPos+1])) ABORT("Invalid distance shift. Use -distanceshift with a real value."); outputDistanceShift = (float) ds; argPos += 2; continue; } ARG_CASE("-exportshape", 1) { shapeExport = argv[argPos+1]; argPos += 2; continue; } ARG_CASE("-testrender", 3) { unsigned w, h; if (!parseUnsigned(w, argv[argPos+2]) || !parseUnsigned(h, argv[argPos+3]) || !w || !h) ABORT("Invalid arguments for test render. Use -testrender ."); testRender = argv[argPos+1]; testWidth = w, testHeight = h; argPos += 4; continue; } ARG_CASE("-testrendermulti", 3) { unsigned w, h; if (!parseUnsigned(w, argv[argPos+2]) || !parseUnsigned(h, argv[argPos+3]) || !w || !h) ABORT("Invalid arguments for test render. Use -testrendermulti ."); testRenderMulti = argv[argPos+1]; testWidthM = w, testHeightM = h; argPos += 4; continue; } ARG_CASE("-yflip", 0) { yFlip = true; argPos += 1; continue; } ARG_CASE("-printmetrics", 0) { printMetrics = true; argPos += 1; continue; } ARG_CASE("-estimateerror", 0) { estimateError = true; argPos += 1; continue; } ARG_CASE("-keeporder", 0) { orientation = KEEP; argPos += 1; continue; } ARG_CASE("-reverseorder", 0) { orientation = REVERSE; argPos += 1; continue; } ARG_CASE("-guessorder", 0) { orientation = GUESS; argPos += 1; continue; } ARG_CASE("-seed", 1) { if (!parseUnsignedLL(coloringSeed, argv[argPos+1])) ABORT("Invalid seed. Use -seed with N being a non-negative integer."); argPos += 2; continue; } ARG_CASE("-version", 0) { puts(versionText); return 0; } ARG_CASE("-help", 0) { puts(helpText); return 0; } printf("Unknown setting or insufficient parameters: %s\n", argv[argPos]); suggestHelp = true; ++argPos; } if (suggestHelp) printf("Use -help for more information.\n"); // Load input Vector2 svgDims; double glyphAdvance = 0; if (!inputType || !input) ABORT("No input specified! Use either -svg or -font , or see -help."); if (mode == MULTI_AND_TRUE && (format == BMP || (format == AUTO && output && cmpExtension(output, ".bmp")))) ABORT("Incompatible image format. A BMP file cannot contain alpha channel, which is required in mtsdf mode."); Shape shape; switch (inputType) { case SVG: { if (!loadSvgShape(shape, input, svgPathIndex, &svgDims)) ABORT("Failed to load shape from SVG file."); break; } case FONT: case VAR_FONT: { if (!glyphIndexSpecified && !unicode) ABORT("No character specified! Use -font . Character code can be a Unicode index (65, 0x41), a character in apostrophes ('A'), or a glyph index prefixed by g (g36, g0x24)."); FreetypeHandle *ft = initializeFreetype(); if (!ft) return -1; FontHandle *font = inputType == VAR_FONT ? loadVarFont(ft, input) : loadFont(ft, input); if (!font) { deinitializeFreetype(ft); ABORT("Failed to load font file."); } if (unicode) getGlyphIndex(glyphIndex, font, unicode); if (!loadGlyph(shape, font, glyphIndex, &glyphAdvance)) { destroyFont(font); deinitializeFreetype(ft); ABORT("Failed to load glyph from font file."); } destroyFont(font); deinitializeFreetype(ft); break; } case DESCRIPTION_ARG: { if (!readShapeDescription(input, shape, &skipColoring)) ABORT("Parse error in shape description."); break; } case DESCRIPTION_STDIN: { if (!readShapeDescription(stdin, shape, &skipColoring)) ABORT("Parse error in shape description."); break; } case DESCRIPTION_FILE: { FILE *file = fopen(input, "r"); if (!file) ABORT("Failed to load shape description file."); if (!readShapeDescription(file, shape, &skipColoring)) ABORT("Parse error in shape description."); fclose(file); break; } default:; } // Validate and normalize shape if (!shape.validate()) ABORT("The geometry of the loaded shape is invalid."); switch (geometryPreproc) { case NO_PREPROCESS: break; case WINDING_PREPROCESS: shape.orientContours(); break; case FULL_PREPROCESS: #ifdef MSDFGEN_USE_SKIA if (!resolveShapeGeometry(shape)) puts("Shape geometry preprocessing failed, skipping."); else if (skipColoring) { skipColoring = false; puts("Note: Input shape coloring won't be preserved due to geometry preprocessing"); } #else ABORT("Shape geometry preprocessing (-preprocess) is not available in this version because the Skia library is not present."); #endif break; } shape.normalize(); if (yFlip) shape.inverseYAxis = !shape.inverseYAxis; double avgScale = .5*(scale.x+scale.y); Shape::Bounds bounds = { }; if (autoFrame || mode == METRICS || printMetrics || orientation == GUESS) bounds = shape.getBounds(); // Auto-frame if (autoFrame) { double l = bounds.l, b = bounds.b, r = bounds.r, t = bounds.t; Vector2 frame(width, height); double m = .5+(double) outputDistanceShift; if (!scaleSpecified) { if (rangeMode == RANGE_UNIT) l -= m*range, b -= m*range, r += m*range, t += m*range; else frame -= 2*m*pxRange; } if (l >= r || b >= t) l = 0, b = 0, r = 1, t = 1; if (frame.x <= 0 || frame.y <= 0) ABORT("Cannot fit the specified pixel range."); Vector2 dims(r-l, t-b); if (scaleSpecified) translate = .5*(frame/scale-dims)-Vector2(l, b); else { if (dims.x*frame.y < dims.y*frame.x) { translate.set(.5*(frame.x/frame.y*dims.y-dims.x)-l, -b); scale = avgScale = frame.y/dims.y; } else { translate.set(-l, .5*(frame.y/frame.x*dims.x-dims.y)-b); scale = avgScale = frame.x/dims.x; } } if (rangeMode == RANGE_PX && !scaleSpecified) translate += m*pxRange/scale; } if (rangeMode == RANGE_PX) range = pxRange/min(scale.x, scale.y); // Print metrics if (mode == METRICS || printMetrics) { FILE *out = stdout; if (mode == METRICS && outputSpecified) out = fopen(output, "w"); if (!out) ABORT("Failed to write output file."); if (shape.inverseYAxis) fprintf(out, "inverseY = true\n"); if (bounds.r >= bounds.l && bounds.t >= bounds.b) fprintf(out, "bounds = %.12g, %.12g, %.12g, %.12g\n", bounds.l, bounds.b, bounds.r, bounds.t); if (svgDims.x != 0 && svgDims.y != 0) fprintf(out, "dimensions = %.12g, %.12g\n", svgDims.x, svgDims.y); if (glyphAdvance != 0) fprintf(out, "advance = %.12g\n", glyphAdvance); if (autoFrame) { if (!scaleSpecified) fprintf(out, "scale = %.12g\n", avgScale); fprintf(out, "translate = %.12g, %.12g\n", translate.x, translate.y); } if (rangeMode == RANGE_PX) fprintf(out, "range = %.12g\n", range); if (mode == METRICS && outputSpecified) fclose(out); } // Compute output Projection projection(scale, translate); Bitmap sdf; Bitmap msdf; Bitmap mtsdf; MSDFGeneratorConfig postErrorCorrectionConfig(generatorConfig); if (scanlinePass) { if (explicitErrorCorrectionMode && generatorConfig.errorCorrection.distanceCheckMode != ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE) { const char *fallbackModeName = "unknown"; switch (generatorConfig.errorCorrection.mode) { case ErrorCorrectionConfig::DISABLED: fallbackModeName = "disabled"; break; case ErrorCorrectionConfig::INDISCRIMINATE: fallbackModeName = "distance-fast"; break; case ErrorCorrectionConfig::EDGE_PRIORITY: fallbackModeName = "auto-fast"; break; case ErrorCorrectionConfig::EDGE_ONLY: fallbackModeName = "edge-fast"; break; } printf("Selected error correction mode not compatible with scanline pass, falling back to %s.\n", fallbackModeName); } generatorConfig.errorCorrection.mode = ErrorCorrectionConfig::DISABLED; postErrorCorrectionConfig.errorCorrection.distanceCheckMode = ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } switch (mode) { case SINGLE: { sdf = Bitmap(width, height); if (legacyMode) generateSDF_legacy(sdf, shape, range, scale, translate); else generateSDF(sdf, shape, projection, range, generatorConfig); break; } case PSEUDO: { sdf = Bitmap(width, height); if (legacyMode) generatePseudoSDF_legacy(sdf, shape, range, scale, translate); else generatePseudoSDF(sdf, shape, projection, range, generatorConfig); break; } case MULTI: { if (!skipColoring) edgeColoring(shape, angleThreshold, coloringSeed); if (edgeAssignment) parseColoring(shape, edgeAssignment); msdf = Bitmap(width, height); if (legacyMode) generateMSDF_legacy(msdf, shape, range, scale, translate, generatorConfig.errorCorrection); else generateMSDF(msdf, shape, projection, range, generatorConfig); break; } case MULTI_AND_TRUE: { if (!skipColoring) edgeColoring(shape, angleThreshold, coloringSeed); if (edgeAssignment) parseColoring(shape, edgeAssignment); mtsdf = Bitmap(width, height); if (legacyMode) generateMTSDF_legacy(mtsdf, shape, range, scale, translate, generatorConfig.errorCorrection); else generateMTSDF(mtsdf, shape, projection, range, generatorConfig); break; } default:; } if (orientation == GUESS) { // Get sign of signed distance outside bounds Point2 p(bounds.l-(bounds.r-bounds.l)-1, bounds.b-(bounds.t-bounds.b)-1); double distance = SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p); orientation = distance <= 0 ? KEEP : REVERSE; } if (orientation == REVERSE) { switch (mode) { case SINGLE: case PSEUDO: invertColor<1>(sdf); break; case MULTI: invertColor<3>(msdf); break; case MULTI_AND_TRUE: invertColor<4>(mtsdf); break; default:; } } if (scanlinePass) { switch (mode) { case SINGLE: case PSEUDO: distanceSignCorrection(sdf, shape, projection, fillRule); break; case MULTI: distanceSignCorrection(msdf, shape, projection, fillRule); msdfErrorCorrection(msdf, shape, projection, range, postErrorCorrectionConfig); break; case MULTI_AND_TRUE: distanceSignCorrection(mtsdf, shape, projection, fillRule); msdfErrorCorrection(msdf, shape, projection, range, postErrorCorrectionConfig); break; default:; } } if (outputDistanceShift) { float *pixel = NULL, *pixelsEnd = NULL; switch (mode) { case SINGLE: case PSEUDO: pixel = (float *) sdf; pixelsEnd = pixel+1*sdf.width()*sdf.height(); break; case MULTI: pixel = (float *) msdf; pixelsEnd = pixel+3*msdf.width()*msdf.height(); break; case MULTI_AND_TRUE: pixel = (float *) mtsdf; pixelsEnd = pixel+4*mtsdf.width()*mtsdf.height(); break; default:; } while (pixel < pixelsEnd) *pixel++ += outputDistanceShift; } // Save output if (shapeExport) { FILE *file = fopen(shapeExport, "w"); if (file) { writeShapeDescription(file, shape); fclose(file); } else puts("Failed to write shape export file."); } const char *error = NULL; switch (mode) { case SINGLE: case PSEUDO: error = writeOutput<1>(sdf, output, format); if (error) ABORT(error); if (is8bitFormat(format) && (testRenderMulti || testRender || estimateError)) simulate8bit(sdf); if (estimateError) { double sdfError = estimateSDFError(sdf, shape, projection, SDF_ERROR_ESTIMATE_PRECISION, fillRule); printf("SDF error ~ %e\n", sdfError); } if (testRenderMulti) { Bitmap render(testWidthM, testHeightM); renderSDF(render, sdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRenderMulti)) puts("Failed to write test render file."); } if (testRender) { Bitmap render(testWidth, testHeight); renderSDF(render, sdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRender)) puts("Failed to write test render file."); } break; case MULTI: error = writeOutput<3>(msdf, output, format); if (error) ABORT(error); if (is8bitFormat(format) && (testRenderMulti || testRender || estimateError)) simulate8bit(msdf); if (estimateError) { double sdfError = estimateSDFError(msdf, shape, projection, SDF_ERROR_ESTIMATE_PRECISION, fillRule); printf("SDF error ~ %e\n", sdfError); } if (testRenderMulti) { Bitmap render(testWidthM, testHeightM); renderSDF(render, msdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRenderMulti)) puts("Failed to write test render file."); } if (testRender) { Bitmap render(testWidth, testHeight); renderSDF(render, msdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRender)) ABORT("Failed to write test render file."); } break; case MULTI_AND_TRUE: error = writeOutput<4>(mtsdf, output, format); if (error) ABORT(error); if (is8bitFormat(format) && (testRenderMulti || testRender || estimateError)) simulate8bit(mtsdf); if (estimateError) { double sdfError = estimateSDFError(mtsdf, shape, projection, SDF_ERROR_ESTIMATE_PRECISION, fillRule); printf("SDF error ~ %e\n", sdfError); } if (testRenderMulti) { Bitmap render(testWidthM, testHeightM); renderSDF(render, mtsdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRenderMulti)) puts("Failed to write test render file."); } if (testRender) { Bitmap render(testWidth, testHeight); renderSDF(render, mtsdf, avgScale*range, .5f+outputDistanceShift); if (!savePng(render, testRender)) ABORT("Failed to write test render file."); } break; default:; } return 0; } #endif