Fix for incorrect font coordinate scaling

This commit is contained in:
Chlumsky 2024-03-21 18:56:27 +01:00
parent 6d252a7dc3
commit bc9f02e156
3 changed files with 143 additions and 62 deletions

View File

@ -12,7 +12,6 @@
namespace msdfgen {
#define F26DOT6_TO_DOUBLE(x) (1/64.*double(x))
#define F16DOT16_TO_DOUBLE(x) (1/65536.*double(x))
#define DOUBLE_TO_F16DOT16(x) FT_Fixed(65536.*x)
@ -35,14 +34,14 @@ class FontHandle {
friend FontHandle *loadFont(FreetypeHandle *library, const char *filename);
friend FontHandle *loadFontData(FreetypeHandle *library, const byte *data, int length);
friend void destroyFont(FontHandle *font);
friend bool getFontMetrics(FontMetrics &metrics, FontHandle *font);
friend bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font);
friend bool getFontMetrics(FontMetrics &metrics, FontHandle *font, FontCoordinateScaling coordinateScaling);
friend bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font, FontCoordinateScaling coordinateScaling);
friend bool getGlyphCount(unsigned &output, FontHandle *font);
friend bool getGlyphIndex(GlyphIndex &glyphIndex, FontHandle *font, unicode_t unicode);
friend bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, double *advance);
friend bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, double *advance);
friend bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1);
friend bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1);
friend bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, FontCoordinateScaling coordinateScaling, double *outAdvance);
friend bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, FontCoordinateScaling coordinateScaling, double *outAdvance);
friend bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1, FontCoordinateScaling coordinateScaling);
friend bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1, FontCoordinateScaling coordinateScaling);
#ifndef MSDFGEN_DISABLE_VARIABLE_FONTS
friend bool setFontVariationAxis(FreetypeHandle *library, FontHandle *font, const char *name, double coordinate);
friend bool listFontVariationAxes(std::vector<FontVariationAxis> &axes, FreetypeHandle *library, FontHandle *font);
@ -54,26 +53,27 @@ class FontHandle {
};
struct FtContext {
double scale;
Point2 position;
Shape *shape;
Contour *contour;
};
static Point2 ftPoint2(const FT_Vector &vector) {
return Point2(F26DOT6_TO_DOUBLE(vector.x), F26DOT6_TO_DOUBLE(vector.y));
static Point2 ftPoint2(const FT_Vector &vector, double scale) {
return Point2(scale*vector.x, scale*vector.y);
}
static int ftMoveTo(const FT_Vector *to, void *user) {
FtContext *context = reinterpret_cast<FtContext *>(user);
if (!(context->contour && context->contour->edges.empty()))
context->contour = &context->shape->addContour();
context->position = ftPoint2(*to);
context->position = ftPoint2(*to, context->scale);
return 0;
}
static int ftLineTo(const FT_Vector *to, void *user) {
FtContext *context = reinterpret_cast<FtContext *>(user);
Point2 endpoint = ftPoint2(*to);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position) {
context->contour->addEdge(EdgeHolder(context->position, endpoint));
context->position = endpoint;
@ -83,9 +83,9 @@ static int ftLineTo(const FT_Vector *to, void *user) {
static int ftConicTo(const FT_Vector *control, const FT_Vector *to, void *user) {
FtContext *context = reinterpret_cast<FtContext *>(user);
Point2 endpoint = ftPoint2(*to);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position) {
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control), endpoint));
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control, context->scale), endpoint));
context->position = endpoint;
}
return 0;
@ -93,14 +93,26 @@ static int ftConicTo(const FT_Vector *control, const FT_Vector *to, void *user)
static int ftCubicTo(const FT_Vector *control1, const FT_Vector *control2, const FT_Vector *to, void *user) {
FtContext *context = reinterpret_cast<FtContext *>(user);
Point2 endpoint = ftPoint2(*to);
if (endpoint != context->position || crossProduct(ftPoint2(*control1)-endpoint, ftPoint2(*control2)-endpoint)) {
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control1), ftPoint2(*control2), endpoint));
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position || crossProduct(ftPoint2(*control1, context->scale)-endpoint, ftPoint2(*control2, context->scale)-endpoint)) {
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control1, context->scale), ftPoint2(*control2, context->scale), endpoint));
context->position = endpoint;
}
return 0;
}
static double getFontCoordinateScale(const FT_Face &face, FontCoordinateScaling coordinateScaling) {
switch (coordinateScaling) {
case FontCoordinateScaling::LEGACY:
return MSDFGEN_LEGACY_FONT_COORDINATE_SCALE;
case FontCoordinateScaling::KEEP_INTEGERS:
return 1;
case FontCoordinateScaling::EM_NORMALIZED:
return 1./(face->units_per_EM ? face->units_per_EM : 1);
}
return 1;
}
GlyphIndex::GlyphIndex(unsigned index) : index(index) { }
unsigned GlyphIndex::getIndex() const {
@ -129,10 +141,11 @@ FontHandle *adoptFreetypeFont(FT_Face ftFace) {
return handle;
}
FT_Error readFreetypeOutline(Shape &output, FT_Outline *outline) {
FT_Error readFreetypeOutline(Shape &output, FT_Outline *outline, double scale) {
output.contours.clear();
output.inverseYAxis = false;
FtContext context = { };
context.scale = scale;
context.shape = &output;
FT_Outline_Funcs ftFunctions;
ftFunctions.move_to = &ftMoveTo;
@ -179,25 +192,27 @@ void destroyFont(FontHandle *font) {
delete font;
}
bool getFontMetrics(FontMetrics &metrics, FontHandle *font) {
metrics.emSize = F26DOT6_TO_DOUBLE(font->face->units_per_EM);
metrics.ascenderY = F26DOT6_TO_DOUBLE(font->face->ascender);
metrics.descenderY = F26DOT6_TO_DOUBLE(font->face->descender);
metrics.lineHeight = F26DOT6_TO_DOUBLE(font->face->height);
metrics.underlineY = F26DOT6_TO_DOUBLE(font->face->underline_position);
metrics.underlineThickness = F26DOT6_TO_DOUBLE(font->face->underline_thickness);
bool getFontMetrics(FontMetrics &metrics, FontHandle *font, FontCoordinateScaling coordinateScaling) {
double scale = getFontCoordinateScale(font->face, coordinateScaling);
metrics.emSize = scale*font->face->units_per_EM;
metrics.ascenderY = scale*font->face->ascender;
metrics.descenderY = scale*font->face->descender;
metrics.lineHeight = scale*font->face->height;
metrics.underlineY = scale*font->face->underline_position;
metrics.underlineThickness = scale*font->face->underline_thickness;
return true;
}
bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font) {
bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font, FontCoordinateScaling coordinateScaling) {
double scale = getFontCoordinateScale(font->face, coordinateScaling);
FT_Error error = FT_Load_Char(font->face, ' ', FT_LOAD_NO_SCALE);
if (error)
return false;
spaceAdvance = F26DOT6_TO_DOUBLE(font->face->glyph->advance.x);
spaceAdvance = scale*font->face->glyph->advance.x;
error = FT_Load_Char(font->face, '\t', FT_LOAD_NO_SCALE);
if (error)
return false;
tabAdvance = F26DOT6_TO_DOUBLE(font->face->glyph->advance.x);
tabAdvance = scale*font->face->glyph->advance.x;
return true;
}
@ -211,33 +226,42 @@ bool getGlyphIndex(GlyphIndex &glyphIndex, FontHandle *font, unicode_t unicode)
return glyphIndex.getIndex() != 0;
}
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, double *advance) {
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, FontCoordinateScaling coordinateScaling, double *outAdvance) {
if (!font)
return false;
FT_Error error = FT_Load_Glyph(font->face, glyphIndex.getIndex(), FT_LOAD_NO_SCALE);
if (error)
return false;
if (advance)
*advance = F26DOT6_TO_DOUBLE(font->face->glyph->advance.x);
return !readFreetypeOutline(output, &font->face->glyph->outline);
double scale = getFontCoordinateScale(font->face, coordinateScaling);
if (outAdvance)
*outAdvance = scale*font->face->glyph->advance.x;
return !readFreetypeOutline(output, &font->face->glyph->outline, scale);
}
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, double *advance) {
return loadGlyph(output, font, GlyphIndex(FT_Get_Char_Index(font->face, unicode)), advance);
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, FontCoordinateScaling coordinateScaling, double *outAdvance) {
return loadGlyph(output, font, GlyphIndex(FT_Get_Char_Index(font->face, unicode)), coordinateScaling, outAdvance);
}
bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1) {
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, double *outAdvance) {
return loadGlyph(output, font, glyphIndex, FontCoordinateScaling::LEGACY, outAdvance);
}
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, double *outAdvance) {
return loadGlyph(output, font, unicode, FontCoordinateScaling::LEGACY, outAdvance);
}
bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1, FontCoordinateScaling coordinateScaling) {
FT_Vector kerning;
if (FT_Get_Kerning(font->face, glyphIndex0.getIndex(), glyphIndex1.getIndex(), FT_KERNING_UNSCALED, &kerning)) {
output = 0;
return false;
}
output = F26DOT6_TO_DOUBLE(kerning.x);
output = getFontCoordinateScale(font->face, coordinateScaling)*kerning.x;
return true;
}
bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1) {
return getKerning(output, font, GlyphIndex(FT_Get_Char_Index(font->face, unicode0)), GlyphIndex(FT_Get_Char_Index(font->face, unicode1)));
bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1, FontCoordinateScaling coordinateScaling) {
return getKerning(output, font, GlyphIndex(FT_Get_Char_Index(font->face, unicode0)), GlyphIndex(FT_Get_Char_Index(font->face, unicode1)), coordinateScaling);
}
#ifndef MSDFGEN_DISABLE_VARIABLE_FONTS

