diff --git a/include/freetype/ftautoh.h b/include/freetype/ftautoh.h index cf7b76f..68d59e7 100644 --- a/include/freetype/ftautoh.h +++ b/include/freetype/ftautoh.h @@ -439,6 +439,36 @@ FT_BEGIN_HEADER */ + /************************************************************************** + * + * @property: + * no-stem-darkening + * + * @description: + * *Experimental* *only* + * + * See the description of the CFF driver for details. Stem darkening is a + * side effect of using the auto-hinter unless setting this property to + * true. + * + */ + + + /************************************************************************** + * + * @property: + * darkening-parameters + * + * @description: + * *Experimental* *only* + * + * See the description of the CFF driver for details. This implementation + * appropriates the CFF_CONFIG_OPTION_DARKENING_PARAMETER_* #defines for + * consistency. + * + */ + + /* */ diff --git a/src/autofit/afcjk.c b/src/autofit/afcjk.c index 905408b..bfbe95d 100644 --- a/src/autofit/afcjk.c +++ b/src/autofit/afcjk.c @@ -247,6 +247,25 @@ FT_TRACE5(( "\n" )); + /* Grab the standard_width for the X-axis (i.e. vertical features) and + * use as standard_width of stems to compute darkening amount with, + * analogous to invoking cf2_computeDarkening() with stdVW. + * + * XXX: Since standard_width varies depending on what glyph is being + * displayed and therefore which script analyzer is invoked, always use the + * biggest value for the stem darkening computation. In my limited testing, + * this was usually near the "true" value. + * + * XXX: Values can be significantly bigger than what the stdVW PS dict + * entry says on well hinted OpenType/CFF fonts, resulting in less + * darkening and therefore less contrast. */ + if ( metrics->root.globals->standard_vertical_width < metrics->axis[AF_DIMENSION_HORZ].standard_width ) + { + metrics->root.globals->standard_vertical_width_old = metrics->root.globals->standard_vertical_width; + metrics->root.globals->standard_vertical_width = + metrics->axis[AF_DIMENSION_HORZ].standard_width; + } + af_glyph_hints_done( hints ); } diff --git a/src/autofit/afglobal.c b/src/autofit/afglobal.c index 64b9293..69314e1 100644 --- a/src/autofit/afglobal.c +++ b/src/autofit/afglobal.c @@ -323,6 +323,10 @@ globals->glyph_count = face->num_glyphs; globals->glyph_styles = (FT_Byte*)( globals + 1 ); globals->module = module; + globals->stem_darkening_for_ppem = 0; + globals->darken_x = 0; + globals->standard_vertical_width = 0; + globals->standard_vertical_width_old = 0; #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ globals->hb_font = hb_ft_font_create( face, NULL ); @@ -375,6 +379,10 @@ #endif globals->glyph_count = 0; + globals->stem_darkening_for_ppem = 0; + globals->darken_x = 0; + globals->standard_vertical_width = 0; + globals->standard_vertical_width_old = 0; globals->glyph_styles = NULL; /* no need to free this one! */ globals->face = NULL; @@ -450,7 +458,6 @@ return error; } - FT_LOCAL_DEF( FT_Bool ) af_face_globals_is_digit( AF_FaceGlobals globals, FT_UInt gindex ) diff --git a/src/autofit/afglobal.h b/src/autofit/afglobal.h index 9bbb687..4589433 100644 --- a/src/autofit/afglobal.h +++ b/src/autofit/afglobal.h @@ -111,6 +111,17 @@ FT_BEGIN_HEADER AF_StyleMetrics metrics[AF_STYLE_MAX]; + FT_UShort stem_darkening_for_ppem; /* Compute darkening amount once + per size. Use this to check if + darken_x needs to be + recomputed. */ + FT_Pos darken_x; + FT_Pos standard_vertical_width; /* Copy from e.g. + AF_LatinMetrics.axis[AF_DIMENSION_HORZ], + to compute the darkening + amount. */ + FT_Pos standard_vertical_width_old; /* Hack for less robust way of finding "true" width. */ + AF_Module module; /* to access global properties */ } AF_FaceGlobalsRec; diff --git a/src/autofit/aflatin.c b/src/autofit/aflatin.c index 3065895..e8b13a5 100644 --- a/src/autofit/aflatin.c +++ b/src/autofit/aflatin.c @@ -251,6 +251,25 @@ FT_TRACE5(( "\n" )); + /* Grab the standard_width for the X-axis (i.e. vertical features) and + * use as standard_width of stems to compute darkening amount with, + * analogous to invoking cf2_computeDarkening() with stdVW. + * + * XXX: Since standard_width varies depending on what glyph is being + * displayed and therefore which script analyzer is invoked, always use the + * biggest value for the stem darkening computation. In my limited testing, + * this was usually near the "true" value. + * + * XXX: Values can be significantly bigger than what the stdVW PS dict + * entry says on well hinted OpenType/CFF fonts, resulting in less + * darkening and therefore less contrast. */ + if ( metrics->root.globals->standard_vertical_width < metrics->axis[AF_DIMENSION_HORZ].standard_width ) + { + metrics->root.globals->standard_vertical_width_old = metrics->root.globals->standard_vertical_width; + metrics->root.globals->standard_vertical_width = + metrics->axis[AF_DIMENSION_HORZ].standard_width; + } + af_glyph_hints_done( hints ); } diff --git a/src/autofit/afloader.c b/src/autofit/afloader.c index 7c2fa7c..8475c23 100644 --- a/src/autofit/afloader.c +++ b/src/autofit/afloader.c @@ -84,6 +84,7 @@ static FT_Error af_loader_load_g( AF_Loader loader, + AF_Module module, AF_Scaler scaler, FT_UInt glyph_index, FT_Int32 load_flags ) @@ -320,6 +321,37 @@ slot->format = FT_GLYPH_FORMAT_OUTLINE; } + /* Apply stem darkening here. Darken only on the X axis, as the autohinter + * snaps to the Y axis. This matches the behavior of Adobe's proprietary + * TrueType engine. + * + * Note: The outline must be emboldened after it's loaded by + * FT_Load_Glyph(), but only in this function! Other occurences of + * FT_Load_Glyph in the writing system class implementations compute their + * hints using the Y axis, so emboldening does nothing there. + * + * First check if we already computed the darkening amount for the size we + * are given so the computation is done only once per size. Then check if + * standard_vertical_width has changed -- this can happen when for + * different glyphs, different writing class style analyzers run over the + * face testing different characters and coming up with different + * standard_vertical_widths. This means the computation may happen multiple + * times per size. In my limited testing, the biggest value was usually not + * too far off the mark except when it was. */ + if ( !module->no_stem_darkening ) + { + if ( face->size->metrics.x_ppem != loader->globals->stem_darkening_for_ppem + || loader->globals->standard_vertical_width + > loader->globals->standard_vertical_width_old) + { + af_loader_compute_darkening( loader, module ); + loader->globals->standard_vertical_width_old = + loader->globals->standard_vertical_width; + } + if ( slot->format == FT_GLYPH_FORMAT_OUTLINE ) + FT_Outline_EmboldenXY(&slot->outline, loader->globals->darken_x, 0); + } + Exit: return error; } @@ -396,7 +428,7 @@ goto Exit; } - error = af_loader_load_g( loader, &scaler, gindex, load_flags ); + error = af_loader_load_g( loader, module, &scaler, gindex, load_flags ); } } Exit: @@ -404,4 +436,138 @@ } + /* Compute amount of pixels the face should be emboldened by, analogous to + * the CFF driver's cf2_computeDarkening(). See there for details of the + * algorithm. + * XXX: Currently a crude adaption of the original algorithm. Do better? + * XXX: what about the emboldening effect in cf2_computeDarkening()? */ + FT_LOCAL_DEF( void ) + af_loader_compute_darkening( AF_Loader loader, + AF_Module module ) + { + #define cf2_intToFixed( i ) ( (FT_Int32)( (FT_UInt32)(i) << 16 ) ) + #define cf2_fixedToInt( x ) ( (FT_Short)( ( (FT_UInt32)(x) + 0x8000U ) >> 16 ) ) + #define cf2_floatToFixed( f ) ( (FT_Int32)( (f) * 65536.0 + 0.5 ) ) + FT_UShort units_per_EM; + FT_Int32 ppem, emRatio; + FT_Int32 stemWidth, stemWidthPer1000, scaledStem, darkenAmount; + FT_Int logBase2; + FT_Int x1, y1, x2, y2, x3, y3, x4, y4; + FT_Pos stdVW; + + ppem = cf2_intToFixed(loader->face->size->metrics.x_ppem); + ppem = FT_MAX( cf2_intToFixed( 4 ), ppem ); + units_per_EM = loader->face->units_per_EM; + + if ( units_per_EM == 0 ) + units_per_EM = 1000; + + emRatio = cf2_intToFixed( 1000 ) / units_per_EM; + if ( emRatio < cf2_floatToFixed( .01 ) ) + { + /* If something goes wrong, silently disable darkening for the size. */ + loader->globals->darken_x = 0; + loader->globals->stem_darkening_for_ppem = loader->globals->face->size->metrics.x_ppem; + return; + } + + x1 = module->darken_params[0]; + y1 = module->darken_params[1]; + x2 = module->darken_params[2]; + y2 = module->darken_params[3]; + x3 = module->darken_params[4]; + y3 = module->darken_params[5]; + x4 = module->darken_params[6]; + y4 = module->darken_params[7]; + + stdVW = loader->globals->standard_vertical_width; + if ( stdVW <= 0) + { + stemWidth = cf2_intToFixed( 75 ); /* Taken from cf2font.c */ + stemWidthPer1000 = stemWidth; + } + else + { + stemWidth = cf2_intToFixed( stdVW ); + stemWidthPer1000 = FT_MulFix( stemWidth, emRatio ); + } + + logBase2 = FT_MSB( (FT_UInt32)stemWidthPer1000 ) + + FT_MSB( (FT_UInt32)ppem ); + + if ( logBase2 >= 46 ) + /* possible overflow */ + scaledStem = cf2_intToFixed( x4 ); + else + scaledStem = FT_MulFix( stemWidthPer1000, ppem ); + + /* now apply the darkening parameters */ + if ( scaledStem < cf2_intToFixed( x1 ) ) + darkenAmount = FT_DivFix( cf2_intToFixed( y1 ), ppem ); + + else if ( scaledStem < cf2_intToFixed( x2 ) ) + { + FT_Int xdelta = x2 - x1; + FT_Int ydelta = y2 - y1; + FT_Int x = stemWidthPer1000 - + FT_DivFix( cf2_intToFixed( x1 ), ppem ); + + + if ( !xdelta ) + goto Try_x3; + + darkenAmount = FT_MulDiv( x, ydelta, xdelta ) + + FT_DivFix( cf2_intToFixed( y1 ), ppem ); + } + + else if ( scaledStem < cf2_intToFixed( x3 ) ) + { +Try_x3: + { + FT_Int xdelta = x3 - x2; + FT_Int ydelta = y3 - y2; + FT_Int x = stemWidthPer1000 - + FT_DivFix( cf2_intToFixed( x2 ), ppem ); + + + if ( !xdelta ) + goto Try_x4; + + darkenAmount = FT_MulDiv( x, ydelta, xdelta ) + + FT_DivFix( cf2_intToFixed( y2 ), ppem ); + } + } + + else if ( scaledStem < cf2_intToFixed( x4 ) ) + { +Try_x4: + { + FT_Int xdelta = x4 - x3; + FT_Int ydelta = y4 - y3; + FT_Int x = stemWidthPer1000 - + FT_DivFix( cf2_intToFixed( x3 ), ppem ); + + + if ( !xdelta ) + goto Use_y4; + + darkenAmount = FT_MulDiv( x, ydelta, xdelta ) + + FT_DivFix( cf2_intToFixed( y3 ), ppem ); + } + } + + else + { +Use_y4: + darkenAmount = FT_DivFix( cf2_intToFixed( y4 ), ppem ); + } + + /* convert back to true character space. */ + darkenAmount = FT_DivFix( darkenAmount, emRatio * 2 ); + + loader->globals->darken_x = cf2_fixedToInt(darkenAmount); + loader->globals->stem_darkening_for_ppem = loader->globals->face->size->metrics.x_ppem; + return; + } + /* END */ diff --git a/src/autofit/afloader.h b/src/autofit/afloader.h index 37cfd14..3c952de 100644 --- a/src/autofit/afloader.h +++ b/src/autofit/afloader.h @@ -75,6 +75,10 @@ FT_BEGIN_HEADER FT_UInt gindex, FT_Int32 load_flags ); + FT_LOCAL_DEF( void ) + af_loader_compute_darkening( AF_Loader loader, + AF_Module module ); + /* */ diff --git a/src/autofit/afmodule.c b/src/autofit/afmodule.c index 8ae425c..614b118 100644 --- a/src/autofit/afmodule.c +++ b/src/autofit/afmodule.c @@ -169,6 +169,45 @@ return error; } #endif /* AF_CONFIG_OPTION_USE_WARPER */ + else if ( !ft_strcmp( property_name, "darkening-parameters" ) ) + { + FT_Int* darken_params = (FT_Int*)value; + + FT_Int x1 = darken_params[0]; + FT_Int y1 = darken_params[1]; + FT_Int x2 = darken_params[2]; + FT_Int y2 = darken_params[3]; + FT_Int x3 = darken_params[4]; + FT_Int y3 = darken_params[5]; + FT_Int x4 = darken_params[6]; + FT_Int y4 = darken_params[7]; + + if ( x1 < 0 || x2 < 0 || x3 < 0 || x4 < 0 || + y1 < 0 || y2 < 0 || y3 < 0 || y4 < 0 || + x1 > x2 || x2 > x3 || x3 > x4 || + y1 > 500 || y2 > 500 || y3 > 500 || y4 > 500 ) + return FT_THROW( Invalid_Argument ); + + module->darken_params[0] = x1; + module->darken_params[1] = y1; + module->darken_params[2] = x2; + module->darken_params[3] = y2; + module->darken_params[4] = x3; + module->darken_params[5] = y3; + module->darken_params[6] = x4; + module->darken_params[7] = y4; + + return error; + } + else if ( !ft_strcmp( property_name, "no-stem-darkening" ) ) + { + FT_Bool* no_stem_darkening = (FT_Bool*)value; + + + module->no_stem_darkening = *no_stem_darkening; + + return error; + } FT_TRACE0(( "af_property_set: missing property `%s'\n", property_name )); @@ -245,6 +284,33 @@ return error; } #endif /* AF_CONFIG_OPTION_USE_WARPER */ + else if ( !ft_strcmp( property_name, "darkening-parameters" ) ) + { + FT_Int* darken_params = module->darken_params; + FT_Int* val = (FT_Int*)value; + + + val[0] = darken_params[0]; + val[1] = darken_params[1]; + val[2] = darken_params[2]; + val[3] = darken_params[3]; + val[4] = darken_params[4]; + val[5] = darken_params[5]; + val[6] = darken_params[6]; + val[7] = darken_params[7]; + + return error; + } + else if ( !ft_strcmp( property_name, "no-stem-darkening" ) ) + { + FT_Bool no_stem_darkening = module->no_stem_darkening; + FT_Bool* val = (FT_Bool*)value; + + + *val = no_stem_darkening; + + return error; + } FT_TRACE0(( "af_property_get: missing property `%s'\n", property_name )); @@ -296,6 +362,16 @@ #ifdef AF_CONFIG_OPTION_USE_WARPER module->warping = 0; #endif + module->no_stem_darkening = FALSE; + + module->darken_params[0] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1; + module->darken_params[1] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1; + module->darken_params[2] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2; + module->darken_params[3] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2; + module->darken_params[4] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3; + module->darken_params[5] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3; + module->darken_params[6] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4; + module->darken_params[7] = CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4; return FT_Err_Ok; } diff --git a/src/autofit/afmodule.h b/src/autofit/afmodule.h index b9c2fd8..d23018a 100644 --- a/src/autofit/afmodule.h +++ b/src/autofit/afmodule.h @@ -41,7 +41,8 @@ FT_BEGIN_HEADER #ifdef AF_CONFIG_OPTION_USE_WARPER FT_Bool warping; #endif - + FT_Bool no_stem_darkening; + FT_Int darken_params[8]; } AF_ModuleRec, *AF_Module;