Skip to content

Commit 111b9e2

Browse files
committed
Implement revisions from review
1 parent 6cb3ff4 commit 111b9e2

16 files changed

Lines changed: 582 additions & 153 deletions

File tree

Src/Common/FwUtils/FontFeatureSettings.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Globalization;
44
using System.Linq;
5+
using System.Diagnostics;
56

67
namespace SIL.FieldWorks.Common.FwUtils
78
{
@@ -10,6 +11,14 @@ namespace SIL.FieldWorks.Common.FwUtils
1011
/// </summary>
1112
public static class FontFeatureSettings
1213
{
14+
private static readonly TraceSwitch s_traceSwitch =
15+
new TraceSwitch("FwUtils_FontFeatureSettings", "Font feature parsing diagnostics");
16+
17+
internal static TraceSwitch DiagnosticsSwitch
18+
{
19+
get { return s_traceSwitch; }
20+
}
21+
1322
/// <summary>
1423
/// Parses a comma-separated font feature string into normalized feature settings.
1524
/// Invalid entries are ignored so project data cannot crash render/UI paths.
@@ -28,16 +37,25 @@ public static IReadOnlyList<FontFeatureSetting> Parse(string features)
2837

2938
var equalsIndex = part.IndexOf('=');
3039
if (equalsIndex <= 0 || equalsIndex == part.Length - 1)
40+
{
41+
TraceIgnoredEntry(part, "expected tag=value");
3142
continue;
43+
}
3244

3345
var tag = part.Substring(0, equalsIndex).Trim();
3446
var valueText = part.Substring(equalsIndex + 1).Trim();
3547
if (!IsValidOpenTypeTag(tag))
48+
{
49+
TraceIgnoredEntry(part, "tag must contain exactly four printable ASCII characters");
3650
continue;
51+
}
3752

3853
int value;
3954
if (!int.TryParse(valueText, NumberStyles.Integer, CultureInfo.InvariantCulture, out value) || value < 0)
55+
{
56+
TraceIgnoredEntry(part, "value must be a non-negative integer");
4057
continue;
58+
}
4159

4260
settingsByTag[tag] = new FontFeatureSetting(tag, value);
4361
}
@@ -63,7 +81,21 @@ public static string NormalizePreservingLegacy(string features)
6381
return string.Empty;
6482

6583
var trimmed = features.Trim();
66-
return char.IsLetter(trimmed[0]) ? Normalize(trimmed) : trimmed;
84+
return LooksLikeLegacyGraphiteFeatureString(trimmed) ? trimmed : Normalize(trimmed);
85+
}
86+
87+
private static bool LooksLikeLegacyGraphiteFeatureString(string features)
88+
{
89+
var firstPart = features.Split(',').FirstOrDefault();
90+
if (string.IsNullOrWhiteSpace(firstPart))
91+
return false;
92+
93+
var equalsIndex = firstPart.IndexOf('=');
94+
if (equalsIndex <= 0)
95+
return false;
96+
97+
var featureId = firstPart.Substring(0, equalsIndex).Trim();
98+
return featureId.Length > 0 && featureId.All(char.IsDigit);
6799
}
68100

69101
/// <summary>
@@ -73,6 +105,16 @@ public static bool IsValidOpenTypeTag(string tag)
73105
{
74106
return tag != null && tag.Length == 4 && tag.All(character => character >= 0x20 && character <= 0x7e);
75107
}
108+
109+
private static void TraceIgnoredEntry(string part, string reason)
110+
{
111+
Trace.WriteLineIf(s_traceSwitch.TraceWarning,
112+
string.Format(CultureInfo.InvariantCulture,
113+
"Ignored invalid font feature entry '{0}': {1}.",
114+
part,
115+
reason),
116+
s_traceSwitch.DisplayName);
117+
}
76118
}
77119

78120
/// <summary>

Src/Common/FwUtils/FwUtilsTests/FontFeatureSettingsTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Linq;
2+
using System.Diagnostics;
3+
using System.IO;
24
using NUnit.Framework;
35

46
namespace SIL.FieldWorks.Common.FwUtils
@@ -33,6 +35,43 @@ public void Parse_IgnoresInvalidEntries()
3335
Is.EqualTo(new[] { "liga=0", "smcp=1" }));
3436
}
3537

