From f4814d3d83d126faf47331a70ea26c115500b6bc Mon Sep 17 00:00:00 2001 From: Thomas Klausner Date: Fri, 10 Oct 2025 20:03:01 +0200 Subject: [PATCH 01/96] Fix iconv(3) arguments (#5637) --- fontforge/macenc.c | 2 +- fontforge/splinefont.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fontforge/macenc.c b/fontforge/macenc.c index a83d04586e..9f9817596c 100644 --- a/fontforge/macenc.c +++ b/fontforge/macenc.c @@ -1132,7 +1132,7 @@ return( NULL ); macenc==sm_korean ? "EUC-KR" : macenc==sm_tradchinese ? "Big5" : "EUC-CN" ); - iconv_t *toutf8; + iconv_t toutf8; ICONV_CONST char *in; char *out; size_t inlen, outlen; diff --git a/fontforge/splinefont.h b/fontforge/splinefont.h index b341fdbed8..51ad0209fc 100644 --- a/fontforge/splinefont.h +++ b/fontforge/splinefont.h @@ -747,8 +747,8 @@ typedef struct enc { int iso_2022_escape_len; int low_page, high_page; char *iconv_name; /* For compatibility to old versions we might use a different name from that used by iconv. */ - iconv_t *tounicode; - iconv_t *fromunicode; + iconv_t tounicode; + iconv_t fromunicode; int (*tounicode_func)(int); int (*fromunicode_func)(int); unsigned int is_temporary: 1; /* freed when the map gets freed */ From afa46903e531080591835d0a0a753d7efb5d27cb Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:17:48 -0400 Subject: [PATCH 02/96] Remove `gi` field from charview (#5640) --- fontforge/views.h | 1 - fontforgeexe/charview.c | 9 --------- 2 files changed, 10 deletions(-) diff --git a/fontforge/views.h b/fontforge/views.h index 7044fa8be8..d7d0213d3d 100644 --- a/fontforge/views.h +++ b/fontforge/views.h @@ -252,7 +252,6 @@ typedef struct charview { GPoint e; /* mouse location */ GPoint olde; BasePoint last_c; - GImage gi; /* used for fill bitmap only */ int enc; EncMap *map_of_enc; /* Only use for comparison against fontview's map to see if our enc be valid */ /* Will not be updated when fontview is reencoded */ diff --git a/fontforgeexe/charview.c b/fontforgeexe/charview.c index 11322b1ded..e07cfa9081 100644 --- a/fontforgeexe/charview.c +++ b/fontforgeexe/charview.c @@ -12265,13 +12265,6 @@ static void _CharViewCreate(CharView *cv, SplineChar *sc, FontView *fv,int enc,i cv->nfh = as+ds; cv->nas = as; cv->height = pos.height; cv->width = pos.width; - cv->gi.u.image = calloc(1,sizeof(struct _GImage)); - cv->gi.u.image->image_type = it_mono; - cv->gi.u.image->clut = calloc(1,sizeof(GClut)); - cv->gi.u.image->clut->trans_index = cv->gi.u.image->trans = 0; - cv->gi.u.image->clut->clut_len = 2; - cv->gi.u.image->clut->clut[0] = view_bgcol; - cv->gi.u.image->clut->clut[1] = fillcol; cv->b1_tool = cv_b1_tool; cv->cb1_tool = cv_cb1_tool; cv->b1_tool_old = cv->b1_tool; cv->b2_tool = cv_b2_tool; cv->cb2_tool = cv_cb2_tool; @@ -12644,8 +12637,6 @@ void CharViewFree(CharView *cv) { GDrawDestroyWindow(cv->ruler_linger_w); cv->ruler_linger_w = NULL; } - free(cv->gi.u.image->clut); - free(cv->gi.u.image); #if HANYANG if ( cv->jamodisplay!=NULL ) Disp_DoFinish(cv->jamodisplay,true); From fe07b9499a15e23ddc3eb2359e04d8500643c04f Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 15 Oct 2025 20:32:08 +0300 Subject: [PATCH 03/96] Differentiate Supplementary PUA planes by name (#5650) --- Unicode/makeutype.py | 4 ++-- Unicode/uninames_data.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Unicode/makeutype.py b/Unicode/makeutype.py index 5a8895f7e5..8f7e643e38 100644 --- a/Unicode/makeutype.py +++ b/Unicode/makeutype.py @@ -244,8 +244,8 @@ def __init__(self, version): (0x20000, 0x2FFFD, "Supplementary Ideographic Plane"), (0x30000, 0x3FFFD, "Tertiary Ideographic Plane"), (0xE0000, 0xEFFFD, "Supplementary Special-purpose Plane"), - (0xF0000, 0xFFFFD, "Supplementary Private Use Area-A"), - (0x100000, 0x10FFFD, "Supplementary Private Use Area-B"), + (0xF0000, 0xFFFFD, "Plane 15 (Supplementary Private Use Area-A)"), + (0x100000, 0x10FFFD, "Plane 16 (Supplementary Private Use Area-B)"), ] planes = [ (s, e, s, sum(1 for i in range(s, e + 1) if table[i]), n) diff --git a/Unicode/uninames_data.h b/Unicode/uninames_data.h index 1238b6fba2..c43c56c011 100644 --- a/Unicode/uninames_data.h +++ b/Unicode/uninames_data.h @@ -403,8 +403,8 @@ static const struct unicode_range unicode_planes[] = { {0xC0000, 0xCFFFD, 0xC0000, 0, N_("")}, {0xD0000, 0xDFFFD, 0xD0000, 0, N_("")}, {0xE0000, 0xEFFFD, 0xE0000, 337, N_("Supplementary Special-purpose Plane")}, - {0xF0000, 0xFFFFD, 0xF0000, 65534, N_("Supplementary Private Use Area-A")}, - {0x100000, 0x10FFFD, 0x100000, 65534, N_("Supplementary Private Use Area-B")}, + {0xF0000, 0xFFFFD, 0xF0000, 65534, N_("Plane 15 (Supplementary Private Use Area-A)")}, + {0x100000, 0x10FFFD, 0x100000, 65534, N_("Plane 16 (Supplementary Private Use Area-B)")}, }; /* lexicon data */ From fb4a9e2c36e631b47e726666bfb2ebc97ae500a2 Mon Sep 17 00:00:00 2001 From: f380cedric Date: Fri, 17 Oct 2025 18:31:23 +0200 Subject: [PATCH 04/96] Fix restrict violation (#5651) Since `select()` is only used as a workaround for microseconds sleep, replace it with `nanosleep()`. --- fontforgeexe/uiutil.c | 16 ++-------------- gdraw/gdraw.c | 4 ---- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/fontforgeexe/uiutil.c b/fontforgeexe/uiutil.c index 42963676cd..ac3a6608a0 100644 --- a/fontforgeexe/uiutil.c +++ b/fontforgeexe/uiutil.c @@ -34,6 +34,7 @@ #include "utype.h" #include +#include extern GBox _ggadget_Default_Box; #define ACTIVE_BORDER (_ggadget_Default_Box.active_border) @@ -540,22 +541,9 @@ static char *UI_saveas_file(const char *title, const char *defaultfile, return( gwwv_save_filename(title,defaultfile,initial_filter) ); } -static void tinysleep(int microsecs) { -#if !defined(__MINGW32__) - fd_set none; - struct timeval timeout; - - FD_ZERO(&none); - memset(&timeout,0,sizeof(timeout)); - timeout.tv_usec = microsecs; - - select(1,&none,&none,&none,&timeout); -#endif -} - static void allow_events(void) { GDrawSync(NULL); - tinysleep(100); + nanosleep(&(const struct timespec){.tv_nsec = 100000}, NULL); GDrawProcessPendingEvents(NULL); } diff --git a/gdraw/gdraw.c b/gdraw/gdraw.c index 38d42cfa0b..3b3d3a3e97 100644 --- a/gdraw/gdraw.c +++ b/gdraw/gdraw.c @@ -33,10 +33,6 @@ #include "gkeysym.h" #include "ustring.h" -#if __Mac -# include -#endif - /* Functions for font metrics: rectangle of text (left side bearing of first char, right of last char) */ From e52bc1e53f48bb5d320a3433a850ea85d95b1997 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 22 Oct 2025 22:52:32 +0300 Subject: [PATCH 05/96] Fix script / language application in HarfBuzz (#5658) --- fontforgeexe/shapers/harfbuzz.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fontforgeexe/shapers/harfbuzz.cpp b/fontforgeexe/shapers/harfbuzz.cpp index eded261f81..7e9761666a 100644 --- a/fontforgeexe/shapers/harfbuzz.cpp +++ b/fontforgeexe/shapers/harfbuzz.cpp @@ -290,9 +290,9 @@ ShaperOutput HarfBuzzShaper::apply_features( hb_buffer_add_utf32(hb_buffer, u_vec.data(), -1, 0, -1); // Set script and language - hb_script_t hb_script = hb_script_from_iso15924_tag((uint32_t)script); + hb_script_t hb_script = hb_ot_tag_to_script(script); hb_buffer_set_script(hb_buffer, hb_script); - hb_language_t hb_lang = hb_language_from_string((const char*)lang, -1); + hb_language_t hb_lang = hb_ot_tag_to_language(lang); hb_buffer_set_language(hb_buffer, hb_lang); bool rtl = false; @@ -553,7 +553,7 @@ std::set HarfBuzzShaper::default_features_from_plan( std::set HarfBuzzShaper::default_features(Tag script, Tag lang, bool vertical) const { - hb_script_t hb_script = hb_script_from_iso15924_tag(script); + hb_script_t hb_script = hb_ot_tag_to_script(script); hb_language_t hb_lang = hb_ot_tag_to_language(lang); hb_direction_t dir = vertical ? HB_DIRECTION_TTB From 512b9667eb7ec739ce2ea991c2b3dde022e1cee2 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Fri, 31 Oct 2025 18:18:50 +0200 Subject: [PATCH 06/96] Update CI to MacOS 14 image (#5642) --- .github/workflows/main.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed22e632c2..2ee76a5183 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -81,15 +81,13 @@ jobs: CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} run: java -jar $DEPSPREFIX/crowdin-cli.jar upload -c .crowdin.yml mac: - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 with: path: repo - name: Install dependencies run: | - # Disable fc-cache on fontconfig install. Because it's slow. - sed -i.bak '/fc-cache/d' "$(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/f/fontconfig.rb" brew install pkg-config ruby cmake ninja brew install cairo coreutils fontconfig gettext giflib gtk+3 gtkmm3 jpeg libpng libspiro libtiff libtool libuninameslist python@3 wget woff2 PREFIX=$GITHUB_WORKSPACE/target From 5a69313d81d869adeaf6e9185773c69f39f48a3e Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sat, 1 Nov 2025 23:03:03 +0200 Subject: [PATCH 07/96] Fix Python documentation errors (#5646) --- doc/sphinx/scripting/python/fontforge.rst | 4 ++-- fontforge/mathconstants.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index d9c6ede00d..12f684af1d 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -4114,7 +4114,7 @@ This type may not be pickled. * ``gsub_ligature`` * ``gsub_context`` * ``gsub_contextchain`` - * ``gsub_revesechain`` + * ``gsub_reversecchain`` * ``morx_indic`` * ``morx_context`` * ``morx_insert`` @@ -4654,7 +4654,7 @@ This type may not be pickled. x-height and another for the top of capitals and ascenders (and perhaps a fourth for descenders). Each such zone is specified by the ``vMap`` argument which is a tuple of 3-tuples, each 3-tuple specifying a zone with: Original - location, original width, and final location. + location, final location, and original width. .. note:: diff --git a/fontforge/mathconstants.c b/fontforge/mathconstants.c index e8d857b01a..352213f743 100644 --- a/fontforge/mathconstants.c +++ b/fontforge/mathconstants.c @@ -64,7 +64,7 @@ struct math_constants_descriptor math_constants_descriptor[] = { MCDD(N_("SuperscriptBottomMaxWithSubscript:"),SuperscriptBottomMaxWithSubscript,SuperscriptBottomMaxWithSubscript_adjust,N_("The maximum level to which the (ink) bottom\nof superscript can be pushed to increase the\ngap between superscript and subscript, before\nsubscript starts being moved down."),0), MCDD(N_("SpaceAfterScript:"),SpaceAfterScript,SpaceAfterScript_adjust,N_("Extra white space to be added after each\nsub/superscript."),0), MCDD(N_("UpperLimitGapMin:"),UpperLimitGapMin,UpperLimitGapMin_adjust,N_("Minimum gap between the bottom of the\nupper limit, and the top of the base operator."),1), - MCDD(N_("UpperLimitBaselineRiseMin:"),UpperLimitBaselineRiseMin,UpperLimitBaselineRiseMin_adjust,N_("Minimum distance between the baseline of an upper\nlimit and the bottom of the base operator."),0), + MCDD(N_("UpperLimitBaselineRiseMin:"),UpperLimitBaselineRiseMin,UpperLimitBaselineRiseMin_adjust,N_("Minimum distance between the baseline of an upper\nlimit and the top of the base operator."),0), MCDD(N_("LowerLimitGapMin:"),LowerLimitGapMin,LowerLimitGapMin_adjust,N_("Minimum gap between (ink) top of the lower limit,\nand (ink) bottom of the base operator."),0), MCDD(N_("LowerLimitBaselineDropMin:"),LowerLimitBaselineDropMin,LowerLimitBaselineDropMin_adjust,N_("Minimum distance between the baseline of the\nlower limit and bottom of the base operator."),0), MCDD(N_("StackTopShiftUp:"),StackTopShiftUp,StackTopShiftUp_adjust,N_("Standard shift up applied to the top element of a stack."),1), From 0de8675922eb04136a5e7dd3142fe427ca2da218 Mon Sep 17 00:00:00 2001 From: MihailJP Date: Mon, 3 Nov 2025 06:44:05 +0900 Subject: [PATCH 08/96] OtfFeatName for `cv??` (#5662) - This implements cvParameters for SFD, TTF, UFO and Python interface - 'character' list is not implemented yet --- doc/sphinx/scripting/python/fontforge.rst | 8 +- fontforge/featurefile.c | 128 ++++++++-- fontforge/parsettfatt.c | 78 ++++-- fontforge/python.c | 198 ++++++++++++--- fontforge/sfd.c | 151 +++++++++-- fontforge/splinefont.h | 7 + fontforge/tottfgpos.c | 79 ++++-- fontforge/tottfgpos.h | 2 +- fontforgeexe/fontinfo.c | 236 +++++++++++++++--- tests/CMakeLists.txt | 2 +- .../{test1020.py => test_style_set_names.py} | 42 +++- 11 files changed, 785 insertions(+), 146 deletions(-) rename tests/{test1020.py => test_style_set_names.py} (50%) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 12f684af1d..13cec10ac4 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -3947,7 +3947,13 @@ This type may not be pickled. .. attribute:: font.style_set_names - A tuple, each entry of which is a 3-element tuple containing the language name (e.g. ``"English (US)"``), the style set tag (e.g. ``"ss01"``) and the style set name. + A tuple, each entry of which is a 3-element tuple containing: + * For style sets, the language name (e.g. ``"English (US)"``), the style set + tag (e.g. ``"ss01"``) and the style set name. + * For character variants, the language name, the character variant tag (e.g. + ``"cv01"``) and a 5-element tuple containing the feature name, the tooltip text, + the sample text (these three can be ``None``), a tuple containing parameter + names (can be empty) and ``None`` (reserved for the character list). .. attribute:: font.temporary diff --git a/fontforge/featurefile.c b/fontforge/featurefile.c index 274ace24ec..f60f24a844 100644 --- a/fontforge/featurefile.c +++ b/fontforge/featurefile.c @@ -50,6 +50,7 @@ #include #include #include +#include #include /* Adobe's opentype feature file */ @@ -1798,14 +1799,35 @@ static void dump_header_languagesystem(FILE *out, SplineFont *sf) { fprintf( out, "\n" ); } +static bool dump_feature_names(FILE *out, uint32_t tag, enum otffn_field field, SplineFont *sf, const char *fieldname, bool *cvprm /* NULL if 'ssXX' */) { + struct otffeatname *fn; + struct otfname *on; + char indent[] = {cvprm ? ' ' : 0,' ',0}; + if ( (fn = findotffeatname(tag,field,sf))!=NULL ) { + if ( cvprm && !(*cvprm) ) + fprintf( out, " cvParameters {\n" ); + fprintf( out, "%s %s {\n", indent, fieldname ); + for ( on = fn->names; on!=NULL; on=on->next ) { + fprintf( out, "%s name 3 1 0x%x \"", indent, on->lang ); + UniOut(out,on->name ); + fprintf( out, "\";\n" ); + } + fprintf( out, "%s };\n", indent ); + if ( cvprm ) + *cvprm = true; + return true; + } + else { + return false; + } +} + static void dump_gsubgpos(FILE *out, SplineFont *sf) { int isgpos; int i,l,s, subl; OTLookup *otl; FeatureScriptLangList *fl; struct scriptlanglist *sl; - struct otffeatname *fn; - struct otfname *on; for ( isgpos=0; isgpos<2; ++isgpos ) { uint32_t *feats = SFFeaturesInScriptLang(sf,isgpos,0xffffffff,0xffffffff); @@ -1818,15 +1840,21 @@ static void dump_gsubgpos(FILE *out, SplineFont *sf) { dump_lookup( out, sf, otl ); for ( i=0; feats[i]!=0; ++i ) { fprintf( out, "\nfeature %c%c%c%c {\n", feats[i]>>24, feats[i]>>16, feats[i]>>8, feats[i] ); - if ( feats[i]>=CHR('s','s','0','1') && feats[i]<=CHR('s','s','2','0') && - (fn = findotffeatname(feats[i],sf))!=NULL ) { - fprintf( out, " featureNames {\n" ); - for ( on = fn->names; on!=NULL; on=on->next ) { - fprintf( out, " name 3 1 0x%x \"", on->lang ); - UniOut(out,on->name ); - fprintf( out, "\";\n" ); - } - fprintf( out, " };\n" ); + if ( feats[i]>=CHR('s','s','0','1') && feats[i]<=CHR('s','s','2','0') ) { + dump_feature_names(out, feats[i], otffn_featname, sf, "featureNames", NULL); + } + if ( feats[i]>=CHR('c','v','0','1') && feats[i]<=CHR('c','v','9','9') ) { + bool cvprm = false; + dump_feature_names(out, feats[i], otffn_featname, sf, "FeatUILabelNameID", &cvprm); + dump_feature_names(out, feats[i], otffn_tooltiptext, sf, "FeatUITooltipTextNameID", &cvprm); + dump_feature_names(out, feats[i], otffn_sampletext, sf, "SampleTextNameID", &cvprm); + for ( l = 0; l < 65536-256; ++l ) { + if ( !dump_feature_names(out, feats[i], otffn_paramname_begin+l, sf, "ParamUILabelNameID", &cvprm) ) + break; + } + if ( cvprm ) { + fprintf( out, " };\n" ); + } } if ( feats[i]==CHR('s','i','z','e') ) { struct otfname *nm; @@ -2229,11 +2257,12 @@ enum toktype { tk_name, tk_class, tk_int, tk_char, tk_cid, tk_eof, tk_lookupflag, tk_mark, tk_nameid, tk_NULL, tk_parameters, tk_position, tk_required, tk_RightToLeft, tk_script, tk_substitute, tk_subtable, tk_table, tk_useExtension, -/* Additional keywords in the 2008 draft */ tk_anchorDef, tk_valueRecordDef, tk_contourpoint, tk_MarkAttachmentType, tk_UseMarkFilteringSet, tk_markClass, tk_reversesub, tk_base, tk_ligature, tk_ligComponent, - tk_featureNames + tk_featureNames, + tk_cvParameters, tk_FeatUILabelNameID, tk_FeatUITooltipTextNameID, + tk_SampleTextNameID, tk_ParamUILabelNameID, tk_Character }; struct glyphclasses { @@ -2341,7 +2370,6 @@ static struct keywords { { "subtable", tk_subtable }, { "table", tk_table }, { "useExtension", tk_useExtension }, -/* Additional keywords in the 2008 draft */ { "anchorDef", tk_anchorDef }, { "valueRecordDef", tk_valueRecordDef }, { "contourpoint", tk_contourpoint }, @@ -2353,6 +2381,12 @@ static struct keywords { { "ligature", tk_ligature }, { "ligComponent", tk_ligComponent }, { "featureNames", tk_featureNames }, + { "cvParameters", tk_cvParameters }, + { "FeatUILabelNameID", tk_FeatUILabelNameID }, + { "FeatUITooltipTextNameID", tk_FeatUITooltipTextNameID }, + { "SampleTextNameID", tk_SampleTextNameID }, + { "ParamUILabelNameID", tk_ParamUILabelNameID }, + { "Character", tk_Character }, /* synonyms */ { "sub", tk_substitute }, { "pos", tk_position }, @@ -5425,7 +5459,7 @@ static struct feat_item *fea_ParseSizeMenuName(struct parseState *tok, struct fe return( feat ); } -static void fea_ParseFeatureNames(struct parseState *tok,uint32_t tag) { +static void fea_ParseFeatureNames(struct parseState *tok,uint32_t tag,enum otffn_field field) { struct otffeatname *cur; struct otfname *head=NULL, *string; struct nameid *temp; @@ -5458,6 +5492,7 @@ static void fea_ParseFeatureNames(struct parseState *tok,uint32_t tag) { item->u2.featnames = cur = chunkalloc(sizeof(*cur)); cur->tag = tag; cur->names = head; + cur->field = field; } if ( tok->type!=tk_char || tok->tokbuf[0]!='}' ) { LogError(_("Expected closing curly brace on line %d of %s"), tok->line[tok->inc_depth], tok->filename[tok->inc_depth] ); @@ -5465,6 +5500,58 @@ static void fea_ParseFeatureNames(struct parseState *tok,uint32_t tag) { } } +static void fea_ParseCvParameters(struct parseState *tok,uint32_t tag) { + bool finished = false; + int paramCount = 0; + bool charList = false; // remove after once character list is implemented + + for (finished = false;!finished;) { + fea_ParseTok(tok); + switch ( tok->type ) { + case tk_FeatUILabelNameID: + fea_TokenMustBe(tok,tk_char,'{'); + fea_ParseFeatureNames(tok,tag,otffn_featname); + fea_end_statement(tok); + break; + case tk_FeatUITooltipTextNameID: + fea_TokenMustBe(tok,tk_char,'{'); + fea_ParseFeatureNames(tok,tag,otffn_tooltiptext); + fea_end_statement(tok); + break; + case tk_SampleTextNameID: + fea_TokenMustBe(tok,tk_char,'{'); + fea_ParseFeatureNames(tok,tag,otffn_sampletext); + fea_end_statement(tok); + break; + case tk_ParamUILabelNameID: + fea_TokenMustBe(tok,tk_char,'{'); + fea_ParseFeatureNames(tok,tag,otffn_paramname_begin + (paramCount++)); + fea_end_statement(tok); + break; + case tk_Character: + tok->base = 0; + fea_TokenMustBe(tok,tk_int,'\0'); + // TODO: character + tok->base = 10; + fea_end_statement(tok); + charList = true; // remove after once character list is implemented + break; + default: + finished = true; + break; + } + } + + if ( charList ) { // remove after once character list is implemented + LogError( _("The character list in the cvParameters block in feature %c%c%c%c is ignored."), tag >> 24,tag >> 16,tag >> 8,tag ); + } + + if ( tok->type!=tk_char || tok->tokbuf[0]!='}' ) { + LogError(_("Expected closing curly brace on line %d of %s"), tok->line[tok->inc_depth], tok->filename[tok->inc_depth] ); + ++tok->err_count; + } +} + static void fea_ParseFeatureDef(struct parseState *tok) { uint32_t feat_tag; struct feat_item *item, *size_item = NULL; @@ -5564,9 +5651,13 @@ return; } break; case tk_featureNames: - /* I don't handle these yet, so ignore 'em */ fea_TokenMustBe(tok,tk_char,'{'); - fea_ParseFeatureNames(tok,feat_tag); + fea_ParseFeatureNames(tok,feat_tag,otffn_featname); + fea_end_statement(tok); + break; + case tk_cvParameters: + fea_TokenMustBe(tok,tk_char,'{'); + fea_ParseCvParameters(tok,feat_tag); fea_end_statement(tok); break; case tk_parameters: @@ -7171,10 +7262,11 @@ static struct feat_item *fea_ApplyFeatureList(struct parseState *tok, fea_AttachFeatureToLookup(otl,feature_tag,sl); continue; case ft_featname: - for ( fn = tok->sf->feat_names; fn!=NULL && fn->tag!=f->u2.featnames->tag; fn=fn->next ); + for ( fn = tok->sf->feat_names; fn!=NULL && (fn->tag!=f->u2.featnames->tag || fn->field!=f->u2.featnames->field); fn=fn->next ); if ( fn!=NULL ) { OtfNameListFree(fn->names); fn->names = f->u2.featnames->names; + fn->field = f->u2.featnames->field; chunkfree(f->u2.featnames,sizeof(struct otffeatname)); f->u2.featnames = NULL; } else { diff --git a/fontforge/parsettfatt.c b/fontforge/parsettfatt.c index 2aaeab96ed..cb0582172f 100644 --- a/fontforge/parsettfatt.c +++ b/fontforge/parsettfatt.c @@ -2405,39 +2405,50 @@ return; } static void readttffeatnameparameters(FILE *ttf,int32_t pos,uint32_t tag, - struct ttfinfo *info) { + struct ttfinfo *info, enum otffn_field field) { int version, nid; struct otffeatname *fn; uint32_t here; here = ftell(ttf); fseek(ttf,pos,SEEK_SET); - version = getushort(ttf); /* Minor version #, currently (2009) 0 */ + if ( (tag & 0xffff0000) == CHR('c','v','\0','\0') && + (tag & 0x0000ff00) >= ('0' << 8) && (tag & 0x0000ff00) <= ('9' << 8) && + (tag & 0x000000ff) >= ('0') && (tag & 0x000000ff) <= ('9') ) { + version = 0; /* ignore version for 'ccXX' */ + } + else { + version = getushort(ttf); /* Minor version #, currently (2009) 0 */ + } nid = getushort(ttf); + if ( field >= otffn_paramname_begin ) { + nid += field - otffn_paramname_begin; + } fseek(ttf,here,SEEK_SET); if ( version>=10 || nid<256 || nid>32767 ) { - if ( nid<256 || nid>32767 ) - LogError(_("The name parameter of the '%c%c%c%c' feature does not contain a valid name id."), - tag>>24, tag>>16, tag>>8, tag ); - else - LogError(_("The name parameter of the '%c%c%c%c' feature has an unlikely version number %d."), - tag>>24, tag>>16, tag>>8, tag, version ); - info->bad_ot = true; -return; + if ( nid<256 || nid>32767 ) + LogError(_("The name parameter of the '%c%c%c%c' feature does not contain a valid name id."), + tag>>24, tag>>16, tag>>8, tag ); + else + LogError(_("The name parameter of the '%c%c%c%c' feature has an unlikely version number %d."), + tag>>24, tag>>16, tag>>8, tag, version ); + info->bad_ot = true; + return; } - for ( fn=info->feat_names; fn!=NULL && fn->tag!=tag; fn=fn->next ); + for ( fn=info->feat_names; fn!=NULL && (fn->tag!=tag || fn->field!=field); fn=fn->next ); if ( fn!=NULL ) { - if ( fn->nid == nid ) -return; - LogError(_("There are multiple name ids naming the '%c%c%c%c' feature\n this is technically legitimate, but fontforge can't handle it.\n"), - tag>>24, tag>>16, tag>>8, tag ); -return; + if ( fn->nid == nid ) + return; + LogError(_("There are multiple name ids naming the '%c%c%c%c' feature\n this is technically legitimate, but fontforge can't handle it.\n"), + tag>>24, tag>>16, tag>>8, tag ); + return; } fn = chunkalloc( sizeof(*fn) ); fn->tag = tag; fn->nid = nid; + fn->field = field; fn->next = info->feat_names; info->feat_names = fn; fn->names = FindAllLangEntries(ttf,info,nid); @@ -2522,6 +2533,7 @@ static struct feature *readttffeatures(FILE *ttf,int32_t pos,int isgpos, struct int i,j; struct feature *features; int parameters; + int ncnt; if ( pos>=info->g_bounds ) { LogError(_("Attempt to read feature data beyond end of %s table"), isgpos ? "GPOS" : "GSUB" ); @@ -2559,7 +2571,39 @@ return( NULL ); } else if ( features[i].tag>=CHR('s','s','0','1') && features[i].tag<=CHR('s','s','2','0') && parameters!=0 && !feof(ttf)) { readttffeatnameparameters(ttf,pos+parameters+features[i].offset, - features[i].tag,info); + features[i].tag,info, otffn_featname); + } else if ( features[i].tag>=CHR('c','v','0','1') && features[i].tag<=CHR('c','v','9','9') && + parameters!=0 && !feof(ttf)) { + fseek(ttf,pos+parameters+features[i].offset+2,SEEK_SET); + if( getushort(ttf) != 0 ) + readttffeatnameparameters(ttf,pos+parameters+features[i].offset+2, + features[i].tag,info, otffn_featname); + + fseek(ttf,pos+parameters+features[i].offset+4,SEEK_SET); + if( getushort(ttf) != 0 ) + readttffeatnameparameters(ttf,pos+parameters+features[i].offset+4, + features[i].tag,info, otffn_tooltiptext); + + fseek(ttf,pos+parameters+features[i].offset+6,SEEK_SET); + if( getushort(ttf) != 0 ) + readttffeatnameparameters(ttf,pos+parameters+features[i].offset+6, + features[i].tag,info, otffn_sampletext); + + fseek(ttf,pos+parameters+features[i].offset+8,SEEK_SET); + ncnt = getushort(ttf); + for ( j = 0; j < ncnt; ++j ) { + readttffeatnameparameters(ttf,pos+parameters+features[i].offset+10, + features[i].tag,info, otffn_paramname_begin + j); + } + fseek(ttf,pos+features[i].offset+12,SEEK_SET); + ncnt = getushort(ttf); + if ( ncnt > 0 ) { + LogError( _("The character list in the feature parameter table of '%c%c%c%c' is ignored."), features[i].tag >> 24,features[i].tag >> 16,features[i].tag >> 8,features[i].tag ); + } + //for ( j = 0; j < ncnt; ++j ) { + // get3byte(ttf); // TODO: character[j] + //} + fseek(ttf,pos+features[i].offset+2,SEEK_SET); } features[i].lcnt = getushort(ttf); if ( feof(ttf) ) { diff --git a/fontforge/python.c b/fontforge/python.c index 8b589a4f76..d25b334a7c 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -14949,10 +14949,12 @@ return( Py_BuildValue("d",val)); static PyObject *PyFF_Font_get_style_set_names(PyFF_Font *self, void *UNUSED(closure)) { int cnt; SplineFont *sf; - struct otffeatname *fn; - struct otfname *on; - PyObject *ss_names_tuple, *ss_name_spec; + struct otffeatname *fn, *fn2; + struct otfname *on, *on2; + PyObject *ss_names_tuple, *ss_name_spec, *ss_names_cv, *ss_names_prm; const char *lang_str; + bool dup; + int i; if ( CheckIfFontClosed(self) ) return( NULL ); @@ -14967,15 +14969,67 @@ static PyObject *PyFF_Font_get_style_set_names(PyFF_Font *self, void *UNUSED(clo for ( cnt=0, fn=sf->feat_names; fn!=NULL; fn=fn->next ) { for ( on=fn->names; on!=NULL; on=on->next, ++cnt ) { lang_str = NOUI_MSLangString(on->lang); - ss_name_spec = PyTuple_Pack(3, PyUnicode_FromString(lang_str), TagToPythonString(fn->tag, false), PyUnicode_FromString(on->name)); - PyTuple_SetItem(ss_names_tuple, cnt, ss_name_spec); + if ( (fn->tag & 0xffff0000) == CHR('s','s','\0','\0') ) { + ss_name_spec = PyTuple_Pack(3, PyUnicode_FromString(lang_str), TagToPythonString(fn->tag, false), PyUnicode_FromString(on->name)); + PyTuple_SetItem(ss_names_tuple, cnt, ss_name_spec); + } + else if ( ((fn->tag & 0xff00) >> 8) >= '0' && ((fn->tag & 0xff00) >> 8) <= '9' && (fn->tag & 0xff) >= '0' && (fn->tag & 0xff) <= '9' ) { + dup = false; + for ( i = 0; i < cnt; ++i ) { + PyObject *langstr = PyUnicode_FromString(lang_str); + PyObject *tagstr = TagToPythonString(CHR('c','v','\0','\0') | (fn->tag & 0xffff), false); + if ( PyUnicode_Compare(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, i), 0), langstr) == 0 && + PyUnicode_Compare(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, i), 1), tagstr) == 0 ) { + dup = true; + } + } + if ( !dup ) { + ss_names_prm = PyTuple_New(0); + ss_names_cv = PyTuple_Pack(5, Py_None, Py_None, Py_None, Py_None, Py_None); + for ( fn2=sf->feat_names; fn2!=NULL; fn2=fn2->next ) { + for ( on2=fn2->names; on2!=NULL; on2=on2->next ) { + if ( fn2->tag == fn->tag && fn2->field == otffn_featname && on->lang == on2->lang ) { + PyTuple_SetItem(ss_names_cv, 0, PyUnicode_FromString(on2->name)); + } + if ( fn2->tag == fn->tag && fn2->field == otffn_tooltiptext && on->lang == on2->lang ) { + PyTuple_SetItem(ss_names_cv, 1, PyUnicode_FromString(on2->name)); + } + if ( fn2->tag == fn->tag && fn2->field == otffn_sampletext && on->lang == on2->lang ) { + PyTuple_SetItem(ss_names_cv, 2, PyUnicode_FromString(on2->name)); + } + if ( fn2->tag == fn->tag && fn2->field >= otffn_paramname_begin && on->lang == on2->lang ) { + if ( PyTuple_Size(ss_names_prm) < (fn2->field - otffn_paramname_begin + 1) ) { + _PyTuple_Resize(&ss_names_prm, (fn2->field - otffn_paramname_begin + 1)); + for ( i = 0; i < PyTuple_Size(ss_names_prm); ++i ) { + if ( !PyTuple_GetItem(ss_names_prm, i) ) { + PyTuple_SetItem(ss_names_prm, i, Py_None); + } + } + } + PyTuple_SetItem(ss_names_prm, (fn2->field - otffn_paramname_begin), PyUnicode_FromString(on2->name)); + } + } + } + PyTuple_SetItem(ss_names_cv, 3, ss_names_prm); + // TODO: character + ss_name_spec = PyTuple_Pack(3, PyUnicode_FromString(lang_str), TagToPythonString(('c' << 24) | ('v' << 16) | (fn->tag & 0xffff), false), ss_names_cv); + PyTuple_SetItem(ss_names_tuple, cnt, ss_name_spec); + } + else { + --cnt; + } + } + else { + --cnt; + } } } + _PyTuple_Resize(&ss_names_tuple, cnt); return( ss_names_tuple ); } static int PyFF_Font_set_style_set_names(PyFF_Font *self, PyObject *value, void *UNUSED(closure)) { - int i, j, rows, is_valid; + int i, j, rows, is_valid, is_cv; PyObject *ss_names_tuple, *lang_py, *lang_py_i, *lang_py_j; const char *tag_str, *name_str; uint32_t tag, lang, lang_i, lang_j; @@ -15025,20 +15079,70 @@ static int PyFF_Font_set_style_set_names(PyFF_Font *self, PyObject *value, void } tag_str = PyUnicode_AsUTF8(tag_str_py); is_valid = 0; + is_cv = 0; if ( strlen(tag_str)== 4 && tag_str[0]=='s' && tag_str[1]=='s' && '0'<=tag_str[2] && tag_str[2]<='9' && '0'<=tag_str[3] && tag_str[3]<='9' ) { int ss_num = 10*(tag_str[2]-'0') + tag_str[3]-'0'; is_valid = 1 <= ss_num && ss_num <= 20; + is_cv = 0; + } + else if ( strlen(tag_str)== 4 && tag_str[0]=='c' && tag_str[1]=='v' + && '0'<=tag_str[2] && tag_str[2]<='9' && '0'<=tag_str[3] && tag_str[3]<='9' + ) { + int ss_num = 10*(tag_str[2]-'0') + tag_str[3]-'0'; + is_valid = 1 <= ss_num && ss_num <= 99; + is_cv = 1; } if ( !is_valid ) { - PyErr_Format(PyExc_ValueError, "The second element of a style set name specification must be a valid style set tag."); + PyErr_Format(PyExc_ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."); return( -1 ); } - if ( !PyUnicode_Check(PyTuple_GetItem(ss_names_tuple, 2)) ) { - PyErr_Format(PyExc_TypeError, "The third element of a style set name specification must be a string."); - return( -1 ); + if ( is_cv ) { + PyObject *ss_cv_names_tuple = PyTuple_GetItem(ss_names_tuple, 2); + PyObject *ss_prm_names_tuple; + if ( !PyTuple_Check(ss_cv_names_tuple) ) { + PyErr_Format(PyExc_TypeError, "The third element of a character variant name specification must be a tuple."); + return( -1 ); + } + if ( PyTuple_Size(ss_cv_names_tuple)!=5 ) { + PyErr_Format(PyExc_ValueError, "The third element of a character variant name specification must have 5 elements (feature name, tooltip text, sample text, tuple of parameter labels and characters)."); + return( -1 ); + } + if ( PyTuple_GetItem(ss_cv_names_tuple, 0) != Py_None && !PyUnicode_Check(PyTuple_GetItem(ss_cv_names_tuple, 0)) ) { + PyErr_Format(PyExc_TypeError, "The first element of the third element of a character variant name specification must be a string or None."); + return( -1 ); + } + if ( PyTuple_GetItem(ss_cv_names_tuple, 1) != Py_None && !PyUnicode_Check(PyTuple_GetItem(ss_cv_names_tuple, 1)) ) { + PyErr_Format(PyExc_TypeError, "The second element of the third element of a character variant name specification must be a string or None."); + return( -1 ); + } + if ( PyTuple_GetItem(ss_cv_names_tuple, 2) != Py_None && !PyUnicode_Check(PyTuple_GetItem(ss_cv_names_tuple, 2)) ) { + PyErr_Format(PyExc_TypeError, "The third element of the third element of a character variant name specification must be a string or None."); + return( -1 ); + } + if ( PyTuple_GetItem(ss_cv_names_tuple, 4) != Py_None ) { // TODO: characters + PyErr_Format(PyExc_TypeError, "The fifth element of the third element of a character variant name specification is reserved and must be None."); + return( -1 ); + } + ss_prm_names_tuple = PyTuple_GetItem(ss_cv_names_tuple, 3); + if ( !PyTuple_Check(ss_prm_names_tuple) ) { + PyErr_Format(PyExc_TypeError, "The fourth element of the third element of a character variant name specification must be a tuple."); + return( -1 ); + } + for ( j = 0; j < PyTuple_Size(ss_prm_names_tuple); ++j ) { + if ( PyTuple_GetItem(ss_prm_names_tuple, j) != Py_None && !PyUnicode_Check(PyTuple_GetItem(ss_prm_names_tuple, j)) ) { + PyErr_Format(PyExc_TypeError, "All elements of the fourth element of the third element of a character variant name specification must be a string or None."); + return( -1 ); + } + } + } + else { + if ( !PyUnicode_Check(PyTuple_GetItem(ss_names_tuple, 2)) ) { + PyErr_Format(PyExc_TypeError, "The third element of a style set name specification must be a string."); + return( -1 ); + } } } @@ -15061,12 +15165,22 @@ static int PyFF_Font_set_style_set_names(PyFF_Font *self, PyObject *value, void lang_i == lang_j && PyObject_RichCompareBool(PyTuple_GetItem(PyTuple_GetItem(value, i), 1), PyTuple_GetItem(PyTuple_GetItem(value, j), 1), Py_EQ) ) { - PyErr_Format(PyExc_ValueError, "The feature %s is named twice in language %s:\n %.80s\n %.80s", - PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, i), 1)), - NOUI_MSLangString(lang_i), - PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, i), 2)), - PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, j), 2)) - ); + if ( PyTuple_Check(PyTuple_GetItem(PyTuple_GetItem(value, i), 2)) ) { // cvXX + PyErr_Format(PyExc_ValueError, "The feature %s is named twice in language %s:\n %.80s\n %.80s", + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, i), 1)), + NOUI_MSLangString(lang_i), + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(PyTuple_GetItem(value, i), 2), 0)), + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(PyTuple_GetItem(value, j), 2), 0)) + ); + } + else { // ssXX + PyErr_Format(PyExc_ValueError, "The feature %s is named twice in language %s:\n %.80s\n %.80s", + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, i), 1)), + NOUI_MSLangString(lang_i), + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, i), 2)), + PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(value, j), 2)) + ); + } return( -1 ); } } @@ -15087,24 +15201,44 @@ static int PyFF_Font_set_style_set_names(PyFF_Font *self, PyObject *value, void tag = StrObjToTag(PyTuple_GetItem(ss_names_tuple, 1), false); - name_str = PyUnicode_AsUTF8(PyTuple_GetItem(ss_names_tuple, 2)); + for ( j = 0; j < ( (tag & 0xffff0000) == (('s' << 24) | ('s' << 16)) ? 1 : 4 + PyTuple_Size(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, 2), 3))); ++j ) { + if ( (tag & 0xffff0000) == (('s' << 24) | ('s' << 16)) ) { + name_str = PyUnicode_AsUTF8(PyTuple_GetItem(ss_names_tuple, 2)); + } + else if ( j < 3 && PyUnicode_Check(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, 2), j)) ) { + name_str = PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, 2), j)); + } + else if ( j == 3 ) { + // TODO: character + name_str = NULL; + } + else if ( j >= 4 && PyUnicode_Check(PyTuple_GetItem(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, 2), 3), j - 4)) ) { + name_str = PyUnicode_AsUTF8(PyTuple_GetItem(PyTuple_GetItem(PyTuple_GetItem(ss_names_tuple, 2), 3), j - 4)); + } + else { + name_str = NULL; + } - for ( fn=sf->feat_names; fn!=NULL && fn->tag!=tag; fn=fn->next ); - if ( fn==NULL ) { - fn = chunkalloc(sizeof(*fn)); - fn->tag = tag; - fn->next = sf->feat_names; - sf->feat_names = fn; + if ( name_str ) { + for ( fn=sf->feat_names; fn!=NULL && (fn->tag!=tag || fn->field!=j); fn=fn->next ); + if ( fn==NULL ) { + fn = chunkalloc(sizeof(*fn)); + fn->tag = tag; + fn->field = j; + fn->next = sf->feat_names; + sf->feat_names = fn; + } + for ( on=fn->names; on!=NULL && on->lang!=lang; on=on->next ); + if ( on==NULL ) { + on = chunkalloc(sizeof(*on)); + on->lang = lang; + on->next = fn->names; + fn->names = on; + } else + free(on->name); + on->name = copy(name_str); + } } - for ( on=fn->names; on!=NULL && on->lang!=lang; on=on->next ); - if ( on==NULL ) { - on = chunkalloc(sizeof(*on)); - on->lang = lang; - on->next = fn->names; - fn->names = on; - } else - free(on->name); - on->name = copy(name_str); } return( 0 ); } diff --git a/fontforge/sfd.c b/fontforge/sfd.c index 1499411489..aca965eef5 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -64,6 +64,7 @@ #include /* For NAME_MAX or _POSIX_NAME_MAX */ #include #include +#include #include #include #include @@ -1841,11 +1842,57 @@ return; putc('\n',sfd); } +static void SFDDumpOtfSubFeatNames(FILE *sfd, SplineFont *sf, uint32_t tag, enum otffn_field field, uint32_t lang) { + struct otffeatname *fn; + struct otfname *on; + bool cvfound = false; + + for ( fn=sf->feat_names; fn!=NULL; fn=fn->next ) { + if ( fn->tag == tag && fn->field == field ) { + for ( on=fn->names; on!=NULL; on=on->next ) { + if ( on->lang == lang ) { + SFDDumpUTF7Str(sfd, on->name); + cvfound = true; + } + } + } + } + if ( !cvfound ) { + fprintf( sfd, "\"\"" ); + } +} +static int SFDDumpOtfCountFeatNames(FILE *sfd, SplineFont *sf, uint32_t tag, uint32_t lang) { + struct otffeatname *fn; + struct otfname *on; + bool cvfound; + int i; + + for ( i = 0; i < 65536-256; ++i ) { + cvfound = false; + for ( fn=sf->feat_names; fn!=NULL; fn=fn->next ) { + if ( fn->tag == tag && fn->field == otffn_paramname_begin + i ) { + for ( on=fn->names; on!=NULL; on=on->next ) { + if ( on->lang == lang ) { + cvfound = true; + } + } + } + } + if ( !cvfound ) { + break; + } + } + return i; +} static void SFDDumpOtfFeatNames(FILE *sfd, SplineFont *sf) { struct otffeatname *fn; struct otfname *on; + bool cv[100] = {false}; + int cvnum, cvcount, i; + char prefix[3] = {0}; for ( fn=sf->feat_names; fn!=NULL; fn=fn->next ) { + if ( (fn->tag & 0xffff0000) == (('s' << 24) | ('s' << 16)) ) { fprintf( sfd, "OtfFeatName: '%c%c%c%c' ", fn->tag>>24, fn->tag>>16, fn->tag>>8, fn->tag ); for ( on=fn->names; on!=NULL; on=on->next ) { @@ -1854,6 +1901,29 @@ static void SFDDumpOtfFeatNames(FILE *sfd, SplineFont *sf) { if ( on->next!=NULL ) putc(' ',sfd); } putc('\n',sfd); + } + else if ( ((fn->tag & 0xff00) >> 8) >= '0' && ((fn->tag & 0xff00) >> 8) <= '9' && (fn->tag & 0xff) >= '0' && (fn->tag & 0xff) <= '9' ) { + cvnum = (((fn->tag & 0xff00) >> 8) - '0') * 10 + (fn->tag & 0xff) - '0'; + if ( !cv[cvnum] ) { + fprintf( sfd, "OtfFeatName: 'cv%c%c' ", fn->tag>>8, fn->tag ); + for ( on=fn->names; on!=NULL; on=on->next ) { + fprintf( sfd, "%d ", on->lang ); + SFDDumpOtfSubFeatNames(sfd, sf, fn->tag, otffn_featname, on->lang); putc(' ',sfd); + SFDDumpOtfSubFeatNames(sfd, sf, fn->tag, otffn_tooltiptext, on->lang); putc(' ',sfd); + SFDDumpOtfSubFeatNames(sfd, sf, fn->tag, otffn_sampletext, on->lang); putc(' ',sfd); + cvcount = SFDDumpOtfCountFeatNames(sfd, sf, fn->tag, on->lang); + fprintf( sfd, "%d", cvcount ); + if ( cvcount ) putc(' ', sfd); + for ( i = 0; i < cvcount; ++i ) { + SFDDumpOtfSubFeatNames(sfd, sf, fn->tag, otffn_paramname_begin + i, on->lang); + if ( on->next!=NULL || i < (cvcount - 1) ) putc(' ',sfd); + } + } + fprintf( sfd, " \"\"" ); // TODO: character + putc('\n',sfd); + cv[cvnum] = true; + } + } } } @@ -6497,26 +6567,79 @@ static void SFDGetDesignSize(FILE *sfd,SplineFont *sf) { } } -static void SFDGetOtfFeatName(FILE *sfd,SplineFont *sf) { - int ch; - struct otfname *cur; - struct otffeatname *fn; +static void SFDGetOtfFeatSubName(FILE *sfd, SplineFont *sf, struct otffeatname *fn, int16_t lang) { + struct otfname *cur; - fn = chunkalloc(sizeof(struct otffeatname)); - fn->tag = gettag(sfd); - for (;;) { - while ( (ch=nlgetc(sfd))==' ' ); - ungetc(ch,sfd); - if ( !isdigit(ch)) - break; cur = chunkalloc(sizeof(struct otfname)); cur->next = fn->names; fn->names = cur; - getsint(sfd,(int16_t *) &cur->lang); + cur->lang = lang; cur->name = SFDReadUTF7Str(sfd); +} + +static void SFDGetOtfFeatNamePrepareNext(SplineFont *sf, struct otffeatname *fn) { + struct otfname *cur; + + if ( fn->names->name ) { + fn->next = sf->feat_names; + sf->feat_names = fn; + } + else { + cur = fn->names; + fn->names = cur->next; + chunkfree(cur, sizeof(struct otfname)); + chunkfree(fn, sizeof(struct otffeatname)); + } +} + +static void SFDGetOtfFeatName(FILE *sfd,SplineFont *sf) { + int ch, i; + struct otffeatname *fn; + uint32_t tag; + int16_t lang, parmcnt; + + fn = chunkalloc(sizeof(struct otffeatname)); + tag = fn->tag = gettag(sfd); + fn->field = otffn_featname; + for (;;) { + while ( (ch=nlgetc(sfd))==' ' ); + ungetc(ch,sfd); + if ( !isdigit(ch)) + break; + getsint(sfd, &lang); + SFDGetOtfFeatSubName(sfd, sf, fn, lang); + if ( (tag & 0xffff0000) == CHR('c','v','\0','\0') ) { + SFDGetOtfFeatNamePrepareNext(sf, fn); + + fn = chunkalloc(sizeof(struct otffeatname)); + fn->tag = tag; + fn->field = otffn_tooltiptext; + SFDGetOtfFeatSubName(sfd, sf, fn, lang); + SFDGetOtfFeatNamePrepareNext(sf, fn); + + fn = chunkalloc(sizeof(struct otffeatname)); + fn->tag = tag; + fn->field = otffn_sampletext; + SFDGetOtfFeatSubName(sfd, sf, fn, lang); + + while ( (ch=nlgetc(sfd))==' ' ); + ungetc(ch,sfd); + if ( !isdigit(ch)) + break; + getsint(sfd, &parmcnt); + + for ( i = 0; i < parmcnt; ++i ) { + SFDGetOtfFeatNamePrepareNext(sf, fn); + fn = chunkalloc(sizeof(struct otffeatname)); + fn->tag = tag; + fn->field = otffn_paramname_begin + i; + SFDGetOtfFeatSubName(sfd, sf, fn, lang); + } + + // TODO: character + } } - fn->next = sf->feat_names; - sf->feat_names = fn; + SFDGetOtfFeatNamePrepareNext(sf, fn); } static Encoding *SFDGetEncoding(FILE *sfd, char *tok) { diff --git a/fontforge/splinefont.h b/fontforge/splinefont.h index 51ad0209fc..4a1bd99a8c 100644 --- a/fontforge/splinefont.h +++ b/fontforge/splinefont.h @@ -666,10 +666,17 @@ struct otfname { char *name; /* utf8 */ }; +enum otffn_field { + otffn_featname, otffn_tooltiptext, otffn_sampletext, + otffn_characters /* reserved */, + otffn_paramname_begin /* this is the beginning of parameter block, so */ + /* there should be no enum elements beyond this */ +}; struct otffeatname { uint32_t tag; /* Feature tag */ struct otfname *names; struct otffeatname *next; + enum otffn_field field; uint16_t nid; /* temporary value */ }; diff --git a/fontforge/tottfgpos.c b/fontforge/tottfgpos.c index d40c8ba070..73d4e01c3b 100644 --- a/fontforge/tottfgpos.c +++ b/fontforge/tottfgpos.c @@ -43,6 +43,8 @@ #include "ustring.h" #include "utype.h" +#include + int coverageformatsallowed=3; int use_second_indic_scripts = true; @@ -3247,11 +3249,27 @@ return( NULL ); return( efile ); } -struct otffeatname *findotffeatname(uint32_t tag,SplineFont *sf) { +struct otffeatname *findotffeatname(uint32_t tag,enum otffn_field field,SplineFont *sf) { struct otffeatname *fn; - for ( fn=sf->feat_names; fn!=NULL && fn->tag!=tag; fn=fn->next ); -return( fn ); + for ( fn=sf->feat_names; fn!=NULL && (fn->tag!=tag || fn->field!=field); fn=fn->next ); + return( fn ); +} + +static bool getfeatnamefromginfo(struct alltabs *at, FILE *g___, uint32_t tag, enum otffn_field field, SplineFont *sf, uint32_t name_param_loc, uint32_t name_param_ptr, uint16_t *nameid) { + struct otffeatname *fn; + if ( (fn = findotffeatname(tag,field,sf))!=NULL ) { + if ( fn->nid==0 ) + fn->nid = at->next_strid++; + fseek(g___,name_param_ptr,SEEK_SET); + putshort(g___,name_param_loc-name_param_ptr); + if ( nameid ) + *nameid = fn->nid; + return true; + } + else { + return false; + } } static FILE *dumpg___info(struct alltabs *at, SplineFont *sf,int is_gpos) { @@ -3347,7 +3365,7 @@ return( NULL ); ginfo.feat_lookups[i].name_param_ptr = 0; if ( ginfo.feat_lookups[i].tag==CHR('s','i','z','e') ) size_params_ptr = ftell(g___); - else if ( ginfo.feat_lookups[i].tag>=CHR('s','s','0','1') && ginfo.feat_lookups[i].tag<=CHR('s','s','2','0')) + else if ( (ginfo.feat_lookups[i].tag>=CHR('s','s','0','1') && ginfo.feat_lookups[i].tag<=CHR('s','s','2','0')) || (ginfo.feat_lookups[i].tag>=CHR('c','v','0','1') && ginfo.feat_lookups[i].tag<=CHR('c','v','9','9')) ) ginfo.feat_lookups[i].name_param_ptr = ftell(g___); putshort(g___,0); /* No feature params (we'll come back for 'size') */ putshort(g___,ginfo.feat_lookups[i].lcnt);/* this many lookups */ @@ -3373,17 +3391,48 @@ return( NULL ); putshort(g___,sf->design_range_top); } for ( i=0; inid==0 ) - fn->nid = at->next_strid++; - uint32_t name_param_loc = ftell(g___); - fseek(g___,ginfo.feat_lookups[i].name_param_ptr,SEEK_SET); - putshort(g___,name_param_loc-ginfo.feat_lookups[i].name_param_ptr); - fseek(g___,name_param_loc,SEEK_SET); - putshort(g___,0); /* Minor version number */ - putshort(g___,fn->nid); - } + if ( ginfo.feat_lookups[i].name_param_ptr!=0 ) { + if ( (ginfo.feat_lookups[i].tag & 0xffff0000) == CHR('c','v','\0','\0') ) { + uint16_t featUiLabelNameId = 0; + uint16_t featUiTooltipTextNameId = 0; + uint16_t sampleTextNameId = 0; + uint16_t numNamedParameters = 0; + uint16_t firstParamUiLabelNameId = 0; + uint32_t name_param_loc = ftell(g___); + getfeatnamefromginfo(at, g___, ginfo.feat_lookups[i].tag, otffn_featname, sf, + name_param_loc, ginfo.feat_lookups[i].name_param_ptr, &featUiLabelNameId); + getfeatnamefromginfo(at, g___, ginfo.feat_lookups[i].tag, otffn_tooltiptext, sf, + name_param_loc, ginfo.feat_lookups[i].name_param_ptr, &featUiTooltipTextNameId); + getfeatnamefromginfo(at, g___, ginfo.feat_lookups[i].tag, otffn_sampletext, sf, + name_param_loc, ginfo.feat_lookups[i].name_param_ptr, &sampleTextNameId); + for ( numNamedParameters = 0; numNamedParameters < 65536-256; ++numNamedParameters ) { + if ( !getfeatnamefromginfo(at, g___, ginfo.feat_lookups[i].tag, + otffn_paramname_begin + numNamedParameters, sf, name_param_loc, + ginfo.feat_lookups[i].name_param_ptr, + numNamedParameters == 0 ? &firstParamUiLabelNameId : NULL) ) + break; + } + + fseek(g___,name_param_loc,SEEK_SET); + putshort(g___,0); // format + putshort(g___,featUiLabelNameId); + putshort(g___,featUiTooltipTextNameId); + putshort(g___,sampleTextNameId); + putshort(g___,numNamedParameters); + putshort(g___,firstParamUiLabelNameId); + putshort(g___,0); // TODO: charCount + //putu24(g___,0); // TODO: character[charCount] + } + else { + uint32_t name_param_loc = ftell(g___); + uint16_t nameId = 0; + getfeatnamefromginfo(at, g___, ginfo.feat_lookups[i].tag, otffn_featname, sf, + name_param_loc, ginfo.feat_lookups[i].name_param_ptr, &nameId); + fseek(g___,name_param_loc,SEEK_SET); + putshort(g___,0); /* Minor version number */ + putshort(g___,nameId); + } + } } /* And that should finish all the features */ diff --git a/fontforge/tottfgpos.h b/fontforge/tottfgpos.h index 53ed8f9d1d..413d939319 100644 --- a/fontforge/tottfgpos.h +++ b/fontforge/tottfgpos.h @@ -16,7 +16,7 @@ extern int gdefclass(SplineChar *sc); extern int SCRightToLeft(SplineChar *sc); extern int ScriptIsRightToLeft(uint32_t script); extern SplineChar **EntryExitDecompose(SplineFont *sf, AnchorClass *ac, struct glyphinfo *gi); -extern struct otffeatname *findotffeatname(uint32_t tag, SplineFont *sf); +extern struct otffeatname *findotffeatname(uint32_t tag, enum otffn_field field, SplineFont *sf); extern uint32_t ScriptFromUnicode(uint32_t u, SplineFont *sf); extern uint32_t SCScriptFromUnicode(SplineChar *sc); extern void AnchorClassDecompose(SplineFont *sf, AnchorClass *_ac, int classcnt, int *subcnts, SplineChar ***marks, SplineChar ***base, SplineChar ***lig, SplineChar ***mkmk, struct glyphinfo *gi); diff --git a/fontforgeexe/fontinfo.c b/fontforgeexe/fontinfo.c index de44a1325b..57852c2700 100644 --- a/fontforgeexe/fontinfo.c +++ b/fontforgeexe/fontinfo.c @@ -1077,6 +1077,146 @@ static GTextInfo otfssfeattags[] = { { (unichar_t *) "ss18", NULL, 0, 0, (void *) (intptr_t) CHR('s','s','1','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, { (unichar_t *) "ss19", NULL, 0, 0, (void *) (intptr_t) CHR('s','s','1','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, { (unichar_t *) "ss20", NULL, 0, 0, (void *) (intptr_t) CHR('s','s','2','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + + { (unichar_t *) "cv01", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv02", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv03", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv04", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv05", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv06", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv07", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv08", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv09", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','0','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv10", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv11", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv12", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv13", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv14", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv15", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv16", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv17", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv18", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv19", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','1','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv20", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv21", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv22", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv23", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv24", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv25", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv26", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv27", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv28", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv29", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','2','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv30", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv31", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv32", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv33", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv34", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv35", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv36", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv37", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv38", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv39", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','3','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv40", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv41", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv42", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv43", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv44", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv45", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv46", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv47", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv48", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv49", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','4','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv50", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv51", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv52", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv53", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv54", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv55", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv56", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv57", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv58", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv59", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','5','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv60", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv61", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv62", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv63", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv64", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv65", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv66", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv67", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv68", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv69", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','6','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv70", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv71", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv72", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv73", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv74", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv75", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv76", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv77", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv78", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv79", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','7','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv80", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv81", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv82", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv83", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv84", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv85", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv86", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv87", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv88", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv89", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','8','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv90", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','0'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv91", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','1'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv92", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','2'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv93", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','3'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv94", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','4'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv95", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','5'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv96", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','6'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv97", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','7'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv98", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','8'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) "cv99", NULL, 0, 0, (void *) (intptr_t) CHR('c','v','9','9'), NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + + GTEXTINFO_EMPTY +}; +static GTextInfo otfssfeatfields[] = { + { (unichar_t *) N_("Feature Name"), NULL, 0, 0, (void *) (intptr_t) otffn_featname, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Tooltip Text"), NULL, 0, 0, (void *) (intptr_t) otffn_tooltiptext, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("featname|Sample Text"), NULL, 0, 0, (void *) (intptr_t) otffn_sampletext, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + //{ (unichar_t *) N_("Character List"), NULL, 0, 0, (void *) (intptr_t) otffn_characters, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 1"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 2"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 1, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 3"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 2, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 4"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 3, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 5"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 4, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 6"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 5, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 7"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 6, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 8"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 7, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 9"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 8, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 10"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 9, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 11"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 10, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 12"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 11, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 13"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 12, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 14"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 13, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 15"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 14, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 16"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 15, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 17"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 16, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 18"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 17, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 19"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 18, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 20"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 19, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 21"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 20, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 22"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 21, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 23"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 22, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 24"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 23, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 25"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 24, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 26"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 25, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 27"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 26, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 28"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 27, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 29"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 28, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 30"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 29, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 31"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 30, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, + { (unichar_t *) N_("Parameter Name 32"), NULL, 0, 0, (void *) (intptr_t) otffn_paramname_begin + 31, NULL, 0, 0, 0, 0, 1, 0, 1, 0, 0, '\0'}, GTEXTINFO_EMPTY }; /* Put styles (docs call it subfamily) first because it is most likely to change */ @@ -1297,6 +1437,7 @@ static struct col_init ci[] = { static struct col_init ssci[] = { { me_enum, NULL, mslanguages, NULL, N_("Language") }, { me_enum, NULL, otfssfeattags, NULL, N_("Feature Tags") }, + { me_enum, NULL, otfssfeatfields, NULL, N_("Field") }, { me_string, NULL, NULL, NULL, N_("Friendly Name") } }; static struct col_init sizeci[] = { @@ -3283,11 +3424,14 @@ static int ss_cmp(const void *_md1, const void *_md2) { const char *l1, *l2; if ( md1[1].u.md_ival == md2[1].u.md_ival ) { - l1 = langname(md1[0].u.md_ival,buf1); - l2 = langname(md2[0].u.md_ival,buf2); -return( strcoll(l1,l2)); + if ( md1[2].u.md_ival == md2[2].u.md_ival ) { + l1 = langname(md1[0].u.md_ival,buf1); + l2 = langname(md2[0].u.md_ival,buf2); + return( strcoll(l1,l2)); + } + return( md1[2].u.md_ival - md2[2].u.md_ival ); } -return( md1[1].u.md_ival - md2[1].u.md_ival ); + return( md1[1].u.md_ival - md2[1].u.md_ival ); } static void SSMatrixInit(struct matrixinit *mi,struct gfi_data *d) { @@ -3298,21 +3442,22 @@ static void SSMatrixInit(struct matrixinit *mi,struct gfi_data *d) { int cnt; memset(mi,0,sizeof(*mi)); - mi->col_cnt = 3; + mi->col_cnt = 4; mi->col_init = ssci; for ( cnt=0, fn=sf->feat_names; fn!=NULL; fn=fn->next ) { for ( on=fn->names; on!=NULL; on=on->next, ++cnt ); } - md = calloc(3*(cnt+10),sizeof(struct matrix_data)); + md = calloc(4*(cnt+10),sizeof(struct matrix_data)); for ( cnt=0, fn=sf->feat_names; fn!=NULL; fn=fn->next ) { for ( on=fn->names; on!=NULL; on=on->next, ++cnt ) { - md[3*cnt ].u.md_ival = on->lang; - md[3*cnt+1].u.md_ival = fn->tag; - md[3*cnt+2].u.md_str = copy(on->name); + md[4*cnt ].u.md_ival = on->lang; + md[4*cnt+1].u.md_ival = fn->tag; + md[4*cnt+2].u.md_ival = fn->field; + md[4*cnt+3].u.md_str = copy(on->name); } } - qsort( md, cnt, 3*sizeof(struct matrix_data), ss_cmp ); + qsort( md, cnt, 4*sizeof(struct matrix_data), ss_cmp ); mi->matrix_data = md; mi->initial_row_cnt = cnt; } @@ -3717,30 +3862,39 @@ static int SSNameValidate(struct gfi_data *d) { int rows; struct matrix_data *strings = GMatrixEditGet(edit, &rows); int i, j, k; + uint32_t tag; for ( i=0; i>24, tag>>16, tag>>8, tag, - mslanguages[k].text, - strings[3*i+2].u.md_str, - strings[3*j+2].u.md_str - ); -return( false ); - } - } + if ( strings[4*i+3].u.md_str == NULL ) + continue; + tag = strings[4*i+1].u.md_ival; + if ( (tag & 0xffff0000) == CHR('s','s','\0','\0') && strings[4*i+2].u.md_ival != otffn_featname ) { + ff_post_error(_("Invalid field"),_("The feature '%c%c%c%c' cannot contain Tooltip Text, Sample Text or Parameter Names."), + tag>>24, tag>>16, tag>>8, tag + ); + return( false ); + } + for ( j=i+1; j>24, tag>>16, tag>>8, tag, + mslanguages[k].text, + strings[4*i+3].u.md_str, + strings[4*j+3].u.md_str + ); + return( false ); + } + } } -return( true ); + return( true ); } static void StoreSSNames(struct gfi_data *d) { @@ -3749,6 +3903,7 @@ static void StoreSSNames(struct gfi_data *d) { struct matrix_data *strings = GMatrixEditGet(edit, &rows); int i; uint32_t tag, lang; + enum otffn_field field; struct otffeatname *fn; struct otfname *on; SplineFont *sf = d->sf; @@ -3756,16 +3911,18 @@ static void StoreSSNames(struct gfi_data *d) { OtfFeatNameListFree(sf->feat_names); sf->feat_names = NULL; - qsort( strings, rows, 3*sizeof(struct matrix_data), ss_cmp ); + qsort( strings, rows, 4*sizeof(struct matrix_data), ss_cmp ); for ( i=rows-1; i>=0; --i ) { - if ( strings[3*i+2].u.md_str == NULL ) + if ( strings[4*i+3].u.md_str == NULL ) continue; - tag = strings[3*i+1].u.md_ival; - lang = strings[3*i].u.md_ival; - for ( fn=sf->feat_names; fn!=NULL && fn->tag!=tag; fn=fn->next ); + field = strings[4*i+2].u.md_ival; + tag = strings[4*i+1].u.md_ival; + lang = strings[4*i].u.md_ival; + for ( fn=sf->feat_names; fn!=NULL && (fn->tag!=tag || fn->field!=field); fn=fn->next ); if ( fn==NULL ) { fn = chunkalloc(sizeof(*fn)); fn->tag = tag; + fn->field = field; fn->next = sf->feat_names; sf->feat_names = fn; } @@ -3773,7 +3930,7 @@ static void StoreSSNames(struct gfi_data *d) { on->next = fn->names; fn->names = on; on->lang = lang; - on->name = copy(strings[3*i+2].u.md_str ); + on->name = copy(strings[4*i+3].u.md_str ); } } @@ -9607,8 +9764,9 @@ return; SSMatrixInit(&ssmi,d); ssngcd[0].gd.flags = gg_visible | gg_enabled; - ssnlabel[0].text = (unichar_t *) _("The OpenType Style Set features ('ss01'-'ss20') may\n" - "be assigned human readable names here."); + ssnlabel[0].text = (unichar_t *) _( + "The OpenType Style Set ('ss01'-'ss20') and Character Variant ('cv01'-'cv99')\n" + "features may be assigned human readable names here."); ssnlabel[0].text_is_1byte = true; ssngcd[0].gd.label = &ssnlabel[0]; ssngcd[0].creator = GLabelCreate; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4152ef0a8d..4f18717a3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -120,7 +120,7 @@ if(ENABLE_PYTHON_SCRIPTING_RESULT) add_py_test(test1017.py "test1017.fea" "Ignore invalid substitutions in feature file") add_py_test(test1018.py "Ambrosia.sfd" "non linear transform anchors") add_py_test(test_math_device_table.py "Ambrosia.sfd" "MATH device table properties") - add_py_test(test1020.py "getter and setter of font.style_set_names including errors") + add_py_test(test_style_set_names.py "getter and setter of font.style_set_names including errors") add_py_test(test1021.py "deleting points from contour") #add_py_test(findoverlapbugs.py "find overlap bug") add_py_test(test926.py "DejaVuSerif.sfd" "Validate WOFF output") diff --git a/tests/test1020.py b/tests/test_style_set_names.py similarity index 50% rename from tests/test1020.py rename to tests/test_style_set_names.py index 214f4e6458..adbc745d7c 100644 --- a/tests/test1020.py +++ b/tests/test_style_set_names.py @@ -25,6 +25,15 @@ def lang_to_lang_str(style_set_names): (("English (British)", "ss20", "My English ss20 with a different name"),), ((0x809, "ss20", "My English ss20 with another different name"),), (("English (British)", "ss20", "My English ss20 with another Name"),), + (("English (US)", "cv01", ("My English cv01", "My tooltip text", "My sample text", ("My parameter text",), None)), ("English (US)", "cv02", ("My English cv02", None, None, (), None))), # same language, more + (("English (British)", "cv01", ("My English cv01", None, None, (), None)), ("French Côte d'Ivoire", "cv01", ("Mon cv01 français", None, None, (), None))), # same style set, non-ascii + (("English (British)", "cv09", ("My English cv09", None, None, (), None)),), # fewer + ((0x809, "cv09", ("My English cv09", None, None, (), None)),), # language as int + (("Chinese (PRC)", "cv10", ("我的中文cv10", None, None, (), None)),), # Unicode + (("English (British)", "cv99", ("😀𝄞", None, None, (), None)),), # Unicode + (("English (British)", "cv99", (None, "My English cv99 with a different name", None, (), None)),), + ((0x809, "cv99", (None, "My English cv99 with another different name", None, (), None)),), + (("English (British)", "cv99", (None, "My English cv99 with another Name", None, (), None)),), () ]: font.style_set_names = style_set_names @@ -43,20 +52,37 @@ def lang_to_lang_str(style_set_names): ((("English", "ss01", "My name"),), ValueError, "The first element of a style set name specification must be a valid language name if it is a string."), (((0, "ss01", "My name"),), ValueError, "The first element of a style set name specification must be a valid language id if it is an integer."), ((("English (US)", 1, "My name"),), TypeError, "The second element of a style set name specification must be a string."), - ((("English (US)", "01", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ss00", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ss21", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ss30", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ss001", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ss0D", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), - ((("English (US)", "ssOI", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set tag."), + ((("English (US)", "01", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ss00", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ss21", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ss30", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ss001", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ss0D", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "ssOI", "My name"),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), ((("English (US)", "ss01", 123),), TypeError, "The third element of a style set name specification must be a string."), ((("English (US)", "ss01", "My name1"), ("English (US)", "ss01", "My name2")), ValueError, "The feature ss01 is named twice in language English (US):\n My name1\n My name2"), ((("English (US)", "ss01", "My name1"), (0x409, "ss01", "My name2")), ValueError, "The feature ss01 is named twice in language English (US):\n My name1\n My name2"), - ((("English (US)", "ss01", "My name"), ("English (US)", "ss01", "My name")), ValueError, "The feature ss01 is named twice in language English (US):\n My name\n My name") + ((("English (US)", "ss01", "My name"), ("English (US)", "ss01", "My name")), ValueError, "The feature ss01 is named twice in language English (US):\n My name\n My name"), + ((("English (US)", "cv00", ("My name", None, None, (), None)),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "cv001", ("My name", None, None, (), None)),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "cv0D", ("My name", None, None, (), None)),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "cvOI", ("My name", None, None, (), None)),), ValueError, "The second element of a style set name specification must be a valid style set or character variation tag."), + ((("English (US)", "cv01", 123),), TypeError, "The third element of a character variant name specification must be a tuple."), + ((("English (US)", "cv01", "My name"),), TypeError, "The third element of a character variant name specification must be a tuple."), + ((("English (US)", "cv01", ("My name",)),), ValueError, "The third element of a character variant name specification must have 5 elements (feature name, tooltip text, sample text, tuple of parameter labels and characters)."), + ((("English (US)", "cv01", (123, None, None, (), None)),), TypeError, "The first element of the third element of a character variant name specification must be a string or None."), + ((("English (US)", "cv01", ("My name", 123, None, (), None)),), TypeError, "The second element of the third element of a character variant name specification must be a string or None."), + ((("English (US)", "cv01", ("My name", None, 123, (), None)),), TypeError, "The third element of the third element of a character variant name specification must be a string or None."), + ((("English (US)", "cv01", ("My name", None, None, (), 123)),), TypeError, "The fifth element of the third element of a character variant name specification is reserved and must be None."), + ((("English (US)", "cv01", ("My name", None, None, None, None)),), TypeError, "The fourth element of the third element of a character variant name specification must be a tuple."), + ((("English (US)", "cv01", ("My name", None, None, (123,), None)),), TypeError, "All elements of the fourth element of the third element of a character variant name specification must be a string or None."), + ((("English (US)", "cv01", ("My name1", None, None, (), None)), ("English (US)", "cv01", ("My name2", None, None, (), None))), ValueError, "The feature cv01 is named twice in language English (US):\n My name1\n My name2"), + ((("English (US)", "cv01", ("My name1", None, None, (), None)), (0x409, "cv01", ("My name2", None, None, (), None))), ValueError, "The feature cv01 is named twice in language English (US):\n My name1\n My name2"), + ((("English (US)", "cv01", ("My name", None, None, (), None)), ("English (US)", "cv01", ("My name", None, None, (), None))), ValueError, "The feature cv01 is named twice in language English (US):\n My name\n My name") ]: try: font = fontforge.font() + print(style_set_names) font.style_set_names = style_set_names style_set_names_with_str_lang = lang_to_lang_str(style_set_names) raise AssertionError("no error raised, but expected") From 9d793fe90ea0c5356e3fb00fa31d0737e1c952fc Mon Sep 17 00:00:00 2001 From: MihailJP Date: Tue, 4 Nov 2025 02:29:32 +0900 Subject: [PATCH 09/96] Fix crash issue in allmarkglyphs (#5668) --- fontforge/tottfgpos.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fontforge/tottfgpos.c b/fontforge/tottfgpos.c index 73d4e01c3b..895ba87b06 100644 --- a/fontforge/tottfgpos.c +++ b/fontforge/tottfgpos.c @@ -1821,24 +1821,24 @@ static SplineChar **allmarkglyphs(SplineChar ***glyphlist, int classcnt) { int i, tot, k; if ( classcnt==1 ) -return( SFOrderedGlyphs(glyphlist[0])); + return( SFOrderedGlyphs(glyphlist[0])); for ( i=tot=0; i Date: Tue, 4 Nov 2025 23:46:52 +0200 Subject: [PATCH 10/96] Fix UFO crash for empty contours (#5645) --- fontforge/ufo.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fontforge/ufo.c b/fontforge/ufo.c index 56643d0f18..e9d9888547 100644 --- a/fontforge/ufo.c +++ b/fontforge/ufo.c @@ -2722,6 +2722,10 @@ static SplineChar *_UFOLoadGlyph(SplineFont *sf, xmlDocPtr doc, char *glifname, for ( points=contour->children; points!=NULL; points=points->next ) if ( xmlStrcmp(points->name,(const xmlChar *) "point")==0 ) break; + if (points == NULL) { + // The UFO3 specification allows empty contours, we just drop them. + continue; + } for ( npoints=points->next; npoints!=NULL; npoints=npoints->next ) if ( xmlStrcmp(npoints->name,(const xmlChar *) "point")==0 ) break; From 5df9f189ea416f4c96e570e6e255c61967a41ab6 Mon Sep 17 00:00:00 2001 From: MihailJP Date: Thu, 6 Nov 2025 05:48:46 +0900 Subject: [PATCH 11/96] Document `no-mac-names` flag for font.generate (#5669) --- doc/sphinx/scripting/python/fontforge.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 13cec10ac4..1990827bc9 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -4542,6 +4542,11 @@ This type may not be pickled. Include a 'TeX ' table in an ttf/otf file + .. object:: no-mac-names + + Do not include Mac names used on Classic Mac OS. This option does not + affect native macOS (formerly known as Mac OS X) applications. + .. object:: round Round PS coordinates to integers From 46dc37435bc5b3daaf160f494cd36677d2bb8cb3 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Thu, 6 Nov 2025 22:54:52 +0200 Subject: [PATCH 12/96] Fix crash in Metrics View (#5647) --- fontforge/tottf.c | 4 ++-- fontforge/ufo.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fontforge/tottf.c b/fontforge/tottf.c index 1b686c0b19..dddd5d6a6d 100644 --- a/fontforge/tottf.c +++ b/fontforge/tottf.c @@ -6174,7 +6174,7 @@ int _WriteTTFFont(FILE *ttf,SplineFont *sf,enum fontformat format, fake_mappings = calloc(sf->glyphcnt,sizeof(bool)); for (i = 0; i < sf->glyphcnt; ++i) { - if (sf->glyphs[i]->unicodeenc == -1) { + if (sf->glyphs[i] && sf->glyphs[i]->unicodeenc == -1) { sf->glyphs[i]->unicodeenc = fake_unicode_base + sf->glyphs[i]->orig_pos; fake_mappings[i] = true; } @@ -6222,7 +6222,7 @@ int _WriteTTFFont(FILE *ttf,SplineFont *sf,enum fontformat format, // Remove temporarily assigned fake Private Area unicode point from all unmapped glyphs if (flags & ttf_flag_fake_map) { for (i = 0; i < sf->glyphcnt; ++i) { - if (fake_mappings[i]) + if (sf->glyphs[i] && fake_mappings[i]) sf->glyphs[i]->unicodeenc = -1; } free(fake_mappings); diff --git a/fontforge/ufo.c b/fontforge/ufo.c index e9d9888547..d686333593 100644 --- a/fontforge/ufo.c +++ b/fontforge/ufo.c @@ -1050,7 +1050,7 @@ void clear_cached_ufo_paths(SplineFont * sf) { // First we clear the glif names. for (i = 0; i < sf->glyphcnt; i++) { struct splinechar * sc = sf->glyphs[i]; - if (sc->glif_name != NULL) { free(sc->glif_name); sc->glif_name = NULL; } + if (sc && sc->glif_name != NULL) { free(sc->glif_name); sc->glif_name = NULL; } } // Then we clear the layer names. for (i = 0; i < sf->layer_cnt; i++) { From 7466cf332ea264676f64da4f9f6a65070ba9350a Mon Sep 17 00:00:00 2001 From: chanicpanic <51764816+chanicpanic@users.noreply.github.com> Date: Sat, 8 Nov 2025 10:08:25 -0800 Subject: [PATCH 13/96] Document missing python module members (#5657) --- doc/sphinx/scripting/python/fontforge.rst | 66 ++++++++++++++++++++++- fontforge/python.c | 4 +- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 1990827bc9..9674917f7a 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -345,6 +345,11 @@ Module functions Returns a tuple listing the directory paths which are searched for python scripts during FontForge initialization. +.. function:: userConfigPath() + + Returns the path to the user's FontForge configuration directory, which should + be writable. + .. function:: fonts() Returns a tuple of all fonts currently loaded into FontForge for editing @@ -439,6 +444,9 @@ Module functions and computing the optical left and right side bearings (for 'lfbd' and 'rtbd' features). For more information see its own section. +.. function:: onAppClosing(hook) + + Add a python function which is called when FontForge is closing down. .. _fontforge.ui_functions: @@ -682,6 +690,16 @@ Not a very useful example. should be a comma separated list of extensions. It may be omitted, in which case it defaults to being the same as the "extension" argument above. +.. function:: getConvexNib(context) + + Returns the specified 'Convex' nib as a layer. ``context`` may be + ``"default"``, ``"freehand"``, or ``"ui"``. + +.. function:: setConvexNib(nib, context) + + Sets the specified 'Convex' to a layer/contour. ``context`` may be + ``"default"``, ``"freehand"``, or ``"ui"``. + .. function:: logWarning(msg) Adds the message (a string) to FontForge's Warnings window. (if you wish to @@ -1323,6 +1341,10 @@ Layers may be compared to see if their contours are similar. the current layer and the first argument. If amount is 0 the result will look like the current layer, if 1 then like the first argument. +.. method:: layer.reverseDirection() + + Reverse the orientation of each contour in the layer. + .. method:: layer.round([factor]) Rounds the x and y coordinates. If factor is specified then :: @@ -1440,14 +1462,14 @@ Layers may be compared to see if their contours are similar. .. method:: layer.xBoundsAtY(ybottom[, ytop]) - Finds the minimum and maximum x positions attained by the contour when y is + Finds the minimum and maximum x positions attained by the layer when y is between ybottom and ytop (if ytop is not specified it is assumed the same as ybottom). If the layer does not have any y values in the specified range then FontForge will return ``None``. .. method:: layer.yBoundsAtX(xleft[, xright]) - Finds the minimum and maximum y positions attained by the contour when x is + Finds the minimum and maximum y positions attained by the layer when x is between xleft and xright (if xright is not specified it is assumed the same as xleft). If the layer does not have any x values in the specified range then FontForge will return ``None``. @@ -1670,6 +1692,10 @@ must be created through the font. Whether this glyph has been modified. This is (should be) maintained automatically, but you may set it if you wish. +.. attribute:: glyph.codepoint + + Unicode code point for this glyph in U+XXXX format, or ``None``. (readonly) + .. attribute:: glyph.color The color of the glyph in the fontview. A 6 hex-digit RGB number or -1 for @@ -2702,6 +2728,22 @@ must be created through the font. validated, if force is unspecified (or specified as false) then it will return the cached value if it is known, otherwise will validate it. +.. method:: glyph.xBoundsAtY(ybottom[, ytop, layer=]) + + Finds the minimum and maximum x positions attained by the glyph when y is + between ybottom and ytop (if ytop is not specified it is assumed the same as + ybottom). If the glyph does not have any y values in the specified range + then FontForge will return ``None``. A layer name or index may be provided + to only find bounds for a particular layer. + +.. method:: glyph.yBoundsAtX(xleft[, xright, layer=]) + + Finds the minimum and maximum y positions attained by the glyph when x is + between xleft and xright (if xright is not specified it is assumed the same + as xleft). If the glyph does not have any x values in the specified range + then FontForge will return ``None``. A layer name or index may be provided + to only find bounds for a particular layer. + .. method:: glyph.draw(pen) Draw the glyph's outline to the `pen argument. `_ @@ -2750,6 +2792,10 @@ This type may not be pickled. This is read-only. +.. attribute:: selection.font + + Returns the font for which this is a selection. + .. method:: selection.__iter__() Returns an iterator for the selection which will return all selected @@ -3293,6 +3339,10 @@ This type may not be pickled. PostScript copyright notice +.. attribute:: font.creationtime + + Font creation time. (readonly) + .. attribute:: font.cvt Returns a sequence object containing the font's cvt table. Changes made @@ -3522,6 +3572,11 @@ This type may not be pickled. (`source `_) +.. attribute:: font.markClasses + + A tuple each entry of which is itself a tuple containing a mark-class-name + and a tuple of glyph-names. + .. attribute:: font.layer_cnt The number of layers in the font. (Read only. Can change using ``add`` @@ -4055,6 +4110,9 @@ This type may not be pickled. might have no lower case letters because it was upper case only, or didn't include glyphs for a script with lower case letters). +.. attribute:: font.xuid + + PostScript eXtended Unique ID. .. method:: font.__iter__() @@ -4354,6 +4412,10 @@ This type may not be pickled. Removes the current subfont from a cid-keyed font. +.. method:: font.clearSpecialData() + + Clear special data not accessible in FontForge. + .. method:: font.close() Frees memory for the current font. diff --git a/fontforge/python.c b/fontforge/python.c index d25b334a7c..71971f5521 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -20187,8 +20187,8 @@ PyMethodDef module_fontforge_methods[] = { { "hasUserInterface", PyFF_hasUserInterface, METH_NOARGS, "Returns whether this fontforge session has a user interface (True if it has opened windows) or is just running a script (False)"}, { "registerImportExport", PyFF_registerImportExport, METH_VARARGS, "Adds an import/export spline conversion module"}, { "registerMenuItem", (PyCFunction)PyFF_registerMenuItemStub, METH_VARARGS | METH_KEYWORDS, "Adds a menu item (which runs a python script) to the font or glyph (or both) windows -- in the Tools menu"}, - { "getConvexNib", PyFF_getConvexNib, METH_VARARGS, "Sets the specified 'Convex' to the layer/contour argument."}, - { "setConvexNib", PyFF_setConvexNib, METH_VARARGS, "Returns the specified 'Convex' nib as a layer."}, + { "getConvexNib", PyFF_getConvexNib, METH_VARARGS, "Returns the specified 'Convex' nib as a layer."}, + { "setConvexNib", PyFF_setConvexNib, METH_VARARGS, "Sets the specified 'Convex' to the layer/contour argument."}, { "logWarning", PyFF_logError, METH_VARARGS, "Adds a non-fatal message to the Warnings window"}, { "postError", PyFF_postError, METH_VARARGS, "Pops up an error dialog box with the given title and message" }, { "postNotice", PyFF_postNotice, METH_VARARGS, "Pops up an notice window with the given title and message" }, From 0ff1f20f21591286ad08e5d26a5c3020123c2e2f Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sun, 9 Nov 2025 22:05:27 +0200 Subject: [PATCH 14/96] Sort entries in "Language List" dialog and allow search (#5648) * Support localization in development environment * Set position for ff::dlg::Dialog * Modal GTK dialog blocks legacy GWindow parent * Class LanguageListDlg * Sort scripts and languages by UI language * Add gtk_post_error() to utils.cpp * Support CSS for Gtk::TreeView and GTK::Entry, tooltip * Fix mistaken copyrights --- doc/sphinx/images/lang-dlg.png | Bin 1922 -> 23600 bytes fontforgeexe/fontview.c | 2 +- fontforgeexe/gtk/CMakeLists.txt | 2 + fontforgeexe/gtk/css_builder.cpp | 62 +++++++- fontforgeexe/gtk/dialog.cpp | 71 ++++++++- fontforgeexe/gtk/dialog.hpp | 18 ++- fontforgeexe/gtk/language_list.cpp | 119 +++++++++++++++ fontforgeexe/gtk/language_list.hpp | 53 +++++++ fontforgeexe/gtk/simple_dialogs.cpp | 112 ++++++++++++--- fontforgeexe/gtk/simple_dialogs.hpp | 50 +++++-- fontforgeexe/gtk/utils.cpp | 66 ++++++--- fontforgeexe/gtk/utils.hpp | 7 +- fontforgeexe/lookupui.c | 200 ++++---------------------- gdraw/ggdkdraw.c | 6 + gutils/fsys.c | 12 +- {fontforgeexe/shapers => inc}/tag.hpp | 21 ++- po/CMakeLists.txt | 2 +- 17 files changed, 562 insertions(+), 241 deletions(-) create mode 100644 fontforgeexe/gtk/language_list.cpp create mode 100644 fontforgeexe/gtk/language_list.hpp rename {fontforgeexe/shapers => inc}/tag.hpp (81%) diff --git a/doc/sphinx/images/lang-dlg.png b/doc/sphinx/images/lang-dlg.png index 4f891e8815f91f00ec3029e2a137816eb4732937..7850e93256d8d639f54e7cf07c423ba34fb3dd30 100644 GIT binary patch literal 23600 zcmeFZbyytDwl54x&;$$a7Tg^McXxMp8Dww`?hpuW0fGb#!993z3lQ8PSa7$m$+maD z`|fkj{+@gPJ2THfPj&UGs@1F3`Yq{+P*RXYLc~Laf`USlmJ(Bef`XO=J~r^MKud&o zY7p?R+e=LgqGIe$;^+*vu(mTNfp|KalbCy0TR=g1%-JgmBR?mFZ#j(=fQH7*S$C_K z_8;5E4wi4Sr*gufh$VMy$wq15DTbFin3J%ziYE;!MCkddr>BI)B&Dccu{~xX^JPGT z2X8Jf`IL_-v-Ya>#`xoha34rl?s>MJ`)rY0|YQs=ZAEMMb)Yv;)d;7U-`d0zAl zl?Yf|khQ3&lC-Gk-_`@HBGdOBzf_kHL64zofeJ0M2Ab11rL6rtvRIuEGBx;Gky2#J z6hGm_&^J9j%8m@&S5;cBj!x549qOnr1cer0sKl{pj{T)`#iQZ+?mgTOoi344@h}Uu z^{WF4=Pf{KsMU??EN79-uq*-8VrkmFnQsbKc-wxVt?cP%T3+Yex}24GMnOZr^GL=m zly4E{YqiGDyo|^%dsEuI-Mvn!AAj|3FR~Au8O8XLkIlwn;Cp1O0 zJ~jCwAJV<50neyvjZan6{MRqZJ$=p7_ShxkyUCJCgo6X|28zE*WNKt#ytJNEyt)e0 zZx>?>(KBaurvHG5^kon3F=%;FC!`;Ni6#N6$&g^%6a)U?*>Kak4>7O8=?i#)=bB#g zhW1cwIUi@{b@t(Z&ciZR%%^{W)B6P_su*fFP=WE01_SZyEv~OB^3QgdPFn+Txo@Tb z6z(pq>vIBvXQtu<2A7`hik9HVZ0qyO=Y@UF&9H8ECV<#^SOemvB`*gu1=}+kn}JQt z89nSB0l9;M;uG|6G&Z$0hme?Aca;P|PTiHziT7lebZ{9y7hc4T5N$?HzwF3Sizp4Q6Kg_kND9&UU~1n3*z}+nL(~L|uUS zvHVkih_%H(nDtL{dph%bIe(l8Fx=ni|5NY3t^IdrpqIQnNDOT1`ZTDt7(dz5d_iVl zQ)@HO??YBLE*=gZGh+r;ZewN!c5VwE25xRnE(UfJ3sz=Mb}rz}@&{7V4lWR52UGJW zQUGyAYk-a^mobZpnF%+8i7_V!13N1h3xf%di8%v{IgbT97mI}j4+qyDNGLj615#;h z_s3a1kun2Fv2by+nH#gQFz~Pf1lWyPco=xhfGKfVaGUcObFrASu`&N9^>iOV!b;No zWUP#TXi>5=hFE}|?fJ>%tQ}lE{?S#<+TL6hV*JE3U3PX9_GJH?KeFQ zb7vPo;GSr*Ff+1o{=VGI6eIyqGzJ96+TPgGoXOF_^7o0S+X4a>12ES3NlpREzuSSe zfJB|mjUixXH89wYpX`Y?$rH)HpOy!1T3oyRt@20=(C{=T(zkK`4S3B$9TZx3^_oe_D zoBm}9F2-)=X20hN(E7_MQ!8T!OLIVb{8e24)^7cO5CwK~HWmvL6AlIoGiE@VELfg| zg2jS?-ISG!huw^w-Hg@zpQ*cmEg{;0@!>ur{bzP@p%+y`7~)lzOWt-?`z(0TGW!uU zSoNs#z=xu!HjGrco7n_akG5(-Qj3OhXT1!HMTPfd)t~#l@g^8 z+leymru-mN8kioP*3%YLk(LMyCH3#`Gn}{{=W2|DLS{ccoLnyZD;U<PHTtJdV}xnh3LSKD$^+I znmI#|+wKYS3W+a8ut${StWy*=1IUsyt;u@K>RAT6Z8BpAbucPk80Bz!LH_uQUFK4C zLPCNlnId@77u+E3;V!ZGA~KH?cGn5^1b!Cf`eZu zWP#F-FV}i}tE#I9C${ElZS`8cDAo^?Xw{R6utZv>iST2K6tX_9dmqpkETnwgc$3a< zJ{U_F5@<}9vh>hA*a~kadCaa!!h5# z>-zB6aUUNa10%i)ig`O(q~LUa<2qfb8`WWCbh05ml*uCMWdMKd6}GNVr$J9e!-HUH zi4E0uWk=l;g-r#bO;Dv$OnCo(XlQ4W#xU2U*U@LaHzpb$FQlV4bjkNQkMk8Xdiz7@ zVzZmf=u1+AL*sLOBz#Vh(ka&SFzc52JlwuG;;zBLjIVz8<2w+mTAhT%#Qr2Y?7`&9 zk6Hfsj0W6s&CR@T0R}ST6l;`&>~-ot8odpiI28%!%6HBLSVIwct5oHqT@e=Ma=xqX z6X~H^Ax9&VbY?zT7<2Z>9q-UPO5D@?N2)T=ccVZE>4z%Lq|N@3!IF?rqHMFvCy`TBfDoZae2_#PTs+?6?d zqVpcs&d$!P&c*rO%yOi!B|9DqUJR3+ujNRl@ydq|dach#Rrc8QTHn432l2RU$&H|t zMqx7{AIvG2YS;s_5?u|<=H}&P+elQbV)%bWG8EhjClty--%6p;z7qf+||wHaMuD3is;U0fg>$rvJ}uf-oo zARztN%E_6T6bJq0lfgh3D9FLwW^Tt7a0uC(!!$Hp&P3^3uCrS``=qMZ>KsDEFZfY1 zE_*10D}{nI%6h&+zn6lyFOINlY3Xf(UxC$+&uH!Ubs?}Zc+f}d``T~1`v$>T9`)3@7({wAEC-ZSV@>Q?f)?Q`Rmi|nWrxUl2&#o&*21cgE2a5?Y+ zv-Kv0_T)k(#)?l1IyPRtUg$z2xsafL5_`5|I)KG#>9I(;)?G;9kD3VM!7r}3c=j@Sqc&KcKGS5Icp9l1fm z508xOWT?b1*RAhn$iX-CVTo3!(=@TQh5F!rC}35iQ>b`LI_Q=q;G6#N9QNeeQuLE% zB@xsy0z#Q~b*SJ2x0Kco4)(~<&XCrhv!~B}e0;vNJgjtx4n1ySi-?L=)mP`cWq}~O zKcFAo4iFIWq-#sm+2(qw-W;8dp3b&|i6f+?Jv=taoz zy6IN`N5APZ{n2n^LzPyqb1JXlrB-iTUcYAy%l6r(s~%i9Cxqhph%uhvS~jz8c0159$;-E% zE5jYm;Mg8YW3yXmh&VcOY|G|m$zN0yd>mTqAqB(;_OZ^s0T;WkG78Jo*7A0!U9J7G zEo*nGWV}$`Rz<}D?TZIXS7_YIc8-7N$i>A;907mlw+|!>r;Vq=m%pI)ms&GAa(n_y zO0<(b_of+Ok?_QPeE1IT=HJn2GF}jKf9;MibfMeC8cye5R6003k?^o=f^R6HZ{p1 ztZ9-=(of7Wy1pQmQ_dKii%nUSZ_WOG1|6vfj(T6-1VG$DVww%|wyu-_-C6j{Mb3(@9sID!g{o1O|Knw5UbO!9g@d~})> z8`I^wTH4y?d_;J714C((Gju4jgi-8x z7_q#Xud^OrQ50werwt}EZO_#*7b-rIuKRLWDdlvCh8~^XmM2QV!Dc0v}&y_twbVKD29 z>qqLp1uuRW-|Bp4G6i!mnT3oxJeF{7eRWkBP=^Q)%NTXw$E>t!f3wajr)YRYldBZN z`1JJ1W=TK0SFoVAnQYsciYmKn8R=Sr_k#X@P)XyRDq)dJ#H2vZ=M4rK(D=!Q-7yt*zw?@&Jj_0>As4%dpOD zUkpaQR3Fd9hNOgqW7Q^zwZl^M`fgI`Xd{?^%%+xJ>xwQnH?aA47ZJdGgC3_)mG>A>6#5nVej#J-!Hu_RXvq`o(9KbvcY7A?X$GWh49vh z@zmIUJJ_#wBcKFgk*@oP^XAP~|J$yLFc&(C5G)2=e#fk1gwe~5oa$<}{J=o? z2R~ZqR)IXT{)8H|vB$zL@l0+bRU+aFIegN?bG6!4 zcDMj0!Jt=)2ar}8k0}U>_;PPl6W9rb^7x+|i{_{ebDse!;p1c9Te%#T#8vX3hP%V| zC;gAONw=c?A>fU^ThMVQ%nb^T)KW_Y_RT6{M57>n zn-v@>1n0Ez5JjhT9eKi<)p?ys*wK5QZc)&(Ch(1n5D2JKKF@qwPlfKb4=5-$*w9;Ern1)j>UjT>4#B@Ve@c8qb&3*48g#UUg2g zJ^J&i-JZ3-0dNXba9G&-=4R4nW9rY;+ikNNydjjU7b0i;_9(;xavoaT_qUh%;-_1< zaa0ScvyBVa3lHZ2I9adT| z4i38;67r#WyoR)(O4Q5K*Qd0nO4L8;eUhnTq$x3I1sTQ=$4Qj7j2ZbXm<#aW#kw>FGVteN;;#myF{(XO&H1jL|4HU+#v5LhW`59v01#IRGZyKE*obv%^5G;gw#dz$Kz3R;*V2nYdN?~z#CdZ zORZNo;+a<-bFzV7ynVN-VNDfYY@lD3`8QvvRXE?8zQtzD9KC7>yoCdFreD9_(-{&m zF)^vAs#ew47im?^yF_`l5&@7J5&MFSfgvWr&#!(vneSx%*DprtOH?=%qK)2|TmODg z%O#QdV6ySz&G}F^e@60Qo2-urU#UiATcyn)G8Gk-PMux(}=4HKdEFBNbWA*SFmf^anms*nI;sic(VW$QQ^~?1;A) zhnANW{FnBkqbT02Hs7saUlE8^7s`+CLI{4e=h$ji3VbHMIG7)OvV$8(XpvY9A~rU7 zDV2JpWNV;hpZjyu<_ERqW?; zIs{xB%NN=^6ZMbk4fuZArE#~9J=A(v?WE}i1qBNds|W|P)hYbm7Xr)}fB{rE6lm41 z_Cm_w1XH^IT41*u3ovN)8V9g`k-@fP>oZ4)qtSYyJb{4E3v5knfQ!;1K78#DP1J)6 zrd_Z|Qu6Yq!*d+3?)P2JG&Izx4YP>@gLblaUdEhqZyT#Ap}K6}cdw9&e*W?$Pb36U zE{!z}(81<}z3QJtl*Xj681=sG6Z?1f_6h-})@mt_M_4%ji;qdqz(C}u4pprhE0*TV zU*@;5gg+r5L@a8d{N9+HpZAQ~O=oR=cXxM0II%E@SR{%>R2r2AvL+_a+}!Fsyj#0w z4E>Wf-W4=}nD0&7-(AduYize4^+qq+?Q=a+;^I&qGfo!KL-c1V^r?00hJ1;c zzag9NHyj&hHA7qhXEtYR*c9+^KE9z6tQpRWr%)?V!>trS$r1f-Rm*Na@qXpdKmN=+ zMlK7KbdJyY#d95&od;rZq3rf+uIK6){c3Bkl18<#W*_xkzMp;r$z8-F!p#Rs6hN|x zuf=QK1w${22gYV+wF8eio(#itI24s)qui9&(NMf~heD-ODh-apv!rVw-!y!eniYw? zE_Y6UxJ8L1OO3V__C^ziB_$D6c7`}XAa582-Xh^oqZ4Av9~uyEUfrb7>kk3p15dd%a}4 z;`T4#gLXPf8@z zsSHp0h9Cr0w;vsC(9Xve7dz7{05=8SN#t|JnXE!J?~>?H3{W zOibJRvB44SHP?EdDO!y(oqI8R>_^teo?ufO9lZ6n^zha!!uwm;wbksn(wR=JMlX(a zN(Hm4?VNTz=rp(B7~Kj}ZGTP|3BQ19GPw7sU|r_6G2`<&*FC@)Tc7L*I-=EsZ#9V1 zTov&Fd7!%0xs@$`UY+_W>_R_YMH}jK?HF%_Ari<^)a7*e_P)k`arf22aLfop*F^K{i-HZD%D=ZxaElcp!goiR}T z1b2pTB{x1_BXuqEwmF3>si`&2({Kw1#woh-F{15t>+?)+FL3?A<25u{fT@t%^V~Id zwcAHpSXdys(rfc^=w51gcTaYB!UXxW7{sw-?&<&Y@;luJ(zPG7ZPv44kbK;h2iwx6 zg^r^0^kZKL%rjX3zU^#B^KAl3-|1k%3cHQ3@9{8-XHpQ_dT z=eyKYk6S8f*$`0DX9fO&Y|(yac7$Wtsu~Z7b-tNQOkeHrxT8}8!WAGa`hh>L%r>wqzh87&k?D-n^oMvnNw0lrZT_~TAq{{VYjW&MzFm3Ww zXqAPDsU30rbz@6be404vQcHlGtYS0xyUnM1v!0SEdc)dXH3?LSk7bc$8|^GMLgM;vRF*|>Bm1>jT>za=2E!I{4(>zBbker`{c3&f)a}q z_=V-LixkRzii?tu28q+!Au1Hk_pj@y%<$gWMvdo%pbT8Y$muxa<`Xc9d_%qE863YQ z#b8hghJO3@uFifQT`f-I`;FNf`Q&r52Knb}u#lp{qU2O`{IvQrdQZLDZu7bQ3DGvo zqtGq6UG0#$_RA0UNr6Ao)*9SypxO_Hx>CX{$K(XasJrnA|k?{t(unHmDY zI}L}~z1L1ByIjH&F^lub`Lhuaiw()&g1&UX1j3zJ{boSiRDh4b{#D(Z?#1}**CH+I zuh$%m>^}pl9JJ{~x-UabneVeD27jEa79&AE!x*-W_{`net!Hj7v^boZ(eO?XL5j2+U%GV4#Pl;o zr8nR)U;5W;COg*Dv6$U0_fu4!WuvZ@CnRK^tr*v$iohcP_9@;G*)`hlE`)vby3oYssc1N(T_o$O9Oi_QN7EIyac$Ek%i(Xf+dOqz zZtcmN>LWu!G%k`+*VJE1BK_iMM#uU2uHd^Is5RhgUF#La2aXCDloNaTc>)>-26{6| z1X6=4r&zJK5oAZq+Pjqi9-AyI^&i@*lG zb?T$Jh?iwaTvxEr{9VoMoGEzZ3&FZVAv&HOdTR)=uz!DRO$chGH1{*D)2!GEXBg{JqRRN<<9Px?6Ft z9(}T&s-pb4)%`52#zRYb`1Ij=$$~@5XL~f;ig_iloj0P*is7szfH10@U%=?0H%1fjP{=74D-p$Qf;7zBAF)IjM%0-~= zuoNyj^<&M*-O{Po@nVEXrtfBxdTmw20S-`t_c(6Uglb9iXeC7|qbSt&bEBg#6-}Rg z|9O7b#DhFDy=a{vn+IfoKw~%krqj18d*v`3f$GE>1r94B6}K?V*2fkX1fbqy zQ(GJ*);($i-5Pvo*R#7;M`+1O^@>XE?!$f<&%=8Vj@dE*O7bkTUE~%7vxrd&O=lV~ z&j~TV@0F$zIpcaiQk13d$E`0u8%vE&+Q~MqrzAdjro&ve?wou#o{=}`mZ-6h-lRS~ zn2jdVQjwIO_CvG-vO_S0JLvq1XzFbXVsU@On^MFJ2L~M1nCO`8`p@SJt;Eo3-==D? z2^T6qJN(k5pSR5Osb_ruy+0P9fd$a`-1;)D!1aYGqSL$FyJ~mzJ@+iC@mzv?tV$CP zp3-^iJ?#w8p;c;w$1B4rJjrGg4KZTF;rCT9BOud@{s}!zAmX;euCn9PhL4q9D1#%Ip&-f|9 zUU9bu>X+2gU5CC(n_E>~nQXkL4wvrz`DTRY*aV+5zC(Y>4@PC09%PzSd9>(t-;3}s z;`PO!?I!|OuF$o+Ef=$X7=zD)i{A6jkm|I{NfX3CC@0P%4JfGQE|z5e$GzdNg!P{p zlp=nywOcydUW-MeuGynV%9B2;??t1&XA1TZ@Bd`{C$$s4O*4p)7P($29S5LH#OY*2 z^OL=P^D#&A{-I;KPj<+V_yItk+~^Vw71baZ_*`I0T2LhG^*L)9mIyqRF4Cj>8hq~= z`@>;3TBaSx>sS%^TxE=*t$D+IE6$8D>c}Uk&d14D@Dc#!{{(5X^Hcoj=z0bc>dpy& zs&xmpBs;Eqk#%A9o)#TnSM-(Vu?_Cg4CP?=@59d?i-cRglFSNwF55i2_U>9CaL2;L zq0nV*jbWNVek~4vcT$2}eQ$d!G2yfOp}a*eEw}YVqmhp?*f7@$;kjqm>sd@XxFFOu z8rr_WqIGSr*;kQ)Nl93WT>KmZyh`P#>#8r@}^IkX(P zgQYW?+ntjDT3N9-u?_*`Z4i!=5d_Ym)-sly3&B+PolMR_(CJ8JMa*j_!<=uBE?rk$iPp30;zAd+e+P zPntbAYs0opE?*4eis7h6BHdHk^86q(dMJmutE{w&Y7lPbkR&SD75=33Llv4rX)A@B;CJ>R-z=C_u1IH>! zk@Q*wG0XuvGQ!r07SA^1%GNq_Xe$l9XByAnkU`qg1hAY;!O02vUc+wMAj!s`b zsz9>|4mbA;Oa{%E#ma74l?~jsT|n9iM(eCF`&u+0B@Ut$W*o9r*iMWg?Y2B)dGE-v z{T0_L#T=b$^JE!p%W_uMLA~MZ4++fYKQE?3lxhSXtEG z1>~Z+Ahi57+pRq78kG$VtzPR`mtD|{n`MSqbrL@!TCZZPy%5QFPTN#Ty^~yJwuK^+ zqbC#h;3#!~g(eNGv3J#y{T3PNJT-CRiiJKc-&qg-cv`hL7N3~YYz&GkKu6U<=C?X# zkj2+(w2<#zp0Ba%Ujbo3$z{JLU|ik?z#j_969jzHx4*IAzaCTu)91wVun*~b0T6eZ zI{8=X@iyl$fwZ(j0-(`J&9oT)-Q~w?JCG*&$CM{{`oXy;@@M&(T7}xNI-(?7ZU@J> z?iJcuit@!o(2eCt^M=c({(3682||$#C~R!YbPk8;sA`vsE*9TM)D~C7Nat=VO?D7;)euS^nXaH>hqE1P zXS!JPmOh6gYlLl@wMZB34D7@u@c1ykwg|=_Z&jzKuW|l}cx)4z4||!ui16y{n~Du( zmVuDbyusb;F}#^Txa+#Y1Kqh8o+&{*dio6#%(cxs4Z-K4RTV=9teGbR))vv(qwQ%9 zjW)5MuVv7~H0x>j_^r?>d9^z%o^Y#|3QV-1nq$ed$#xcs#r@9{^J}}RVc8wG;ceSk z6*l|ETgW!j*CGlyWv{p~sbxi32qU+w3M&c)8fjKthvd&?fZAQ*xZ+xm5Q$07AF=enZj$V zI+TLucBYDB*DbP`%l*i@)$2?*S!n8E`FJwW`c=85S(l}J@nyf;8f#b#PoOxo{q^#c z%2_6JRE|Y#=kg}|DO~-BTSd)=465CjWn7hJ&Q?F2mFDa#s;YD7-`mRJPECBN0j)`$_zw?p zO`Y@esg&0ynbtx`Ey%Tqo_vMik$uAgy&cp#`3 zF_c{_Ig1W|SCa?vi7=GTlw5n;E2FD%3_73I7<>pdH*nZTtW9*8-h5!GDK44x`sC|3 z*rJz@2WpN;mklZbvueh$mZBuxKX`_}x3|f;)0rwcCg7OJ-0}S>Q$qeIrx}9EbLfB- zOUNa#&!abxZ)9sLq^v3>qug=kUuNj?tk%p{$1_7pPOjxX@neBVmW);rk@;w@oTVeC zHy*@6+JNFz)cn&*eisIsL@+LMmU-7r^~&VCfTmb8n@IWDxS!hceYLf8Jn<6Kn{L)I zx?7u|_xUO;gj?Z`WWxbFuXO#I;l+86jI)LYr&K=pa_#=0=DU3Gw2IJ1WkO*n%&1y; zKcO6+qHwt%rPSz2j;wAP6_f=lmz#;g@s0>xjHmAdT+IwRaY}gPa2A1czL4L&Z$`l+ zG$pzPCbQwz1SWcujey5ua}S?J zup)*kL@%e${KMrLN&rh{>uDjg{mvl)ze~u^V2lZQiwSUns3&HlqhvT2DZ#KNw--A> zbsIw4l8%!4kOk(TO_vs16-ymT>ekTa!TV1#llR9hw|V!BlA&?Pr4e3SH#ph;ZY*1OsEnaXdyVP(8~qj$^XgI= zCMHAmtzr-g(ynJ-wod2W^9b$G&MMvHs)IDjN1_Em8)vfqlEBss{7|`&Psevyb=iq= z2#Ck&>aiA`7mQ}f(`eB&-QuAqqf7E-%dXjCVa{i|Es?96Wvp1*Uf(WXEsbSL^Oq{! zUJ})mtDy3#63i$NuheIGUY#7hN;ltIC)4q{^&hpg#bax8v0uCOlS*7e{;0RmaYu~h zJW!6_4m(A4oAP1r6Uc*|L9~gj)?V~A+~r3%Xa*K4P8Q2mV|s$@v*lQ-L*f`q``wm& zxKH zPs3x4_0vQj+dM8b*#pnMK4Ymp<=00m>t7Cm;ibpDzt*I`p^d+mfJ4-@?)pH%#=YFi zqpF-u^7+fSAl4>Mn+Y_{^FHQsQ}y0rw+*JL;3d_1w3>EI^_f9on(k5|TwVvCAMwJ^ z7of`QUrms^7u+l4h#*}T=)fz>&M)%UGXfE##a52J4VsSC6W>^rkFT2=f>2a(Q zX;Hvqg};t9tn7(Tt>~HViTYyS%SvMfFR#Ce5dlB|Fsb~~jgLU3K+a$lN$|H!qzRaR ziwgb~C;X}9Dl-)UsUl*jGwwrs^h6gBN@F9KyhQ2HE&4$1?#He}kEGy{Tqb_op*rhK zs_g~%hP~tb|E=wA%nAUU(f=}Tq`RX`+C9%w=7;eo3%w<$_-_pH$im3Kv6+JnefI~R za-}WARTOt5Y|ru6w;lYw(|{zw`jYvx`jJW#tqf-mkdCSOwnRkleX2KoCdZ<)@gjt1 zoK^?~C*TX0rS5AH`bitx=q~Xc1;f}(`ha;g0kw$KW~%&eBiyTXv#$@tp4xnVdU1`C zX<>|E=^u~yaOdJz1Xo=afzvn|V9@IPtLiKtc>4)PJ23Ym=*Q9n+}bdPl7^O}s93DU zbjb)FOl*yEqhDy6)-iv4(2+C9`pHu=d0_s)7t$D{i)wv|ZR@>YQ0V0g1APwqOr497 z1?LasVeu-6wL7i~U8^Q2KHT)jVvB4za_4R2n$e)J^D;VKYeTq(Uh_TD&HkM65vMjcD}TXNvpS>lO~N*Hsm z9B3e_DW-s2diasNt_~K1Bjn?IIZ@S~Y}hPaix=w$VFM~1l3_Gt(czLv>I^T}HgkI* zY{?ngp}QCCKPN!sIM5hj&T6!UT?A+f9|8MkJP5|lwSp5UbD_e49{Y<$`?mNZ1eGbz zS!{DFhFdL$0og6>i^iVB+q;`vjiM)8wbjgH^UCtDg+@;IhF5Y_j}$~20!d6e)7?=r zpcs_gQJeHc_( z&nauAt;NTvNv?HntiB7&xY1+?m52ql7|k^$5UdM@rf@|WI_YC%ThF3d&?E;0b&jf> zAO=U?IRKtCX!~dDK=e%7w+U&YAyF^~cD>+K zk5%N;(BT-4v{*EdXv%mE)IU*|;8vH2Y;ki)pki6WTJlH*#}BZ0Q;=7M1@27nq_QP% z;_#J-CSUge`PB`v1a%m1F0NN;E;~==z0`cOc73_&;DqwIQ0d zjlbLI`9#MP9i2LpZcd|F|MgS>&@JQojxF!LOR-r&v`?B%Cz}xa?n%zRvX?S+vjBEbpimleVk9B(<^TwKIEsNy-YiQ_qY}-me_hTP+m- zL(wNP^#d+-obmY{#r;<)JYTnTlUmv}U23M-8wlCs?AN$SO&1D7DoS^zK#hym->T6* zH=X@VU??*F@akOWRm6^toqh}Tyajz?VCdL)bpZP+W0*t99!P%KKM@6(gVmty}92?9$mmGRsd!GX+&7@8f6u%sm{%^c9#t0hT@L^A~0 z=LQ*6dNcG&wVH9^ii%uT=R+k&(%_ayvirA?>0|!n9!ncj(6A;Zy)p!5r=5=B)C|fS z7E8o(F?@DLojIh+yH^RsK^+$-$yjy{o7q}AZr!QTDd&s|#GYLKlvR*}wv07W7Seri zuf^b}8d@t#%Rh4G8NTaa`1vr2IW(&iMaZ$MDU#EWlO_51QuwqWrtDTSEM}8~6U;5) zC1I6qzWvMAN`~u0MfJ087snniGJbqCW@o&Y*szl`OzBhPa}%yx1$GWms3M_LiX#1@ z{VGIOZh9;G;^YlWjg>h&pehQiqsuP}8?7#arO4&2;Y_=uUkWlJcG12jE^o__DjwXt zpST-Y(l26}rU^l2)8wq!Y*06C;^O_UC2iWPpUmRHXzzxd<#j(JySA?G z|7z9adcNhzS-d$e`ervCcJZEW7U1O|yt>#B*Dd97uq^q-w-sQ*3Lm3J_OC&2o93 zt`VG%Ux%9QmXPI5R#h<_V8a=J*HMB}s>0<+I&=RlwZ?NNYu1(QophRxgWlqt__uKs8zJlrvR&C29d*bqsx zy*I5WFO-<|L2xd;UkHLEUB9umUwtffcXAOnZR$=t-OuIA}S<{); zdaC74@|Fy!`N@YFQ)y5}k1RMaJ@{^a_vuEZ-9dmoAU#v+ONr6pRw${P=Q3<$UA>9^)n3pl+9i7!1j7BOX``QIFxPFV22RZMG;1HWr^FZ_Fe~ z`{1#std?V;R&uL+mh%PqwosJY zUqdApy$Bn;}oR{)t_&i?^MKZCM5 z{#76IbBPuUYmQ*)4!e;ND6m?8jwtu<<7@U3-Y=uD&#??Pnu1YT%lw{&n&_vsnsIL? zO%CDk1JwXWV~rgv=~_TUYnTfo6Lrymkp| z#smre?i&1>xDf_LCk1%?ZNu@!XZrD=Yz}(q+4k6S<+$uiRa3w9Nr^1&c#SaV4c&Jdb(M4=6tH zTP=1zl^`hT#e!?@qgTklRzw-}CYsC>kCHXO{xY4ZQ=`M2v2i0#+P#PK2mE=t&R8^_ z>-|m0VEkK{D_~WXxG8_-jKslBR+Uah-o!?p75~oivHcSieC|nsAA$-Fr{dGKr~EIG zoQ#($P=OMX|L@V3o)v&Rs;N!;Vo>l|tpe}^YGZ_dhm>Xh3Vj@T#OePB@#sJKng0^J z{(G&Pf!80)8YU-O1sW*52ERn}l3oHi$CkYR0-#*Bi1ef;NU5lh(MYI!X~7@sO#Ju0 z=^y1@XUCYdJA)t=Z^71$ig}D&_EQ?f3;M0S_pz*yt=>zbjrsIC+yjP<&|m-wd1~EL z|5@JSSuCwqCC3bVc^SZh7D<=nb-~1D7DlFb3kOv|1X-tslRxkh&E9Y5=wYG~LjSR^ zVnL~{ZvnR=qw@#9DRma!`@TMM>}jv(BTG|1*^TLpS@mgxJ+xQhjkNRN_-(ZkzW5XB z<`ePv75Ca1OWkX{Fgz}pFDAY^4qL4ma7j0W1!B9SnjTsL>6Mj5{?$+#d(Bmji(2oc|hhmyH1$BXhKfZk_9H9A+%L73!3v9tBFTrn_EtwQ!oc*7zc z$guz0IApvO{P78`5mH~-Wyj2|Q_8QO*?07AH`1KMOmanA*}=nmT`T9x+Fc4U zMpI$xpYR*v_xzycbXwB*!(yH8e&E3ZWeVAtA}tF8EKZLQKfcsh{dJA+?rJNZQ?^a+ zKZ(C2NK>FB7U_t#?VY35A~WV^Y#%^rR90GeeKX&`Px)-~MKIPqZ1-!vb>o>-85}-a)onr~bnLj*Qk``x&7sJj>!oR|Zlv!< zKpSsrQP*IC|GmlpEBF)i2Il#0ziw%LP3&_okTb>o^%7}-#i!?)3T&*gPG3p1^1(O5 z5`@+w2XZTax{`{NEa0);(2l(yPU~Hvukco2jqFY1pDG_}RT2`RQu!#j_1Z>(tks{& zV*jo){>KM@H^?n#j_l~5ZT*wR?;G>zJWly^|442fQ-Gs@YAvL`vwP8_X&UN*EaDg} z*&y_63fpnNBegm`skUuQ+TQnK2??*IS=#n>KeoxtlLdQZ{SWG?yr614F6Sj6V9b{< z4`ltkD<|rJ!i%qt-&YK(dEdSz(v}(iIQbIZZa&KJna;Y-5y|p&&9D{N__(B{N7Gst zumD4!ZSlN5YEeT(>PDVu{+utc~oq^?~xp>RY>Kbmo=UN^K~6y%WdgaM{=c{mynNspt>{_1Wo?-5jC&@Qj;#OQJM!F=tXvg4~JA6ruTqsa_?ae?8&S zP2G7rMd@O=v>9xM(n8UZib^bLvLGM%o;t*aGch?E(YpR)kh70np^>en;`nR7O#Wjo z4UOp^bG&A=f{)8UXiHLg(dmwjtvO?#{B1n^TfTtc6y>*(Flb`$0@dP3zE4y2)CFMD zU@GlBuL&e6$BemxvY}J?{DCiUO}9n|6$1km)$>JsJgl@Nqr*KE)XUDN-vt)T80z!oOrGhpfvf-;_` zHYR$*H9i=F(MQa^(Wt*(&iUMQ=W5zaSVZpdlv;nYsrY<86Q{n)vA$Dy`o2N5bHq3|p`(2MRvxR~ z-^BIQ0ydPo z>Ck%s!>-66*=15zu#@#4E#cu0-r>WEGkNf;Opfn9XR8_t3aVOn+z;+s&I2=6^KH)w zJU8u1+F>?}lJ#u$QIu?C?$kfAVxLaS&V3m;atP^(8iA6p*|GT(KG=0RqleS1eAHAc zLZvuLd5o|!Zs+-#3)txI#4mII%pDiDz_!9W(x@5}Zr{qYQ|@Ka+*vHl%4KXfSEJWu z7OiFGv=`WuF^aj#FTDf555@OIf@#NZN>5{PCI$9DyLAlct&9vi0>< z*Id0L)ySV9}narMlB}I$>!K(E;IaFceOYOoX z!)|5#i1X@jBr3k*^<}THX59`Blmt+H$)t52#Q1w}rdNUzw%NsNi(evd-Ip9HGjOMN zrQg-JFm7ZIlC{XBj#3;YBAGU_ZPRuFj&u3xPugNz4czgzg`IX8L%J?x?v_n#(+4sj zzMhSL7cV?Gi`RnvdFbgo=L@Kb(y`^qf;XA04nCBx4s)p3!p=~? zyB1qNz=|1_XQz;`F8$)C@-DF6LI-#1aBw z!=-&wDZ6npuYS6XieYVNsf2rK6;s;b0hT^;1uccZMq%*f83b;a#@Y`zQ+DN0S}7*G zmd_`zAc?CUoyW9G5^7$PR8&+&VCW$P7WsJ#SaCR$U(NpuW3p9*4a$K2bPSDV(wfD* zIkG#~rPfXs?IyXG zt(40Rw8Op);t$y1_2M~gp9qX#H9=j+Fbpiyz_5~NcaDars-n8K>lF_d54jj$*w!>c z7>*9)bkGoG<&@iiDBzP%3n6m|KffrBkgj7GA&gKET@Td=wkTx7hJ)DI1L%KF6&010 z96MG?Wra?sALS5l?B$Eyk=ff)W{$CyIz}1Xc;gi)sis<-Y^b*JSNlO}S!4g*oRf{! z`U++~dIQx@-NB62bI9%fB=`4ALWy~9+<4*@C=QF-=^WoG<%_pgvhu^N>@Fy#!XLsE z0yAVnH|j14R8vSzR;aVfnysK}U<(Ud0HTB=M+Mpi>7+I`0c;(m@QA>y_#30I`C9`9 zPW*ly+m4rWjuJ7Xj%o^-8S&Wr_mf{N=+(9nNfY0YzrPyWolg7KwHLWL$QgDsm$Yhu zde7Z#xos+Io_vZ6J5A-MZPeK2Mol3zJq|m6AG?bzdS#r#A9fiZ&6&hQYfXAwcPo>| zokvD14=#?dYU*7qIYE_FG*v*IVaTpi`a*aIFy$m=uwAnwk+b_~o)~{Io(4?3t$iqU z{=aF|QI^#)CXLb6;^^L|CmwST@4SByqY=B%fq(JtUO_^yi|OvF7aEpPlij#`ap$z_ z$?|`~Q;)6YfEnxW$HdXSPY>MIUfy`~D*~sm7iAmC`=A)_6?gL3_^asKHJx@TNu(sX zafGwJhOVKp>$3q{O(r!(#X4L-q1DK_wbrDiYKU^3xU6itb~&-7C2FTU@7dr+ilbD= zh_=K_Z(u}w*t_gW7JOD#v;3x36J=X@e$EQ=)lU54=DzqEi$r1J>@u3^W6sCk{vxxM z>>(KAZx6P(^m?w%P&kw~hdFBt(NAJ)2Sez9FDVg44`G@X!V(w-oA|UqG@P*6r%*?= zJ$-toVjo(|s!f$OS!DgDq|?8Ddz8cP^3vKOLZ*dPuL*G)=RFJD$g*0N)sgLy)|oL6 zPvGmj=d$enacu0>kG?tWNOGD~6z=BZb(=Y;w`IsZ4>3B62A2DLe`nj4Mdo$)F?ri~ zW-j{+3oe+$?|$TMCi9PHo*Ub7E@P+P!Pj>^!`puv&u6Q9)2~Md+ISrp)uj|3*hk>P ziA*1niQ1+I7iFj{d~Xr|$Qi}JOcxdT8+dui2b3jgVIMZ8j*VT9LeBNKbJf59$Q!e! zP&)J{^v>`ijuo+chs|YE$I{;o9e@6NZu$5RESfW!iCcznSx##_mY?E+U2NKx$f&0$ z(AU}YWwjJXDUA{Cl!cOZHIFUK;q8}S;kA#pux9CdRGSWbDe0U$;8%>h@j3>kDd>hc z;achs{Fa%-(0lG?%WtQ#^6`1}oI9CIla$!x#x~K?ui^0*b9iIvE38?!mG@SDjNep= z^R*(aa}Ne**L-cyRi`U_13B=PU5|892f+v15vf^asQq#$?kSOnpX4Lhh59YQ(9l$+<{64@BP!{;`2f`QQYHj0MB<-+N7pdJ4*qp+|{ose#x z;=E^;8;8Te6S;GXWacPy$0+HViJ_Yq`f1u*0jetljT|dPuqq_E`Col2Lg@aQxyL#% zk5Q%9Hmjmx_gWbK3XBtuYhVP86MtW?kEU*7>L=@7W9L0PkPl=eBqSsnfXp0Ys?3%? zklaW}ag=CYWwqo+x**CR%W4S;2??1w#_%Yps#umlRpodXDF`7@RZVi!Ot_Ka7z0og zg?P7zpuY-_FF`saLJ0b+hDVeg;5`>U@dPOQTDf_mB z7D>sVC@A43xrsh*q&UWqK&py@+na#Lo50>Lzo4SL6w9)tbD98_Wl>RH%HA)(z~f86 z?M*N6Iu)nOMaefs96p#YgRlvpX&PQ%B5CcjNNm{(O;b-O zt0gzl$&D1p7%8ofPxdFaOeUd462i9S;<~0GAe@b;Xqt+us^6E?$ICa#O;mCt#WBVN zQc)Cyt)hkByOwAI-+vc`$_c_KxrttG8~~3$ky|8PB_Sap@jdb&|Nj7hfPnY+_hx2h-QC^p?(V3ls2Lb9_y7P097#k$RA}Dqn@exwHV}X{ zhsE|dBe-yTl>b7IoNFL|ASkl!adX-BS{U10_Yxq-_SD_K&|g_Th7=`>B1Lg_8%K0v z!!LcF7l*?~L(;AJ3WS4fyA(p_Ri>W1)-JwOj8M(mhHyi4k|jJ4?L`SU+LKO_ga=L73leT@IGoN2DnwMniLaR;$|l@% zJ0nm77$GRe;2~w{iHOq#j9tAJ!F(A2QT;1nfb2h7-RXDrxB>M&0G1QfB{cxdJMLx) z_H}cQvocKfe4d~*rLLlGXkKuStR95^K2G2q{bz!pok#tekf44QxcfQ6&sIq{Ng0fH z1q7o(zAqveUm6l{NZ|L%2|6AEE%7as!CShJpk1*iSZ`I%8`uF26sed{t6$m(|91k7 zx*M!b;QY{EzdB)*AF#TXTz=rh06LsaE}*&u47v-ODQ2@lS>Ngt19C2qAlSr!oC_of zHZdUQ0<7sv&?g0S*`gkS^TXKE6$zXlTw>6v!R7+odcKXYDnCGd!YDro5Jvd{Wb%U@ z58DI*BtdBU@#NCPkWE1D_8KTlq>AvXoqu>YO#rK@RfKo{nie6jxkU&rKe)<4fS}_6 z8=oKq2)2~%y;Xovt6!2Q{1T~lxBQURYY<5jnkuUu^8^ql1Qi2?^Gr1fFupBbDN4%F z^yA@ho~f#qMg$_Ntfg}q>iQBivIKIoM{H#`&r~(0UB#fi)z32t^-Cz*^A!%3mdH@t z4Zf!Q0L7#4-25O#fXOunnf#z5ttqM?vh#zRbf_aMZV3d#?xrG8~<1%;qH^h=jOxS0B3Fpex4RHnyE z!UPyzP-K3|%qcwmB` zXsRF6JaI)-U50IL=2guv!J0*;?ED}}fOQLHnf;PJ?_f7=N)gP4M<`#A%aFlLnw0A* zk6IOYrrj1@eb`~^`8LAZ{LqN!hnKzn0DLB{Z0}Nj zesHtXCDX2%eQQ&?`j909pNRuL1|cCs;|Ww^hu6zb*BOQD?x%0;v`AXLhP?Z3D;VL5#g~``)IuNZa?MNF4dMDU zCe#Bw{xKbKRIyF=d{XTK32#$2{A(@YP3rR-!4PJ{0z*PFC}M=?czYTUet4d5cQL}} z_;>CHgdgVtPf@~`_;*Hx&$ER4IN|S@UcVm^zRX%-ju7*TGs4_h1UAA8BIscy_H!uH zgo&e)kV_!D$_}$Km@_e@1oQKQ3Cn51L=lin;7Szc?SO8WZcA|FZBpZkR(cg@rSkyRE;1$nu!+=Bf>w4k~c<}`QFDNAyx9m2yGPiqe7X0{6b.cidmaster); + int i = add_encoding_slots_dialog(gw, fv->b.cidmaster); if(i == 0) { /* User canceled the action. */ return; diff --git a/fontforgeexe/gtk/CMakeLists.txt b/fontforgeexe/gtk/CMakeLists.txt index 19d88e515a..5a8cbf1c6e 100644 --- a/fontforgeexe/gtk/CMakeLists.txt +++ b/fontforgeexe/gtk/CMakeLists.txt @@ -7,6 +7,8 @@ add_library(ffgtk OBJECT css_builder.cpp dialog.hpp dialog.cpp + language_list.hpp + language_list.cpp simple_dialogs.hpp simple_dialogs.cpp utils.hpp diff --git a/fontforgeexe/gtk/css_builder.cpp b/fontforgeexe/gtk/css_builder.cpp index 1704e2648e..98db836648 100644 --- a/fontforgeexe/gtk/css_builder.cpp +++ b/fontforgeexe/gtk/css_builder.cpp @@ -197,11 +197,25 @@ std::map collect_css_properties_disabled( return evaluate_css_properties(css_property_map, box_resource, false); } +std::map collect_css_properties_selected( + const GBox& box_resource) { + static const std::vector> + css_property_map = { + {"color", color_property<&GBox::main_foreground>}, + {"background-color", color_property<&GBox::active_border>}, + }; + + return evaluate_css_properties(css_property_map, box_resource, true); +} + std::map collect_css_properties_main( const struct resed* ri_extras) { std::map collection; for (const struct resed* rec = ri_extras; rec && rec->resname; ++rec) { + if (std::strcmp(rec->resname, "Foreground") == 0) { + collection["color"] = css_color(*(Color*)(rec->val)); + } if (std::strcmp(rec->resname, "Background") == 0) { collection["background-color"] = css_color(*(Color*)(rec->val)); } @@ -210,6 +224,17 @@ std::map collect_css_properties_main( return collection; } +std::map collect_css_properties_color( + const GBox& box_resource) { + static const std::vector> + css_property_map = { + {"color", color_property<&GBox::main_foreground>}, + {"background-color", color_property<&GBox::main_background>}, + }; + + return evaluate_css_properties(css_property_map, box_resource, true); +} + std::string build_style(const std::string& selector, const std::map& properties) { std::string property_list; @@ -241,28 +266,59 @@ std::string build_styles(const GResInfo* gdraw_ri) { }; static const std::map css_selector_map = { - {"", {"box", {}}}, - {"GLabel", {"label", {"button"}}}, + {"", {"box", {"tooltip"}}}, + {"GLabel", {"label", {"button", "tooltip"}}}, {"GButton", {"button", {"spinbutton"}}}, {"GDefaultButton", {"button#ok", {}}}, {"GCancelButton", {"button#cancel", {}}}, {"GNumericField", {"spinbutton", {}}}, {"GNumericFieldSpinner", {"spinbutton button", {}}}, + {"GTextField", {"entry", {"spinbutton"}}}, + {"GGadget.Popup", {"tooltip", {}}}, + }; + + // Some GTK widgets have substantially different structure from their GDraw + // analogs. For them we collect only color properties. + static const std::map css_selector_map_color = { + {"GList", "treeview"}, }; std::string styles; for (const GResInfo* ri = gdraw_ri; ri->next != NULL; ri = ri->next) { + auto sel_color_it = css_selector_map_color.find(ri->resname); + if (sel_color_it != css_selector_map_color.end()) { + auto props_color = collect_css_properties_color(*(ri->boxdata)); + styles += build_style(sel_color_it->second, props_color); + + auto props_selected = + collect_css_properties_selected(*(ri->boxdata)); + styles += + build_style(sel_color_it->second + ":selected", props_selected); + + continue; + } + auto sel_it = css_selector_map.find(ri->resname); if (sel_it == css_selector_map.end()) { continue; } const Selector& selector = sel_it->second; - if ((std::strcmp(ri->resname, "") == 0) && ri->extras) { + if ((std::strcmp(ri->resname, "") == 0 || + std::strcmp(ri->resname, "GGadget.Popup") == 0) && + ri->extras) { // Collect some default properties from the base class. auto props_main = collect_css_properties_main(ri->extras); styles += build_style(selector.node_name, props_main); + + // When the node is inside predefined containers, we shall unset the + // affected properties + for (const std::string& container : selector.excluded_containers) { + styles += build_unset_style( + container + " " + selector.node_name, props_main); + } + continue; } diff --git a/fontforgeexe/gtk/dialog.cpp b/fontforgeexe/gtk/dialog.cpp index 35f86649b2..86a214c31c 100644 --- a/fontforgeexe/gtk/dialog.cpp +++ b/fontforgeexe/gtk/dialog.cpp @@ -27,14 +27,41 @@ #include "dialog.hpp" +extern "C" { +#include "gutils.h" +} + #include "intl.h" +#include "gimage.h" #include "utils.hpp" +typedef struct gevent GEvent; + namespace ff::dlg { -Dialog::Dialog() { - parent_window = gtk_get_topmost_window(); +static GdkWindow* get_toplevel_gdk_window(GWindow gwin) { + // Redeclare GWindow to avoid dependency on the legacy headers. + struct ggdkwindow_local { /* :GWindow */ + // Inherit GWindow start + void* ggc; + void* display; + int (*eh)(GWindow, GEvent*); + GRect pos; + struct ggdkwindow_local* parent; + void* user_data; + void* widget_data; + GdkWindow* w; + }; + if (gwin) { + GdkWindow* gdk_win = ((ggdkwindow_local*)gwin)->w; + return gdk_window_get_effective_toplevel(gdk_win); + } else { + return nullptr; + } +} + +Dialog::Dialog(GWindow parent_gwin) : parent_gwindow_(parent_gwin) { auto ok_button = add_button(_("_OK"), Gtk::RESPONSE_OK); ok_button->set_name("ok"); @@ -42,15 +69,49 @@ Dialog::Dialog() { cancel_button->set_name("cancel"); set_default_response(Gtk::RESPONSE_OK); + set_position(Gtk::WIN_POS_CENTER); +} + +Dialog::~Dialog() { + if (parent_gwindow_) { + // Unblock the parent GDraw window + GdkWindow* parent_gdk_window = get_toplevel_gdk_window(parent_gwindow_); + g_object_set_data(G_OBJECT(parent_gdk_window), "GTKModalBlock", NULL); + } } Gtk::ResponseType Dialog::run() { - if (parent_window) { - Glib::RefPtr win = get_window(); - win->set_transient_for(parent_window); + if (parent_gwindow_) { + GdkWindow* parent_gdk_window = get_toplevel_gdk_window(parent_gwindow_); + GdkWindow* this_window = get_window().get()->gobj(); + gdk_window_set_transient_for(this_window, parent_gdk_window); + + // Mark the parent window as blocked, this would cause GDraw to discard + // its events. + g_object_set_data(G_OBJECT(parent_gdk_window), "GTKModalBlock", + (gpointer) true); } return (Gtk::ResponseType)Gtk::Dialog::run(); } +void Dialog::set_help_context(const std::string& file, + const std::string& section) { + help_file_ = file; + help_section_ = section; + + if (!help_file_.empty()) { + signal_key_press_event().connect( + sigc::mem_fun(*this, &Dialog::on_help_key_press), false); + } +} + +bool Dialog::on_help_key_press(GdkEventKey* event) { + if (event->keyval == GDK_KEY_F1 || event->keyval == GDK_KEY_Help) { + help(help_file_.c_str(), help_section_.c_str()); + return true; + } + return false; // let other keys be processed normally +} + } // namespace ff::dlg diff --git a/fontforgeexe/gtk/dialog.hpp b/fontforgeexe/gtk/dialog.hpp index 978dce021a..bc310e4841 100644 --- a/fontforgeexe/gtk/dialog.hpp +++ b/fontforgeexe/gtk/dialog.hpp @@ -28,16 +28,30 @@ #include +typedef struct gwindow* GWindow; + namespace ff::dlg { +// Modal dialog class Dialog : public Gtk::Dialog { public: - Dialog(); + // The parent is a legacy GDraw window. + // TODO(iorsh): remove this constructor after the transition to GTK is + // complete. + Dialog(GWindow parent_gwin); + ~Dialog(); Gtk::ResponseType run(); + // Add Help context to be opened if the user presses "F1". + void set_help_context(const std::string& file, const std::string& section); + private: - Glib::RefPtr parent_window; + GWindow parent_gwindow_ = nullptr; + + std::string help_file_, help_section_; + + bool on_help_key_press(GdkEventKey* event); }; } // namespace ff::dlg diff --git a/fontforgeexe/gtk/language_list.cpp b/fontforgeexe/gtk/language_list.cpp new file mode 100644 index 0000000000..ca9bb71edf --- /dev/null +++ b/fontforgeexe/gtk/language_list.cpp @@ -0,0 +1,119 @@ +/* Copyright (C) 2025 by Maxim Iorsh + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "language_list.hpp" + +#include "utils.hpp" +#include "intl.h" + +namespace ff::dlg { + +LanguageListDlg::LanguageListDlg(GWindow parent, + const ff::dlg::LanguageRecords& lang_recs, + const std::vector& initial_selection) + : Dialog(parent), list_(1, false, Gtk::SELECTION_MULTIPLE) { + set_title(_("Language List")); + set_help_context("ui/dialogs/lookups.html", "#lookups-scripts-dlg"); + + auto scrolled_window = Gtk::make_managed(); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + auto search_entry = Gtk::make_managed(); + // The default behavior resets the current selection and selects the + // matching row. Since we have a multiple-selection list, we want to just + // scroll to the matching row. + search_entry->signal_search_changed().connect([search_entry, this]() { + Glib::ustring search_text = search_entry->get_text(); + // Look for matching row, assuming the rows are sorted + // alphabetically. + int row_idx = 0; + for (; row_idx < list_.size(); ++row_idx) { + if (search_text.lowercase() < list_.get_text(row_idx).lowercase()) + break; + } + list_.scroll_to_row(Gtk::TreePath(std::to_string(row_idx)), 0.0); + }); + + for (const auto& [name, tag] : lang_recs) { + list_.append(name); + } + list_.set_headers_visible(false); + list_.set_enable_search(false); + list_.set_tooltip_text( + _("Select as many languages as needed\n" + "Hold down the control key when clicking\n" + "to make disjoint selections.")); + + // Set initial selection + for (int idx : initial_selection) { + list_.get_selection()->select(Gtk::TreePath(std::to_string(idx))); + } + + // Show initially roughly 8 rows. + property_default_height() = 40 * ui_font_eX_size(); + + scrolled_window->add(list_); + + get_content_area()->pack_start(*search_entry, Gtk::PACK_SHRINK); + get_content_area()->pack_start(*scrolled_window, Gtk::PACK_EXPAND_WIDGET); + + // Connect this signal before the normal Ok/Cancel processing slot, to + // prevent dialog closing if we wish. + signal_response().connect( + [this](int response_id) { + if (response_id == Gtk::RESPONSE_OK) { + if (list_.get_selected().empty()) { + gtk_post_error( + _("Language Missing"), + _("You must select at least one language.\nUse the " + "\"Default\" language if nothing else fits.")); + signal_response().emission_stop(); + } + } + }, + false); + + signal_realize().connect([this]() { list_.grab_focus(); }); + + show_all(); +} + +std::vector LanguageListDlg::show( + GWindow parent, const ff::dlg::LanguageRecords& lang_recs, + const std::vector& initial_selection) { + LanguageListDlg dialog(parent, lang_recs, initial_selection); + + Gtk::ResponseType result = dialog.run(); + std::vector selection; + + if (result == Gtk::RESPONSE_OK) { + selection = dialog.get_selection(); + } + return selection; +} + +} // namespace ff::dlg diff --git a/fontforgeexe/gtk/language_list.hpp b/fontforgeexe/gtk/language_list.hpp new file mode 100644 index 0000000000..46939c2570 --- /dev/null +++ b/fontforgeexe/gtk/language_list.hpp @@ -0,0 +1,53 @@ +/* Copyright (C) 2025 by Maxim Iorsh + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include "dialog.hpp" +#include "tag.hpp" + +namespace ff::dlg { + +using LanguageRecords = std::vector>; + +class LanguageListDlg final : public Dialog { + private: + Gtk::ListViewText list_; + + LanguageListDlg(GWindow parent, const ff::dlg::LanguageRecords& lang_recs, + const std::vector& initial_selection); + + std::vector get_selection() { return list_.get_selected(); } + + public: + // Show the dialog and return indexes of selected rows (or empty vector if + // the dialog was cancelled/closed). + static std::vector show(GWindow parent, + const ff::dlg::LanguageRecords& lang_recs, + const std::vector& initial_selection); +}; + +} // namespace ff::dlg diff --git a/fontforgeexe/gtk/simple_dialogs.cpp b/fontforgeexe/gtk/simple_dialogs.cpp index 655b92e5e2..94f96b59d0 100644 --- a/fontforgeexe/gtk/simple_dialogs.cpp +++ b/fontforgeexe/gtk/simple_dialogs.cpp @@ -1,35 +1,52 @@ -/* Copyright 2023 Joey Sabey +/* Copyright (C) 2025 by Maxim Iorsh * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "simple_dialogs.hpp" +#include #include #include #include "intl.h" #include "application.hpp" #include "dialog.hpp" +#include "language_list.hpp" +#include "utils.hpp" namespace ff::dlg { // A simple dialog to query the user for a number of new encoding slots to add. -class NumericalInputDialog final : public ff::dlg::Dialog { +class NumericalInputDialog final : public Dialog { private: Gtk::SpinButton* input; - NumericalInputDialog(const std::string& title, const std::string& label) { + NumericalInputDialog(GWindow parent, const std::string& title, + const std::string& label) + : Dialog(parent) { set_title(title); set_resizable(false); @@ -50,8 +67,9 @@ class NumericalInputDialog final : public ff::dlg::Dialog { public: // Show the dialog and return either the entered integer, 0 if the dialog // was cancelled/closed, or -1 if something strange happened - static int show(const std::string& title, const std::string& label) { - NumericalInputDialog dialog(title, label); + static int show(GWindow parent, const std::string& title, + const std::string& label) { + NumericalInputDialog dialog(parent, title, label); int i = -1; dialog.signal_response().connect([&](int response_id) { // Ok/Add returns the entered integer @@ -76,17 +94,75 @@ class NumericalInputDialog final : public ff::dlg::Dialog { } // namespace ff::dlg // Shim for the C code to call the dialog -int add_encoding_slots_dialog(bool cid) { +int add_encoding_slots_dialog(GWindow parent, bool cid) { // To avoid instability, the GTK application is lazily initialized only when // a GTK window is invoked. ff::app::GtkApp(); return ff::dlg::NumericalInputDialog::show( - _("Add Encoding Slots..."), + parent, _("Add Encoding Slots..."), cid ? _("How many CID slots do you wish to add?") : _("How many unencoded glyph slots do you wish to add?")); } +char* language_list_dialog(GWindow parent, const LanguageRec* languages, + const char* initial_tags) { + // To avoid instability, the GTK application is lazily initialized only when + // a GTK window is invoked. + ff::app::GtkApp(); + + ff::dlg::LanguageRecords language_vec; + for (const LanguageRec* lang = languages; lang->name != nullptr; ++lang) { + language_vec.emplace_back(lang->name, lang->tag); + } + + // Compute initial selection + std::stringstream ss(initial_tags); + std::vector tag_list; + std::vector unrecognized_tags; + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + auto it = std::find_if( + language_vec.begin(), language_vec.end(), + [&substr](const auto& rec) { return (rec.second == substr); }); + if (it != language_vec.end()) { + tag_list.push_back(it - language_vec.begin()); + } else { + unrecognized_tags.push_back(substr); + } + } + + if (unrecognized_tags.size() == 1) + gtk_post_error(_("Unknown Language"), + _("The language, '%s', is not in the list of known " + "languages and will be omitted"), + unrecognized_tags[0].c_str()); + else if (unrecognized_tags.size() > 1) + gtk_post_error(_("Unknown Language"), + _("Several language tags, including '%s', are not in " + "the list of known languages and will be omitted"), + unrecognized_tags[0].c_str()); + + std::vector selection = + ff::dlg::LanguageListDlg::show(parent, language_vec, tag_list); + + if (selection.empty()) { + return nullptr; + } else { + auto append_tag = [&language_vec](std::string a, int b) { + return std::move(a) + ',' + (const char*)language_vec[b].second; + }; + + // Comma-seperated tags + std::string s((const char*)language_vec[selection[0]].second); + s = std::accumulate(std::next(selection.begin()), selection.end(), + s, // start with first element + append_tag); + return strdup(s.c_str()); + } +} + void update_appearance() { ff::app::GtkApp(); ff::app::load_legacy_style(); diff --git a/fontforgeexe/gtk/simple_dialogs.hpp b/fontforgeexe/gtk/simple_dialogs.hpp index d219554d4a..295d57c8ce 100644 --- a/fontforgeexe/gtk/simple_dialogs.hpp +++ b/fontforgeexe/gtk/simple_dialogs.hpp @@ -1,16 +1,28 @@ -/* Copyright 2023 Joey Sabey +/* Copyright (C) 2025 by Maxim Iorsh * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once @@ -20,7 +32,19 @@ extern "C" { #include "gresource.h" -int add_encoding_slots_dialog(bool cid); +typedef struct gwindow* GWindow; + +typedef struct { + const char* name; + uint32_t tag; +} LanguageRec; + +int add_encoding_slots_dialog(GWindow parent, bool cid); + +// Return comma-separated list of language tags, or NULL if the action was +// canceled. The caller is responsible to release the returned pointer. +char* language_list_dialog(GWindow parent, const LanguageRec* languages, + const char* initial_tags); void update_appearance(); diff --git a/fontforgeexe/gtk/utils.cpp b/fontforgeexe/gtk/utils.cpp index ec77b8364f..481eda2bd0 100644 --- a/fontforgeexe/gtk/utils.cpp +++ b/fontforgeexe/gtk/utils.cpp @@ -27,25 +27,57 @@ #include "utils.hpp" -Glib::RefPtr gtk_get_topmost_window() { - Glib::RefPtr screen = Gdk::Screen::get_default(); +#include +#include - Glib::RefPtr topmost_window = screen->get_active_window(); - if (topmost_window) { - return topmost_window; - } +static Cairo::TextExtents ui_font_extents(const std::string& sample_text) { + Cairo::RefPtr srf = + Cairo::ImageSurface::create(Cairo::Format::FORMAT_RGB24, 100, 100); + Cairo::RefPtr cairo_context = Cairo::Context::create(srf); + Glib::RefPtr style_context = Gtk::StyleContext::create(); + + Pango::FontDescription font = style_context->get_font(); + cairo_context->select_font_face(font.get_family(), + Cairo::FontSlant::FONT_SLANT_NORMAL, + Cairo::FontWeight::FONT_WEIGHT_NORMAL); + cairo_context->set_font_size(font.get_size() / PANGO_SCALE * + Gdk::Screen::get_default()->get_resolution() / + 96); + + Cairo::TextExtents extents; + cairo_context->get_text_extents("m", extents); + return extents; +} + +double ui_font_em_size() { + Cairo::TextExtents extents = ui_font_extents("m"); + return extents.x_advance; +} + +double ui_font_eX_size() { + Cairo::TextExtents extents = ui_font_extents("X"); + return extents.height; +} + +void gtk_post_error(const char* title, const char* statement, ...) { + va_list ap; + va_start(ap, statement); - // Gdk::Screen::get_active_window() is not always reliable, e.g when the - // active window was just closed, and its parent hasn't been activated back - // yet. - std::vector> stack = screen->get_window_stack(); - for (auto rit = stack.rbegin(); rit != stack.rend(); ++rit) { - if ((*rit)->get_window_type() == Gdk::WINDOW_TOPLEVEL && - (*rit)->is_visible()) { - topmost_window = *rit; - break; - } + // Format error statement + gchar* result_string = NULL; + gint chars_written = g_vasprintf(&result_string, statement, ap); + if (chars_written >= 0 && result_string != NULL) { + // Passing use_markup=false causes GTK to embolden the text, and we + // don't want it. + Gtk::MessageDialog message_dlg(result_string, true, Gtk::MESSAGE_ERROR, + Gtk::BUTTONS_OK, true); + message_dlg.set_title(title); + message_dlg.run(); + g_free(result_string); + } else { + std::cerr << "Error formatting statement \"" << statement << "\"" + << std::endl; } - return topmost_window; + va_end(ap); } diff --git a/fontforgeexe/gtk/utils.hpp b/fontforgeexe/gtk/utils.hpp index 7b7f7dbd9f..dd0bcba9b9 100644 --- a/fontforgeexe/gtk/utils.hpp +++ b/fontforgeexe/gtk/utils.hpp @@ -28,5 +28,8 @@ #include -// Get the current topmost window -Glib::RefPtr gtk_get_topmost_window(); +double ui_font_em_size(); +double ui_font_eX_size(); + +// TODO(iorsh): Integrate this function into the global log collection +void gtk_post_error(const char* title, const char* statement, ...); diff --git a/fontforgeexe/lookupui.c b/fontforgeexe/lookupui.c index 17eaddb5fb..d64dd56060 100644 --- a/fontforgeexe/lookupui.c +++ b/fontforgeexe/lookupui.c @@ -44,6 +44,7 @@ #include "ttf.h" #include "ustring.h" #include "utype.h" +#include "gtk/simple_dialogs.hpp" #include #include @@ -1063,13 +1064,13 @@ GTextInfo languages[] = { GTEXTINFO_EMPTY }; -static char *LK_LangsDlg(GGadget *, int r, int c); +static char *LK_GTKLangsDlg(GGadget *, int r, int c); static char *LK_ScriptsDlg(GGadget *, int r, int c); static struct col_init scriptci[] = { /* GT: English uses "script" to mean a general writing system (latin, greek, kanji) */ /* GT: and the cursive handwriting style. Here we mean the general writing system. */ { me_stringchoicetag , NULL, scripts, NULL, N_("writing system|Script") }, - { me_funcedit, LK_LangsDlg, NULL, NULL, N_("Language(s)") }, + { me_funcedit, LK_GTKLangsDlg, NULL, NULL, N_("Language(s)") }, COL_INIT_EMPTY }; static struct col_init featureci[] = { @@ -1078,6 +1079,15 @@ static struct col_init featureci[] = { COL_INIT_EMPTY }; +int cmp_item_by_name(const void *a, const void *b) { + /* TODO: strcmp() doesn't consider linguistic comparison, e.g. putting + accented and non-accented character together. Consider using + g_utf8_collate() with an appropriate locale. */ + int res = strcmp((const char *)((const GTextInfo *)a)->text, + (const char *)((const GTextInfo *)b)->text); + return res; +} + void LookupUIInit(void) { static int done = false; int i, j; @@ -1094,6 +1104,13 @@ return; if ( needswork[j][i].text!=NULL ) needswork[j][i].text = (unichar_t *) S_((char *) needswork[j][i].text); } + + /* Sort scripts and languages by localized name */ + size_t n_scripts = sizeof(scripts) / sizeof(scripts[0]) - 1; + qsort(scripts, n_scripts, sizeof(GTextInfo), cmp_item_by_name); + size_t n_languages = sizeof(languages) / sizeof(languages[0]) - 1; + qsort(languages, n_languages, sizeof(GTextInfo), cmp_item_by_name); + LookupInit(); featureci[0].title = S_(featureci[0].title); @@ -1128,181 +1145,18 @@ struct lookup_dlg { char *scriptret; }; -static int langs_e_h(GWindow gw, GEvent *event) { - int *done = GDrawGetUserData(gw); - - if ( event->type==et_close ) { - *done = true; - } else if ( event->type==et_char ) { - if ( event->u.chr.keysym == GK_F1 || event->u.chr.keysym == GK_Help ) { - help("ui/dialogs/lookups.html", "#lookups-scripts-dlg"); -return( true ); - } -return( false ); - } else if ( event->type==et_controlevent && event->u.control.subtype == et_buttonactivate ) { - switch ( GGadgetGetCid(event->u.control.g)) { - case CID_OK: - *done = 2; - break; - case CID_Cancel: - *done = true; - break; - } - } -return( true ); -} - -static char *LK_LangsDlg(GGadget *g, int r, int c) { - int rows, i; +static char *LK_GTKLangsDlg(GGadget *g, int r, int c) { + LanguageRec lang_recs[sizeof(languages) / sizeof(languages[0])]; + int rows; struct matrix_data *strings = GMatrixEditGet(g, &rows); - char *langstr = strings[2*r+c].u.md_str, *pt, *start; - unsigned char tagstr[4], warnstr[8]; - uint32_t tag; - int warn_cnt = 0; - int j,done=0; - GWindowAttrs wattrs; - GGadgetCreateData gcd[5], *varray[6], *harray[7], boxes[3]; - GTextInfo label[5]; - GRect pos; - GWindow gw; - int32_t len; - GTextInfo **ti; - char *ret; - - for ( i=0; languages[i].text!=NULL; ++i ) - languages[i].selected = false; + char *langstr = strings[2*r+c].u.md_str; - for ( start= langstr; *start; ) { - memset(tagstr,' ',sizeof(tagstr)); - for ( pt=start, j=0; *pt!='\0' && *pt!=','; ++pt, ++j ) { - if ( j<4 ) - tagstr[j] = *pt; - } - if ( *pt==',' ) ++pt; - tag = (tagstr[0]<<24) | (tagstr[1]<<16) | (tagstr[2]<<8) | tagstr[3]; - for ( i=0; languages[i].text!=NULL; ++i ) - if ( languages[i].userdata == (void *) (intptr_t) tag ) { - languages[i].selected = true; - break; - } - if ( languages[i].text==NULL ) { - ++warn_cnt; - memcpy(warnstr,tagstr,4); - warnstr[4] = '\0'; - } - start = pt; + for (int i=0; i < sizeof(languages) / sizeof(languages[0]); ++i ) { + lang_recs[i].name = (const char*)languages[i].text; + lang_recs[i].tag = (uint32_t)(intptr_t)languages[i].userdata; } - if ( warn_cnt!=0 ) { - if ( warn_cnt==1 ) - ff_post_error(_("Unknown Language"),_("The language, '%s', is not in the list of known languages and will be omitted"), warnstr ); - else - ff_post_error(_("Unknown Language"),_("Several language tags, including '%s', are not in the list of known languages and will be omitted"), warnstr ); - } - - memset(&wattrs,0,sizeof(wattrs)); - wattrs.mask = wam_events|wam_cursor|wam_utf8_wtitle|wam_undercursor|wam_isdlg|wam_restrict; - wattrs.event_masks = ~(1<selected ) - ++lcnt; - } - if ( lcnt==0 ) { - ff_post_error(_("Language Missing"),_("You must select at least one language.\nUse the \"Default\" language if nothing else fits.")); - done = 0; - goto retry; - } - ret = malloc(5*lcnt+1); - *ret = '\0'; - pt = ret; - for ( i=0; iselected ) { - uint32_t tag = (uint32_t) (intptr_t) (ti[i]->userdata); - *pt++ = tag>>24; - *pt++ = tag>>16; - *pt++ = tag>>8; - *pt++ = tag&0xff; - *pt++ = ','; - } - } - if ( pt!=ret ) - pt[-1] = '\0'; - } - GDrawDestroyWindow(gw); -return( ret ); + return language_list_dialog(GGadgetGetWindow(g), lang_recs, langstr); } static void LK_NewScript(GGadget *g,int row) { diff --git a/gdraw/ggdkdraw.c b/gdraw/ggdkdraw.c index ce83cb576d..b7afe07c37 100644 --- a/gdraw/ggdkdraw.c +++ b/gdraw/ggdkdraw.c @@ -807,6 +807,12 @@ static bool _GGDKDraw_FilterByModal(GdkEvent *event, GGDKWindow gw) { break; } + // Check if this window has an active GTK modal dialog over it + GdkWindow* top_gdk_win = gdk_window_get_effective_toplevel(gw->w); + if (g_object_get_data(G_OBJECT(top_gdk_win), "GTKModalBlock")) { + return true; + } + GGDKWindow gww = gw; GPtrArray *stack = gw->display->transient_stack; diff --git a/gutils/fsys.c b/gutils/fsys.c index 981a8f25c6..b77be1104a 100644 --- a/gutils/fsys.c +++ b/gutils/fsys.c @@ -768,9 +768,17 @@ const char *getShareDir(void) { } const char *getLocaleDir(void) { - static char *localedir=NULL; + static char *localedir = NULL; if (!localedir) { - localedir = smprintf("%s/share/locale", program_root); + if (devel_env) { +#if defined(__MINGW32__) + localedir = smprintf("%s/share/locale", program_root); +#else + localedir = smprintf("%s/po", program_root); +#endif + } else { + localedir = smprintf("%s/share/locale", program_root); + } } return localedir; } diff --git a/fontforgeexe/shapers/tag.hpp b/inc/tag.hpp similarity index 81% rename from fontforgeexe/shapers/tag.hpp rename to inc/tag.hpp index 6d0f8cc669..25a9fe456c 100644 --- a/fontforgeexe/shapers/tag.hpp +++ b/inc/tag.hpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace ff { @@ -41,6 +42,22 @@ class Tag { return (arr[0] << 24) | (arr[1] << 16) | (arr[2] << 8) | arr[3]; } + bool operator==(const char* rhs) const { + for (int i = 0; i < 5; ++i) { + // We shall usually continue here. + if (arr[i] == rhs[i]) continue; + + // If tags differ, it's either a real difference or a space vs zero + // terminator. + return ((arr[i] == ' ') && (rhs[i] == '\0')); + } + return true; + } + + bool operator==(const std::string& rhs) const { + return operator==(rhs.c_str()); + } + private: // OpenType tags normally have four characters, the fifth is the trailing // zero. @@ -49,8 +66,4 @@ class Tag { const Tag REQUIRED_FEATURE = Tag(" RQD"); -inline bool operator==(const Tag& tag1, const char tag2[5]) { - return std::memcmp((const char*)tag1, tag2, 4) == 0; -} - } // namespace ff diff --git a/po/CMakeLists.txt b/po/CMakeLists.txt index 11a60e7a05..77491e3433 100644 --- a/po/CMakeLists.txt +++ b/po/CMakeLists.txt @@ -31,7 +31,7 @@ file(STRINGS LINGUAS _langs) foreach(_lang ${_langs}) set(_input "${CMAKE_CURRENT_SOURCE_DIR}/${_lang}.po") - set(_output "${CMAKE_CURRENT_BINARY_DIR}/${_lang}.mo") + set(_output "${CMAKE_CURRENT_BINARY_DIR}/${_lang}/LC_MESSAGES/FontForge.mo") set(_full "${CMAKE_CURRENT_BINARY_DIR}/full/${_lang}.po") add_custom_command(OUTPUT "${_output}" COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" --check -o "${_output}" "${_input}" From d6c0701444bfbe1d438b635e5a5b11d85e4ed6ca Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Mon, 10 Nov 2025 21:02:23 +0100 Subject: [PATCH 15/96] FindMatchingHVEdge: report problematic glyph (#5087) (#5675) Co-authored-by: Steve White --- fontforge/stemdb.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fontforge/stemdb.c b/fontforge/stemdb.c index b0995dd6af..75d68e451b 100644 --- a/fontforge/stemdb.c +++ b/fontforge/stemdb.c @@ -811,7 +811,10 @@ return( 0 ); winding += nw; } if ( space[i]==NULL ) { - fprintf( stderr, "FindMatchinHVEdge didn't\n" ); + if ( gd->sc ) + fprintf( stderr, "FindMatchingHVEdge didn't: 0x%X, '%s'\n", gd->sc->unicodeenc, gd->sc->name ); + else + fprintf( stderr, "FindMatchingHVEdge didn't: unknown glyph\n" ); return( 0 ); } From f450725d623e587aef3e362f6e8a7dd069f6311e Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Tue, 11 Nov 2025 22:34:13 +0100 Subject: [PATCH 16/96] Document mark set parameter for font.addLookup() Python API (#5674) --- doc/sphinx/scripting/python/fontforge.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 9674917f7a..ddf98ceb86 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -4193,7 +4193,7 @@ This type may not be pickled. * ``kern_statemachine`` The flags argument is a tuple of strings, or ``None``. At most one of these - strings may be the name of a mark class. The others are: + strings may be the name of a mark class or a mark set. The others are: * ``right_to_left`` * ``ignore_bases`` From 6a0be00f947d4901023c2d95c94cfa0bf515c690 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 12 Nov 2025 22:43:23 +0200 Subject: [PATCH 17/96] Fix UI font on Windows for CJK locales (#5663) * Use custom alias 'windows-cjk-workaround' to allow user to utilize 'system-ui' independently. --- cmake/BuildUtils.cmake | 2 +- gdraw/gresedit.c | 16 +++++++++++---- gdraw/gresource.c | 45 ++++++++++++++++++++++++++++++++++++++++++ gutils/gwwintl.c | 31 +++++++++++++++++++++++++++++ inc/gresource.h | 4 ++++ inc/intl.h | 1 + 6 files changed, 94 insertions(+), 5 deletions(-) diff --git a/cmake/BuildUtils.cmake b/cmake/BuildUtils.cmake index 6c6ea3af33..e77b31f1e2 100644 --- a/cmake/BuildUtils.cmake +++ b/cmake/BuildUtils.cmake @@ -145,7 +145,7 @@ function(set_font_family type value) if(APPLE) set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans,Lucida Grande,sans" PARENT_SCOPE) elseif(WIN32 OR CYGWIN) - set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans,Calibri" PARENT_SCOPE) + set(FONTFORGE_UI_FAMILIES "windows-cjk-workaround,system-ui,Deja Vu Sans,Calibri" PARENT_SCOPE) else() set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans" PARENT_SCOPE) endif() diff --git a/gdraw/gresedit.c b/gdraw/gresedit.c index 34aa36603b..91780f04f2 100644 --- a/gdraw/gresedit.c +++ b/gdraw/gresedit.c @@ -2476,11 +2476,19 @@ int _GResEditInitialize(GResInfo *ri) { _GGadgetInitDefaultBox(ri->resname, ri->boxdata); } - if ( ri->font!=NULL ) - GResourceFindFont(ri->resname, "Font", ri->font); + if ( ri->font!=NULL ) { + fix_CJK_UI_font(ri->font); + GResourceFindFont(ri->resname, "Font", ri->font); + } - if ( ri->extras!=NULL ) - GResEditFind(ri->extras, ri->resname); + if ( ri->extras!=NULL ) { + for ( int i=0; ri->extras[i].name!=NULL; ++i ) { + if (ri->extras[i].type == rt_font) { + fix_CJK_UI_font(ri->extras[i].val); + } + } + GResEditFind(ri->extras, ri->resname); + } ri->is_initialized = true; return true; diff --git a/gdraw/gresource.c b/gdraw/gresource.c index 186dae56ff..dfb542c259 100644 --- a/gdraw/gresource.c +++ b/gdraw/gresource.c @@ -804,3 +804,48 @@ GImage *GResImageGetImage(GResImage *ri) { return ri->bucket->image; } + +void fix_CJK_UI_font(GResFont* font) { +#if defined(__MINGW32__) + /* On Windows systems the default UI font doesn't support CJK. We prepend it + with locale-specific font by replacing the predefined + "windows-cjk-workaround" alias. */ + char locale[100]; + char *ui_font = NULL; + + gettext_locale(locale); + if (strcmp(locale, "zh_CN") == 0) ui_font = "Microsoft YaHei UI"; + if (strcmp(locale, "zh_SG") == 0) ui_font = "Microsoft YaHei UI"; + if (strcmp(locale, "zh_TW") == 0) ui_font = "Microsoft JhengHei UI"; + if (strcmp(locale, "zh_HK") == 0) ui_font = "Microsoft JhengHei UI"; + if (strcmp(locale, "ja_JP") == 0) ui_font = "Meiryo UI,Yu Gothic UI"; + if (strcmp(locale, "ja") == 0) ui_font = "Meiryo UI,Yu Gothic UI"; + if (strcmp(locale, "ko_KR") == 0) ui_font = "Malgun Gothic"; + if (strcmp(locale, "ko") == 0) ui_font = "Malgun Gothic"; + + if (ui_font == NULL) { + return; + } + + char* pos = strstr(font->rstr, "windows-cjk-workaround"); + if (pos == NULL) { + return; + } + + size_t prefix_len = pos - font->rstr; + size_t suffix_len = strlen(pos) + strlen("windows-cjk-workaround"); /* Length after "windows-cjk-workaround" */ + size_t ui_font_len = strlen(ui_font); + char* result = malloc(prefix_len + ui_font_len + suffix_len + 1); + + /* Copy parts together */ + strncpy(result, font->rstr, prefix_len); /* Copy prefix */ + strcpy(result + prefix_len, ui_font); /* Insert replacement */ + strcpy(result + prefix_len + ui_font_len, pos + strlen("windows-cjk-workaround")); /* Copy suffix */ + + if (font->can_free_name) + free(font->rstr); + + font->rstr = result; + font->can_free_name = true; +#endif +} diff --git a/gutils/gwwintl.c b/gutils/gwwintl.c index 10e3403c41..abc956ff78 100644 --- a/gutils/gwwintl.c +++ b/gutils/gwwintl.c @@ -41,3 +41,34 @@ char *sgettext(const char *msgid) { msgval = found+1; return (char *) msgval; } + +char* gettext_locale(char* locale_out) { + const char *loc = getenv("LANGUAGE"); + + if (loc == NULL) loc = getenv("LC_ALL"); + if (loc == NULL) loc = getenv("LC_MESSAGES"); + if (loc == NULL) loc = getenv("LANG"); + + /* Fallback for empty environment */ + if (loc == NULL) { + strcpy(locale_out, "en"); + return locale_out; + } + + char* dot_position = strchr(loc, '.'); + char* colon_position = strchr(loc, ':'); + + // If no dot or colon is found, copy the entire string + if (dot_position == NULL && colon_position == NULL) { + strcpy(locale_out, loc); + return locale_out; + } + + size_t substring_length = (dot_position == NULL) ? (colon_position - loc) : + (colon_position == NULL) ? (dot_position - loc) : + ((colon_position < dot_position) ? (colon_position - loc) : (dot_position - loc)); + + strncpy(locale_out, loc, substring_length); + locale_out[substring_length] = '\0'; + return locale_out; +} diff --git a/inc/gresource.h b/inc/gresource.h index 5c0a08b7e3..c8c34da312 100644 --- a/inc/gresource.h +++ b/inc/gresource.h @@ -104,6 +104,10 @@ typedef struct gresfont { uint8_t can_free_name; } GResFont; +/* Locate and override the "windows-cjk-workaround" font alias, because the + native Pango alias "system-ui" doesn't support CJK scripts on Windows */ +void fix_CJK_UI_font(GResFont* font); + #define GRESIMAGE_INIT(defstr) { (defstr), NULL } #define GRESFONT_INIT(defstr) { NULL, (defstr), false } diff --git a/inc/intl.h b/inc/intl.h index ddcf95fdf7..544adca4f0 100644 --- a/inc/intl.h +++ b/inc/intl.h @@ -71,5 +71,6 @@ #define H_(str) (str) char *sgettext(const char *msgid); +char *gettext_locale(); #endif /* FONTFORGE_INTL_H */ From 74b7969ebb4b45bf860ff2b51c80049034c754af Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Thu, 13 Nov 2025 20:23:59 +0100 Subject: [PATCH 18/96] Python API: Add font.markSets. Add tests for font.markClasses and font.markSets. (#5673) --- doc/sphinx/scripting/python/fontforge.rst | 5 ++ fontforge/python.c | 103 +++++++++++++++++----- tests/CMakeLists.txt | 1 + tests/test_font_mark_classes_and_sets.py | 36 ++++++++ 4 files changed, 121 insertions(+), 24 deletions(-) create mode 100644 tests/test_font_mark_classes_and_sets.py diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index ddf98ceb86..69ce863b7b 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -3577,6 +3577,11 @@ This type may not be pickled. A tuple each entry of which is itself a tuple containing a mark-class-name and a tuple of glyph-names. +.. attribute:: font.markSets + + A tuple each entry of which is itself a tuple containing a mark-set-name + and a tuple of glyph-names. + .. attribute:: font.layer_cnt The number of layers in the font. (Read only. Can change using ``add`` diff --git a/fontforge/python.c b/fontforge/python.c index 71971f5521..9b1b4e05c9 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -14651,35 +14651,37 @@ return( -1 ); return( 0 ); } -static PyObject *PyFF_Font_get_mark_classes(PyFF_Font *self, void *UNUSED(closure)) { +static PyObject *PyFF_Font_get_mark_classes_or_sets(int start, int count, char **names, char **values) { PyObject *tuple, *nametuple; - SplineFont *sf; int i; + tuple = PyTuple_New(count-start); + for ( i=start; ifv->sf; if ( sf->mark_class_cnt==0 ) Py_RETURN_NONE; - tuple = PyTuple_New(sf->mark_class_cnt-1); - for ( i=1; imark_class_cnt; ++i ) { - nametuple = TupleOfGlyphNames(sf->mark_classes[i],0); - PyTuple_SetItem(tuple,i-1,Py_BuildValue("(sO)", sf->mark_class_names[i], nametuple)); - } -return( tuple ); + return( PyFF_Font_get_mark_classes_or_sets(1, sf->mark_class_cnt, sf->mark_class_names, sf->mark_classes) ); } -static int PyFF_Font_set_mark_classes(PyFF_Font *self,PyObject *value, void *UNUSED(closure)) { - SplineFont *sf; +static int PyFF_Font_parse_mark_classes_or_sets(PyObject *value, bool sets, char ***names_ptr, char ***values_ptr) { + int start = sets ? 0 : 1; int i, cnt; char **names, **classes; char *nm; PyObject *subtuple; - if ( CheckIfFontClosed(self) ) - return (-1); - sf = self->fv->sf; if ( value==NULL || value==Py_None ) { /* del font.markClasses or font.markClasses=None removes old values */ cnt = 0; @@ -14691,27 +14693,29 @@ static int PyFF_Font_set_mark_classes(PyFF_Font *self,PyObject *value, void *UNU cnt = PySequence_Size(value); if ( cnt==-1 ) return( -1 ); - if ( cnt>=256 ) { + if ( sets && cnt>=65536 ) { + PyErr_Format(PyExc_ValueError, "There may be at most 65535 mark sets" ); + return( -1 ); + } + if ( !sets && cnt>=256 ) { PyErr_Format(PyExc_ValueError, "There may be at most 255 mark classes" ); return( -1 ); } } if ( cnt==0 ) { - /* markClasses is being removed, delete any old, reset counts and pointers */ - MarkClassFree(sf->mark_class_cnt,sf->mark_classes,sf->mark_class_names); - sf->mark_class_cnt = 0; - sf->mark_classes = NULL; - sf->mark_class_names = NULL; + /* del font.markClasses or font.markClasses=None removes old values */ + *names_ptr = NULL; + *values_ptr = NULL; return( 0 ); } names = malloc((cnt+1)*sizeof(char *)); classes = malloc((cnt+1)*sizeof(char *)); names[0] = classes[0] = NULL; - /* Fill in names[] and classes[], starting at index 1 instead of 0 */ - for ( i=1; i<=cnt; ++i ) { + /* Fill in names[] and classes[], starting at index 1 instead of 0 in case of classes */ + for ( i=start; ifv->sf; + cnt = PyFF_Font_parse_mark_classes_or_sets(value, false, &names, &classes); + if ( cnt==-1 ) + return (-1); MarkClassFree(sf->mark_class_cnt,sf->mark_classes,sf->mark_class_names); - sf->mark_class_cnt = cnt+1; /* +1 because index 0 was skipped */ + sf->mark_class_cnt = cnt; sf->mark_classes = classes; sf->mark_class_names = names; return( 0 ); } +static PyObject *PyFF_Font_get_mark_sets(PyFF_Font *self, void *UNUSED(closure)) { + SplineFont *sf; + + if ( CheckIfFontClosed(self) ) + return (NULL); + sf = self->fv->sf; + if ( sf->mark_set_cnt==0 ) + Py_RETURN_NONE; + + return ( PyFF_Font_get_mark_classes_or_sets(0, sf->mark_set_cnt, sf->mark_set_names, sf->mark_sets) ); +} + +static int PyFF_Font_set_mark_sets(PyFF_Font *self,PyObject *value, void *UNUSED(closure)) { + SplineFont *sf; + int cnt; + char **names, **sets; + + if ( CheckIfFontClosed(self) ) + return (-1); + sf = self->fv->sf; + cnt = PyFF_Font_parse_mark_classes_or_sets(value, true, &names, &sets); + if ( cnt==-1 ) + return (-1); + + MarkSetFree(sf->mark_set_cnt,sf->mark_sets,sf->mark_set_names); + sf->mark_set_cnt = cnt; + sf->mark_sets = sets; + sf->mark_set_names = names; + + return( 0 ); +} + static PyObject *PyFF_Font_get_em(PyFF_Font *self, void *UNUSED(closure)) { if ( CheckIfFontClosed(self) ) return (NULL); @@ -15616,6 +15668,9 @@ static PyGetSetDef PyFF_Font_getset[] = { {(char *)"markClasses", (getter)PyFF_Font_get_mark_classes, (setter)PyFF_Font_set_mark_classes, (char *)"A tuple each entry of which is itself a tuple containing a mark-class-name and a tuple of glyph-names", NULL}, + {(char *)"markSets", + (getter)PyFF_Font_get_mark_sets, (setter)PyFF_Font_set_mark_sets, + (char *)"A tuple each entry of which is itself a tuple containing a mark-set-name and a tuple of glyph-names", NULL}, {(char *)"style_set_names", (getter)PyFF_Font_get_style_set_names, (setter)PyFF_Font_set_style_set_names, (char *)"A tuple, each entry of which is a 3-element tuple containing the language name (e.g. \"English (US)\"), the style set tag (e.g. \"ss01\") and the style set name.", NULL}, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4f18717a3d..18b1250c4a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -140,4 +140,5 @@ if(ENABLE_PYTHON_SCRIPTING_RESULT) add_py_test(test_py_invalid_objs.py "Ambrosia.sfd" "Accessing invalid objects") add_py_test(test_sfnt_name.py "Ambrosia.sfd" "Testing font.appendSFNTName() function") add_py_test(test_logging.py "Sanitization of logging functions") + add_py_test(test_font_mark_classes_and_sets.py "font.markClasses, font.markSets getters and setters") endif() diff --git a/tests/test_font_mark_classes_and_sets.py b/tests/test_font_mark_classes_and_sets.py new file mode 100644 index 0000000000..0c9f88ce91 --- /dev/null +++ b/tests/test_font_mark_classes_and_sets.py @@ -0,0 +1,36 @@ +"""Test getter and setter of font.markClasses and font.markSets""" +import fontforge + + +GLYPHS = ('A', 'B', 'C') +MARK_CLASSES = (('MarkClass-vowel', ('A',)), + ('MarkClass-consonant', ('B', 'C'))) +MARK_SETS = (('MarkSet-roman-numeral', ('C',)), + ('MarkSet-letter', ('A', 'B', 'C'))) + +font = fontforge.font() +for name in GLYPHS: + font.createMappedChar(name) + +assert font.markClasses is None +assert font.markSets is None + +font.markClasses = MARK_CLASSES +assert font.markClasses == MARK_CLASSES + +del font.markClasses +assert font.markClasses is None + +font.markClasses = () +assert font.markClasses is None + +font.markSets = MARK_SETS +assert font.markSets == MARK_SETS + +del font.markSets +assert font.markSets is None + +font.markSets = () +assert font.markSets is None + +font.close() From ce3693d8bb8c1f0d1c22641df3bf2d608bba7387 Mon Sep 17 00:00:00 2001 From: MihailJP Date: Sat, 15 Nov 2025 04:19:24 +0900 Subject: [PATCH 19/96] Fix flags parameter of font.generateTTC (#5677) --- fontforge/python.c | 2 +- fontforge/savefont.c | 125 +++++++++++++++++-------------------------- fontforge/savefont.h | 1 + 3 files changed, 52 insertions(+), 76 deletions(-) diff --git a/fontforge/python.c b/fontforge/python.c index 9b1b4e05c9..424bc3917e 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -18137,7 +18137,7 @@ return( NULL ); locfilename = utf82def_copy(filename); - if ( !WriteTTC(locfilename,head,ff_ttc,bf,iflags,layer,ittcflags)) { + if ( !WriteTTC(locfilename,head,ff_ttc,bf,fmflag2ttfflag(iflags,false),layer,ittcflags)) { PyErr_Format(PyExc_EnvironmentError, "Font generation failed"); /* Don't return here, will return after memory is freed below */ } diff --git a/fontforge/savefont.c b/fontforge/savefont.c index 55e472ce19..7bd2f662f9 100644 --- a/fontforge/savefont.c +++ b/fontforge/savefont.c @@ -1036,6 +1036,54 @@ static int32_t *AllBitmapSizes(SplineFont *sf) { return( sizes ); } +int fmflag2ttfflag(int fmflags, bool is_postscript_or_cff) { + int ttfflags = 0; + /* Applicable postscript flags */ + if ( !is_postscript_or_cff ) { + if ( fmflags&fm_flag_afm ) ttfflags |= ps_flag_afm; + if ( fmflags&fm_flag_pfm ) ttfflags |= ps_flag_pfm; + if ( fmflags&fm_flag_nohintsubs ) ttfflags |= ps_flag_nohintsubs; + if ( fmflags&fm_flag_noflex ) ttfflags |= ps_flag_noflex; + if ( fmflags&fm_flag_nopshints ) ttfflags |= ps_flag_nohints; + if ( fmflags&fm_flag_round ) ttfflags |= ps_flag_round; + if ( fmflags&fm_flag_afmwithmarks ) ttfflags |= ps_flag_afmwithmarks; + /* Applicable truetype flags */ + if ( fmflags&fm_flag_nottfhints ) ttfflags |= ttf_flag_nohints; + if ( fmflags&fm_flag_glyphmap ) ttfflags |= ttf_flag_glyphmap; + } + switch ( fmflags&(fm_flag_apple|fm_flag_opentype) ) { + case fm_flag_opentype: + ttfflags |= ttf_flag_applemode|ttf_flag_otmode; + break; + case fm_flag_apple|fm_flag_opentype: + /* Neither */; + break; + case fm_flag_apple: + ttfflags |= ttf_flag_applemode; + break; + case 0x00: + ttfflags |= ttf_flag_otmode; + break; + default: + break; + } + if ( fmflags&fm_flag_shortps ) ttfflags |= ttf_flag_shortps; + if ( fmflags&fm_flag_pfed_comments ) ttfflags |= ttf_flag_pfed_comments; + if ( fmflags&fm_flag_pfed_colors ) ttfflags |= ttf_flag_pfed_colors; + if ( fmflags&fm_flag_TeXtable ) ttfflags |= ttf_flag_TeXtable; + if ( fmflags&fm_flag_ofm ) ttfflags |= ttf_flag_ofm; + if ( (fmflags&fm_flag_applemode) && !(ttfflags&ttf_flag_applemode) ) + ttfflags |= ttf_flag_oldkern; + if ( fmflags&fm_flag_symbol ) ttfflags |= ttf_flag_symbol; + if ( fmflags&fm_flag_dummyDSIG ) ttfflags |= ttf_flag_dummyDSIG; + if ( fmflags&fm_flag_nofftm ) ttfflags |= ttf_flag_noFFTMtable; + if ( fmflags&fm_flag_pfed_lookups ) ttfflags |= ttf_flag_pfed_lookupnames; + if ( fmflags&fm_flag_pfed_guides ) ttfflags |= ttf_flag_pfed_guides; + if ( fmflags&fm_flag_pfed_layers ) ttfflags |= ttf_flag_pfed_layers; + if ( fmflags&fm_flag_winkern ) ttfflags |= ttf_flag_oldkernmappedonly; + if ( fmflags&fm_flag_nomacnames ) ttfflags |= ttf_flag_nomacnames; + return ttfflags; +} int GenerateScript(SplineFont *sf,char *filename,const char *bitmaptype, int fmflags, int res, char *subfontdefinition, struct sflist *sfs,EncMap *map, NameList *rename_to,int layer) { @@ -1157,83 +1205,10 @@ int GenerateScript(SplineFont *sf,char *filename,const char *bitmaptype, int fmf if ( fmflags&fm_flag_round ) old_ps_flags |= ps_flag_round; if ( fmflags&fm_flag_afmwithmarks ) old_ps_flags |= ps_flag_afmwithmarks; if ( i==bf_otb ) { - old_sfnt_flags = 0; - switch ( fmflags&(fm_flag_apple|fm_flag_opentype) ) { - case fm_flag_opentype: - old_sfnt_flags |= ttf_flag_applemode|ttf_flag_otmode; - break; - case fm_flag_apple|fm_flag_opentype: - /* Neither */; - break; - case fm_flag_apple: - old_sfnt_flags |= ttf_flag_applemode; - break; - case 0x00: - old_sfnt_flags |= ttf_flag_otmode; - break; - default: - break; - } - if ( fmflags&fm_flag_shortps ) old_sfnt_flags |= ttf_flag_shortps; - if ( fmflags&fm_flag_pfed_comments ) old_sfnt_flags |= ttf_flag_pfed_comments; - if ( fmflags&fm_flag_pfed_colors ) old_sfnt_flags |= ttf_flag_pfed_colors; - if ( fmflags&fm_flag_TeXtable ) old_sfnt_flags |= ttf_flag_TeXtable; - if ( fmflags&fm_flag_ofm ) old_sfnt_flags |= ttf_flag_ofm; - if ( (fmflags&fm_flag_applemode) && !(old_sfnt_flags&ttf_flag_applemode) ) - old_sfnt_flags |= ttf_flag_oldkern; - if ( fmflags&fm_flag_symbol ) old_sfnt_flags |= ttf_flag_symbol; - if ( fmflags&fm_flag_dummyDSIG ) old_sfnt_flags |= ttf_flag_dummyDSIG; - if ( fmflags&fm_flag_nofftm ) old_sfnt_flags |= ttf_flag_noFFTMtable; - if ( fmflags&fm_flag_pfed_lookups ) old_sfnt_flags |= ttf_flag_pfed_lookupnames; - if ( fmflags&fm_flag_pfed_guides ) old_sfnt_flags |= ttf_flag_pfed_guides; - if ( fmflags&fm_flag_pfed_layers ) old_sfnt_flags |= ttf_flag_pfed_layers; - if ( fmflags&fm_flag_winkern ) old_sfnt_flags |= ttf_flag_oldkernmappedonly; - if ( fmflags&fm_flag_nomacnames ) old_sfnt_flags |= ttf_flag_nomacnames; + old_sfnt_flags = fmflag2ttfflag(fmflags, true); } } else { - old_sfnt_flags = 0; - /* Applicable postscript flags */ - if ( fmflags&fm_flag_afm ) old_sfnt_flags |= ps_flag_afm; - if ( fmflags&fm_flag_pfm ) old_sfnt_flags |= ps_flag_pfm; - if ( fmflags&fm_flag_nohintsubs ) old_sfnt_flags |= ps_flag_nohintsubs; - if ( fmflags&fm_flag_noflex ) old_sfnt_flags |= ps_flag_noflex; - if ( fmflags&fm_flag_nopshints ) old_sfnt_flags |= ps_flag_nohints; - if ( fmflags&fm_flag_round ) old_sfnt_flags |= ps_flag_round; - if ( fmflags&fm_flag_afmwithmarks ) old_sfnt_flags |= ps_flag_afmwithmarks; - /* Applicable truetype flags */ - switch ( fmflags&(fm_flag_apple|fm_flag_opentype) ) { - case fm_flag_opentype: - old_sfnt_flags |= ttf_flag_applemode|ttf_flag_otmode; - break; - case fm_flag_apple|fm_flag_opentype: - /* Neither */; - break; - case fm_flag_apple: - old_sfnt_flags |= ttf_flag_applemode; - break; - case 0x00: - old_sfnt_flags |= ttf_flag_otmode; - break; - default: - break; - } - if ( fmflags&fm_flag_shortps ) old_sfnt_flags |= ttf_flag_shortps; - if ( fmflags&fm_flag_nottfhints ) old_sfnt_flags |= ttf_flag_nohints; - if ( fmflags&fm_flag_pfed_comments ) old_sfnt_flags |= ttf_flag_pfed_comments; - if ( fmflags&fm_flag_pfed_colors ) old_sfnt_flags |= ttf_flag_pfed_colors; - if ( fmflags&fm_flag_glyphmap ) old_sfnt_flags |= ttf_flag_glyphmap; - if ( fmflags&fm_flag_TeXtable ) old_sfnt_flags |= ttf_flag_TeXtable; - if ( fmflags&fm_flag_ofm ) old_sfnt_flags |= ttf_flag_ofm; - if ( (fmflags&fm_flag_applemode) && !(old_sfnt_flags&ttf_flag_applemode) ) - old_sfnt_flags |= ttf_flag_oldkern; - if ( fmflags&fm_flag_symbol ) old_sfnt_flags |= ttf_flag_symbol; - if ( fmflags&fm_flag_dummyDSIG ) old_sfnt_flags |= ttf_flag_dummyDSIG; - if ( fmflags&fm_flag_nofftm ) old_sfnt_flags |= ttf_flag_noFFTMtable; - if ( fmflags&fm_flag_pfed_lookups ) old_sfnt_flags |= ttf_flag_pfed_lookupnames; - if ( fmflags&fm_flag_pfed_guides ) old_sfnt_flags |= ttf_flag_pfed_guides; - if ( fmflags&fm_flag_pfed_layers ) old_sfnt_flags |= ttf_flag_pfed_layers; - if ( fmflags&fm_flag_winkern ) old_sfnt_flags |= ttf_flag_oldkernmappedonly; - if ( fmflags&fm_flag_nomacnames ) old_sfnt_flags |= ttf_flag_nomacnames; + old_sfnt_flags = fmflag2ttfflag(fmflags, false); } } diff --git a/fontforge/savefont.h b/fontforge/savefont.h index 6a51dd9d39..7bc4826085 100644 --- a/fontforge/savefont.h +++ b/fontforge/savefont.h @@ -71,6 +71,7 @@ int _DoSave(SplineFont *sf,char *newname,int32_t *sizes,int res, EncMap *map, char *subfontdefinition,int layer); int CheckIfTransparent(SplineFont *sf); +int fmflag2ttfflag(int fmflags, bool is_postscript_or_cff); extern int GenerateScript(SplineFont *sf, char *filename, const char *bitmaptype, int fmflags, int res, char *subfontdirectory, struct sflist *sfs, EncMap *map, NameList *rename_to, int layer); #ifdef FONTFORGE_CONFIG_WRITE_PFM From aca4f524c6cb14cdc7bc4cd493492a33f5154797 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Mon, 17 Nov 2025 19:55:43 +0100 Subject: [PATCH 20/96] Metrics view: Fix crash on Up/Down keypress while in the feature list (#5683) --- fontforgeexe/metricsview.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fontforgeexe/metricsview.c b/fontforgeexe/metricsview.c index 8284673e49..c6d38f4108 100644 --- a/fontforgeexe/metricsview.c +++ b/fontforgeexe/metricsview.c @@ -4327,8 +4327,12 @@ static void MVChar(MetricsView *mv,GEvent *event) // should always move up/down in the list of kerning words. if( active != mv->text ) { + const unichar_t *title = _GGadgetGetTitle(active); + if (!title) + return; + unichar_t *end; - double val = u_strtod(_GGadgetGetTitle(active),&end); + double val = u_strtod(title,&end); if (isValidInt(end)) { int dir = ( event->u.chr.keysym == GK_Up || event->u.chr.keysym==GK_KP_Up ) ? 1 : -1; if( event->u.chr.state&ksm_control && event->u.chr.state&ksm_shift ) { From d0634839fa9508283ce54c8463e22c0a9d1f8c0d Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Tue, 18 Nov 2025 21:25:39 +0100 Subject: [PATCH 21/96] Cleanup, fix UI font for CJK when loading resource (#5679) * [cleanup] A late review of PR #5663 * Override font in resource file on Windows for CJK locales --------- Co-authored-by: Maxim Iorsh --- gdraw/gresource.c | 90 ++++++++++++++++++++++++++++------------------- gutils/gwwintl.c | 28 ++++----------- inc/intl.h | 2 +- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/gdraw/gresource.c b/gdraw/gresource.c index dfb542c259..6257976c10 100644 --- a/gdraw/gresource.c +++ b/gdraw/gresource.c @@ -181,8 +181,45 @@ return; return; } +static char* replace_CJK_font_alias(const char* resource) { + /* On Windows systems the default UI font doesn't support CJK. We prepend it + with locale-specific font by replacing the predefined + "windows-cjk-workaround" alias. */ + static const char alias[] = "windows-cjk-workaround"; + char locale[100]; + const char *ui_font = NULL; + + gettext_locale(locale, sizeof(locale)); + if (strcmp(locale, "zh_CN") == 0) ui_font = "Microsoft YaHei UI"; + else if (strcmp(locale, "zh_SG") == 0) ui_font = "Microsoft YaHei UI"; + else if (strcmp(locale, "zh_TW") == 0) ui_font = "Microsoft JhengHei UI"; + else if (strcmp(locale, "zh_HK") == 0) ui_font = "Microsoft JhengHei UI"; + else if (strcmp(locale, "ja_JP") == 0) ui_font = "Meiryo UI,Yu Gothic UI"; + else if (strcmp(locale, "ja") == 0) ui_font = "Meiryo UI,Yu Gothic UI"; + else if (strcmp(locale, "ko_KR") == 0) ui_font = "Malgun Gothic"; + else if (strcmp(locale, "ko") == 0) ui_font = "Malgun Gothic"; + + if (ui_font == NULL) { + return NULL; + } + + const char* pos = strstr(resource, alias); + if (pos == NULL) { + return NULL; + } + + int prefix_len = pos - resource; + const char *suffix = pos + sizeof(alias) - 1; + char *result = smprintf("%.*s%s%s", + prefix_len, resource, + ui_font, + suffix); + + return result; +} + void GResourceAddResourceString(const char *string,const char *prog) { - char *ept; + char *ept, *value=NULL; const char *pt, *next; int cnt, plen; struct _GResource_Res temp; @@ -238,7 +275,17 @@ return; temp.res = copyn(pt+off,ept-(pt+off)); pt = ept+1; while ( isspace( *pt ) && ptrstr, "windows-cjk-workaround"); - if (pos == NULL) { - return; - } - - size_t prefix_len = pos - font->rstr; - size_t suffix_len = strlen(pos) + strlen("windows-cjk-workaround"); /* Length after "windows-cjk-workaround" */ - size_t ui_font_len = strlen(ui_font); - char* result = malloc(prefix_len + ui_font_len + suffix_len + 1); - - /* Copy parts together */ - strncpy(result, font->rstr, prefix_len); /* Copy prefix */ - strcpy(result + prefix_len, ui_font); /* Insert replacement */ - strcpy(result + prefix_len + ui_font_len, pos + strlen("windows-cjk-workaround")); /* Copy suffix */ + char* fixed_res = replace_CJK_font_alias(font->rstr); + if (fixed_res == NULL) + return; if (font->can_free_name) free(font->rstr); - font->rstr = result; + font->rstr = fixed_res; font->can_free_name = true; #endif } diff --git a/gutils/gwwintl.c b/gutils/gwwintl.c index abc956ff78..e0c4c03789 100644 --- a/gutils/gwwintl.c +++ b/gutils/gwwintl.c @@ -42,33 +42,19 @@ char *sgettext(const char *msgid) { return (char *) msgval; } -char* gettext_locale(char* locale_out) { +void gettext_locale(char* buffer, int size) { const char *loc = getenv("LANGUAGE"); + int i; if (loc == NULL) loc = getenv("LC_ALL"); if (loc == NULL) loc = getenv("LC_MESSAGES"); if (loc == NULL) loc = getenv("LANG"); /* Fallback for empty environment */ - if (loc == NULL) { - strcpy(locale_out, "en"); - return locale_out; - } + if (loc == NULL) loc = "en"; - char* dot_position = strchr(loc, '.'); - char* colon_position = strchr(loc, ':'); - - // If no dot or colon is found, copy the entire string - if (dot_position == NULL && colon_position == NULL) { - strcpy(locale_out, loc); - return locale_out; - } - - size_t substring_length = (dot_position == NULL) ? (colon_position - loc) : - (colon_position == NULL) ? (dot_position - loc) : - ((colon_position < dot_position) ? (colon_position - loc) : (dot_position - loc)); - - strncpy(locale_out, loc, substring_length); - locale_out[substring_length] = '\0'; - return locale_out; + /* Copy the string until '.' or ':' */ + for (i = 0; i < size - 1 && loc[i] && loc[i] != '.' && loc[i] != ':'; i++) + buffer[i] = loc[i]; + buffer[i] = 0; } diff --git a/inc/intl.h b/inc/intl.h index 544adca4f0..5cb3baaf2d 100644 --- a/inc/intl.h +++ b/inc/intl.h @@ -71,6 +71,6 @@ #define H_(str) (str) char *sgettext(const char *msgid); -char *gettext_locale(); +void gettext_locale(char *buffer, int size); #endif /* FONTFORGE_INTL_H */ From fbc4114e1af657b0889f78d24f70c4d56937d189 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sat, 22 Nov 2025 20:19:10 +0200 Subject: [PATCH 22/96] Use importlib in plugin manager (#5643) --- fontforge/plugin.c | 192 +++++++++++++++++++++++++++++----------- fontforgeexe/pluginui.c | 3 +- 2 files changed, 140 insertions(+), 55 deletions(-) diff --git a/fontforge/plugin.c b/fontforge/plugin.c index 35ffb30168..54de155019 100644 --- a/fontforge/plugin.c +++ b/fontforge/plugin.c @@ -285,28 +285,144 @@ static void ReimportPlugins() { } } -static bool DiscoverPlugins(int do_import) { - int do_ask = false; - PluginEntry *pe; - GList_Glib *i; - PyObject *str, *str2, *iter, *tmp, *tmp2, *entrypoint; - PyObject *pkgres = PyImport_ImportModule("pkg_resources"); - if (pkgres == NULL || !PyObject_HasAttrString(pkgres, "iter_entry_points")) { - LogError(_("Core python package 'pkg_resources' not found: Cannot discover plugins")); +static PyObject *GetPluginEntryPoints() { + PyObject *iter = NULL, *all_eps = NULL; + PyObject *entry_points = NULL; + + PyObject *importlib = PyImport_ImportModule("importlib.metadata"); + if (!importlib) { + PyErr_Clear(); + LogError(_("Core python package 'importlib.metadata' not found: Cannot discover plugins")); + return NULL; + } + if (!PyObject_HasAttrString(importlib, "entry_points")) { + LogError(_("Method 'entry_points()' not found in module 'importlib.metadata'")); PyErr_Clear(); - return false; + Py_DECREF(importlib); + return NULL; + } + all_eps = PyObject_CallMethod(importlib, "entry_points", NULL); + + /* The object returned from importlib.metadata.entry_points() varies between versions*/ + if (PyDict_Check(all_eps)) { + /* entry_points is a dictionary of entry point groups */ + entry_points = PyDict_GetItemString(all_eps, "fontforge_plugin"); + if (!entry_points) { + /* No FontForge plugins found */ + PyErr_Clear(); + return NULL; + } + } else { + /* all_eps is an EntryPoints object. Call all_eps.select(group="fontforge_plugin") */ + PyObject *select_method = PyObject_GetAttrString(all_eps, "select"); + PyObject *kw_args = Py_BuildValue("{s,s}", "group", "fontforge_plugin"); + entry_points = PyObject_Call(select_method, PyTuple_New(0), kw_args); + Py_DECREF(select_method); + Py_DECREF(kw_args); + if (!entry_points) { + LogError(_("Failed to retrieve plugin entry points")); + PyErr_Print(); + return NULL; + } } - str = PyUnicode_FromString("iter_entry_points"); - str2 = PyUnicode_FromString("fontforge_plugin"); - iter = PyObject_CallMethodObjArgs(pkgres, str, str2, NULL); - if (!PyIter_Check(iter)) { + + Py_DECREF(importlib); + + iter = PyObject_GetIter(entry_points); + if (!iter || !PyIter_Check(iter)) { + PyErr_Clear(); LogError(_("Could not iterate 'fontforge_plugin' entry points.")); - return false; + Py_XDECREF(iter); + return NULL; + } + + return iter; +} + +static void RetrieveStringItem(PyObject* obj, const char* key, char** p_value) { + PyObject* str = PyUnicode_FromString(key); + PyObject* val = PyObject_GetItem(obj, str); + if (val) { + free(*p_value); + *p_value = copy(PyUnicode_AsUTF8(val)); + } + Py_XDECREF(str); + Py_XDECREF(val); +} + +/* Retrieve name, URL, and summary of the plugin */ +static void LoadPluginMetadata(PluginEntry* pe) { + PyObject *globals = PyDict_New(), *locals = PyDict_New(); + PyObject *dist = NULL, *function_args = NULL; + + /* The `EntryPoint.dist` attribute is available starting with Python 3.10. + * To support Python 3.8+ we resort to the ugly but more uniform method of + * retrieving all `Distribution` objects and traversing them until we find + * one which points to our `EntryPoint` object. */ + const char* function_string = + "def load_plugin_metadata(entrypoint):\n" + " import importlib.metadata\n" + " all_dists = importlib.metadata.distributions()\n" + " for dist in all_dists:\n" + " dist_eps = dist.entry_points\n" + " for ep in dist_eps:\n" + " if ep == entrypoint:\n" + " return dist\n" + " return None\n"; + + /* Execute the function definition */ + if (PyRun_String(function_string, Py_file_input, globals, locals) == NULL) { + Py_DECREF(globals); + Py_DECREF(locals); + return; + } + + /* Retrieve the function object */ + PyObject* func = PyDict_GetItemString(locals, "load_plugin_metadata"); + if (func == NULL || !PyCallable_Check(func)) { + PyErr_SetString(PyExc_RuntimeError, "Could not find the function"); + Py_DECREF(globals); + Py_DECREF(locals); + return; + } + + /* Call the function */ + function_args = PyTuple_Pack(1, pe->entrypoint); + if (function_args != NULL) { + dist = PyObject_Call(func, function_args, NULL); + } + Py_XDECREF(func); + Py_XDECREF(function_args); + + if (dist == NULL) { + PyErr_Print(); + PyErr_SetString(PyExc_RuntimeError, "Could not retrieve distribution"); + return; + } + + if (PyObject_HasAttrString(dist, "metadata")) { + PyObject* metadata = PyObject_GetAttrString(dist, "metadata"); + + RetrieveStringItem(metadata, "Home-page", &pe->package_url); + RetrieveStringItem(metadata, "Name", &pe->package_name); + RetrieveStringItem(metadata, "Summary", &pe->summary); + Py_XDECREF(metadata); + } + + Py_XDECREF(dist); +} + +static bool DiscoverPlugins(int do_import) { + int do_ask = false; + PluginEntry *pe = NULL; + GList_Glib *i; + PyObject *str, *str2, *iter, *entrypoint; + + iter = GetPluginEntryPoints(); + if (iter == NULL) { + return false; } - Py_DECREF(str); - Py_DECREF(str2); - PyObject *getmetastr = PyUnicode_FromString("get_metadata_lines"); while ((entrypoint = PyIter_Next(iter))) { // Find name and module_name str = PyObject_GetAttrString(entrypoint, "name"); @@ -316,7 +432,7 @@ static bool DiscoverPlugins(int do_import) { PyErr_Clear(); continue; } - str2 = PyObject_GetAttrString(entrypoint, "module_name"); + str2 = PyObject_GetAttrString(entrypoint, "value"); const char *modname = PyUnicode_AsUTF8(str2); if (modname == NULL) { Py_XDECREF(str); @@ -341,8 +457,8 @@ static bool DiscoverPlugins(int do_import) { } Py_DECREF(str); Py_DECREF(str2); - str = PyObject_GetAttrString(entrypoint, "attrs"); - if (str == NULL) { + str = PyObject_GetAttrString(entrypoint, "attr"); + if (str == NULL || str == Py_None) { PyErr_Clear(); } else { if (pe->attrs) { @@ -355,43 +471,13 @@ static bool DiscoverPlugins(int do_import) { pe->is_present = true; Py_XDECREF(pe->entrypoint); pe->entrypoint = entrypoint; - // Extract project URL from package data - PyObject *dist = PyObject_GetAttrString(entrypoint, "dist"); - if (dist != NULL) { - tmp = PyObject_GetAttrString(dist, "PKG_INFO"); - tmp2 = PyObject_CallMethodObjArgs(dist, getmetastr, tmp, NULL); - Py_DECREF(tmp); - if (PyIter_Check(tmp2)) { - while ((str = PyIter_Next(tmp2))) { - const char *metaline = PyUnicode_AsUTF8(str); - // printf("%s\n", metaline); - if (strncmp(metaline, "Home-page: ", 11) == 0) { - if (pe->package_url != NULL) { - free(pe->package_url); - } - pe->package_url = copy(metaline + 11); - } else if (strncmp(metaline, "Name: ", 6) == 0) { - if (pe->package_name != NULL) { - free(pe->package_name); - } - pe->package_name = copy(metaline + 6); - } else if (strncmp(metaline, "Summary: ", 9) == 0) { - if (pe->summary != NULL) { - free(pe->summary); - } - pe->summary = copy(metaline + 9); - } - Py_DECREF(str); - } - } - Py_DECREF(tmp2); - } + + LoadPluginMetadata(pe); if (do_import && pe->startup_mode == sm_on) { LoadPlugin(pe); } else if (do_import && pe->startup_mode == sm_ask) { do_ask = true; } - Py_XDECREF(dist); } if (PyErr_Occurred()) { PyErr_Print(); @@ -403,8 +489,6 @@ static bool DiscoverPlugins(int do_import) { } } Py_DECREF(iter); - Py_DECREF(getmetastr); - Py_DECREF(pkgres); return do_ask; } diff --git a/fontforgeexe/pluginui.c b/fontforgeexe/pluginui.c index fa7f366650..dda25430d8 100644 --- a/fontforgeexe/pluginui.c +++ b/fontforgeexe/pluginui.c @@ -164,7 +164,7 @@ static void PluginInfoDlg(PluginEntry *pe) { gcd[k++].creator = GLabelCreate; hvgrid[3][0] = &gcd[k - 1]; - label[k].text = (unichar_t *) pe->attrs; + label[k].text = (unichar_t *)(pe->attrs == NULL ? "" : pe->attrs); label[k].text_is_1byte = true; gcd[k].gd.label = &label[k]; gcd[k].gd.flags = gg_enabled | gg_visible; @@ -522,6 +522,7 @@ static int PLUG_PluginOp(GGadget *g, GEvent *e) { } else if (cid == CID_Revert) { FigurePluginList(d); } + GGadgetRedraw(list); return true; } From c6c7bd276e64c4fd874127a9535033d228dd7be4 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sun, 23 Nov 2025 22:56:43 +0200 Subject: [PATCH 23/96] Implement `with` statement for Python font object (#5694) --- doc/sphinx/scripting/python/fontforge.rst | 9 +++++++++ fontforge/python.c | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 69ce863b7b..8e961ee32c 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -414,6 +414,15 @@ Module functions Retain all recognized font tables that do not have a native format. + This function can also be used with the ``with`` statement, in which case + there is no need to call :meth:`font.close()` explicitly: + + :: + + with fontforge.open('somefont.sfd') as fnt: + # do something + fnt.generate('somefont.ttf') + .. function:: parseTTInstrs(string) Returns a binary string each byte of which corresponds to a truetype diff --git a/fontforge/python.c b/fontforge/python.c index 424bc3917e..cb16d38486 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -19465,6 +19465,10 @@ return (NULL); Py_RETURN( self ); } +static PyObject *PyFFFont_enter(PyFF_Font *self, PyObject *UNUSED(args)) { + Py_RETURN(self); +} + PyMethodDef PyFF_Font_methods[] = { { "appendSFNTName", (PyCFunction) PyFFFont_appendSFNTName, METH_VARARGS, "Adds or replaces a name in the sfnt 'name' table. Takes three arguments, a language, a string id, and the string value" }, { "close", (PyCFunction) PyFFFont_close, METH_NOARGS, "Frees up memory for the current font. Any python pointers to it will become invalid." }, @@ -19564,6 +19568,8 @@ PyMethodDef PyFF_Font_methods[] = { { "validate", (PyCFunction)PyFFFont_validate, METH_VARARGS, "Check whether a font is valid and return True if it is." }, { "reencode", (PyCFunction)PyFFFont_reencode, METH_VARARGS, "Reencodes the current font into the given encoding." }, { "clearSpecialData", (PyCFunction)PyFFFont_clearSpecialData, METH_NOARGS, "Clear special data not accessible in FontForge." }, + { "__enter__", (PyCFunction) PyFFFont_enter, METH_NOARGS, "Empty function declaring the entry into context statement." }, + { "__exit__", (PyCFunction) PyFFFont_close, METH_FASTCALL, "Frees up memory for the current font. Any python pointers to it will become invalid." }, // Leave some sentinel slots here so that the UI // code can add it's methods to the end of the object declaration. From 9d119ca0bf415e3d69086f4931fcbb39d3d85c51 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 1 Dec 2025 20:19:47 +0200 Subject: [PATCH 24/96] Ignore bit 1 of head.flags for CFF fonts (#5698) --- fontforge/parsettf.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fontforge/parsettf.c b/fontforge/parsettf.c index 356c7c3c2e..381eccee02 100644 --- a/fontforge/parsettf.c +++ b/fontforge/parsettf.c @@ -5594,6 +5594,9 @@ return( 0 ); readttfglyphs(ttf,info); } else if ( info->cff_start!=0 ) { info->to_order2 = (loaded_fonts_same_as_new && new_fonts_are_order2); + // Relevant only for TrueType (Font Header Table specification): + // https://learn.microsoft.com/en-us/typography/opentype/spec/head#flags + info->apply_lsb = false; if ( !readcffglyphs(ttf,info) ) { switch_to_old_locale(&tmplocale, &oldlocale); // Switch to the cached locale. return( 0 ); From 0c9d0ca50bf795846080ad897dfa365fc9bf7459 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 3 Dec 2025 00:59:35 +0200 Subject: [PATCH 25/96] Fix Python config initialization (#5699) --- fontforge/python.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fontforge/python.c b/fontforge/python.c index cb16d38486..c3228c63ae 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -20826,7 +20826,7 @@ void FontForge_FinalizeEmbeddedPython(void) { } /* This is called to start up the embedded python interpreter */ -void FontForge_InitializeEmbeddedPython(void) { +void FontForge_InitializeEmbeddedPythonEx(int argc, wchar_t **argv) { // static int python_initialized is declared above. if ( python_initialized ) return; @@ -20834,7 +20834,8 @@ void FontForge_InitializeEmbeddedPython(void) { PyConfig config; PyStatus status; PyConfig_InitPythonConfig(&config); - + if (argc > 0) + PyConfig_SetArgv(&config, argc, argv); status = PyConfig_SetBytesString(&config, &config.program_name, "fontforge"); if (PyStatus_Exception(status)) { @@ -20860,6 +20861,10 @@ void FontForge_InitializeEmbeddedPython(void) { InitializePythonMainNamespace(); } +void FontForge_InitializeEmbeddedPython() { + FontForge_InitializeEmbeddedPythonEx(0, NULL); +} + static wchar_t ** copy_argv(char *arg0, int argc ,char **argv); /* PyFF_Main() -- This is called to run a script as the main task, by @@ -20879,8 +20884,6 @@ _Noreturn void PyFF_Main(int argc,char **argv,int start, int do_inits, no_windowing_ui = running_script = true; - FontForge_InitializeEmbeddedPython(); - PyFF_ProcessInitFiles(do_inits, do_plugins); /* Skip '-script' option */ arg = argv[start]; @@ -20892,8 +20895,11 @@ _Noreturn void PyFF_Main(int argc,char **argv,int start, int do_inits, newargc = argc - start + 1; newargv = copy_argv(argv[0], newargc-1, &argv[start] ); + FontForge_InitializeEmbeddedPythonEx(newargc, newargv); + PyFF_ProcessInitFiles(do_inits, do_plugins); + /* Run Python */ - exitcode = Py_Main( newargc, newargv ); + exitcode = Py_RunMain(); FontForge_FinalizeEmbeddedPython(); exit(exitcode); } From 85026c213a0518424fca371d09f9a433270e4d89 Mon Sep 17 00:00:00 2001 From: Fini Date: Sun, 7 Dec 2025 22:23:13 +0100 Subject: [PATCH 26/96] Add dependency missing in INSTALL.md (#5701) --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 3595a0e7a1..fc2b7f9ed2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ The exact method to do this depends on your OS distribution. To download all dependencies on Ubuntu, run: ```sh -sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev libfreetype-dev libgif-dev libgtk-3-dev libxml2-dev libpango1.0-dev libcairo2-dev libspiro-dev libwoff-dev python3-dev ninja-build cmake build-essential gettext; +sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev libfreetype-dev libgif-dev libgtk-3-dev libgtkmm-3.0-dev libxml2-dev libpango1.0-dev libcairo2-dev libspiro-dev libwoff-dev python3-dev ninja-build cmake build-essential gettext; ``` Now run the build and installation scripts: From aedb8f2e95ddba99e8cb6446f082a356e2c2625b Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 8 Dec 2025 23:38:32 +0100 Subject: [PATCH 27/96] tottf: Fix hmtx table (all widths zero) for monospaced fonts (#5702) `hmtx` table is incorrectly optimized assuming all the widths are same in a monospace font. This optimization is now canceled. Signed-off-by: Fini Jastrow --- fontforge/tottf.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fontforge/tottf.c b/fontforge/tottf.c index dddd5d6a6d..3b53cf21ac 100644 --- a/fontforge/tottf.c +++ b/fontforge/tottf.c @@ -1459,7 +1459,6 @@ static int dumpglyphs(SplineFont *sf,struct glyphinfo *gi) { FigureFullMetricsEnd(sf,gi,true); if ( fixed>0 ) { - gi->lasthwidth = 3; gi->hfullcnt = 3; } for ( i=0; igcnt; ++i ) { From dfc1598822bbe358a8f547dcd69eb5247a696121 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 10 Dec 2025 22:44:41 +0200 Subject: [PATCH 28/96] Fix SplineCharCopy() for Python objects, SplineChar::comment (#5696) --- fontforge/fvfonts.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fontforge/fvfonts.c b/fontforge/fvfonts.c index e38bfd3fb8..c586098891 100644 --- a/fontforge/fvfonts.c +++ b/fontforge/fvfonts.c @@ -531,6 +531,7 @@ SplineChar *SplineCharCopy(SplineChar *sc,SplineFont *into,struct sfmergecontext nsc->parent = into; nsc->orig_pos = -2; nsc->name = copy(sc->name); + nsc->comment = copy(sc->comment); nsc->hstem = StemInfoCopy(nsc->hstem); nsc->vstem = StemInfoCopy(nsc->vstem); nsc->dstem = DStemInfoCopy(nsc->dstem); @@ -550,6 +551,10 @@ SplineChar *SplineCharCopy(SplineChar *sc,SplineFont *into,struct sfmergecontext nsc->altuni = AltUniCopy(nsc->altuni,into); nsc->charinfo = NULL; nsc->views = NULL; +#ifndef _NO_PYTHON + nsc->python_sc_object = NULL; + nsc->python_temporary = NULL; +#endif return(nsc); } From 438679c0dc7bfc8a67c03c51f8d06f4d6cd3b2e0 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Thu, 11 Dec 2025 22:42:37 +0200 Subject: [PATCH 29/96] Remove X11 backend options from CLI and from documentation (#5690) --- doc/sphinx/techref/cliargs.rst | 57 --------------------------- doc/sphinx/ui/misc/HotKeys.rst | 6 --- doc/sphinx/ui/misc/xres.rst | 70 ---------------------------------- fontforgeexe/fontforge.1 | 30 --------------- fontforgeexe/startui.c | 36 +---------------- 5 files changed, 2 insertions(+), 197 deletions(-) diff --git a/doc/sphinx/techref/cliargs.rst b/doc/sphinx/techref/cliargs.rst index 622cadd6bd..462ddc3d34 100644 --- a/doc/sphinx/techref/cliargs.rst +++ b/doc/sphinx/techref/cliargs.rst @@ -47,30 +47,10 @@ FontForge recognizes the following options: Will read a font from "foo.sfd" and then generate a truetype font from it called "foo.ttf" -.. option:: -cmap type - - Where type may be - - Current | Copy | Private - - Gives the user some control over :ref:`colormap handling ` on - 8bit screens. - -.. option:: -depth val - - Specifies that FontForge should search for a visual with the given depth - .. option:: -display name Specifies the name of the display on which FontForge will open its windows -.. option:: -dontopenxdevices - - Various people have complained that when FontForge attempts to open the - devices of the wacom graphics tablet, the X server gives a BadDevice error. I - can't duplicate this, the open works fine on my system, but this argument - allows them to tell fontforge not to try to use the tablet. - .. option:: -help Bring up a :doc:`browser ` looking at this documentation. @@ -85,33 +65,6 @@ FontForge recognizes the following options: Prints out the source version and exits. -.. option:: -keyboard type - - .. warning:: Deprecated option, may not do anything - - Where type may be - - ibm | mac | sun | ppc | 0 | 1 | 2 | 3 - - Allows you to specify the type of keyboard. Currently this is only relevant - when generating menus. The modifier keys are in different locations on - different keyboards (under different operating systems) and if FontForge - knows what keyboard you are using it can make the hot-keys have better - labels. - - * ibm | 0 - - Uses the Control and Alt keys - * mac | 1 - - Uses the Control and Option keys (Mac OS/X, Mac keyboard) - * ppc | 3 - - Uses the Control and Command keys (Suse ppc linux, Mac keyboard) - * sun | 2 - - Uses the Control and Meta keys - .. option:: -last Opens the last sfd file closed. If used more than once will open the last @@ -190,16 +143,6 @@ FontForge recognizes the following options: Display a brief description of the options -.. option:: -vc type - - Where type may be: - - StaticGray GrayScale StaticColor PseudoColor TrueColor DirectColor - - (See the X manuals for a description of what these mean). FontForge will - search through the visuals in an attempt to find one with the desired - VisualClass (given here) and depth (given with the -depth option). - .. _cliargs.Environment: diff --git a/doc/sphinx/ui/misc/HotKeys.rst b/doc/sphinx/ui/misc/HotKeys.rst index e42e8cdca5..5314961e89 100644 --- a/doc/sphinx/ui/misc/HotKeys.rst +++ b/doc/sphinx/ui/misc/HotKeys.rst @@ -134,12 +134,6 @@ I am aware of the following significant differences: when not using X) * On Suns the meta (diamond) key is equivalent to the Alt modifier key. -FontForge will attempt to guess what keyboard you are using and produce menus -with hot-key indicators that match the host machine. If you are displaying on a -different machine from the one you are running on the menu names may be wrong. -You can fix this up with the :ref:`keyboard resource `, or the --keyboard command line argument. - Tool modifiers in the Outline Glyph Window ------------------------------------------ diff --git a/doc/sphinx/ui/misc/xres.rst b/doc/sphinx/ui/misc/xres.rst index fbe435b685..a613da34eb 100644 --- a/doc/sphinx/ui/misc/xres.rst +++ b/doc/sphinx/ui/misc/xres.rst @@ -864,71 +864,6 @@ GDraw Whether to synchronize the display before raising the first window. -GDraw (X backend only) -~~~~~~~~~~~~~~~~~~~~~~ - -These are not included in the appearance editor and need to be set -some other way, perhaps through the normal X Resources system. - -.. object:: Gdraw.Depth - - An integer (1, 8, 16, 32) - - You can use this to request a different depth than the default one. Not all - servers will support all depths. If FontForge can't find a visual with the - desired depth it will use the default depth. - -.. object:: Gdraw.VisualClass - - A string ("StaticGray", "GrayScale", "StaticColor", "PseudoColor", - "TrueColor", "DirectColor") - - FontForge will search for a visual with the given class (and possibly depth - if the depth argument is specified too). - -.. _xres.Colormap: - -.. object:: Gdraw.Colormap - - An string ("Current", "Copy", "Private") - - You can use this to control what FontForge does about the colormap on an 8bit - screen - - * Current -- FontForge will attempt to allocate its colors in the current - colormap. - * Copy -- FontForge will allocate what colors it can and then copy the current - color map into a private copy. This means FontForge has access to a much - wider range of colors, and (as long as the shared colormap doesn't change) - FontForge's colormap will match that of the rest of the screen. - * Private -- FontForge will allocate a private colormap and set the colors just - as it wants them. It will almost certainly not match the shared colormap. - -.. _xres.Keyboard: - -.. object:: Gdraw.Keyboard - - ibm | mac | sun | ppc | 0 | 1 | 2 | 3 - - Allows you to specify the type of keyboard. Currently this is only relevant - when generating menus. The modifier keys are in different locations on - different keyboards (under different operating systems) and if FontForge - knows what keyboard you are using it can make the hot-keys have better - labels. - - * ibm | 0 - - Uses the Control and Alt keys - * mac | 1 - - Uses the Command and Option keys (Mac OS/X, Mac keyboard) - * ppc | 3 - - Uses the Control and Command keys (Suse ppc linux, Mac keyboard) - * sun | 2 - - Uses the Control and Meta keys - Popup ~~~~~ @@ -1590,11 +1525,6 @@ Almost all keyboards now-a-days will have the needed modifier keys, but which key is used for what will depend on the keyboard and the OS (for instance XDarwin and suse linux use quite different mappings for the modifier keys on the mac keyboard). Usually this is only relevant for menus (and mnemonics). -FontForge tries to guess the keyboard from the environment in which it was -compiled. But with X this may not always be appropriate. So the -":ref:`Gdraw.Keyboard `" resource above may be used to change -this. (Currently this setting only control the labels that appear in menus for -the hotkeys). Mice are more problematic. On PCs we usually have two button mice and on mac single button mice. Many linuxes that run on a PC will give you an option of diff --git a/fontforgeexe/fontforge.1 b/fontforgeexe/fontforge.1 index ea27b62148..109d50c0a8 100644 --- a/fontforgeexe/fontforge.1 +++ b/fontforgeexe/fontforge.1 @@ -11,13 +11,10 @@ fontforge \- create, modify, and view font files .BR fontforge [\fB\-allglyphs\fP] [\fB\-c\fP\ \fIstring\fP] -[\fB\-cmap\fP\ \fImaptype\fP] -[\fB\-depth\fP\ \fIpixeld\fP] [\fB\-display\fP\ \fIstr\fP] [\fB\-lang\fP=ff] [\fB\-lang\fP=py] [\fB\-help\fP] -[\fB\-keyboard\fP\ \fIktype\fP] [\fB\-last\fP] [\fB\-new\fP] [\fB\-nosplash\fP] @@ -29,7 +26,6 @@ fontforge \- create, modify, and view font files [\fB\-sync\fP] [\fB\-unique\fP] [\fB\-usage\fP] -[\fB\-vc\fP\ \fIclass\fP] [\fB\-version\fP] [\fIfontfile\fP\ ...] .SH DESCRIPTION @@ -96,35 +92,9 @@ in it and have this process exit. Employ the X display specified by the string \fIdisplay-name\fP (for example: localhost:0). .TP -.B \-depth \fIpixeld\fP -Attempt to employ a visual that matches the specified pixel depth, -\fIpixeld\fP. -.TP -.B \-vc \fIval\fP -Sets the visual class if possible. -.TP -.B \-cmap current\fR|\fPcopy\fR|\fPprivate -Sets the type of color map. -\fBcurrent\fP attempts to allocate colors int he current (shared) color map. -The program will likely not find everything it requires. -\fBcopy\fP allocates what can be allocated, -then copies the current color map; it can thus -make use of cells other programs are using. -\fBprivate\fP creates a new color map and fills it with the required colors. -.TP -.B \-dontopenxdevices -In case that fails. -.TP .B \-sync Syncs the display, debugging. .TP -.B \-keyboard ibm\fR|\fPmac\fR|\fPsun\fR|\fPppc -Generates appropriate hotkeys in menus. -Use \fBibm\fP on an IBM-compatible PC. -Use \fBmac\fP on a Mac computer running Mac\ OS. -Use \fBsun\fP on a Sun workstation. -Use \fBppc\fP on Power PC Mac running SUSE GNU/Linux. -.TP .B \-help Displays a help message and exits. .TP diff --git a/fontforgeexe/startui.c b/fontforgeexe/startui.c index 80dc606224..dff118177e 100644 --- a/fontforgeexe/startui.c +++ b/fontforgeexe/startui.c @@ -90,12 +90,7 @@ static void _dousage(void) { printf( "\t-quiet\t\t\t (don't print non-essential\n\t\t\t\t information to stderr)\n" ); printf( "\t-unique\t\t\t (if a fontforge is already running open all\n\t\t\t\t arguments in it and have this process exit)\n" ); printf( "\t-display display-name\t (sets the X display)\n" ); - printf( "\t-depth val\t\t (sets the display depth if possible)\n" ); - printf( "\t-vc val\t\t\t (sets the visual class if possible)\n" ); - printf( "\t-cmap current|copy|private\t (sets the type of colormap)\n" ); - printf( "\t-dontopenxdevices\t (in case that fails)\n" ); printf( "\t-sync\t\t\t (syncs the display, debugging)\n" ); - printf( "\t-keyboard ibm|mac|sun|ppc (generates appropriate hotkeys in menus)\n" ); #if MyMemory printf( "\t-memory\t\t\t (turns on memory checks, debugging)\n" ); #endif @@ -481,20 +476,6 @@ return( true ); return( true ); } -static void AddR(char *program_name, char *window_name, char *cmndline_val) { -/* Add this command line value to this GUI resource. */ -/* These are the command line options expected when using this routine: */ -/* -depth, -vc,-cmap or -colormap,-dontopenxdevices, -keyboard */ - char *full; - if ((full = malloc(strlen(window_name)+strlen(cmndline_val)+4))!=NULL) { - strcpy(full,window_name); - strcat(full,": "); - strcat(full,cmndline_val); - GResourceAddResourceString(full,program_name); - free(full); - } -} - static int ReopenLastFonts(void) { char buffer[1024]; char *ffdir = getFontForgeUserDir(Config); @@ -741,16 +722,6 @@ int fontforge_main( int argc, char **argv ) { ++pt; if ( strcmp(pt,"-sync")==0 ) GResourceAddResourceString("Gdraw.Synchronize: true",argv[0]); - else if ( strcmp(pt,"-depth")==0 && i Date: Sat, 13 Dec 2025 21:19:28 +0200 Subject: [PATCH 30/96] Documentation fixes (#5687) * Python: glyph.glyphPen([replace=True]) * Fix `showttf` docs * Fix links to Apple TrueType Reference Manual * Links to Adobe PDFs --- doc/sphinx/appendices/bibliography.rst | 12 ++-- doc/sphinx/faq.rst | 2 +- doc/sphinx/fontutils.rst | 11 ++-- doc/sphinx/scripting/python/fontforge.rst | 3 +- doc/sphinx/techref/TrueOpenTables.rst | 68 +++++++++++------------ doc/sphinx/techref/gposgsub.rst | 18 +++--- doc/sphinx/techref/hinting.rst | 4 +- doc/sphinx/ui/dialogs/fontinfo.rst | 4 +- doc/sphinx/ui/dialogs/multiplemaster.rst | 4 +- doc/sphinx/ui/dialogs/prefs.rst | 2 +- doc/sphinx/ui/dialogs/problems.rst | 4 +- doc/sphinx/ui/dialogs/statemachine.rst | 10 ++-- doc/sphinx/ui/dialogs/ttfinstrs.rst | 8 +-- fontforge/parsettf.c | 4 +- fontforge/parsettfvar.c | 2 +- fontforge/tottf.c | 2 +- 16 files changed, 79 insertions(+), 79 deletions(-) diff --git a/doc/sphinx/appendices/bibliography.rst b/doc/sphinx/appendices/bibliography.rst index 8453461892..4e3afd4940 100644 --- a/doc/sphinx/appendices/bibliography.rst +++ b/doc/sphinx/appendices/bibliography.rst @@ -68,25 +68,25 @@ Font File Formats (Sadly different sources have slightly different definitions of less important parts of the standard, be warned) - * `Apple `__ (I find Apple's prose + * `Apple `__ (I find Apple's prose difficult, and sometimes misleading. I suggest using a different source when possible) * `Microsoft `__ * `random useful site `__ * `TTC `__ -- True Type Font Collection -* `Apple Advanced Typography `__ +* `Apple Advanced Typography `__ extensions to TrueType * Apple distortable font (variation tables) -- vaguely equivalent to Multiple Master fonts for TrueType - * `fvar `__ (font + * `fvar `__ (font variations) - * `gvar `__ (glyph + * `gvar `__ (glyph variations) - * `cvar `__ (cvt + * `cvar `__ (cvt variations) - * `avar `__ (axis + * `avar `__ (axis variations) * .. _bibliography.OpenType: diff --git a/doc/sphinx/faq.rst b/doc/sphinx/faq.rst index 2762ccf0d7..4e9f287683 100644 --- a/doc/sphinx/faq.rst +++ b/doc/sphinx/faq.rst @@ -952,7 +952,7 @@ Random questions postscript file. It is based on the postscript font name but suffers a transformation. Don't try to rename this file. Basically the rules are (see - `Adobe Technical Note 0091 `__): + `Adobe Technical Note 0091 `__): * The fontname is broken into chunks where each chunk starts with a capital letter. diff --git a/doc/sphinx/fontutils.rst b/doc/sphinx/fontutils.rst index 84f735fc48..1b178254ed 100644 --- a/doc/sphinx/fontutils.rst +++ b/doc/sphinx/fontutils.rst @@ -13,12 +13,11 @@ https://github.com/fontforge/fontforge/tree/master/contrib/fonttools ShowTTF ------- -showttf is a program which will decompose a truetype (or opentype) font file +``showttf`` is a program which will decompose a truetype (or opentype) font file into its various tables and display the contents of those tables as best it can. It will also point out the errors it finds in the file. This is similar to -MicroSoft's `ttfdump `_ -(except mine shows some of Apple's tables which ttfdump does not), and to -Apple's `ftxdumperfuser `_ +fontTools `ttx `_ +utility, and to Apple's `ftxdumperfuser `_ (except mine shows some OpenType tables which Apple's ignores). And, of course, mine is open source and runs on most any system. @@ -30,14 +29,14 @@ mine is open source and runs on most any system. Ttf2Eps ------- -ttf2eps will allow you to extract one (or all) glyphs from a font and convert +``ttf2eps`` will allow you to extract one (or all) glyphs from a font and convert them into eps files. PfaDecrypt ----------- -pfadecrypt will undo Adobe's encryption on pfa/pfb files and converts the byte +``pfadecrypt`` will undo Adobe's encryption on pfa/pfb files and converts the byte codes into readable postscript like instructions. diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 8e961ee32c..8f32ebc4f9 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -2757,11 +2757,12 @@ must be created through the font. Draw the glyph's outline to the `pen argument. `_ -.. method:: glyph.glyphPen([replace=False]) +.. method:: glyph.glyphPen([replace=True]) Creates a new glyphPen which will draw into the current glyph. By default the pen will replace any existing contours and references, but setting the optional keyword argument, ``replace`` to false will retain the old contents. + Replacing the foreground layer will also reset the advance. .. method:: glyph.addInflections() diff --git a/doc/sphinx/techref/TrueOpenTables.rst b/doc/sphinx/techref/TrueOpenTables.rst index b92f47a51b..5da808b625 100644 --- a/doc/sphinx/techref/TrueOpenTables.rst +++ b/doc/sphinx/techref/TrueOpenTables.rst @@ -1,7 +1,7 @@ TrueType and OpenType tables supported by FontForge =================================================== -`Apple `__, +`Apple `__, `MS `__ and `Adobe `__ all have their descriptions of TrueType and OpenType. MS and Adobe's versions @@ -19,7 +19,7 @@ are essentially the same, while Apple's differ significantly from either. * - avar - axis variation - Used to specify piecewise linear sections of axes for distortable fonts - - `avar `__ + - `avar `__ - * * - BASE - baseline data @@ -30,7 +30,7 @@ are essentially the same, while Apple's differ significantly from either. * - bsln - baseline data - Provides information on baseline positioning for AAT. - - `bsln `__ + - `bsln `__ - * * - BDF - X11 BDF header @@ -43,24 +43,24 @@ are essentially the same, while Apple's differ significantly from either. - bitmap data - Provides the actual bitmap data for all bitmap glyphs in Apple fonts. 'EBDT' is used by MS/Adobe instead (and has the same format) - - `bdat `__ + - `bdat `__ - * * - bhed - bitmap font header - Replaces the 'head' table in Apple's bitmap only fonts. (similar to 'head') - - `bhed `__ + - `bhed `__ - * * - bloc - bitmap location data - Provides pointers to the appropriate bitmap data for each bitmap glyph in Apple fonts. 'EBLC' is used by MS/Adobe instead (and has the same format) - - `bloc `__ + - `bloc `__ - * * - CFF - - `Compact Font Format `__ + - `Compact Font Format `__ - Provides all the outline data (and sometimes glyph names as well) for the PostScript - `Type2 `__ + `Type2 `__ font that is the heart of an OpenType font. Vaguely equivalent to 'glyf' , 'loca' and 'post' combined. - * @@ -76,18 +76,18 @@ are essentially the same, while Apple's differ significantly from either. For fonts with special 1-byte encodings (such as symbol) it will provide a "symbol" encoding which maps a known page in the corporate use area to the glyphs. - - `cmap `__ + - `cmap `__ - `cmap `__ * - cvar - variations on cvt table - Used to specify hinting differences in distortable fonts - - `cvar `__ + - `cvar `__ - * * - cvt - control value table - FontForge uses this when it attempts to hint truetype fonts (FontForge will read it from a font and save it into another). - - `cvt `__ + - `cvt `__ - `cvt `__ * - EBDT - embedded bitmap data @@ -106,14 +106,14 @@ are essentially the same, while Apple's differ significantly from either. - Provides information on how to scale bitmaps (on those rare occasions where this is desirable). FontForge uses it when making bitmap only fonts for windows. - - `EBSC `__ + - `EBSC `__ - `EBSC `__ * - feat - layout feature table - Maps features specified in Apple's 'morx' (or 'mort') tables into names provided in the 'name' table. FontForge generates this when it generates a 'morx' table. - - `feat `__ + - `feat `__ - * * - FFTM - FontForge timestamp @@ -126,20 +126,20 @@ are essentially the same, while Apple's differ significantly from either. - font program - FontForge never generates this, but it will preserve it and allows users to edit it. - - `fpgm `__ + - `fpgm `__ - `fpgm `__ * - fvar - font variations - Provides top level information about distortable fonts. Specifies the types of distortions possible. - - `fvar `__ + - `fvar `__ - * * - gasp - grid-fitting and scan conversion - When FontForge does not attempt to hint a truetype font it will generate this table which tells the rasterizer not to to do grid-fitting (hinting) but to do anti-aliasing instead. - - `gasp `__ + - `gasp `__ - `gasp `__ * - GDEF - glyph definition @@ -152,7 +152,7 @@ are essentially the same, while Apple's differ significantly from either. - glyph outline - Provides outline information for truetype glyphs. Vaguely equivalent to 'CFF '. - - `glyf `__ + - `glyf `__ - `glyf `__ * - GPOS - glyph positioning @@ -172,43 +172,43 @@ are essentially the same, while Apple's differ significantly from either. - glyph variations - This table contains the meat of a distortable font. It specifies how each glyph can be distorted. - - `gvar `__ + - `gvar `__ - * * - head - font header - Contains general font information, such as the size of the em-square, a time stamp, check sum information, etc. - - `head `__ + - `head `__ - `head `__ * - hhea - horizontal header - This table contains font-wide horizontal metric information. - - `hhea `__ + - `hhea `__ - `hhea `__ * - hmtx - horizontal metrics - This contains the per-glyph horizontal metrics. - - `hmtx `__ + - `hmtx `__ - `hmtx `__ * - kern - kerning - Provides kerning information for Apple's truetype fonts (and for older MS truetype fonts). In OpenType fonts this information is contained in the GPOS table. - - `kern `__ + - `kern `__ - `kern `__ * - lcar - ligature caret - This table provides the location of carets within ligatures in an Apple font. This information is contained in the GDEF table in opentype fonts. - - `lcar `__ + - `lcar `__ - * * - loca - glyph location - This provides a pointer into the 'glyf' table for each glyph in the font. It is required for truetype and meaningless for opentype (where vaguely equivalent information is provided in the CFF table). - - `loca `__ + - `loca `__ - `loca `__ * - MATH - mathematical typesetting @@ -223,14 +223,14 @@ are essentially the same, while Apple's differ significantly from either. - Provides general "maximum" information about the font. This contains: the maximum number of glyphs in a font, the maximum number of points in a glyph, the maximum number of references in a glyph, etc. - - `maxp `__ + - `maxp `__ - `maxp `__ * - mort - metamorphosis - FontForge will read this table but not generate it. This table has been replaced by the 'morx' table which will be generated instead. It is vaguely equivalent to the GSUB table. - - `mort `__ + - `mort `__ - * * - morx - extended metamorphosis @@ -238,21 +238,21 @@ are essentially the same, while Apple's differ significantly from either. FontForge can read and write this table. See the :ref:`chapter on morx ` in FontForge for more information. Vaguely equivalent to 'GSUB'. - - `morx `__ + - `morx `__ - * * - name - name - Provides certain standard strings relevant to the font (font name, family name, license, etc.) in various languages. In Apple fonts also provides the names of various features of the 'morx'/'mort' table. - - `name `__ + - `name `__ - `name `__ * - opbd - optical bounds - This table provides optical bound information for each glyph in the table in an Apple font. This information may also be provided in the GPOS table of an opentype font. - - `opbd `__ + - `opbd `__ - * * - PfEd - FontForge's personal table @@ -264,19 +264,19 @@ are essentially the same, while Apple's differ significantly from either. - glyph name and postscript compatibility - This table provides some additional postscript information (italic angle), but mostly it provides names for all glyphs. - - `post `__ + - `post `__ - `post `__ * - prep - cvt program - FontForge never generatest this table itself, but it will retain it when reading other fonts, and it allows users to edit it. - - `prep `__ + - `prep `__ - `prep `__ * - prop - glyph properties - Provides the unicode properties of each glyph for an Apple font. This table bears some similarities to the GDEF table. - - `prop `__ + - `prop `__ - * * - TeX - TeX information @@ -288,13 +288,13 @@ are essentially the same, while Apple's differ significantly from either. - vertical header - FontForge generates this table for fonts with vertical metrics. This table contains font-wide vertical metric information. - - `vhea `__ + - `vhea `__ - `vhea `__ * - vmtx - vertical metrics - FontForge generates this table for fonts with vertical metrics. This contains the per-glyph vertical metrics. - - `vmtx `__ + - `vmtx `__ - `vmtx `__ :doc:`Advanced Typography tables in FontForge ` \ No newline at end of file diff --git a/doc/sphinx/techref/gposgsub.rst b/doc/sphinx/techref/gposgsub.rst index e5de5848eb..99a4060cd4 100644 --- a/doc/sphinx/techref/gposgsub.rst +++ b/doc/sphinx/techref/gposgsub.rst @@ -357,17 +357,17 @@ Apple Advanced Typography As above I do not go deeply into the abilities of these tables, for more information see Apple's docs: -* `The 'bsln' (baseline) table `__ -* `The 'kern' table `__ -* `The 'lcar' (ligature caret) table `__ -* `The 'morx' (extended glyph metamorphosis) table `__ +* `The 'bsln' (baseline) table `__ +* `The 'kern' table `__ +* `The 'lcar' (ligature caret) table `__ +* `The 'morx' (extended glyph metamorphosis) table `__ - * `The 'mort' (older version of 'morx') table `__ - * `The 'feat' (feature) table `__ + * `The 'mort' (older version of 'morx') table `__ + * `The 'feat' (feature) table `__ * `Apple's Font Feature registry `__ - * `Description of the subtables of the 'mort'/'morx' tables `__ -* `The 'opbd' (optical bounds) table `__ -* `The 'prop' (glyph properties) table `__ + * `Description of the subtables of the 'mort'/'morx' tables `__ +* `The 'opbd' (optical bounds) table `__ +* `The 'prop' (glyph properties) table `__ FontForge will currently read and produce (if Apple mode is set in font generation) the following tables: diff --git a/doc/sphinx/techref/hinting.rst b/doc/sphinx/techref/hinting.rst index 04b8d717b0..fe1cdc9664 100644 --- a/doc/sphinx/techref/hinting.rst +++ b/doc/sphinx/techref/hinting.rst @@ -213,10 +213,10 @@ Hint questions: I find the hint documentation inadequate for me to do a good job at hinting. * Section 2.4 of - `T1_Supp.pdf `__ + `T1_Supp.pdf `__ says that vertical counters are offset from the lbearing, but the example 2.6 shows them being offset from 0. -* In `Type2 `__ is +* In `Type2 `__ is it ok to use Counter mask if LanguageGroup is not 1 and the stems don't fit into a \*stem3 pattern? Or can cntrmask only be used for \*stem3 in latin letters? * .. image:: /images/NoPointHint.png diff --git a/doc/sphinx/ui/dialogs/fontinfo.rst b/doc/sphinx/ui/dialogs/fontinfo.rst index deaed02690..d66c5286d4 100644 --- a/doc/sphinx/ui/dialogs/fontinfo.rst +++ b/doc/sphinx/ui/dialogs/fontinfo.rst @@ -400,7 +400,7 @@ PANOSE This sub-dialog allows you to describe your font in 10 different dimensions. The exact meanings of many of these entries vary from script to script, and even the Latin ones are not clear to me, I have merely typed them in as specified in the -`true type docs `__. Better +`true type docs `__. Better information is available from `HP `__, `MS `__, `RS `__, @@ -584,7 +584,7 @@ The various strings and a brief description of their meanings are: Whatever. These are described in the -`original true type docs `__, +`original true type docs `__, but they apply to `open type `__ as well. diff --git a/doc/sphinx/ui/dialogs/multiplemaster.rst b/doc/sphinx/ui/dialogs/multiplemaster.rst index 32926525cd..cd469fb8d1 100644 --- a/doc/sphinx/ui/dialogs/multiplemaster.rst +++ b/doc/sphinx/ui/dialogs/multiplemaster.rst @@ -10,11 +10,11 @@ What is a multiple master font? ------------------------------- A multiple master font is a -(`PostScript Type1 `__) +(`PostScript Type1 `__) font which contains, in one font description several extreme styles of a font family and code for interpolating between them to produce intermediate designs. Apple has a -`similar concept `__ +`similar concept `__ for TrueType -- The only name I've seen given to this is "Apple Distortable Font" -- I call both multiple master. diff --git a/doc/sphinx/ui/dialogs/prefs.rst b/doc/sphinx/ui/dialogs/prefs.rst index 4edb3623d6..7c79e78d99 100644 --- a/doc/sphinx/ui/dialogs/prefs.rst +++ b/doc/sphinx/ui/dialogs/prefs.rst @@ -617,7 +617,7 @@ many languages as you like. The Mac Mapping dialog allows you to define a mapping between OTF ``GSUB`` feature tags and Apple's -`mort/morx `__ +`mort/morx `__ Feature/Setting codes. `Apple's published list `__ diff --git a/doc/sphinx/ui/dialogs/problems.rst b/doc/sphinx/ui/dialogs/problems.rst index 92670b1012..3216f98698 100644 --- a/doc/sphinx/ui/dialogs/problems.rst +++ b/doc/sphinx/ui/dialogs/problems.rst @@ -283,7 +283,7 @@ Refs Deeper Than ---------------- Appendix B of the the -`Type2 `__ spec +`Type2 `__ spec says that an interpreter is only required to support subroutine nesting up to 10 levels. FontForge uses subroutine calls to handle referenced glyphs and sometimes also to handle hinting. Hinting will take up a maximum of 1 level of @@ -369,7 +369,7 @@ More Hints Than --------------- Appendix B of the the -`Type2 `__ spec +`Type2 `__ spec says that an interpreter is only required to support a total of 96 horizontal and vertical hints. diff --git a/doc/sphinx/ui/dialogs/statemachine.rst b/doc/sphinx/ui/dialogs/statemachine.rst index d662e8f901..29eff1d712 100644 --- a/doc/sphinx/ui/dialogs/statemachine.rst +++ b/doc/sphinx/ui/dialogs/statemachine.rst @@ -106,14 +106,14 @@ For a complete description of what these state machines can do you should read Apple's documentation * On the - `'morx' `__ and - `'mort' `__ + `'morx' `__ and + `'mort' `__ tables * On the - `'kern' `__ table + `'kern' `__ table (format 1) -* `On state machines in general `__ -* `The 'feat' (feature) table `__ +* `On state machines in general `__ +* `The 'feat' (feature) table `__ * `Apple's Font Feature registry `__ The general idea is that a word processor converts text into a string of glyphs, diff --git a/doc/sphinx/ui/dialogs/ttfinstrs.rst b/doc/sphinx/ui/dialogs/ttfinstrs.rst index e9d89751c4..1acacdda56 100644 --- a/doc/sphinx/ui/dialogs/ttfinstrs.rst +++ b/doc/sphinx/ui/dialogs/ttfinstrs.rst @@ -5,10 +5,10 @@ Viewing and Editing truetype instructions in FontForge. .. image:: /images/ttfinstrs-edit.png -* `Apple's overview on instructing glyphs `__ -* `Apple's list of truetype instructions `__ -* `Apple's description of the truetype graphics state `__ -* `Apple's summary of truetype instructions `__ +* `Apple's overview on instructing glyphs `__ +* `Apple's list of truetype instructions `__ +* `Apple's description of the truetype graphics state `__ +* `Apple's summary of truetype instructions `__ I shall not attempt to describe the mechanics of instructing truetype, if you want to understand it I refer you to the above links. diff --git a/fontforge/parsettf.c b/fontforge/parsettf.c index 381eccee02..522124e84d 100644 --- a/fontforge/parsettf.c +++ b/fontforge/parsettf.c @@ -66,7 +66,7 @@ int ask_user_for_cmap = false; /* True Type is a really icky format. Nothing is together. It's badly described */ /* much of the description is misleading */ /* Apple's version: */ -/* http://fonts.apple.com/TTRefMan/index.html */ +/* http://fonts.apple.com/TrueType-Reference-Manual/index.html */ /* MS's version: */ /* http://www.microsoft.com/typography/tt/tt.htm */ /* An helpful but incomplete description is given at */ @@ -2110,7 +2110,7 @@ return; /* site about how these should be interpreted when there are */ /* scale factors, or rotations */ /* It isn't well enough described to be comprehensible */ - /* http://fonts.apple.com/TTRefMan/RM06/Chap6glyf.html */ + /* http://fonts.apple.com/TrueType-Reference-Manual/RM06/Chap6glyf.html */ /* Microsoft says nothing about this */ /* Adobe implies this is a difference between MS and Apple */ /* MS doesn't do this, Apple does (GRRRGH!!!!) */ diff --git a/fontforge/parsettfvar.c b/fontforge/parsettfvar.c index fe928ab0a8..c09999eea1 100644 --- a/fontforge/parsettfvar.c +++ b/fontforge/parsettfvar.c @@ -45,7 +45,7 @@ /***************************************************************************/ /* */ /* Apple documents these tables at */ -/* http://developer.apple.com/fonts/TTRefMan/RM06/Chap6[fgca]var.html */ +/* http://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6[fgca]var.html */ /* The documentation for fvar is inconsistent. At one point it says that */ /* countSizePairs should be 3, at another point 2. It should be 2. */ /* The documentation for gvar is not intelligible, cvar refers you to gvar */ diff --git a/fontforge/tottf.c b/fontforge/tottf.c index 3b53cf21ac..92d9f1137d 100644 --- a/fontforge/tottf.c +++ b/fontforge/tottf.c @@ -1147,7 +1147,7 @@ static void dumpcomposite(SplineChar *sc, struct glyphinfo *gi) { /* scale factors, or rotations */ /* That description does not match the behavior of their rasterizer*/ /* I've reverse engineered something else (see parsettf.c) */ - /* http://fonts.apple.com/TTRefMan/RM06/Chap6glyf.html */ + /* http://fonts.apple.com/TrueType-Reference-Manual/RM06/Chap6glyf.html */ /* Adobe says that setting bit 12 means that this will not happen */ /* Apple doesn't mention bit 12 though...(but they do support it) */ } From 9565a274e00fa2491984c4ce0c45c395dc8545b3 Mon Sep 17 00:00:00 2001 From: MihailJP Date: Mon, 15 Dec 2025 06:42:59 +0900 Subject: [PATCH 31/96] At UFO export, use family name for styleMapFamilyName (#5672) * Drop SplineFont::styleMapFamilyName * Add SFD_GetFontMetaDataData::lastStyleMapFamilyName --- fontforge/sfd.c | 20 +++++++++++++++----- fontforge/sfd.h | 1 + fontforge/splinefont.h | 1 - fontforge/splineutil.c | 1 - fontforge/ufo.c | 21 ++++----------------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/fontforge/sfd.c b/fontforge/sfd.c index aca965eef5..6b980a4785 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -2254,8 +2254,6 @@ int SFD_DumpSplineFontMetadata( FILE *sfd, SplineFont *sf ) if ( sf->version!=NULL ) fprintf(sfd, "Version: %s\n", sf->version ); - if ( sf->styleMapFamilyName!=NULL ) - fprintf(sfd, "StyleMapFamilyName: %s\n", sf->styleMapFamilyName ); if ( sf->fondname!=NULL ) fprintf(sfd, "FONDName: %s\n", sf->fondname ); if ( sf->defbasefilename!=NULL ) @@ -7691,13 +7689,11 @@ bool SFD_GetFontMetaData( FILE *sfd, } else if ( strmatch(tok,"StyleMapFamilyName:")==0 ) { - sf->styleMapFamilyName = SFDReadUTF7Str(sfd); + d->lastStyleMapFamilyName = SFDReadUTF7Str(sfd); } /* Legacy attribute for StyleMapFamilyName. Deprecated. */ else if ( strmatch(tok,"OS2FamilyName:")==0 ) { - if (sf->styleMapFamilyName == NULL) - sf->styleMapFamilyName = SFDReadUTF7Str(sfd); } else if ( strmatch(tok,"FONDName:")==0 ) { @@ -7738,7 +7734,21 @@ bool SFD_GetFontMetaData( FILE *sfd, } else if ( strmatch(tok,"LangName:")==0 ) { + struct ttflangname *english; + sf->names = SFDGetLangName(sfd,sf->names); + + for ( english=sf->names; english!=NULL && english->lang!=0x409; english=english->next ); + if ( english && d->lastStyleMapFamilyName ) { + if ( english->names[ttf_family] ) { + LogError(_("'StyleMapFamilyName' entry has been ignored") ); + free(d->lastStyleMapFamilyName); + } + else { + english->names[ttf_family] = d->lastStyleMapFamilyName; + } + d->lastStyleMapFamilyName = NULL; + } } else if ( strmatch(tok,"GaspTable:")==0 ) { diff --git a/fontforge/sfd.h b/fontforge/sfd.h index caf9838fb4..017d15522d 100644 --- a/fontforge/sfd.h +++ b/fontforge/sfd.h @@ -25,6 +25,7 @@ typedef struct sfd_getfontmetadatadata { FPST* lastfp; ASM* lastsm; struct ttf_table* lastttf[2]; + char* lastStyleMapFamilyName; } SFD_GetFontMetaDataData; extern void SFD_GetFontMetaDataData_Init(SFD_GetFontMetaDataData* d); diff --git a/fontforge/splinefont.h b/fontforge/splinefont.h index 4a1bd99a8c..dae2a530f3 100644 --- a/fontforge/splinefont.h +++ b/fontforge/splinefont.h @@ -1343,7 +1343,6 @@ typedef struct splinefont { char *woffMetadata; real ufo_ascent, ufo_descent; /* I don't know what these mean, they don't seem to correspond to any other ascent/descent pair, but retain them so round-trip ufo input/output leaves them unchanged */ /* ufo_descent is negative */ - char *styleMapFamilyName; struct sfundoes *undoes; int preferred_kerning; // 1 for U. F. O. native, 2 for feature file, 0 undefined. Input functions shall flag 2, I think. This is now in S. F. D. in order to round-trip U. F. O. consistently. } SplineFont; diff --git a/fontforge/splineutil.c b/fontforge/splineutil.c index 40ba51dd5f..d7c3979d9b 100644 --- a/fontforge/splineutil.c +++ b/fontforge/splineutil.c @@ -6584,7 +6584,6 @@ return; free(sf->xuid); free(sf->cidregistry); free(sf->ordering); - if ( sf->styleMapFamilyName && sf->styleMapFamilyName[0]!='\0' ) { free(sf->styleMapFamilyName); sf->styleMapFamilyName = NULL; } MacFeatListFree(sf->features); /* We don't free the EncMap. That field is only a temporary pointer. Let the FontViewBase free it, that's where it really lives */ // TODO: But that doesn't always get freed. The statement below causes double-frees, so we need to come up with better conditions. diff --git a/fontforge/ufo.c b/fontforge/ufo.c index d686333593..a45bdf4f62 100644 --- a/fontforge/ufo.c +++ b/fontforge/ufo.c @@ -1170,19 +1170,8 @@ static int UFOOutputFontInfo(const char *basedir, SplineFont *sf, int layer, int if (styleNameSynthetic) PListAddString(dictnode,"styleName",styleNameSynthetic); { - char* preferredFamilyName = fetchTTFAttribute(sf,ttf_preffamilyname); - char* preferredSubfamilyName = fetchTTFAttribute(sf,ttf_prefmodifiers); - char* styleMapFamily = NULL; - if (sf->styleMapFamilyName != NULL) { - /* Empty styleMapStyleName means we imported a UFO that does not have this field. Bypass the fallback. */ - if (sf->styleMapFamilyName[0]!='\0') - styleMapFamily = sf->styleMapFamilyName; - } else if (preferredFamilyName != NULL && preferredSubfamilyName != NULL) { - styleMapFamily = malloc(strlen(preferredFamilyName)+strlen(preferredSubfamilyName)+2); - strcpy(styleMapFamily, preferredFamilyName); - strcat(styleMapFamily, " "); - strcat(styleMapFamily, preferredSubfamilyName); - } else if (sf->fullname != NULL) styleMapFamily = sf->fullname; + char* styleMapFamily = fetchTTFAttribute(sf,ttf_family); + if (styleMapFamily == NULL && sf->familyname != NULL) styleMapFamily = sf->familyname; if (styleMapFamily != NULL) PListAddString(dictnode,"styleMapFamilyName", styleMapFamily); } { @@ -3840,8 +3829,8 @@ SplineFont *SFReadUFO(char *basedir, int flags) { else free(valname); } else if ( xmlStrcmp(keyname,(xmlChar *) "styleMapFamilyName")==0 ) { - if (sf->styleMapFamilyName == NULL) sf->styleMapFamilyName = (char *) valname; - else free(valname); + if (strcmp((char *) valname, sf->familyname) != 0) + UFOAddName(sf,(char *) valname, ttf_family); } else if ( xmlStrcmp(keyname,(xmlChar *) "styleMapStyleName")==0 ) { if (strcmp((char *) valname, "regular")==0) sf->pfminfo.stylemap = 0x40; @@ -4111,8 +4100,6 @@ SplineFont *SFReadUFO(char *basedir, int flags) { if ( sf->familyname==NULL ) sf->familyname = copy(sf->fontname); free(stylename); stylename = NULL; - if (sf->styleMapFamilyName == NULL) - sf->styleMapFamilyName = ""; // Empty default to disable fallback at export (not user-accessible anyway as of now). if ( sf->weight==NULL ) sf->weight = copy("Regular"); // We can now free the document. From 3a960a4d83c65b8ff5fc0bab8b1f19964aa023df Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Mon, 22 Dec 2025 21:40:36 +0200 Subject: [PATCH 32/96] Fix glyph.codepoint attribute (#5709) --- doc/sphinx/scripting/python/fontforge.rst | 3 ++- fontforge/python.c | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index 8f32ebc4f9..ee10724d24 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -1703,7 +1703,8 @@ must be created through the font. .. attribute:: glyph.codepoint - Unicode code point for this glyph in U+XXXX format, or ``None``. (readonly) + Unicode code point for this glyph in U+XXXX format for the Basic Multilingual + Plane, and up to U+XXXXXX for the supplementary planes, or ``None``. (readonly) .. attribute:: glyph.color diff --git a/fontforge/python.c b/fontforge/python.c index c3228c63ae..d4b8025589 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -6984,9 +6984,13 @@ return( Py_BuildValue("i", map->backmap[sc->orig_pos] )); } static PyObject *PyFF_Glyph_get_codepoint(PyFF_Glyph *self, SplineChar *sc, void *UNUSED(closure)) { + char buffer[12]; if ( sc==NULL || sc->unicodeenc < 0 ) -Py_RETURN_NONE; - return PyUnicode_FromFormat("U+%04X", sc->unicodeenc); + Py_RETURN_NONE; + + /* PyUnicode_FromFormat() needs Python 3.12+ */ + sprintf(buffer, "U+%04X", sc->unicodeenc); + return PyUnicode_FromString(buffer); } static PyObject *PyFF_Glyph_get_unicode(PyFF_Glyph *self, SplineChar *sc, void *UNUSED(closure)) { From 7d67700cf8888e0bb37b453ad54ed932c8587073 Mon Sep 17 00:00:00 2001 From: Ahmet Furkan Kavraz <55850855+ahmetfurkankavraz@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:47:43 +0100 Subject: [PATCH 33/96] Fix CVE-2025-15279: Heap buffer overflow in BMP RLE decompression (#5720) CVSS: 7.8 (High) ZDI-CAN-27517 Co-authored-by: Ahmet Furkan Kavraz --- gutils/gimagereadbmp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gutils/gimagereadbmp.c b/gutils/gimagereadbmp.c index 5a137e28af..133336787c 100644 --- a/gutils/gimagereadbmp.c +++ b/gutils/gimagereadbmp.c @@ -181,12 +181,18 @@ static int readpixels(FILE *file,struct bmpheader *head) { int ii = 0; while ( iiheight*head->width ) { int cnt = getc(file); + if (cnt < 0 || ii + cnt > head->height * head->width) { + return 0; + } if ( cnt!=0 ) { int ch = getc(file); while ( --cnt>=0 ) head->byte_pixels[ii++] = ch; } else { cnt = getc(file); + if (cnt < 0 || ii + cnt > head->height * head->width) { + return 0; + } if ( cnt>= 3 ) { int odd = cnt&1; while ( --cnt>=0 ) From 7195402701ace7783753ef9424153eff48c9af44 Mon Sep 17 00:00:00 2001 From: Ahmet Furkan Kavraz <55850855+ahmetfurkankavraz@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:58:23 +0100 Subject: [PATCH 34/96] Fix CVE-2025-15275: Heap buffer overflow in SFD image parsing (#5721) Fixes: CVE-2025-15275 | ZDI-25-1189 | ZDI-CAN-28543 Co-authored-by: Ahmet Furkan Kavraz --- fontforge/sfd.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fontforge/sfd.c b/fontforge/sfd.c index 6b980a4785..0590c119f3 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -3653,6 +3653,10 @@ static ImageList *SFDGetImage(FILE *sfd) { getint(sfd,&image_type); getint(sfd,&bpl); getint(sfd,&clutlen); + if ( clutlen < 0 || clutlen > 256 ) { + LogError(_("Invalid clut length %d in sfd file, must be between 0 and 256"), clutlen); + return NULL; + } gethex(sfd,&trans); image = GImageCreate(image_type,width,height); base = image->list_len==0?image->u.image:image->u.images[0]; From 6aea6db5da332d8ac94e3501bb83c1b21f52074d Mon Sep 17 00:00:00 2001 From: Ahmet Furkan Kavraz <55850855+ahmetfurkankavraz@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:06:53 +0100 Subject: [PATCH 35/96] Fix CVE-2025-15269: Use-after-free in SFD ligature parsing (#5722) Prevent circular linked list in LigaCreateFromOldStyleMultiple by clearing the next pointer after shallow copy. The shallow copy propagates liga's modified next pointer from previous iterations, creating a cycle that causes double-free when the list is traversed and freed. Fixes: CVE-2025-15269 | ZDI-25-1195 | ZDI-CAN-28564 Co-authored-by: Ahmet Furkan Kavraz --- fontforge/sfd.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fontforge/sfd.c b/fontforge/sfd.c index 0590c119f3..a349d0b2f2 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -4715,6 +4715,7 @@ static PST1 *LigaCreateFromOldStyleMultiple(PST1 *liga) { while ( (pt = strrchr(liga->pst.u.lig.components,';'))!=NULL ) { new = chunkalloc(sizeof( PST1 )); *new = *liga; + new->pst.next = NULL; new->pst.u.lig.components = copy(pt+1); last->pst.next = (PST *) new; last = new; From e9474d087aa4c179f672d454de1e6defd0e76e92 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sun, 11 Jan 2026 22:58:53 +0200 Subject: [PATCH 36/96] Fix Python hooks, test_font_hooks.py (#5724) --- fontforge/python.c | 31 ++++++++++++++++++++++--------- tests/CMakeLists.txt | 1 + tests/test_font_hooks.py | 12 ++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 tests/test_font_hooks.py diff --git a/fontforge/python.c b/fontforge/python.c index d4b8025589..63e0a16780 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -187,7 +187,6 @@ static struct flaglist sfnt_name_mslangs[]; /*SplineChar *sc_active_in_ui = NULL;*/ /*int layer_active_in_ui = ly_fore;*/ -static PyObject *hook_dict; /* Dictionary of python hook scripts (to be activated when certain fontforge events happen) */ static PyObject *pickler, *unpickler; /* cPickle.dumps, cPickle.loads */ static PyObject *_new_point, *_new_contour, *_new_layer; /* Python handles to c functions, needed for pickler */ static void PyFF_PickleTypesInit(void); @@ -20788,6 +20787,13 @@ static void CreateAllPyModules(void) { CreatePyModule( all_modules[i] ); } +static PyObject* GetRuntimePyModule(const char* module_name) { + PyObject* module = NULL; + for (unsigned i = 0; i < NUM_MODULES; i++) + if (strcmp(all_modules[i]->module_name, module_name) == 0) + module = all_modules[i]->runtime.module; + return module; +} static void RegisterAllPyModules(void) { /* This adds all the modules to Python's 'builtin' module list. @@ -21222,6 +21228,7 @@ void PyFF_InitFontHook(FontViewBase *fv) { /* We have not added a window or menu to it yet */ SplineFont *sf = fv->sf; PyObject *obj; + PyObject *fontforge_module = GetRuntimePyModule("fontforge"); if ( fv->nextsame!=NULL ) /* Duplicate window looking at previously loaded font */ return; @@ -21245,10 +21252,19 @@ return; } Py_XDECREF(obj); - if ( sf->new ) - PyFF_CallDictFunc(hook_dict,"newFontHook","f", fv ); - else - PyFF_CallDictFunc(hook_dict,"loadFontHook","f", fv ); + /* Uninitialized module probably means we are running a native script. */ + if (!fontforge_module) return; + + if (PyObject_HasAttrString(fontforge_module, "hooks")) { + PyObject* hook_dict = PyObject_GetAttrString(fontforge_module, "hooks"); + if (sf->new) + PyFF_CallDictFunc(hook_dict, "newFontHook", "f", fv); + else + PyFF_CallDictFunc(hook_dict, "loadFontHook", "f", fv); + Py_DECREF(hook_dict); + } else { + LogError(_("FontForge Python hooks not found")); + } } @@ -21276,10 +21292,7 @@ PyMODINIT_FUNC fontforge_python_init(const char* modulename) { initted = true; } - for ( unsigned i=0; imodule_name, modulename)==0 ) - return all_modules[i]->runtime.module; - return NULL; + return GetRuntimePyModule(modulename); } #else diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18b1250c4a..8ceb6c16e5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -141,4 +141,5 @@ if(ENABLE_PYTHON_SCRIPTING_RESULT) add_py_test(test_sfnt_name.py "Ambrosia.sfd" "Testing font.appendSFNTName() function") add_py_test(test_logging.py "Sanitization of logging functions") add_py_test(test_font_mark_classes_and_sets.py "font.markClasses, font.markSets getters and setters") + add_py_test(test_font_hooks.py "Set and activate Python hooks") endif() diff --git a/tests/test_font_hooks.py b/tests/test_font_hooks.py new file mode 100644 index 0000000000..64824abfe7 --- /dev/null +++ b/tests/test_font_hooks.py @@ -0,0 +1,12 @@ +import fontforge + +def on_new_font(new_font): + new_font.createChar(-1, "dummy_char") + +fontforge.hooks["newFontHook"] = on_new_font + +# Create a new font. We expect to find the dummy character which was created +# by the hook. +font = fontforge.font() +char_idx = font.findEncodingSlot("dummy_char") +assert char_idx != -1 From 720ea95020c964202928afd2e93b0f5fac11027e Mon Sep 17 00:00:00 2001 From: Ahmet Furkan Kavraz <55850855+ahmetfurkankavraz@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:45:16 +0100 Subject: [PATCH 37/96] Fix CVE-2025-15279: Move bounds check inside cnt >= 3 block (#5723) Co-authored-by: Ahmet Furkan Kavraz --- gutils/gimagereadbmp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gutils/gimagereadbmp.c b/gutils/gimagereadbmp.c index 133336787c..ad365158cc 100644 --- a/gutils/gimagereadbmp.c +++ b/gutils/gimagereadbmp.c @@ -190,10 +190,10 @@ static int readpixels(FILE *file,struct bmpheader *head) { head->byte_pixels[ii++] = ch; } else { cnt = getc(file); - if (cnt < 0 || ii + cnt > head->height * head->width) { - return 0; - } if ( cnt>= 3 ) { + if (ii + cnt > head->height * head->width) { + return 0; + } int odd = cnt&1; while ( --cnt>=0 ) head->byte_pixels[ii++] = getc(file); From 0df57ac0d44fbe668f33a57593f94ba6c987a440 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Tue, 13 Jan 2026 19:10:56 +0200 Subject: [PATCH 38/96] Fix crash for content over 32767 characters in GDraw multiline text field (#5728) --- gdraw/ggadgetP.h | 12 ++++++------ gdraw/gtextfield.c | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gdraw/ggadgetP.h b/gdraw/ggadgetP.h index 85b67ea06d..b2115fc909 100644 --- a/gdraw/ggadgetP.h +++ b/gdraw/ggadgetP.h @@ -314,19 +314,19 @@ typedef struct gtextfield { uint8_t fh; uint8_t as; uint8_t nw; /* Width of one character (an "n") */ - int16_t xoff_left, loff_top; - int16_t sel_start, sel_end, sel_base; - int16_t sel_oldstart, sel_oldend, sel_oldbase; - int16_t dd_cursor_pos; + int32_t xoff_left, loff_top; + int32_t sel_start, sel_end, sel_base; + int32_t sel_oldstart, sel_oldend, sel_oldbase; + int32_t dd_cursor_pos; unichar_t *text, *oldtext; FontInstance *font; GTimer *pressed; GTimer *cursor; GCursor old_cursor; GScrollBar *hsb, *vsb; - int16_t lcnt, lmax; + int32_t lcnt, lmax; int32_t *lines; /* offsets in text to the start of the nth line */ - int16_t xmax; + int32_t xmax; GIC *gic; GTimer *numeric_scroll; char *utf8_text; /* For Pango */ diff --git a/gdraw/gtextfield.c b/gdraw/gtextfield.c index 19b16274c6..f56a9d6a67 100644 --- a/gdraw/gtextfield.c +++ b/gdraw/gtextfield.c @@ -633,7 +633,7 @@ static int GTextFieldSelForeword(unichar_t *text,int end) { return( end ); } -static void GTextFieldSelectWord(GTextField *gt,int mid, int16_t *start, int16_t *end) { +static void GTextFieldSelectWord(GTextField *gt,int mid, int32_t *start, int32_t *end) { unichar_t *text; unichar_t ch = gt->text[mid]; @@ -664,7 +664,7 @@ static void GTextFieldSelectWord(GTextField *gt,int mid, int16_t *start, int16_t } static void GTextFieldSelectWords(GTextField *gt,int last) { - int16_t ss, se; + int32_t ss, se; GTextFieldSelectWord(gt,gt->sel_base,>->sel_start,>->sel_end); if ( last!=gt->sel_base ) { GTextFieldSelectWord(gt,last,&ss,&se); @@ -2185,7 +2185,7 @@ static void gtextfield_redraw(GGadget *g) { static void gtextfield_move(GGadget *g, int32_t x, int32_t y ) { GTextField *gt = (GTextField *) g; - int fxo=0, fyo=0, bxo, byo; + int fxo=0, fyo=0, bxo=0, byo=0; if ( gt->listfield || gt->numericfield ) { fxo = ((GListField *) gt)->fieldrect.x - g->r.x; @@ -2209,7 +2209,7 @@ static void gtextfield_move(GGadget *g, int32_t x, int32_t y ) { static void gtextfield_resize(GGadget *g, int32_t width, int32_t height ) { GTextField *gt = (GTextField *) g; int gtwidth=width, gtheight=height, oldheight=0; - int fxo=0, fwo=0, fyo=0, bxo, byo; + int fxo=0, fwo=0, fyo=0, bxo=0, byo=0; int l; if ( gt->listfield || gt->numericfield ) { From 7bd768af596f0eeef521fc25733deb64d4c07a3d Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Fri, 16 Jan 2026 22:38:01 +0200 Subject: [PATCH 39/96] Add CMake target to autoformat source files (#5730) --- .clang-files | 10 ++++++++++ CMakeLists.txt | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 .clang-files diff --git a/.clang-files b/.clang-files new file mode 100644 index 0000000000..c2c7cec6d9 --- /dev/null +++ b/.clang-files @@ -0,0 +1,10 @@ +inc/tag.hpp +fontforge/metrics.h +fontforge/splinechar.h +fontforge/splinefont_enums.h +fontforge/std_maps.cpp +fontforge/std_maps.hpp +fontforgeexe/shapers/*.cpp +fontforgeexe/shapers/*.hpp +fontforgeexe/gtk/*.cpp +fontforgeexe/gtk/*.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7280ea8ad4..e5f0290871 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,3 +202,10 @@ endif() setup_cpack() print_build_options() + +add_custom_target( + format + COMMAND cat .clang-files | xargs -I % sh -c 'clang-format --verbose -i --style=file:.clang-format %' + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running clang-format" +) From dfe5c8035d716e99ebd0fdb1c49ea623d94737a7 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Mon, 19 Jan 2026 22:45:20 +0200 Subject: [PATCH 40/96] Fix multiple crashes in Multiple Masters (#5733) * Fix corruption in SFMakeChar() for MM fonts * Crash when opening MM font with kerning * Fix crash for missing MM Mac name --- Unicode/memory.c | 7 +++++++ Unicode/ustring.c | 2 +- fontforge/encoding.c | 40 ++++++++++++++++++++++++++-------------- fontforge/sfd.c | 4 +++- fontforgeexe/mmdlg.c | 6 +++--- inc/basics.h | 2 ++ 6 files changed, 42 insertions(+), 19 deletions(-) diff --git a/Unicode/memory.c b/Unicode/memory.c index 774b138fd1..1613f77300 100644 --- a/Unicode/memory.c +++ b/Unicode/memory.c @@ -39,6 +39,13 @@ void NoMoreMemMessage(void) { fprintf(stderr, "Out of memory\n" ); } +void ExpandBuffer(void** p_buf, size_t elem_size, size_t increment, + int* p_maxalloc) { + *p_buf = realloc(*p_buf, (*p_maxalloc + increment) * elem_size); + memset((char*)*p_buf + (*p_maxalloc) * elem_size, 0, increment * elem_size); + *p_maxalloc += increment; +} + char *copy(const char *str) { return str ? strdup(str) : NULL; } diff --git a/Unicode/ustring.c b/Unicode/ustring.c index b0fe3387ef..893eeedf23 100644 --- a/Unicode/ustring.c +++ b/Unicode/ustring.c @@ -535,7 +535,7 @@ unichar_t *utf82u_strncpy(unichar_t *ubuf,const char *utf8buf,int len) { } unichar_t *utf82u_strcpy(unichar_t *ubuf,const char *utf8buf) { -return( utf82u_strncpy(ubuf,utf8buf,strlen(utf8buf)+1)); +return( utf82u_strncpy(ubuf,utf8buf,c_strlen(utf8buf)+1)); } unichar_t *utf82u_copyn(const char *utf8buf,int len) { diff --git a/fontforge/encoding.c b/fontforge/encoding.c index 194ecd035a..6eb9c6dc7f 100644 --- a/fontforge/encoding.c +++ b/fontforge/encoding.c @@ -1233,7 +1233,7 @@ enum cmaptype { cmt_out=-1, cmt_coderange, cmt_notdefs, cmt_cid, cmt_max }; struct coderange { uint32_t first, last, cid; }; struct cmap { struct { - int n; + size_t n; struct coderange *ranges; } groups[cmt_max]; char *registry; @@ -1253,7 +1253,7 @@ static void cmapfree(struct cmap *cmap) { free(cmap); } -static struct coderange *ExtendArray(struct coderange *ranges,int *n, int val) { +static struct coderange *ExtendArray(struct coderange *ranges, size_t *n, int val) { if ( *n == 0 ) ranges = calloc(val,sizeof(struct coderange)); else { @@ -1280,7 +1280,8 @@ static struct cmap *ParseCMap(char *filename) { FILE *file; struct cmap *cmap; char *end, *pt; - int val, pos; + int val; + size_t pos; enum cmaptype in; int in_is_single; // We set this if we are to parse cidchars into cidranges. static const char *bcsr = "begincodespacerange", *bndr = "beginnotdefrange", *bcr = "begincidrange", *bcc = "begincidchar"; @@ -2363,21 +2364,32 @@ return( any ); } void SFAddGlyphAndEncode(SplineFont *sf,SplineChar *sc,EncMap *basemap, int baseenc) { - int gid, mapfound = false; + int j, gid, mapfound = false; FontViewBase *fv; BDFFont *bdf; if ( sf->cidmaster==NULL ) { - if ( sf->glyphcnt+1>=sf->glyphmax ) - sf->glyphs = realloc(sf->glyphs,(sf->glyphmax+=10)*sizeof(SplineChar *)); - gid = sf->glyphcnt++; - for ( bdf = sf->bitmaps; bdf!=NULL; bdf=bdf->next ) { - if ( sf->glyphcnt+1>=bdf->glyphmax ) - bdf->glyphs = realloc(bdf->glyphs,(bdf->glyphmax=sf->glyphmax)*sizeof(BDFChar *)); - if ( sf->glyphcnt>bdf->glyphcnt ) { - memset(bdf->glyphs+bdf->glyphcnt,0,(sf->glyphcnt-bdf->glyphcnt)*sizeof(BDFChar *)); - bdf->glyphcnt = sf->glyphcnt; - } + if (sf->glyphcnt + 1 > sf->glyphmax) + ExpandBuffer((void**)&sf->glyphs, sizeof(SplineChar*), 10, + &sf->glyphmax); + gid = sf->glyphcnt++; + for (bdf = sf->bitmaps; bdf != NULL; bdf = bdf->next) { + if (sf->glyphcnt + 1 > bdf->glyphmax) + ExpandBuffer((void**)&bdf->glyphs, sizeof(BDFChar*), + sf->glyphmax - bdf->glyphmax, &bdf->glyphmax); + } + if (sf->mm != NULL) { + if (sf->mm->normal->glyphmax < sf->glyphmax) + ExpandBuffer((void**)&sf->mm->normal->glyphs, + sizeof(SplineChar*), + sf->glyphmax - sf->mm->normal->glyphmax, + &sf->mm->normal->glyphmax); + for (j = 0; j < sf->mm->instance_count; ++j) + if (sf->mm->instances[j]->glyphmax < sf->glyphmax) + ExpandBuffer((void**)&sf->mm->instances[j]->glyphs, + sizeof(SplineChar*), + sf->glyphmax - sf->mm->instances[j]->glyphmax, + &sf->mm->instances[j]->glyphmax); } for ( fv=sf->fv; fv!=NULL; fv = fv->nextsame ) { EncMap *map = fv->map; diff --git a/fontforge/sfd.c b/fontforge/sfd.c index a349d0b2f2..f3c84080da 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -9014,7 +9014,9 @@ exit( 1 ); } } } - if ( sf->cidmaster==NULL ) + + /* MM font has already been already fixed up. */ + if (sf->cidmaster == NULL && sf->mm == NULL) SFDFixupRefs(sf); if ( !haddupenc ) diff --git a/fontforgeexe/mmdlg.c b/fontforgeexe/mmdlg.c index 1baf9ffda1..540afdd1cd 100644 --- a/fontforgeexe/mmdlg.c +++ b/fontforgeexe/mmdlg.c @@ -1632,7 +1632,7 @@ return( NULL ); pt[-1] = ']'; ustyle = PickNameFromMacName(mmw->old->named_instances[i].names); ti[i].bg = ti[i].fg = COLOR_DEFAULT; - ti[i].text = malloc((strlen(buffer)+3+strlen(ustyle))*sizeof(unichar_t)); + ti[i].text = calloc(strlen(buffer)+3+c_strlen(ustyle), sizeof(unichar_t)); utf82u_strcpy(ti[i].text,ustyle); uc_strcat(ti[i].text," "); uc_strcat(ti[i].text,buffer); @@ -1762,9 +1762,9 @@ static void MMW_ParseNamedStyles(MMSet *setto,MMW *mmw) { static void MMW_DoOK(MMW *mmw) { real weights[AppleMmMax+1]; - real fbt; + real fbt = 0.; int err = false; - char *familyname, *fn, *origname=NULL; + char *familyname, *fn=NULL, *origname=NULL; int i,j; MMSet *setto, *dlgmm; FontView *fv = NULL; diff --git a/inc/basics.h b/inc/basics.h index 86f0db7129..9204285d5f 100644 --- a/inc/basics.h +++ b/inc/basics.h @@ -73,6 +73,8 @@ typedef uint32_t unichar_t; #endif extern void NoMoreMemMessage(void); +extern void ExpandBuffer(void** p_buf, size_t elem_size, size_t increment, + int* p_maxalloc); static inline int imin(int a, int b) { From 7f71b51d248e38687c2a76384071234c62296656 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Fri, 23 Jan 2026 20:57:09 +0200 Subject: [PATCH 41/96] Improve bulk_test.sh script (#5714) * Preserve output of failed cases * More testing environments * Add slavonic fonts to testing suite --- tests/bulk_test.sh | 38 +++++++++++++++++++++++++++++++--- tests/fonts/PublicFontList.txt | 15 +++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/tests/bulk_test.sh b/tests/bulk_test.sh index e24d52bc32..25dd91233c 100644 --- a/tests/bulk_test.sh +++ b/tests/bulk_test.sh @@ -15,11 +15,33 @@ FF_BIN_PROJECT=../build/bin/fontforge FF_PY_SCRIPT_LINE="f=fontforge.open(argv[1]); f.save(argv[2])" FF_OUTPUT_FILE="Output.sfd" +# FF_PY_SCRIPT_LINE="f=fontforge.open(argv[1]); f.generate(argv[2], flags='no-FFTM-table')" +# FF_OUTPUT_FILE="Output.ttf" + +# FF_PY_SCRIPT_LINE="\ +# f=fontforge.open(argv[1]);\ +# f.generateFeatureFile('Out.fea')"$'\n'"\ +# for l in f.gsub_lookups: f.removeLookup(l)"$'\n'"\ +# for l in f.gpos_lookups: f.removeLookup(l)"$'\n'"\ +# f.mergeFeature('Out.fea'); f.save(argv[2])" +# FF_OUTPUT_FILE="Output.sfd" + +# # NOTE: Zeroize "CreationTime" and "ModificationTime" in SFD_DumpSplineFontMetadata() +# FF_PY_SCRIPT_LINE="\ +# f=fontforge.open(argv[1]);\ +# f.cidConvertByCmap('/home/iorsh/devel/cmap-resources/Adobe-Identity-0/CMap/Identity-H')"$'\n'"\ +# f.generateFeatureFile('Out.fea')"$'\n'"\ +# for l in f.gsub_lookups: f.removeLookup(l)"$'\n'"\ +# for l in f.gpos_lookups: f.removeLookup(l)"$'\n'"\ +# f.mergeFeature('Out.fea'); f.save(argv[2])" +# FF_OUTPUT_FILE="Output.sfd" + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) FF_BIN_PROJECT=$SCRIPT_DIR/$FF_BIN_PROJECT cat $FONT_LIST | while read font_url; do + failure=0 TMP_DIR_SYS=`mktemp -d` pushd $TMP_DIR_SYS > /dev/null if (wget -q --timeout=60 $font_url); then @@ -36,17 +58,27 @@ while read font_url; do popd > /dev/null if [ ! -f $TMP_DIR_SYS/$FF_OUTPUT_FILE ] && [ ! -f $TMP_DIR_PROJ/$FF_OUTPUT_FILE ]; then - echo "NO OUTPUT: $font_url" + echo "\e[0;33mNO OUTPUT\e[0m: $font_url" + failure=1 elif (cmp --quiet $TMP_DIR_SYS/$FF_OUTPUT_FILE $TMP_DIR_PROJ/$FF_OUTPUT_FILE); then echo "PASS: $font_url" else echo -e "\e[0;31mFAIL\e[0m: $font_url" + failure=1 fi - rm -rf $TMP_DIR_PROJ + if [ $failure -eq 0 ]; then + rm -rf $TMP_DIR_PROJ + else + echo " Project output: $TMP_DIR_PROJ" + fi else echo "SKIP: $font_url" fi popd > /dev/null - rm -rf $TMP_DIR_SYS + if [ $failure -eq 0 ]; then + rm -rf $TMP_DIR_SYS + else + echo " System output: $TMP_DIR_SYS" + fi done diff --git a/tests/fonts/PublicFontList.txt b/tests/fonts/PublicFontList.txt index a2e82d35a9..2efd7fa6ab 100644 --- a/tests/fonts/PublicFontList.txt +++ b/tests/fonts/PublicFontList.txt @@ -34,7 +34,6 @@ https://github.com/marutan/riscos-free-fonts/raw/main/Fonts/Trinity.Bold.sfd https://github.com/marutan/riscos-free-fonts/raw/main/Fonts/Trinity.Medium.Italic.sfd https://github.com/marutan/riscos-free-fonts/raw/main/Fonts/Trinity.Medium.sfd https://github.com/dokutan/legacy_computing-font/raw/master/LegacyComputing.sfd -https://github.com/dokutan/legacy_computing-font/raw/master/LegacyComputing114.sfd https://github.com/fontforge/debugfonts/raw/master/charstring-corruption-1747.sfd https://github.com/fontforge/debugfonts/raw/master/jieub-bt-ps.sfd https://github.com/fontforge/debugfonts/raw/master/point-on-curve.sfd @@ -118,7 +117,6 @@ https://gitlab.com/ameliedumont/fonts/-/raw/master/Ductus/regular/source/qunific https://gitlab.com/ameliedumont/fonts/-/raw/master/Ductus/regular/source/DuctusRegular.sfd https://gitlab.com/ameliedumont/fonts/-/raw/master/Ductus/regular/source/DuctusGeometric.sfd https://gitlab.com/ameliedumont/fonts/-/raw/master/Ductus/regular/source/DuctusCalligraphic.sfd -https://gitlab.com/ameliedumont/fonts/-/raw/master/Ductus/regular/DuctusGeometric.sfd https://gitlab.com/kreativekorp/bitsnpicas/-/raw/master/fonts/sfsu/tos/ibm8.sfd https://gitlab.com/kreativekorp/bitsnpicas/-/raw/master/fonts/sfsu/tos/rgb.sfd https://gitlab.com/kreativekorp/bitsnpicas/-/raw/master/fonts/sfsu/tos/tosx.sfd @@ -177,4 +175,15 @@ https://gitlab.com/typebits/font-persiana/-/raw/master/Persiana-Regular.sfd https://gitlab.com/typebits/font-roseta/-/raw/master/Roseta-Regular.sfd https://gitlab.com/typebits/font-setperset/-/raw/master/Setperset-Regular.sfd https://gitlab.com/icewoodfire/tty-clock-ttf-font/-/raw/master/ttyclock.sfd -https://gitlab.com/balasankarc/sokanasini/-/raw/master/Sokanasini.sfd \ No newline at end of file +https://gitlab.com/balasankarc/sokanasini/-/raw/master/Sokanasini.sfd +https://github.com/slavonic/fonts-znam/raw/master/Mezenets/MezenetsUnicode.sfd +https://github.com/slavonic/fonts-znam/raw/master/Shaidur/ShaidurUnicode.sfd +https://github.com/slavonic/fonts-znam/raw/master/Smolensky/Smolensky-Regular.sfd +https://github.com/slavonic/fonts-znam/raw/master/Stolp/Stolp-Regular.sfd +https://github.com/slavonic/Monomakh/raw/main/sources/Monomakh-Regular.sfd +https://github.com/slavonic/Pochaevsk/raw/main/sources/Pochaevsk.sfd +https://github.com/slavonic/Ponomar/raw/main/sources/Ponomar.sfd +https://github.com/slavonic/Triodion/raw/main/sources/Triodion.sfd +https://github.com/slavonic/Shafarik/raw/main/sources/Shafarik-Regular.sfd +https://github.com/slavonic/FiraSlav/raw/master/sources/FiraSlav-Regular.sfd +https://github.com/slavonic/FiraSlav/raw/master/sources/FiraSlav-Bold.sfd \ No newline at end of file From d6664dbd1de70ac199b7d06b1388fc8da85c3f4b Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Mon, 26 Jan 2026 20:24:26 +0200 Subject: [PATCH 42/96] Fix "FONT_ASCENT", "FONT_DESCENT", "SPACING" BDF properties (#5710) * Modernize spacing computation for bitmap fonts --- fontforge/bitmapchar.c | 80 ++++++++++++++++++++++++++++++------------ fontforge/bitmapchar.h | 2 ++ fontforge/dumpbdf.c | 44 ++++++----------------- fontforge/fvfonts.c | 30 ++++++++++++++++ fontforge/fvfonts.h | 10 ++++++ fontforge/splinesave.c | 29 ++------------- fontforgeexe/bdfinfo.c | 2 ++ 7 files changed, 114 insertions(+), 83 deletions(-) diff --git a/fontforge/bitmapchar.c b/fontforge/bitmapchar.c index 7a373f9a00..f8879eb451 100644 --- a/fontforge/bitmapchar.c +++ b/fontforge/bitmapchar.c @@ -215,32 +215,39 @@ return; bdf->props[i].u.val = value; } -static const char *AllSame(BDFFont *font,int *avg,int *cnt) { - int c=0, a=0, common= -1; +/* Return 'P' if a proportional font + Return 'C' if an X11 "cell" font + (all glyphs within (0,width)x(ascent,desent) & monospace ) + Return 'M' if monospace or dual space and not a cell */ +static const char* Spacing(BDFFont* font, int* avg, int* cnt) { + FontPitch pitch = pitch_unknown; + int c = 0, a = 0, common = -1; int i; - BDFChar *bdfc; - int cell = -1; - - /* Return 'P' if a proporitional font */ - /* Return 'C' if an X11 "cell" font (all glyphs within (0,width)x(ascent,desent) & monospace ) */ - /* Return 'M' if monospace (and not a cell) */ - for ( i=0; iglyphcnt; ++i ) { - if ( (bdfc = font->glyphs[i])!=NULL && !IsntBDFChar(bdfc)) { - ++c; - a += bdfc->width; - if ( common==-1 ) common = bdfc->width; else if ( common!=bdfc->width ) { common = -2; cell = 0; } - if ( cell ) { - if ( bdfc->xmin<0 || bdfc->xmax>common || bdfc->ymax>font->ascent || - -bdfc->ymin>font->descent ) - cell = 0; - else - cell = 1; - } - } + BDFChar* bdfc = NULL; + bool cell = false; + + for (i = 0; i < font->glyphcnt; ++i) { + if ((bdfc = font->glyphs[i]) != NULL && !IsntBDFChar(bdfc)) { + ++c; + a += bdfc->width; + RecomputePitch(bdfc->width, &pitch, &common); + } } if ( c==0 ) *avg=0; else *avg = (10*a)/c; *cnt = c; -return(common==-2 ? "P" : cell ? "C" : "M" ); + if (pitch == pitch_fixed || pitch == pitch_dual) { + int fbb_height, fbb_width, fbb_descent, fbb_lbearing; + CalculateBoundingBox(font, &fbb_width, &fbb_height, &fbb_lbearing, + &fbb_descent); + if (fbb_lbearing >= 0 && (fbb_width + fbb_lbearing) <= common && + -fbb_descent <= font->descent && + (fbb_height + fbb_descent) <= font->ascent) + cell = true; + } + + return (pitch == pitch_unknown || pitch == pitch_variable) ? "P" + : cell ? "C" + : "M"; } static void BDFPropAppendString(BDFFont *bdf,const char *keyword,char *value) { @@ -564,7 +571,7 @@ void XLFD_CreateComponents(BDFFont *font,EncMap *map, int res, struct xlfd_compo char reg[100], enc[40]; int old_res; - mono = AllSame(font,&avg,&cnt); + mono = Spacing(font,&avg,&cnt); old_res = BdfPropHasInt(font,"RESOLUTION_X",-1); if ( res!=-1 ) /* Already set */; @@ -948,3 +955,30 @@ struct bc_interface *bc_interface = &noui_bc; void FF_SetBCInterface(struct bc_interface *bci) { bc_interface = bci; } + +void CalculateBoundingBox(BDFFont* font, int* fbb_width, int* fbb_height, + int* fbb_lbearing, int* fbb_descent) { + int minx = 0, maxx = 0, miny = 0, maxy = 0; + BDFChar* bdfc; + int i; + + if (font->glyphcnt > 0 && (bdfc = font->glyphs[0]) != NULL) { + minx = bdfc->xmin; + maxx = bdfc->xmax; + miny = bdfc->ymin; + maxy = bdfc->ymax; + } + + for (i = 1; i < font->glyphcnt; ++i) { + if ((bdfc = font->glyphs[i]) != NULL) { + if (minx > bdfc->xmin) minx = bdfc->xmin; + if (maxx < bdfc->xmax) maxx = bdfc->xmax; + if (miny > bdfc->ymin) miny = bdfc->ymin; + if (maxy < bdfc->ymax) maxy = bdfc->ymax; + } + } + *fbb_height = maxy - miny + 1; + *fbb_width = maxx - minx + 1; + *fbb_descent = miny; + *fbb_lbearing = minx; +} diff --git a/fontforge/bitmapchar.h b/fontforge/bitmapchar.h index 430086b26f..585929bfdf 100644 --- a/fontforge/bitmapchar.h +++ b/fontforge/bitmapchar.h @@ -20,5 +20,7 @@ extern void SFReplaceEncodingBDFProps(SplineFont *sf, EncMap *map); extern void SFReplaceFontnameBDFProps(SplineFont *sf); extern void XLFD_CreateComponents(BDFFont *font, EncMap *map, int res, struct xlfd_components *components); extern void XLFD_GetComponents(const char *xlfd, struct xlfd_components *components); +extern void CalculateBoundingBox(BDFFont *font, + int *fbb_width,int *fbb_height,int *fbb_lbearing, int *fbb_descent); #endif /* FONTFORGE_BITMAPCHAR_H */ diff --git a/fontforge/dumpbdf.c b/fontforge/dumpbdf.c index e8a166a2d0..8015240ba0 100644 --- a/fontforge/dumpbdf.c +++ b/fontforge/dumpbdf.c @@ -51,36 +51,6 @@ return( true ); return( !SCWorthOutputting(bdfc->sc)); } -static void calculate_bounding_box(BDFFont *font, - int *fbb_width,int *fbb_height,int *fbb_lbearing, int *fbb_descent) { - int minx=0,maxx=0,miny=0,maxy=0; - BDFChar *bdfc; - int i; - - for ( i=0; iglyphcnt; ++i ) { - if ( (bdfc = font->glyphs[i])!=NULL ) { - if ( minx==0 && maxx==0 ) { - minx = bdfc->xmin; - maxx = bdfc->xmax; - } else { - if ( minx>bdfc->xmin ) minx=bdfc->xmin; - if ( maxxxmax ) maxx = bdfc->xmax; - } - if ( miny==0 && maxy==0 ) { - miny = bdfc->ymin; - maxy = bdfc->ymax; - } else { - if ( miny>bdfc->ymin ) miny=bdfc->ymin; - if ( maxyymax ) maxy = bdfc->ymax; - } - } - } - *fbb_height = maxy-miny+1; - *fbb_width = maxx-minx+1; - *fbb_descent = miny; - *fbb_lbearing = minx; -} - #define MS_Hor 1 #define MS_Vert 2 struct metric_defaults { @@ -311,7 +281,7 @@ static void BDFDumpHeader(FILE *file,BDFFont *font,EncMap *map, #else fprintf( file, "SIZE %d %d %d\n", components.point_size/10, components.res_x, components.res_y ); #endif - calculate_bounding_box(font,&fbb_width,&fbb_height,&fbb_lbearing,&fbb_descent); + CalculateBoundingBox(font,&fbb_width,&fbb_height,&fbb_lbearing,&fbb_descent); fprintf( file, "FONTBOUNDINGBOX %d %d %d %d\n", fbb_width, fbb_height, fbb_lbearing, fbb_descent); if ( defs->metricssets!=0 ) { @@ -353,8 +323,10 @@ static void BDFDumpHeader(FILE *file,BDFFont *font,EncMap *map, ++pcnt; } if ( pcnt!=0 ) { + if (!BdfPropHasKey(font, "FONT_ASCENT", NULL, 0)) ++pcnt; + if (!BdfPropHasKey(font, "FONT_DESCENT", NULL, 0)) ++pcnt; fprintf( file, "STARTPROPERTIES %d\n", pcnt ); - for ( i=pcnt=0; iprop_cnt; ++i ) { + for ( i=0; iprop_cnt; ++i ) { if ( font->props[i].type&prt_property ) { fprintf( file, "%s ", font->props[i].name ); switch ( font->props[i].type&~prt_property ) { @@ -378,8 +350,12 @@ static void BDFDumpHeader(FILE *file,BDFFont *font,EncMap *map, } } } + if (!BdfPropHasKey(font, "FONT_ASCENT", NULL, 0)) + fprintf( file, "FONT_ASCENT %d\n",font->ascent ); + if (!BdfPropHasKey(font, "FONT_DESCENT", NULL, 0)) + fprintf( file, "FONT_DESCENT %d\n",font->descent ); } else { - fprintf( file, "STARTPROPERTIES %d\n", 15+ (font->clut!=NULL)); + fprintf( file, "STARTPROPERTIES %d\n", 17+ (font->clut!=NULL)); fprintf( file, "FONTNAME_REGISTRY \"\"\n" ); fprintf( file, "FOUNDRY \"%s\"\n", components.foundry ); fprintf( file, "FAMILY_NAME \"%s\"\n", components.family ); @@ -397,6 +373,8 @@ static void BDFDumpHeader(FILE *file,BDFFont *font,EncMap *map, fprintf( file, "BITS_PER_PIXEL %d\n", BDFDepth(font)); fprintf( file, "CHARSET_REGISTRY \"%s\"\n", components.cs_reg ); fprintf( file, "CHARSET_ENCODING \"%s\"\n", components.cs_enc ); + fprintf( file, "FONT_ASCENT %d\n",font->ascent ); + fprintf( file, "FONT_DESCENT %d\n",font->descent ); } fprintf( file, "ENDPROPERTIES\n" ); { /* AllSame tells us how many glyphs in the font. Which is great for */ diff --git a/fontforge/fvfonts.c b/fontforge/fvfonts.c index c586098891..864fd8346c 100644 --- a/fontforge/fvfonts.c +++ b/fontforge/fvfonts.c @@ -1657,3 +1657,33 @@ return( NULL ); new->map = EncMapFromEncoding(new,enc); return( new ); } + +bool RecomputePitch(int16_t this_width, FontPitch* pitch, int* width) { + if (this_width == 0) { + /* Zero-width glyphs, such as control characters or diacritical marks + don't affect the state. */ + return true; + } + + if (*pitch == pitch_unknown) { + *width = this_width; + *pitch = pitch_fixed; + } else if (*pitch == pitch_fixed) { + if (this_width == 2 * (*width)) { + *pitch = pitch_dual; + } else if (2 * this_width == *width) { + *width = this_width; + *pitch = pitch_dual; + } else if (this_width != *width) { + *width = -1; + *pitch = pitch_variable; + return false; /* no further check necessary */ + } + } else if (*pitch == pitch_dual && + (this_width != *width && this_width != 2 * (*width))) { + *width = -1; + *pitch = pitch_variable; + return false; /* no further check necessary */ + } + return true; +} diff --git a/fontforge/fvfonts.h b/fontforge/fvfonts.h index 304c7a0337..c2173511cb 100644 --- a/fontforge/fvfonts.h +++ b/fontforge/fvfonts.h @@ -31,4 +31,14 @@ extern void MergeFont(FontViewBase *fv, SplineFont *other, int preserveCrossFont extern void SFFinishMergeContext(struct sfmergecontext *mc); extern void SFHashGlyph(SplineFont *sf, SplineChar *sc); +typedef enum font_pitch { + pitch_unknown, + pitch_variable, + pitch_fixed, + pitch_dual +} FontPitch; +/* Call RecomputePitch() in a cycle for each glyph, while treating pitch and + * width as a state. */ +bool RecomputePitch(int16_t this_width, FontPitch* pitch, int* width); + #endif /* FONTFORGE_FVFONTS_H */ diff --git a/fontforge/splinesave.c b/fontforge/splinesave.c index 3bd0c2635a..91d949b6af 100644 --- a/fontforge/splinesave.c +++ b/fontforge/splinesave.c @@ -1796,8 +1796,6 @@ static void SplineFont2FullSubrs1(int flags,GlyphInfo *gi) { #endif /* FONTFORGE_CONFIG_PS_REFS_GET_SUBRS */ } -typedef enum font_pitch { pitch_unknown, pitch_variable, pitch_fixed, pitch_dual } FontPitch; - static FontPitch SFComputePitch(SplineFont *sf, int *p_width) { FontPitch pitch = pitch_unknown; int width = -1; @@ -1811,31 +1809,8 @@ static FontPitch SFComputePitch(SplineFont *sf, int *p_width) { (strcmp(sf->glyphs[i]->name,".notdef")!=0 || sf->glyphs[i]->layers[ly_fore].splines!=NULL)) { int16_t this_width = sf->glyphs[i]->width; - if (this_width == 0) { - /* Zero-width glyphs, such as control characters or diacritical marks - don't affect the state. */ - continue; - } - - if (pitch == pitch_unknown) { - width = this_width; - pitch = pitch_fixed; - } else if (pitch == pitch_fixed) { - if (this_width == 2*width) { - pitch = pitch_dual; - } else if (2*this_width == width) { - width = this_width; - pitch = pitch_dual; - } else if (this_width != width){ - width = -1; - pitch = pitch_variable; - break; /* no further check necessary */ - } - } else if (pitch == pitch_dual && (this_width != width && this_width != 2*width)) { - width = -1; - pitch = pitch_variable; - break; /* no further check necessary */ - } + if (!RecomputePitch(this_width, &pitch, &width)) + break; } } diff --git a/fontforgeexe/bdfinfo.c b/fontforgeexe/bdfinfo.c index f20e2eed28..88573b2dc7 100644 --- a/fontforgeexe/bdfinfo.c +++ b/fontforgeexe/bdfinfo.c @@ -775,6 +775,8 @@ return; pos.height = GDrawPointsToPixels(NULL,375); bd.gw = gw = GDrawCreateTopWindow(NULL,&pos,bdfp_e_h,&bd,&wattrs); + /* TODO(iorsh): Show read-only BDF properties which are not stored in + BDFFont::props, e.g. "FONT_ASCENT", "FONT_DESCENT". */ sbwidth = GDrawPointsToPixels(bd.gw,_GScrollBar_Width); subpos.x = 0; subpos.y = GDrawPointsToPixels(NULL,28); subpos.width = pos.width-sbwidth; From 6338c3605c86ad487a99dabff732f09354a03e8e Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Tue, 27 Jan 2026 23:26:49 +0200 Subject: [PATCH 43/96] Fix glyph name and Unicode value deduction for SVG fonts (#5667) --- fontforge/splinechar.c | 40 +++++++++++ fontforge/splinechar.h | 6 ++ fontforge/svg.c | 152 +++++++++++++++++++++++++--------------- fontforgeexe/charinfo.c | 47 ++++--------- 4 files changed, 154 insertions(+), 91 deletions(-) diff --git a/fontforge/splinechar.c b/fontforge/splinechar.c index d22dbe91be..a91f0d43dc 100644 --- a/fontforge/splinechar.c +++ b/fontforge/splinechar.c @@ -2829,3 +2829,43 @@ void SCRemoveVKern(SplineChar* sc) { sc->parent->fv->cidmaster->changed = true; } } + +const char* SCNameCheck(const unichar_t *name, bool *p_questionable) { + bool bad = false, questionable = false; + extern int allow_utf8_glyphnames; + + if (p_questionable) *p_questionable = questionable; + + if ( uc_strcmp(name,".notdef")==0 ) /* This name is a special case and doesn't follow conventions */ + return NULL; + if ( u_strlen(name)>31 ) { + return _("Glyph names are limited to 31 characters"); + } else if ( *name=='\0' ) { + return _("Bad Name"); + } else if ( isdigit(*name) || *name=='.' ) { + return _("A glyph name may not start with a digit nor a full stop (period)"); + } + + while ( *name ) { + if ( *name<=' ' || (!allow_utf8_glyphnames && *name>=0x7f) || + *name=='(' || *name=='[' || *name=='{' || *name=='<' || + *name==')' || *name==']' || *name=='}' || *name=='>' || + *name=='%' || *name=='/' ) + bad=true; + else if ( !isalnum(*name) && *name!='.' && *name!='_' ) + questionable = true; + ++name; + } + if ( bad ) { + return _("A glyph name must be ASCII, without spaces and may not contain the characters \"([{<>}])/%%\", and should contain only alphanumerics, periods and underscores"); + } else if ( questionable ) { + if (p_questionable == NULL) { + /* The caller doesn't care, just accept the name. */ + return NULL; + } else { + *p_questionable = questionable; + return _("A glyph name should contain only alphanumerics, periods and underscores\nDo you want to use this name in spite of that?"); + } + } + return NULL; +} diff --git a/fontforge/splinechar.h b/fontforge/splinechar.h index 0e75707d1b..af9bad4cd0 100644 --- a/fontforge/splinechar.h +++ b/fontforge/splinechar.h @@ -729,3 +729,9 @@ typedef struct splinechar_ttf_map { SplineChar* glyph; int ttf_glyph; } SplineCharTTFMap; + +/* Returns NULL if the name is valid, or error message. If the function returned + error message and set `questionable` to true, the name should be accepted, but + discouraged. + The error message should not be freed. */ +const char* SCNameCheck(const unichar_t *name, bool *questionable); diff --git a/fontforge/svg.c b/fontforge/svg.c index 66272724d4..7e7a841c16 100644 --- a/fontforge/svg.c +++ b/fontforge/svg.c @@ -593,6 +593,10 @@ static void svg_dumpscdefs(FILE *file,SplineChar *sc,const char *name,int istop) } } +static char* svg_sc_name(SplineChar *sc) { + return sc->glif_name ? sc->glif_name : sc->name; +} + static void svg_dumptype3(FILE *file,SplineChar *sc,const char *name,int istop) { int i, j; RefChar *ref; @@ -623,12 +627,12 @@ static void svg_dumptype3(FILE *file,SplineChar *sc,const char *name,int istop) fprintf(file, " layers[j].splines; if ( SSHasClip(transed)) - fprintf( file, "clip-path=\"url(#%s-%s-ly%d-clip)\" ", name, ref->sc->name, j ); + fprintf( file, "clip-path=\"url(#%s-%s-ly%d-clip)\" ", name, svg_sc_name(ref->sc), j ); if ( ref->layers[j].dostroke ) { - svg_dumpstroke(file,&ref->layers[j].stroke_pen,&sc->layers[i].stroke_pen,sc->name,ref->sc,j,istop); + svg_dumpstroke(file,&ref->layers[j].stroke_pen,&sc->layers[i].stroke_pen,svg_sc_name(sc),ref->sc,j,istop); transed = TransBy(transed,ref->layers[j].stroke_pen.trans); } - svg_dumpfill(file,&ref->layers[j].fill_brush,&sc->layers[i].fill_brush,ref->layers[j].dofill,sc->name,ref->sc,j,istop); + svg_dumpfill(file,&ref->layers[j].fill_brush,&sc->layers[i].fill_brush,ref->layers[j].dofill,svg_sc_name(sc),ref->sc,j,istop); fprintf( file, ">\n" ); fprintf(file, " layers[j].dostroke,false); @@ -706,10 +710,10 @@ static void svg_scpathdump(FILE *file, SplineChar *sc,const char *endpath,int la } if ( needs_defs ) { fprintf(file, " \n" ); - svg_dumpscdefs(file,sc,sc->name,true); + svg_dumpscdefs(file,sc,svg_sc_name(sc),true); fprintf(file, " \n" ); } - svg_dumptype3(file,sc,sc->name,true); + svg_dumptype3(file,sc,svg_sc_name(sc),true); fputs(endpath,file); } } @@ -779,7 +783,7 @@ static void svg_scdump(FILE *file, SplineChar *sc,int defwid, int encuni, int vs if ( sc->comment!=NULL ) { fprintf( file, "\n\n",sc->comment ); } - fprintf(file," name ); + fprintf(file," parent,best,univals,sizeof(univals)/sizeof(univals[0])); fputs("unicode=\"",file); @@ -888,7 +892,7 @@ static void svg_dumpkerns(FILE *file,SplineFont *sf,int isv) { else fprintf( file, "u1=\"&#x%x;\" ", sf->glyphs[i]->unicodeenc ); if ( kp->sc->unicodeenc==-1 || HasLigature(kp->sc)) - fprintf( file, "g2=\"%s\" ", kp->sc->name ); + fprintf( file, "g2=\"%s\" ", svg_sc_name(kp->sc) ); else if ( kp->sc->unicodeenc>='A' && kp->sc->unicodeenc<='z' ) fprintf( file, "u2=\"%c\" ", kp->sc->unicodeenc ); else @@ -2888,12 +2892,77 @@ static void SVGParseGlyphBody(SplineChar *sc, xmlNodePtr glyph, SCCategorizePoints(sc); } +static int SVGGetUnicodeEnc(xmlChar *unicode, xmlChar *arabic_form, xmlChar *glyphname) { + int unicodeenc = -1; + if (unicode != NULL && unicode[0] != '\0') { + uint32_t *u = utf82u_copy((char *) unicode); + if ( u[1]=='\0' ) { + unicodeenc = u[0]; + if ( arabic_form!=NULL && u[0]>=0x600 && u[0]<=0x6ff ) { + const struct arabicforms* aform = arabicform(u[0]); + if ( xmlStrcmp(arabic_form,(xmlChar *) "initial")==0 ) + unicodeenc = aform->initial; + else if ( xmlStrcmp(arabic_form,(xmlChar *) "medial")==0 ) + unicodeenc = aform->medial; + else if ( xmlStrcmp(arabic_form,(xmlChar *) "final")==0 ) + unicodeenc = aform->final; + else if ( xmlStrcmp(arabic_form,(xmlChar *) "isolated")==0 ) + unicodeenc = aform->isolated; + } + } + free(u); + } + if (unicodeenc == -1 && glyphname != NULL) + unicodeenc = UniFromName((char *) glyphname,ui_none,&custom); + + return unicodeenc; +} + +/* This function should never return NULL or empty string. */ +static char* SVGGetGlyphName(xmlChar *glyphname, xmlChar *orientation, + int unicodeenc, unsigned int glyph_idx, bool* glyphname_valid) { + char* name = NULL; + bool has_vert_suffix = false; + unsigned int name_len = 0, vert_len = 0; + *glyphname_valid = false; + + if ( glyphname!=NULL ) { + /* Check name validity. */ + unichar_t *uname = utf82u_copy((char *) glyphname); + if (SCNameCheck(uname, NULL) == NULL) { + name = copy((char *) glyphname); + *glyphname_valid = true; + } + free(uname); + } + + /* Get name from unicode value. */ + if (name == NULL) { + char buffer[40]; + if (unicodeenc == -1) + name = smprintf("glyph%d", glyph_idx); + else + name = copy(StdGlyphName(buffer,unicodeenc,ui_none,NULL)); + } + + name_len = strlen(name); + vert_len = strlen(".vert"); + has_vert_suffix = (name_len >= vert_len) && + (strcmp(".vert", name + name_len - vert_len) == 0); + if (!has_vert_suffix && orientation != NULL && *orientation == 'v') { + char* new_name = smprintf("%s.vert", name); + free(name); + name = new_name; + } + + return name; +} + static SplineChar *SVGParseGlyphArgs(xmlNodePtr glyph,int defh, int defv, - SplineFont *sf) { + SplineFont *sf, unsigned int glyph_idx) { SplineChar *sc = SFSplineCharCreate(sf); xmlChar *name, *form, *glyphname, *unicode, *orientation; - uint32_t *u; - char buffer[100]; + bool glyphname_valid = false; name = xmlGetProp(glyph,(xmlChar *) "horiz-adv-x"); if ( name!=NULL ) { @@ -2919,50 +2988,27 @@ static SplineChar *SVGParseGlyphArgs(xmlNodePtr glyph,int defh, int defv, unicode = xmlGetProp(glyph,(xmlChar *) "unicode"); glyphname = xmlGetProp(glyph,(xmlChar *) "glyph-name"); orientation = xmlGetProp(glyph,(xmlChar *) "orientation"); - if ( unicode!=NULL ) { - u = utf82u_copy((char *) unicode); - xmlFree(unicode); - if ( u[1]=='\0' ) { - sc->unicodeenc = u[0]; - if ( form!=NULL && u[0]>=0x600 && u[0]<=0x6ff ) { - const struct arabicforms* aform = arabicform(u[0]); - if ( xmlStrcmp(form,(xmlChar *) "initial")==0 ) - sc->unicodeenc = aform->initial; - else if ( xmlStrcmp(form,(xmlChar *) "medial")==0 ) - sc->unicodeenc = aform->medial; - else if ( xmlStrcmp(form,(xmlChar *) "final")==0 ) - sc->unicodeenc = aform->final; - else if ( xmlStrcmp(form,(xmlChar *) "isolated")==0 ) - sc->unicodeenc = aform->isolated; - } - } - free(u); + sc->unicodeenc = SVGGetUnicodeEnc(unicode, form, glyphname); + sc->name = SVGGetGlyphName(glyphname, orientation, sc->unicodeenc, glyph_idx, &glyphname_valid); + if (!glyphname_valid && glyphname && glyphname[0] != '\0') { + /* Preserve the original glyph name and use it for SVG export. */ + free(sc->glif_name); + sc->glif_name = copy((char *) glyphname); } - if ( glyphname!=NULL ) { - if ( sc->unicodeenc==-1 ) - sc->unicodeenc = UniFromName((char *) glyphname,ui_none,&custom); - sc->name = copy((char *) glyphname); - xmlFree(glyphname); - } else if ( orientation!=NULL && *orientation=='v' && sc->unicodeenc!=-1 ) { - if ( sc->unicodeenc<0x10000 ) - sprintf( buffer, "uni%04X.vert", sc->unicodeenc ); - else - sprintf( buffer, "u%04X.vert", sc->unicodeenc ); - sc->name = copy( buffer ); - } - /* we finish off defaulting the glyph name in the parseglyph routine */ - if ( form!=NULL ) - xmlFree(form); - if ( orientation!=NULL ) - xmlFree(orientation); -return( sc ); + + xmlFree(form); + xmlFree(unicode); + xmlFree(glyphname); + xmlFree(orientation); + return sc; } static SplineChar *SVGParseMissing(SplineFont *sf,xmlNodePtr notdef,int defh, int defv, int enc, ImportParams *ip) { - SplineChar *sc = SVGParseGlyphArgs(notdef,defh,defv,sf); + SplineChar *sc = SVGParseGlyphArgs(notdef,defh,defv,sf,enc); sc->parent = sf; + free(sc->name); sc->name = copy(".notdef"); sc->unicodeenc = 0; SVGParseGlyphBody(sc,notdef,ip); @@ -2971,16 +3017,8 @@ return( sc ); static SplineChar *SVGParseGlyph(SplineFont *sf,xmlNodePtr glyph,int defh, int defv, int enc, ImportParams *ip) { - char buffer[400]; - SplineChar *sc = SVGParseGlyphArgs(glyph,defh,defv,sf); + SplineChar *sc = SVGParseGlyphArgs(glyph,defh,defv,sf,enc); sc->parent = sf; - if ( sc->name==NULL ) { - if ( sc->unicodeenc==-1 ) { - sprintf( buffer, "glyph%d", enc); - sc->name = copy(buffer); - } else - sc->name = copy(StdGlyphName(buffer,sc->unicodeenc,ui_none,NULL)); - } SVGParseGlyphBody(sc,glyph,ip); return( sc ); } @@ -2993,7 +3031,7 @@ static void SVGLigatureFixupCheck(SplineChar *sc,xmlNodePtr glyph) { char *comp, *pt; unicode = xmlGetProp(glyph,(xmlChar *) "unicode"); - if ( unicode!=NULL ) { + if (unicode != NULL && unicode[0] != '\0') { u = utf82u_copy((char *) unicode); xmlFree(unicode); if ( u[1]!='\0' && u[2]=='\0' && diff --git a/fontforgeexe/charinfo.c b/fontforgeexe/charinfo.c index 3a3b8b336f..88bad76b2d 100644 --- a/fontforgeexe/charinfo.c +++ b/fontforgeexe/charinfo.c @@ -511,42 +511,20 @@ void SCInsertPST(SplineChar *sc,PST *new_) { } static int CI_NameCheck(const unichar_t *name) { - int bad, questionable; - extern int allow_utf8_glyphnames; - char *buts[3]; - buts[0] = _("_Yes"); buts[1]=_("_No"); buts[2] = NULL; + bool questionable = false; + const char* error = SCNameCheck(name, &questionable); - if ( uc_strcmp(name,".notdef")==0 ) /* This name is a special case and doesn't follow conventions */ -return( true ); - if ( u_strlen(name)>31 ) { - ff_post_error(_("Bad Name"),_("Glyph names are limited to 31 characters")); -return( false ); - } else if ( *name=='\0' ) { - ff_post_error(_("Bad Name"),_("Bad Name")); -return( false ); - } else if ( isdigit(*name) || *name=='.' ) { - ff_post_error(_("Bad Name"),_("A glyph name may not start with a digit nor a full stop (period)")); -return( false ); - } - bad = questionable = false; - while ( *name ) { - if ( *name<=' ' || (!allow_utf8_glyphnames && *name>=0x7f) || - *name=='(' || *name=='[' || *name=='{' || *name=='<' || - *name==')' || *name==']' || *name=='}' || *name=='>' || - *name=='%' || *name=='/' ) - bad=true; - else if ( !isalnum(*name) && *name!='.' && *name!='_' ) - questionable = true; - ++name; - } - if ( bad ) { - ff_post_error(_("Bad Name"),_("A glyph name must be ASCII, without spaces and may not contain the characters \"([{<>}])/%%\", and should contain only alphanumerics, periods and underscores")); -return( false ); - } else if ( questionable ) { - if ( gwwv_ask(_("Bad Name"),(const char **) buts,0,1,_("A glyph name should contain only alphanumerics, periods and underscores\nDo you want to use this name in spite of that?"))==1 ) -return(false); + if (error == NULL) + return true; + + if (questionable) { + char *buts[3]; + buts[0] = _("_Yes"); buts[1]=_("_No"); buts[2] = NULL; + return (gwwv_ask(_("Bad Name"),(const char **) buts,0,1,error) != 1); } -return( true ); + + ff_post_error(_("Bad Name"), error); + return false; } static void CI_ParseCounters(CharInfo *ci) { @@ -1569,6 +1547,7 @@ static void CI_ApplyAll(CharInfo *ci) { if ( sc->name==NULL || strcmp( sc->name,cached->name )!=0 ) { if ( sc->name!=NULL ) SFGlyphRenameFixup(sf,sc->name,cached->name,false); + free(sc->glif_name); sc->glif_name = NULL; free(sc->name); sc->name = copy(cached->name); sc->namechanged = true; GlyphHashFree(sf); From 6781d61104a878379237ffdbe99f0d7413bc85a9 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 28 Jan 2026 22:55:09 +0200 Subject: [PATCH 44/96] Apply width when copying chars in bitmap-only font (#5711) --- fontforge/bitmapchar.c | 2 +- fontforge/cvundoes.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/fontforge/bitmapchar.c b/fontforge/bitmapchar.c index f8879eb451..c8768ecd3e 100644 --- a/fontforge/bitmapchar.c +++ b/fontforge/bitmapchar.c @@ -871,7 +871,7 @@ return( NULL ); bdf->glyphcnt = sf->glyphcnt; } if ( (bc = bdf->glyphs[gid])==NULL ) { - if ( use_freetype_to_rasterize_fv ) { + if ( use_freetype_to_rasterize_fv && SCDrawsSomething(sc) ) { void *freetype_context = FreeTypeFontContext(sf,sc,NULL,ly_fore); if ( freetype_context != NULL ) { bc = SplineCharFreeTypeRasterize(freetype_context, diff --git a/fontforge/cvundoes.c b/fontforge/cvundoes.c index e1df4504e9..5763b55c75 100644 --- a/fontforge/cvundoes.c +++ b/fontforge/cvundoes.c @@ -2366,8 +2366,7 @@ static void _PasteToSC(SplineChar *sc,Undoes *paster,FontViewBase *fv,int pastei if ( !pasteinto ) { if ( !sc->layers[layer].background && SCWasEmpty(sc,pasteinto==0?layer:-1) ) { - if ( !sc->parent->onlybitmaps ) - SCSynchronizeWidth(sc,width,sc->width,fv); + SCSynchronizeWidth(sc,width,sc->width,fv); sc->vwidth = vwidth; } SplinePointListsFree(sc->layers[layer].splines); From 42a35cba09c3da12e42333416b8173d9dc499931 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Thu, 29 Jan 2026 20:17:14 +0200 Subject: [PATCH 45/96] Fix and disallow duplicate-encoded glyphs (#5715) * Fix duplicate encodings in SFD * Fix for invalid local encoding * Fix SCSetMetaData() to reject duplicate encoding --- Unicode/ustring.c | 9 +++++ fontforge/sfd.c | 72 ++++++++++++++++++++--------------- fontforge/splinechar.c | 85 ++++++++++++++++++++++-------------------- inc/ustring.h | 4 ++ 4 files changed, 99 insertions(+), 71 deletions(-) diff --git a/Unicode/ustring.c b/Unicode/ustring.c index 893eeedf23..151fce071a 100644 --- a/Unicode/ustring.c +++ b/Unicode/ustring.c @@ -1169,3 +1169,12 @@ char* tostr( int v ) snprintf(buf,bufsz,"%d",v); return buf; } + +void realloc_tail(char** p_buf, size_t size_delta, char** p_tail, + char** p_proc) { + size_t new_size = size_delta + (*p_tail - *p_buf); + char* new_buf = realloc(*p_buf, new_size); + *p_tail = new_buf + new_size; + if (p_proc) *p_proc = new_buf + (*p_proc - *p_buf); + *p_buf = new_buf; +} diff --git a/fontforge/sfd.c b/fontforge/sfd.c index f3c84080da..f2c8f903cf 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -175,6 +175,7 @@ static void SFDDumpHintList(FILE *sfd,const char *key, StemInfo *h); static void SFDDumpDHintList( FILE *sfd,const char *key, DStemInfo *d ); static StemInfo *SFDReadHints(FILE *sfd); static DStemInfo *SFDReadDHints( SplineFont *sf,FILE *sfd,int old ); +static void SFDSizeMap(EncMap *map,int glyphcnt,int enccnt); static int PeekMatch(FILE *stream, const char * target) { // This returns 1 if target matches the next characters in the stream. @@ -1250,12 +1251,10 @@ return; static void *SFDUnPickle(FILE *sfd, int python_data_has_lists) { int ch, quoted; - static int max = 0; - static char *buf = NULL; - char *pt, *end; - int cnt; + static char *buf = NULL, *end = NULL; + char *pt; - pt = buf; end = buf+max; + pt = buf; while ( (ch=nlgetc(sfd))!='"' && ch!='\n' && ch!=EOF ); if ( ch!='"' ) return( NULL ); @@ -1265,12 +1264,8 @@ return( NULL ); if ( !quoted && ch=='\\' ) quoted = true; else { - if ( pt>=end ) { - cnt = pt-buf; - buf = realloc(buf,(max+=200)+1); - pt = buf+cnt; - end = buf+max; - } + if ( pt>=end ) + realloc_tail(&buf, 200, &end, &pt); *pt++ = ch; quoted = false; } @@ -1889,7 +1884,6 @@ static void SFDDumpOtfFeatNames(FILE *sfd, SplineFont *sf) { struct otfname *on; bool cv[100] = {false}; int cvnum, cvcount, i; - char prefix[3] = {0}; for ( fn=sf->feat_names; fn!=NULL; fn=fn->next ) { if ( (fn->tag & 0xffff0000) == (('s' << 24) | ('s' << 16)) ) { @@ -3286,12 +3280,8 @@ char *getquotedeol(FILE *sfd) { /* FontForge doesn't write other escape sequences in this context. */ /* So any other value of ch is assumed impossible. */ } - if ( pt>=end ) { - pt = realloc(str,end-str+101); - end = pt+(end-str)+100; - str = pt; - pt = end-100; - } + if ( pt>=end ) + realloc_tail(&str, 100, &end, &pt); *pt++ = ch; ch = nlgetc(sfd); } @@ -3773,12 +3763,8 @@ static void SFDGetTtInstrs(FILE *sfd, SplineChar *sc) { int instr_len; while ( (ch=nlgetc(sfd))!=EOF ) { - if ( pt>=end ) { - char *newbuf = realloc(buf,(end-buf+200)); - pt = newbuf+(pt-buf); - end = newbuf+(end+200-buf); - buf = newbuf; - } + if ( pt>=end ) + realloc_tail(&buf, 200, &end, &pt); *pt++ = ch; if ( pt-buf>backlen && strncmp(pt-backlen,end_tt_instrs,backlen)==0 ) { pt -= backlen; @@ -3896,12 +3882,8 @@ static struct ttf_table *SFDGetTtTable(FILE *sfd, SplineFont *sf,struct ttf_tabl which = 1; while ( (ch=nlgetc(sfd))!=EOF ) { - if ( pt>=end ) { - char *newbuf = realloc(buf,(end-buf+200)); - pt = newbuf+(pt-buf); - end = newbuf+(end+200-buf); - buf = newbuf; - } + if ( pt>=end ) + realloc_tail(&buf, 200, &end, &pt); *pt++ = ch; if ( pt-buf>backlen && strncmp(pt-backlen,end_tt_instrs,backlen)==0 ) { pt -= backlen; @@ -4783,6 +4765,29 @@ return; map->map[enc] = orig_pos; } +static void SFDFixDuplicateEnc(SplineFont* sf, int enc) { + if (enc < 0 || enc >= sf->map->encmax) return; + + int32_t glyph_idx = sf->map->map[enc]; + if (glyph_idx < 0 || glyph_idx >= sf->glyphcnt) return; + + /* The last of the identically encoded glyphs occludes the others, so we + * keep it, and drop the encoding of the previously encountered duplicate. + */ + SplineChar* dup_sc = sf->glyphs[glyph_idx]; + if (dup_sc == NULL || dup_sc->orig_pos < 0) return; + LogError(_("Duplicate local encoding 0x%04x encountered in glyph %s. This " + "glyph will be unencoded."), + enc, dup_sc->name); + + /* Append this glyph at the unencoded area at the end. */ + dup_sc->unicodeenc = -1; + if (dup_sc->orig_pos < sf->map->backmax) + sf->map->backmap[dup_sc->orig_pos] = -1; + SFDSetEncMap(sf, dup_sc->orig_pos, sf->map->enccount); + SFDSizeMap(sf->map, sf->glyphcnt, sf->map->enccount + 1); +} + static void SCDefaultInterpolation(SplineChar *sc) { SplineSet *cur; SplinePoint *sp; @@ -5298,7 +5303,12 @@ return( NULL ); } else { sc->orig_pos = orig_pos++; } - SFDSetEncMap(sf,sc->orig_pos,enc); + if (enc != -1 && sf->map && sf->map->map[enc] != -1) { + /* Multiple glyphs with same encoding are not accessible from + FontView, and we consider them corrupted. */ + SFDFixDuplicateEnc(sf, enc); + } + SFDSetEncMap(sf, sc->orig_pos, enc); } else if ( strmatch(tok,"AltUni:")==0 ) { int uni; while ( getint(sfd,&uni)==1 ) { diff --git a/fontforge/splinechar.c b/fontforge/splinechar.c index a91f0d43dc..808d160e20 100644 --- a/fontforge/splinechar.c +++ b/fontforge/splinechar.c @@ -1113,19 +1113,10 @@ void UnlinkThisReference(FontViewBase *fv,SplineChar *sc,int layer) { } } -static int MultipleValues(char *name, int local) { - char *buts[3]; - buts[0] = _("_Yes"); buts[1]=_("_No"); buts[2] = NULL; - if ( ff_ask(_("Multiple"),(const char **) buts,0,1,_("There is already a glyph with this Unicode encoding\n(named %1$.40s, at local encoding %2$d).\nIs that what you want?"),name,local)==0 ) -return( true ); - -return( false ); -} - -static int MultipleNames(void) { +static int MultipleNames(const char *name) { char *buts[3]; buts[0] = _("_Yes"); buts[1]=_("_Cancel"); buts[2] = NULL; - if ( ff_ask(_("Multiple"),(const char **) buts,0,1,_("There is already a glyph with this name,\ndo you want to swap names?"))==0 ) + if ( ff_ask(_("Multiple"),(const char **) buts,0,1,_("There is already a glyph with name %.40s,\ndo you want to swap names?"), name)==0 ) return( true ); return( false ); @@ -1133,8 +1124,7 @@ return( false ); int SCSetMetaData(SplineChar *sc,const char *name,int unienc,const char *comment) { SplineFont *sf = sc->parent; - int i, mv=0; - int isnotdef, samename=false, sameuni=false; + int samename=false, sameuni=false; struct altuni *alt; if ( sf->glyphs[sc->orig_pos]!=sc ) @@ -1147,33 +1137,48 @@ int SCSetMetaData(SplineChar *sc,const char *name,int unienc,const char *comment samename = true; /* No change, it must be good */ } if ( alt!=NULL || !samename ) { - isnotdef = strcmp(name,".notdef")==0; - for ( i=0; iglyphcnt; ++i ) if ( sf->glyphs[i]!=NULL && sf->glyphs[i]->orig_pos!=sc->orig_pos ) { - if ( unienc!=-1 && sf->glyphs[i]->unicodeenc==unienc ) { - if ( !mv && !MultipleValues(sf->glyphs[i]->name,i)) { -return( false ); - } - mv = 1; - } else if ( !isnotdef && strcmp(name,sf->glyphs[i]->name)==0 ) { - if ( !MultipleNames()) { -return( false ); - } - free(sf->glyphs[i]->name); - sf->glyphs[i]->namechanged = true; - if ( strncmp(sc->name,"uni",3)==0 && sf->glyphs[i]->unicodeenc!=-1) { - char buffer[12]; - if ( sf->glyphs[i]->unicodeenc<0x10000 ) - sprintf( buffer,"uni%04X", sf->glyphs[i]->unicodeenc); - else - sprintf( buffer,"u%04X", sf->glyphs[i]->unicodeenc); - sf->glyphs[i]->name = copy(buffer); - } else { - sf->glyphs[i]->name = sc->name; - sc->name = NULL; - } - break; - } - } + int i; + if (unienc != -1) { + for (i = 0; i < sf->glyphcnt; ++i) + if (sf->glyphs[i] != NULL && + sf->glyphs[i]->orig_pos != sc->orig_pos && + sf->glyphs[i]->unicodeenc == unienc) { + /* Duplicate encoding is never allowed */ + ff_post_notice(_("Duplicate encodings"), + _("There is already a glyph with Unicode " + "encoding 0x%1$04x named %2$.40s. The " + "current name and encoding will be kept."), + unienc, sf->glyphs[i]->name); + return false; + } + /* Proceed to copy encoding and name. */ + } else { + /* For unencoded glyphs, check name duplication and propose to swap. */ + int isnotdef = strcmp(name, ".notdef") == 0; + SplineChar* swap_sc = NULL; + for (i = 0; i < sf->glyphcnt && swap_sc == NULL; ++i) + if (!isnotdef && strcmp(name, sf->glyphs[i]->name) == 0) { + if (!MultipleNames(name)) + return false; + else + swap_sc = sf->glyphs[i]; + } + if (swap_sc) { + free(swap_sc->name); + swap_sc->namechanged = true; + if (strncmp(sc->name, "uni", 3) == 0 && + swap_sc->unicodeenc != -1) { + if (swap_sc->unicodeenc < 0x10000) + swap_sc->name = + smprintf("uni%04X", swap_sc->unicodeenc); + else + swap_sc->name = smprintf("u%04X", swap_sc->unicodeenc); + } else { + swap_sc->name = sc->name; + sc->name = NULL; + } + } + } if ( sc->unicodeenc!=unienc ) { struct splinecharlist *scl; int layer; diff --git a/inc/ustring.h b/inc/ustring.h index 2a9f1d4cc2..93c516cd22 100644 --- a/inc/ustring.h +++ b/inc/ustring.h @@ -233,6 +233,10 @@ extern char* str_replace_all( char* s, char* orig, char* replacement, int free_s int toint( char* v ); char* tostr( int v ); +/* reallocate buffer and adjust its tail and intermediate processor if present + */ +void realloc_tail(char** p_buf, size_t size_delta, char** p_tail, + char** p_proc); #pragma pop_macro("PRINTF_FORMAT_ATTRIBUTE") From d01333a5bfa2ac4ed698c24b323d02107deacad7 Mon Sep 17 00:00:00 2001 From: Ahmet Furkan Kavraz <55850855+ahmetfurkankavraz@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:23:41 +0100 Subject: [PATCH 46/96] Fix CVE-2025-15270: Heap buffer overflow in SFD kern class parsing (#5743) Fixes: CVE-2025-15270 | ZDI-25-1194 | ZDI-CAN-28563 Co-authored-by: Ahmet Furkan Kavraz --- fontforge/sfd.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fontforge/sfd.c b/fontforge/sfd.c index f2c8f903cf..fdbb0f3f47 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -8290,6 +8290,10 @@ bool SFD_GetFontMetaData( FILE *sfd, for ( i=classstart; ifirst_cnt; ++i ) { if (kernclassversion < 3) { getint(sfd,&temp); + if (temp < 0) { + LogError(_("Corrupted SFD file: Invalid kern class name length %d. Aborting load."), temp); + return false; + } kc->firsts[i] = malloc(temp+1); kc->firsts[i][temp] = '\0'; nlgetc(sfd); /* skip space */ fread(kc->firsts[i],1,temp,sfd); @@ -8307,6 +8311,10 @@ bool SFD_GetFontMetaData( FILE *sfd, for ( i=1; isecond_cnt; ++i ) { if (kernclassversion < 3) { getint(sfd,&temp); + if (temp < 0) { + LogError(_("Corrupted SFD file: Invalid kern class name length %d. Aborting load."), temp); + return false; + } kc->seconds[i] = malloc(temp+1); kc->seconds[i][temp] = '\0'; nlgetc(sfd); /* skip space */ fread(kc->seconds[i],1,temp,sfd); From cb443433e907588d2aa610172031b0c3899edb3c Mon Sep 17 00:00:00 2001 From: Rob Hagemans Date: Sun, 1 Feb 2026 21:20:30 +0000 Subject: [PATCH 47/96] Fix crashes on importing Mac-style dfont/NFNT bitmap fonts with some legal (but unusual?) values (#5246) --- fontforge/macbinary.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fontforge/macbinary.c b/fontforge/macbinary.c index 964ed25fa8..5532a4a7b6 100644 --- a/fontforge/macbinary.c +++ b/fontforge/macbinary.c @@ -2348,7 +2348,7 @@ static FOND *BuildFondList(FILE *f,long rlistpos,int subcnt,long rdata_pos, cur->stylewidths[j].style = getushort(f); cur->stylewidths[j].widthtab = malloc((cur->last-cur->first+3)*sizeof(short)); for ( k=cur->first; k<=cur->last+2; ++k ) - cur->stylewidths[j].widthtab[k] = getushort(f); + cur->stylewidths[j].widthtab[k-cur->first] = getushort(f); } } if ( kernoff!=0 && (flags&ttf_onlykerns) ) { @@ -2395,13 +2395,13 @@ static FOND *BuildFondList(FILE *f,long rlistpos,int subcnt,long rdata_pos, continue; /* this style doesn't exist */ format = stringoffsets[j]-1; stringlen = strings[0][0]; - if ( format!=0 ) + if ( format>0 ) for ( k=0; kpsnames[j] = malloc(stringlen+1); strcpy(pt,strings[ 0 ]+1); pt += strings[ 0 ][0]; - if ( format!=0 ) + if ( format>0 ) for ( k=0; k Date: Tue, 3 Feb 2026 03:03:39 +1000 Subject: [PATCH 48/96] [Python] make generateTtc use the active layer by default (#5221) --- fontforge/python.c | 1 + tests/CMakeLists.txt | 2 +- tests/{test1001.py => test_generate.py} | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) rename tests/{test1001.py => test_generate.py} (96%) diff --git a/fontforge/python.c b/fontforge/python.c index 63e0a16780..6c39cd0971 100644 --- a/fontforge/python.c +++ b/fontforge/python.c @@ -18047,6 +18047,7 @@ static PyObject *PyFFFont_GenerateTTC(PyFF_Font *self, PyObject *args, PyObject if ( CheckIfFontClosed(self) ) return(NULL); fv = self->fv; + layer = fv->active_layer; if ( !PyArg_ParseTupleAndKeywords(args, keywds, "sO|sOOsi", (char **)genttc_keywords, &filename, &others, &bitmaptype, &flags, &ttcflags, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ceb6c16e5..b55d3ffb84 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -99,7 +99,6 @@ if(ENABLE_PYTHON_SCRIPTING_RESULT) add_py_test(test0003.py "BoxesAndBounds.sfd" "Bounding box sanity tests") add_py_test(test0004.py "OpenType script tag tests") add_py_test(test0101.py "NotoSans-Regular.ttc" "FontForge .ttc file open check") - add_py_test(test1001.py "Caliban.sfd" "Generate several font files") add_py_test(test1001a.py "Zapfino-4.0d3.dfont" "Point count check") add_py_test(test1001b.py "Zapfino-4.1d6.dfont" "Odd morx test without crashing") add_py_test(test1001c.py "Make new font") @@ -134,6 +133,7 @@ if(ENABLE_PYTHON_SCRIPTING_RESULT) add_py_test(test932.py "Round-trip point selection tests") add_py_test(test933.py "Test for SplineSetJoin Expand Stroke logic") add_py_test(test934.py "SFDBitmapParsing.sfd" "Parsing of a malformed SFD") + add_py_test(test_generate.py "Caliban.sfd" "Generate several font files") add_py_test(test_fea_hyphens.py "Ambrosia.sfd" "Feature file with hyphens in glyph names") add_py_test(test_sfd_suppl_plane.py "Save TTF Names with emoji Unicode characters to SFD") add_py_test(test_fea_context_sub.py "Ambrosia.sfd" "Export contextual lookups to FEA") diff --git a/tests/test1001.py b/tests/test_generate.py similarity index 96% rename from tests/test1001.py rename to tests/test_generate.py index 837e9c1a1f..7bcd86cfca 100644 --- a/tests/test1001.py +++ b/tests/test_generate.py @@ -27,6 +27,9 @@ print("...Generated X11 opentype bitmap") caliban.generate("Caliban.dfont",bitmap_type="dfont") print("...Generated dfont w/ apple bitmaps") +caliban.generateTtc("Caliban.ttc", None) +print("...Generated TTC") + caliban.layers["Fore"].is_quadratic = 1 caliban.setTableData("cvt ","") caliban.selection.all() From 41b0c979485d832fadca8262cdc8f4b41703d302 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Tue, 3 Feb 2026 22:40:05 +0200 Subject: [PATCH 49/96] Simplify fake encoding trick in Metrics View (#5736) --- fontforge/splinefont.c | 52 ---------------------------- fontforge/splinefont.h | 4 --- fontforge/splinefont_enums.h | 4 +-- fontforge/tottf.c | 24 ------------- fontforge/views.h | 1 - fontforgeexe/metricsview.c | 31 +++++------------ fontforgeexe/shapers/harfbuzz.cpp | 44 ++++++++++++++++------- fontforgeexe/shapers/harfbuzz.hpp | 3 ++ fontforgeexe/shapers/shaper_shim.hpp | 6 ++-- 9 files changed, 46 insertions(+), 123 deletions(-) diff --git a/fontforge/splinefont.c b/fontforge/splinefont.c index 1678b1517c..7523024ab0 100644 --- a/fontforge/splinefont.c +++ b/fontforge/splinefont.c @@ -1888,58 +1888,6 @@ bigreal SFDescender(SplineFont *sf, int layer, int return_error) { return( result ); } -extern int SFFakeUnicodeBase(SplineFont *sf) { - int fake_unicode_base=0; - /* If they have nothing in Supplementary Private Use Area-A use it */ - /* If they have nothing in Supplementary Private Use Area-B use it */ - int a, al, ah, b, bl, bh; - int gid,k,max; - SplineChar *test; - SplineFont *_sf; - if ( sf->cidmaster ) sf = sf->cidmaster; - k=0; - a = al = ah = b = bl = bh = 0; - max = 0; - do { - _sf = ( sf->subfontcnt==0 ) ? sf : sf->subfonts[k]; - for ( gid=0; gid<_sf->glyphcnt; ++gid ) if ( (test=_sf->glyphs[gid])!=NULL ) { - if ( test->unicodeenc>=0xf0000 && test->unicodeenc<=0xfffff ) { - a = true; - if ( test->unicodeenc<0xf8000 ) - al = true; - else - ah = true; - } else if ( test->unicodeenc>=0x100000 && test->unicodeenc<=0x10ffff ) { - b = true; - if ( test->unicodeenc<0x108000 ) - bl = true; - else - bh = true; - } - } - if ( gid>max ) max = gid; - ++k; - } while ( ksubfontcnt ); - if ( !a ) /* Nothing in SPUA-A */ - fake_unicode_base = 0xf0000; - else if ( !b ) - fake_unicode_base = 0x100000; - else if ( max<0x8000 ) { - if ( !al ) - fake_unicode_base = 0xf0000; - else if ( !ah ) - fake_unicode_base = 0xf8000; - else if ( !bl ) - fake_unicode_base = 0x100000; - else if ( !bh ) - fake_unicode_base = 0x108000; - } - if ( fake_unicode_base==0 ) - fake_unicode_base = -1; - - return fake_unicode_base; -} - static void arraystring(char *buffer,real *array,int cnt) { int i, ei; diff --git a/fontforge/splinefont.h b/fontforge/splinefont.h index dae2a530f3..9d41160de4 100644 --- a/fontforge/splinefont.h +++ b/fontforge/splinefont.h @@ -1853,10 +1853,6 @@ extern bigreal SFXHeight(SplineFont *sf, int layer, int return_error); extern bigreal SFAscender(SplineFont *sf, int layer, int return_error); extern bigreal SFDescender(SplineFont *sf, int layer, int return_error); -/* Find Private Use Area range big enough to accomodate unencoded glyphs - with fake encoding slots. */ -extern int SFFakeUnicodeBase(SplineFont *sf); - extern void SCRemoveKern(SplineChar* sc); extern void SCRemoveVKern(SplineChar* sc); diff --git a/fontforge/splinefont_enums.h b/fontforge/splinefont_enums.h index 7aeaca89b2..a5182e2cc8 100644 --- a/fontforge/splinefont_enums.h +++ b/fontforge/splinefont_enums.h @@ -101,10 +101,8 @@ enum ttf_flags { ttf_flag_symbol = 1 << 14, ttf_flag_dummyDSIG = 1 << 15, ttf_native_kern = 1 << 16, // This applies mostly to U. F. O. right now. - ttf_flag_fake_map = - 1 << 17, // Set fake unicode mappings for unmapped glyphs ttf_flag_no_outlines = - 1 << 18, // HarfBuzz: Skip "glyf" and "CFF " tables for performance + 1 << 17, // HarfBuzz: Skip "glyf" and "CFF " tables for performance ttf_flag_oldkernmappedonly = 1 << 29, // Allow only mapped glyphs in the old-style "kern" table, // required for Windows compatibility diff --git a/fontforge/tottf.c b/fontforge/tottf.c index 92d9f1137d..e314e112ac 100644 --- a/fontforge/tottf.c +++ b/fontforge/tottf.c @@ -6165,21 +6165,6 @@ int _WriteTTFFont(FILE *ttf,SplineFont *sf,enum fontformat format, if ( sf->subfontcnt!=0 ) sf = sf->subfonts[0]; } - // Temporarily assign a fake Private Area unicode point to all unmapped glyphs - if (flags & ttf_flag_fake_map) { - int fake_unicode_base = SFFakeUnicodeBase(sf); - if (fake_unicode_base == -1) - fake_unicode_base = 0xfffd; - - fake_mappings = calloc(sf->glyphcnt,sizeof(bool)); - for (i = 0; i < sf->glyphcnt; ++i) { - if (sf->glyphs[i] && sf->glyphs[i]->unicodeenc == -1) { - sf->glyphs[i]->unicodeenc = fake_unicode_base + sf->glyphs[i]->orig_pos; - fake_mappings[i] = true; - } - } - } - if ( sf->subfontcnt==0 ) { anyglyphs = false; for ( i=sf->glyphcnt-1; i>=0 ; --i ) { @@ -6218,15 +6203,6 @@ int _WriteTTFFont(FILE *ttf,SplineFont *sf,enum fontformat format, dumpttf(ttf,&at); } - // Remove temporarily assigned fake Private Area unicode point from all unmapped glyphs - if (flags & ttf_flag_fake_map) { - for (i = 0; i < sf->glyphcnt; ++i) { - if (sf->glyphs[i] && fake_mappings[i]) - sf->glyphs[i]->unicodeenc = -1; - } - free(fake_mappings); - } - switch_to_old_locale(&tmplocale, &oldlocale); // Switch to the cached locale. SubtableMap_delete(&at.subtable_map); if ( at.error || ferror(ttf)) diff --git a/fontforge/views.h b/fontforge/views.h index d7d0213d3d..abf1e8d931 100644 --- a/fontforge/views.h +++ b/fontforge/views.h @@ -420,7 +420,6 @@ typedef struct metricsview { GTextInfo *scriptlangs; int word_index; int layer; - int fake_unicode_base; GIC *gwgic; int ptsize, dpi; int ybaseline; diff --git a/fontforgeexe/metricsview.c b/fontforgeexe/metricsview.c index c6d38f4108..cab3f97ff1 100644 --- a/fontforgeexe/metricsview.c +++ b/fontforgeexe/metricsview.c @@ -483,7 +483,7 @@ return; } static void MVSetSubtables(SplineFont *sf) { - GTextInfo **ti; + GTextInfo **ti = NULL; OTLookup *otl; struct lookup_subtable *sub; int cnt, doit; @@ -1622,9 +1622,8 @@ static SplineChar *MVSCFromUnicode(MetricsView *mv, SplineFont *sf, EncMap *map, int i; SplineChar *sc; - if ( mv->fake_unicode_base && ch>=mv->fake_unicode_base && - ch<=mv->fake_unicode_base+mv->sf->glyphcnt ) -return( mv->sf->glyphs[ch-mv->fake_unicode_base] ); + if ( ch>=FAKE_UNICODE_BASE && chsf->glyphcnt ) + return( mv->sf->glyphs[ch-FAKE_UNICODE_BASE] ); i = SFFindSlot(sf,map,ch,NULL); if ( i==-1 ) @@ -1820,27 +1819,17 @@ static void MVVScroll(MetricsView *mv,struct sbevent *sb) { } static int MVFakeUnicodeOfSc(MetricsView *mv, SplineChar *sc) { - - if ( sc->unicodeenc!=-1 ) -return( sc->unicodeenc ); - - if ( mv->fake_unicode_base==0 ) { /* Not set */ - mv->fake_unicode_base = SFFakeUnicodeBase(mv->sf); - } - - if ( mv->fake_unicode_base==-1 ) -return( 0xfffd ); + if (sc->unicodeenc != -1) + return sc->unicodeenc; else -return( mv->fake_unicode_base+sc->orig_pos ); + return FAKE_UNICODE_BASE + sc->orig_pos; } static int MVOddMatch(MetricsView *mv,int uni,SplineChar *sc) { if ( sc->unicodeenc!=-1 ) return( false ); - else if ( mv->fake_unicode_base<=0 ) -return( uni==0xfffd ); else -return( uni>=mv->fake_unicode_base && sc->orig_pos == uni-mv->fake_unicode_base ); +return( uni>=FAKE_UNICODE_BASE && sc->orig_pos == uni-FAKE_UNICODE_BASE ); } void MVSetSCs(MetricsView *mv, SplineChar **scs) { @@ -1923,8 +1912,7 @@ return; /* Nothing changed */ missing = 0; for ( tpt=pt; tptfake_unicode_base>0 && *tpt>=mv->fake_unicode_base && - *tpt<=mv->fake_unicode_base+mv->sf->glyphcnt ) + if ( *tpt>=FAKE_UNICODE_BASE && *tptsf->glyphcnt ) /* That's ok */; else if ( SFFindSlot(mv->sf,mv->fv->b.map,*tpt,NULL)==-1 ) ++missing; @@ -3949,7 +3937,6 @@ static ShaperContext* MVMakeShaperContext(MetricsView *mv) { context->sf = mv->sf; context->mv = mv; context->apply_ticked_features = ApplyTickedFeatures; - context->fake_unicode = MVFakeUnicodeOfSc; context->get_enc_map = SFGetMap; context->get_char_width = MVCharWidth; context->get_metrics = MVGetMetrics; @@ -5190,7 +5177,7 @@ GTextInfo *SLOfFont(SplineFont *sf) { int s, l, i, k, cnt; extern GTextInfo scripts[], languages[]; GTextInfo *ret = NULL; - char *sname, *lname, *temp; + char *sname=NULL, *lname, *temp; char sbuf[8], lbuf[8]; LookupUIInit(); diff --git a/fontforgeexe/shapers/harfbuzz.cpp b/fontforgeexe/shapers/harfbuzz.cpp index 7e9761666a..63c44a8e69 100644 --- a/fontforgeexe/shapers/harfbuzz.cpp +++ b/fontforgeexe/shapers/harfbuzz.cpp @@ -36,18 +36,24 @@ extern "C" { namespace ff::shapers { +static hb_bool_t resolve_fake_encoding(hb_font_t* font, void* font_data, + hb_codepoint_t unicode, + hb_codepoint_t* glyph, void* user_data) { + hb_font_t* parent = (hb_font_t*)font_data; + + if (unicode >= FAKE_UNICODE_BASE) { + *glyph = unicode - FAKE_UNICODE_BASE; + return true; + } + return hb_font_get_nominal_glyph(parent, unicode, glyph); +} + HarfBuzzShaper::HarfBuzzShaper(std::shared_ptr context) : context_(context) { FILE* ttf_file = GFileTmpfile(); - // HarfBuzz can only accept Unicode as the input buffer, but the Metrics - // View must support the capability to shape glyphs which don't have - // assigned Unicode values. When writing a temporary TrueType font, we need - // to make sure that every glyph is assigned Unicode value (possibly fake, - // see flag ttf_flag_fake_map), and that we have means to map each - // SplineChar to its assigned Unicode value. _WriteTTFFont(ttf_file, context_->sf, ff_ttf, NULL, bf_ttf, - ttf_flag_otmode | ttf_flag_fake_map | ttf_flag_no_outlines, + ttf_flag_otmode | ttf_flag_no_outlines, context_->get_enc_map(context_->sf), ly_fore); // Build map of TTF codepoints @@ -70,14 +76,26 @@ HarfBuzzShaper::HarfBuzzShaper(std::shared_ptr context) hb_blob_create(blob, blob_size, HB_MEMORY_MODE_WRITABLE, NULL, NULL); hb_ttf_face = hb_face_create(hb_ttf_blob, 0); + hb_ttf_raw_font = hb_font_create(hb_ttf_face); + + // To access unencoded glyphs with HarfBuzz, we need to assign them fake + // encodings above the canonic Unicode range. We also need to create a + // subfont with custom encoding resolution callback. + hb_ttf_font = hb_font_create_sub_font(hb_ttf_raw_font); + hb_font_funcs_t* funcs = hb_font_funcs_create(); + hb_font_funcs_set_nominal_glyph_func(funcs, resolve_fake_encoding, NULL, + NULL); + hb_font_set_funcs(hb_ttf_font, funcs, hb_ttf_raw_font, NULL); - hb_ttf_font = hb_font_create(hb_ttf_face); + // Contrary to the name, it just decreases the reference. + hb_font_funcs_destroy(funcs); fclose(ttf_file); } HarfBuzzShaper::~HarfBuzzShaper() { hb_font_destroy(hb_ttf_font); + hb_font_destroy(hb_ttf_raw_font); hb_face_destroy(hb_ttf_face); hb_blob_destroy(hb_ttf_blob); delete[] blob; @@ -278,16 +296,16 @@ ShaperOutput HarfBuzzShaper::apply_features( SplineChar** glyphs, const std::map& feature_map, Tag script, Tag lang, int pixelsize, bool vertical) { std::vector u_vec; + // Assigned fake encodings will be interpreted by resolve_fake_encoding(). for (size_t len = 0; glyphs[len] != NULL; ++len) { - u_vec.push_back( - (glyphs[len]->unicodeenc > 0) - ? glyphs[len]->unicodeenc - : context_->fake_unicode(context_->mv, glyphs[len])); + u_vec.push_back((glyphs[len]->unicodeenc > 0) + ? glyphs[len]->unicodeenc + : (glyphs[len]->ttf_glyph + FAKE_UNICODE_BASE)); } u_vec.push_back(0); hb_buffer_t* hb_buffer = hb_buffer_create(); - hb_buffer_add_utf32(hb_buffer, u_vec.data(), -1, 0, -1); + hb_buffer_add_codepoints(hb_buffer, u_vec.data(), -1, 0, -1); // Set script and language hb_script_t hb_script = hb_ot_tag_to_script(script); diff --git a/fontforgeexe/shapers/harfbuzz.hpp b/fontforgeexe/shapers/harfbuzz.hpp index 4c1653812c..77c645843d 100644 --- a/fontforgeexe/shapers/harfbuzz.hpp +++ b/fontforgeexe/shapers/harfbuzz.hpp @@ -55,6 +55,9 @@ class HarfBuzzShaper : public IShaper { char* blob = nullptr; hb_blob_t* hb_ttf_blob = nullptr; hb_face_t* hb_ttf_face = nullptr; + hb_font_t* hb_ttf_raw_font = nullptr; + + // HarfBuzz subfont with custom resolution for fake encodings hb_font_t* hb_ttf_font = nullptr; // Map glyph indexes in TTF file to SplineChar objects diff --git a/fontforgeexe/shapers/shaper_shim.hpp b/fontforgeexe/shapers/shaper_shim.hpp index 854e07864c..3ebdbf8264 100644 --- a/fontforgeexe/shapers/shaper_shim.hpp +++ b/fontforgeexe/shapers/shaper_shim.hpp @@ -37,7 +37,8 @@ typedef struct splinefont SplineFont; typedef struct metricsview MetricsView; typedef struct encmap EncMap; -static int INVALID_KERN_OFFSET = 0x7ffffff; +static const int INVALID_KERN_OFFSET = 0x7ffffff; +static const int FAKE_UNICODE_BASE = 0x110000; char* u2utf8_copy(const unichar_t* ubuf); int _WriteTTFFont(FILE* ttf, SplineFont* sf, int /*enum fontformat*/ format, @@ -57,9 +58,6 @@ typedef struct shaper_context { SplineFont* sf, uint32_t* flist, uint32_t script, uint32_t lang, bool gpos_only, int pixelsize, SplineChar** glyphs); - // Map glyphs without unicode value to a private area - int (*fake_unicode)(MetricsView* mv, SplineChar* sc); - // Get encoding map EncMap* (*get_enc_map)(SplineFont* sf); From 4a69512ef6d063ef30f0a1a74b7bd01baaca7254 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Fri, 6 Feb 2026 17:11:11 +0100 Subject: [PATCH 50/96] Quickfix a collision between ttf_flags and ps_flags (#5678) (#5681) --------- Co-authored-by: Maxim Iorsh --- fontforge/savefont.c | 3 --- fontforge/savefont.h | 4 ++-- fontforge/splinechar.h | 7 +++---- fontforge/splinefont_enums.h | 35 +++++++++++++++++------------------ fontforgeexe/savefontdlg.c | 11 ----------- 5 files changed, 22 insertions(+), 38 deletions(-) diff --git a/fontforge/savefont.c b/fontforge/savefont.c index 7bd2f662f9..e164e0c504 100644 --- a/fontforge/savefont.c +++ b/fontforge/savefont.c @@ -1042,7 +1042,6 @@ int fmflag2ttfflag(int fmflags, bool is_postscript_or_cff) { if ( !is_postscript_or_cff ) { if ( fmflags&fm_flag_afm ) ttfflags |= ps_flag_afm; if ( fmflags&fm_flag_pfm ) ttfflags |= ps_flag_pfm; - if ( fmflags&fm_flag_nohintsubs ) ttfflags |= ps_flag_nohintsubs; if ( fmflags&fm_flag_noflex ) ttfflags |= ps_flag_noflex; if ( fmflags&fm_flag_nopshints ) ttfflags |= ps_flag_nohints; if ( fmflags&fm_flag_round ) ttfflags |= ps_flag_round; @@ -1198,10 +1197,8 @@ int GenerateScript(SplineFont *sf,char *filename,const char *bitmaptype, int fmf if ( fmflags&fm_flag_afm ) old_ps_flags |= ps_flag_afm; if ( fmflags&fm_flag_pfm ) old_ps_flags |= ps_flag_pfm; if ( fmflags&fm_flag_tfm ) old_ps_flags |= ps_flag_tfm; - if ( fmflags&fm_flag_nohintsubs ) old_ps_flags |= ps_flag_nohintsubs; if ( fmflags&fm_flag_noflex ) old_ps_flags |= ps_flag_noflex; if ( fmflags&fm_flag_nopshints ) old_ps_flags |= ps_flag_nohints; - if ( fmflags&fm_flag_restrict256 ) old_ps_flags |= ps_flag_restrict256; if ( fmflags&fm_flag_round ) old_ps_flags |= ps_flag_round; if ( fmflags&fm_flag_afmwithmarks ) old_ps_flags |= ps_flag_afmwithmarks; if ( i==bf_otb ) { diff --git a/fontforge/savefont.h b/fontforge/savefont.h index 7bc4826085..020de7f8e0 100644 --- a/fontforge/savefont.h +++ b/fontforge/savefont.h @@ -49,10 +49,10 @@ enum fm_flags { fm_flag_afm = 0x1, fm_flag_dummyDSIG = 0x4000, fm_flag_nofftm = 0x8000, fm_flag_tfm = 0x10000, - fm_flag_nohintsubs = 0x20000, + fm_flag_noflex = 0x40000, fm_flag_nopshints = 0x80000, - fm_flag_restrict256 = 0x100000, + fm_flag_round = 0x200000, fm_flag_afmwithmarks = 0x400000, fm_flag_pfed_lookups = 0x800000, diff --git a/fontforge/splinechar.h b/fontforge/splinechar.h index af9bad4cd0..25f7d546b1 100644 --- a/fontforge/splinechar.h +++ b/fontforge/splinechar.h @@ -731,7 +731,6 @@ typedef struct splinechar_ttf_map { } SplineCharTTFMap; /* Returns NULL if the name is valid, or error message. If the function returned - error message and set `questionable` to true, the name should be accepted, but - discouraged. - The error message should not be freed. */ -const char* SCNameCheck(const unichar_t *name, bool *questionable); + error message and set `questionable` to true, the name should be accepted, + but discouraged. The error message should not be freed. */ +const char* SCNameCheck(const unichar_t* name, bool* questionable); diff --git a/fontforge/splinefont_enums.h b/fontforge/splinefont_enums.h index a5182e2cc8..8b1469fdd9 100644 --- a/fontforge/splinefont_enums.h +++ b/fontforge/splinefont_enums.h @@ -84,6 +84,7 @@ enum bitmapformat { }; enum ttf_flags { + // WARNING: These flags must not collide with ps_flags ttf_flag_shortps = 1 << 0, ttf_flag_nohints = 1 << 1, ttf_flag_applemode = 1 << 2, @@ -102,7 +103,7 @@ enum ttf_flags { ttf_flag_dummyDSIG = 1 << 15, ttf_native_kern = 1 << 16, // This applies mostly to U. F. O. right now. ttf_flag_no_outlines = - 1 << 17, // HarfBuzz: Skip "glyf" and "CFF " tables for performance + 1 << 19, // HarfBuzz: Skip "glyf" and "CFF " tables for performance ttf_flag_oldkernmappedonly = 1 << 29, // Allow only mapped glyphs in the old-style "kern" table, // required for Windows compatibility @@ -120,25 +121,23 @@ enum openflags { }; enum ps_flags { - ps_flag_nohintsubs = 0x10000, - ps_flag_noflex = 0x20000, - ps_flag_nohints = 0x40000, - ps_flag_restrict256 = 0x80000, - ps_flag_afm = 0x100000, - ps_flag_pfm = 0x200000, - ps_flag_tfm = 0x400000, - ps_flag_round = 0x800000, + // WARNING: These flags must not collide with ttf_flags + ps_flag_noflex = 1 << 17, + ps_flag_nohints = 1 << 18, + ps_flag_afm = 1 << 20, + ps_flag_pfm = 1 << 21, + ps_flag_tfm = 1 << 22, + ps_flag_round = 1 << 23, /* CFF fonts are wrapped up in some postscript sugar -- unless they are to - */ - /* go into a pdf file or an otf font */ - ps_flag_nocffsugar = 0x1000000, + go into a pdf file or an otf font */ + ps_flag_nocffsugar = 1 << 24, /* in type42 cid fonts we sometimes want an identity map from gid to cid */ - ps_flag_identitycidmap = 0x2000000, - ps_flag_afmwithmarks = 0x4000000, - ps_flag_noseac = 0x8000000, - ps_flag_outputfontlog = 0x10000000, - ps_flag_mask = (ps_flag_nohintsubs | ps_flag_noflex | ps_flag_afm | - ps_flag_pfm | ps_flag_tfm | ps_flag_round) + ps_flag_identitycidmap = 1 << 25, + ps_flag_afmwithmarks = 1 << 26, + ps_flag_noseac = 1 << 27, + ps_flag_outputfontlog = 1 << 28, + ps_flag_mask = (ps_flag_noflex | ps_flag_afm | ps_flag_pfm | ps_flag_tfm | + ps_flag_round) }; enum layer_type { diff --git a/fontforgeexe/savefontdlg.c b/fontforgeexe/savefontdlg.c index b478227648..1f16989817 100644 --- a/fontforgeexe/savefontdlg.c +++ b/fontforgeexe/savefontdlg.c @@ -255,7 +255,6 @@ static int OPT_PSHints(GGadget *g, GEvent *e) { int flags = (&d->ps_flags)[which]; /*GGadgetSetEnabled(GWidgetGetControl(gw,CID_PS_HintSubs),true);*/ GGadgetSetEnabled(GWidgetGetControl(gw,CID_PS_Flex),true); - /*GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_HintSubs),!(flags&ps_flag_nohintsubs));*/ GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_Flex),!(flags&ps_flag_noflex)); } else { /*GGadgetSetEnabled(GWidgetGetControl(gw,CID_PS_HintSubs),false);*/ @@ -308,16 +307,12 @@ return( false ); d->ps_flags |= ps_flag_pfm; if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_TFM)) ) d->ps_flags |= ps_flag_tfm; - /*if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_HintSubs)) )*/ - /*d->ps_flags |= ps_flag_nohintsubs;*/ if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Flex)) ) d->ps_flags |= ps_flag_noflex; if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Hints)) ) d->ps_flags |= ps_flag_nohints; if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Round)) ) d->ps_flags |= ps_flag_round; - /*if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Restrict256)) )*/ - /*d->ps_flags |= ps_flag_restrict256;*/ if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_FontLog)) ) d->ps_flags |= ps_flag_outputfontlog; } else if ( d->sod_which==1 || d->sod_which==2 ) { /* Open/TrueType */ @@ -358,8 +353,6 @@ return( false ); d->sfnt_flags |= ps_flag_afm; if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_AFMmarks)) ) d->sfnt_flags |= ps_flag_afmwithmarks; - /*if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_HintSubs)) )*/ - /*d->sfnt_flags |= ps_flag_nohintsubs;*/ if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Flex)) ) d->sfnt_flags |= ps_flag_noflex; if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Hints)) ) @@ -379,8 +372,6 @@ return( false ); d->ps_flags = d->psotb_flags = 0; if ( GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_AFMmarks)) ) d->psotb_flags = d->ps_flags |= ps_flag_afmwithmarks; - /*if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_HintSubs)) )*/ - /*d->psotb_flags = d->ps_flags |= ps_flag_nohintsubs;*/ if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Flex)) ) d->psotb_flags = d->ps_flags |= ps_flag_noflex; if ( !GGadgetIsChecked(GWidgetGetControl(gw,CID_PS_Hints)) ) @@ -433,9 +424,7 @@ static void OptSetDefaults(GWindow gw,struct gfc_data *d,int which,int iscid) { /* which==3 => generating an sfnt based bitmap only font with a postscript outline font */ GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_Hints),!(flags&ps_flag_nohints)); - /*GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_HintSubs),!(flags&ps_flag_nohintsubs));*/ GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_Flex),!(flags&ps_flag_noflex)); - /*GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_Restrict256),flags&ps_flag_restrict256);*/ GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_Round),flags&ps_flag_round); GGadgetSetChecked(GWidgetGetControl(gw,CID_PS_AFM),flags&ps_flag_afm); From b2789ba46ef0cf01e97ff2f150a3a385caf53ed9 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Mon, 9 Feb 2026 20:12:04 +0200 Subject: [PATCH 51/96] Make fontforge module dependent on shapers (#5749) * Link HarfBuzz for non-GUI mode * Add virtual IShaper::apply_features() / mv_apply_features() * Refactor HarfBuzzShaper and BuiltInShaper * Expand struct MetricsCore * Decouple shapers module by using shaper_context --- .clang-files | 6 +- CMakeLists.txt | 9 +- fontforge/CMakeLists.txt | 4 +- .../shapers/CMakeLists.txt | 5 +- .../shapers/builtin.cpp | 41 +++-- .../shapers/builtin.hpp | 19 ++- .../shapers/harfbuzz.cpp | 151 +++++++++--------- .../shapers/harfbuzz.hpp | 28 ++-- .../shapers/i_shaper.hpp | 26 ++- fontforge/{ => shapers}/metrics.h | 16 ++ fontforge/shapers/opentype_str.h | 63 ++++++++ .../shapers/shaper_shim.cpp | 11 +- .../shapers/shaper_shim.hpp | 34 ++-- fontforge/splinechar.c | 7 + fontforge/splinechar.h | 39 +---- fontforge/splinefont.c | 4 - fontforge/splinefont.h | 1 - fontforge/tottf.c | 12 +- fontforge/tottf.h | 3 +- fontforge/views.h | 1 - fontforgeexe/CMakeLists.txt | 1 - fontforgeexe/metricsview.c | 35 ++-- inc/ustring.h | 2 +- 23 files changed, 315 insertions(+), 203 deletions(-) rename {fontforgeexe => fontforge}/shapers/CMakeLists.txt (83%) rename {fontforgeexe => fontforge}/shapers/builtin.cpp (76%) rename {fontforgeexe => fontforge}/shapers/builtin.hpp (77%) rename {fontforgeexe => fontforge}/shapers/harfbuzz.cpp (87%) rename {fontforgeexe => fontforge}/shapers/harfbuzz.hpp (84%) rename {fontforgeexe => fontforge}/shapers/i_shaper.hpp (73%) rename fontforge/{ => shapers}/metrics.h (84%) create mode 100644 fontforge/shapers/opentype_str.h rename {fontforgeexe => fontforge}/shapers/shaper_shim.cpp (93%) rename {fontforgeexe => fontforge}/shapers/shaper_shim.hpp (83%) diff --git a/.clang-files b/.clang-files index c2c7cec6d9..4b35b8fe06 100644 --- a/.clang-files +++ b/.clang-files @@ -1,10 +1,10 @@ inc/tag.hpp -fontforge/metrics.h fontforge/splinechar.h fontforge/splinefont_enums.h fontforge/std_maps.cpp fontforge/std_maps.hpp -fontforgeexe/shapers/*.cpp -fontforgeexe/shapers/*.hpp +fontforge/shapers/*.cpp +fontforge/shapers/*.hpp +fontforge/shapers/*.h fontforgeexe/gtk/*.cpp fontforgeexe/gtk/*.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f0290871..cb1d209658 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,16 +124,17 @@ if(HAVE_PTHREAD_H) find_package(Threads) endif() +if (ENABLE_HARFBUZZ) + find_package(PkgConfig REQUIRED) + pkg_check_modules(HarfBuzz REQUIRED IMPORTED_TARGET harfbuzz) +endif() + # Requirements for options if(ENABLE_GUI) find_package(GDK3 3.10 REQUIRED) find_package(PkgConfig) pkg_check_modules(GTK3 REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-3.0) - find_package(PkgConfig) - if (ENABLE_HARFBUZZ) - pkg_check_modules(HarfBuzz REQUIRED IMPORTED_TARGET harfbuzz) - endif() endif() if(ENABLE_FREETYPE_DEBUGGER) diff --git a/fontforge/CMakeLists.txt b/fontforge/CMakeLists.txt index 3fcdf53801..a61f375b11 100644 --- a/fontforge/CMakeLists.txt +++ b/fontforge/CMakeLists.txt @@ -92,7 +92,6 @@ set(FONTFORGE_INST_HEADERS groups.h lookups.h mem.h - metrics.h mm.h namehash.h nonlineartrans.h @@ -217,6 +216,8 @@ add_library(fontforge zapfnomen.c ) +add_subdirectory(shapers) + if(ENABLE_WOFF2_RESULT) target_sources(fontforge PRIVATE woff2.cc) endif() @@ -242,6 +243,7 @@ target_include_directories(fontforge PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(fontforge PUBLIC fontforge_common_headers + shapers Freetype::Freetype LibXml2::LibXml2 GLIB::GLIB diff --git a/fontforgeexe/shapers/CMakeLists.txt b/fontforge/shapers/CMakeLists.txt similarity index 83% rename from fontforgeexe/shapers/CMakeLists.txt rename to fontforge/shapers/CMakeLists.txt index 5de0f998c9..d9c04cfc7f 100644 --- a/fontforgeexe/shapers/CMakeLists.txt +++ b/fontforge/shapers/CMakeLists.txt @@ -9,9 +9,12 @@ add_library(shapers OBJECT shaper_shim.cpp ) +if(BUILD_SHARED_LIBS) + set_property(TARGET shapers PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() + target_link_libraries(shapers PRIVATE fontforge_common_headers - fontforge gunicode ) diff --git a/fontforgeexe/shapers/builtin.cpp b/fontforge/shapers/builtin.cpp similarity index 76% rename from fontforgeexe/shapers/builtin.cpp rename to fontforge/shapers/builtin.cpp index 30d80ff07e..2c633a36af 100644 --- a/fontforgeexe/shapers/builtin.cpp +++ b/fontforge/shapers/builtin.cpp @@ -25,13 +25,26 @@ #include #include -extern "C" { -#include "splinechar.h" -} - namespace ff::shapers { -ShaperOutput BuiltInShaper::apply_features( +std::vector BuiltInShaper::apply_features( + const std::vector& ubuf, const std::map& feature_map, + Tag script, Tag lang, bool vertical) { + std::vector glyphs; + + // ubuf is zero-terminated, ignore its last zero element. + for (unichar_t u : ubuf) { + if (u != 0) + glyphs.push_back(context_->get_or_make_char(context_->sf, u, NULL)); + } + glyphs.push_back(NULL); // NULL-terminated array + ShaperOutput shaper_output = mv_apply_features(glyphs.data(), feature_map, + script, lang, 0, vertical); + scale_metrics(NULL, shaper_output.second.data(), 1.0, 1.0, vertical); + return shaper_output.second; +} + +ShaperOutput BuiltInShaper::mv_apply_features( SplineChar** glyphs, const std::map& feature_map, Tag script, Tag lang, int pixelsize, bool vertical) { // Zero-terminated list of enabled features @@ -64,23 +77,29 @@ ShaperOutput BuiltInShaper::apply_features( return {ots_arr_, metrics}; } -void BuiltInShaper::scale_metrics(MetricsView* mv, double iscale, double scale, - bool vertical) { - MetricsCore* metrics = context_->get_metrics(mv, NULL); +void BuiltInShaper::scale_metrics(MetricsView* mv, MetricsCore* metrics, + double iscale, double scale, bool vertical) { // Calculate positions. int x = 10; int y = 10; for (int i = 0; ots_arr_[i].sc != NULL; ++i) { assert(!metrics[i].scaled); - SplineChar* sc = ots_arr_[i].sc; - metrics[i].dwidth = rint(iscale * context_->get_char_width(mv, sc)); + SplineChar* sc = metrics[i].sc = ots_arr_[i].sc; + int16_t width, vwidth; + int ttf_glyph = -1; + + context_->get_encoding(sc, NULL, &ttf_glyph); + metrics[i].codepoint = ttf_glyph; + + context_->get_char_metrics(mv, sc, &width, &vwidth); + metrics[i].dwidth = rint(iscale * width); metrics[i].dx = x; metrics[i].xoff = rint(iscale * ots_arr_[i].vr.xoff); metrics[i].yoff = rint(iscale * ots_arr_[i].vr.yoff); metrics[i].kernafter = rint(iscale * ots_arr_[i].vr.h_adv_off); x += metrics[i].dwidth + metrics[i].kernafter; - metrics[i].dheight = rint(sc->vwidth * scale); + metrics[i].dheight = rint(vwidth * scale); metrics[i].dy = y; if (vertical) { metrics[i].kernafter = rint(iscale * ots_arr_[i].vr.v_adv_off); diff --git a/fontforgeexe/shapers/builtin.hpp b/fontforge/shapers/builtin.hpp similarity index 77% rename from fontforgeexe/shapers/builtin.hpp rename to fontforge/shapers/builtin.hpp index eeef4ffc03..d0a5f455cb 100644 --- a/fontforgeexe/shapers/builtin.hpp +++ b/fontforge/shapers/builtin.hpp @@ -35,13 +35,18 @@ class BuiltInShaper : public IShaper { const char* name() const override { return "builtin"; } - ShaperOutput apply_features(SplineChar** glyphs, - const std::map& feature_map, - Tag script, Tag lang, int pixelsize, - bool vertical) override; - - void scale_metrics(MetricsView* mv, double iscale, double scale, - bool vertical) override; + std::vector apply_features( + const std::vector& ubuf, + const std::map& feature_map, Tag script, Tag lang, + bool vertical) override; + + ShaperOutput mv_apply_features(SplineChar** glyphs, + const std::map& feature_map, + Tag script, Tag lang, int pixelsize, + bool vertical) override; + + void scale_metrics(MetricsView* mv, MetricsCore* metrics, double iscale, + double scale, bool vertical) override; std::set default_features(Tag script, Tag lang, bool vertical) const override; diff --git a/fontforgeexe/shapers/harfbuzz.cpp b/fontforge/shapers/harfbuzz.cpp similarity index 87% rename from fontforgeexe/shapers/harfbuzz.cpp rename to fontforge/shapers/harfbuzz.cpp index 63c44a8e69..cf40917f37 100644 --- a/fontforgeexe/shapers/harfbuzz.cpp +++ b/fontforge/shapers/harfbuzz.cpp @@ -30,8 +30,6 @@ extern "C" { #include "gfile.h" -#include "splinechar.h" -#include "splinefont_enums.h" } namespace ff::shapers { @@ -52,12 +50,9 @@ HarfBuzzShaper::HarfBuzzShaper(std::shared_ptr context) : context_(context) { FILE* ttf_file = GFileTmpfile(); - _WriteTTFFont(ttf_file, context_->sf, ff_ttf, NULL, bf_ttf, - ttf_flag_otmode | ttf_flag_no_outlines, - context_->get_enc_map(context_->sf), ly_fore); + SplineCharTTFMap* ttf_map = + context_->write_font_into_memory(ttf_file, context_->sf); - // Build map of TTF codepoints - SplineCharTTFMap* ttf_map = MakeGlyphTTFMap(context_->sf); for (SplineCharTTFMap* entry = ttf_map; entry->glyph != NULL; ++entry) { ttf_map_[entry->ttf_glyph] = entry->glyph; } @@ -129,18 +124,14 @@ std::vector HarfBuzzShaper::hb_features( return hb_feature_vec; } -std::pair> -HarfBuzzShaper::extract_shaped_data(hb_buffer_t* hb_buffer) { +std::vector HarfBuzzShaper::extract_shaped_data( + hb_buffer_t* hb_buffer) { unsigned int glyph_count; hb_glyph_info_t* glyph_info_arr = hb_buffer_get_glyph_infos(hb_buffer, &glyph_count); hb_glyph_position_t* glyph_pos_arr = hb_buffer_get_glyph_positions(hb_buffer, &glyph_count); - // Return NULL-terminated raw C-style array of pointers - SplineChar** glyphs_after_gpos = - (SplineChar**)calloc(glyph_count + 1, sizeof(SplineChar*)); - std::vector metrics(glyph_count + 1); // Process the glyphs and positions @@ -156,12 +147,15 @@ HarfBuzzShaper::extract_shaped_data(hb_buffer_t* hb_buffer) { SplineChar* glyph_out = (ttf_map_it != ttf_map_.end()) ? ttf_map_it->second : get_notdef_glyph(); + int16_t width, vwidth; + context_->get_char_metrics(NULL, glyph_out, &width, &vwidth); - glyphs_after_gpos[i] = glyph_out; + metrics[i].sc = glyph_out; + metrics[i].codepoint = glyph_info.codepoint; // Fill unscaled metrics in font units - metrics[i].dwidth = glyph_out->width; - metrics[i].dheight = glyph_out->vwidth; + metrics[i].dwidth = width; + metrics[i].dheight = vwidth; metrics[i].xoff = glyph_pos.x_offset; metrics[i].yoff = glyph_pos.y_offset; @@ -181,7 +175,7 @@ HarfBuzzShaper::extract_shaped_data(hb_buffer_t* hb_buffer) { metrics.back().dy = total_y_advance; metrics.back().scaled = false; - return {glyphs_after_gpos, metrics}; + return metrics; } std::vector HarfBuzzShaper::reverse_rtl_metrics( @@ -195,6 +189,8 @@ std::vector HarfBuzzShaper::reverse_rtl_metrics( for (int i = 0; i < glyph_count; ++i) { int rev_idx = glyph_count - i - 1; + fixed_metrics[i].sc = reverse_metrics[rev_idx].sc; + fixed_metrics[i].codepoint = reverse_metrics[rev_idx].codepoint; fixed_metrics[i].dwidth = reverse_metrics[rev_idx].dwidth; fixed_metrics[i].dheight = reverse_metrics[rev_idx].dheight; @@ -238,10 +234,8 @@ std::vector HarfBuzzShaper::reverse_ttb_metrics( } std::vector HarfBuzzShaper::compute_kerning_deltas( - hb_buffer_t* hb_buffer, struct opentype_str* ots_arr) { - unsigned int glyph_count; - hb_glyph_info_t* glyph_info_arr = - hb_buffer_get_glyph_infos(hb_buffer, &glyph_count); + const std::vector& metrics, struct opentype_str* ots_arr) { + size_t glyph_count = metrics.size() - 1; // Ignore auxiliary data // Retrieve the current kerning offsets and apply them manually if they // differ from their initial value. The initial value doesn't need @@ -255,8 +249,8 @@ std::vector HarfBuzzShaper::compute_kerning_deltas( } // Keep initial kerning offsets when they are first encountered - auto key = std::make_pair(glyph_info_arr[i].codepoint, - glyph_info_arr[i + 1].codepoint); + auto key = + std::make_pair(metrics[i].codepoint, metrics[i + 1].codepoint); // Insert only if absent initial_kerning_.insert({key, kerning_offset}); @@ -268,19 +262,17 @@ std::vector HarfBuzzShaper::compute_kerning_deltas( return kerning_deltas; } -std::vector HarfBuzzShaper::compute_width_deltas(hb_buffer_t* hb_buffer, - SplineChar** glyphs) { - unsigned int glyph_count; - hb_glyph_info_t* glyph_info_arr = - hb_buffer_get_glyph_infos(hb_buffer, &glyph_count); +std::vector HarfBuzzShaper::compute_width_deltas( + const std::vector& metrics) { + size_t glyph_count = metrics.size() - 1; // Ignore auxiliary data // Compute width deltas for glyphs which might have changed. std::vector width_deltas; for (int i = 0; i < glyph_count; ++i) { - int16_t width = glyphs[i]->width; + int16_t width = metrics[i].dwidth; // Glyph width before adjustments. // Keep initial widths when they are first encountered - auto key = glyph_info_arr[i].codepoint; + auto key = metrics[i].codepoint; // Insert only if absent initial_width_.insert({key, width}); @@ -292,20 +284,11 @@ std::vector HarfBuzzShaper::compute_width_deltas(hb_buffer_t* hb_buffer, return width_deltas; } -ShaperOutput HarfBuzzShaper::apply_features( - SplineChar** glyphs, const std::map& feature_map, Tag script, - Tag lang, int pixelsize, bool vertical) { - std::vector u_vec; - // Assigned fake encodings will be interpreted by resolve_fake_encoding(). - for (size_t len = 0; glyphs[len] != NULL; ++len) { - u_vec.push_back((glyphs[len]->unicodeenc > 0) - ? glyphs[len]->unicodeenc - : (glyphs[len]->ttf_glyph + FAKE_UNICODE_BASE)); - } - u_vec.push_back(0); - +std::vector HarfBuzzShaper::apply_features( + const std::vector& ubuf, const std::map& feature_map, + Tag script, Tag lang, bool vertical) { hb_buffer_t* hb_buffer = hb_buffer_create(); - hb_buffer_add_codepoints(hb_buffer, u_vec.data(), -1, 0, -1); + hb_buffer_add_codepoints(hb_buffer, ubuf.data(), -1, 0, -1); // Set script and language hb_script_t hb_script = hb_ot_tag_to_script(script); @@ -329,18 +312,52 @@ ShaperOutput HarfBuzzShaper::apply_features( hb_feature_vec.size()); // Retrieve the results - auto [glyphs_after_gpos, metrics] = extract_shaped_data(hb_buffer); + std::vector metrics = extract_shaped_data(hb_buffer); int glyph_count = metrics.size() - 1; - std::vector width_deltas = - compute_width_deltas(hb_buffer, glyphs_after_gpos); - + // Perhaps counterintuitively, when setting RTL direction for RTL + // languages, HarfBuzz would reverse the glyph order in the output + // buffer. We therefore need to recompute metrics in reverse direction if (rtl) { - // HarfBuzz reverses the order of an RTL output buffer - std::reverse(glyphs_after_gpos, glyphs_after_gpos + glyph_count); - std::reverse(width_deltas.begin(), width_deltas.end()); + metrics = reverse_rtl_metrics(metrics); } + // Reverse vertical metrics from bottom-up direction (HarfBuzz convention) + // to top-down (MetricsView convention) + if (vertical) { + metrics = reverse_ttb_metrics(metrics); + } + + // Cleanup + hb_buffer_destroy(hb_buffer); + + return metrics; +} + +ShaperOutput HarfBuzzShaper::mv_apply_features( + SplineChar** glyphs, const std::map& feature_map, Tag script, + Tag lang, int pixelsize, bool vertical) { + std::vector u_vec; + // Assigned fake encodings will be interpreted by resolve_fake_encoding(). + for (size_t len = 0; glyphs[len] != NULL; ++len) { + int unicodeenc = -1, ttf_glyph = -1; + context_->get_encoding(glyphs[len], &unicodeenc, &ttf_glyph); + u_vec.push_back((unicodeenc > 0) ? unicodeenc + : (ttf_glyph + FAKE_UNICODE_BASE)); + } + u_vec.push_back(0); + + std::vector metrics = + apply_features(u_vec, feature_map, script, lang, vertical); + + std::vector width_deltas = compute_width_deltas(metrics); + + // glyphs_after_gpos is NULL-terminated thanks to metrics auxiliary data + std::vector glyphs_after_gpos; + std::transform(metrics.begin(), metrics.end(), + std::back_inserter(glyphs_after_gpos), + [](const MetricsCore& m) { return m.sc; }); + // Zero-terminated list of features std::vector flist; for (const auto& [feature, enabled] : feature_map) { @@ -354,28 +371,18 @@ ShaperOutput HarfBuzzShaper::apply_features( // calculated by the legacy shaper are ignored, except for kerning deltas. struct opentype_str* ots_arr = context_->apply_ticked_features( context_->sf, flist.data(), (uint32_t)script, (uint32_t)lang, true, - pixelsize, glyphs_after_gpos); + pixelsize, glyphs_after_gpos.data()); // For simplicity, all characters are marked with the same direction. + size_t glyph_count = metrics.size() - 1; // Ignore auxiliary data + hb_script_t hb_script = hb_ot_tag_to_script(script); + bool rtl = + (hb_script_get_horizontal_direction(hb_script) == HB_DIRECTION_RTL); for (int i = 0; i < glyph_count; ++i) { ots_arr[i].r2l = rtl; } - std::vector kerning_deltas = - compute_kerning_deltas(hb_buffer, ots_arr); - - // Perhaps counterintuitively, when setting RTL direction for RTL - // languages, HarfBuzz would reverse the glyph order in the output - // buffer. We therefore need to recompute metrics in reverse direction - if (rtl) { - metrics = reverse_rtl_metrics(metrics); - } - - // Reverse vertical metrics from bottom-up direction (HarfBuzz convention) - // to top-down (MetricsView convention) - if (vertical) { - metrics = reverse_ttb_metrics(metrics); - } + std::vector kerning_deltas = compute_kerning_deltas(metrics, ots_arr); // Compute the accumulated shifts dx for each glyph as partial sums of // kerning and width deltas. Adjust glyph widths as appropriate. @@ -388,18 +395,14 @@ ShaperOutput HarfBuzzShaper::apply_features( metrics[i].dwidth += width_deltas[i]; } - // Cleanup - hb_buffer_destroy(hb_buffer); - return {ots_arr, metrics}; } -void HarfBuzzShaper::scale_metrics(MetricsView* mv, double iscale, double scale, - bool vertical) { +void HarfBuzzShaper::scale_metrics(MetricsView* mv, MetricsCore* metrics, + double iscale, double scale, bool vertical) { int glyphcnt = 0; - MetricsCore* metrics = context_->get_metrics(mv, &glyphcnt); int x0 = 10, y0 = 10; - for (int i = 0; i < glyphcnt; ++i) { + for (int i = 0; metrics[i].sc != NULL; ++i) { MetricsCore& m = metrics[i]; assert(!m.scaled); m.dx = x0 + m.dx * scale; @@ -598,7 +601,7 @@ SplineChar* HarfBuzzShaper::get_notdef_glyph() { "uni000D", "glyph1", "glyph2"}; for (const std::string& name : notdef_names) { for (const auto& [idx, sc] : ttf_map_) { - if (name == sc->name) { + if (name == context_->get_name(sc)) { notdef_glyph_ = sc; return notdef_glyph_; } diff --git a/fontforgeexe/shapers/harfbuzz.hpp b/fontforge/shapers/harfbuzz.hpp similarity index 84% rename from fontforgeexe/shapers/harfbuzz.hpp rename to fontforge/shapers/harfbuzz.hpp index 77c645843d..e615d0f17c 100644 --- a/fontforgeexe/shapers/harfbuzz.hpp +++ b/fontforge/shapers/harfbuzz.hpp @@ -38,13 +38,18 @@ class HarfBuzzShaper : public IShaper { const char* name() const override { return "harfbuzz"; } - ShaperOutput apply_features(SplineChar** glyphs, - const std::map& feature_map, - Tag script, Tag lang, int pixelsize, - bool vertical) override; + std::vector apply_features( + const std::vector& ubuf, + const std::map& feature_map, Tag script, Tag lang, + bool vertical) override; - void scale_metrics(MetricsView* mv, double iscale, double scale, - bool vertical) override; + ShaperOutput mv_apply_features(SplineChar** glyphs, + const std::map& feature_map, + Tag script, Tag lang, int pixelsize, + bool vertical) override; + + void scale_metrics(MetricsView* mv, MetricsCore* metrics, double iscale, + double scale, bool vertical) override; std::set default_features(Tag script, Tag lang, bool vertical) const override; @@ -95,8 +100,7 @@ class HarfBuzzShaper : public IShaper { const std::map& feature_map) const; // Retrieve data from shaped buffer and fill metrics. - std::pair> extract_shaped_data( - hb_buffer_t* hb_buffer); + std::vector extract_shaped_data(hb_buffer_t* hb_buffer); // RTL HarfBuzz shaping returns metrics end-to-start. This method reverses // them. @@ -108,13 +112,13 @@ class HarfBuzzShaper : public IShaper { // Compute changes in kerning due to user's input after the font was // generated. - std::vector compute_kerning_deltas(hb_buffer_t* hb_buffer, - struct opentype_str* ots_arr); + std::vector compute_kerning_deltas( + const std::vector& metrics, struct opentype_str* ots_arr); // Compute changes in glyph width due to user's input after the font was // generated. - std::vector compute_width_deltas(hb_buffer_t* hb_buffer, - SplineChar** glyphs); + std::vector compute_width_deltas( + const std::vector& metrics); const std::set& default_features_by_direction( hb_direction_t dir) const; diff --git a/fontforgeexe/shapers/i_shaper.hpp b/fontforge/shapers/i_shaper.hpp similarity index 73% rename from fontforgeexe/shapers/i_shaper.hpp rename to fontforge/shapers/i_shaper.hpp index a7ce80fa6d..a30adebcea 100644 --- a/fontforgeexe/shapers/i_shaper.hpp +++ b/fontforge/shapers/i_shaper.hpp @@ -27,11 +27,13 @@ #include #include "metrics.h" +#include "opentype_str.h" #include "tag.hpp" +typedef uint32_t unichar_t; + typedef struct splinechar SplineChar; typedef struct metricsview MetricsView; -struct opentype_str; namespace ff::shapers { @@ -45,18 +47,28 @@ class IShaper { // shaper in the system. virtual const char* name() const = 0; + // ubuf - buffer of Unicode encodings. External shapers (e.g. HarfBuzz) can + // only accept Unicode-based input, so unencoded glyphs need to be mapped to + // fake encodings. + virtual std::vector apply_features( + const std::vector& ubuf, + const std::map& feature_map, Tag script, Tag lang, + bool vertical) = 0; + // glyphs - a sequence of glyphs to be shaped // NOTE: the glyph sequence can't be passed as a Unicode string, since some // glyphs don't have encoding at all, and the shaper should still be able to // apply features which involve these glyphs. - virtual ShaperOutput apply_features(SplineChar** glyphs, - const std::map& feature_map, - Tag script, Tag lang, int pixelsize, - bool vertical) = 0; + // + // TODO(iorsh): eliminate the reliance on SplineChar structure when + // modernizing the Metrics View. + virtual ShaperOutput mv_apply_features( + SplineChar** glyphs, const std::map& feature_map, Tag script, + Tag lang, int pixelsize, bool vertical) = 0; // Scale glyph sequence metrics from font units to pixels - virtual void scale_metrics(MetricsView* mv, double iscale, double scale, - bool vertical) = 0; + virtual void scale_metrics(MetricsView* mv, MetricsCore* metrics, + double iscale, double scale, bool vertical) = 0; // OpenType features enabled by default virtual std::set default_features(Tag script, Tag lang, diff --git a/fontforge/metrics.h b/fontforge/shapers/metrics.h similarity index 84% rename from fontforge/metrics.h rename to fontforge/shapers/metrics.h index aa994473ac..4f09da65aa 100644 --- a/fontforge/metrics.h +++ b/fontforge/shapers/metrics.h @@ -26,9 +26,18 @@ */ #pragma once +#include #include +typedef struct splinechar SplineChar; + typedef struct metrics_core { + SplineChar* sc; + + /* Glyph index in TTF font, if applicable. May be set to INVALID_CODEPOINT. + */ + uint32_t codepoint; + /* The shaper fills values in font units (scaled=false), and they are later rescaled to UI pixel units (scaled=true). */ bool scaled; @@ -38,3 +47,10 @@ typedef struct metrics_core { int xoff, yoff; int16_t kernafter; } MetricsCore; + +#define INVALID_CODEPOINT (uint32_t)(-1) + +typedef struct splinechar_ttf_map { + SplineChar* glyph; + int ttf_glyph; +} SplineCharTTFMap; diff --git a/fontforge/shapers/opentype_str.h b/fontforge/shapers/opentype_str.h new file mode 100644 index 0000000000..caa55f700e --- /dev/null +++ b/fontforge/shapers/opentype_str.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2000-2012 by George Williams */ +/* + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +struct valdev; +struct splinechar; +struct kernpair; +struct kernclass; + +struct vr { + int16_t xoff, yoff, h_adv_off, v_adv_off; + struct valdev* adjust; +}; + +struct opentype_str { + struct splinechar* sc; + struct vr vr; /* Scaled and rounded gpos modifications (device table info + included in xoff, etc. not in adjusts) */ + struct kernpair* kp; + struct kernclass* kc; + unsigned int prev_kc0 : 1; + unsigned int next_kc0 : 1; + int16_t advance_width; /* Basic advance, modifications in vr, scaled and + rounded */ + /* Er... not actually set by ApplyLookups, but somewhere the caller */ + /* can stash info. (Extract width from hinted bdf if possible, tt */ + /* instructions can change it from the expected value) */ + int16_t kc_index; + int16_t lig_pos; /* when skipping marks to form a ligature keep track of + what ligature element a mark was attached to */ + int16_t context_pos; /* When doing a contextual match remember which glyphs + are used, and where in the match they occur. Skipped + glyphs have -1 */ + int32_t orig_index; + void* fl; + unsigned int line_break_after : 1; + unsigned int r2l : 1; + int16_t bsln_off; +}; diff --git a/fontforgeexe/shapers/shaper_shim.cpp b/fontforge/shapers/shaper_shim.cpp similarity index 93% rename from fontforgeexe/shapers/shaper_shim.cpp rename to fontforge/shapers/shaper_shim.cpp index 12c00822b7..f7d148ff4e 100644 --- a/fontforgeexe/shapers/shaper_shim.cpp +++ b/fontforge/shapers/shaper_shim.cpp @@ -128,8 +128,8 @@ struct shaper_out shaper_apply_features(cpp_IShaper* shaper, } if (shaper) { - auto output = ishaper->apply_features(glyphs, feature_map, Tag(script), - Tag(lang), pixelsize, vertical); + auto output = ishaper->mv_apply_features( + glyphs, feature_map, Tag(script), Tag(lang), pixelsize, vertical); MetricsCore* metrics = (MetricsCore*)calloc(output.second.size() + 1, sizeof(MetricsCore)); memcpy(metrics, output.second.data(), @@ -140,10 +140,11 @@ struct shaper_out shaper_apply_features(cpp_IShaper* shaper, } } -void shaper_scale_metrics(cpp_IShaper* shaper, MetricsView* mv, double iscale, - double scale, bool vertical) { +void shaper_scale_metrics(cpp_IShaper* shaper, MetricsView* mv, + MetricsCore* metrics, double iscale, double scale, + bool vertical) { ff::shapers::IShaper* ishaper = toCPP(shaper); - ishaper->scale_metrics(mv, iscale, scale, vertical); + ishaper->scale_metrics(mv, metrics, iscale, scale, vertical); } uint32_t* shaper_default_features(cpp_IShaper* shaper, uint32_t script, diff --git a/fontforgeexe/shapers/shaper_shim.hpp b/fontforge/shapers/shaper_shim.hpp similarity index 83% rename from fontforgeexe/shapers/shaper_shim.hpp rename to fontforge/shapers/shaper_shim.hpp index 3ebdbf8264..bf3dae860c 100644 --- a/fontforgeexe/shapers/shaper_shim.hpp +++ b/fontforge/shapers/shaper_shim.hpp @@ -30,6 +30,7 @@ extern "C" { #include "basics.h" #include "metrics.h" +#include "ustring.h" typedef struct splinechar SplineChar; typedef struct splinechar_ttf_map SplineCharTTFMap; @@ -40,12 +41,6 @@ typedef struct encmap EncMap; static const int INVALID_KERN_OFFSET = 0x7ffffff; static const int FAKE_UNICODE_BASE = 0x110000; -char* u2utf8_copy(const unichar_t* ubuf); -int _WriteTTFFont(FILE* ttf, SplineFont* sf, int /*enum fontformat*/ format, - int32_t* bsizes, int /*enum bitmapformat*/ bf, int flags, - EncMap* enc, int layer); -extern SplineCharTTFMap* MakeGlyphTTFMap(SplineFont* sf); - /* Dummy incomplete type which can be casted to C++ type ff::shapers::IShaper */ typedef struct cpp_IShaper cpp_IShaper; @@ -58,14 +53,10 @@ typedef struct shaper_context { SplineFont* sf, uint32_t* flist, uint32_t script, uint32_t lang, bool gpos_only, int pixelsize, SplineChar** glyphs); - // Get encoding map - EncMap* (*get_enc_map)(SplineFont* sf); - - // Case-specific glyph width computation, not to be used in general case. - int16_t (*get_char_width)(MetricsView* mv, SplineChar* sc); - - // Accessor for MetricsView::metrics - MetricsCore* (*get_metrics)(MetricsView* mv, int* p_glyphcnt); + // Glyph metrics callback, optional for HarfBuzz. May accept mv=NULL, and in + // that case plain SplineChar metrics are retrieved. + void (*get_char_metrics)(MetricsView* mv, SplineChar* sc, int16_t* width, + int16_t* vwidth); // Retrieve current kerning value from glyph data int (*get_kern_offset)(struct opentype_str* glyph); @@ -78,6 +69,16 @@ typedef struct shaper_context { SplineChar* (*get_or_make_char)(SplineFont* sf, int unienc, const char* name); + // Create a binary blob with font data, return its encoding map + SplineCharTTFMap* (*write_font_into_memory)(FILE* ttf, SplineFont* sf); + + // SplineChar::name accessor + const char* (*get_name)(const SplineChar* sc); + + // SplineChar::unicodeenc, SplineChar::ttf_glyph accessor + void (*get_encoding)(const SplineChar* sc, int* p_unicodeenc, + int* p_ttf_glyph); + } ShaperContext; typedef struct shaper_def { @@ -126,8 +127,9 @@ struct shaper_out shaper_apply_features(cpp_IShaper* shaper, uint32_t lang, int pixelsize, bool vertical); -void shaper_scale_metrics(cpp_IShaper* shaper, MetricsView* mv, double iscale, - double scale, bool vertical); +void shaper_scale_metrics(cpp_IShaper* shaper, MetricsView* mv, + MetricsCore* metrics, double iscale, double scale, + bool vertical); uint32_t* shaper_default_features(cpp_IShaper* shaper, uint32_t script, uint32_t lang, bool vertical); diff --git a/fontforge/splinechar.c b/fontforge/splinechar.c index 808d160e20..efec8e39e0 100644 --- a/fontforge/splinechar.c +++ b/fontforge/splinechar.c @@ -2874,3 +2874,10 @@ const char* SCNameCheck(const unichar_t *name, bool *p_questionable) { } return NULL; } + +const char* SCGetName(const SplineChar* sc) { return sc->name; } + +void SCGetEncoding(const SplineChar* sc, int* p_unicodeenc, int* p_ttf_glyph) { + if (p_unicodeenc) *p_unicodeenc = sc->unicodeenc; + if (p_ttf_glyph) *p_ttf_glyph = sc->ttf_glyph; +} diff --git a/fontforge/splinechar.h b/fontforge/splinechar.h index 25f7d546b1..49a27f691d 100644 --- a/fontforge/splinechar.h +++ b/fontforge/splinechar.h @@ -29,6 +29,7 @@ #include #include "basics.h" +#include "shapers/opentype_str.h" #ifndef _NO_LIBSPIRO #include @@ -694,43 +695,9 @@ typedef struct splinechar { unichar_t* user_decomp; // User decomposition for building this character } SplineChar; -struct vr { - int16_t xoff, yoff, h_adv_off, v_adv_off; - struct valdev* adjust; -}; - -struct opentype_str { - struct splinechar* sc; - struct vr vr; /* Scaled and rounded gpos modifications (device table info - included in xoff, etc. not in adjusts) */ - struct kernpair* kp; - struct kernclass* kc; - unsigned int prev_kc0 : 1; - unsigned int next_kc0 : 1; - int16_t advance_width; /* Basic advance, modifications in vr, scaled and - rounded */ - /* Er... not actually set by ApplyLookups, but somewhere the caller */ - /* can stash info. (Extract width from hinted bdf if possible, tt */ - /* instructions can change it from the expected value) */ - int16_t kc_index; - int16_t lig_pos; /* when skipping marks to form a ligature keep track of - what ligature element a mark was attached to */ - int16_t context_pos; /* When doing a contextual match remember which glyphs - are used, and where in the match they occur. Skipped - glyphs have -1 */ - int32_t orig_index; - void* fl; - unsigned int line_break_after : 1; - unsigned int r2l : 1; - int16_t bsln_off; -}; - -typedef struct splinechar_ttf_map { - SplineChar* glyph; - int ttf_glyph; -} SplineCharTTFMap; - /* Returns NULL if the name is valid, or error message. If the function returned error message and set `questionable` to true, the name should be accepted, but discouraged. The error message should not be freed. */ const char* SCNameCheck(const unichar_t* name, bool* questionable); +const char* SCGetName(const SplineChar* sc); +void SCGetEncoding(const SplineChar* sc, int* p_unicodeenc, int* p_ttf_glyph); diff --git a/fontforge/splinefont.c b/fontforge/splinefont.c index 7523024ab0..647daee109 100644 --- a/fontforge/splinefont.c +++ b/fontforge/splinefont.c @@ -390,10 +390,6 @@ void SplineFontSetUnChanged(SplineFont *sf) { _SplineFontSetUnChanged(sf->mm->instances[i]); } -EncMap* SFGetMap(SplineFont *sf) { - return sf->map; -} - static char *scaleString(char *string, double scale) { char *result; char *pt; diff --git a/fontforge/splinefont.h b/fontforge/splinefont.h index 9d41160de4..84e66f6248 100644 --- a/fontforge/splinefont.h +++ b/fontforge/splinefont.h @@ -1461,7 +1461,6 @@ extern const unichar_t *_uGetModifiers(const unichar_t *fontname, const unichar_ const unichar_t *weight); extern void ttfdumpbitmap(SplineFont *sf,struct alltabs *at,int32_t *sizes); extern void SplineFontSetUnChanged(SplineFont *sf); -extern EncMap* SFGetMap(SplineFont *sf); extern bool RealNear(real a,real b); diff --git a/fontforge/tottf.c b/fontforge/tottf.c index e314e112ac..083495568b 100644 --- a/fontforge/tottf.c +++ b/fontforge/tottf.c @@ -6000,7 +6000,7 @@ static void dumpttf(FILE *ttf,struct alltabs *at) { /* ttfcopyfile closed all the files (except ttf) */ } -SplineCharTTFMap* MakeGlyphTTFMap(SplineFont *sf) { +static SplineCharTTFMap* MakeGlyphTTFMap(SplineFont *sf) { int i,k,max, map_idx; SplineChar *sc; SplineCharTTFMap *map = NULL; @@ -6152,7 +6152,6 @@ int _WriteTTFFont(FILE *ttf,SplineFont *sf,enum fontformat format, int32_t *bsizes, enum bitmapformat bf,int flags,EncMap *map, int layer) { struct alltabs at; int i, anyglyphs; - bool *fake_mappings = NULL; short_too_long_warned = 0; // This is a static variable defined for putshort. /* TrueType probably doesn't need this, but OpenType does for floats in dictionaries */ @@ -6237,6 +6236,15 @@ return( 0 ); return( ret ); } +/* A special version of TrueType font, which drops all outlines for performance. + */ +SplineCharTTFMap* WriteTTFFontForShaper(FILE* ttf, SplineFont* sf) { + _WriteTTFFont(ttf, sf, ff_ttf, NULL, bf_ttf, + ttf_flag_otmode | ttf_flag_no_outlines, sf->map, ly_fore); + // Build map of TTF codepoints + return MakeGlyphTTFMap(sf); +} + /* ************************************************************************** */ /* ****************************** Type42 stuff ****************************** */ /* ************************************************************************** */ diff --git a/fontforge/tottf.h b/fontforge/tottf.h index 89db1deb77..3ae94094bc 100644 --- a/fontforge/tottf.h +++ b/fontforge/tottf.h @@ -3,6 +3,7 @@ #include "splinefont.h" #include "ttf.h" +#include "shapers/metrics.h" extern char *utf8_verify_copy(const char *str); extern int32_t filechecksum(FILE *file); @@ -16,7 +17,7 @@ extern int ttfcopyfile(FILE *ttf, FILE *other, int pos, const char *tab_name); extern int WriteTTC(const char *filename, struct sflist *sfs, enum fontformat format, enum bitmapformat bf, int flags, int layer, enum ttc_flags ttcflags); extern int WriteTTFFont(char *fontname, SplineFont *sf, enum fontformat format, int32_t *bsizes, enum bitmapformat bf, int flags, EncMap *enc, int layer); extern int _WriteTTFFont(FILE *ttf, SplineFont *sf, enum fontformat format, int32_t *bsizes, enum bitmapformat bf, int flags, EncMap *enc, int layer); -extern SplineCharTTFMap* MakeGlyphTTFMap(SplineFont *sf); +extern SplineCharTTFMap* WriteTTFFontForShaper(FILE *ttf, SplineFont *sf); extern int _WriteType42SFNTS(FILE *type42, SplineFont *sf, enum fontformat format, int flags, EncMap *enc, int layer); extern void cvt_unix_to_1904(long long time, int32_t result[2]); extern void DefaultTTFEnglishNames(struct ttflangname *dummy, SplineFont *sf); diff --git a/fontforge/views.h b/fontforge/views.h index abf1e8d931..893932ed6f 100644 --- a/fontforge/views.h +++ b/fontforge/views.h @@ -39,7 +39,6 @@ #include "ggadget.h" #include "multidialog.h" #include "search.h" -#include "metrics.h" struct gfi_data; struct contextchaindlg; diff --git a/fontforgeexe/CMakeLists.txt b/fontforgeexe/CMakeLists.txt index b7cd5603bc..ea6e11a81e 100644 --- a/fontforgeexe/CMakeLists.txt +++ b/fontforgeexe/CMakeLists.txt @@ -89,7 +89,6 @@ if(ENABLE_GUI) wordlistparser.h ) - add_subdirectory(shapers) add_subdirectory(gtk) if(APPLE) diff --git a/fontforgeexe/metricsview.c b/fontforgeexe/metricsview.c index cab3f97ff1..50ac3067e4 100644 --- a/fontforgeexe/metricsview.c +++ b/fontforgeexe/metricsview.c @@ -45,6 +45,7 @@ #include "splineoverlap.h" #include "splineutil.h" #include "splineutil2.h" +#include "tottf.h" #include "tottfgpos.h" #include "ustring.h" #include "utype.h" @@ -938,16 +939,19 @@ static void MVCreateFields(MetricsView *mv,int i) { static void MVSetSb(MetricsView *mv); static int MVSetVSb(MetricsView *mv); -static int16_t MVCharWidth(MetricsView *mv, SplineChar *sc) { - BDFChar * bdfc = mv->bdf!=NULL ? mv->bdf->glyphs[sc->orig_pos] : BDFPieceMealCheck(mv->show,sc->orig_pos); - return bdfc->width; -} - -static MetricsCore* MVGetMetrics(MetricsView *mv, int* p_glyphcnt) { - if (p_glyphcnt) { - *p_glyphcnt = mv->glyphcnt; +static void MVCharMetrics(MetricsView* mv, SplineChar* sc, int16_t* width, + int16_t* vwidth) { + if (width) { + if (mv) { + BDFChar* bdfc = mv->bdf != NULL + ? mv->bdf->glyphs[sc->orig_pos] + : BDFPieceMealCheck(mv->show, sc->orig_pos); + *width = bdfc->width; + } else + *width = sc->width; } - return mv->metrics; + + if (vwidth) *vwidth = sc->vwidth; } void MVRefreshMetric(MetricsView *mv) { @@ -958,7 +962,7 @@ void MVRefreshMetric(MetricsView *mv) { for ( cnt=0; mv->glyphs[cnt].sc!=NULL; ++cnt ) { MVRefreshValues(mv,cnt); } - shaper_scale_metrics(mv->shaper, mv, iscale, scale, mv->vertical); + shaper_scale_metrics(mv->shaper, mv, mv->metrics, iscale, scale, mv->vertical); MVSetVSb(mv); MVSetSb(mv); } @@ -3932,17 +3936,18 @@ static void ellistcheck(GWindow gw, struct gmenuitem *mi, GEvent *UNUSED(e)) { } } -static ShaperContext* MVMakeShaperContext(MetricsView *mv) { - ShaperContext *context = calloc(1,sizeof(ShaperContext)); +static ShaperContext* MVMakeShaperContext(MetricsView* mv) { + ShaperContext* context = calloc(1, sizeof(ShaperContext)); context->sf = mv->sf; context->mv = mv; context->apply_ticked_features = ApplyTickedFeatures; - context->get_enc_map = SFGetMap; - context->get_char_width = MVCharWidth; - context->get_metrics = MVGetMetrics; + context->get_char_metrics = MVCharMetrics; context->get_kern_offset = MVGetKernOffset; context->script_is_rtl = ScriptIsRightToLeft; context->get_or_make_char = SFGetOrMakeChar; + context->write_font_into_memory = WriteTTFFontForShaper; + context->get_name = SCGetName; + context->get_encoding = SCGetEncoding; return context; } diff --git a/inc/ustring.h b/inc/ustring.h index 93c516cd22..bf31ba2223 100644 --- a/inc/ustring.h +++ b/inc/ustring.h @@ -139,7 +139,7 @@ extern char *latin1_2_utf8_copy(const char *lbuf); extern char *utf8_2_latin1_copy(const char *utf8buf); extern long utf8_strlen(const char *utf8_str); /* Count how many characters in the string NOT bytes */ extern long utf82u_strlen(const char *utf8_str); /* Count how many shorts needed to represent in UCS2 */ -extern void utf8_strncpy(register char *to, const char *from, int len); /* copy n characters NOT bytes */ +extern void utf8_strncpy(char *to, const char *from, int len); /* copy n characters NOT bytes */ extern char *def2utf8_copy(const char *from); extern char *utf82def_copy(const char *ufrom); extern char *utf82def_copy_safe(const char *ufrom); From a4533e9301910cc40640192fb5a2a36c31b52053 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Sat, 14 Feb 2026 20:10:35 +0200 Subject: [PATCH 52/96] Fix memory allocation in utf8toutf7_copy() (#5745) --- fontforge/sfd.c | 81 ++++++++++++++----------------------------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/fontforge/sfd.c b/fontforge/sfd.c index fdbb0f3f47..270747274d 100644 --- a/fontforge/sfd.c +++ b/fontforge/sfd.c @@ -213,54 +213,41 @@ void SFDDumpUTF7Str(FILE *sfd, const char *str) { char *utf8toutf7_copy(const char *_str) { uint16_t ch; int prev_cnt=0, prev=0, in=0; - int i, len; + int len = 0; char *ret=NULL, *ostr=NULL; uint16_t *utf16_str, *pt; if ( _str==NULL ) return( NULL ); - utf16_str = utf82utf16_copy(_str); + pt = utf16_str = utf82utf16_copy(_str); + while ( *pt++!='\0' ) + ++len; + /* 5x buffer is guaranteed to suffice for UTF-7 encoded string. */ + ostr = ret = malloc(5 * len+1); - for ( i=0; i<2; ++i ) { pt = utf16_str; - len= prev_cnt= prev= in=0; + prev_cnt= prev= in=0; while ( (ch = *pt++)!='\0' ) { if ( ch<127 && ch!='\n' && ch!='\r' && ch!='\\' && ch!='~' && ch!='+' && ch!='=' && ch!='"' ) { if ( prev_cnt!=0 ) { - if ( i ) { - prev<<= (prev_cnt==1?16:8); - ostr = base64_encode(ostr,prev); - prev_cnt=prev=0; - } else - len += 4; + prev<<= (prev_cnt==1?16:8); + ostr = base64_encode(ostr,prev); + prev_cnt=prev=0; } if ( in ) { - if ( inbase64[ch]!=-1 || ch=='-' ) { - if ( i ) - *ostr++ = '-'; - else - ++len; - } + if ( inbase64[ch]!=-1 || ch=='-' ) + *ostr++ = '-'; in = 0; } - if ( i ) - *ostr++ = ch; - else - ++len; + *ostr++ = ch; } else if ( ch=='+' && !in ) { - if ( i ) { - *ostr++ = '+'; - *ostr++ = '-'; - } else - len += 2; + *ostr++ = '+'; + *ostr++ = '-'; } else if ( prev_cnt== 0 ) { if ( !in ) { - if ( i ) - *ostr++ = '+'; - else - ++len; + *ostr++ = '+'; in = 1; } prev = ch; @@ -268,21 +255,13 @@ char *utf8toutf7_copy(const char *_str) { } else if ( prev_cnt==2 ) { prev<<=8; prev += (ch>>8)&0xff; - if ( i ) { - ostr = base64_encode(ostr,prev); - prev_cnt=prev=0; - } else - len += 4; + ostr = base64_encode(ostr,prev); prev = (ch&0xff); prev_cnt=1; } else { prev<<=16; prev |= ch; - if ( i ) { - ostr = base64_encode(ostr,prev); - prev_cnt=prev=0; - } else - len += 4; + ostr = base64_encode(ostr,prev); prev_cnt = prev = 0; } } @@ -294,32 +273,18 @@ char *utf8toutf7_copy(const char *_str) { output 4 chars. */ if ( prev_cnt==2 ) { prev<<=8; - if ( i ) { - ostr = base64_encode(ostr,prev); - prev_cnt=prev=0; - } else - len += 4; + ostr = base64_encode(ostr,prev); } else if ( prev_cnt==1 ) { prev<<=16; - if ( i ) { - ostr = base64_encode(ostr,prev); - prev_cnt=prev=0; - } else - len += 4; + ostr = base64_encode(ostr,prev); } /* Base64 block can optionally end with a hyphen '-'. We omit it at the end of the encoded string to preserve the existing SFD convention. */ /* - if ( in ) { - if ( i ) - *ostr++ = '-'; - else - ++len; - } + if ( in ) + *ostr++ = '-'; */ - if ( i==0 ) - ostr = ret = malloc(len+1); - } + *ostr = '\0'; free(utf16_str); return( ret ); From 763b8fc6d05815282b83f9668d23cd2fe58e310a Mon Sep 17 00:00:00 2001 From: skef <6175836+skef@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:13:10 -0800 Subject: [PATCH 53/96] Fix crash in LoadPluginMetadata when distribution not found (#5755) - Don't DECREF borrowed reference from PyDict_GetItemString - Fix memory leak: DECREF globals and locals dictionaries - Handle Py_None return (not just NULL) when no distribution matches - Print original Python exception if present, then set formatted error Fixes #5754 Co-authored-by: Claude Opus 4.5 --- fontforge/plugin.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fontforge/plugin.c b/fontforge/plugin.c index 54de155019..34ec78679f 100644 --- a/fontforge/plugin.c +++ b/fontforge/plugin.c @@ -390,13 +390,20 @@ static void LoadPluginMetadata(PluginEntry* pe) { function_args = PyTuple_Pack(1, pe->entrypoint); if (function_args != NULL) { dist = PyObject_Call(func, function_args, NULL); + Py_DECREF(function_args); } - Py_XDECREF(func); - Py_XDECREF(function_args); + /* Note: func is a borrowed reference from PyDict_GetItemString, don't DECREF */ - if (dist == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_RuntimeError, "Could not retrieve distribution"); + Py_DECREF(globals); + Py_DECREF(locals); + + if (dist == NULL || dist == Py_None) { + if (dist == NULL) { + PyErr_Print(); + } + PyErr_Format(PyExc_RuntimeError, + "Could not retrieve distribution for plugin '%s'", pe->name); + Py_XDECREF(dist); return; } @@ -409,7 +416,7 @@ static void LoadPluginMetadata(PluginEntry* pe) { Py_XDECREF(metadata); } - Py_XDECREF(dist); + Py_DECREF(dist); } static bool DiscoverPlugins(int do_import) { From 716d3f0ce0cfd7d14a44de9435d9b4f6aa60f641 Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Tue, 17 Feb 2026 18:03:05 +0200 Subject: [PATCH 54/96] Fix import of multiple images in UI (#5744) --- fontforge/cvimages.c | 35 ++++++++++++++------------------ fontforge/cvimages.h | 2 +- fontforge/fvimportbdf.c | 38 ++++++++++++----------------------- fontforge/fvimportbdf.h | 1 + fontforge/scripting.c | 7 ++++--- fontforgeexe/cvimportdlg.c | 27 +++++++++++++++---------- fontforgeexe/fontview.c | 26 ++++++++---------------- gdraw/gfilechooser.c | 41 ++++++++++++++++++++++++++++++++++++++ inc/ggadget.h | 1 + 9 files changed, 100 insertions(+), 78 deletions(-) diff --git a/fontforge/cvimages.c b/fontforge/cvimages.c index ffc05501a2..d245bb93fb 100644 --- a/fontforge/cvimages.c +++ b/fontforge/cvimages.c @@ -991,55 +991,50 @@ void SCAddScaleImage(SplineChar *sc,GImage *image,bool doclear, int layer, SCInsertImage(sc,image,scale,sc->parent->ascent,0,layer); } -int FVImportImages(FontViewBase *fv,char *path,int format,int toback, +int FVImportImages(FontViewBase *fv,char** path_list,int format,int toback, bool preclear, ImportParams *ip) { GImage *image; - /*struct _GImage *base;*/ int tot; - char *start = path, *endpath=path; - int i; + int idx, file_idx = 0; SplineChar *sc; tot = 0; - for ( i=0; imap->enccount; ++i ) if ( fv->selected[i]) { - sc = SFMakeChar(fv->sf,fv->map,i); - endpath = strchr(start,';'); - if ( endpath!=NULL ) *endpath = '\0'; + for ( idx=0; idxmap->enccount && path_list[file_idx] != NULL; ++idx ) if ( fv->selected[idx]) { + char* image_path = path_list[file_idx++]; + sc = SFMakeChar(fv->sf,fv->map,idx); if ( format==fv_image ) { - image = GImageRead(start); + image = GImageRead(image_path); if ( image==NULL ) { - ff_post_error(_("Bad image file"),_("Bad image file: %.100s"),start); + ff_post_error(_("Bad image file"),_("Bad image file: %.100s"),image_path); return(false); } ++tot; SCAddScaleImage(sc,image,true,toback?ly_back:ly_fore, ip); } else if ( format==fv_svg ) { - SCImportSVG(sc,toback?ly_back:fv->active_layer,start,NULL,0,preclear,ip); + SCImportSVG(sc,toback?ly_back:fv->active_layer,image_path,NULL,0,preclear,ip); ++tot; } else if ( format==fv_glif ) { - SCImportGlif(sc,toback?ly_back:fv->active_layer,start,NULL,0,preclear,ip); + SCImportGlif(sc,toback?ly_back:fv->active_layer,image_path,NULL,0,preclear,ip); ++tot; } else if ( format==fv_eps ) { - SCImportPS(sc,toback?ly_back:fv->active_layer,start,preclear,ip); + SCImportPS(sc,toback?ly_back:fv->active_layer,image_path,preclear,ip); ++tot; } else if ( format==fv_pdf ) { - SCImportPDF(sc,toback?ly_back:fv->active_layer,start,preclear,ip); + SCImportPDF(sc,toback?ly_back:fv->active_layer,image_path,preclear,ip); ++tot; #ifndef _NO_PYTHON } else if ( format>=fv_pythonbase ) { - PyFF_SCImport(sc,format-fv_pythonbase,start, toback?ly_back:fv->active_layer,preclear); + PyFF_SCImport(sc,format-fv_pythonbase,image_path, toback?ly_back:fv->active_layer,preclear); ++tot; #endif } - if ( endpath==NULL ) - break; - start = endpath+1; } if ( tot==0 ) ff_post_error(_("Nothing Selected"),_("You must select a glyph before you can import an image into it")); - else if ( endpath!=NULL ) + else if ( path_list[file_idx]!=NULL ) ff_post_error(_("More Images Than Selected Glyphs"),_("More Images Than Selected Glyphs")); -return( true ); + + return( true ); } int FVImportImageTemplate(FontViewBase *fv,char *path,int format,int toback, diff --git a/fontforge/cvimages.h b/fontforge/cvimages.h index 9c2ffedea1..4a99baf18d 100644 --- a/fontforge/cvimages.h +++ b/fontforge/cvimages.h @@ -7,7 +7,7 @@ #include "splinefont.h" extern GImage *ImageAlterClut(GImage *image); -extern int FVImportImages(FontViewBase *fv, char *path, int format, int toback, +extern int FVImportImages(FontViewBase *fv, char** path_list, int format, int toback, bool preclear, ImportParams *ip); extern int FVImportImageTemplate(FontViewBase *fv, char *path, int format, int toback, bool preclear, ImportParams *ip); diff --git a/fontforge/fvimportbdf.c b/fontforge/fvimportbdf.c index 58be0d067c..206877d12f 100644 --- a/fontforge/fvimportbdf.c +++ b/fontforge/fvimportbdf.c @@ -2295,47 +2295,36 @@ static void SFMergeBitmaps(SplineFont *sf,BDFFont *strikes,EncMap *map) { static void SFAddToBackground(SplineFont *sf,BDFFont *bdf); int FVImportBDF(FontViewBase *fv, char *filename, int ispk, int toback) { + char *path_list[2] = {filename, NULL}; + return FVImportBDFs(fv, path_list, ispk, toback); +} + +int FVImportBDFs(FontViewBase *fv, char **path_list, int ispk, int toback) { BDFFont *b, *anyb=NULL; - char *buf, *eod, *fpt, *file, *full, *freeme; + char *buf, *full; int fcnt, any = 0; int oldenccnt = fv->map->enccount; - freeme = filename = copy(filename); - eod = strrchr(filename,'/'); - if (eod != NULL) { - *eod = '\0'; - file = eod+1; - } else { - file = filename; - filename = "."; - } - fcnt = 1; - fpt = file; - while (( fpt=strstr(fpt,"; "))!=NULL ) - { ++fcnt; fpt += 2; } + for (fcnt = 0; path_list[fcnt] != NULL; ++fcnt); - buf = smprintf(_("Loading font from %.100s"), filename); + buf = smprintf(_("Loading font from %.100s"), path_list[0]); ff_progress_start_indicator(10,_("Loading..."),buf,_("Reading Glyphs"),0,fcnt); ff_progress_enable_stop(false); free(buf); - do { - fpt = strstr(file,"; "); - if ( fpt!=NULL ) *fpt = '\0'; - full = smprintf("%s/%s", filename, file); + for (char** p_path = path_list; *p_path != NULL; ++p_path) { + full = *p_path; buf = smprintf(_("Loading font from %.100s"), full); ff_progress_change_line1(buf); free(buf); b = _SFImportBDF(fv->sf,full,ispk,toback, fv->map); - free(full); - if ( fpt!=NULL ) ff_progress_next_stage(); + ff_progress_next_stage(); if ( b!=NULL ) { anyb = b; any = true; FVRefreshAll(fv->sf); } - file = fpt+2; - } while ( fpt!=NULL ); + } ff_progress_end_indicator(); if ( oldenccnt != fv->map->enccount ) { FontViewBase *fvs; @@ -2346,10 +2335,9 @@ int FVImportBDF(FontViewBase *fv, char *filename, int ispk, int toback) { FontViewReformatAll(fv->sf); } if ( anyb==NULL ) { - ff_post_error( _("No Bitmap Font"), _("Could not find a bitmap font in %s"), filename ); + ff_post_error( _("No Bitmap Font"), _("Could not find a bitmap font in %s"), path_list[0]); } else if ( toback ) SFAddToBackground(fv->sf,anyb); - free(freeme); return( any ); } diff --git a/fontforge/fvimportbdf.h b/fontforge/fvimportbdf.h index dce6ad3ec0..d9c3e1d4ab 100644 --- a/fontforge/fvimportbdf.h +++ b/fontforge/fvimportbdf.h @@ -5,6 +5,7 @@ #include "splinefont.h" extern int FVImportBDF(FontViewBase *fv, char *filename, int ispk, int toback); +extern int FVImportBDFs(FontViewBase *fv, char **path_list, int ispk, int toback); extern int FVImportMult(FontViewBase *fv, char *filename, int toback, int bf); extern SplineFont *SFFromBDF(char *filename, int ispk, int toback); extern void SFCheckPSBitmap(SplineFont *sf); diff --git a/fontforge/scripting.c b/fontforge/scripting.c index d2590090c6..32798db093 100644 --- a/fontforge/scripting.c +++ b/fontforge/scripting.c @@ -2309,9 +2309,10 @@ static void bImport(Context *c) { else if ( format==fv_pk ) ok = FVImportBDF(c->curfv,filename,true, back); else if ( format==fv_image || format==fv_eps || format==fv_svg - || format==fv_pdf ) - ok = FVImportImages(c->curfv,filename,format,back,gclear,ipp); - else + || format==fv_pdf ) { + char *path_list[2] = {filename, NULL}; + ok = FVImportImages(c->curfv,path_list,format,back,gclear,ipp); + } else ok = FVImportImageTemplate(c->curfv,filename,format,back,gclear,ipp); free(filename); if ( !ok ) diff --git a/fontforgeexe/cvimportdlg.c b/fontforgeexe/cvimportdlg.c index 6ae0a7adea..2849835bef 100644 --- a/fontforgeexe/cvimportdlg.c +++ b/fontforgeexe/cvimportdlg.c @@ -567,7 +567,7 @@ static int GFD_ImportOk(GGadget *g, GEvent *e) { ImportParams *ip = ImportParamsState(); GFileChooserGetChildren(d->gfc,NULL,NULL,&tf); - if ( *_GGadgetGetTitle(tf)=='\0' ) + if ( *_GGadgetGetTitle(tf)=='\0' || !temp) return( true ); GDrawSetCursor(GGadgetGetWindow(g),ct_watch); if ( d->fv!=NULL ) { @@ -579,17 +579,18 @@ return( true ); } free(ret); if ( d->fv!=NULL ) { + char** path_list = GFileChooserGetMultipleFiles(temp); int toback = GGadgetIsChecked(d->background); - if ( toback && strchr(temp,';')!=NULL && format<3 ) + if ( toback && path_list[1]!=NULL && format<3 ) ff_post_error(_("Only One Font"),_("Only one font may be imported into the background")); else if ( format==fv_bdf ) - d->done = FVImportBDF((FontViewBase *) d->fv,temp,false, toback); + d->done = FVImportBDFs((FontViewBase *) d->fv,path_list,false, toback); else if ( format==fv_ttf ) d->done = FVImportMult((FontViewBase *) d->fv,temp,toback,bf_ttf); else if ( format==fv_pk ) /* pk */ - d->done = FVImportBDF((FontViewBase *) d->fv,temp,true, toback); + d->done = FVImportBDFs((FontViewBase *) d->fv,path_list,true, toback); else if ( format==fv_pcf ) /* pcf */ - d->done = FVImportBDF((FontViewBase *) d->fv,temp,2, toback); + d->done = FVImportBDFs((FontViewBase *) d->fv,path_list,2, toback); else if ( format==fv_mac ) d->done = FVImportMult((FontViewBase *) d->fv,temp,toback,bf_nfntmacbin); else if ( format==fv_win ) @@ -598,34 +599,38 @@ return( true ); d->done = FVImportMult((FontViewBase *) d->fv,temp,toback,bf_palm); else if ( format==fv_image ) { ShowImportOptions(ip, d->opts_shown, sp_scale); - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); } else if ( format==fv_imgtemplate ) { ShowImportOptions(ip, d->opts_shown, sp_scale); d->done = FVImportImageTemplate((FontViewBase *) d->fv,temp,format,toback,true,ip); } else if ( format==fv_eps ) { ShowImportOptions(ip, d->opts_shown, sp_eps); - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); } else if ( format==fv_epstemplate ) { ShowImportOptions(ip, d->opts_shown, sp_eps); d->done = FVImportImageTemplate((FontViewBase *) d->fv,temp,format,toback,true,ip); } else if ( format==fv_pdf ) { ShowImportOptions(ip, d->opts_shown, sp_eps); - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); } else if ( format==fv_pdftemplate ) { ShowImportOptions(ip, d->opts_shown, sp_eps); d->done = FVImportImageTemplate((FontViewBase *) d->fv,temp,format,toback,true,ip); } else if ( format==fv_svg ) { ShowImportOptions(ip, d->opts_shown, sp_svg); - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); } else if ( format==fv_svgtemplate ) { ShowImportOptions(ip, d->opts_shown, sp_svg); d->done = FVImportImageTemplate((FontViewBase *) d->fv,temp,format,toback,true,ip); } else if ( format==fv_glif ) - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); else if ( format==fv_gliftemplate ) d->done = FVImportImageTemplate((FontViewBase *) d->fv,temp,format,toback,true,ip); else if ( format>=fv_pythonbase ) - d->done = FVImportImages((FontViewBase *) d->fv,temp,format,toback,true,ip); + d->done = FVImportImages((FontViewBase *) d->fv,path_list,format,toback,true,ip); + + for (char** p_path = path_list; *p_path != NULL; ++p_path) + free(*p_path); + free(path_list); } else if ( d->bv!=NULL ) d->done = BVImportImage(d->bv,temp); else { diff --git a/fontforgeexe/fontview.c b/fontforgeexe/fontview.c index 1c3a852911..d7947bc8c7 100644 --- a/fontforgeexe/fontview.c +++ b/fontforgeexe/fontview.c @@ -1251,7 +1251,6 @@ static void FVMenuMergeKern(GWindow gw, struct gmenuitem *UNUSED(mi), GEvent *UN void _FVMenuOpen(FontView *fv) { char *temp; - char *eod, *fpt, *file, *full; FontView *test; int fvcnt, fvtest; char* OpenDir = NULL, *DefaultDir = NULL, *NewDir = NULL; @@ -1278,6 +1277,7 @@ void _FVMenuOpen(FontView *fv) { } temp = GetPostScriptFontName(OpenDir,true,fv != NULL); + char** path_list = GFileChooserGetMultipleFiles(temp); if ( temp==NULL ) return; @@ -1288,24 +1288,14 @@ void _FVMenuOpen(FontView *fv) { NewDir = NULL; } - eod = strrchr(temp,'/'); - if (eod != NULL) { - *eod = '\0'; - file = eod+1; - - if (*file) { - do { - fpt = strstr(file,"; "); - if ( fpt!=NULL ) *fpt = '\0'; - full = malloc(strlen(temp)+1+strlen(file)+1); - strcpy(full,temp); strcat(full,"/"); strcat(full,file); - ViewPostScriptFont(full,0); - file = fpt+2; - free(full); - } while ( fpt!=NULL ); - } - } + for (char** p_path = path_list; *p_path != NULL; ++p_path) + ViewPostScriptFont(*p_path,0); + free(temp); + for (char** p_path = path_list; *p_path != NULL; ++p_path) + free(*p_path); + free(path_list); + for ( fvtest=0, test=fv_list; test!=NULL; ++fvtest, test=(FontView *) (test->b.next) ); } while ( fvtest==fvcnt ); /* did the load fail for some reason? try again */ diff --git a/gdraw/gfilechooser.c b/gdraw/gfilechooser.c index dceaf9c93d..c657669680 100644 --- a/gdraw/gfilechooser.c +++ b/gdraw/gfilechooser.c @@ -1811,3 +1811,44 @@ return; dirs[dcnt] = utf82u_copy(path[dcnt]); dirs[dcnt] = NULL; } + +/* Split multipath of form [/; ; ; ...; + * ] */ +char** GFileChooserGetMultipleFiles(const char* multipath) { + char *path = NULL, *eod, *full; + const char *spt = multipath, *file = multipath, *fpt; + char** path_list = NULL; + size_t sc_count = 0, i = 0; + if (multipath == NULL) return NULL; + + /* Count delimiting semicolons and allocate the NULL-terminated array of + * strings */ + for (sc_count = 0; spt[sc_count]; + spt[sc_count] == ';' ? sc_count++ : *spt++); + path_list = calloc(sc_count + 2, sizeof(char*)); + + eod = strrchr(multipath, '/'); + if (eod != NULL) { + path = copyn(multipath, eod - multipath); + file = eod + 1; + } + + do { + fpt = strstr(file, "; "); + + if (fpt == NULL) fpt = file + strlen(file); + full = malloc(c_strlen(path) + 1 + (fpt - file) + 1); + full[0] = '\0'; + + if (path) { + strcpy(full, path); + strcat(full, "/"); + } + strncat(full, file, fpt - file); + path_list[i++] = full; + file = fpt + 2; + } while (*fpt != '\0'); + + free(path); + return path_list; +} diff --git a/inc/ggadget.h b/inc/ggadget.h index 438c44447b..2a2980c99b 100644 --- a/inc/ggadget.h +++ b/inc/ggadget.h @@ -451,6 +451,7 @@ void GFileChooserSetBookmarks(unichar_t **b); void GFileChooserSetPaths(GGadget *g, const char* const* path); unichar_t **GFileChooserGetBookmarks(void); void GFileChooserSetPrefsChangedCallback(void *data, void (*p_c)(void *)); +char **GFileChooserGetMultipleFiles(const char* multipath); void GHVBoxSetExpandableCol(GGadget *g,int col); void GHVBoxSetExpandableRow(GGadget *g,int row); From 320644749a515208350f51f7a30b845a9b3a4a7a Mon Sep 17 00:00:00 2001 From: Maxim Iorsh Date: Wed, 18 Feb 2026 23:03:07 +0200 Subject: [PATCH 55/96] Fix SFGenerateFont() for Mac family flags (#5757) --- fontforgeexe/savefontdlg.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fontforgeexe/savefontdlg.c b/fontforgeexe/savefontdlg.c index 1f16989817..141e5e12ee 100644 --- a/fontforgeexe/savefontdlg.c +++ b/fontforgeexe/savefontdlg.c @@ -2007,11 +2007,6 @@ int SFGenerateFont(SplineFont *sf,int layer,int family,EncMap *map) { bitmaptypes[i].text = (unichar_t *) _((char *) bitmaptypes[i].text); } - if ( family==gf_macfamily ) { - old_sfnt_flags |= ttf_flag_applemode; - old_sfnt_flags &= ~ttf_flag_otmode; - } - if ( family ) { /* I could just disable the menu item, but I think it's a bit confusing*/ /* and I want people to know why they can't generate a family */ @@ -2668,6 +2663,10 @@ return( 0 ); d.ps_flags = old_ps_flags; d.sfnt_flags = old_sfnt_flags; + if ( family==gf_macfamily ) { + d.sfnt_flags |= ttf_flag_applemode; + d.sfnt_flags &= ~ttf_flag_otmode; + } d.psotb_flags = old_ps_flags | (old_psotb_flags&~ps_flag_mask); GFD_FigureWhich(&d); From f118600c9774a9ca9d826a0ba20facb59caa2871 Mon Sep 17 00:00:00 2001 From: skef <6175836+skef@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:52:03 -0800 Subject: [PATCH 56/96] Support PyPI python modules, including for Windows (#5750) * Enable MSVC compilation of fontforge module * Port GLib-using code to C++ * Replace GLib functions with C++ std:: equivalents for MSVC compatibility. * Add extern "C" guards to C headers * Remove deprecated 'register' keyword * Add explicit casts to malloc(), realloc() etc (C++ requires this) * All Unicode library files now compile as C++ * Port most gutils files to C++ * Port 17 fontforge files to C++ * Remove GLib dependency from fontforge library * Replace all GLib function calls in the fontforge library with portable alternatives to enable MSVC builds for Python wheel distribution. - Add optional libunibreak support for Unicode line breaking (UAX #14) with simple Latin-text fallback when not available - Add ffprocess.c/h for process-related utility functions * Add MSVC compatibility macros and fixes * Simplified workflow focused on Python module builds (Linux, macOS, and Windows MSVC) * Add mINI header-only INI parser library for plugin configuration * Add vcpkg.json manifest for Windows MSVC dependencies * Port files with C++ changes to .cpp extension --- .clang-files | 10 + .clang-format | 6 +- .github/workflows/main.yml | 68 +- .github/workflows/wheels.yml | 158 + .gitignore | 1 + .readthedocs.yaml | 16 + CMakeLists.txt | 107 +- INSTALL.md | 10 + README_PYPI.md | 120 + Unicode/CMakeLists.txt | 10 +- Unicode/{memory.c => memory.cpp} | 8 +- Unicode/{ucharmap.c => ucharmap.cpp} | 16 +- Unicode/{ustring.c => ustring.cpp} | 57 +- WHEEL_BUILD_STATUS.md | 45 + _build_meta/version_provider.py | 46 + cmake/FontForgeConfigure.cmake | 7 +- cmake/PackageUtils.cmake | 25 + cmake/TestUtils.cmake | 10 +- cmake/packages/FindCairo.cmake | 102 - cmake/packages/FindGDK3.cmake | 47 - cmake/packages/FindPango.cmake | 51 - cmake/packages/FindReadline.cmake | 76 - cmake/packages/FindWOFF2.cmake | 50 - doc/sphinx/conf.py | 7 +- doc/sphinx/requirements.txt | 2 + doc/sphinx/scripting/python-extension.rst | 128 + doc/sphinx/scripting/python.rst | 35 +- doc/sphinx/scripting/scripting.rst | 1 + extern/CMakeLists.txt | 11 + extern/cxxopts/cxxopts.hpp | 2923 +++++++++++++++++ extern/mINI/README.md | 32 + extern/mINI/ini.h | 780 +++++ fontforge/CMakeLists.txt | 47 +- fontforge/activeinui.c | 2 +- fontforge/asmfpst.c | 46 +- fontforge/autohint.c | 19 +- fontforge/autohint.h | 10 +- fontforge/autosave.c | 42 +- fontforge/autosave.h | 8 + fontforge/autotrace.c | 64 +- fontforge/autotrace.h | 8 + fontforge/autowidth.h | 8 + fontforge/autowidth2.h | 8 + fontforge/baseviews.h | 32 +- fontforge/bezctx_ff.h | 8 + fontforge/bitmapchar.h | 8 + fontforge/bitmapcontrol.h | 8 + fontforge/bvedit.h | 8 + fontforge/clipnoui.h | 8 + fontforge/{crctab.c => crctab.cpp} | 6 +- fontforge/crctab.h | 8 + fontforge/cvexport.c | 26 +- fontforge/cvexport.h | 8 + fontforge/cvimages.c | 24 +- fontforge/cvimages.h | 8 + fontforge/cvundoes.c | 2 +- fontforge/cvundoes.h | 8 + fontforge/dumpbdf.h | 8 + fontforge/dumppfa.c | 71 +- fontforge/dumppfa.h | 8 + fontforge/effects.h | 8 + fontforge/encoding.c | 92 +- fontforge/encoding.h | 13 + fontforge/featurefile.c | 82 +- fontforge/featurefile.h | 8 + fontforge/fffreetype.h | 4 +- fontforge/ffglib_compat.cpp | 737 +++++ fontforge/ffprocess.c | 1046 ++++++ fontforge/ffpython.h | 19 +- fontforge/flaglist.h | 8 + fontforge/fontforge.h | 8 + fontforge/fontforgeui.h | 1 - fontforge/fontviewbase.c | 26 +- fontforge/freetype.c | 8 +- fontforge/fvcomposite.h | 8 + fontforge/fvfonts.h | 8 + fontforge/fvimportbdf.c | 58 +- fontforge/fvimportbdf.h | 8 + fontforge/fvmetrics.c | 8 +- fontforge/fvmetrics.h | 10 +- fontforge/getline.c | 4 +- fontforge/getline.h | 13 + fontforge/glif_name_hash.c | 37 - fontforge/glif_name_hash.cpp | 63 + fontforge/glif_name_hash.h | 8 + fontforge/glyphcomp.c | 28 +- fontforge/glyphcomp.h | 8 + fontforge/groups.c | 5 +- fontforge/ikarus.h | 8 + fontforge/langfreq.c | 22 +- fontforge/langfreq.h | 8 + fontforge/lookups.c | 75 +- fontforge/lookups.h | 10 +- fontforge/macbinary.c | 17 +- fontforge/macbinary.h | 8 + fontforge/macenc.h | 8 + fontforge/mathconstants.h | 8 + fontforge/mem.h | 8 + fontforge/mm.c | 48 +- fontforge/mm.h | 8 + fontforge/multidialog.h | 6 + fontforge/namehash.h | 6 +- fontforge/namelist.c | 33 +- fontforge/namelist.h | 14 +- fontforge/nonlineartrans.c | 50 +- fontforge/nonlineartrans.h | 14 +- fontforge/noprefs.c | 22 +- fontforge/nowakowskittfinstr.c | 14 +- fontforge/othersubrs.c | 8 +- fontforge/othersubrs.h | 8 + fontforge/palmfonts.h | 8 + fontforge/parsepdf.h | 8 + fontforge/parsepfa.c | 83 +- fontforge/parsepfa.h | 8 + fontforge/parsettf.c | 69 +- fontforge/parsettf.h | 8 + fontforge/parsettfatt.c | 26 +- fontforge/parsettfatt.h | 8 + fontforge/parsettfbmf.h | 8 + fontforge/parsettfvar.h | 8 + fontforge/{plugin.c => plugin.cpp} | 137 +- fontforge/plugin.h | 16 +- fontforge/{print.c => print.cpp} | 237 +- fontforge/print.h | 8 + fontforge/psfont.h | 8 +- fontforge/psread.c | 12 +- fontforge/psread.h | 8 + fontforge/pua.h | 8 + fontforge/{python.c => python.cpp} | 783 +++-- fontforge/savefont.c | 1 + fontforge/savefont.h | 8 + fontforge/{scripting.c => scripting.cpp} | 305 +- fontforge/scripting.h | 8 + fontforge/scstyles.c | 54 +- fontforge/scstyles.h | 8 + fontforge/sd.h | 8 + fontforge/search.h | 8 + fontforge/{sfd.c => sfd.cpp} | 649 ++-- fontforge/sfd.h | 27 +- fontforge/sfd1.h | 8 + fontforge/{sflayout.c => sflayout.cpp} | 196 +- fontforge/sflayoutP.h | 8 + fontforge/shapers/harfbuzz.cpp | 1 + fontforge/spiro.c | 6 +- fontforge/spiro.h | 8 + fontforge/splinechar.c | 38 +- fontforge/splinechar.h | 39 +- fontforge/splinefill.h | 8 + fontforge/splinefit.h | 10 +- fontforge/splinefont.c | 260 +- fontforge/splinefont.h | 22 +- fontforge/splineorder2.c | 2 +- fontforge/splineorder2.h | 8 + fontforge/splineoverlap.c | 59 +- fontforge/splineoverlap.h | 8 + fontforge/splinerefigure.h | 8 + fontforge/splinesave.c | 2 +- fontforge/splinesave.h | 8 + fontforge/splinesaveafm.c | 22 +- fontforge/splinesaveafm.h | 8 + fontforge/splinestroke.c | 15 + fontforge/splinestroke.h | 8 + fontforge/splineutil.c | 74 +- fontforge/splineutil.h | 10 +- fontforge/splineutil2.c | 11 +- fontforge/splineutil2.h | 8 + fontforge/start.c | 13 +- fontforge/start.h | 8 + fontforge/stemdb.c | 4 +- fontforge/svg.c | 18 +- fontforge/svg.h | 8 + fontforge/tottf.c | 79 +- fontforge/tottf.h | 8 + fontforge/tottfaat.h | 8 + fontforge/tottfgpos.c | 34 +- fontforge/tottfgpos.h | 8 + fontforge/tottfvar.h | 8 + fontforge/ttf.h | 12 +- fontforge/ttfinstrs.c | 6 +- fontforge/ttfinstrs.h | 8 + fontforge/ttfspecial.h | 8 + fontforge/ufo.c | 80 +- fontforge/uiinterface.h | 10 +- fontforge/unicoderange.c | 2 +- fontforge/utanvec.h | 39 +- fontforge/views.h | 51 +- fontforge/winfonts.h | 8 + fontforge/woff.h | 8 + fontforge/zapfnomen.h | 8 + fontforgeexe/CMakeLists.txt | 14 +- fontforgeexe/bitmapview.c | 2 +- fontforgeexe/charview.c | 5 +- fontforgeexe/charview_private.h | 26 + fontforgeexe/contextchain.c | 60 +- fontforgeexe/cvexportdlg.c | 4 +- fontforgeexe/cvimportdlg.c | 4 +- fontforgeexe/cvpointer.c | 8 +- fontforgeexe/displayfonts.c | 2 +- fontforgeexe/fontinfo.c | 33 +- fontforgeexe/fontview.c | 21 +- fontforgeexe/fvmetricsdlg.c | 7 + fontforgeexe/gtk/css_builder.cpp | 2 +- fontforgeexe/histograms.c | 6 +- fontforgeexe/lookupui.c | 8 +- fontforgeexe/mmdlg.c | 12 +- fontforgeexe/pluginui.c | 10 +- fontforgeexe/scstylesui.c | 12 +- fontforgeexe/sfundo.c | 7 +- fontforgeexe/showatt.c | 34 +- fontforgeexe/startnoui.c | 10 +- gdraw/CMakeLists.txt | 1 + gutils/gutils.c => gdraw/ghelp.c | 59 +- gutils/CMakeLists.txt | 23 +- gutils/{dlist.c => dlist.cpp} | 2 +- gutils/ffdir.cpp | 104 + gutils/{fsys.c => fsys.cpp} | 317 +- gutils/{gimageread.c => gimageread.cpp} | 1 + .../{gimagereadjpeg.c => gimagereadjpeg.cpp} | 2 +- ...{gimagewritejpeg.c => gimagewritejpeg.cpp} | 4 +- .../{gimagewritepng.c => gimagewritepng.cpp} | 26 +- .../{gimagewritexpm.c => gimagewritexpm.cpp} | 4 +- gutils/gutils.cpp | 77 + gutils/{gwwintl.c => gwwintl.cpp} | 2 +- inc/CMakeLists.txt | 5 - inc/basics.h | 16 + inc/dlist.h | 6 + inc/ffdir.h | 64 + inc/ffglib_compat.h | 292 ++ inc/ffprocess.h | 191 ++ inc/ffunistd.h | 96 + inc/fontforge-config.h.in | 6 + inc/gdraw.h | 4 +- inc/gfile.h | 34 +- inc/ggadget.h | 7 - inc/gimage.h | 7 + inc/gutils.h | 8 + inc/intl.h | 8 + inc/ustring.h | 8 + inc/utype.h | 8 + pyhook/CMakeLists.txt | 150 +- pyhook/fontforgepyhook.c | 7 + pyproject.toml | 167 + tests/CMakeLists.txt | 19 +- tests/pyhook_smoketest.py | 1 + tests/randomtest.c | 15 +- tests/systestdriver.c | 330 -- tests/systestdriver.cpp | 383 +++ vcpkg.json | 12 + 248 files changed, 11255 insertions(+), 3521 deletions(-) create mode 100644 .github/workflows/wheels.yml create mode 100644 .readthedocs.yaml create mode 100644 README_PYPI.md rename Unicode/{memory.c => memory.cpp} (96%) rename Unicode/{ucharmap.c => ucharmap.cpp} (89%) rename Unicode/{ustring.c => ustring.cpp} (96%) create mode 100644 WHEEL_BUILD_STATUS.md create mode 100644 _build_meta/version_provider.py delete mode 100644 cmake/packages/FindCairo.cmake delete mode 100644 cmake/packages/FindGDK3.cmake delete mode 100644 cmake/packages/FindPango.cmake delete mode 100644 cmake/packages/FindReadline.cmake delete mode 100644 cmake/packages/FindWOFF2.cmake create mode 100644 doc/sphinx/requirements.txt create mode 100644 doc/sphinx/scripting/python-extension.rst create mode 100644 extern/CMakeLists.txt create mode 100644 extern/cxxopts/cxxopts.hpp create mode 100644 extern/mINI/README.md create mode 100644 extern/mINI/ini.h rename fontforge/{crctab.c => crctab.cpp} (96%) create mode 100644 fontforge/ffglib_compat.cpp create mode 100644 fontforge/ffprocess.c delete mode 100644 fontforge/glif_name_hash.c create mode 100644 fontforge/glif_name_hash.cpp rename fontforge/{plugin.c => plugin.cpp} (86%) rename fontforge/{print.c => print.cpp} (95%) rename fontforge/{python.c => python.cpp} (97%) rename fontforge/{scripting.c => scripting.cpp} (97%) rename fontforge/{sfd.c => sfd.cpp} (93%) rename fontforge/{sflayout.c => sflayout.cpp} (89%) rename gutils/gutils.c => gdraw/ghelp.c (78%) rename gutils/{dlist.c => dlist.cpp} (97%) create mode 100644 gutils/ffdir.cpp rename gutils/{fsys.c => fsys.cpp} (76%) rename gutils/{gimageread.c => gimageread.cpp} (98%) rename gutils/{gimagereadjpeg.c => gimagereadjpeg.cpp} (99%) rename gutils/{gimagewritejpeg.c => gimagewritejpeg.cpp} (99%) rename gutils/{gimagewritepng.c => gimagewritepng.cpp} (91%) rename gutils/{gimagewritexpm.c => gimagewritexpm.cpp} (95%) create mode 100644 gutils/gutils.cpp rename gutils/{gwwintl.c => gwwintl.cpp} (98%) create mode 100644 inc/ffdir.h create mode 100644 inc/ffglib_compat.h create mode 100644 inc/ffprocess.h create mode 100644 inc/ffunistd.h create mode 100644 pyproject.toml delete mode 100644 tests/systestdriver.c create mode 100644 tests/systestdriver.cpp create mode 100644 vcpkg.json diff --git a/.clang-files b/.clang-files index 4b35b8fe06..e850fa7d32 100644 --- a/.clang-files +++ b/.clang-files @@ -1,10 +1,20 @@ inc/tag.hpp +inc/ffdir.h +inc/ffglib_compat.h +inc/ffprocess.h +inc/ffunistd.h fontforge/splinechar.h fontforge/splinefont_enums.h fontforge/std_maps.cpp fontforge/std_maps.hpp +fontforge/ffglib_compat.cpp +fontforge/ffprocess.c +fontforge/glif_name_hash.cpp fontforge/shapers/*.cpp fontforge/shapers/*.hpp fontforge/shapers/*.h fontforgeexe/gtk/*.cpp fontforgeexe/gtk/*.hpp +gutils/ffdir.cpp +gutils/gutils.cpp +tests/systestdriver.cpp diff --git a/.clang-format b/.clang-format index 190985864e..98d42d0f98 100644 --- a/.clang-format +++ b/.clang-format @@ -1,10 +1,8 @@ --- BasedOnStyle: Google ---- -Language: Cpp -Standard: c++17 -AccessModifierOffset: -3 +Standard: c++17 IndentWidth: 4 +AccessModifierOffset: -3 SpacesBeforeTrailingComments: 2 SortIncludes: false IndentAccessModifiers: false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ee76a5183..f0a43101f2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,11 @@ jobs: FFCONFIG="$FFCONFIG -DENABLE_GUI=OFF -DENABLE_PYTHON_SCRIPTING=OFF" fi mkdir build && pushd build && cmake -GNinja $FFCONFIG .. && popd - ninja -C build all potfiles install + if [ "${{ matrix.target }}" == "NoUI" ]; then + ninja -C build all install + else + ninja -C build all potfiles install + fi - name: Run FontForge tests run: CTEST_PARALLEL_LEVEL=100 ninja -C repo/build check - name: Build dist bundle @@ -80,6 +84,7 @@ jobs: env: CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} run: java -jar $DEPSPREFIX/crowdin-cli.jar upload -c .crowdin.yml + mac: runs-on: macos-14 steps: @@ -120,6 +125,7 @@ jobs: with: name: appbundle path: repo/build/osx/FontForge-*.dmg + windows: runs-on: windows-latest env: @@ -167,3 +173,63 @@ jobs: path: | fontforgebuilds/*.7z fontforgebuilds/fontforge-setup/*.exe + + # Test Python module build with MSVC toolchain + # This ensures compatibility with Python.org Windows builds (which use MSVC) + windows-msvc: + runs-on: windows-latest + env: + VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg-cache,readwrite" + steps: + - uses: actions/checkout@v4 + - name: Restore vcpkg binary cache + id: vcpkg-cache + uses: actions/cache/restore@v4 + with: + path: vcpkg-cache + key: vcpkg-files-${{ runner.os }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-files-${{ runner.os }}- + - name: Create vcpkg cache directory + run: New-Item -ItemType Directory -Force -Path vcpkg-cache + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgJsonGlob: 'vcpkg.json' + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Configure + run: | + $pythonExe = (Get-Command python).Source + Write-Host "Using Python: $pythonExe" + cmake -B build -G "NMake Makefiles" ` + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + -DCMAKE_BUILD_TYPE=Release ` + -DPython3_EXECUTABLE="$pythonExe" ` + -DENABLE_GUI=OFF ` + -DENABLE_PYTHON_SCRIPTING=ON ` + -DENABLE_PYTHON_EXTENSION=ON ` + -DENABLE_LIBSPIRO=OFF ` + -DENABLE_WOFF2=OFF ` + -DENABLE_DOCS=OFF ` + -DBUILD_SHARED_LIBS=ON + - name: Build + run: cmake --build build + - name: Run pyhook tests + run: | + # Copy DLLs to lib directory so Python can find them + Copy-Item build\bin\*.dll build\lib\ -Force + Copy-Item build\vcpkg_installed\x64-windows\bin\*.dll build\lib\ -Force -ErrorAction SilentlyContinue + ctest --test-dir build -R "_pyhook" --output-on-failure + - name: Save vcpkg binary cache + if: always() + uses: actions/cache/save@v4 + with: + path: vcpkg-cache + key: vcpkg-files-${{ runner.os }}-${{ hashFiles('vcpkg.json') }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000000..190d976b4a --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,158 @@ +# Build Python wheels for PyPI distribution +# Triggered on YYYYMMDD version tags (e.g., 20260220) +# Wheels are built with CalVer version (e.g., 2026.2.20) + +name: Build Wheels + +on: + # Build on YYYYMMDD version tags + push: + tags: + - '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' + # Allow manual trigger for testing + workflow_dispatch: + inputs: + debug: + description: 'Enable debug output' + required: false + default: false + type: boolean + +jobs: + build_wheels: + name: Build ${{ matrix.os }} ${{ matrix.arch }} wheels + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Linux x86_64 + - os: linux + arch: x86_64 + runner: ubuntu-latest + # Linux aarch64 (uses QEMU emulation) + - os: linux + arch: aarch64 + runner: ubuntu-latest + # macOS x86_64 + - os: macos + arch: x86_64 + runner: macos-13 + # macOS arm64 (native runner) + - os: macos + arch: arm64 + runner: macos-14 + # Windows x86_64 + - os: windows + arch: AMD64 + runner: windows-latest + + steps: + - uses: actions/checkout@v4 + + # Set up QEMU for Linux aarch64 emulation + - name: Set up QEMU + if: matrix.os == 'linux' && matrix.arch == 'aarch64' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.21 + env: + # Only build for the current architecture + CIBW_ARCHS: ${{ matrix.arch }} + # Debug output if requested + CIBW_BUILD_VERBOSITY: ${{ inputs.debug && '3' || '1' }} + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + run: pipx run build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + # Test that the sdist can be installed from source + test_sdist: + name: Test source distribution + needs: build_sdist + runs-on: ubuntu-latest + steps: + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build \ + libfreetype-dev libxml2-dev zlib1g-dev \ + libpng-dev libjpeg-dev libtiff-dev + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: sdist + + - name: Install from sdist + run: pip install fontforge-*.tar.gz -v + + - name: Test import + run: python -c "import fontforge; import psMat; print('fontforge version:', fontforge.version())" + + # Combine all artifacts into a single download + combine_artifacts: + name: Combine artifacts + needs: [build_wheels, build_sdist, test_sdist] + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: List artifacts + run: ls -la dist/ + + - name: Upload combined artifacts + uses: actions/upload-artifact@v4 + with: + name: all-packages + path: dist/ + + # PyPI upload - DISABLED for now + # Uncomment and configure trusted publishing when ready + # See: https://docs.pypi.org/trusted-publishers/ + # + # publish: + # name: Publish to PyPI + # needs: [build_wheels, build_sdist] + # runs-on: ubuntu-latest + # # Only publish on tag pushes + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + # environment: + # name: pypi + # url: https://pypi.org/p/fontforge + # permissions: + # id-token: write # Required for trusted publishing + # steps: + # - name: Download all artifacts + # uses: actions/download-artifact@v4 + # with: + # path: dist + # merge-multiple: true + # + # - name: Publish to PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 8f745bf771..7569fa431a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tags __pycache__ *.py[co] +build-cpp/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..ac4497f706 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +# ReadTheDocs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: doc/sphinx/conf.py + +python: + install: + - requirements: doc/sphinx/requirements.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index cb1d209658..2743fabb5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,27 @@ cmake_minimum_required(VERSION 3.5) # Update the version for each new release project(fontforge VERSION 20251009 LANGUAGES C CXX) +# Detect if we're being built by scikit-build-core for wheel packaging +# SKBUILD is set automatically by scikit-build-core +if(DEFINED SKBUILD) + set(BUILDING_WHEEL TRUE) + message(STATUS "Building for Python wheel (detected scikit-build-core)") +else() + set(BUILDING_WHEEL FALSE) +endif() + +# Generate Python-compatible CalVer version from YYYYMMDD format +# 20251009 -> 2025.10.9 +string(SUBSTRING "${PROJECT_VERSION}" 0 4 _FF_YEAR) +string(SUBSTRING "${PROJECT_VERSION}" 4 2 _FF_MONTH) +string(SUBSTRING "${PROJECT_VERSION}" 6 2 _FF_DAY) +math(EXPR _FF_MONTH_INT "${_FF_MONTH}") +math(EXPR _FF_DAY_INT "${_FF_DAY}") +set(FONTFORGE_PYTHON_VERSION "${_FF_YEAR}.${_FF_MONTH_INT}.${_FF_DAY_INT}") +if(BUILDING_WHEEL) + message(STATUS "Python wheel version: ${FONTFORGE_PYTHON_VERSION}") +endif() + # No in source builds if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}") message(FATAL_ERROR "In-source builds are disallowed. Create a build folder to run CMake from.") @@ -56,6 +77,7 @@ build_option(ENABLE_LIBPNG AUTO ON "Enables PNG support") build_option(ENABLE_LIBREADLINE AUTO ON "Enables Readline support") build_option(ENABLE_LIBTIFF AUTO ON "Enables TIFF support") build_option(ENABLE_WOFF2 AUTO ON "Enables WOFF2 support") +build_option(ENABLE_LIBUNIBREAK AUTO ON "Enables libunibreak for Unicode line breaking") build_option(ENABLE_HARFBUZZ AUTO ON "Enables HarfBuzz shaper support") build_option(ENABLE_DOCS AUTO ON "Enables building and installing documentation. Sphinx is required to build it.") @@ -105,14 +127,17 @@ set_font_family(label "${FONT_FAMILIES_LABEL}") set_font_family(mono "${FONT_FAMILIES_MONO}") # Required and default dependencies -# Note: When adding a dependency, ensure it has an imported target (wherever appropriate) +# Note: When adding a dependency, ensure it has an imported target (wherever appropriate) find_package(Freetype 2.3.7 REQUIRED) -find_package(Gettext REQUIRED) -find_package_with_target(Intl REQUIRED) -if(UNIX AND NOT APPLE) - find_package(GLIB 2.6 REQUIRED COMPONENTS gio gobject) -else() - find_package(GLIB 2.58 REQUIRED COMPONENTS gio gobject) +# Gettext/Intl and GLIB only needed for GUI builds +if(ENABLE_GUI) + find_package(Gettext REQUIRED) + find_package_with_target(Intl REQUIRED) + if(UNIX AND NOT APPLE) + find_package(GLIB 2.6 REQUIRED COMPONENTS gio gobject) + else() + find_package(GLIB 2.58 REQUIRED COMPONENTS gio gobject) + endif() endif() find_package(Iconv REQUIRED) find_package(LibXml2 REQUIRED) @@ -125,16 +150,37 @@ if(HAVE_PTHREAD_H) endif() if (ENABLE_HARFBUZZ) - find_package(PkgConfig REQUIRED) - pkg_check_modules(HarfBuzz REQUIRED IMPORTED_TARGET harfbuzz) + unset(_harfbuzz_required) + if(NOT ${ENABLE_HARFBUZZ} STREQUAL "AUTO") + set(_harfbuzz_required REQUIRED) + endif() + + if(MSVC) + find_package(harfbuzz CONFIG ${_harfbuzz_required}) + set(ENABLE_HARFBUZZ_RESULT ${harfbuzz_FOUND}) + if(harfbuzz_FOUND) + # Can't use ALIAS because harfbuzz::harfbuzz is itself an alias + add_library(PkgConfig::HarfBuzz INTERFACE IMPORTED) + target_link_libraries(PkgConfig::HarfBuzz INTERFACE harfbuzz::harfbuzz) + endif() + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(HarfBuzz ${_harfbuzz_required} IMPORTED_TARGET harfbuzz) + set(ENABLE_HARFBUZZ_RESULT ${HarfBuzz_FOUND}) + endif() endif() # Requirements for options if(ENABLE_GUI) - find_package(GDK3 3.10 REQUIRED) - find_package(PkgConfig) + find_package(PkgConfig REQUIRED) + pkg_check_modules(GDK3 REQUIRED IMPORTED_TARGET gdk-3.0>=3.10) + include(${CMAKE_CURRENT_LIST_DIR}/cmake/TargetUtils.cmake) + alias_imported_target(GDK3::GDK3 PkgConfig::GDK3) pkg_check_modules(GTK3 REQUIRED IMPORTED_TARGET gtk+-3.0) - pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-3.0) + pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-3.0) + if (ENABLE_HARFBUZZ) + pkg_check_modules(HarfBuzz REQUIRED IMPORTED_TARGET harfbuzz) + endif() endif() if(ENABLE_FREETYPE_DEBUGGER) @@ -150,25 +196,36 @@ set(Python3_FIND_REGISTRY LAST) if(BUILD_SHARED_LIBS) set(Python3_USE_STATIC_LIBS FALSE) endif() -find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development Interpreter) +# Wheel builds only need Development.Module (not Embed) +# Full builds need Development (Module + Embed) for fontforgeexe +if(BUILDING_WHEEL) + find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development.Module Interpreter) +else() + find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development Interpreter) +endif() find_package_auto(ENABLE_DOCS Sphinx) # Should come after ENABLE_PYTHON_SCRIPTING find_package_auto(ENABLE_LIBSPIRO Libspiro) find_package_auto(ENABLE_LIBGIF GIF) find_package_auto(ENABLE_LIBJPEG JPEG) find_package_auto(ENABLE_LIBPNG PNG) -find_package_auto(ENABLE_LIBREADLINE Readline) +find_package(PkgConfig) # Needed for pkg_check_auto calls below +pkg_check_auto(ENABLE_LIBREADLINE Readline readline) find_package_auto(ENABLE_LIBTIFF TIFF) -find_package_auto(ENABLE_WOFF2 WOFF2) +pkg_check_auto(ENABLE_WOFF2 WOFF2 "libwoff2enc libwoff2dec") +pkg_check_auto(ENABLE_LIBUNIBREAK Libunibreak libunibreak) find_package_auto(ENABLE_CODE_COVERAGE Gcov) add_subdirectory(inc) +add_subdirectory(extern) add_subdirectory(Unicode) add_subdirectory(gutils) add_subdirectory(fontforge) if(ENABLE_GUI) add_subdirectory(gdraw) endif() -add_subdirectory(fontforgeexe) +if(NOT BUILDING_WHEEL) + add_subdirectory(fontforgeexe) +endif() if(ENABLE_PYTHON_EXTENSION) if(ENABLE_PYTHON_SCRIPTING_RESULT AND BUILD_SHARED_LIBS) set(ENABLE_PYTHON_EXTENSION_RESULT ON) @@ -182,8 +239,10 @@ if(ENABLE_PYTHON_EXTENSION) add_subdirectory(pyhook) endif() endif() -add_subdirectory(contrib) -if(ENABLE_PYTHON_SCRIPTING_RESULT) +if(NOT BUILDING_WHEEL) + add_subdirectory(contrib) +endif() +if(ENABLE_PYTHON_SCRIPTING_RESULT AND NOT BUILDING_WHEEL) add_subdirectory(pycontrib) endif() if(BUILD_TESTING) @@ -192,12 +251,16 @@ endif() if(ENABLE_GUI) add_subdirectory(desktop) endif() -if(ENABLE_DOCS AND Sphinx_FOUND) +if(ENABLE_DOCS AND Sphinx_FOUND AND NOT BUILDING_WHEEL) add_subdirectory(doc) endif() -add_subdirectory(po) -add_subdirectory(share) -if(APPLE) +if(NOT BUILDING_WHEEL AND ENABLE_GUI) + add_subdirectory(po) +endif() +if(NOT BUILDING_WHEEL) + add_subdirectory(share) +endif() +if(APPLE AND ENABLE_GUI) add_subdirectory(osx) endif() diff --git a/INSTALL.md b/INSTALL.md index fc2b7f9ed2..bc3dfc8520 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -22,6 +22,16 @@ sudo ninja install For even more details on the build system itself, see [this wiki page](https://github.com/fontforge/fontforge/wiki/CMake-guide-for-FontForge). +## Python Module + +FontForge's Python bindings are also available directly from PyPI: + +```sh +pip install fontforge +``` + +If you install the Python module via pip, building and installing FontForge from source will detect this and skip installing the Python module to avoid conflicts. To switch back to app-installed modules, first run `pip uninstall fontforge`. + **Attention, Designers Who Love TrueType Hinting:** Run `cmake` with `-DENABLE_FREETYPE_DEBUGGER=/path/to/freetype/source`. This option enables advanced features for debugging TrueType font hints, such as stepping through hinting instructions one by one. diff --git a/README_PYPI.md b/README_PYPI.md new file mode 100644 index 0000000000..fd66c33575 --- /dev/null +++ b/README_PYPI.md @@ -0,0 +1,120 @@ +# FontForge Python Module + +FontForge is a powerful, open-source font editor. This package provides the +Python bindings for FontForge, allowing you to create, edit, and convert fonts +programmatically. + +## Installation + +```bash +pip install fontforge +``` + +## Quick Start + +```python +import fontforge + +# Create a new font +font = fontforge.font() +font.familyname = "MyFont" +font.fontname = "MyFont-Regular" + +# Create a glyph +glyph = font.createChar(ord('A')) +pen = glyph.glyphPen() +# Draw the glyph... + +# Open an existing font +font = fontforge.open("path/to/font.ttf") + +# Access glyphs +for glyph in font.glyphs(): + print(glyph.glyphname, glyph.unicode) + +# Generate output +font.generate("output.otf") +``` + +## Features + +- **Read/Write Font Formats**: OTF, TTF, UFO, Type1, WOFF, WOFF2, and more +- **Glyph Editing**: Create and modify glyph outlines programmatically +- **OpenType Features**: Build and manipulate GSUB/GPOS tables +- **Font Conversion**: Convert between font formats +- **Metrics & Kerning**: Set sidebearings, create kerning pairs +- **Validation**: Check fonts for common issues +- **Batch Processing**: Script operations across multiple fonts + +## Included Modules + +This package installs two Python modules: + +- **`fontforge`** - The main font manipulation module +- **`psMat`** - PostScript transformation matrix utilities + +```python +import fontforge +import psMat + +# Create a transformation matrix (scale 2x) +matrix = psMat.scale(2.0) + +# Apply to a glyph layer +glyph.transform(matrix) +``` + +## Documentation + +- [Python Scripting Guide](https://fontforge.org/docs/scripting/python.html) +- [Python API Reference](https://fontforge.org/docs/scripting/python/fontforge.html) +- [FontForge Documentation](https://fontforge.org/docs/) + +## Requirements + +- Python 3.8 or later +- No additional Python dependencies required + +The wheel includes all required native libraries (FreeType, libxml2, etc.). + +## Platform Support + +Pre-built wheels are available for: + +- **Linux**: x86_64, aarch64 (manylinux_2_28) +- **macOS**: x86_64, arm64 (macOS 11.0+) +- **Windows**: x86_64 + +## Security Considerations + +FontForge was designed for interactive use with trusted font files. The +codebase has known vulnerabilities, including buffer overruns, that can cause +crashes and may be exploitable. **Do not process untrusted input without +thorough sandboxing.** + +## Building from Source + +If no wheel is available for your platform, pip will attempt to build from source. This requires: + +- CMake 3.15+ +- C/C++ compiler +- FreeType, libxml2, zlib development headers + +See the [build documentation](https://github.com/fontforge/fontforge#building) for details. + +## FontForge Application + +This package provides only the Python module. For the full FontForge GUI application, see: + +- [FontForge Downloads](https://fontforge.org/en-US/downloads/) +- [GitHub Releases](https://github.com/fontforge/fontforge/releases) + +## License + +FontForge is licensed under the [GPLv3+](https://github.com/fontforge/fontforge/blob/master/COPYING.gplv3). + +## Links + +- [Homepage](https://fontforge.org) +- [GitHub Repository](https://github.com/fontforge/fontforge) +- [Issue Tracker](https://github.com/fontforge/fontforge/issues) diff --git a/Unicode/CMakeLists.txt b/Unicode/CMakeLists.txt index 908cc226d1..603fc3aa07 100644 --- a/Unicode/CMakeLists.txt +++ b/Unicode/CMakeLists.txt @@ -3,11 +3,11 @@ add_library(gunicode OBJECT ArabicForms.c char.c - memory.c - ucharmap.c + memory.cpp + ucharmap.cpp unialt.c uninames.c - ustring.c + ustring.cpp utype.c ) @@ -19,14 +19,14 @@ if(${CMAKE_VERSION} VERSION_LESS "3.12.0") make_object_interface(gunicode fontforge_common_headers Iconv::Iconv - Intl::Intl + $<$:Intl::Intl> ) else() target_link_libraries(gunicode PUBLIC fontforge_common_headers Iconv::Iconv - Intl::Intl + $<$:Intl::Intl> ) endif() diff --git a/Unicode/memory.c b/Unicode/memory.cpp similarity index 96% rename from Unicode/memory.c rename to Unicode/memory.cpp index 1613f77300..a5b506a9ca 100644 --- a/Unicode/memory.c +++ b/Unicode/memory.cpp @@ -29,7 +29,7 @@ #include "ustring.h" -#include +#include #include #include @@ -82,21 +82,21 @@ char *vsmprintf(const char *fmt, va_list args) { int len; va_copy(args2, args); - len = g_vsnprintf(NULL, 0, fmt, args2); + len = vsnprintf(NULL, 0, fmt, args2); va_end(args2); if (len < 0) { return NULL; } - ret = malloc(len + 1); + ret = (char*)malloc(len + 1); if (ret == NULL) { perror("smprintf"); abort(); return NULL; } - len = g_vsnprintf(ret, len + 1, fmt, args); + len = vsnprintf(ret, len + 1, fmt, args); if (len < 0) { free(ret); diff --git a/Unicode/ucharmap.c b/Unicode/ucharmap.cpp similarity index 89% rename from Unicode/ucharmap.c rename to Unicode/ucharmap.cpp index 2442f2d08b..509c331ce4 100644 --- a/Unicode/ucharmap.c +++ b/Unicode/ucharmap.cpp @@ -100,12 +100,12 @@ char *u2def_strncpy(char *to, const unichar_t *ufrom, size_t n) { static void* do_iconv(iconv_t cd, const void* inbuf, size_t incount, size_t inunitsize, size_t outunitsize) { size_t outbytes = (incount + 1) * outunitsize, outremain = outbytes; incount *= inunitsize; - char *buf = malloc(outbytes), *dst = buf; + char *buf = (char*) malloc(outbytes), *dst = buf; while (incount > 0 && buf) { if (iconv(cd, (iconv_arg2_t)&inbuf, &incount, &dst, &outremain) == (size_t)-1) { if (errno == E2BIG) { - buf = realloc(buf, outbytes*2); + buf = (char*) realloc(buf, outbytes*2); if (buf) { dst = buf + outbytes; outremain += outbytes; @@ -122,7 +122,7 @@ static void* do_iconv(iconv_t cd, const void* inbuf, size_t incount, size_t inun if (outremain < outunitsize) { outbytes += outunitsize - outremain; outremain = outunitsize; - buf = realloc(buf, outbytes); + buf = (char*) realloc(buf, outbytes); } if (buf) { memset(buf + (outbytes - outremain), 0, outremain); @@ -137,7 +137,7 @@ unichar_t *def2u_copy(const char *from) { return NULL; else if (is_local_encoding_utf8) return utf82u_copy(from); - return do_iconv(to_unicode, from, strlen(from), sizeof(from[0]), sizeof(unichar_t)); + return (unichar_t*) do_iconv(to_unicode, from, strlen(from), sizeof(from[0]), sizeof(unichar_t)); } char *u2def_copy(const unichar_t *ufrom) { @@ -145,7 +145,7 @@ char *u2def_copy(const unichar_t *ufrom) { return NULL; else if (is_local_encoding_utf8) return u2utf8_copy(ufrom); - return do_iconv(from_unicode, ufrom, u_strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + return (char*) do_iconv(from_unicode, ufrom, u_strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } char *def2utf8_copy(const char *from) { @@ -153,7 +153,7 @@ char *def2utf8_copy(const char *from) { return NULL; else if (is_local_encoding_utf8) return copy(from); - return do_iconv(to_utf8, from, strlen(from), sizeof(from[0]), sizeof(char)); + return (char*) do_iconv(to_utf8, from, strlen(from), sizeof(from[0]), sizeof(char)); } char *utf82def_copy(const char *ufrom) { @@ -161,7 +161,7 @@ char *utf82def_copy(const char *ufrom) { return NULL; else if (is_local_encoding_utf8) return copy(ufrom); - return do_iconv(from_utf8, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + return (char*) do_iconv(from_utf8, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } char *utf82def_copy_safe(const char *ufrom) { @@ -172,7 +172,7 @@ char *utf82def_copy_safe(const char *ufrom) { ret_val = utf82def_copy(ufrom); if (ret_val == NULL) { // This should never fail, unless iconv() is severely broken. - ret_val = do_iconv(from_utf8_safe, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + ret_val = (char*) do_iconv(from_utf8_safe, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } if (ret_val == NULL) { // We should never get here. diff --git a/Unicode/ustring.c b/Unicode/ustring.cpp similarity index 96% rename from Unicode/ustring.c rename to Unicode/ustring.cpp index 151fce071a..6e2de63dac 100644 --- a/Unicode/ustring.c +++ b/Unicode/ustring.cpp @@ -27,12 +27,14 @@ #include -#include "ffglib.h" #include "ustring.h" #include "utype.h" +#include "ffglib_compat.h" + #include #include +#include long uc_strcmp(const unichar_t *str1,const char *str2) { long ch1, ch2; @@ -119,21 +121,21 @@ return(ch1-ch2); } void cu_strcpy(char *to, const unichar_t *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *from++) != '\0' ) *(to++) = ch; *to = 0; } void uc_strcpy(unichar_t *to, const char *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *(unsigned char *) from++) != '\0' ) *(to++) = ch; *to = 0; } void u_strcpy(unichar_t *to, const unichar_t *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *from++) != '\0' ) *(to++) = ch; *to = 0; @@ -149,22 +151,22 @@ char *cc_strncpy(char *to, const char *from, int len) { } -void u_strncpy(register unichar_t *to, const unichar_t *from, int len) { - register unichar_t ch; +void u_strncpy(unichar_t *to, const unichar_t *from, int len) { + unichar_t ch; while ( (ch = *from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; } -void cu_strncpy(register char *to, const unichar_t *from, int len) { - register unichar_t ch; +void cu_strncpy(char *to, const unichar_t *from, int len) { + unichar_t ch; while ( (ch = *from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; } -void uc_strncpy(register unichar_t *to, const char *from, int len) { - register unichar_t ch; +void uc_strncpy(unichar_t *to, const char *from, int len) { + unichar_t ch; while ( (ch = *(unsigned char *) from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; @@ -195,8 +197,8 @@ void u_strncat(unichar_t *to, const unichar_t *from, int len) { } -int u_strlen(register const unichar_t *str) { - register int len = 0; +int u_strlen(const unichar_t *str) { + int len = 0; while ( *str++!='\0' ) ++len; @@ -212,7 +214,7 @@ int c_strlen( const char * p ) unichar_t *u_strchr(const unichar_t *str ,unichar_t ch) { - register unichar_t test; + unichar_t test; while ( (test=*(str++))!='\0' ) if ( test==ch ) @@ -222,7 +224,8 @@ return( NULL ); } unichar_t *u_strrchr(const unichar_t *str ,unichar_t ch) { - register unichar_t test, *last = NULL; + unichar_t test; + unichar_t *last = NULL; while ( (test=*(str++))!='\0' ) if ( test==ch ) @@ -321,7 +324,7 @@ unichar_t *u_copynallocm(const unichar_t *pt, long n, long m) { if ( n*sizeof(unichar_t)>=MEMORY_MASK ) n = MEMORY_MASK/sizeof(unichar_t)-1; #endif - res = malloc((m+1)*sizeof(unichar_t)); + res = (unichar_t *) malloc((m+1)*sizeof(unichar_t)); memcpy(res,pt,n*sizeof(unichar_t)); res[n]='\0'; return(res); @@ -860,7 +863,7 @@ long utf82u_strlen(const char *utf8_str) { return( len ); } -void utf8_strncpy(register char *to, const char *from, int len) { +void utf8_strncpy(char *to, const char *from, int len) { /* copy n characters NOT bytes */ const char *old = from; while ( len && *old ) { @@ -989,7 +992,7 @@ return( newcr ); int AllAscii(const char *txt) { /* Verify string only All ASCII printable characters */ - register unsigned char ch; + unsigned char ch; if ( txt==NULL ) return( false ); @@ -1044,16 +1047,16 @@ int endswith(const char *haystack,const char *needle) { int nedlen = strlen( needle ); if( haylen < nedlen ) return 0; - char* p = strstr( haystack + haylen - nedlen, needle ); + const char* p = strstr( haystack + haylen - nedlen, needle ); return p == ( haystack + haylen - nedlen ); } int endswithi(const char *haystackZ,const char *needleZ) { - gchar* haystack = g_ascii_strdown(haystackZ,-1); - gchar* needle = g_ascii_strdown(needleZ,-1); + char* haystack = ff_ascii_strdown(haystackZ,-1); + char* needle = ff_ascii_strdown(needleZ,-1); int ret = endswith( haystack, needle ); - g_free( haystack ); - g_free( needle ); + free( haystack ); + free( needle ); return ret; } @@ -1062,8 +1065,8 @@ int endswithi_partialExtension( const char *haystackZ,const char *needleZ) { if( nedlen == 0 ) { return 0; } - gchar* haystack = g_ascii_strdown(haystackZ,-1); - gchar* needle = g_ascii_strdown(needleZ,-1); + char* haystack = ff_ascii_strdown(haystackZ,-1); + char* needle = ff_ascii_strdown(needleZ,-1); int ret = 0; int i = nedlen-1; ret |= endswith( haystack, needle ); @@ -1071,8 +1074,8 @@ int endswithi_partialExtension( const char *haystackZ,const char *needleZ) { needle[i] = '\0'; ret |= endswith( haystack, needle ); } - g_free( haystack ); - g_free( needle ); + free( haystack ); + free( needle ); return ret; } @@ -1173,7 +1176,7 @@ char* tostr( int v ) void realloc_tail(char** p_buf, size_t size_delta, char** p_tail, char** p_proc) { size_t new_size = size_delta + (*p_tail - *p_buf); - char* new_buf = realloc(*p_buf, new_size); + char* new_buf = (char*) realloc(*p_buf, new_size); *p_tail = new_buf + new_size; if (p_proc) *p_proc = new_buf + (*p_proc - *p_buf); *p_buf = new_buf; diff --git a/WHEEL_BUILD_STATUS.md b/WHEEL_BUILD_STATUS.md new file mode 100644 index 0000000000..16d301be9b --- /dev/null +++ b/WHEEL_BUILD_STATUS.md @@ -0,0 +1,45 @@ +# Wheel Build Status + +Temporary tracking document for PyPI wheel distribution work. + +For technical documentation, see `doc/sphinx/scripting/python-extension.rst`. + +## cibuildwheel Configuration + +```toml +[tool.cibuildwheel] +skip = ["pp*", "*-win32", "*-manylinux_i686", "*-musllinux*"] + +[tool.cibuildwheel.linux] +manylinux-x86_64-image = "manylinux_2_28" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] +repair-wheel-command = "delocate-wheel -w {dest_dir} {wheel}" + +[tool.cibuildwheel.windows] +archs = ["AMD64"] +repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" +``` + +## Implementation Checklist + +- [x] `BUILDING_WHEEL` detection in CMakeLists.txt +- [x] Python version generation (CalVer) +- [x] Wheel install layout in pyhook/CMakeLists.txt +- [x] pyproject.toml with scikit-build-core +- [x] Custom version provider +- [x] cibuildwheel in GitHub Actions +- [x] README_PYPI.md +- [x] Context-aware version (CalVer vs numeric) +- [x] dist-info on app install +- [ ] PyPI trusted publishing +- [ ] sdist build testing +- [ ] Initial PyPI release + +## Notes + +- Windows wheels require MSVC build (separate from MinGW GUI build) +- Wheels bundle all dependencies via auditwheel/delocate/delvewheel +- Python versions: support current CPython releases (check pyproject.toml) diff --git a/_build_meta/version_provider.py b/_build_meta/version_provider.py new file mode 100644 index 0000000000..49f24dc71d --- /dev/null +++ b/_build_meta/version_provider.py @@ -0,0 +1,46 @@ +# FontForge version provider for scikit-build-core +# Extracts version from CMakeLists.txt and converts YYYYMMDD to CalVer YYYY.M.D + +from __future__ import annotations + +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping + +__all__ = ["dynamic_metadata"] + + +def dynamic_metadata( + field: str, + settings: Mapping[str, Any], +) -> str: + if field != "version": + msg = f"This provider only supports 'version', not {field!r}" + raise RuntimeError(msg) + + # Read CMakeLists.txt from project root + cmake_file = Path("CMakeLists.txt") + if not cmake_file.exists(): + msg = "CMakeLists.txt not found" + raise RuntimeError(msg) + + content = cmake_file.read_text(encoding="utf-8") + + # Extract version from: project(fontforge VERSION 20251009 ...) + match = re.search(r"project\s*\(\s*fontforge\s+VERSION\s+(\d{8})", content) + if not match: + msg = "Could not find project VERSION in CMakeLists.txt" + raise RuntimeError(msg) + + version_str = match.group(1) + + # Parse YYYYMMDD format + year = version_str[0:4] + month = int(version_str[4:6]) # int() strips leading zero + day = int(version_str[6:8]) + + # Return CalVer format: YYYY.M.D + return f"{year}.{month}.{day}" diff --git a/cmake/FontForgeConfigure.cmake b/cmake/FontForgeConfigure.cmake index 3184dc2bc2..2b90e65f43 100644 --- a/cmake/FontForgeConfigure.cmake +++ b/cmake/FontForgeConfigure.cmake @@ -58,8 +58,10 @@ function(fontforge_generate_config template destination) check_function_exists(realpath HAVE_REALPATH) cmake_pop_check_state() - # These are hard requirements/unsupported, should get rid of these - set(HAVE_LIBINTL_H 1) + # Set HAVE_LIBINTL_H only if Intl was found + if(Intl_FOUND) + set(HAVE_LIBINTL_H 1) + endif() # Configurable settings set(FONTFORGE_CONFIG_SHOW_RAW_POINTS ${ENABLE_DEBUG_RAW_POINTS}) @@ -79,6 +81,7 @@ function(fontforge_generate_config template destination) _set_negated(_NO_LIBJPEG "${ENABLE_LIBJPEG_RESULT}") _set_negated(_NO_LIBPNG "${ENABLE_LIBPNG_RESULT}") _set_negated(_NO_LIBSPIRO "${ENABLE_LIBSPIRO_RESULT}") + _set_negated(_NO_LIBUNIBREAK "${ENABLE_LIBUNIBREAK_RESULT}") _set_negated(_NO_LIBTIFF "${ENABLE_LIBTIFF_RESULT}") _set_negated(_NO_LIBUNGIF "${ENABLE_LIBGIF_RESULT}") _set_negated(_NO_PYTHON "${ENABLE_PYTHON_SCRIPTING_RESULT}") diff --git a/cmake/PackageUtils.cmake b/cmake/PackageUtils.cmake index 9be37d02f2..bc47ad7870 100644 --- a/cmake/PackageUtils.cmake +++ b/cmake/PackageUtils.cmake @@ -42,3 +42,28 @@ macro(find_package_auto auto_option) set(${auto_option}_RESULT ${${ARGV1}_FOUND}) endif() endmacro() + +# pkg_check_auto - pkg-config based alternative to find_package_auto +# +# Usage: pkg_check_auto(ENABLE_OPTION TargetName pkg-config-name) +# +# If ENABLE_OPTION is truthy, calls pkg_check_modules to find the package. +# Creates an aliased target TargetName::TargetName from PkgConfig::TargetName. +# Sets ENABLE_OPTION_RESULT to the result. +# +macro(pkg_check_auto auto_option target_name pkg_name) + if(${auto_option}) + unset(_pkg_check_auto_required) + if(NOT ${${auto_option}} STREQUAL "AUTO") + set(_pkg_check_auto_required REQUIRED) + endif() + + pkg_check_modules(${target_name} ${_pkg_check_auto_required} IMPORTED_TARGET ${pkg_name}) + set(${auto_option}_RESULT ${${target_name}_FOUND}) + + if(${target_name}_FOUND AND NOT TARGET ${target_name}::${target_name}) + include(${PROJECT_SOURCE_DIR}/cmake/TargetUtils.cmake) + alias_imported_target(${target_name}::${target_name} PkgConfig::${target_name}) + endif() + endif() +endmacro() diff --git a/cmake/TestUtils.cmake b/cmake/TestUtils.cmake index cc7546c94f..f5d61e63fd 100644 --- a/cmake/TestUtils.cmake +++ b/cmake/TestUtils.cmake @@ -63,7 +63,10 @@ function(_add_systest test_mode binary test_script) endfunction() function(add_ff_test test_script) - _add_systest(ff "$" "${test_script}" ${ARGN}) + # Native scripting tests require fontforgeexe, not available in wheel builds + if(NOT BUILDING_WHEEL) + _add_systest(ff "$" "${test_script}" ${ARGN}) + endif() endfunction() function(add_py_test test_script) @@ -73,7 +76,10 @@ function(add_py_test test_script) set(_disable_pyhook 1) endif() - _add_systest(py "$" "${test_script}" ${ARGN}) + # fontforgeexe-based Python tests not available in wheel builds + if(NOT BUILDING_WHEEL) + _add_systest(py "$" "${test_script}" ${ARGN}) + endif() if(ENABLE_PYTHON_EXTENSION_RESULT AND NOT _disable_pyhook) _add_systest(pyhook "${Python3_EXECUTABLE}" "${test_script}" ${ARGN}) endif() diff --git a/cmake/packages/FindCairo.cmake b/cmake/packages/FindCairo.cmake deleted file mode 100644 index 1a9d9f5157..0000000000 --- a/cmake/packages/FindCairo.cmake +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (C) 2012 Raphael Kubo da Costa -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS -# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindCairo ---------- - -Find the Cairo library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Cairo::Cairo`` - The Cairo ``cairo`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Cairo_FOUND`` - true if the Cairo headers and libraries were found -``Cairo_INCLUDE_DIRS`` - directories containing the Cairo headers. -``Cairo_LIBRARIES`` - the library to link against -``Cairo_VERSION`` - the version of Cairo found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(PC_CAIRO QUIET cairo) - -find_path(Cairo_INCLUDE_DIRS - NAMES cairo.h - HINTS ${PC_CAIRO_INCLUDEDIR} - ${PC_CAIRO_INCLUDE_DIRS} - PATH_SUFFIXES cairo -) - -find_library(Cairo_LIBRARIES - NAMES cairo - HINTS ${PC_CAIRO_LIBDIR} - ${PC_CAIRO_LIBRARY_DIRS} -) - -if(Cairo_INCLUDE_DIRS) - if(EXISTS "${Cairo_INCLUDE_DIRS}/cairo-version.h") - FILE(READ "${Cairo_INCLUDE_DIRS}/cairo-version.h" CAIRO_VERSION_CONTENT) - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MAJOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MAJOR "${CMAKE_MATCH_1}") - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MINOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MINOR "${CMAKE_MATCH_1}") - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MICRO +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MICRO "${CMAKE_MATCH_1}") - - SET(Cairo_VERSION "${Cairo_VERSION_MAJOR}.${Cairo_VERSION_MINOR}.${Cairo_VERSION_MICRO}") - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Cairo REQUIRED_VARS Cairo_INCLUDE_DIRS Cairo_LIBRARIES - VERSION_VAR Cairo_VERSION) - -mark_as_advanced( - Cairo_INCLUDE_DIRS - Cairo_LIBRARIES -) - -if(Cairo_FOUND AND NOT TARGET Cairo::Cairo) - add_library(Cairo::Cairo INTERFACE IMPORTED) - set_property(TARGET Cairo::Cairo PROPERTY - INTERFACE_INCLUDE_DIRECTORIES "${Cairo_INCLUDE_DIRS}") - set_property(TARGET Cairo::Cairo PROPERTY - INTERFACE_LINK_LIBRARIES "${Cairo_LIBRARIES}") -endif() diff --git a/cmake/packages/FindGDK3.cmake b/cmake/packages/FindGDK3.cmake deleted file mode 100644 index 59672d9ab2..0000000000 --- a/cmake/packages/FindGDK3.cmake +++ /dev/null @@ -1,47 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindGDK3 ---------- - -Find the GDK3 library. This currently depends on pkg-config to work. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``GDK3::GDK3`` - The GDK3 ``GDK3`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``GDK3_FOUND`` - true if the GDK3 headers and libraries were found -``GDK3_INCLUDE_DIRS`` - directories containing the GDK3 headers. -``GDK3_LIBRARIES`` - the library to link against -``GDK3_VERSION`` - the version of GDK3 found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(GDK3 QUIET IMPORTED_TARGET gdk-3.0) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - GDK3 - REQUIRED_VARS - GDK3_LIBRARIES - GDK3_INCLUDE_DIRS - VERSION_VAR - GDK3_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(GDK3::GDK3 PkgConfig::GDK3) diff --git a/cmake/packages/FindPango.cmake b/cmake/packages/FindPango.cmake deleted file mode 100644 index 2f78353619..0000000000 --- a/cmake/packages/FindPango.cmake +++ /dev/null @@ -1,51 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindPango ---------- - -Find the Pango library. This currently depends on pkg-config to work. - -If additional ``COMPONENTS`` are passed in, these are passed on to -``pkg_check_modules()``. Typical usages for this are to include additional -dependencies on ``pangocairo``, ``pangoft2``, ``pangoxft`` and ``pangowin32``. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Pango::Pango`` - The Pango ``pango`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Pango_FOUND`` - true if the Pango headers and libraries were found -``Pango_INCLUDE_DIRS`` - directories containing the Pango headers. -``Pango_LIBRARIES`` - the library to link against -``Pango_VERSION`` - the version of Pango found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(Pango QUIET IMPORTED_TARGET pango ${Pango_FIND_COMPONENTS}) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - Pango - REQUIRED_VARS - Pango_LIBRARIES - Pango_INCLUDE_DIRS - VERSION_VAR - Pango_pango_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(Pango::Pango PkgConfig::Pango) \ No newline at end of file diff --git a/cmake/packages/FindReadline.cmake b/cmake/packages/FindReadline.cmake deleted file mode 100644 index 644df60c8b..0000000000 --- a/cmake/packages/FindReadline.cmake +++ /dev/null @@ -1,76 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindReadline ------------- - -Find the Readline library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Readline::Readline`` - The Readline ``readline`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Readline_FOUND`` - true if the Readline headers and libraries were found -``Readline_INCLUDE_DIRS`` - directories containing the Readline headers. -``Readline_LIBRARIES`` - the library to link against -``Readline_VERSION`` - the version of Readline found - -#]=======================================================================] - -find_path(Readline_INCLUDE_DIRS - NAMES readline.h - HINTS ${Readline_ROOT_DIR} - PATH_SUFFIXES readline -) - -find_library(Readline_LIBRARIES - NAMES readline termcap - HINTS ${Readline_ROOT_DIR} -) - -if(Readline_INCLUDE_DIRS) - if(EXISTS "${Readline_INCLUDE_DIRS}/readline.h") - file(READ "${Readline_INCLUDE_DIRS}/readline.h" READLINE_H_CONTENTS) - string(REGEX MATCH "#define RL_VERSION_MAJOR[\t ]+([0-9]+)" _dummy "${READLINE_H_CONTENTS}") - set(Readline_VERSION_MAJOR "${CMAKE_MATCH_1}") - string(REGEX MATCH "#define RL_VERSION_MINOR[\t ]+([0-9]+)" _dummy "${READLINE_H_CONTENTS}") - set(Readline_VERSION_MINOR "${CMAKE_MATCH_1}") - set(Readline_VERSION "${Readline_VERSION_MAJOR}.${Readline_VERSION_MINOR}") - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - Readline - REQUIRED_VARS - Readline_LIBRARIES - Readline_INCLUDE_DIRS - VERSION_VAR - Readline_VERSION -) - -mark_as_advanced( - Readline_INCLUDE_DIRS - Readline_LIBRARIES -) - -if(Readline_FOUND AND NOT TARGET Readline::Readline) - add_library(Readline::Readline INTERFACE IMPORTED) - set_property(TARGET Readline::Readline PROPERTY - INTERFACE_INCLUDE_DIRECTORIES "${Readline_INCLUDE_DIRS}") - set_property(TARGET Readline::Readline PROPERTY - INTERFACE_LINK_LIBRARIES "${Readline_LIBRARIES}") -endif() diff --git a/cmake/packages/FindWOFF2.cmake b/cmake/packages/FindWOFF2.cmake deleted file mode 100644 index 523cea0155..0000000000 --- a/cmake/packages/FindWOFF2.cmake +++ /dev/null @@ -1,50 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindWOFF2 ---------- - -Find the WOFF2 library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``WOFF2::WOFF2`` - The WOFF2 ``woff2`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``WOFF2_FOUND`` - true if the WOFF2 headers and libraries were found -``WOFF2_INCLUDE_DIRS`` - directories containing the WOFF2 headers. -``WOFF2_LIBRARIES`` - the library to link against -``WOFF2_VERSION`` - the version of WOFF2 found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(WOFF2 QUIET IMPORTED_TARGET libwoff2enc libwoff2dec) - -if(WOFF2_FOUND) - set(WOFF2_VERSION ${WOFF2_libwoff2enc_VERSION}) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - WOFF2 - REQUIRED_VARS - WOFF2_LIBRARIES - VERSION_VAR - WOFF2_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(WOFF2::WOFF2 PkgConfig::WOFF2) diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index c016c71ccb..72716fcfd9 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -16,8 +16,11 @@ copyright = '2000-2012 by George Williams, 2012-2025 by FontForge authors' author = 'FontForge authors' -# The full version, including alpha/beta/rc tags -release = '20251009' +# The full version - read from environment (set by ReadTheDocs) or default +# ReadTheDocs sets READTHEDOCS_VERSION to the tag/branch name +release = os.environ.get('READTHEDOCS_VERSION', '20251009') +if release.startswith('v'): + release = release[1:] # Strip 'v' prefix from tags # -- General configuration --------------------------------------------------- diff --git a/doc/sphinx/requirements.txt b/doc/sphinx/requirements.txt new file mode 100644 index 0000000000..dae33fb0bb --- /dev/null +++ b/doc/sphinx/requirements.txt @@ -0,0 +1,2 @@ +# Sphinx documentation build requirements +sphinx>=4.0 diff --git a/doc/sphinx/scripting/python-extension.rst b/doc/sphinx/scripting/python-extension.rst new file mode 100644 index 0000000000..8311c4cad8 --- /dev/null +++ b/doc/sphinx/scripting/python-extension.rst @@ -0,0 +1,128 @@ +Python Extension Technical Details +================================== + +This document covers the technical details of FontForge's Python extension +module, including building wheels for PyPI distribution. + +Modules +------- + +FontForge provides two Python modules: + +- ``fontforge`` - Main font manipulation API +- ``psMat`` - PostScript matrix operations + +Build Scenarios +--------------- + +.. list-table:: + :header-rows: 1 + + * - Scenario + - Detection + - GUI + - Install Location + * - Wheel (pip) + - ``SKBUILD`` defined + - OFF + - Wheel root + * - System install + - Manual cmake + - ON/OFF + - site-packages + +Dependencies +------------ + +Required (always linked): + libxml2, zlib, libiconv + +Bundled in wheels: + FreeType, libpng, libjpeg, libtiff, libspiro, woff2 + +Not needed for wheels (GUI-only): + GLIB, Gettext/Intl, Readline + +Versioning +---------- + +The source uses ``PROJECT_VERSION = 20251009`` (YYYYMMDD format). +Wheels use CalVer format ``2025.10.9``. + +``fontforge.version()`` returns: + +- CalVer when imported as a module (from site-packages or wheel) +- Numeric format when Python is embedded in the running FontForge application + +CMake transformation:: + + string(SUBSTRING "${PROJECT_VERSION}" 0 4 _FF_YEAR) + string(SUBSTRING "${PROJECT_VERSION}" 4 2 _FF_MONTH) + string(SUBSTRING "${PROJECT_VERSION}" 6 2 _FF_DAY) + math(EXPR _FF_MONTH_INT "${_FF_MONTH}") + math(EXPR _FF_DAY_INT "${_FF_DAY}") + set(FONTFORGE_PYTHON_VERSION "${_FF_YEAR}.${_FF_MONTH_INT}.${_FF_DAY_INT}") + +Windows MSVC Build +------------------ + +Windows wheels require MSVC because Python.org's Windows Python is built +with MSVC. The MinGW build continues for the GUI application. + +MSVC compatibility patterns:: + + // UNUSED macro + #ifdef _MSC_VER + # define UNUSED(x) x + #elif defined(__GNUC__) + # define UNUSED(x) UNUSED_ ## x __attribute__((unused)) + #endif + +DLL exports: MinGW uses ``--export-all-symbols``. MSVC needs explicit +``__declspec(dllexport)`` or a DEF file. + +vcpkg provides dependencies:: + + { + "name": "fontforge", + "dependencies": ["freetype", "libxml2", "glib", "gettext", "libiconv", "zlib"] + } + +Build Configuration +------------------- + +Wheels are built using scikit-build-core. See ``pyproject.toml`` for the +current configuration. + +:: + + [build-system] + requires = ["scikit-build-core>=0.5"] + build-backend = "scikit_build_core.build" + + [project] + name = "fontforge" + dynamic = ["version"] + + [tool.scikit-build] + cmake.args = ["-DENABLE_GUI=OFF", "-DENABLE_PYTHON_EXTENSION=ON"] + wheel.install-dir = "." + +Wheel Target Platforms +---------------------- + +The project builds wheels for: + +- Linux: x86_64, aarch64 (manylinux_2_28) +- macOS: x86_64, arm64 (separate wheels, macOS 11.0+) +- Windows: x64 (MSVC) + +Install Conflict Handling +------------------------- + +App installs create ``fontforge-{version}.dist-info/`` with +``INSTALLER = fontforge-app``. This allows ``pip list`` to show the install +and ``pip uninstall`` to work. + +If a pip-installed version exists, app install skips and suggests +``pip uninstall fontforge``. diff --git a/doc/sphinx/scripting/python.rst b/doc/sphinx/scripting/python.rst index c8b6296f04..44e8da4be8 100644 --- a/doc/sphinx/scripting/python.rst +++ b/doc/sphinx/scripting/python.rst @@ -57,13 +57,40 @@ Trivial example .. _python.extension: -FontForge as a python extension +FontForge as a Python extension ------------------------------- -In addition to embedding Python, FontForge typically installs a Python -module accessible to the system's ``python`` executable, which can be -accessed using: +FontForge's Python modules can be used independently of the GUI application. + +Installation via pip +^^^^^^^^^^^^^^^^^^^^ + +:: + + pip install fontforge + +Pre-built wheels are available for: + +- Linux: x86_64, aarch64 +- macOS: x86_64, arm64 +- Windows: x86_64 + +The wheels include all required native libraries (FreeType, libxml2, etc.). + +Installation from system packages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When FontForge is installed via system package manager or built from source +with ``-DENABLE_PYTHON_EXTENSION=ON``, the modules are installed to the +system Python's site-packages. + +Usage +^^^^^ :: >>> import fontforge + >>> import psMat + +For technical details on building wheels and the extension architecture, +see :doc:`python-extension`. diff --git a/doc/sphinx/scripting/scripting.rst b/doc/sphinx/scripting/scripting.rst index 6d7721a445..b3b1777fa9 100644 --- a/doc/sphinx/scripting/scripting.rst +++ b/doc/sphinx/scripting/scripting.rst @@ -12,6 +12,7 @@ means use the old interpreter) scripting-alpha python + python-extension .. contents:: :backlinks: none diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt new file mode 100644 index 0000000000..b22816e18c --- /dev/null +++ b/extern/CMakeLists.txt @@ -0,0 +1,11 @@ +# Vendored third-party libraries + +# mINI - header-only INI file parser +add_library(mINI INTERFACE) +target_include_directories(mINI INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/mINI) +# Enable case-sensitive mode for GKeyFile compatibility +target_compile_definitions(mINI INTERFACE MINI_CASE_SENSITIVE) + +# cxxopts - header-only command line argument parser +add_library(cxxopts INTERFACE) +target_include_directories(cxxopts INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cxxopts) diff --git a/extern/cxxopts/cxxopts.hpp b/extern/cxxopts/cxxopts.hpp new file mode 100644 index 0000000000..537b2e79f5 --- /dev/null +++ b/extern/cxxopts/cxxopts.hpp @@ -0,0 +1,2923 @@ +/* + +Copyright (c) 2014-2022 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// vim: ts=2:sw=2:expandtab + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +# if __has_include() +# include +# ifdef __cpp_lib_filesystem +# define CXXOPTS_HAS_FILESYSTEM +# endif +# endif +#endif + +#define CXXOPTS_FALLTHROUGH +#ifdef __has_cpp_attribute + #if __has_cpp_attribute(fallthrough) + #undef CXXOPTS_FALLTHROUGH + #define CXXOPTS_FALLTHROUGH [[fallthrough]] + #endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 3 +#define CXXOPTS__VERSION_PATCH 1 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 + #define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif + +namespace cxxopts { +static constexpr struct { + uint8_t major, minor, patch; +} version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH +}; +} // namespace cxxopts + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts { + +using String = icu::UnicodeString; + +inline +String +toLocalString(std::string s) +{ + return icu::UnicodeString::fromUTF8(std::move(s)); +} + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") +// This will be ignored under other compilers like LLVM clang. +class UnicodeStringIterator +{ + public: + + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; +}; +CXXOPTS_DIAGNOSTIC_POP + +inline +String& +stringAppend(String&s, String a) +{ + return s.append(std::move(a)); +} + +inline +String& +stringAppend(String& s, std::size_t n, UChar32 c) +{ + for (std::size_t i = 0; i != n; ++i) + { + s.append(c); + } + + return s; +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; +} + +inline +size_t +stringLength(const String& s) +{ + return static_cast(s.length()); +} + +inline +std::string +toUTF8String(const String& s) +{ + std::string result; + s.toUTF8String(result); + + return result; +} + +inline +bool +empty(const String& s) +{ + return s.isEmpty(); +} + +} // namespace cxxopts + +namespace std { + +inline +cxxopts::UnicodeStringIterator +begin(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, 0); +} + +inline +cxxopts::UnicodeStringIterator +end(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, s.length()); +} + +} // namespace std + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts { + +using String = std::string; + +template +T +toLocalString(T&& t) +{ + return std::forward(t); +} + +inline +std::size_t +stringLength(const String& s) +{ + return s.length(); +} + +inline +String& +stringAppend(String&s, const String& a) +{ + return s.append(a); +} + +inline +String& +stringAppend(String& s, std::size_t n, char c) +{ + return s.append(n, c); +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + return s.append(begin, end); +} + +template +std::string +toUTF8String(T&& t) +{ + return std::forward(t); +} + +inline +bool +empty(const std::string& s) +{ + return s.empty(); +} + +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts { + +namespace { +CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); +} // namespace + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we +// want to silence it: warning: base class 'class +// std::enable_shared_from_this' has accessible non-virtual +// destructor This will be ignored under other compilers like LLVM clang. +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + +// some older versions of GCC warn under this warning +CXXOPTS_IGNORE_WARNING("-Weffc++") +class Value : public std::enable_shared_from_this +{ + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + add(const std::string& text) const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; +}; + +CXXOPTS_DIAGNOSTIC_POP + +namespace exceptions { + +class exception : public std::exception +{ + public: + explicit exception(std::string message) + : m_message(std::move(message)) + { + } + + CXXOPTS_NODISCARD + const char* + what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; +}; + +class specification : public exception +{ + public: + + explicit specification(const std::string& message) + : exception(message) + { + } +}; + +class parsing : public exception +{ + public: + explicit parsing(const std::string& message) + : exception(message) + { + } +}; + +class option_already_exists : public specification +{ + public: + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } +}; + +class invalid_option_format : public specification +{ + public: + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) + { + } +}; + +class invalid_option_syntax : public parsing { + public: + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } +}; + +class no_such_option : public parsing +{ + public: + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } +}; + +class missing_argument : public parsing +{ + public: + explicit missing_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } +}; + +class option_requires_argument : public parsing +{ + public: + explicit option_requires_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } +}; + +class gratuitous_argument_for_option : public parsing +{ + public: + gratuitous_argument_for_option + ( + const std::string& option, + const std::string& arg + ) + : parsing( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } +}; + +class requested_option_not_present : public parsing +{ + public: + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") + { + } +}; + +class option_has_no_value : public exception +{ + public: + explicit option_has_no_value(const std::string& option) + : exception( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } +}; + +class incorrect_argument_type : public parsing +{ + public: + explicit incorrect_argument_type + ( + const std::string& arg + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } +}; + +} // namespace exceptions + + +template +void throw_or_mimic(const std::string& text) +{ + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{text}; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif +} + +using OptionNames = std::vector; + +namespace values { + +namespace parser_tool { + +struct IntegerDesc +{ + std::string negative = ""; + std::string base = ""; + std::string value = ""; +}; +struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; +}; + +#ifdef CXXOPTS_NO_REGEX +inline IntegerDesc SplitInteger(const std::string &text) +{ + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char *pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') + { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) + { + return true; + } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; +} + +inline bool IsFalseText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) + { + return true; + } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; +} + +inline OptionNames split_option_names(const std::string &text) +{ + OptionNames split_names; + + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) + { + throw_or_mimic(text); + } + + while (token_start_pos < length) { + const auto &npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { + throw_or_mimic(text); + } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { + next_delimiter_pos = length; + } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ + { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } + } + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + ArguDesc argu_desc; + const char *pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) + { + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_' || *pdata == '.') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) + { + pdata += 1; + if(isalnum(*pdata, std::locale::classic())) { + argu_desc.grouping = true; + argu_desc.arg_name = std::string(pdata); + matched = true; + } + } + return argu_desc; +} + +#else // CXXOPTS_NO_REGEX + +namespace { +CXXOPTS_LINKONCE +const char* const integer_pattern = + "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; +CXXOPTS_LINKONCE +const char* const truthy_pattern = + "(t|T)(rue)?|1"; +CXXOPTS_LINKONCE +const char* const falsy_pattern = + "(f|F)(alse)?|0"; +CXXOPTS_LINKONCE +const char* const option_pattern = + "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; +CXXOPTS_LINKONCE +const char* const option_specifier_pattern = + "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; +CXXOPTS_LINKONCE +const char* const option_specifier_separator_pattern = ", *"; + +} // namespace + +inline IntegerDesc SplitInteger(const std::string &text) +{ + static const std::basic_regex integer_matcher(integer_pattern); + + std::smatch match; + std::regex_match(text, match, integer_matcher); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) + { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + static const std::basic_regex truthy_matcher(truthy_pattern); + std::smatch result; + std::regex_match(text, result, truthy_matcher); + return !result.empty(); +} + +inline bool IsFalseText(const std::string &text) +{ + static const std::basic_regex falsy_matcher(falsy_pattern); + std::smatch result; + std::regex_match(text, result, falsy_matcher); + return !result.empty(); +} + +// Gets the option names specified via a single, comma-separated string, +// and returns the separate, space-discarded, non-empty names +// (without considering which or how many are single-character) +inline OptionNames split_option_names(const std::string &text) +{ + static const std::basic_regex option_specifier_matcher(option_specifier_pattern); + if (!std::regex_match(text.c_str(), option_specifier_matcher)) + { + throw_or_mimic(text); + } + + OptionNames split_names; + + static const std::basic_regex option_specifier_separator_matcher(option_specifier_separator_pattern); + constexpr int use_non_matches { -1 }; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator_matcher, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + static const std::basic_regex option_matcher(option_pattern); + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) + { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; +} + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX +} // namespace parser_tool + +namespace detail { + +template +struct SignedCheck; + +template +struct SignedCheck +{ + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } +}; + +template +struct SignedCheck +{ + template + void + operator()(bool, U, const std::string&) const {} +}; + +template +void +check_signed_range(bool negative, U value, const std::string& text) +{ + SignedCheck::is_signed>()(negative, value, text); +} + +} // namespace detail + +template +void +checked_negate(R& r, T&& t, const std::string&, std::true_type) +{ + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t-1)-1); +} + +template +void +checked_negate(R&, T&&, const std::string& text, std::false_type) +{ + throw_or_mimic(text); +} + +template +void +integer_parser(const std::string& text, T& value) +{ + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string & value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) + { + US digit = 0; + + if (ch >= '0' && ch <= '9') + { + digit = static_cast(ch - '0'); + } + else if (base == 16 && ch >= 'a' && ch <= 'f') + { + digit = static_cast(ch - 'a' + 10); + } + else if (base == 16 && ch >= 'A' && ch <= 'F') + { + digit = static_cast(ch - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + US limit = 0; + if (negative) + { + limit = static_cast(std::abs(static_cast((std::numeric_limits::min)()))); + } + else + { + limit = (std::numeric_limits::max)(); + } + + if (base != 0 && result > limit / base) + { + throw_or_mimic(text); + } + if (result * base > limit - digit) + { + throw_or_mimic(text); + } + + result = static_cast(result * base + digit); + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } +} + +template +void stringstream_parser(const std::string& text, T& value) +{ + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } +} + +template ::value>::type* = nullptr + > +void parse_value(const std::string& text, T& value) +{ + integer_parser(text, value); +} + +inline +void +parse_value(const std::string& text, bool& value) +{ + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } + + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } + + throw_or_mimic(text); +} + +inline +void +parse_value(const std::string& text, std::string& value) +{ + value = text; +} + +// The fallback parser. It uses the stringstream parser to parse all types +// that have not been overloaded explicitly. It has to be placed in the +// source code before all other more specialized templates. +template ::value>::type* = nullptr + > +void +parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); +} + +#ifdef CXXOPTS_HAS_OPTIONAL +template +void +parse_value(const std::string& text, std::optional& value) +{ + T result; + parse_value(text, result); + value = std::move(result); +} +#endif + +#ifdef CXXOPTS_HAS_FILESYSTEM +inline +void +parse_value(const std::string& text, std::filesystem::path& value) +{ + value.assign(text); +} +#endif + +inline +void parse_value(const std::string& text, char& c) +{ + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; +} + +template void add_value(const std::string& text, std::vector& value); + +template +void +parse_value(const std::string& text, std::vector& value) +{ + if (text.empty()) { + return; + } + std::stringstream in(text); + std::string token; + while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + add_value(token, value); + } +} + +template +void +add_value(const std::string& text, T& value) +{ + parse_value(text, value); +} + +template +void +add_value(const std::string& text, std::vector& value) +{ + T v; + add_value(text, v); + value.emplace_back(std::move(v)); +} + +template +struct type_is_container +{ + static constexpr bool value = false; +}; + +template +struct type_is_container> +{ + static constexpr bool value = true; +}; + +template +class abstract_value : public Value +{ + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + add(const std::string& text) const override + { + add_value(text, *m_store); + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + return *m_store; + } + + protected: + std::shared_ptr m_result{}; + T* m_store{}; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value{}; + std::string m_implicit_value{}; +}; + +template +class standard_value : public abstract_value +{ + public: + using abstract_value::abstract_value; + + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } +}; + +template <> +class standard_value : public abstract_value +{ + public: + standard_value() + { + set_default_and_implicit(); + } + + explicit standard_value(bool* b) + : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } + + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } +}; + +} // namespace values + +template +std::shared_ptr +value() +{ + return std::make_shared>(); +} + +template +std::shared_ptr +value(T& t) +{ + return std::make_shared>(&t); +} + +class OptionAdder; + +CXXOPTS_NODISCARD +inline +const std::string& +first_or_empty(const OptionNames& long_names) +{ + static const std::string empty{""}; + return long_names.empty() ? empty : long_names.front(); +} + +class OptionDetails +{ + public: + OptionDetails + ( + std::string short_, + OptionNames long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(first_long_name() + m_short); + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } + + OptionDetails(OptionDetails&& rhs) = default; + + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } + + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } + + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } + + CXXOPTS_NODISCARD + const std::string& + first_long_name() const + { + return first_or_empty(m_long); + } + + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long.front(); + } + + CXXOPTS_NODISCARD + const OptionNames & + long_names() const + { + return m_long; + } + + std::size_t + hash() const + { + return m_hash; + } + + private: + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; + + std::size_t m_hash{}; +}; + +struct HelpOptionDetails +{ + std::string s; + OptionNames l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; +}; + +struct HelpGroupDetails +{ + std::string name{}; + std::string description{}; + std::vector options{}; +}; + +class OptionValue +{ + public: + void + add + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->add(text); + m_long_names = &details->long_names(); + } + + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_names = &details->long_names(); + } + + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_names = &details->long_names(); + m_value->parse(); + } + + void + parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif + + CXXOPTS_NODISCARD + std::size_t + count() const noexcept + { + return m_count; + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_POP +#endif + + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } + + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional() const + { + if (m_value == nullptr) { + return std::nullopt; + } + return as(); + } +#endif + + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + + const OptionNames * m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; +}; + +class KeyValue +{ + public: + KeyValue(std::string key_, std::string value_) noexcept + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; + } + + CXXOPTS_NODISCARD + const std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; +}; + +using ParsedHashMap = std::unordered_map; +using NameHashMap = std::unordered_map; + +class ParseResult +{ + public: + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + +// GCC complains about m_iter not being initialised in the member +// initializer list +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult *pr, bool end=false) + : m_pr(pr) + { + if (end) + { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } + else + { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } + } +CXXOPTS_DIAGNOSTIC_POP + + Iterator& operator++() + { + ++m_iter; + if(m_sequential && m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + const KeyValue& operator*() + { + return *m_iter; + } + + const KeyValue* operator->() + { + return m_iter.operator->(); + } + + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) + { + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator + begin() const + { + return Iterator(this); + } + + Iterator + end() const + { + return Iterator(this, true); + } + + std::size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) + { + return 0; + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + return 0; + } + + return viter->second.count(); + } + + bool + contains(const std::string& o) const + { + return static_cast(count(o)); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) + { + throw_or_mimic(option); + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + throw_or_mimic(option); + } + + return viter->second; + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional(const std::string& option) const + { + auto iter = m_keys.find(option); + if (iter != m_keys.end()) + { + auto viter = m_values.find(iter->second); + if (viter != m_values.end()) + { + return viter->second.as_optional(); + } + } + return std::nullopt; + } +#endif + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for(const auto& kv: m_sequential) + { + result += kv.key() + " = " + kv.value() + "\n"; + } + for(const auto& kv: m_defaults) + { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; + } + return result; + } + + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; +}; + +struct Option +{ + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; +}; + +using OptionMap = std::unordered_map>; +using PositionalList = std::vector; +using PositionalListIterator = PositionalList::const_iterator; + +class OptionParser +{ + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(const std::shared_ptr& value, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: + + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; +}; + +class Options +{ + public: + + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + Options& + set_width(std::size_t width) + { + m_width = width; + return *this; + } + + Options& + set_tab_expansion(bool expansion=true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult + parse(int argc, const char* const* argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list