View File

@ -5,6 +5,8 @@
namespace msdfgen {
#define MSDFGEN_LEGACY_FONT_COORDINATE_SCALE (1/64.)
typedef unsigned unicode_t;
class FreetypeHandle;
@ -45,6 +47,16 @@ struct FontVariationAxis {
double defaultValue;
};
/// The scaling applied to font glyph coordinates when loading a glyph
enum class FontCoordinateScaling {
/// The incorrect legacy version that was in effect before version 1.12, coordinate values are divided by 64
LEGACY,
/// The coordinates are kept as the integer values native to the font file
KEEP_INTEGERS,
/// The coordinates will be normalized to the em size, i.e. 1 = 1 em
EM_NORMALIZED
};
/// Initializes the FreeType library.
FreetypeHandle *initializeFreetype();
/// Deinitializes the FreeType library.
@ -54,7 +66,7 @@ void deinitializeFreetype(FreetypeHandle *library);
/// Creates a FontHandle from FT_Face that was loaded by the user. destroyFont must still be called but will not affect the FT_Face.
FontHandle *adoptFreetypeFont(FT_Face ftFace);
/// Converts the geometry of FreeType's FT_Outline to a Shape object.
FT_Error readFreetypeOutline(Shape &output, FT_Outline *outline);
FT_Error readFreetypeOutline(Shape &output, FT_Outline *outline, double scale = MSDFGEN_LEGACY_FONT_COORDINATE_SCALE);
#endif
/// Loads a font file and returns its handle.
@ -64,19 +76,22 @@ FontHandle *loadFontData(FreetypeHandle *library, const byte *data, int length);
/// Unloads a font.
void destroyFont(FontHandle *font);
/// Outputs the metrics of a font.
bool getFontMetrics(FontMetrics &metrics, FontHandle *font);
bool getFontMetrics(FontMetrics &metrics, FontHandle *font, FontCoordinateScaling coordinateScaling = FontCoordinateScaling::LEGACY);
/// Outputs the width of the space and tab characters.
bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font);
bool getFontWhitespaceWidth(double &spaceAdvance, double &tabAdvance, FontHandle *font, FontCoordinateScaling coordinateScaling = FontCoordinateScaling::LEGACY);
/// Outputs the total number of glyphs available in the font.
bool getGlyphCount(unsigned &output, FontHandle *font);
/// Outputs the glyph index corresponding to the specified Unicode character.
bool getGlyphIndex(GlyphIndex &glyphIndex, FontHandle *font, unicode_t unicode);
/// Loads the geometry of a glyph from a font.
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, double *advance = NULL);
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, double *advance = NULL);
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, FontCoordinateScaling coordinateScaling, double *outAdvance = NULL);
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, FontCoordinateScaling coordinateScaling, double *outAdvance = NULL);
// Legacy API - FontCoordinateScaling is LEGACY
bool loadGlyph(Shape &output, FontHandle *font, GlyphIndex glyphIndex, double *outAdvance = NULL);
bool loadGlyph(Shape &output, FontHandle *font, unicode_t unicode, double *outAdvance = NULL);
/// Outputs the kerning distance adjustment between two specific glyphs.
bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1);
bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1);
bool getKerning(double &output, FontHandle *font, GlyphIndex glyphIndex0, GlyphIndex glyphIndex1, FontCoordinateScaling coordinateScaling = FontCoordinateScaling::LEGACY);
bool getKerning(double &output, FontHandle *font, unicode_t unicode0, unicode_t unicode1, FontCoordinateScaling coordinateScaling = FontCoordinateScaling::LEGACY);
#ifndef MSDFGEN_DISABLE_VARIABLE_FONTS
/// Sets a single variation axis of a variable font.

