[PATCH v6 0/3] MR10603: win32u/freetype: Fix tmExternalLeading for CFF fonts.
This fixes font scaling issues with CFF fonts this was discovered from games utilizing DxLib. https://github.com/yumetodo/DxLib/tree/master https://dxlib.xsrv.jp/index.html I've ran tests on Windows and to confirm that this change is consistent with a number of fonts. Before: {width="801" height="600"} After (this matches the windows visual from testing in a VM): {width="767" height="572"} Related Proton Issue: https://github.com/ValveSoftware/Proton/issues/7299 Full Disclosure: An LLM was used to assist in finding this bug, the original fix written by the LLM wrote a loader binary that changed the pointers for `GetTextMetricsA` and `GetTextMetricsW` to point to a custom implementation that always set `tmExternalLeading`. The LLM determined that `GDI returns 0 when (winAscent + winDescent) > (Ascender - Descender)`, after testing on WIndows I found this to be incorrect, the actual difference is that CFF fonts always set `tmExternalLeading` to 0. ~~The test case for this fix was LLM generated but reviewed and tested on both Windows and Linux by me.~~ Test case: ``` WINEPREFIX=/tmp/wine-font winetricks allfonts WINEPREFIX=/tmp/wine-font ./loader/wine ./dlls/gdi32/tests/i386-windows/gdi32_test.exe ``` Patched: ``` font.c:4266: test_CFF_external_leading: tested 51 CFF fonts (0 failures) ``` Unpatched Wine: ``` font.c:4266: test_CFF_external_leading: tested 51 CFF fonts (51 failures) ``` Windows 11: ``` font.c:4266: test_CFF_external_leading: tested 48 CFF fonts (0 failures) ``` -- v6: gdi32: Add test for tmExternalLeading for CFF fonts. tools: Suport generation of otf fonts. win32u/freetype: Fix tmExternalLeading for CFF fonts. https://gitlab.winehq.org/wine/wine/-/merge_requests/10603
From: Arie Miller <renari@arimil.com> --- dlls/win32u/freetype.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dlls/win32u/freetype.c b/dlls/win32u/freetype.c index a660b166a0f..42f62dc191a 100644 --- a/dlls/win32u/freetype.c +++ b/dlls/win32u/freetype.c @@ -3392,12 +3392,16 @@ static BOOL freetype_set_outline_text_metrics( struct gdi_font *font ) TM.tmHeight = TM.tmAscent + TM.tmDescent; - /* MSDN says: - el = MAX(0, LineGap - ((WinAscent + WinDescent) - (Ascender - Descender))) - */ - TM.tmExternalLeading = max(0, SCALE_Y(pHori->Line_Gap - - ((ascent + descent) - - (pHori->Ascender - pHori->Descender)))); + /* MSDN documents the formula: + * el = MAX(0, LineGap - ((WinAscent + WinDescent) - (Ascender - Descender))) + * but Windows GDI does not follow this for OpenType-CFF fonts: for any font + * containing a 'CFF ' table it reports tmExternalLeading = 0. + */ + if (font->ntmFlags & NTM_PS_OPENTYPE) + TM.tmExternalLeading = 0; + else + TM.tmExternalLeading = max(0, + SCALE_Y(pHori->Line_Gap - ((ascent + descent) - (pHori->Ascender - pHori->Descender)))); TM.tmAveCharWidth = SCALE_X(pOS2->xAvgCharWidth); if (TM.tmAveCharWidth == 0) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10603
From: Arie Miller <renari@arimil.com> --- fonts/{genttf.ff => genfont.ff} | 2 +- tools/makedep.c | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) rename fonts/{genttf.ff => genfont.ff} (71%) diff --git a/fonts/genttf.ff b/fonts/genfont.ff similarity index 71% rename from fonts/genttf.ff rename to fonts/genfont.ff index 30d1a938ee8..9dc16c4e723 100644 --- a/fonts/genttf.ff +++ b/fonts/genfont.ff @@ -3,4 +3,4 @@ flags = 0 if ($1 == "symbol.sfd") flags = 0x2000 endif -Generate($2, "ttf", flags) +Generate($2, $3, flags) diff --git a/tools/makedep.c b/tools/makedep.c index 1782f3ed79d..cebc8b65bba 100644 --- a/tools/makedep.c +++ b/tools/makedep.c @@ -95,6 +95,7 @@ struct incl_file #define FLAG_RC_PO 0x00100000 /* rc file contains translations */ #define FLAG_RC_HEADER 0x00200000 /* rc file is a header */ #define FLAG_SFD_FONTS 0x00400000 /* sfd file generated bitmap fonts */ +#define FLAG_SFD_OTF 0x00800000 /* sfd file generated otf font */ #define FLAG_C_IMPLIB 0x01000000 /* file is part of an import library */ #define FLAG_C_UNIX 0x02000000 /* file is part of a Unix library */ #define FLAG_C_CXX 0x04000000 /* file uses C++ */ @@ -1022,6 +1023,10 @@ static void parse_pragma_directive( struct file *source, char *str ) strarray_add( array, xstrdup( strtok( NULL, "" ))); return; } + else if (!strcmp( flag, "otf" )) + { + source->flags |= FLAG_SFD_OTF; + } } else { @@ -1549,6 +1554,7 @@ static struct file *open_include_file( const struct makefile *make, struct incl_ if ((file = open_local_generated_file( make, source, ".tab.h", ".y" ))) return file; if ((file = open_local_generated_file( make, source, ".h", ".idl" ))) return file; if (fontforge && (file = open_local_generated_file( make, source, ".ttf", ".sfd" ))) return file; + if (fontforge && (file = open_local_generated_file( make, source, ".otf", ".sfd" ))) return file; if (convert && rsvg && icotool) { if ((file = open_local_maintainer_file( make, source, ".bmp", ".svg" ))) return file; @@ -3282,21 +3288,23 @@ static void output_source_x( struct makefile *make, struct incl_file *source, co */ static void output_source_sfd( struct makefile *make, struct incl_file *source, const char *obj ) { - char *ttf_obj = strmake( "%s.ttf", obj ); - char *ttf_file = src_dir_path( make, ttf_obj ); + int is_otf = source->file->flags & FLAG_SFD_OTF; + const char *ext = is_otf ? "otf" : "ttf"; + char *font_obj = strmake( "%s.%s", obj, ext ); + char *font_file = src_dir_path( make, font_obj ); if (fontforge && !make->src_dir) { - output( "%s: %s\n", ttf_file, source->filename ); - output( "\t%s%s -script %s %s $@\n", cmd_prefix( "GEN" ), - fontforge, root_src_dir_path( "fonts/genttf.ff" ), source->filename ); - if (!(source->file->flags & FLAG_SFD_FONTS)) strarray_add( &make->font_files, ttf_obj ); - strarray_add( &make->maintainerclean_files, ttf_obj ); + output( "%s: %s\n", font_file, source->filename ); + output( "\t%s%s -script %s %s $@ %s\n", cmd_prefix( "GEN" ), + fontforge, root_src_dir_path( "fonts/genfont.ff" ), source->filename, ext ); + if (!(source->file->flags & FLAG_SFD_FONTS)) strarray_add( &make->font_files, font_obj ); + strarray_add( &make->maintainerclean_files, font_obj ); } if (source->file->flags & FLAG_INSTALL) { - install_data_file_src( make, source->name, ttf_obj, "$(datadir)/wine/fonts" ); - output_srcdir_symlink( make, ttf_obj ); + install_data_file_src( make, source->name, font_obj, "$(datadir)/wine/fonts" ); + output_srcdir_symlink( make, font_obj ); } if (source->file->flags & FLAG_SFD_FONTS) @@ -3308,8 +3316,8 @@ static void output_source_sfd( struct makefile *make, struct incl_file *source, char *args = strtok( NULL, "" ); strarray_add( &make->all_targets[0], xstrdup( font )); - output( "%s: %s %s\n", obj_dir_path( make, font ), sfnt2fon, ttf_file ); - output( "\t%s%s -q -o $@ %s %s\n", cmd_prefix( "GEN" ), sfnt2fon, ttf_file, args ); + output( "%s: %s %s\n", obj_dir_path( make, font ), sfnt2fon, font_file ); + output( "\t%s%s -q -o $@ %s %s\n", cmd_prefix( "GEN" ), sfnt2fon, font_file, args ); install_data_file( make, source->name, font, "$(datadir)/wine/fonts", NULL ); } } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10603
From: Arie Miller <renari@arimil.com> --- dlls/gdi32/tests/Makefile.in | 1 + dlls/gdi32/tests/cff.otf | Bin 0 -> 1544 bytes dlls/gdi32/tests/cff.sfd | 52 ++++++++++++++++++++++++++ dlls/gdi32/tests/font.c | 70 ++++++++++++++++++++++++++++------- dlls/gdi32/tests/resource.rc | 3 ++ 5 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 dlls/gdi32/tests/cff.otf create mode 100644 dlls/gdi32/tests/cff.sfd diff --git a/dlls/gdi32/tests/Makefile.in b/dlls/gdi32/tests/Makefile.in index 3ea3cdce957..c7b52d0229e 100644 --- a/dlls/gdi32/tests/Makefile.in +++ b/dlls/gdi32/tests/Makefile.in @@ -4,6 +4,7 @@ IMPORTS = setupapi user32 gdi32 advapi32 SOURCES = \ bitmap.c \ brush.c \ + cff.sfd \ clipping.c \ dc.c \ dib.c \ diff --git a/dlls/gdi32/tests/cff.otf b/dlls/gdi32/tests/cff.otf new file mode 100644 index 0000000000000000000000000000000000000000..608c0a8e8d4ca99d3c9a4832d6d487115c15afca GIT binary patch literal 1544 zcmeYd3Grv(W@unwW-xGeb5mFuImv>7fmMWofnmFwTZr$*nX~pYFtEO0U|^7OcX4%N z5M>ZyU|>B1;`;~d8--TJBrq^ADljlGBqZl17BKKKO=MtT-oe1YXqAzgn4+zB{~-ed z!yX0(2D6Ne)I{c*{}v1k3||-+7*sNHODY&aECx0f1_lO}+{B6k2F3se1_nk61_lPU zyu{qp^~rxHGB7YUFfcHg7vvY0{NKS~#K6D;(xt$_2vW+xV6lA{PdvZPR|alokUJTk zarjFyFff4VM<5!CIT)CrEJg+fW`+M23{fEd|NjtEKx||jzyLO%h4BmH1O`?H2Bt11 zkS}^5G~)vXLq;YB1||kpW@KFmc0ihI3PS_Pbc9`KHbH#A$FL4;6G*EZgB_I3$iU4| z3S~1furo}AvY8pw8D2u!EDYR?nou??13zOZl+DJ#%~;F8%)rRP$-uy1!@vM#Gct%V z#6sCj415fYP&PAzAHz&2n}tD)Q4q>zWsqYug|gWg#28Z<oEh>N3K%LGiWo8((it)s zN*FX4k{L7^6c~&c3>b_U%or3H!Wl9d@)%MX6c_>+iWu@4vKUesk{L=E7@YG9DvL7H zGfFg)H5H5ujLa0mGxJgv0*dmpQj<#<;2J|1QW=UFN*J6O+!)*#7(j|bQj1HR-P{-$ zf*4X6(iut_au^aBiWnGzQqxOw5{noZ+!*p1@)%0MbP+>3*m@%dJq7~?1qLf5N1>Vx zkuzj4XV7IZVbFzpg2636uf#3CC_PodNY6mQ3hYp*8x*V*49#^-bU_{<&dp&AsSHI7 z#SEEXH!3g~Fc>fxg53sUF@&WS6=&w>DHs?S>KPaq5N9ShPC@a?z#s#SbrVobf$}8- z3j-+sFhSBgC<Xri4^hL&21(SQq&9*vG{^qm3Mxx5!-0{3g@u8Uk&y>E<uNibFfwTT z;Ai+D&G<ux>4z-y4+)m;?(S|@8HQ$l83qOc8HN@?83ukCh802#%pleB3|b7P_!B*n z4M<FolNcEp6}tESaNWzeckl1Gz07;XRDZ1e$hP-Sx#$nq-*JCj+4*D}7#J8tz@%t5 j4;Lb8c^E_(KqV@uyn~jJu#)F#$L~WR<B`iqP#FvW+l1UG literal 0 HcmV?d00001 diff --git a/dlls/gdi32/tests/cff.sfd b/dlls/gdi32/tests/cff.sfd new file mode 100644 index 00000000000..2176d622a36 --- /dev/null +++ b/dlls/gdi32/tests/cff.sfd @@ -0,0 +1,52 @@ +SplineFontDB: 3.0 +FontName: WineTestCFF +FullName: WineTestCFF +FamilyName: WineTestCFF +Weight: Regular +Copyright: Copyright(c) 2026 Wine Project +UComments: "#pragma makedep otf" +Version: 001.000 +ItalicAngle: 0 +UnderlinePosition: -100 +UnderlineWidth: 50 +Ascent: 800 +Descent: 200 +InvalidEm: 0 +LayerCount: 2 +Layer: 0 0 "Back" 1 +Layer: 1 0 "Fore" 0 +XUID: [1021 820 1266790224 8728248] +OS2Version: 0 +OS2_WeightWidthSlopeOnly: 0 +OS2_UseTypoMetrics: 1 +CreationTime: 1776459418 +ModificationTime: 1776459618 +OS2TypoAscent: 0 +OS2TypoAOffset: 1 +OS2TypoDescent: 0 +OS2TypoDOffset: 1 +OS2TypoLinegap: 0 +OS2WinAscent: 0 +OS2WinAOffset: 1 +OS2WinDescent: 0 +OS2WinDOffset: 1 +HheadAscent: 0 +HheadAOffset: 1 +HheadDescent: 0 +HheadDOffset: 1 +OS2Vendor: 'PfEd' +MarkAttachClasses: 1 +DEI: 91125 +Encoding: ISO8859-1 +UnicodeInterp: none +NameList: AGL For New Fonts +DisplaySize: -48 +AntiAlias: 1 +FitToEm: 0 +WinInfo: 0 28 11 +OnlyBitmaps: 1 +BeginPrivate: 0 +EndPrivate +BeginChars: 256 0 +EndChars +EndSplineFont diff --git a/dlls/gdi32/tests/font.c b/dlls/gdi32/tests/font.c index c56180b1ce6..df6b2518ed5 100644 --- a/dlls/gdi32/tests/font.c +++ b/dlls/gdi32/tests/font.c @@ -154,7 +154,7 @@ static BOOL write_tmp_file( const void *data, DWORD *size, char *tmp_name ) return ret; } -static BOOL write_ttf_file(const char *fontname, char *tmp_name) +static BOOL write_font_file(const char *fontname, char *tmp_name) { void *rsrc_data; DWORD rsrc_size; @@ -1671,7 +1671,7 @@ static void test_GetGlyphIndices(void) ok(glyphs[4] == 0, "GetGlyphIndicesW should have returned 0 not %04x\n", glyphs[4]); DeleteObject(SelectObject(hdc, hOldFont)); - ret = write_ttf_file("wine_nul.ttf", ttf_name); + ret = write_font_file("wine_nul.ttf", ttf_name); ok(ret, "Failed to create test font file.\n"); font = load_font(ttf_name, &font_size); ok(font != NULL, "Failed to map font file.\n"); @@ -4132,6 +4132,49 @@ static void test_GetTextMetrics(void) ReleaseDC(0, hdc); } +static void test_CFF_external_leading(void) +{ + char font_file[MAX_PATH]; + DWORD num, ret; + HDC hdc; + LOGFONTW lf; + HFONT hfont, hfont_prev; + TEXTMETRICW tm; + + if (!write_font_file("cff.otf", font_file)) + { + skip("Failed to create otf file for testing\n"); + return; + } + + num = AddFontResourceExA(font_file, FR_PRIVATE, 0); + ok(num == 1, "AddFontResourceExA should add 1 font from cff.otf, got %ld\n", num); + if (num == 0) goto cleanup; + + memset(&lf, 0, sizeof(lf)); + lf.lfHeight = -18; + wcscpy(lf.lfFaceName, L"WineTestCFF"); + hfont = CreateFontIndirectW(&lf); + ok(hfont != NULL, "CreateFontIndirectW failed\n"); + + hdc = GetDC(NULL); + hfont_prev = SelectObject(hdc, hfont); + ret = GetFontData(hdc, MS_MAKE_TAG('C','F','F',' '), 0, NULL, 0); + ok(ret != GDI_ERROR, "Expected CFF table, got GDI_ERROR\n"); + ok(GetTextMetricsW(hdc, &tm), "GetTextMetricsW failed\n"); + ok(tm.tmExternalLeading == 0, "Expected tmExternalLeading 0, got %ld\n", tm.tmExternalLeading); + + SelectObject(hdc, hfont_prev); + DeleteObject(hfont); + ReleaseDC(NULL, hdc); + ret = RemoveFontResourceExA(font_file, FR_PRIVATE, 0); + ok(ret, "RemoveFontResourceExA error %ld\n", GetLastError()); + + cleanup: + ret = DeleteFileA(font_file); + ok(ret, "Failed to delete font file, %ld.\n", GetLastError()); +} + static void test_nonexistent_font(void) { static const struct @@ -5239,7 +5282,7 @@ static void test_AddFontMemResource(void) GetLastError()); /* Now with scalable font */ - bRet = write_ttf_file("wine_test.ttf", ttf_name); + bRet = write_font_file("wine_test.ttf", ttf_name); ok(bRet, "Failed to create test font file.\n"); font = load_font(ttf_name, &font_size); @@ -5967,7 +6010,7 @@ static void test_CreateScalableFontResource(void) ULONG (WINAPI *pNtGdiMakeFontDir)( DWORD embed, BYTE *buffer, UINT size, const WCHAR *path, UINT len ); int i; - if (!write_ttf_file("wine_test.ttf", ttf_name)) + if (!write_font_file("wine_test.ttf", ttf_name)) { skip("Failed to create ttf file for testing\n"); return; @@ -6290,7 +6333,7 @@ static void test_vertical_font(void) "@MS UI Gothic", /* has vmtx table, available on native */ }; - if (!write_ttf_file("vertical.ttf", ttf_name)) + if (!write_font_file("vertical.ttf", ttf_name)) { skip("Failed to create ttf file for testing\n"); return; @@ -6329,7 +6372,7 @@ static void test_vertical_font(void) DeleteFileA(ttf_name); - if (!write_ttf_file("vertical2.ttf", ttf_name)) + if (!write_font_file("vertical2.ttf", ttf_name)) { skip("Failed to create ttf file for testing\n"); return; @@ -7068,7 +7111,7 @@ static void test_long_names(void) int ret; HDC dc; - if (!write_ttf_file("wine_longname.ttf", ttf_name)) + if (!write_font_file("wine_longname.ttf", ttf_name)) { skip("Failed to create ttf file for testing\n"); return; @@ -7114,13 +7157,13 @@ static void test_ttf_names(void) int ret; HDC dc; - if (!write_ttf_file("wine_ttfnames.ttf", ttf_name)) + if (!write_font_file("wine_ttfnames.ttf", ttf_name)) { skip("Failed to create ttf file for testing\n"); return; } - if (!write_ttf_file("wine_ttfnames_bold.ttf", ttf_name_bold)) + if (!write_font_file("wine_ttfnames_bold.ttf", ttf_name_bold)) { skip("Failed to create ttf file for testing\n"); DeleteFileA(ttf_name); @@ -7194,20 +7237,20 @@ static void test_lang_names(void) return; } - if (!write_ttf_file( "wine_langnames.ttf", ttf_name )) + if (!write_font_file( "wine_langnames.ttf", ttf_name )) { skip( "Failed to create ttf file for testing\n" ); return; } - if (!write_ttf_file( "wine_langnames2.ttf", ttf_name2 )) + if (!write_font_file( "wine_langnames2.ttf", ttf_name2 )) { skip( "Failed to create ttf file for testing\n" ); DeleteFileA( ttf_name ); return; } - if (!write_ttf_file( "wine_langnames3.ttf", ttf_name3 )) + if (!write_font_file( "wine_langnames3.ttf", ttf_name3 )) { skip( "Failed to create ttf file for testing\n" ); DeleteFileA( ttf_name2 ); @@ -7848,7 +7891,7 @@ static void test_font_weight(void) BOOL bret; HDC hdc; - bret = write_ttf_file("wine_heavy.ttf", ttf_name); + bret = write_font_file("wine_heavy.ttf", ttf_name); ok(bret, "Failed to create test font file.\n"); count = AddFontResourceExA(ttf_name, 0, NULL); @@ -8067,6 +8110,7 @@ START_TEST(font) skip("Arial Black or Symbol/Wingdings is not installed\n"); test_EnumFontFamiliesEx_default_charset(); test_GetTextMetrics(); + test_CFF_external_leading(); test_RealizationInfo(); test_GetTextFace(); test_GetGlyphOutline(); diff --git a/dlls/gdi32/tests/resource.rc b/dlls/gdi32/tests/resource.rc index 2f1273d1e42..340e281a145 100644 --- a/dlls/gdi32/tests/resource.rc +++ b/dlls/gdi32/tests/resource.rc @@ -55,3 +55,6 @@ wine_nul.ttf RCDATA wine_nul.ttf /* @makedep: wine_heavy.ttf */ wine_heavy.ttf RCDATA wine_heavy.ttf + +/* @makedep: cff.otf */ +cff.otf RCDATA cff.otf -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10603
participants (2)
-
Arie Miller -
Arie Miller (@Arimil)