38+
[Test]
39+
public void Parse_LogsIgnoredInvalidEntries()
40+
{
41+
var writer = new StringWriter();
42+
var listener = new TextWriterTraceListener(writer);
43+
var previousLevel = FontFeatureSettings.DiagnosticsSwitch.Level;
44+
45+
try
46+
{
47+
FontFeatureSettings.DiagnosticsSwitch.Level = TraceLevel.Warning;
48+
Trace.Listeners.Add(listener);
49+
50+
FontFeatureSettings.Parse("smcp=1,bad=2,kern=x,broken");
51+
52+
listener.Flush();
53+
var output = writer.ToString();
54+
Assert.That(output, Does.Contain("Ignored invalid font feature entry 'bad=2'"));
55+
Assert.That(output, Does.Contain("Ignored invalid font feature entry 'kern=x'"));
56+
Assert.That(output, Does.Contain("Ignored invalid font feature entry 'broken'"));
57+
}
58+
finally
59+
{
60+
Trace.Listeners.Remove(listener);
61+
listener.Dispose();
62+
FontFeatureSettings.DiagnosticsSwitch.Level = previousLevel;
63+
}
64+
}
65+
66+
[Test]
67+
public void Parse_AcceptsCustomPrintableAsciiTags()
68+
{
69+
var settings = FontFeatureSettings.Parse("!abc=1,a\"b\\=2").ToArray();
70+
71+
Assert.That(settings.Select(setting => setting.ToString()),
72+
Is.EqualTo(new[] { "!abc=1", "a\"b\\=2" }));
73+
}
74+
3675
[Test]
3776
public void Normalize_ReturnsDeterministicRendererNeutralString()
3877
{
@@ -50,5 +89,12 @@ public void NormalizePreservingLegacy_NormalizesOpenTypeFeatures()
5089
{
5190
Assert.That(FontFeatureSettings.NormalizePreservingLegacy(" smcp = 1, kern=0 "), Is.EqualTo("kern=0,smcp=1"));
5291
}
92+
93+
[Test]
94+
public void NormalizePreservingLegacy_NormalizesOpenTypeFeaturesThatStartWithPunctuation()
95+
{
96+
Assert.That(FontFeatureSettings.NormalizePreservingLegacy(" !abc = 1, kern=0 "),
97+
Is.EqualTo("!abc=1,kern=0"));
98+
}
5399
}
54100
}

Src/FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ public class FontFeaturesButton : Button
4040
private string m_fontName; // The font for which we are editing the features.
4141
private string m_fontFeatures; // The font feature string stored in the writing system.
4242
private IFontFeatureProvider m_featureProvider;
43+
private static readonly TraceSwitch s_openTypeTraceSwitch =
44+
new TraceSwitch("FontFeatures.OpenType", "OpenType font feature discovery and provider selection", "Off");
4345
private ILgWritingSystemFactory m_wsf;
4446
private int[] m_values; // The actual list of values we're editing.
4547
private int[] m_ids; // The corresponding ids.
4648
private bool m_isGraphiteFont;
4749
private bool m_hasFontFeatures;
48-
private bool m_useGraphiteFeatures = true;
50+
private bool m_useGraphiteFeatures;
4951
#endregion
5052

5153
#region Constructor and dispose stuff
@@ -376,28 +378,42 @@ public void SetupFontFeatures()
376378
{
377379
var graphiteProvider = CreateGraphiteProvider(hdg);
378380
m_isGraphiteFont = graphiteProvider != null;
381+
var openTypeProvider = OpenTypeFontFeatureProvider.Create(hdg.Hdc);
379382

380-
if (m_useGraphiteFeatures && graphiteProvider != null && graphiteProvider.HasFeatures)
381-
{
382-
m_featureProvider = graphiteProvider;
383-
m_hasFontFeatures = true;
384-
Enabled = true;
383+
var primaryProvider = m_useGraphiteFeatures
384+
? (IFontFeatureProvider)graphiteProvider
385+
: openTypeProvider;
386+
var secondaryProvider = m_useGraphiteFeatures
387+
? openTypeProvider
388+
: (IFontFeatureProvider)graphiteProvider;
389+
390+
if (TrySelectFeatureProvider(primaryProvider))
385391
return;
386-
}
387392

388-
var openTypeProvider = OpenTypeFontFeatureProvider.Create(hdg.Hdc);
389-
if (openTypeProvider != null && openTypeProvider.HasFeatures)
390-
{
391-
m_featureProvider = openTypeProvider;
392-
m_hasFontFeatures = true;
393-
Enabled = true;
393+
if (TrySelectFeatureProvider(secondaryProvider))
394394
return;
395-
}
396395

397396
Enabled = false;
398397
}
399398
}
400399