View File

@ -402,6 +402,10 @@ static const char *const helpText =
"\tSets the dimensions of the output image.\n"
" -edgecolors <sequence>\n"
"\tOverrides automatic edge coloring with the specified color sequence.\n"
#ifdef MSDFGEN_EXTENSIONS
" -emnormalize\n"
"\tBefore applying scale, normalizes font glyph coordinates so that 1 = 1 em.\n"
#endif
" -errorcorrection <mode>\n"
"\tChanges the MSDF/MTSDF error correction mode. Use -errorcorrection help for a list of valid modes.\n"
" -errordeviationratio <ratio>\n"
@ -426,6 +430,10 @@ static const char *const helpText =
"\tDisplays this help.\n"
" -legacy\n"
"\tUses the original (legacy) distance field algorithms.\n"
#ifdef MSDFGEN_EXTENSIONS
" -noemnormalize\n"
"\tRaw integer font glyph coordinates will be used. Without this option, legacy scaling will be applied.\n"
#endif
#ifdef MSDFGEN_USE_SKIA
" -nopreprocess\n"
"\tDisables path preprocessing which resolves self-intersections and overlapping contours.\n"
@ -547,6 +555,8 @@ int main(int argc, const char *const *argv) {
bool glyphIndexSpecified = false;
GlyphIndex glyphIndex;
unicode_t unicode = 0;
FontCoordinateScaling fontCoordinateScaling = FontCoordinateScaling::LEGACY;
bool fontCoordinateScalingSpecified = false;
#endif
int width = 64, height = 64;
@ -631,6 +641,21 @@ int main(int argc, const char *const *argv) {
}
continue;
}
ARG_CASE("-noemnormalize", 0) {
fontCoordinateScaling = FontCoordinateScaling::KEEP_INTEGERS;
fontCoordinateScalingSpecified = true;
continue;
}
ARG_CASE("-emnormalize", 0) {
fontCoordinateScaling = FontCoordinateScaling::EM_NORMALIZED;
fontCoordinateScalingSpecified = true;
continue;
}
ARG_CASE("-legacyfontscaling", 0) {
fontCoordinateScaling = FontCoordinateScaling::LEGACY;
fontCoordinateScalingSpecified = true;
continue;
}
#else
ARG_CASE("-svg", 1) {
ABORT("SVG input is not available in core-only version.");
@ -668,6 +693,10 @@ int main(int argc, const char *const *argv) {
}
ARG_CASE("-legacy", 0) {
legacyMode = true;
#ifdef MSDFGEN_EXTENSIONS
fontCoordinateScaling = FontCoordinateScaling::LEGACY;
fontCoordinateScalingSpecified = true;
#endif
continue;
}
ARG_CASE("-nopreprocess", 0) {
@ -987,28 +1016,40 @@ int main(int argc, const char *const *argv) {
case FONT: case VAR_FONT: {
if (!glyphIndexSpecified && !unicode)
ABORT("No character specified! Use -font <file.ttf/otf> <character code>. 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 = (
struct FreetypeFontGuard {
FreetypeHandle *ft;
FontHandle *font;
FreetypeFontGuard() : ft(), font() { }
~FreetypeFontGuard() {
if (ft) {
if (font)
destroyFont(font);
deinitializeFreetype(ft);
}
}
} guard;
if (!(guard.ft = initializeFreetype()))
ABORT("Failed to initialize FreeType library.");
if (!(guard.font = (
#ifndef MSDFGEN_DISABLE_VARIABLE_FONTS
inputType == VAR_FONT ? loadVarFont(ft, input) :
inputType == VAR_FONT ? loadVarFont(guard.ft, input) :
#endif
loadFont(ft, input)
);
if (!font) {
deinitializeFreetype(ft);
loadFont(guard.ft, input)
)))
ABORT("Failed to load font file.");
}
if (unicode)
getGlyphIndex(glyphIndex, font, unicode);
if (!loadGlyph(shape, font, glyphIndex, &glyphAdvance)) {
destroyFont(font);
deinitializeFreetype(ft);
getGlyphIndex(glyphIndex, guard.font, unicode);
if (!loadGlyph(shape, guard.font, glyphIndex, fontCoordinateScaling, &glyphAdvance))
ABORT("Failed to load glyph from font file.");
if (!fontCoordinateScalingSpecified && (!autoFrame || scaleSpecified || rangeMode == RANGE_UNIT || mode == METRICS || printMetrics || shapeExport)) {
fputs(
"Warning: Using legacy font coordinate conversion for compatibility reasons.\n"
" The scaling behavior in this configuration will likely change in a future version resulting in different output.\n"
" To silence this warning, use one of the following options:\n"
" -noemnormalize to switch to the correct native font coordinates,\n"
" -emnormalize to switch to coordinates normalized to 1 em, or\n"
" -legacyfontscaling to keep current behavior and make sure it will not change.\n", stderr);
}
destroyFont(font);
deinitializeFreetype(ft);
break;
}
#endif
@ -1026,9 +1067,10 @@ int main(int argc, const char *const *argv) {
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.");
bool readSuccessful = readShapeDescription(file, shape, &skipColoring);
fclose(file);
if (!readSuccessful)
ABORT("Parse error in shape description.");
break;
}
default:;