/* * MULTI-CHANNEL SIGNED DISTANCE FIELD ATLAS GENERATOR - standalone console program * -------------------------------------------------------------------------------- * A utility by Viktor Chlumsky, (c) 2020 - 2023 */ #ifdef MSDF_ATLAS_STANDALONE #define _USE_MATH_DEFINES #include #include #include #include #include #include #include #include "msdf-atlas-gen.h" using namespace msdf_atlas; #define DEFAULT_ANGLE_THRESHOLD 3.0 #define DEFAULT_MITER_LIMIT 1.0 #define DEFAULT_PIXEL_RANGE 2.0 #define SDF_ERROR_ESTIMATE_PRECISION 19 #define GLYPH_FILL_RULE msdfgen::FILL_NONZERO #define LCG_MULTIPLIER 6364136223846793005ull #define LCG_INCREMENT 1442695040888963407ull #define STRINGIZE_(x) #x #define STRINGIZE(x) STRINGIZE_(x) #define MSDF_ATLAS_VERSION_STRING STRINGIZE(MSDF_ATLAS_VERSION) #define MSDFGEN_VERSION_STRING STRINGIZE(MSDFGEN_VERSION) #ifdef MSDF_ATLAS_VERSION_UNDERLINE #define VERSION_UNDERLINE STRINGIZE(MSDF_ATLAS_VERSION_UNDERLINE) #else #define VERSION_UNDERLINE "--------" #endif #ifdef MSDFGEN_USE_SKIA #define TITLE_SUFFIX " & Skia" #define EXTRA_UNDERLINE "-------" #else #define TITLE_SUFFIX #define EXTRA_UNDERLINE #endif static const char * const versionText = "MSDF-Atlas-Gen v" MSDF_ATLAS_VERSION_STRING "\n" " with MSDFgen v" MSDFGEN_VERSION_STRING TITLE_SUFFIX "\n" "(c) 2020 - " STRINGIZE(MSDF_ATLAS_COPYRIGHT_YEAR) " Viktor Chlumsky"; static const char * const helpText = R"( MSDF Atlas Generator by Viktor Chlumsky v)" MSDF_ATLAS_VERSION_STRING R"( (with MSDFgen v)" MSDFGEN_VERSION_STRING TITLE_SUFFIX R"() ----------------------------------------------------------------)" VERSION_UNDERLINE EXTRA_UNDERLINE R"( INPUT SPECIFICATION -font Specifies the input TrueType / OpenType font file. A font specification is required. -varfont Specifies an input variable font file and configures its variables. -charset Specifies the input character set. Refer to the documentation for format of charset specification. Defaults to ASCII. -glyphset Specifies the set of input glyphs as glyph indices within the font file. -fontscale Specifies the scale to be applied to the glyph geometry of the font. -fontname Specifies a name for the font that will be propagated into the output files as metadata. -and Separates multiple inputs to be combined into a single atlas. ATLAS CONFIGURATION -type Selects the type of atlas to be generated. -format Selects the format for the atlas image output. Some image formats may be incompatible with embedded output formats. -dimensions Sets the atlas to have fixed dimensions (width x height). -pots / -potr / -square / -square2 / -square4 Picks the minimum atlas dimensions that fit all glyphs and satisfy the selected constraint: power of two square / ... rectangle / any square / square with side divisible by 2 / ... 4 -yorigin Determines whether the Y-axis is oriented upwards (bottom origin, default) or downwards (top origin). OUTPUT SPECIFICATION - one or more can be specified -imageout Saves the atlas as an image file with the specified format. Layout data must be stored separately. -json Writes the atlas's layout data, as well as other metrics into a structured JSON file. -csv Writes the layout data of the glyphs into a simple CSV file.)" #ifndef MSDF_ATLAS_NO_ARTERY_FONT R"( -arfont Stores the atlas and its layout data as an Artery Font file. Supported formats: png, bin, binfloat.)" #endif R"( -shadronpreview Generates a Shadron script that uses the generated atlas to draw a sample text as a preview. GLYPH CONFIGURATION -size Specifies the size of the glyphs in the atlas bitmap in pixels per EM. -minsize Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used. -emrange Specifies the SDF distance range in EM's. -pxrange Specifies the SDF distance range in output pixels. The default value is 2. -nokerning Disables inclusion of kerning pair table in output files. DISTANCE FIELD GENERATOR SETTINGS -angle Specifies the minimum angle between adjacent edges to be considered a corner. Append D for degrees. (msdf / mtsdf only) -coloringstrategy Selects the strategy of the edge coloring heuristic. -errorcorrection Changes the MSDF/MTSDF error correction mode. Use -errorcorrection help for a list of valid modes. -errordeviationratio Sets the minimum ratio between the actual and maximum expected distance delta to be considered an error. -errorimproveratio Sets the minimum ratio between the pre-correction distance error and the post-correction distance error. -miterlimit Sets the miter limit that limits the extension of each glyph's bounding box due to very sharp corners. (psdf / msdf / mtsdf only))" #ifdef MSDFGEN_USE_SKIA R"( -overlap Switches to distance field generator with support for overlapping contours. -nopreprocess Disables path preprocessing which resolves self-intersections and overlapping contours. -scanline Performs an additional scanline pass to fix the signs of the distances.)" #else R"( -nooverlap Disables resolution of overlapping contours. -noscanline Disables the scanline pass, which corrects the distance field's signs according to the non-zero fill rule.)" #endif R"( -seed Sets the initial seed for the edge coloring heuristic. -threads Sets the number of threads for the parallel computation. (0 = auto) )"; static const char *errorCorrectionHelpText = R"( ERROR CORRECTION MODES auto-fast Detects inversion artifacts and distance errors that do not affect edges by range testing. auto-full Detects inversion artifacts and distance errors that do not affect edges by exact distance evaluation. auto-mixed (default) Detects inversions by distance evaluation and distance errors that do not affect edges by range testing. disabled Disables error correction. distance-fast Detects distance errors by range testing. Does not care if edges and corners are affected. distance-full Detects distance errors by exact distance evaluation. Does not care if edges and corners are affected, slow. edge-fast Detects inversion artifacts only by range testing. edge-full Detects inversion artifacts only by exact distance evaluation. help Displays this help. )"; static char toupper(char c) { return c >= 'a' && c <= 'z' ? c-'a'+'A' : c; } static bool parseUnsigned(unsigned &value, const char *arg) { static char c; return sscanf(arg, "%u%c", &value, &c) == 1; } static bool parseUnsignedLL(unsigned long long &value, const char *arg) { static char c; return sscanf(arg, "%llu%c", &value, &c) == 1; } static bool parseDouble(double &value, const char *arg) { static char c; return sscanf(arg, "%lf%c", &value, &c) == 1; } 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 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; } static msdfgen::FontHandle * loadVarFont(msdfgen::FreetypeHandle *library, const char *filename) { std::string buffer; while (*filename && *filename != '?') buffer.push_back(*filename++); msdfgen::FontHandle *font = msdfgen::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) { msdfgen::setFontVariationAxis(library, font, buffer.c_str(), value); filename += skip; } } } while (*filename++ == '&'); } return font; } struct FontInput { const char *fontFilename; bool variableFont; GlyphIdentifierType glyphIdentifierType; const char *charsetFilename; double fontScale; const char *fontName; }; struct Configuration { ImageType imageType; ImageFormat imageFormat; YDirection yDirection; int width, height; double emSize; double pxRange; double angleThreshold; double miterLimit; void (*edgeColoring)(msdfgen::Shape &, double, unsigned long long); bool expensiveColoring; unsigned long long coloringSeed; GeneratorAttributes generatorAttributes; bool preprocessGeometry; bool kerning; int threadCount; const char *arteryFontFilename; const char *imageFilename; const char *jsonFilename; const char *csvFilename; const char *shadronPreviewFilename; const char *shadronPreviewText; }; template GEN_FN> static bool makeAtlas(const std::vector &glyphs, const std::vector &fonts, const Configuration &config) { ImmediateAtlasGenerator > generator(config.width, config.height); generator.setAttributes(config.generatorAttributes); generator.setThreadCount(config.threadCount); generator.generate(glyphs.data(), glyphs.size()); msdfgen::BitmapConstRef bitmap = (msdfgen::BitmapConstRef) generator.atlasStorage(); bool success = true; if (config.imageFilename) { if (saveImage(bitmap, config.imageFormat, config.imageFilename, config.yDirection)) puts("Atlas image file saved."); else { success = false; puts("Failed to save the atlas as an image file."); } } #ifndef MSDF_ATLAS_NO_ARTERY_FONT if (config.arteryFontFilename) { ArteryFontExportProperties arfontProps; arfontProps.fontSize = config.emSize; arfontProps.pxRange = config.pxRange; arfontProps.imageType = config.imageType; arfontProps.imageFormat = config.imageFormat; arfontProps.yDirection = config.yDirection; if (exportArteryFont(fonts.data(), fonts.size(), bitmap, config.arteryFontFilename, arfontProps)) puts("Artery Font file generated."); else { success = false; puts("Failed to generate Artery Font file."); } } #endif return success; } int main(int argc, const char * const *argv) { #define ABORT(msg) { puts(msg); return 1; } int result = 0; std::vector fontInputs; FontInput fontInput = { }; Configuration config = { }; fontInput.glyphIdentifierType = GlyphIdentifierType::UNICODE_CODEPOINT; fontInput.fontScale = -1; config.imageType = ImageType::MSDF; config.imageFormat = ImageFormat::UNSPECIFIED; config.yDirection = YDirection::BOTTOM_UP; config.edgeColoring = msdfgen::edgeColoringInkTrap; config.kerning = true; const char *imageFormatName = nullptr; int fixedWidth = -1, fixedHeight = -1; config.preprocessGeometry = ( #ifdef MSDFGEN_USE_SKIA true #else false #endif ); config.generatorAttributes.config.overlapSupport = !config.preprocessGeometry; config.generatorAttributes.scanlinePass = !config.preprocessGeometry; double minEmSize = 0; enum { /// Range specified in EMs RANGE_EM, /// Range specified in output pixels RANGE_PIXEL, } rangeMode = RANGE_PIXEL; double rangeValue = 0; TightAtlasPacker::DimensionsConstraint atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE; config.angleThreshold = DEFAULT_ANGLE_THRESHOLD; config.miterLimit = DEFAULT_MITER_LIMIT; config.threadCount = 0; // Parse command line int argPos = 1; bool suggestHelp = false; bool explicitErrorCorrectionMode = false; while (argPos < argc) { const char *arg = argv[argPos]; #define ARG_CASE(s, p) if (!strcmp(arg, s) && argPos+(p) < argc) // Accept arguments prefixed with -- instead of - if (arg[0] == '-' && arg[1] == '-') ++arg; ARG_CASE("-type", 1) { arg = argv[++argPos]; if (!strcmp(arg, "hardmask")) config.imageType = ImageType::HARD_MASK; else if (!strcmp(arg, "softmask")) config.imageType = ImageType::SOFT_MASK; else if (!strcmp(arg, "sdf")) config.imageType = ImageType::SDF; else if (!strcmp(arg, "psdf")) config.imageType = ImageType::PSDF; else if (!strcmp(arg, "msdf")) config.imageType = ImageType::MSDF; else if (!strcmp(arg, "mtsdf")) config.imageType = ImageType::MTSDF; else ABORT("Invalid atlas type. Valid types are: hardmask, softmask, sdf, psdf, msdf, mtsdf"); ++argPos; continue; } ARG_CASE("-format", 1) { arg = argv[++argPos]; if (!strcmp(arg, "png")) config.imageFormat = ImageFormat::PNG; else if (!strcmp(arg, "bmp")) config.imageFormat = ImageFormat::BMP; else if (!strcmp(arg, "tiff")) config.imageFormat = ImageFormat::TIFF; else if (!strcmp(arg, "text")) config.imageFormat = ImageFormat::TEXT; else if (!strcmp(arg, "textfloat")) config.imageFormat = ImageFormat::TEXT_FLOAT; else if (!strcmp(arg, "bin")) config.imageFormat = ImageFormat::BINARY; else if (!strcmp(arg, "binfloat")) config.imageFormat = ImageFormat::BINARY_FLOAT; else if (!strcmp(arg, "binfloatbe")) config.imageFormat = ImageFormat::BINARY_FLOAT_BE; else ABORT("Invalid image format. Valid formats are: png, bmp, tiff, text, textfloat, bin, binfloat"); imageFormatName = arg; ++argPos; continue; } ARG_CASE("-font", 1) { fontInput.fontFilename = argv[++argPos]; fontInput.variableFont = false; ++argPos; continue; } ARG_CASE("-varfont", 1) { fontInput.fontFilename = argv[++argPos]; fontInput.variableFont = true; ++argPos; continue; } ARG_CASE("-charset", 1) { fontInput.charsetFilename = argv[++argPos]; fontInput.glyphIdentifierType = GlyphIdentifierType::UNICODE_CODEPOINT; ++argPos; continue; } ARG_CASE("-glyphset", 1) { fontInput.charsetFilename = argv[++argPos]; fontInput.glyphIdentifierType = GlyphIdentifierType::GLYPH_INDEX; ++argPos; continue; } ARG_CASE("-fontscale", 1) { double fs; if (!(parseDouble(fs, argv[++argPos]) && fs > 0)) ABORT("Invalid font scale argument. Use -fontscale with a positive real number."); fontInput.fontScale = fs; ++argPos; continue; } ARG_CASE("-fontname", 1) { fontInput.fontName = argv[++argPos]; ++argPos; continue; } ARG_CASE("-and", 0) { if (!fontInput.fontFilename && !fontInput.charsetFilename && fontInput.fontScale < 0) ABORT("No font, character set, or font scale specified before -and separator."); if (!fontInputs.empty() && !memcmp(&fontInputs.back(), &fontInput, sizeof(FontInput))) ABORT("No changes between subsequent inputs. A different font, character set, or font scale must be set inbetween -and separators."); fontInputs.push_back(fontInput); fontInput.fontName = nullptr; ++argPos; continue; } #ifndef MSDF_ATLAS_NO_ARTERY_FONT ARG_CASE("-arfont", 1) { config.arteryFontFilename = argv[++argPos]; ++argPos; continue; } #endif ARG_CASE("-imageout", 1) { config.imageFilename = argv[++argPos]; ++argPos; continue; } ARG_CASE("-json", 1) { config.jsonFilename = argv[++argPos]; ++argPos; continue; } ARG_CASE("-csv", 1) { config.csvFilename = argv[++argPos]; ++argPos; continue; } ARG_CASE("-shadronpreview", 2) { config.shadronPreviewFilename = argv[++argPos]; config.shadronPreviewText = argv[++argPos]; ++argPos; continue; } ARG_CASE("-dimensions", 2) { unsigned w, h; if (!(parseUnsigned(w, argv[argPos+1]) && parseUnsigned(h, argv[argPos+2]) && w && h)) ABORT("Invalid atlas dimensions. Use -dimensions with two positive integers."); fixedWidth = w, fixedHeight = h; argPos += 3; continue; } ARG_CASE("-pots", 0) { atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_SQUARE; fixedWidth = -1, fixedHeight = -1; ++argPos; continue; } ARG_CASE("-potr", 0) { atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::POWER_OF_TWO_RECTANGLE; fixedWidth = -1, fixedHeight = -1; ++argPos; continue; } ARG_CASE("-square", 0) { atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::SQUARE; fixedWidth = -1, fixedHeight = -1; ++argPos; continue; } ARG_CASE("-square2", 0) { atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::EVEN_SQUARE; fixedWidth = -1, fixedHeight = -1; ++argPos; continue; } ARG_CASE("-square4", 0) { atlasSizeConstraint = TightAtlasPacker::DimensionsConstraint::MULTIPLE_OF_FOUR_SQUARE; fixedWidth = -1, fixedHeight = -1; ++argPos; continue; } ARG_CASE("-yorigin", 1) { arg = argv[++argPos]; if (!strcmp(arg, "bottom")) config.yDirection = YDirection::BOTTOM_UP; else if (!strcmp(arg, "top")) config.yDirection = YDirection::TOP_DOWN; else ABORT("Invalid Y-axis origin. Use bottom or top."); ++argPos; continue; } ARG_CASE("-size", 1) { double s; if (!(parseDouble(s, argv[++argPos]) && s > 0)) ABORT("Invalid EM size argument. Use -size with a positive real number."); config.emSize = s; ++argPos; continue; } ARG_CASE("-minsize", 1) { double s; if (!(parseDouble(s, argv[++argPos]) && s > 0)) ABORT("Invalid minimum EM size argument. Use -minsize with a positive real number."); minEmSize = s; ++argPos; continue; } ARG_CASE("-emrange", 1) { double r; if (!(parseDouble(r, argv[++argPos]) && r >= 0)) ABORT("Invalid range argument. Use -emrange with a positive real number."); rangeMode = RANGE_EM; rangeValue = r; ++argPos; continue; } ARG_CASE("-pxrange", 1) { double r; if (!(parseDouble(r, argv[++argPos]) && r >= 0)) ABORT("Invalid range argument. Use -pxrange with a positive real number."); rangeMode = RANGE_PIXEL; rangeValue = r; ++argPos; 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."); config.angleThreshold = at; argPos += 2; continue; } ARG_CASE("-errorcorrection", 1) { msdfgen::ErrorCorrectionConfig &ec = config.generatorAttributes.config.errorCorrection; if (!strcmp(argv[argPos+1], "disabled") || !strcmp(argv[argPos+1], "0") || !strcmp(argv[argPos+1], "none")) { ec.mode = msdfgen::ErrorCorrectionConfig::DISABLED; ec.distanceCheckMode = msdfgen::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")) { ec.mode = msdfgen::ErrorCorrectionConfig::EDGE_PRIORITY; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::CHECK_DISTANCE_AT_EDGE; } else if (!strcmp(argv[argPos+1], "auto-fast") || !strcmp(argv[argPos+1], "fast")) { ec.mode = msdfgen::ErrorCorrectionConfig::EDGE_PRIORITY; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "auto-full") || !strcmp(argv[argPos+1], "full")) { ec.mode = msdfgen::ErrorCorrectionConfig::EDGE_PRIORITY; ec.distanceCheckMode = msdfgen::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")) { ec.mode = msdfgen::ErrorCorrectionConfig::INDISCRIMINATE; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "distance-full") || !strcmp(argv[argPos+1], "indiscriminate-full")) { ec.mode = msdfgen::ErrorCorrectionConfig::INDISCRIMINATE; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::ALWAYS_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "edge-fast")) { ec.mode = msdfgen::ErrorCorrectionConfig::EDGE_ONLY; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "edge") || !strcmp(argv[argPos+1], "edge-full")) { ec.mode = msdfgen::ErrorCorrectionConfig::EDGE_ONLY; ec.distanceCheckMode = msdfgen::ErrorCorrectionConfig::ALWAYS_CHECK_DISTANCE; } else if (!strcmp(argv[argPos+1], "help")) { puts(errorCorrectionHelpText); return 0; } else ABORT("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."); config.generatorAttributes.config.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."); config.generatorAttributes.config.errorCorrection.minImproveRatio = eir; argPos += 2; continue; } ARG_CASE("-coloringstrategy", 1) { if (!strcmp(argv[argPos+1], "simple")) config.edgeColoring = msdfgen::edgeColoringSimple, config.expensiveColoring = false; else if (!strcmp(argv[argPos+1], "inktrap")) config.edgeColoring = msdfgen::edgeColoringInkTrap, config.expensiveColoring = false; else if (!strcmp(argv[argPos+1], "distance")) config.edgeColoring = msdfgen::edgeColoringByDistance, config.expensiveColoring = true; else puts("Unknown coloring strategy specified."); argPos += 2; continue; } ARG_CASE("-miterlimit", 1) { double m; if (!(parseDouble(m, argv[++argPos]) && m >= 0)) ABORT("Invalid miter limit argument. Use -miterlimit with a positive real number."); config.miterLimit = m; ++argPos; continue; } ARG_CASE("-nokerning", 0) { config.kerning = false; ++argPos; continue; } ARG_CASE("-kerning", 0) { config.kerning = true; ++argPos; continue; } ARG_CASE("-nopreprocess", 0) { config.preprocessGeometry = false; ++argPos; continue; } ARG_CASE("-preprocess", 0) { config.preprocessGeometry = true; ++argPos; continue; } ARG_CASE("-nooverlap", 0) { config.generatorAttributes.config.overlapSupport = false; ++argPos; continue; } ARG_CASE("-overlap", 0) { config.generatorAttributes.config.overlapSupport = true; ++argPos; continue; } ARG_CASE("-noscanline", 0) { config.generatorAttributes.scanlinePass = false; ++argPos; continue; } ARG_CASE("-scanline", 0) { config.generatorAttributes.scanlinePass = true; ++argPos; continue; } ARG_CASE("-seed", 1) { if (!parseUnsignedLL(config.coloringSeed, argv[argPos+1])) ABORT("Invalid seed. Use -seed with N being a non-negative integer."); argPos += 2; continue; } ARG_CASE("-threads", 1) { unsigned tc; if (!parseUnsigned(tc, argv[argPos+1]) || (int) tc < 0) ABORT("Invalid thread count. Use -threads with N being a non-negative integer."); config.threadCount = (int) tc; 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"); // Nothing to do? if (argc == 1) { printf( "Usage: msdf-atlas-gen" #ifdef _WIN32 ".exe" #endif " -font -charset \n" "Use -help for more information.\n" ); return 0; } if (!fontInput.fontFilename) ABORT("No font specified."); if (!(config.arteryFontFilename || config.imageFilename || config.jsonFilename || config.csvFilename || config.shadronPreviewFilename)) { puts("No output specified."); return 0; } bool layoutOnly = !(config.arteryFontFilename || config.imageFilename); // Finalize font inputs const FontInput *nextFontInput = &fontInput; for (std::vector::reverse_iterator it = fontInputs.rbegin(); it != fontInputs.rend(); ++it) { if (!it->fontFilename && nextFontInput->fontFilename) it->fontFilename = nextFontInput->fontFilename; if (!it->charsetFilename && nextFontInput->charsetFilename) { it->charsetFilename = nextFontInput->charsetFilename; it->glyphIdentifierType = nextFontInput->glyphIdentifierType; } if (it->fontScale < 0 && nextFontInput->fontScale >= 0) it->fontScale = nextFontInput->fontScale; nextFontInput = &*it; } if (fontInputs.empty() || memcmp(&fontInputs.back(), &fontInput, sizeof(FontInput))) fontInputs.push_back(fontInput); // Fix up configuration based on related values if (!(config.imageType == ImageType::PSDF || config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF)) config.miterLimit = 0; if (config.emSize > minEmSize) minEmSize = config.emSize; if (!(fixedWidth > 0 && fixedHeight > 0) && !(minEmSize > 0)) { puts("Neither atlas size nor glyph size selected, using default..."); minEmSize = MSDF_ATLAS_DEFAULT_EM_SIZE; } if (config.imageType == ImageType::HARD_MASK || config.imageType == ImageType::SOFT_MASK) { rangeMode = RANGE_PIXEL; rangeValue = 1; } else if (rangeValue <= 0) { rangeMode = RANGE_PIXEL; rangeValue = DEFAULT_PIXEL_RANGE; } if (config.kerning && !(config.arteryFontFilename || config.jsonFilename || config.shadronPreviewFilename)) config.kerning = false; if (config.threadCount <= 0) config.threadCount = std::max((int) std::thread::hardware_concurrency(), 1); if (config.generatorAttributes.scanlinePass) { if (explicitErrorCorrectionMode && config.generatorAttributes.config.errorCorrection.distanceCheckMode != msdfgen::ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE) { const char *fallbackModeName = "unknown"; switch (config.generatorAttributes.config.errorCorrection.mode) { case msdfgen::ErrorCorrectionConfig::DISABLED: fallbackModeName = "disabled"; break; case msdfgen::ErrorCorrectionConfig::INDISCRIMINATE: fallbackModeName = "distance-fast"; break; case msdfgen::ErrorCorrectionConfig::EDGE_PRIORITY: fallbackModeName = "auto-fast"; break; case msdfgen::ErrorCorrectionConfig::EDGE_ONLY: fallbackModeName = "edge-fast"; break; } printf("Selected error correction mode not compatible with scanline mode, falling back to %s.\n", fallbackModeName); } config.generatorAttributes.config.errorCorrection.distanceCheckMode = msdfgen::ErrorCorrectionConfig::DO_NOT_CHECK_DISTANCE; } // Finalize image format ImageFormat imageExtension = ImageFormat::UNSPECIFIED; if (config.imageFilename) { if (cmpExtension(config.imageFilename, ".png")) imageExtension = ImageFormat::PNG; else if (cmpExtension(config.imageFilename, ".bmp")) imageExtension = ImageFormat::BMP; else if (cmpExtension(config.imageFilename, ".tif") || cmpExtension(config.imageFilename, ".tiff")) imageExtension = ImageFormat::TIFF; else if (cmpExtension(config.imageFilename, ".txt")) imageExtension = ImageFormat::TEXT; else if (cmpExtension(config.imageFilename, ".bin")) imageExtension = ImageFormat::BINARY; } if (config.imageFormat == ImageFormat::UNSPECIFIED) { config.imageFormat = ImageFormat::PNG; imageFormatName = "png"; // If image format is not specified and -imageout is the only image output, infer format from its extension if (imageExtension != ImageFormat::UNSPECIFIED && !config.arteryFontFilename) config.imageFormat = imageExtension; } if (config.imageType == ImageType::MTSDF && config.imageFormat == ImageFormat::BMP) ABORT("Atlas type not compatible with image format. MTSDF requires a format with alpha channel."); #ifndef MSDF_ATLAS_NO_ARTERY_FONT if (config.arteryFontFilename && !(config.imageFormat == ImageFormat::PNG || config.imageFormat == ImageFormat::BINARY || config.imageFormat == ImageFormat::BINARY_FLOAT)) { config.arteryFontFilename = nullptr; result = 1; puts("Error: Unable to create an Artery Font file with the specified image format!"); // Recheck whether there is anything else to do if (!(config.arteryFontFilename || config.imageFilename || config.jsonFilename || config.csvFilename || config.shadronPreviewFilename)) return result; layoutOnly = !(config.arteryFontFilename || config.imageFilename); } #endif if (imageExtension != ImageFormat::UNSPECIFIED) { // Warn if image format mismatches -imageout extension bool mismatch = false; switch (config.imageFormat) { case ImageFormat::TEXT: case ImageFormat::TEXT_FLOAT: mismatch = imageExtension != ImageFormat::TEXT; break; case ImageFormat::BINARY: case ImageFormat::BINARY_FLOAT: case ImageFormat::BINARY_FLOAT_BE: mismatch = imageExtension != ImageFormat::BINARY; break; default: mismatch = imageExtension != config.imageFormat; } if (mismatch) printf("Warning: Output image file extension does not match the image's actual format (%s)!\n", imageFormatName); } imageFormatName = nullptr; // No longer consistent with imageFormat bool floatingPointFormat = ( config.imageFormat == ImageFormat::TIFF || config.imageFormat == ImageFormat::TEXT_FLOAT || config.imageFormat == ImageFormat::BINARY_FLOAT || config.imageFormat == ImageFormat::BINARY_FLOAT_BE ); // Load fonts std::vector glyphs; std::vector fonts; bool anyCodepointsAvailable = false; { class FontHolder { msdfgen::FreetypeHandle *ft; msdfgen::FontHandle *font; const char *fontFilename; public: FontHolder() : ft(msdfgen::initializeFreetype()), font(nullptr), fontFilename(nullptr) { } ~FontHolder() { if (ft) { if (font) msdfgen::destroyFont(font); msdfgen::deinitializeFreetype(ft); } } bool load(const char *fontFilename, bool isVarFont) { if (ft && fontFilename) { if (this->fontFilename && !strcmp(this->fontFilename, fontFilename)) return true; if (font) msdfgen::destroyFont(font); if ((font = isVarFont ? loadVarFont(ft, fontFilename) : msdfgen::loadFont(ft, fontFilename))) { this->fontFilename = fontFilename; return true; } this->fontFilename = nullptr; } return false; } operator msdfgen::FontHandle *() const { return font; } } font; for (FontInput &fontInput : fontInputs) { if (!font.load(fontInput.fontFilename, fontInput.variableFont)) ABORT("Failed to load specified font file."); if (fontInput.fontScale <= 0) fontInput.fontScale = 1; // Load character set Charset charset; if (fontInput.charsetFilename) { if (!charset.load(fontInput.charsetFilename, fontInput.glyphIdentifierType != GlyphIdentifierType::UNICODE_CODEPOINT)) ABORT(fontInput.glyphIdentifierType == GlyphIdentifierType::GLYPH_INDEX ? "Failed to load glyph set specification." : "Failed to load character set specification."); } else { charset = Charset::ASCII; fontInput.glyphIdentifierType = GlyphIdentifierType::UNICODE_CODEPOINT; } // Load glyphs FontGeometry fontGeometry(&glyphs); int glyphsLoaded = -1; switch (fontInput.glyphIdentifierType) { case GlyphIdentifierType::GLYPH_INDEX: glyphsLoaded = fontGeometry.loadGlyphset(font, fontInput.fontScale, charset, config.preprocessGeometry, config.kerning); break; case GlyphIdentifierType::UNICODE_CODEPOINT: glyphsLoaded = fontGeometry.loadCharset(font, fontInput.fontScale, charset, config.preprocessGeometry, config.kerning); anyCodepointsAvailable |= glyphsLoaded > 0; break; } if (glyphsLoaded < 0) ABORT("Failed to load glyphs from font."); printf("Loaded geometry of %d out of %d glyphs", glyphsLoaded, (int) charset.size()); if (fontInputs.size() > 1) printf(" from font \"%s\"", fontInput.fontFilename); printf(".\n"); // List missing glyphs if (glyphsLoaded < (int) charset.size()) { printf("Missing %d %s", (int) charset.size()-glyphsLoaded, fontInput.glyphIdentifierType == GlyphIdentifierType::UNICODE_CODEPOINT ? "codepoints" : "glyphs"); bool first = true; switch (fontInput.glyphIdentifierType) { case GlyphIdentifierType::GLYPH_INDEX: for (unicode_t cp : charset) if (!fontGeometry.getGlyph(msdfgen::GlyphIndex(cp))) printf("%c 0x%02X", first ? ((first = false), ':') : ',', cp); break; case GlyphIdentifierType::UNICODE_CODEPOINT: for (unicode_t cp : charset) if (!fontGeometry.getGlyph(cp)) printf("%c 0x%02X", first ? ((first = false), ':') : ',', cp); break; } printf("\n"); } if (fontInput.fontName) fontGeometry.setName(fontInput.fontName); fonts.push_back((FontGeometry &&) fontGeometry); } } if (glyphs.empty()) ABORT("No glyphs loaded."); // Determine final atlas dimensions, scale and range, pack glyphs { double unitRange = 0, pxRange = 0; switch (rangeMode) { case RANGE_EM: unitRange = rangeValue; break; case RANGE_PIXEL: pxRange = rangeValue; break; } bool fixedDimensions = fixedWidth >= 0 && fixedHeight >= 0; bool fixedScale = config.emSize > 0; TightAtlasPacker atlasPacker; if (fixedDimensions) atlasPacker.setDimensions(fixedWidth, fixedHeight); else atlasPacker.setDimensionsConstraint(atlasSizeConstraint); atlasPacker.setPadding(config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF ? 0 : -1); // TODO: In this case (if padding is -1), the border pixels of each glyph are black, but still computed. For floating-point output, this may play a role. if (fixedScale) atlasPacker.setScale(config.emSize); else atlasPacker.setMinimumScale(minEmSize); atlasPacker.setPixelRange(pxRange); atlasPacker.setUnitRange(unitRange); atlasPacker.setMiterLimit(config.miterLimit); if (int remaining = atlasPacker.pack(glyphs.data(), glyphs.size())) { if (remaining < 0) { ABORT("Failed to pack glyphs into atlas."); } else { printf("Error: Could not fit %d out of %d glyphs into the atlas.\n", remaining, (int) glyphs.size()); return 1; } } atlasPacker.getDimensions(config.width, config.height); if (!(config.width > 0 && config.height > 0)) ABORT("Unable to determine atlas size."); config.emSize = atlasPacker.getScale(); config.pxRange = atlasPacker.getPixelRange(); if (!fixedScale) printf("Glyph size: %.9g pixels/EM\n", config.emSize); if (!fixedDimensions) printf("Atlas dimensions: %d x %d\n", config.width, config.height); } // Generate atlas bitmap if (!layoutOnly) { // Edge coloring if (config.imageType == ImageType::MSDF || config.imageType == ImageType::MTSDF) { if (config.expensiveColoring) { Workload([&glyphs, &config](int i, int threadNo) -> bool { unsigned long long glyphSeed = (LCG_MULTIPLIER*(config.coloringSeed^i)+LCG_INCREMENT)*!!config.coloringSeed; glyphs[i].edgeColoring(config.edgeColoring, config.angleThreshold, glyphSeed); return true; }, glyphs.size()).finish(config.threadCount); } else { unsigned long long glyphSeed = config.coloringSeed; for (GlyphGeometry &glyph : glyphs) { glyphSeed *= LCG_MULTIPLIER; glyph.edgeColoring(config.edgeColoring, config.angleThreshold, glyphSeed); } } } bool success = false; switch (config.imageType) { case ImageType::HARD_MASK: if (floatingPointFormat) success = makeAtlas(glyphs, fonts, config); else success = makeAtlas(glyphs, fonts, config); break; case ImageType::SOFT_MASK: case ImageType::SDF: if (floatingPointFormat) success = makeAtlas(glyphs, fonts, config); else success = makeAtlas(glyphs, fonts, config); break; case ImageType::PSDF: if (floatingPointFormat) success = makeAtlas(glyphs, fonts, config); else success = makeAtlas(glyphs, fonts, config); break; case ImageType::MSDF: if (floatingPointFormat) success = makeAtlas(glyphs, fonts, config); else success = makeAtlas(glyphs, fonts, config); break; case ImageType::MTSDF: if (floatingPointFormat) success = makeAtlas(glyphs, fonts, config); else success = makeAtlas(glyphs, fonts, config); break; } if (!success) result = 1; } if (config.csvFilename) { if (exportCSV(fonts.data(), fonts.size(), config.width, config.height, config.yDirection, config.csvFilename)) puts("Glyph layout written into CSV file."); else { result = 1; puts("Failed to write CSV output file."); } } if (config.jsonFilename) { if (exportJSON(fonts.data(), fonts.size(), config.emSize, config.pxRange, config.width, config.height, config.imageType, config.yDirection, config.jsonFilename, config.kerning)) puts("Glyph layout and metadata written into JSON file."); else { result = 1; puts("Failed to write JSON output file."); } } if (config.shadronPreviewFilename && config.shadronPreviewText) { if (anyCodepointsAvailable) { std::vector previewText; utf8Decode(previewText, config.shadronPreviewText); previewText.push_back(0); if (generateShadronPreview(fonts.data(), fonts.size(), config.imageType, config.width, config.height, config.pxRange, previewText.data(), config.imageFilename, floatingPointFormat, config.shadronPreviewFilename)) puts("Shadron preview script generated."); else { result = 1; puts("Failed to generate Shadron preview file."); } } else { result = 1; puts("Shadron preview not supported in -glyphset mode."); } } return result; } #endif