400+
private bool TrySelectFeatureProvider(IFontFeatureProvider provider)
401+
{
402+
if (provider == null || !provider.HasFeatures)
403+
return false;
404+
405+
m_featureProvider = provider;
406+
m_hasFontFeatures = true;
407+
Enabled = true;
408+
Trace.WriteLineIf(s_openTypeTraceSwitch.TraceInfo,
409+
string.Format(CultureInfo.InvariantCulture,
410+
"FontFeaturesButton selected {0} provider for '{1}'.",
411+
provider.ProviderName,
412+
m_fontName ?? string.Empty),
413+
s_openTypeTraceSwitch.DisplayName);
414+
return true;
415+
}
416+
401417
private IFontFeatureProvider CreateGraphiteProvider(HoldDummyGraphics hdg)
402418
{
403419
IRenderEngine renderer = GraphiteEngineClass.Create();
@@ -849,6 +865,7 @@ private void ItemClickHandler(Object sender, EventArgs e)
849865

850866
private interface IFontFeatureProvider
851867
{
868+
string ProviderName { get; }
852869
bool HasFeatures { get; }
853870
int[] GetFeatureIds();
854871
string GetFeatureTag(int featureId);
@@ -885,6 +902,11 @@ public bool HasFeatures
885902
get { return m_featureIds.Length > 0; }
886903
}
887904

905+
public string ProviderName
906+
{
907+
get { return "Graphite"; }
908+
}
909+
888910
public int[] GetFeatureIds()
889911
{
890912
return m_featureIds.ToArray();
@@ -964,6 +986,11 @@ public bool HasFeatures
964986
get { return m_featureIds.Length > 0; }
965987
}
966988

989+
public string ProviderName
990+
{
991+
get { return "OpenType"; }
992+
}
993+
967994
public int[] GetFeatureIds()
968995
{
969996
return m_featureIds.ToArray();
@@ -1022,6 +1049,14 @@ internal static class OpenTypeFontFeatureReader
10221049
private const uint GdiError = 0xFFFFFFFF;
10231050
private const int MaxCacheEntries = 32;
10241051
private const int ObjFont = 6;
1052+
private static readonly HashSet<string> s_nonUserConfigurableTags =
1053+
new HashSet<string>(StringComparer.Ordinal)
1054+
{
1055+
"abvf", "abvm", "abvs", "akhn", "blwf", "blwm", "blws", "ccmp",
1056+
"cjct", "curs", "dist", "fina", "haln", "half", "init", "isol",
1057+
"ljmo", "locl", "mark", "medi", "mkmk", "nukt", "pref", "pres",
1058+
"pstf", "psts", "rclt", "rkrf", "rlig", "tjmo", "vjmo"
1059+
};
10251060
private static readonly uint[] s_layoutTables = { MakeTableTag("GSUB"), MakeTableTag("GPOS") };
10261061
private static readonly object s_cacheLock = new object();
10271062
private static readonly Dictionary<FontFeatureCacheKey, string[]> s_featureTagCache =
@@ -1130,11 +1165,24 @@ private static void ReadFeatureList(byte[] tableData, ISet<string> tags)
11301165
return;
11311166

11321167
var tag = System.Text.Encoding.ASCII.GetString(tableData, recordOffset, 4);
1133-
if (FontFeatureSettings.IsValidOpenTypeTag(tag))
1168+
if (FontFeatureSettings.IsValidOpenTypeTag(tag) && IsUserConfigurableTag(tag))
11341169
tags.Add(tag);
11351170
}
11361171
}
11371172

1173+
private static bool IsUserConfigurableTag(string tag)
1174+
{
1175+
if (!s_nonUserConfigurableTags.Contains(tag))
1176+
return true;
1177+
1178+
Trace.WriteLineIf(s_openTypeTraceSwitch.TraceInfo,
1179+
string.Format(CultureInfo.InvariantCulture,
1180+
"FontFeaturesButton filtered non-user OpenType feature '{0}'.",
1181+
tag),
1182+
s_openTypeTraceSwitch.DisplayName);
1183+
return false;
1184+
}
1185+
11381186
private static ushort ReadUInt16(byte[] data, int offset)
11391187
{
11401188
return (ushort)((data[offset] << 8) | data[offset + 1]);

Src/FwCoreDlgs/FwCoreDlgControls/FwCoreDlgControls.resx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -566,4 +566,4 @@
566566
<data name="ksIllegalXmlChars" xml:space="preserve">
567567
<value>Characters with hexadecimal values 0x0 through 0x1f are illegal in XML documents and have been removed.</value>
568568
</data>
569-
</root>
569+
</root>

0 commit comments

Comments
 (0)