/**************************************************************************** * * afshaper.c * * HarfBuzz interface for accessing OpenType features (body). * * Copyright (C) 2013-2026 by * David Turner, Robert Wilhelm, and Werner Lemberg. * * This file is part of the FreeType project, and may only be used, * modified, and distributed under the terms of the FreeType project * license, LICENSE.TXT. By continuing to use, modify, or distribute * this file you indicate that you have read the license and * understand and accept it fully. * */ #include #include #include "afglobal.h" #include "aftypes.h" #include "afshaper.h" #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ /************************************************************************** * * The macro FT_COMPONENT is used in trace mode. It is an implicit * parameter of the FT_TRACE() and FT_ERROR() macros, used to print/log * messages during execution. */ #undef FT_COMPONENT #define FT_COMPONENT afshaper /* * We use `sets' (in the HarfBuzz sense, which comes quite near to the * usual mathematical meaning) to manage both lookups and glyph indices. * * 1. For each coverage, collect lookup IDs in a set. Note that an * auto-hinter `coverage' is represented by one `feature', and a * feature consists of an arbitrary number of (font specific) `lookup's * that actually do the mapping job. Please check the OpenType * specification for more details on features and lookups. * * 2. Create glyph ID sets from the corresponding lookup sets. * * 3. The glyph set corresponding to AF_COVERAGE_DEFAULT is computed * with all lookups specific to the OpenType script activated. It * relies on the order of AF_DEFINE_STYLE_CLASS entries so that * special coverages (like `oldstyle figures') don't get overwritten. * */ /* load coverage tags */ #undef COVERAGE #define COVERAGE( name, NAME, description, \ tag1, tag2, tag3, tag4 ) \ static const hb_tag_t name ## _coverage[] = \ { \ HB_TAG( tag1, tag2, tag3, tag4 ), \ HB_TAG_NONE \ }; #include "afcover.h" /* define mapping between coverage tags and AF_Coverage */ #undef COVERAGE #define COVERAGE( name, NAME, description, \ tag1, tag2, tag3, tag4 ) \ name ## _coverage, static const hb_tag_t* coverages[] = { #include "afcover.h" NULL /* AF_COVERAGE_DEFAULT */ }; /* load HarfBuzz script tags */ #undef SCRIPT #define SCRIPT( s, S, d, h, H, ss ) h, FT_LOCAL_ARRAY_DEF( hb_script_t ) af_hb_scripts[] = { #include "afscript.h" }; static FT_Error af_shaper_get_coverage_hb( AF_FaceGlobals globals, AF_StyleClass style_class, FT_UShort* gstyles, FT_Bool default_script ) { hb_face_t* face; hb_set_t* gsub_lookups = NULL; /* GSUB lookups for a given script */ hb_set_t* gsub_glyphs = NULL; /* glyphs covered by GSUB lookups */ hb_set_t* gpos_lookups = NULL; /* GPOS lookups for a given script */ hb_set_t* gpos_glyphs = NULL; /* glyphs covered by GPOS lookups */ hb_script_t script; const hb_tag_t* coverage_tags; hb_tag_t script_tags[] = { HB_TAG_NONE, HB_TAG_NONE, HB_TAG_NONE, HB_TAG_NONE }; hb_codepoint_t idx; #ifdef FT_DEBUG_LEVEL_TRACE int count; #endif if ( !globals || !style_class || !gstyles ) return FT_THROW( Invalid_Argument ); face = hb( font_get_face )( globals->hb_font ); coverage_tags = coverages[style_class->coverage]; script = af_hb_scripts[style_class->script]; /* Convert a HarfBuzz script tag into the corresponding OpenType */ /* tag or tags -- some Indic scripts like Devanagari have an old */ /* and a new set of features. */ { unsigned int tags_count = 3; hb_tag_t tags[3]; hb( ot_tags_from_script_and_language )( script, HB_LANGUAGE_INVALID, &tags_count, tags, NULL, NULL ); script_tags[0] = tags_count > 0 ? tags[0] : HB_TAG_NONE; script_tags[1] = tags_count > 1 ? tags[1] : HB_TAG_NONE; script_tags[2] = tags_count > 2 ? tags[2] : HB_TAG_NONE; } /* If the second tag is HB_OT_TAG_DEFAULT_SCRIPT, change that to */ /* HB_TAG_NONE except for the default script. */ if ( default_script ) { if ( script_tags[0] == HB_TAG_NONE ) script_tags[0] = HB_OT_TAG_DEFAULT_SCRIPT; else { if ( script_tags[1] == HB_TAG_NONE ) script_tags[1] = HB_OT_TAG_DEFAULT_SCRIPT; else if ( script_tags[1] != HB_OT_TAG_DEFAULT_SCRIPT ) script_tags[2] = HB_OT_TAG_DEFAULT_SCRIPT; } } else { /* we use non-standard tags like `khms' for special purposes; */ /* HarfBuzz maps them to `DFLT', which we don't want to handle here */ if ( script_tags[0] == HB_OT_TAG_DEFAULT_SCRIPT ) goto Exit; } gsub_lookups = hb( set_create )(); hb( ot_layout_collect_lookups )( face, HB_OT_TAG_GSUB, script_tags, NULL, coverage_tags, gsub_lookups ); if ( hb( set_is_empty )( gsub_lookups ) ) goto Exit; /* nothing to do */ FT_TRACE4(( "GSUB lookups (style `%s'):\n", af_style_names[style_class->style] )); FT_TRACE4(( " " )); #ifdef FT_DEBUG_LEVEL_TRACE count = 0; #endif gsub_glyphs = hb( set_create )(); for ( idx = HB_SET_VALUE_INVALID; hb( set_next )( gsub_lookups, &idx ); ) { #ifdef FT_DEBUG_LEVEL_TRACE FT_TRACE4(( " %u", idx )); count++; #endif /* get output coverage of GSUB feature */ hb( ot_layout_lookup_collect_glyphs )( face, HB_OT_TAG_GSUB, idx, NULL, NULL, NULL, gsub_glyphs ); } #ifdef FT_DEBUG_LEVEL_TRACE if ( !count ) FT_TRACE4(( " (none)" )); FT_TRACE4(( "\n" )); FT_TRACE4(( "\n" )); #endif FT_TRACE4(( "GPOS lookups (style `%s'):\n", af_style_names[style_class->style] )); FT_TRACE4(( " " )); gpos_lookups = hb( set_create )(); hb( ot_layout_collect_lookups )( face, HB_OT_TAG_GPOS, script_tags, NULL, coverage_tags, gpos_lookups ); #ifdef FT_DEBUG_LEVEL_TRACE count = 0; #endif gpos_glyphs = hb( set_create )(); for ( idx = HB_SET_VALUE_INVALID; hb( set_next )( gpos_lookups, &idx ); ) { #ifdef FT_DEBUG_LEVEL_TRACE FT_TRACE4(( " %u", idx )); count++; #endif /* get input coverage of GPOS feature */ hb( ot_layout_lookup_collect_glyphs )( face, HB_OT_TAG_GPOS, idx, NULL, gpos_glyphs, NULL, NULL ); } #ifdef FT_DEBUG_LEVEL_TRACE if ( !count ) FT_TRACE4(( " (none)" )); FT_TRACE4(( "\n" )); FT_TRACE4(( "\n" )); #endif /* * We now check whether we can construct blue zones, using glyphs * covered by the feature only. In case there is not a single zone * (that is, not a single character is covered), we skip this coverage. * */ if ( style_class->coverage != AF_COVERAGE_DEFAULT ) { AF_Blue_Stringset bss = style_class->blue_stringset; const AF_Blue_StringRec* bs = &af_blue_stringsets[bss]; FT_Bool found = 0; for ( ; bs->string != AF_BLUE_STRING_MAX; bs++ ) { const char* p = &af_blue_strings[bs->string]; while ( *p ) { hb_codepoint_t ch; GET_UTF8_CHAR( ch, p ); for ( idx = HB_SET_VALUE_INVALID; hb( set_next )( gsub_lookups, &idx ); ) { hb_codepoint_t gidx = FT_Get_Char_Index( globals->face, ch ); if ( hb( ot_layout_lookup_would_substitute )( face, idx, &gidx, 1, 1 ) ) { found = 1; break; } } } } if ( !found ) { FT_TRACE4(( " no blue characters found; style skipped\n" )); goto Exit; } } /* * Various OpenType features might use the same glyphs at different * vertical positions; for example, superscript and subscript glyphs * could be the same. However, the auto-hinter is completely * agnostic of OpenType features after the feature analysis has been * completed: The engine then simply receives a glyph index and returns a * hinted and usually rendered glyph. * * Consider the superscript feature of font `pala.ttf': Some of the * glyphs are `real', that is, they have a zero vertical offset, but * most of them are small caps glyphs shifted up to the superscript * position (that is, the `sups' feature is present in both the GSUB and * GPOS tables). The code for blue zones computation actually uses a * feature's y offset so that the `real' glyphs get correct hints. But * later on it is impossible to decide whether a glyph index belongs to, * say, the small caps or superscript feature. * * For this reason, we don't assign a style to a glyph if the current * feature covers the glyph in both the GSUB and the GPOS tables. This * is quite a broad condition, assuming that * * (a) glyphs that get used in multiple features are present in a * feature without vertical shift, * * and * * (b) a feature's GPOS data really moves the glyph vertically. * * Not fulfilling condition (a) makes a font larger; it would also * reduce the number of glyphs that could be addressed directly without * using OpenType features, so this assumption is rather strong. * * Condition (b) is much weaker, and there might be glyphs which get * missed. However, the OpenType features we are going to handle are * primarily located in GSUB, and HarfBuzz doesn't provide an API to * directly get the necessary information from the GPOS table. A * possible solution might be to directly parse the GPOS table to find * out whether a glyph gets shifted vertically, but this is something I * would like to avoid if not really necessary. * * Note that we don't follow this logic for the default coverage. * Complex scripts like Devanagari have mandatory GPOS features to * position many glyph elements, using mark-to-base or mark-to-ligature * tables; the number of glyphs missed due to condition (b) would be far * too large. * */ if ( style_class->coverage != AF_COVERAGE_DEFAULT ) hb( set_subtract )( gsub_glyphs, gpos_glyphs ); #ifdef FT_DEBUG_LEVEL_TRACE FT_TRACE4(( " glyphs without GPOS data (`*' means already assigned)" )); count = 0; #endif for ( idx = HB_SET_VALUE_INVALID; hb( set_next )( gsub_glyphs, &idx ); ) { #ifdef FT_DEBUG_LEVEL_TRACE if ( !( count % 10 ) ) { FT_TRACE4(( "\n" )); FT_TRACE4(( " " )); } FT_TRACE4(( " %u", idx )); count++; #endif /* glyph indices returned by `hb_ot_layout_lookup_collect_glyphs' */ /* can be arbitrary: some fonts use fake indices for processing */ /* internal to GSUB or GPOS, which is fully valid */ if ( idx >= (hb_codepoint_t)globals->glyph_count ) continue; if ( gstyles[idx] == AF_STYLE_UNASSIGNED ) gstyles[idx] = (FT_UShort)style_class->style; #ifdef FT_DEBUG_LEVEL_TRACE else FT_TRACE4(( "*" )); #endif } #ifdef FT_DEBUG_LEVEL_TRACE if ( !count ) { FT_TRACE4(( "\n" )); FT_TRACE4(( " (none)" )); } FT_TRACE4(( "\n" )); FT_TRACE4(( "\n" )); #endif Exit: hb( set_destroy )( gsub_lookups ); hb( set_destroy )( gsub_glyphs ); hb( set_destroy )( gpos_lookups ); hb( set_destroy )( gpos_glyphs ); return FT_Err_Ok; } /* construct HarfBuzz features */ #undef COVERAGE #define COVERAGE( name, NAME, description, \ tag1, tag2, tag3, tag4 ) \ static const hb_feature_t name ## _feature[] = \ { \ { \ HB_TAG( tag1, tag2, tag3, tag4 ), \ 1, 0, (unsigned int)-1 \ } \ }; #include "afcover.h" /* define mapping between HarfBuzz features and AF_Coverage */ #undef COVERAGE #define COVERAGE( name, NAME, description, \ tag1, tag2, tag3, tag4 ) \ name ## _feature, static const hb_feature_t* features[] = { #include "afcover.h" NULL /* AF_COVERAGE_DEFAULT */ }; static void* af_shaper_buf_create_hb( AF_FaceGlobals globals ) { FT_UNUSED( globals ); return (void*)hb( buffer_create )(); } static void af_shaper_buf_destroy_hb( AF_FaceGlobals globals, void* buf ) { FT_UNUSED( globals ); hb( buffer_destroy )( (hb_buffer_t*)buf ); } static const char* af_shaper_get_cluster_hb( const char* p, AF_StyleMetrics metrics, void* buf_, unsigned int* count ) { AF_FaceGlobals globals = metrics->globals; AF_StyleClass style_class; const hb_feature_t* feature; FT_Int upem; const char* q; int len; hb_buffer_t* buf = (hb_buffer_t*)buf_; hb_font_t* font; hb_codepoint_t dummy; FT_UNUSED( globals ); upem = (FT_Int)metrics->globals->face->units_per_EM; style_class = metrics->style_class; feature = features[style_class->coverage]; font = metrics->globals->hb_font; /* we shape at a size of units per EM; this means font units */ hb( font_set_scale )( font, upem, upem ); while ( *p == ' ' ) p++; /* count bytes up to next space (or end of buffer) */ q = p; while ( !( *q == ' ' || *q == '\0' ) ) GET_UTF8_CHAR( dummy, q ); len = (int)( q - p ); /* feed character(s) to the HarfBuzz buffer */ hb( buffer_clear_contents )( buf ); hb( buffer_add_utf8 )( buf, p, len, 0, len ); /* we let HarfBuzz guess the script and writing direction */ hb( buffer_guess_segment_properties )( buf ); /* shape buffer, which means conversion from character codes to */ /* glyph indices, possibly applying a feature */ hb( shape )( font, buf, feature, feature ? 1 : 0 ); if ( feature ) { hb_buffer_t* hb_buf = metrics->globals->hb_buf; unsigned int gcount; hb_glyph_info_t* ginfo; unsigned int hb_gcount; hb_glyph_info_t* hb_ginfo; /* we have to check whether applying a feature does actually change */ /* glyph indices; otherwise the affected glyph or glyphs aren't */ /* available at all in the feature */ hb( buffer_clear_contents )( hb_buf ); hb( buffer_add_utf8 )( hb_buf, p, len, 0, len ); hb( buffer_guess_segment_properties )( hb_buf ); hb( shape )( font, hb_buf, NULL, 0 ); ginfo = hb( buffer_get_glyph_infos )( buf, &gcount ); hb_ginfo = hb( buffer_get_glyph_infos )( hb_buf, &hb_gcount ); if ( gcount == hb_gcount ) { unsigned int i; for (i = 0; i < gcount; i++ ) if ( ginfo[i].codepoint != hb_ginfo[i].codepoint ) break; if ( i == gcount ) { /* both buffers have identical glyph indices */ hb( buffer_clear_contents )( buf ); } } } *count = hb( buffer_get_length )( buf ); #ifdef FT_DEBUG_LEVEL_TRACE if ( feature && *count > 1 ) FT_TRACE1(( "af_shaper_get_cluster:" " input character mapped to multiple glyphs\n" )); #endif return q; } static FT_ULong af_shaper_get_elem_hb( AF_StyleMetrics metrics, void* buf_, unsigned int idx, FT_Long* advance, FT_Long* y_offset ) { AF_FaceGlobals globals = metrics->globals; hb_buffer_t* buf = (hb_buffer_t*)buf_; hb_glyph_info_t* ginfo; hb_glyph_position_t* gpos; unsigned int gcount; FT_UNUSED( globals ); ginfo = hb( buffer_get_glyph_infos )( buf, &gcount ); gpos = hb( buffer_get_glyph_positions )( buf, &gcount ); if ( idx >= gcount ) return 0; if ( advance ) *advance = gpos[idx].x_advance; if ( y_offset ) *y_offset = gpos[idx].y_offset; return ginfo[idx].codepoint; } #endif /* FT_CONFIG_OPTION_USE_HARFBUZZ */ static FT_Error af_shaper_get_coverage_nohb( AF_FaceGlobals globals, AF_StyleClass style_class, FT_UShort* gstyles, FT_Bool default_script ) { FT_UNUSED( globals ); FT_UNUSED( style_class ); FT_UNUSED( gstyles ); FT_UNUSED( default_script ); return FT_Err_Ok; } static void* af_shaper_buf_create_nohb( AF_FaceGlobals globals ) { FT_UNUSED( globals ); return NULL; } static void af_shaper_buf_destroy_nohb( AF_FaceGlobals globals, void* buf ) { FT_UNUSED( globals ); FT_UNUSED( buf ); } static const char* af_shaper_get_cluster_nohb( const char* p, AF_StyleMetrics metrics, void* buf_, unsigned int* count ) { FT_Face face = metrics->globals->face; FT_ULong ch, dummy = 0; FT_ULong* buf = (FT_ULong*)buf_; while ( *p == ' ' ) p++; GET_UTF8_CHAR( ch, p ); /* since we don't have an engine to handle clusters, */ /* we scan the characters but return zero */ while ( !( *p == ' ' || *p == '\0' ) ) GET_UTF8_CHAR( dummy, p ); if ( dummy ) { *buf = 0; *count = 0; } else { *buf = FT_Get_Char_Index( face, ch ); *count = 1; } return p; } static FT_ULong af_shaper_get_elem_nohb( AF_StyleMetrics metrics, void* buf_, unsigned int idx, FT_Long* advance, FT_Long* y_offset ) { FT_Face face = metrics->globals->face; FT_ULong glyph_index = *(FT_ULong*)buf_; FT_UNUSED( idx ); if ( advance ) FT_Get_Advance( face, glyph_index, FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_IGNORE_TRANSFORM, advance ); if ( y_offset ) *y_offset = 0; return glyph_index; } /********************************************************************/ FT_Error af_shaper_get_coverage( AF_FaceGlobals globals, AF_StyleClass style_class, FT_UShort* gstyles, FT_Bool default_script ) { #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ if ( ft_hb_enabled( globals ) ) return af_shaper_get_coverage_hb( globals, style_class, gstyles, default_script ); else #endif return af_shaper_get_coverage_nohb( globals, style_class, gstyles, default_script ); } void* af_shaper_buf_create( AF_FaceGlobals globals ) { #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ if ( ft_hb_enabled( globals ) ) return af_shaper_buf_create_hb( globals ); else #endif return af_shaper_buf_create_nohb( globals ); } void af_shaper_buf_destroy( AF_FaceGlobals globals, void* buf ) { #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ if ( ft_hb_enabled( globals ) ) af_shaper_buf_destroy_hb( globals, buf ); else #endif af_shaper_buf_destroy_nohb( globals, buf ); } const char* af_shaper_get_cluster( const char* p, AF_StyleMetrics metrics, void* buf_, unsigned int* count ) { #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ if ( ft_hb_enabled( metrics->globals ) ) return af_shaper_get_cluster_hb( p, metrics, buf_, count ); else #endif return af_shaper_get_cluster_nohb( p, metrics, buf_, count ); } FT_ULong af_shaper_get_elem( AF_StyleMetrics metrics, void* buf_, unsigned int idx, FT_Long* advance, FT_Long* y_offset ) { #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ if ( ft_hb_enabled( metrics->globals ) ) return af_shaper_get_elem_hb( metrics, buf_, idx, advance, y_offset ); #endif return af_shaper_get_elem_nohb( metrics, buf_, idx, advance, y_offset ); } /* END */