
/*
* 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 <cstdio>
#include <cmath>
#include <cstring>
#include <cassert>
#include <vector>
#include <algorithm>
#include <thread>

#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 <filename.ttf/otf>
      Specifies the input TrueType / OpenType font file. A font specification is required.
  -varfont <filename.ttf/otf?var0=value0&var1=value1>
      Specifies an input variable font file and configures its variables.
  -charset <filename>
      Specifies the input character set. Refer to the documentation for format of charset specification. Defaults to ASCII.
  -glyphset <filename>
      Specifies the set of input glyphs as glyph indices within the font file.
  -fontscale <scale>
      Specifies the scale to be applied to the glyph geometry of the font.
  -fontname <name>
      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 <hardmask / softmask / sdf / psdf / msdf / mtsdf>
      Selects the type of atlas to be generated.
  -format <png / bmp / tiff / text / textfloat / bin / binfloat / binfloatbe>
      Selects the format for the atlas image output. Some image formats may be incompatible with embedded output formats.
  -dimensions <width> <height>
      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 <bottom / top>
      Determines whether the Y-axis is oriented upwards (bottom origin, default) or downwards (top origin).

OUTPUT SPECIFICATION - one or more can be specified
  -imageout <filename.*>
      Saves the atlas as an image file with the specified format. Layout data must be stored separately.
  -json <filename.json>
      Writes the atlas's layout data, as well as other metrics into a structured JSON file.
  -csv <filename.csv>
      Writes the layout data of the glyphs into a simple CSV file.)"
#ifndef MSDF_ATLAS_NO_ARTERY_FONT
R"(
  -arfont <filename.arfont>
      Stores the atlas and its layout data as an Artery Font file. Supported formats: png, bin, binfloat.)"
#endif
R"(
  -shadronpreview <filename.shadron> <sample text>
      Generates a Shadron script that uses the generated atlas to draw a sample text as a preview.

GLYPH CONFIGURATION
  -size <EM size>
      Specifies the size of the glyphs in the atlas bitmap in pixels per EM.
  -minsize <EM size>
      Specifies the minimum size. The largest possible size that fits the same atlas dimensions will be used.
  -emrange <EM range>
      Specifies the SDF distance range in EM's.
  -pxrange <pixel range>
      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 <angle>
      Specifies the minimum angle between adjacent edges to be considered a corner. Append D for degrees. (msdf / mtsdf only)
  -coloringstrategy <simple / inktrap / distance>
      Selects the strategy of the edge coloring heuristic.
  -errorcorrection <mode>
      Changes the MSDF/MTSDF error correction mode. Use -errorcorrection help for a list of valid modes.
  -errordeviationratio <ratio>
      Sets the minimum ratio between the actual and maximum expected distance delta to be considered an error.
  -errorimproveratio <ratio>
      Sets the minimum ratio between the pre-correction distance error and the post-correction distance error.
  -miterlimit <value>
      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 <N>
      Sets the initial seed for the edge coloring heuristic.
  -threads <N>
      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 <typename T, typename S, int N, GeneratorFunction<S, N> GEN_FN>
static bool makeAtlas(const std::vector<GlyphGeometry> &glyphs, const std::vector<FontGeometry> &fonts, const Configuration &config) {
    ImmediateAtlasGenerator<S, N, GEN_FN, BitmapAtlasStorage<T, N> > generator(config.width, config.height);
    generator.setAttributes(config.generatorAttributes);
    generator.setThreadCount(config.threadCount);
    generator.generate(glyphs.data(), glyphs.size());
    msdfgen::BitmapConstRef<T, N> bitmap = (msdfgen::BitmapConstRef<T, N>) 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<float>(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<FontInput> 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 <font scale> 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 <width> <height> 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 <EM 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 <EM size> 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 <EM range> 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 <pixel range> 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 <min 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 <ratio> 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 <ratio> 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 <limit> 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 <N> 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 <N> 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 <filename.ttf/otf> -charset <charset> <output specification> <options>\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<FontInput>::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<GlyphGeometry> glyphs;
    std::vector<FontGeometry> 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<float, float, 1, scanlineGenerator>(glyphs, fonts, config);
                else
                    success = makeAtlas<byte, float, 1, scanlineGenerator>(glyphs, fonts, config);
                break;
            case ImageType::SOFT_MASK:
            case ImageType::SDF:
                if (floatingPointFormat)
                    success = makeAtlas<float, float, 1, sdfGenerator>(glyphs, fonts, config);
                else
                    success = makeAtlas<byte, float, 1, sdfGenerator>(glyphs, fonts, config);
                break;
            case ImageType::PSDF:
                if (floatingPointFormat)
                    success = makeAtlas<float, float, 1, psdfGenerator>(glyphs, fonts, config);
                else
                    success = makeAtlas<byte, float, 1, psdfGenerator>(glyphs, fonts, config);
                break;
            case ImageType::MSDF:
                if (floatingPointFormat)
                    success = makeAtlas<float, float, 3, msdfGenerator>(glyphs, fonts, config);
                else
                    success = makeAtlas<byte, float, 3, msdfGenerator>(glyphs, fonts, config);
                break;
            case ImageType::MTSDF:
                if (floatingPointFormat)
                    success = makeAtlas<float, float, 4, mtsdfGenerator>(glyphs, fonts, config);
                else
                    success = makeAtlas<byte, float, 4, mtsdfGenerator>(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<unicode_t> 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
