diff --git a/FTL.TXT b/FTL.TXT new file mode 100644 index 00000000..b9e8cbd6 --- /dev/null +++ b/FTL.TXT @@ -0,0 +1,169 @@ + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (https://freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://freetype.org + + +--- end of FTL.TXT --- diff --git a/README.md b/README.md index 090102e0..e3ee0a0e 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,10 @@ SixLabors.Fonts [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/Fonts/build-and-test.yml?branch=main)](https://github.com/SixLabors/Fonts/actions) [![codecov](https://codecov.io/gh/SixLabors/Fonts/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/Fonts) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/Fonts/blob/main/LICENSE) -[![GitHub issues](https://img.shields.io/github/issues/SixLabors/Fonts.svg)](https://github.com/SixLabors/Fonts/issues) -[![GitHub stars](https://img.shields.io/github/stars/SixLabors/Fonts.svg)](https://github.com/SixLabors/Fonts/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/SixLabors/Fonts.svg)](https://github.com/SixLabors/Fonts/network) -**SixLabors.Fonts** is a new cross-platform font loading and drawing library. +**SixLabors.Fonts** is a cross-platform library for loading, measuring, and laying out fonts and text. It supports TrueType and OpenType fonts (including CFF1 and CFF2 outlines), WOFF/WOFF2 web fonts, variable fonts, color fonts (COLR v0/v1 and SVG), and TrueType hinting. The library provides a full OpenType layout engine with GSUB/GPOS support, advanced text shaping for complex scripts, and bidirectional text rendering. ## License @@ -98,15 +95,17 @@ git lfs pull ### Features - Reading font description (name, family, subname etc plus other string metadata). -- Loading OpenType fonts with with CFF1 and True Type outlines. -- Loading True Type fonts. -- Loading [WOFF fonts](https://www.w3.org/Submission/WOFF/). -- Loading [WOFF2 fonts](https://www.w3.org/TR/WOFF2). -- Load all compatible fonts from local machine store. -- Support for line breaking based on [UAX 14](https://www.unicode.org/reports/tr14/) -- Support for rendering left to right, right to left and bidirectional text. -- Support for ligatures. -- Support for advanced OpenType features glyph substitution ([GSUB](https://docs.microsoft.com/en-us/typography/opentype/spec/gsub)) and glyph positioning ([GPOS](https://docs.microsoft.com/en-us/typography/opentype/spec/gpos)) +- Loading TrueType fonts and OpenType fonts with CFF1 and CFF2 outlines. +- Loading [WOFF](https://www.w3.org/Submission/WOFF/) and [WOFF2](https://www.w3.org/TR/WOFF2) web fonts. +- Loading all compatible fonts from the local machine store. +- Support for [variable fonts](https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview) (fvar, gvar, cvar, CFF2 variations). +- Support for color fonts ([COLR](https://learn.microsoft.com/en-us/typography/opentype/spec/colr) v0/v1 and [SVG](https://learn.microsoft.com/en-us/typography/opentype/spec/svg)). +- TrueType hinting (FreeType v40-compatible interpreter). +- Support for advanced OpenType layout via glyph substitution ([GSUB](https://learn.microsoft.com/en-us/typography/opentype/spec/gsub)) and glyph positioning ([GPOS](https://learn.microsoft.com/en-us/typography/opentype/spec/gpos)). +- Advanced text shaping for complex scripts (Indic, Myanmar, Universal Shaping Engine). +- Support for ligatures and kerning. +- Support for rendering left to right, right to left, and bidirectional text. +- Support for line breaking based on [UAX 14](https://www.unicode.org/reports/tr14/). ## API Examples diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES new file mode 100644 index 00000000..deff2ca9 --- /dev/null +++ b/THIRD-PARTY-NOTICES @@ -0,0 +1,62 @@ +SixLabors.Fonts uses third-party libraries or other resources that may be +distributed under licenses different from the SixLabors.Fonts software. + +The attached notices are provided for informational purposes only. Please +review the original license files for complete terms. + +================================================================================ + +FreeType (https://freetype.org) +License: FreeType License (FTL) + +The TrueType hinting interpreter in this library is based in part on the work +of the FreeType Project. The full license text is available in the accompanying +FTL.TXT file. + +Portions of this software are copyright (c) 1996-2002, 2006 The FreeType +Project (https://freetype.org). All rights reserved. + +================================================================================ + +HarfBuzz (https://harfbuzz.github.io) +License: MIT (Old MIT) + +The text shaping engine in this library is based in part on the work of the +HarfBuzz Project, including the OpenType layout engine (GPOS/GSUB) and the +advanced typographic shaping engines (Default, Indic, Myanmar, Universal). + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +Permission is hereby granted, without written agreement and without license or +royalty fees, to use, copy, modify, and distribute this software and its +documentation for any purpose, provided that the above copyright notice and +the following two paragraphs appear in all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, +INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, +UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +================================================================================ diff --git a/src/SixLabors.Fonts/SixLabors.Fonts.csproj b/src/SixLabors.Fonts/SixLabors.Fonts.csproj index 6b435d17..b9dca3b3 100644 --- a/src/SixLabors.Fonts/SixLabors.Fonts.csproj +++ b/src/SixLabors.Fonts/SixLabors.Fonts.csproj @@ -52,6 +52,8 @@ + + diff --git a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs index 8d039bc4..0806ca17 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs @@ -76,6 +76,9 @@ internal void ApplyTrueTypeHinting(HintingMode hintingMode, GlyphMetrics metrics cvtValues = this.GlyphVariationProcessor.ApplyCvtDeltas(cvtValues) ?? cvtValues; } + // Provide normalized axis coordinates for the GETVARIATION opcode. + interpreter.SetNormalizedAxisCoordinates(this.GlyphVariationProcessor?.NormalizedCoordinates); + interpreter.SetControlValueTable(cvtValues, hintingScaleFactor, pixelSize, prep?.Instructions); Bounds bounds = glyphVector.Bounds; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs index 704b96f5..0c1ba74b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs @@ -75,7 +75,7 @@ public GlyphVariationProcessor( /// Gets the normalized variation coordinates for this processor instance. /// Used by FeatureVariations condition evaluation. /// - internal ReadOnlySpan NormalizedCoordinates => this.normalizedCoords; + internal float[] NormalizedCoordinates => this.normalizedCoords; /// /// Transforms glyph outline points by applying gvar variation deltas. diff --git a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs new file mode 100644 index 00000000..1f6afe70 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs @@ -0,0 +1,476 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.TrueType.Hinting; + +internal partial class TrueTypeInterpreter +{ + /// + /// Gets stack pre-validation table matching FreeType's Pop_Push_Count. + /// Each byte encodes (popCount << 4) | pushCount for the corresponding opcode. + /// Used to validate stack depth before executing each instruction. + /// Opcodes that consume a variable number of arguments (e.g. NPUSHB, SHP, SHPIX, IP, + /// ALIGNRP, FLIPPT, GETVARIATION) are encoded as (0, 0) and handled specially. + /// + private static ReadOnlySpan PopPushCount => + [ + + // 0x00 + /* SVTCA[0] */ 0x00, + /* SVTCA[1] */ 0x00, + /* SPVTCA[0] */ 0x00, + /* SPVTCA[1] */ 0x00, + /* SFVTCA[0] */ 0x00, + /* SFVTCA[1] */ 0x00, + /* SPVTL[0] */ 0x20, + /* SPVTL[1] */ 0x20, + /* SFVTL[0] */ 0x20, + /* SFVTL[1] */ 0x20, + /* SPVFS */ 0x20, + /* SFVFS */ 0x20, + /* GPV */ 0x02, + /* GFV */ 0x02, + /* SFVTPV */ 0x00, + /* ISECT */ 0x50, + + // 0x10 + /* SRP0 */ 0x10, + /* SRP1 */ 0x10, + /* SRP2 */ 0x10, + /* SZP0 */ 0x10, + /* SZP1 */ 0x10, + /* SZP2 */ 0x10, + /* SZPS */ 0x10, + /* SLOOP */ 0x10, + /* RTG */ 0x00, + /* RTHG */ 0x00, + /* SMD */ 0x10, + /* ELSE */ 0x00, + /* JMPR */ 0x10, + /* SCVTCI */ 0x10, + /* SSWCI */ 0x10, + /* SSW */ 0x10, + + // 0x20 + /* DUP */ 0x12, + /* POP */ 0x10, + /* CLEAR */ 0x00, + /* SWAP */ 0x22, + /* DEPTH */ 0x01, + /* CINDEX */ 0x11, + /* MINDEX */ 0x10, + /* ALIGNPTS */ 0x20, + /* INS_$28 */ 0x00, + /* UTP */ 0x10, + /* LOOPCALL */ 0x20, + /* CALL */ 0x10, + /* FDEF */ 0x10, + /* ENDF */ 0x00, + /* MDAP[0] */ 0x10, + /* MDAP[1] */ 0x10, + + // 0x30 + /* IUP[0] */ 0x00, + /* IUP[1] */ 0x00, + /* SHP[0] */ 0x00, // loops + /* SHP[1] */ 0x00, // loops + /* SHC[0] */ 0x10, + /* SHC[1] */ 0x10, + /* SHZ[0] */ 0x10, + /* SHZ[1] */ 0x10, + /* SHPIX */ 0x10, // loops + /* IP */ 0x00, // loops + /* MSIRP[0] */ 0x20, + /* MSIRP[1] */ 0x20, + /* ALIGNRP */ 0x00, // loops + /* RTDG */ 0x00, + /* MIAP[0] */ 0x20, + /* MIAP[1] */ 0x20, + + // 0x40 + /* NPUSHB */ 0x00, + /* NPUSHW */ 0x00, + /* WS */ 0x20, + /* RS */ 0x11, + /* WCVTP */ 0x20, + /* RCVT */ 0x11, + /* GC[0] */ 0x11, + /* GC[1] */ 0x11, + /* SCFS */ 0x20, + /* MD[0] */ 0x21, + /* MD[1] */ 0x21, + /* MPPEM */ 0x01, + /* MPS */ 0x01, + /* FLIPON */ 0x00, + /* FLIPOFF */ 0x00, + /* DEBUG */ 0x10, + + // 0x50 + /* LT */ 0x21, + /* LTEQ */ 0x21, + /* GT */ 0x21, + /* GTEQ */ 0x21, + /* EQ */ 0x21, + /* NEQ */ 0x21, + /* ODD */ 0x11, + /* EVEN */ 0x11, + /* IF */ 0x10, + /* EIF */ 0x00, + /* AND */ 0x21, + /* OR */ 0x21, + /* NOT */ 0x11, + /* DELTAP1 */ 0x10, + /* SDB */ 0x10, + /* SDS */ 0x10, + + // 0x60 + /* ADD */ 0x21, + /* SUB */ 0x21, + /* DIV */ 0x21, + /* MUL */ 0x21, + /* ABS */ 0x11, + /* NEG */ 0x11, + /* FLOOR */ 0x11, + /* CEILING */ 0x11, + /* ROUND[0] */ 0x11, + /* ROUND[1] */ 0x11, + /* ROUND[2] */ 0x11, + /* ROUND[3] */ 0x11, + /* NROUND[0] */ 0x11, + /* NROUND[1] */ 0x11, + /* NROUND[2] */ 0x11, + /* NROUND[3] */ 0x11, + + // 0x70 + /* WCVTF */ 0x20, + /* DELTAP2 */ 0x10, + /* DELTAP3 */ 0x10, + /* DELTAC1 */ 0x10, + /* DELTAC2 */ 0x10, + /* DELTAC3 */ 0x10, + /* SROUND */ 0x10, + /* S45ROUND */ 0x10, + /* JROT */ 0x20, + /* JROF */ 0x20, + /* ROFF */ 0x00, + /* INS_$7B */ 0x00, + /* RUTG */ 0x00, + /* RDTG */ 0x00, + /* SANGW */ 0x10, + /* AA */ 0x10, + + // 0x80 + /* FLIPPT */ 0x00, // loops + /* FLIPRGON */ 0x20, + /* FLIPRGOFF */ 0x20, + /* INS_$83 */ 0x00, + /* INS_$84 */ 0x00, + /* SCANCTRL */ 0x10, + /* SDPVTL[0] */ 0x20, + /* SDPVTL[1] */ 0x20, + /* GETINFO */ 0x11, + /* IDEF */ 0x10, + /* ROLL */ 0x33, + /* MAX */ 0x21, + /* MIN */ 0x21, + /* SCANTYPE */ 0x10, + /* INSTCTRL */ 0x20, + /* INS_$8F */ 0x00, + + // 0x90 + /* INS_$90 */ 0x00, + /* GETVAR */ 0x00, // variable push, handled specially + /* GETDATA */ 0x01, + /* INS_$93 */ 0x00, + /* INS_$94 */ 0x00, + /* INS_$95 */ 0x00, + /* INS_$96 */ 0x00, + /* INS_$97 */ 0x00, + /* INS_$98 */ 0x00, + /* INS_$99 */ 0x00, + /* INS_$9A */ 0x00, + /* INS_$9B */ 0x00, + /* INS_$9C */ 0x00, + /* INS_$9D */ 0x00, + /* INS_$9E */ 0x00, + /* INS_$9F */ 0x00, + + // 0xA0 + /* INS_$A0 */ 0x00, + /* INS_$A1 */ 0x00, + /* INS_$A2 */ 0x00, + /* INS_$A3 */ 0x00, + /* INS_$A4 */ 0x00, + /* INS_$A5 */ 0x00, + /* INS_$A6 */ 0x00, + /* INS_$A7 */ 0x00, + /* INS_$A8 */ 0x00, + /* INS_$A9 */ 0x00, + /* INS_$AA */ 0x00, + /* INS_$AB */ 0x00, + /* INS_$AC */ 0x00, + /* INS_$AD */ 0x00, + /* INS_$AE */ 0x00, + /* INS_$AF */ 0x00, + + // 0xB0 + /* PUSHB[0] */ 0x01, + /* PUSHB[1] */ 0x02, + /* PUSHB[2] */ 0x03, + /* PUSHB[3] */ 0x04, + /* PUSHB[4] */ 0x05, + /* PUSHB[5] */ 0x06, + /* PUSHB[6] */ 0x07, + /* PUSHB[7] */ 0x08, + /* PUSHW[0] */ 0x01, + /* PUSHW[1] */ 0x02, + /* PUSHW[2] */ 0x03, + /* PUSHW[3] */ 0x04, + /* PUSHW[4] */ 0x05, + /* PUSHW[5] */ 0x06, + /* PUSHW[6] */ 0x07, + /* PUSHW[7] */ 0x08, + + // 0xC0 + /* MDRP[00] */ 0x10, + /* MDRP[01] */ 0x10, + /* MDRP[02] */ 0x10, + /* MDRP[03] */ 0x10, + /* MDRP[04] */ 0x10, + /* MDRP[05] */ 0x10, + /* MDRP[06] */ 0x10, + /* MDRP[07] */ 0x10, + /* MDRP[08] */ 0x10, + /* MDRP[09] */ 0x10, + /* MDRP[10] */ 0x10, + /* MDRP[11] */ 0x10, + /* MDRP[12] */ 0x10, + /* MDRP[13] */ 0x10, + /* MDRP[14] */ 0x10, + /* MDRP[15] */ 0x10, + + // 0xD0 + /* MDRP[16] */ 0x10, + /* MDRP[17] */ 0x10, + /* MDRP[18] */ 0x10, + /* MDRP[19] */ 0x10, + /* MDRP[20] */ 0x10, + /* MDRP[21] */ 0x10, + /* MDRP[22] */ 0x10, + /* MDRP[23] */ 0x10, + /* MDRP[24] */ 0x10, + /* MDRP[25] */ 0x10, + /* MDRP[26] */ 0x10, + /* MDRP[27] */ 0x10, + /* MDRP[28] */ 0x10, + /* MDRP[29] */ 0x10, + /* MDRP[30] */ 0x10, + /* MDRP[31] */ 0x10, + + // 0xE0 + /* MIRP[00] */ 0x20, + /* MIRP[01] */ 0x20, + /* MIRP[02] */ 0x20, + /* MIRP[03] */ 0x20, + /* MIRP[04] */ 0x20, + /* MIRP[05] */ 0x20, + /* MIRP[06] */ 0x20, + /* MIRP[07] */ 0x20, + /* MIRP[08] */ 0x20, + /* MIRP[09] */ 0x20, + /* MIRP[10] */ 0x20, + /* MIRP[11] */ 0x20, + /* MIRP[12] */ 0x20, + /* MIRP[13] */ 0x20, + /* MIRP[14] */ 0x20, + /* MIRP[15] */ 0x20, + + // 0xF0 + /* MIRP[16] */ 0x20, + /* MIRP[17] */ 0x20, + /* MIRP[18] */ 0x20, + /* MIRP[19] */ 0x20, + /* MIRP[20] */ 0x20, + /* MIRP[21] */ 0x20, + /* MIRP[22] */ 0x20, + /* MIRP[23] */ 0x20, + /* MIRP[24] */ 0x20, + /* MIRP[25] */ 0x20, + /* MIRP[26] */ 0x20, + /* MIRP[27] */ 0x20, + /* MIRP[28] */ 0x20, + /* MIRP[29] */ 0x20, + /* MIRP[30] */ 0x20, + /* MIRP[31] */ 0x20, + ]; + +#pragma warning disable SA1201 // Elements should appear in the correct order + private enum OpCode : byte +#pragma warning restore SA1201 // Elements should appear in the correct order + { + SVTCA0, + SVTCA1, + SPVTCA0, + SPVTCA1, + SFVTCA0, + SFVTCA1, + SPVTL0, + SPVTL1, + SFVTL0, + SFVTL1, + SPVFS, + SFVFS, + GPV, + GFV, + SFVTPV, + ISECT, + SRP0, + SRP1, + SRP2, + SZP0, + SZP1, + SZP2, + SZPS, + SLOOP, + RTG, + RTHG, + SMD, + ELSE, + JMPR, + SCVTCI, + SSWCI, + SSW, + DUP, + POP, + CLEAR, + SWAP, + DEPTH, + CINDEX, + MINDEX, + ALIGNPTS, + /* unused: 0x28 */ + UTP = 0x29, + LOOPCALL, + CALL, + FDEF, + ENDF, + MDAP0, + MDAP1, + IUP0, + IUP1, + SHP0, + SHP1, + SHC0, + SHC1, + SHZ0, + SHZ1, + SHPIX, + IP, + MSIRP0, + MSIRP1, + ALIGNRP, + RTDG, + MIAP0, + MIAP1, + NPUSHB, + NPUSHW, + WS, + RS, + WCVTP, + RCVT, + GC0, + GC1, + SCFS, + MD0, + MD1, + MPPEM, + MPS, + FLIPON, + FLIPOFF, + DEBUG, + LT, + LTEQ, + GT, + GTEQ, + EQ, + NEQ, + ODD, + EVEN, + IF, + EIF, + AND, + OR, + NOT, + DELTAP1, + SDB, + SDS, + ADD, + SUB, + DIV, + MUL, + ABS, + NEG, + FLOOR, + CEILING, + ROUND0, + ROUND1, + ROUND2, + ROUND3, + NROUND0, + NROUND1, + NROUND2, + NROUND3, + WCVTF, + DELTAP2, + DELTAP3, + DELTAC1, + DELTAC2, + DELTAC3, + SROUND, + S45ROUND, + JROT, + JROF, + ROFF, + /* unused: 0x7B */ + RUTG = 0x7C, + RDTG, + SANGW, + AA, + FLIPPT, + FLIPRGON, + FLIPRGOFF, + /* unused: 0x83 - 0x84 */ + SCANCTRL = 0x85, + SDPVTL0, + SDPVTL1, + GETINFO, + IDEF, + ROLL, + MAX, + MIN, + SCANTYPE, + INSTCTRL, + /* unused: 0x8F - 0x90 */ + GETVARIATION = 0x91, + GETDATA, + /* unused: 0x93 - 0xAF */ + PUSHB1 = 0xB0, + PUSHB2, + PUSHB3, + PUSHB4, + PUSHB5, + PUSHB6, + PUSHB7, + PUSHB8, + PUSHW1, + PUSHW2, + PUSHW3, + PUSHW4, + PUSHW5, + PUSHW6, + PUSHW7, + PUSHW8, + MDRP, // range of 32 values, 0xC0 - 0xDF, + MIRP = 0xE0 // range of 32 values, 0xE0 - 0xFF + } +} diff --git a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs index 91ec160e..a1bd21bf 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs @@ -34,29 +34,63 @@ namespace SixLabors.Fonts.Tables.TrueType.Hinting; /// which v40 intentionally omits. /// /// -internal class TrueTypeInterpreter +internal partial class TrueTypeInterpreter { + // Current and saved graphics state. cvtState is captured after the prep (CVT) program + // runs so that each glyph program begins with a consistent baseline. private GraphicsState state; private GraphicsState cvtState; + private readonly ExecutionStack stack; private readonly InstructionStream[] functions; private readonly InstructionStream[] instructionDefs; + + // Control Value Table: baseControlValueTable holds the scaled values after prep execution; + // controlValueTable is a working copy restored at the start of each glyph program. private float[] baseControlValueTable; private float[] controlValueTable; - private readonly int[] storage; + + // Storage area shared between prep and glyph programs. prepStorage holds the reference + // to the storage array as it was after prep execution. Glyph programs use copy-on-write + // (see WS instruction) so that prep state is preserved across glyphs. + private int[] storage; + private int[]? prepStorage; + private bool inGlyphProgram; + private IReadOnlyList contours; private float scale; private int ppem; private int callStackSize; + + // Dot product of freedom and projection vectors, used to decompose + // scalar distances into movement along the freedom vector. private float fdotp; + + // Super-rounding parameters set by SROUND/S45ROUND. private float roundThreshold; private float roundPhase; private float roundPeriod; + // IUP tracking — once both axes have been interpolated, further IUP calls are skipped + // and v40 backward compatibility blocks Y movement (post-IUP restriction). private bool iupXCalled; private bool iupYCalled; private bool isComposite; + // Normalized variation axis coordinates for variable fonts, used by GETVARIATION/GETINFO. + private float[]? normalizedAxisCoordinates; + + // FreeType TT_RunIns safety counters to prevent pathological fonts + // from hanging the interpreter. Limits are computed per-glyph based on + // point count and CVT size. + private long insCounter; + private long loopcallCounter; + private long negJumpCounter; + private long loopcallCounterMax; + private long negJumpCounterMax; + + // Zone pointers: zp0/zp1/zp2 are the three zone pointer registers (ZP0-ZP2). + // They can reference either the glyph zone (points) or the twilight zone. private Zone zp0; private Zone zp1; private Zone zp2; @@ -65,12 +99,27 @@ internal class TrueTypeInterpreter private static readonly float Sqrt2Over2 = (float)(Math.Sqrt(2) / 2); private const int MaxCallStack = 128; + private const long MaxRunnableOpcodes = 1_000_000; private const float Epsilon = 0.000001F; #if DEBUG private readonly List debugList = []; #endif +#if HINTING_TRACE + private readonly System.Text.StringBuilder traceLog = new(); + private int traceGlyphIndex; +#endif + + /// + /// Initializes a new instance of the class + /// with resource limits sourced from the font's maxp table. + /// + /// Maximum stack depth. + /// Number of storage area locations. + /// Number of function definition slots (FDEF). + /// Number of instruction definition slots (IDEF). When non-zero, a full 256-entry lookup table is allocated. + /// Number of points in the twilight zone. public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int maxInstructionDefs, int maxTwilightPoints) { this.stack = new ExecutionStack(maxStack); @@ -85,9 +134,32 @@ public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int m this.contours = []; } + /// + /// Sets the normalized axis coordinates for variable font hinting. + /// These are used by the GETVARIATION and GETINFO instructions. + /// + /// Normalized axis coordinates in the range [-1, 1], or for non-variable fonts. + public void SetNormalizedAxisCoordinates(float[]? coordinates) + => this.normalizedAxisCoordinates = coordinates; + + /// + /// Executes the font program (fpgm) to populate function definitions (FDEF/IDEF). + /// This must be called once per font before any CVT or glyph programs are executed. + /// + /// The raw font program bytecode. public void InitializeFunctionDefs(byte[] instructions) => this.Execute(new StackInstructionStream(instructions, 0), false, true); + /// + /// Scales the Control Value Table and executes the prep (CVT) program. + /// The prep program typically sets up the graphics state and may modify CVT entries + /// for the current pixel size. The resulting state is saved and restored for each + /// subsequent glyph program execution. + /// + /// The raw CVT entries from the font, or if absent. + /// The scale factor to apply to CVT entries (units-per-em to pixels). + /// The pixels-per-em value at the current size. + /// The raw prep program bytecode, or if absent. public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]? cvProgram) { if (this.scale == scale || cvt == null) @@ -115,8 +187,19 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]? if (cvProgram != null) { + // Initialize safety counters for the prep program (no glyph points yet). + this.insCounter = 0; + this.loopcallCounter = 0; + this.negJumpCounter = 0; + int cvtSize = this.controlValueTable.Length; + this.loopcallCounterMax = 300 + (22 * (long)cvtSize); + this.negJumpCounterMax = this.loopcallCounterMax; + this.Execute(new StackInstructionStream(cvProgram, 0), false, false); + // Save prep program storage state so glyph programs can read it (copy-on-write in WS). + this.prepStorage = this.storage; + // save off the CVT graphics state so that we can restore it for each glyph we hint if ((this.state.InstructionControl & InstructionControlFlags.UseDefaultGraphicsState) != 0) { @@ -189,9 +272,18 @@ public bool TryHintGlyph( this.state = this.cvtState; this.callStackSize = 0; - // FreeType's interpreter treats the storage area and glyph-level CVT modifications as non-persistent. - // Reset storage and restore the baseline CVT state for each glyph. - Array.Clear(this.storage, 0, this.storage.Length); + // FreeType preserves prep program storage via copy-on-write in WS. + // Restore the prep storage pointer; if glyph writes, WS will copy first. + if (this.prepStorage != null) + { + this.storage = this.prepStorage; + } + else + { + Array.Clear(this.storage, 0, this.storage.Length); + } + + this.inGlyphProgram = true; if (this.baseControlValueTable.Length > 0) { @@ -213,12 +305,34 @@ public bool TryHintGlyph( this.debugList.Clear(); #endif +#if HINTING_TRACE + this.traceLog.Clear(); + this.traceLog.AppendLine(System.FormattableString.Invariant($"=== GLYPH {this.traceGlyphIndex++} pts={controlPoints.Length - 4} composite={isComposite} ===")); +#endif + this.stack.Clear(); this.OnVectorsUpdated(); this.iupXCalled = false; this.iupYCalled = false; this.isComposite = isComposite; + // FreeType TT_RunIns — initialize safety counters. + this.insCounter = 0; + this.loopcallCounter = 0; + this.negJumpCounter = 0; + int nPoints = controlPoints.Length; + int cvtSize = this.controlValueTable.Length; + if (nPoints > 0) + { + this.loopcallCounterMax = Math.Max(50, 10 * (long)nPoints) + Math.Max(50, cvtSize / 10); + } + else + { + this.loopcallCounterMax = 300 + (22 * (long)cvtSize); + } + + this.negJumpCounterMax = this.loopcallCounterMax; + // normalize the round state settings switch (this.state.RoundState) { @@ -231,24 +345,21 @@ public bool TryHintGlyph( } this.Execute(new StackInstructionStream(instructions, 0), false, false); + +#if HINTING_TRACE + System.Console.Error.Write(this.traceLog); +#endif + return true; } catch (Exception) { - // The interpreter can fail for malformed instructions; in that case we skip hinting. - Array.Clear(this.points.TouchState, 0, this.points.TouchState.Length); - - // Reset interpreter state so nothing leaks if the caller catches. - this.stack.Clear(); - this.callStackSize = 0; - this.contours = []; - this.zp0 = this.zp1 = this.zp2 = this.points = default; +#if HINTING_TRACE + System.Console.Error.Write(this.traceLog); - this.state = this.cvtState; - this.OnVectorsUpdated(); - this.iupXCalled = false; - this.iupYCalled = false; - this.isComposite = false; + // Rethrow to diagnose hinting failures. + throw; +#endif return false; } } @@ -270,16 +381,63 @@ private void ResetTwilightZone() Array.Clear(this.twilight.TouchState, 0, this.twilight.TouchState.Length); } + /// + /// Core instruction dispatch loop. Reads and executes opcodes from the given + /// instruction stream until the stream is exhausted or an error terminates execution. + /// + /// The instruction stream to execute. + /// + /// when executing inside a CALL/LOOPCALL function body. + /// Controls whether ENDF returns to the caller or exits execution. + /// + /// + /// when executing the font program (fpgm), which permits + /// FDEF and IDEF instructions. Glyph and prep programs set this to . + /// private void Execute(StackInstructionStream stream, bool inFunction, bool allowFunctionDefs) { - // dispatch each instruction in the stream while (!stream.Done) { - OpCode opcode = stream.NextOpCode(); + int rawOpcode = stream.NextByte(); + OpCode opcode = (OpCode)rawOpcode; #if DEBUG this.debugList.Add(opcode); #endif + + // FreeType TT_RunIns — global instruction counter to prevent infinite loops. + if (++this.insCounter > MaxRunnableOpcodes) + { + return; + } + + // FreeType TT_RunIns — pre-validate stack depth before dispatch. + byte popPush = PopPushCount[rawOpcode]; + int pops = popPush >> 4; + int pushes = popPush & 0xF; + +#if HINTING_TRACE + int preStackCount = this.stack.Count; + this.TracePreInstruction(opcode, pops); +#endif + + // Underflow: push zeroes to fill missing args (FreeType non-pedantic mode). + if (this.stack.Count < pops) + { + int missing = pops - this.stack.Count; + this.stack.Clear(); + for (int z = 0; z < pops; z++) + { + this.stack.Push(0); + } + } + + // Overflow: exit the run loop (FreeType non-pedantic: set error and return). + if (this.stack.Count - pops + pushes > this.stack.Capacity) + { + return; + } + switch (opcode) { // ==== PUSH INSTRUCTIONS ==== @@ -323,16 +481,35 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF // ==== STORAGE MANAGEMENT ==== case OpCode.RS: { - int loc = CheckIndex(this.stack.Pop(), this.storage.Length); - this.stack.Push(this.storage[loc]); + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.storage.Length) + { + this.stack.Push(0); + } + else + { + this.stack.Push(this.storage[loc]); + } } break; case OpCode.WS: { int value = this.stack.Pop(); - int loc = CheckIndex(this.stack.Pop(), this.storage.Length); - this.storage[loc] = value; + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.storage.Length) + { + // FreeType copy-on-write: when glyph program first writes to storage, + // make a private copy so prep program state is preserved for other glyphs. + if (this.inGlyphProgram && this.storage == this.prepStorage) + { + int[] glyphStorage = new int[this.storage.Length]; + Array.Copy(this.storage, glyphStorage, this.storage.Length); + this.storage = glyphStorage; + } + + this.storage[loc] = value; + } } break; @@ -341,22 +518,37 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.WCVTP: { float value = this.stack.PopFloat(); - int loc = CheckIndex(this.stack.Pop(), this.controlValueTable.Length); - this.controlValueTable[loc] = value; + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.controlValueTable.Length) + { + this.controlValueTable[loc] = value; + } } break; case OpCode.WCVTF: { int value = this.stack.Pop(); - int loc = CheckIndex(this.stack.Pop(), this.controlValueTable.Length); - this.controlValueTable[loc] = value * this.scale; + int loc = this.stack.Pop(); + if ((uint)loc < (uint)this.controlValueTable.Length) + { + this.controlValueTable[loc] = value * this.scale; + } } break; case OpCode.RCVT: { - this.stack.Push(this.ReadCvt()); + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.controlValueTable.Length) + { + this.stack.Push(0); + } + else + { + this.stack.Push(this.controlValueTable[loc]); + } + break; } @@ -458,25 +650,41 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.SZP0: { - this.zp0 = this.GetZoneFromStack(); + if (this.TryGetZoneFromStack(out Zone szp0Zone)) + { + this.zp0 = szp0Zone; + } + break; } case OpCode.SZP1: { - this.zp1 = this.GetZoneFromStack(); + if (this.TryGetZoneFromStack(out Zone szp1Zone)) + { + this.zp1 = szp1Zone; + } + break; } case OpCode.SZP2: { - this.zp2 = this.GetZoneFromStack(); + if (this.TryGetZoneFromStack(out Zone szp2Zone)) + { + this.zp2 = szp2Zone; + } + break; } case OpCode.SZPS: { - this.zp0 = this.zp1 = this.zp2 = this.GetZoneFromStack(); + if (this.TryGetZoneFromStack(out Zone szpsZone)) + { + this.zp0 = this.zp1 = this.zp2 = szpsZone; + } + break; } @@ -532,16 +740,24 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.INSTCTRL: { + // FreeType Ins_INSTCTRL. + // Always pop both arguments to keep the stack balanced. int selector = this.stack.Pop(); - if (selector is >= 1 and <= 2) + int value = this.stack.Pop(); + + // FreeType restricts selectors 1-2 to the prep (CVT) program only. + // Selector 3 (NativeClearType) can also be set during prep. + // Glyph programs cannot modify instruction control flags. + if (selector is >= 1 and <= 3 && !this.inGlyphProgram) { - // value is false if zero, otherwise shift the right bit into the flags int bit = 1 << (selector - 1); - if (this.stack.Pop() == 0) + + // FreeType validates: if value != 0, it must equal the expected bit. + if (value == 0) { this.state.InstructionControl = (InstructionControlFlags)((int)this.state.InstructionControl & ~bit); } - else + else if (value == bit) { this.state.InstructionControl = (InstructionControlFlags)((int)this.state.InstructionControl | bit); } @@ -559,7 +775,15 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.SLOOP: { - this.state.Loop = this.stack.Pop(); + int loop = this.stack.Pop(); + if (loop < 0) + { + // FreeType sets Bad_Argument error and returns without modifying state. + break; + } + + // FreeType heuristically caps loop count at 16 bits. + this.state.Loop = loop > 0xFFFF ? 0xFFFF : loop; break; } @@ -614,13 +838,27 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF // ==== POINT MEASUREMENT ==== case OpCode.GC0: { - this.stack.Push(this.Project(this.zp2.GetCurrent(this.stack.Pop()))); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.Project(this.zp2.GetCurrent(pointIndex))); break; } case OpCode.GC1: { - this.stack.Push(this.DualProject(this.zp2.GetOriginal(this.stack.Pop()))); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.DualProject(this.zp2.GetOriginal(pointIndex))); break; } @@ -628,6 +866,11 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { float value = this.stack.PopFloat(); int index = this.stack.Pop(); + if ((uint)index >= (uint)this.zp2.Current.Length) + { + break; + } + Vector2 point = this.zp2.GetCurrent(index); this.MovePoint(this.zp2, index, value - this.Project(point)); @@ -641,17 +884,31 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.MD0: { - Vector2 p1 = this.zp1.GetOriginal(this.stack.Pop()); - Vector2 p2 = this.zp0.GetOriginal(this.stack.Pop()); - this.stack.Push(this.DualProject(p2 - p1)); + int i0 = this.stack.Pop(); + int i1 = this.stack.Pop(); + if ((uint)i0 >= (uint)this.zp1.Current.Length || + (uint)i1 >= (uint)this.zp0.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.DualProject(this.zp0.GetOriginal(i1) - this.zp1.GetOriginal(i0))); } break; case OpCode.MD1: { - Vector2 p1 = this.zp1.GetCurrent(this.stack.Pop()); - Vector2 p2 = this.zp0.GetCurrent(this.stack.Pop()); - this.stack.Push(this.Project(p2 - p1)); + int i0 = this.stack.Pop(); + int i1 = this.stack.Pop(); + if ((uint)i0 >= (uint)this.zp1.Current.Length || + (uint)i1 >= (uint)this.zp0.Current.Length) + { + this.stack.Push(0); + break; + } + + this.stack.Push(this.Project(this.zp0.GetCurrent(i1) - this.zp1.GetCurrent(i0))); } break; @@ -671,15 +928,13 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF // ==== POINT MODIFICATION ==== case OpCode.FLIPPT: { - // FLIPRGON, FLIPRGOFF, and FLIPPT don't execute post-IUP. This - // prevents dents in e.g. Arial-Regular's `D' and `G' glyphs at - // various sizes. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 - bool postIUP = this.iupXCalled && this.iupYCalled; + // FreeType: FLIP instructions skip when backward_compatibility == 0x7. + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; for (int i = 0; i < this.state.Loop; i++) { int index = this.stack.Pop(); - if (postIUP) + if (blocked || (uint)index >= (uint)this.points.Current.Length) { continue; } @@ -693,19 +948,19 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.FLIPRGON: { - // FLIPRGON, FLIPRGOFF, and FLIPPT don't execute post-IUP. This - // prevents dents in e.g. Arial-Regular's `D' and `G' glyphs at - // various sizes. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 - bool postIUP = this.iupXCalled && this.iupYCalled; + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; int end = this.stack.Pop(); - for (int i = this.stack.Pop(); i <= end; i++) + int start = this.stack.Pop(); + if (blocked || + (uint)end >= (uint)this.points.Current.Length || + (uint)start >= (uint)this.points.Current.Length) { - if (postIUP) - { - continue; - } + break; + } + for (int i = start; i <= end; i++) + { this.points.Current[i].OnCurve = true; } } @@ -713,19 +968,19 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.FLIPRGOFF: { - // FLIPRGON, FLIPRGOFF, and FLIPPT don't execute post-IUP. This - // prevents dents in e.g. Arial-Regular's `D' and `G' glyphs at - // various sizes. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 - bool postIUP = this.iupXCalled && this.iupYCalled; + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool blocked = !nativeClearType && this.iupXCalled && this.iupYCalled; int end = this.stack.Pop(); - for (int i = this.stack.Pop(); i <= end; i++) + int start = this.stack.Pop(); + if (blocked || + (uint)end >= (uint)this.points.Current.Length || + (uint)start >= (uint)this.points.Current.Length) { - if (postIUP) - { - continue; - } + break; + } + for (int i = start; i <= end; i++) + { this.points.Current[i].OnCurve = false; } } @@ -734,23 +989,90 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.SHP0: case OpCode.SHP1: { - Vector2 displacement = this.ComputeDisplacement((int)opcode, out Zone zone, out int point); - this.ShiftPoints(displacement); + // FreeType Ins_SHP: uses Move_Zp2_Point for each point. + if (!this.TryComputeDisplacement((int)opcode, out _, out _, out Vector2 displacement)) + { + // FreeType: Compute_Point_Displacement failure returns (no Fail label, loop NOT reset). + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex < (uint)this.zp2.Current.Length) + { + this.MoveZp2Point(this.zp2, pointIndex, displacement.X, displacement.Y, true); + } + } + + this.state.Loop = 1; } break; case OpCode.SHPIX: { - this.ShiftPoints(this.stack.PopFloat() * this.state.Freedom); + // FreeType Ins_SHPIX: v40 backward compatibility gating. + float magnitude = this.stack.PopFloat(); + float dx = magnitude * this.state.Freedom.X; + float dy = magnitude * this.state.Freedom.Y; + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool postIUP = this.iupXCalled && this.iupYCalled; + bool inTwilight = this.zp0.IsTwilight || this.zp1.IsTwilight || this.zp2.IsTwilight; + + for (int i = 0; i < this.state.Loop; i++) + { + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + continue; + } + + if (!nativeClearType) + { + // Backward compat mode: gated Y-only movement. + // Twilight zone always allowed; otherwise need composite+freeY or Y-touched. + // Post-IUP (0x7): nothing moves (MoveZp2Point blocks Y at post-IUP). + if (inTwilight || + (!postIUP && + ((this.isComposite && this.state.Freedom.Y != 0) || + ((this.zp2.TouchState[pointIndex] & TouchState.Y) == TouchState.Y)))) + { + this.MoveZp2Point(this.zp2, pointIndex, 0, dy, true); + } + } + else + { + // Native ClearType: move freely on both axes. + this.MoveZp2Point(this.zp2, pointIndex, dx, dy, true); + } + } + + this.state.Loop = 1; break; } case OpCode.SHC0: case OpCode.SHC1: { - Vector2 displacement = this.ComputeDisplacement((int)opcode, out Zone zone, out int point); + if (!this.TryComputeDisplacement((int)opcode, out Zone zone, out int point, out Vector2 displacement)) + { + this.stack.Pop(); + break; + } int contour = this.stack.Pop(); + int bounds = this.zp2.IsTwilight ? 1 : this.contours.Count; + if ((uint)contour >= (uint)bounds) + { + break; + } + int start = contour == 0 ? 0 : this.contours[contour - 1] + 1; int count = this.zp2.IsTwilight ? this.zp2.Current.Length : this.contours[contour] + 1; ControlPoint[] current = this.zp2.Current; @@ -761,8 +1083,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF // Don't move the reference point if (zone.Current != current || point != i) { - current[i].Point.Y += displacement.Y; - states[i] |= TouchState.Y; + this.MoveZp2Point(this.zp2, i, displacement.X, displacement.Y, true); } } } @@ -771,7 +1092,18 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.SHZ0: case OpCode.SHZ1: { - Vector2 displacement = this.ComputeDisplacement((int)opcode, out Zone zone, out int point); + // FreeType Ins_SHZ: pop zone index first, then compute displacement. + int shzZone = this.stack.Pop(); + if ((uint)shzZone >= 2) + { + break; + } + + if (!this.TryComputeDisplacement((int)opcode, out Zone zone, out int point, out Vector2 displacement)) + { + break; + } + int count = 0; if (this.zp2.IsTwilight) { @@ -788,7 +1120,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF // Don't move the reference point if (zone.Current != current || point != i) { - current[i].Point.Y += displacement.Y; + this.MoveZp2Point(this.zp2, i, displacement.X, displacement.Y, false); } } } @@ -799,6 +1131,13 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { float distance = this.ReadCvt(); int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets rp0/rp1. + this.state.Rp0 = pointIndex; + this.state.Rp1 = pointIndex; + break; + } // this instruction is used in the CVT to set up twilight points with original values if (this.zp0.IsTwilight) @@ -831,7 +1170,13 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.MDAP0: case OpCode.MDAP1: { + // FreeType Ins_MDAP: bounds check before access. int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + break; + } + Vector2 point = this.zp0.GetCurrent(pointIndex); float distance = 0.0f; if (opcode == OpCode.MDAP1) @@ -851,6 +1196,11 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { float targetDistance = this.stack.PopFloat(); int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + break; + } // if we're operating on the twilight zone, initialize the points if (this.zp1.IsTwilight) @@ -876,14 +1226,39 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.IP: { + // FreeType Ins_IP: bounds check rp1 first. + if ((uint)this.state.Rp1 >= (uint)this.zp0.Current.Length) + { + // Fail label: drain stack and reset loop. + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + Vector2 originalBase = this.zp0.GetOriginal(this.state.Rp1); Vector2 currentBase = this.zp0.GetCurrent(this.state.Rp1); - float originalRange = this.DualProject(this.zp1.GetOriginal(this.state.Rp2) - originalBase); - float currentRange = this.Project(this.zp1.GetCurrent(this.state.Rp2) - currentBase); + + // FreeType: if rp2 fails, set ranges to 0 but continue. + float originalRange = 0; + float currentRange = 0; + if ((uint)this.state.Rp2 < (uint)this.zp1.Current.Length) + { + originalRange = this.DualProject(this.zp1.GetOriginal(this.state.Rp2) - originalBase); + currentRange = this.Project(this.zp1.GetCurrent(this.state.Rp2) - currentBase); + } for (int i = 0; i < this.state.Loop; i++) { int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp2.Current.Length) + { + continue; + } + Vector2 point = this.zp2.GetCurrent(pointIndex); float currentDistance = this.Project(point - currentBase); float originalDistance = this.DualProject(this.zp2.GetOriginal(pointIndex) - originalBase); @@ -911,9 +1286,26 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.ALIGNRP: { + // FreeType Ins_ALIGNRP: bounds check rp0 first. + if ((uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + for (int i = 0; i < this.state.Loop; i++) + { + this.stack.Pop(); + } + + this.state.Loop = 1; + break; + } + for (int i = 0; i < this.state.Loop; i++) { int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length) + { + continue; + } + Vector2 p1 = this.zp1.GetCurrent(pointIndex); Vector2 p2 = this.zp0.GetCurrent(this.state.Rp0); this.MovePoint(this.zp1, pointIndex, -this.Project(p1 - p2)); @@ -925,8 +1317,15 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.ALIGNPTS: { - int p1 = this.stack.Pop(); + // FreeType Ins_ALIGNPTS: args[1] (top) = p2 in zp0, args[0] (deeper) = p1 in zp1. int p2 = this.stack.Pop(); + int p1 = this.stack.Pop(); + if ((uint)p1 >= (uint)this.zp1.Current.Length || + (uint)p2 >= (uint)this.zp0.Current.Length) + { + break; + } + float distance = this.Project(this.zp0.GetCurrent(p2) - this.zp1.GetCurrent(p1)) / 2; this.MovePoint(this.zp1, p1, distance); this.MovePoint(this.zp0, p2, -distance); @@ -935,13 +1334,25 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.UTP: { - this.zp0.TouchState[this.stack.Pop()] &= ~this.GetTouchState(); + int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + break; + } + + this.zp0.TouchState[pointIndex] &= ~this.GetTouchState(); break; } case OpCode.IUP0: case OpCode.IUP1: { + // FreeType: IUP returns immediately once both axes have been processed. + if (this.iupXCalled && this.iupYCalled) + { + break; + } + unsafe { // bail if no contours (empty outline) @@ -1048,11 +1459,24 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.ISECT: { // move point P to the intersection of lines A and B - Vector2 b1 = this.zp0.GetCurrent(this.stack.Pop()); - Vector2 b0 = this.zp0.GetCurrent(this.stack.Pop()); - Vector2 a1 = this.zp1.GetCurrent(this.stack.Pop()); - Vector2 a0 = this.zp1.GetCurrent(this.stack.Pop()); + int ib1 = this.stack.Pop(); + int ib0 = this.stack.Pop(); + int ia1 = this.stack.Pop(); + int ia0 = this.stack.Pop(); int index = this.stack.Pop(); + if ((uint)ib0 >= (uint)this.zp0.Current.Length || + (uint)ib1 >= (uint)this.zp0.Current.Length || + (uint)ia0 >= (uint)this.zp1.Current.Length || + (uint)ia1 >= (uint)this.zp1.Current.Length || + (uint)index >= (uint)this.zp2.Current.Length) + { + break; + } + + Vector2 b1 = this.zp0.GetCurrent(ib1); + Vector2 b0 = this.zp0.GetCurrent(ib0); + Vector2 a1 = this.zp1.GetCurrent(ia1); + Vector2 a0 = this.zp1.GetCurrent(ia0); // calculate intersection using determinants: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line Vector2 da = a0 - a1; @@ -1188,7 +1612,13 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { if (this.stack.PopBool() == (opcode == OpCode.JROT)) { - stream.Jump(this.stack.Pop() - 1); + int offset = this.stack.Pop(); + if (offset < 0 && ++this.negJumpCounter > this.negJumpCounterMax) + { + return; + } + + stream.Jump(offset - 1); } else { @@ -1199,7 +1629,14 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; case OpCode.JMPR: { - stream.Jump(this.stack.Pop() - 1); + int offset = this.stack.Pop(); + if (offset < 0 && ++this.negJumpCounter > this.negJumpCounterMax) + { + // FreeType sets Execution_Too_Long error and returns. + return; + } + + stream.Jump(offset - 1); break; } @@ -1309,12 +1746,13 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.DIV: { int b = this.stack.Pop(); + int a = this.stack.Pop(); if (b == 0) { - throw new InvalidOperationException("Division by zero."); + // FreeType sets Divide_By_Zero error and returns. + return; } - int a = this.stack.Pop(); long result = ((long)a << 6) / b; this.stack.Push((int)result); } @@ -1370,7 +1808,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { if (!allowFunctionDefs || inFunction) { - throw new FontException("Can't define functions here."); + return; } this.functions[this.stack.Pop()] = stream.ToMemory(); @@ -1384,7 +1822,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { if (!allowFunctionDefs || inFunction) { - throw new FontException("Can't define functions here."); + return; } this.instructionDefs[this.stack.Pop()] = stream.ToMemory(); @@ -1398,7 +1836,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { if (!inFunction) { - throw new FontException("Found invalid ENDF marker outside of a function definition."); + return; } return; @@ -1410,14 +1848,37 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF this.callStackSize++; if (this.callStackSize > MaxCallStack) { - throw new FontException("Stack overflow; infinite recursion?"); + // FreeType sets Stack_Overflow error and returns. + return; + } + + int funcIndex = this.stack.Pop(); + if ((uint)funcIndex >= (uint)this.functions.Length) + { + // FreeType sets Invalid_Reference error and returns. + return; } - InstructionStream function = this.functions[this.stack.Pop()]; + InstructionStream function = this.functions[funcIndex]; int count = opcode == OpCode.LOOPCALL ? this.stack.Pop() : 1; - for (int i = 0; i < count; i++) + + // FreeType: only LOOPCALL increments the loopcall counter, not CALL. + if (opcode == OpCode.LOOPCALL) + { + this.loopcallCounter += count; + if (this.loopcallCounter > this.loopcallCounterMax) + { + // FreeType sets Execution_Too_Long error and returns. + return; + } + } + + if (count > 0) { - this.Execute(function.ToStack(), true, false); + for (int i = 0; i < count; i++) + { + this.Execute(function.ToStack(), true, false); + } } this.callStackSize--; @@ -1474,9 +1935,11 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF amount *= 1 << (6 - this.state.DeltaShift); - // update the CVT - CheckIndex(cvtIndex, this.controlValueTable.Length); - this.controlValueTable[cvtIndex] += F26Dot6ToFloat(amount); + // update the CVT (FreeType non-pedantic: silently ignore out-of-bounds) + if ((uint)cvtIndex < (uint)this.controlValueTable.Length) + { + this.controlValueTable[cvtIndex] += F26Dot6ToFloat(amount); + } } } } @@ -1496,6 +1959,10 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF { int pointIndex = this.stack.Pop(); int arg = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp0.Current.Length) + { + continue; + } // upper 4 bits of the 8-bit arg is the relative ppem // the opcode specifies the base to add to the ppem @@ -1519,13 +1986,23 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF amount *= 1 << (6 - this.state.DeltaShift); - // SHPIX and DELTAP don't execute unless moving a composite on the - // y axis or moving a previously y touched point. - TouchState state = this.zp0.TouchState[pointIndex]; - if (!postIUP && ((composite && this.state.Freedom.Y != 0) || ((state & TouchState.Y) == TouchState.Y))) + // FreeType Ins_DELTAP: v40 backward compatibility gating. + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + if (nativeClearType) { this.MovePoint(this.zp0, pointIndex, F26Dot6ToFloat(amount)); } + else + { + // Compat mode: gate on !postIUP AND (composite+freeY or Y-touched). + TouchState state = this.zp0.TouchState[pointIndex]; + if (!postIUP && + ((composite && this.state.Freedom.Y != 0) || + ((state & TouchState.Y) == TouchState.Y))) + { + this.MovePoint(this.zp0, pointIndex, F26Dot6ToFloat(amount)); + } + } } } } @@ -1541,30 +2018,77 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF case OpCode.GETINFO: { + // FreeType Ins_GETINFO. + // Report v40 interpreter identity and ClearType capability flags. int selector = this.stack.Pop(); int result = 0; + + // Selector bit 0: interpreter version. if ((selector & 0x1) != 0) { - // pretend we are MS Rasterizer v35 - result = 35; + result = 40; } - // TODO: rotation and stretching - // if ((selector & 0x2) != 0) - // if ((selector & 0x4) != 0) + // Selector bits 1-2: rotation/stretching — always false in v40. - // we're always rendering in grayscale - if ((selector & 0x20) != 0) + // Selector bit 3: variation glyph (FreeType Ins_GETINFO). + // Set result bit 10 when the font is a variable font instance. + if ((selector & 0x8) != 0 && this.normalizedAxisCoordinates is not null) { - result |= 1 << 12; + result |= 1 << 10; + } + + // Selector bit 5: grayscale rendering. + // FreeType v40 sets grayscale = FALSE, so this bit is NOT set. + + // Selector bit 6: subpixel hinting is available (v40 default). + if ((selector & 0x40) != 0) + { + result |= 1 << 13; + } + + // Selector bit 10: subpixel positioned. + if ((selector & 0x400) != 0) + { + result |= 1 << 17; + } + + // Selector bit 11: symmetrical smoothing. + if ((selector & 0x800) != 0) + { + result |= 1 << 18; } - // TODO: ClearType flags this.stack.Push(result); } break; + case OpCode.GETVARIATION: + { + // FreeType Ins_GETVARIATION. + // Push normalized axis coordinates as F2Dot14 integers. + // FreeType stores coords as F16Dot16 and does >> 2 to get F2Dot14. + // We store floats in [-1,1], so multiply by 16384 to get F2Dot14. + if (this.normalizedAxisCoordinates is not null) + { + for (int i = 0; i < this.normalizedAxisCoordinates.Length; i++) + { + this.stack.Push((int)Math.Round(this.normalizedAxisCoordinates[i] * 16384)); + } + } + + break; + } + + case OpCode.GETDATA: + { + // FreeType Ins_GETDATA. + // Always returns 17. + this.stack.Push(17); + break; + } + default: { if (opcode >= OpCode.MIRP) @@ -1581,13 +2105,14 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF int index = (int)opcode; if (index > this.instructionDefs.Length || !this.instructionDefs[index].IsValid) { - throw new FontException("Unknown opcode in font program."); + // FreeType sets Invalid_Opcode error and terminates execution. + return; } this.callStackSize++; if (this.callStackSize > MaxCallStack) { - throw new FontException("Stack overflow; infinite recursion?"); + return; } this.Execute(this.instructionDefs[index].ToStack(), true, false); @@ -1597,17 +2122,32 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF break; } } + +#if HINTING_TRACE + this.TracePostInstruction(opcode, pops, pushes, preStackCount); +#endif } } - private static int CheckIndex(int index, int length) + /// + /// Pops a CVT index from the stack and returns the corresponding value. + /// Returns 0 for out-of-bounds indices (FreeType non-pedantic behavior). + /// + private float ReadCvt() { - Guard.MustBeBetweenOrEqualTo(index, 0, length - 1, nameof(index)); - return index; - } + int loc = this.stack.Pop(); + if ((uint)loc >= (uint)this.controlValueTable.Length) + { + return 0; + } - private float ReadCvt() => this.controlValueTable[CheckIndex(this.stack.Pop(), this.controlValueTable.Length)]; + return this.controlValueTable[loc]; + } + /// + /// Recomputes the cached dot product of the freedom and projection vectors. + /// Must be called whenever either vector changes. + /// private void OnVectorsUpdated() { this.fdotp = Vector2.Dot(this.state.Freedom, this.state.Projection); @@ -1617,12 +2157,20 @@ private void OnVectorsUpdated() } } + /// + /// Sets the freedom vector to one of the coordinate axes (SFVTCA). + /// + /// 0 for the Y-axis, 1 for the X-axis. private void SetFreedomVectorToAxis(int axis) { this.state.Freedom = axis == 0 ? Vector2.UnitY : Vector2.UnitX; this.OnVectorsUpdated(); } + /// + /// Sets the projection and dual-projection vectors to one of the coordinate axes (SPVTCA). + /// + /// 0 for the Y-axis, 1 for the X-axis. private void SetProjectionVectorToAxis(int axis) { this.state.Projection = axis == 0 ? Vector2.UnitY : Vector2.UnitX; @@ -1631,13 +2179,14 @@ private void SetProjectionVectorToAxis(int axis) this.OnVectorsUpdated(); } + /// + /// Sets a projection or freedom vector to the direction of a line between two points + /// (SPVTL/SFVTL/SDPVTL). The mode's low bit selects the perpendicular direction. + /// + /// 0=SPVTL0, 1=SPVTL1, 2=SFVTL0, 3=SFVTL1. + /// When , also sets the dual-projection vector from original coordinates. private void SetVectorToLine(int mode, bool dual) { - // mode here should be as follows: - // 0: SPVTL0 - // 1: SPVTL1 - // 2: SFVTL0 - // 3: SFVTL1 int index1 = this.stack.Pop(); int index2 = this.stack.Pop(); Vector2 p1 = this.zp2.GetCurrent(index1); @@ -1703,25 +2252,42 @@ private void SetVectorToLine(int mode, bool dual) this.OnVectorsUpdated(); } - private Zone GetZoneFromStack() - => this.stack.Pop() switch + /// + /// Pops a zone index from the stack and returns the corresponding zone. + /// Returns for invalid indices (FreeType non-pedantic: silently ignores). + /// + private bool TryGetZoneFromStack(out Zone zone) + { + int zoneIndex = this.stack.Pop(); + switch (zoneIndex) { - 0 => this.twilight, - 1 => this.points, - _ => throw new FontException("Invalid zone pointer."), - }; + case 0: + zone = this.twilight; + return true; + case 1: + zone = this.points; + return true; + default: + // FreeType non-pedantic: silently ignore invalid zone pointers. + zone = default; + return false; + } + } + /// + /// Configures super-rounding parameters from a packed mode byte (SROUND/S45ROUND). + /// Bits 7-6 select the period multiplier, bits 5-4 the phase, and bits 3-0 the threshold. + /// + /// Base period: 1.0 for SROUND, sqrt(2)/2 for S45ROUND. private void SetSuperRound(float period) { - // mode is a bunch of packed flags - // bits 7-6 are the period multiplier int mode = this.stack.Pop(); this.roundPeriod = (mode & 0xC0) switch { 0 => period / 2, 0x40 => period, 0x80 => period * 2, - _ => throw new FontException("Unknown rounding period multiplier."), + _ => period * 2, // Reserved; FreeType treats as period * 2. }; // bits 5-4 are the phase @@ -1752,13 +2318,29 @@ private void SetSuperRound(float period) } } + /// + /// Move Indirect Relative Point (MIRP). Moves a point so that its distance from RP0 + /// matches a CVT value, subject to rounding, cut-in, and minimum distance constraints + /// controlled by the instruction's flag bits. + /// + /// MIRP flag bits: bit 4=set RP0, bit 3=minimum distance, bit 2=round, bits 1-0=engine compensation. private void MoveIndirectRelative(int flags) { - // this instruction tries to make the current distance between a given point - // and the reference point rp0 be equivalent to the same distance in the original outline - // there are a bunch of flags that control how that distance is measured float cvt = this.ReadCvt(); int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets reference points. + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + + return; + } if (Math.Abs(cvt - this.state.SingleWidthValue) < this.state.SingleWidthCutIn) { @@ -1826,10 +2408,29 @@ private void MoveIndirectRelative(int flags) } } + /// + /// Move Direct Relative Point (MDRP). Moves a point so that its distance from RP0 + /// matches the original outline distance, subject to rounding and minimum distance + /// constraints controlled by the instruction's flag bits. + /// + /// MDRP flag bits: bit 4=set RP0, bit 3=minimum distance, bit 2=round, bits 1-0=engine compensation. private void MoveDirectRelative(int flags) { - // determine the original distance between the two reference points int pointIndex = this.stack.Pop(); + if ((uint)pointIndex >= (uint)this.zp1.Current.Length || + (uint)this.state.Rp0 >= (uint)this.zp0.Current.Length) + { + // FreeType Fail label: still sets reference points. + this.state.Rp1 = this.state.Rp0; + this.state.Rp2 = pointIndex; + if ((flags & 0x10) != 0) + { + this.state.Rp0 = pointIndex; + } + + return; + } + Vector2 p1 = this.zp0.GetOriginal(this.state.Rp0); Vector2 p2 = this.zp1.GetOriginal(pointIndex); float originalDistance = this.DualProject(p2 - p1); @@ -1878,9 +2479,18 @@ private void MoveDirectRelative(int flags) } } - private Vector2 ComputeDisplacement(int mode, out Zone zone, out int point) + /// + /// Computes the displacement vector for SHP/SHC/SHZ instructions by projecting the + /// movement of the reference point (RP1 or RP2 depending on mode) from its original + /// to its current position onto the freedom vector. + /// + /// Opcode value; bit 0 selects RP1 in ZP0 (1) or RP2 in ZP1 (0). + /// Receives the reference zone. + /// Receives the reference point index. + /// Receives the computed displacement vector. + /// if the reference point is valid; otherwise . + private bool TryComputeDisplacement(int mode, out Zone zone, out int point, out Vector2 displacement) { - // compute displacement of the reference point if ((mode & 1) == 0) { zone = this.zp1; @@ -1892,10 +2502,21 @@ private Vector2 ComputeDisplacement(int mode, out Zone zone, out int point) point = this.state.Rp1; } + if ((uint)point >= (uint)zone.Current.Length) + { + displacement = default; + return false; + } + float distance = this.Project(zone.GetCurrent(point) - zone.GetOriginal(point)); - return distance * this.state.Freedom / this.fdotp; + displacement = distance * this.state.Freedom / this.fdotp; + return true; } + /// + /// Returns the touch state flags corresponding to the current freedom vector axes. + /// Used by UTP to selectively clear touch bits. + /// private TouchState GetTouchState() { TouchState touch = TouchState.None; @@ -1912,56 +2533,94 @@ private TouchState GetTouchState() return touch; } - private void ShiftPoints(Vector2 displacement) + /// + /// Moves a point along the freedom vector by the given distance, applying v40 + /// backward compatibility restrictions: X movement is always blocked in compat mode, + /// Y movement is blocked only after both IUP passes have completed (post-IUP). + /// Corresponds to FreeType's Direct_Move / func_move. + /// + private void MovePoint(Zone zone, int index, float distance) { - // SHPIX and DELTAP don't execute unless moving a composite on the - // y axis or moving a previously y touched point. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 + // X is always blocked in backward compat mode. + // Y is blocked only when backward_compatibility == 0x7 (post-IUP). + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; bool postIUP = this.iupXCalled && this.iupYCalled; - bool composite = this.isComposite; - ControlPoint[] current = this.zp2.Current; - bool inTwilight = this.zp0.IsTwilight || this.zp1.IsTwilight || this.zp2.IsTwilight; - for (int i = 0; i < this.state.Loop; i++) + if (this.state.Freedom.X != 0) { - // Special case: allow SHPIX to move points in the twilight zone. - // Otherwise, treat SHPIX the same as DELTAP. Unbreaks various - // fonts such as older versions of Rokkitt and DTL Argo T Light - // that would glitch severely after calling ALIGNRP after a - // blocked SHPIX. - int pointIndex = this.stack.Pop(); - ref TouchState state = ref this.zp2.TouchState[pointIndex]; - if (inTwilight || (!postIUP && ((composite && this.state.Freedom.Y != 0) || ((state & TouchState.Y) == TouchState.Y)))) + if (nativeClearType) { - // Copy FreeType Interpreter V40 and ignore instructions on the x-axis. - // This prevents outline distortion on legacy fonts. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 - current[pointIndex].Point.Y += displacement.Y; - state |= TouchState.Y; + float dx = distance * this.state.Freedom.X / this.fdotp; + zone.Current[index].Point.X += dx; } + + zone.TouchState[index] |= TouchState.X; } - this.state.Loop = 1; + if (this.state.Freedom.Y != 0) + { + if (nativeClearType || !postIUP) + { + float dy = distance * this.state.Freedom.Y / this.fdotp; + zone.Current[index].Point.Y += dy; + } + + zone.TouchState[index] |= TouchState.Y; + } + +#if HINTING_TRACE + this.traceLog.AppendLine(System.FormattableString.Invariant($" -> pt[{index}] = ({zone.Current[index].Point.X:F2}, {zone.Current[index].Point.Y:F2}) dist={distance:F2}")); +#endif } - private void MovePoint(Zone zone, int index, float distance) + /// + /// Moves a ZP2 point by explicit (dx, dy) deltas with the same v40 backward + /// compatibility restrictions as . Used by SHP, SHC, SHZ, + /// and SHPIX where the displacement is pre-computed rather than derived from a scalar distance. + /// Corresponds to FreeType's Move_Zp2_Point. + /// + private void MoveZp2Point(Zone zone, int index, float dx, float dy, bool touch) { - // Copy FreeType Interpreter V40 and ignore instructions on the x-axis. - // This increases resolution on the x-axis and prevents glyph explosions on legacy fonts. - // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298 - Vector2 cur = zone.GetCurrent(index); + // X is always blocked in compat mode. + // Y is blocked only at backward_compatibility == 0x7 (post-IUP). + bool nativeClearType = (this.state.InstructionControl & InstructionControlFlags.NativeClearType) != 0; + bool postIUP = this.iupXCalled && this.iupYCalled; - // V40: ignore x movement, apply only the Y component. - float dy = distance * this.state.Freedom.Y / this.fdotp; + if (this.state.Freedom.X != 0) + { + if (nativeClearType) + { + zone.Current[index].Point.X += dx; + } + + if (touch) + { + zone.TouchState[index] |= TouchState.X; + } + } - // Only mark Y as touched if Y actually changed. - if (dy != 0F) + if (this.state.Freedom.Y != 0) { - zone.Current[index].Point.Y = cur.Y + dy; - zone.TouchState[index] |= TouchState.Y; + if (nativeClearType || !postIUP) + { + zone.Current[index].Point.Y += dy; + } + + if (touch) + { + zone.TouchState[index] |= TouchState.Y; + } } + +#if HINTING_TRACE + this.traceLog.AppendLine(System.FormattableString.Invariant($" -> zp2[{index}] = ({zone.Current[index].Point.X:F2}, {zone.Current[index].Point.Y:F2}) dx={dx:F2} dy={dy:F2}")); +#endif } + /// + /// Rounds a distance value according to the current round state. + /// FreeType v40 uses zero engine compensation for all modes. + /// private float Round(float value) { switch (this.state.RoundState) @@ -2138,13 +2797,19 @@ private float Round(float value) } } + /// Projects a point difference onto the projection vector. private float Project(Vector2 point) => Vector2.Dot(point, this.state.Projection); + /// Projects a point difference onto the dual-projection vector (used for original coordinates). private float DualProject(Vector2 point) => Vector2.Dot(point, this.state.DualProjection); + /// + /// Reads and skips the next instruction in the stream, advancing past any inline + /// data bytes for push instructions. Used by FDEF/IDEF to scan for ENDF and by + /// IF/ELSE to skip over conditional blocks. + /// private static OpCode SkipNext(ref StackInstructionStream stream) { - // grab the next opcode, and if it's one of the push instructions skip over its arguments OpCode opcode = stream.NextOpCode(); switch (opcode) { @@ -2183,6 +2848,11 @@ private static OpCode SkipNext(ref StackInstructionStream stream) return opcode; } + /// + /// Interpolates untouched points between two reference points, preserving + /// their relative positions in the original outline. Used by IUP. + /// Operates on raw byte pointers to support direction-agnostic X/Y processing. + /// private static unsafe void InterpolatePoints(byte* current, byte* original, int start, int end, int ref1, int ref2) { if (start > end) @@ -2235,6 +2905,11 @@ private static unsafe void InterpolatePoints(byte* current, byte* original, int } } + // Fixed-point conversion helpers. + // F2Dot14: 2-bit integer + 14-bit fraction, range [-2, ~2). Used for unit vectors. + // F26Dot6: 26-bit integer + 6-bit fraction. The native format for point coordinates + // in the TrueType interpreter. Our implementation uses float throughout but converts + // at the stack boundary to maintain compatibility with instruction semantics. private static float F2Dot14ToFloat(int value) => (short)value / 16384.0f; private static int FloatToF2Dot14(float value) => (int)(uint)(short)Math.Round(value * 16384.0f); @@ -2245,6 +2920,70 @@ private static unsafe void InterpolatePoints(byte* current, byte* original, int private static unsafe float* GetPoint(byte* data, int index) => (float*)(data + (sizeof(ControlPoint) * index)); +#if HINTING_TRACE + private void TracePreInstruction(OpCode opcode, int pops) + { + System.Text.StringBuilder sb = this.traceLog; + sb.Append(System.FormattableString.Invariant($"[{this.insCounter}] {opcode} (stk={this.stack.Count})")); + + // Show the top stack values that this instruction will consume. + int available = Math.Min(pops, this.stack.Count); + if (available > 0) + { + sb.Append(" args=["); + for (int i = available - 1; i >= 0; i--) + { + if (i < available - 1) + { + sb.Append(", "); + } + + sb.Append(this.stack.Peek(i)); + } + + sb.Append(']'); + } + + sb.AppendLine(); + } + + private void TracePostInstruction(OpCode opcode, int pops, int pushes, int preStackCount) + { + int postStackCount = this.stack.Count; + int expectedDelta = pushes - pops; + int actualDelta = postStackCount - preStackCount; + + // Skip variable-pop/push instructions where PopPushCount is not authoritative. + bool variablePop = opcode is + OpCode.NPUSHB or OpCode.NPUSHW or + OpCode.PUSHB1 or OpCode.PUSHB2 or OpCode.PUSHB3 or OpCode.PUSHB4 or + OpCode.PUSHB5 or OpCode.PUSHB6 or OpCode.PUSHB7 or OpCode.PUSHB8 or + OpCode.PUSHW1 or OpCode.PUSHW2 or OpCode.PUSHW3 or OpCode.PUSHW4 or + OpCode.PUSHW5 or OpCode.PUSHW6 or OpCode.PUSHW7 or OpCode.PUSHW8 or + OpCode.SHP0 or OpCode.SHP1 or + OpCode.FLIPRGON or OpCode.FLIPRGOFF or + OpCode.DELTAP1 or OpCode.DELTAP2 or OpCode.DELTAP3 or + OpCode.DELTAC1 or OpCode.DELTAC2 or OpCode.DELTAC3 or + OpCode.LOOPCALL or OpCode.CALL or + OpCode.FDEF or OpCode.IDEF or + OpCode.GETVARIATION or + OpCode.ENDF or OpCode.AA; + + if (!variablePop && actualDelta != expectedDelta) + { + this.traceLog.AppendLine( + System.FormattableString.Invariant( + $" *** STACK IMBALANCE: expected delta={expectedDelta} (pop={pops} push={pushes}), actual delta={actualDelta} (pre={preStackCount} post={postStackCount})")); + } + } + + /// + /// Gets the accumulated trace log for the most recent glyph hinting operation. + /// Only available when compiled with the HINTING_TRACE constant. + /// + internal string GetTraceLog() => this.traceLog.ToString(); +#endif + #pragma warning disable SA1201 // Elements should appear in the correct order private enum RoundMode #pragma warning restore SA1201 // Elements should appear in the correct order @@ -2264,7 +3003,8 @@ private enum InstructionControlFlags { None, InhibitGridFitting = 0x1, - UseDefaultGraphicsState = 0x2 + UseDefaultGraphicsState = 0x2, + NativeClearType = 0x4 } [Flags] @@ -2276,171 +3016,6 @@ private enum TouchState Both = X | Y } - private enum OpCode : byte - { - SVTCA0, - SVTCA1, - SPVTCA0, - SPVTCA1, - SFVTCA0, - SFVTCA1, - SPVTL0, - SPVTL1, - SFVTL0, - SFVTL1, - SPVFS, - SFVFS, - GPV, - GFV, - SFVTPV, - ISECT, - SRP0, - SRP1, - SRP2, - SZP0, - SZP1, - SZP2, - SZPS, - SLOOP, - RTG, - RTHG, - SMD, - ELSE, - JMPR, - SCVTCI, - SSWCI, - SSW, - DUP, - POP, - CLEAR, - SWAP, - DEPTH, - CINDEX, - MINDEX, - ALIGNPTS, - /* unused: 0x28 */ - UTP = 0x29, - LOOPCALL, - CALL, - FDEF, - ENDF, - MDAP0, - MDAP1, - IUP0, - IUP1, - SHP0, - SHP1, - SHC0, - SHC1, - SHZ0, - SHZ1, - SHPIX, - IP, - MSIRP0, - MSIRP1, - ALIGNRP, - RTDG, - MIAP0, - MIAP1, - NPUSHB, - NPUSHW, - WS, - RS, - WCVTP, - RCVT, - GC0, - GC1, - SCFS, - MD0, - MD1, - MPPEM, - MPS, - FLIPON, - FLIPOFF, - DEBUG, - LT, - LTEQ, - GT, - GTEQ, - EQ, - NEQ, - ODD, - EVEN, - IF, - EIF, - AND, - OR, - NOT, - DELTAP1, - SDB, - SDS, - ADD, - SUB, - DIV, - MUL, - ABS, - NEG, - FLOOR, - CEILING, - ROUND0, - ROUND1, - ROUND2, - ROUND3, - NROUND0, - NROUND1, - NROUND2, - NROUND3, - WCVTF, - DELTAP2, - DELTAP3, - DELTAC1, - DELTAC2, - DELTAC3, - SROUND, - S45ROUND, - JROT, - JROF, - ROFF, - /* unused: 0x7B */ - RUTG = 0x7C, - RDTG, - SANGW, - AA, - FLIPPT, - FLIPRGON, - FLIPRGOFF, - /* unused: 0x83 - 0x84 */ - SCANCTRL = 0x85, - SDPVTL0, - SDPVTL1, - GETINFO, - IDEF, - ROLL, - MAX, - MIN, - SCANTYPE, - INSTCTRL, - /* unused: 0x8F - 0xAF */ - PUSHB1 = 0xB0, - PUSHB2, - PUSHB3, - PUSHB4, - PUSHB5, - PUSHB6, - PUSHB7, - PUSHB8, - PUSHW1, - PUSHW2, - PUSHW3, - PUSHW4, - PUSHW5, - PUSHW6, - PUSHW7, - PUSHW8, - MDRP, // range of 32 values, 0xC0 - 0xDF, - MIRP = 0xE0 // range of 32 values, 0xE0 - 0xFF - } - private readonly struct InstructionStream { private readonly ReadOnlyMemory instructions; @@ -2582,10 +3157,13 @@ public Zone(ControlPoint[] controlPoints, bool isTwilight) private class ExecutionStack { private readonly int[] s; - private int count; public ExecutionStack(int maxStack) => this.s = new int[maxStack]; + public int Count { get; private set; } + + public int Capacity => this.s.Length; + public int Peek() => this.Peek(0); public bool PopBool() => this.Pop() != 0; @@ -2596,9 +3174,9 @@ private class ExecutionStack public void Push(float value) => this.Push(FloatToF26Dot6(value)); - public void Clear() => this.count = 0; + public void Clear() => this.Count = 0; - public void Depth() => this.Push(this.count); + public void Depth() => this.Push(this.Count); public void Duplicate() => this.Push(this.Peek()); @@ -2612,7 +3190,7 @@ private class ExecutionStack public void Move(int index) { - int c = this.count; + int c = this.Count; int[] a = this.s; int val = this.Peek(index); for (int i = c - index - 1; i < c - 1; i++) @@ -2625,7 +3203,7 @@ public void Move(int index) public void Swap() { - int c = this.count; + int c = this.Count; if (c < 2) { ThrowStackOverflow(); @@ -2637,32 +3215,32 @@ public void Swap() public void Push(int value) { - if (this.count == this.s.Length) + if (this.Count == this.s.Length) { ThrowStackOverflow(); } - this.s[this.count++] = value; + this.s[this.Count++] = value; } public int Pop() { - if (this.count == 0) + if (this.Count == 0) { ThrowStackOverflow(); } - return this.s[--this.count]; + return this.s[--this.Count]; } public int Peek(int index) { - if (index < 0 || index >= this.count) + if (index < 0 || index >= this.Count) { ThrowStackOverflow(); } - return this.s[this.count - index - 1]; + return this.s[this.Count - index - 1]; } private static void ThrowStackOverflow() => throw new FontException("stack overflow"); diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs index 528197a8..b97921bf 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs @@ -44,8 +44,8 @@ public partial class TrueTypeGlyphMetrics private static readonly HashSet NeverHint = new(StringComparer.Ordinal) { - "MgOpen Canonica", - "MgOpenCanonica", + // Currently empty, but we may add entries here in the future if we identify + // any fonts that render better without hinting. }; /// diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png index 7d129712..47d8d8a1 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:927d41161e0644a5ca5b3009ae153f2fc768d6af1a4593101fa37ddeb292078c -size 296804 +oid sha256:328bd7718418edf2bb383c083d27ea947c76780a2eb951cefc7ebacccf5c1d9f +size 295254 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png index 507a4678..390b39dd 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48d7e5435e7301d68174e5c46fbecf3ae0e8800432d94375b75299d0856fce1b -size 308372 +oid sha256:aaf2a205e4eb3cb973a1c80171f15eeb06d32102c7b2a99c12270ebce7cd009b +size 307664 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png index 651123a0..5216bd95 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4393ddb3b4374526184e48848091e1aec813fc4822ceaf6fb617d26b3bf1cb6b -size 291879 +oid sha256:8f19df131df1975c5709a75a18f3a228dbf05ebf7bf70bad33ecb26f931b2a76 +size 289194 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png b/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png index a818cc8b..f3b28186 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27a31c3f11ebc6344efa7a1bb8088487cfa04e85bcb4e0b1488f5a801345ad53 -size 4259 +oid sha256:d9013db99d410e4a6c1819010a08e42acd40f07a8d7f009493fc3d0f44e9fa02 +size 4222 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png b/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png index b2948b69..199cd17e 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f867cbe3661411cebf96c156874fd2040b7999c7f8e9280903f0555bc23983a -size 1763 +oid sha256:03f67cd0a7a8b27c540c6e582d35c8ee8f0f9fd7237fd51adee9e6470a42cb63 +size 1767 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png b/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png index 54a692ff..c4e8736c 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:358e564d97c9024542521c059940627d78ec48a933d0c5a1459ffaa2f20f8c67 -size 2217 +oid sha256:0f5e22c27ebee375842ccf9a35129f48985f36eddc5df51039febd45dcaf8269 +size 2243 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png index ce25bde4..d0b56270 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afacbc6b658b621c6d4dd338e09545eeac9d35975c71c6675714e6c7e1d822f3 -size 2367 +oid sha256:214f2d3e5418d7d47478cdda5bebe2b84146efe2ca1b4533aa02a4819cbbf5ad +size 1204 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png index ce25bde4..5e90acaf 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afacbc6b658b621c6d4dd338e09545eeac9d35975c71c6675714e6c7e1d822f3 -size 2367 +oid sha256:a63e847da813652a5dda8e7c6defe78e54053860fafbdf8afeb8be39ef4b7912 +size 1227 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png index ba410c26..a9b85c96 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c9a4219e0b49b547e9470e3b610bc4e1abe54139f1e5197097b26c1c1461bc -size 2168 +oid sha256:17aa26476c6e8162b2e14af09c82b457af7b6a43eafda61c1c880f4b1b533f09 +size 1050 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png index ba410c26..59347a6a 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c9a4219e0b49b547e9470e3b610bc4e1abe54139f1e5197097b26c1c1461bc -size 2168 +oid sha256:a5ddd8f69220c8a74a59211391f55ca0d95032050ac37bbf261ec79369275ef5 +size 1038 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png index 960e9f74..75dae6a5 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ba57973e3d238940f4596a624be2d3c84ccd83cb43aeeb96f7c96c6829a5e2 -size 2378 +oid sha256:b59d11736ec35969b6c6625c724223dc0fa8024ee0bca73214c09af2ed46ad08 +size 1234 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png index 960e9f74..9790ad2b 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ba57973e3d238940f4596a624be2d3c84ccd83cb43aeeb96f7c96c6829a5e2 -size 2378 +oid sha256:46e67e9cc7ad2418aff076859a267ff58d567174cc4ecc4c36a09fa7da5c337e +size 1198 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_493.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_493.cs index ad1720ca..62451b7f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_493.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_493.cs @@ -56,8 +56,8 @@ public void Test_Issue_493_MgOpenCanonic() FontFamily mainFontFamily = fontCollection.Get(name); Font mainFont = mainFontFamily.CreateFont(30, FontStyle.Regular); - TextOptions options = new(mainFont) { HintingMode = HintingMode.Standard }; + TextOptions hintOptions = new(mainFont) { HintingMode = HintingMode.Standard }; - TextLayoutTestUtilities.TestLayout(text, options); + TextLayoutTestUtilities.TestLayout(text, hintOptions); } }