Skip to content

Commit ce1bd75

Browse files
committed
Add word generation font options
1 parent f60173a commit ce1bd75

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Linq;
99
using System.Text.RegularExpressions;
1010
using System.Xml;
11+
using DocumentFormat.OpenXml;
12+
using DocumentFormat.OpenXml.Wordprocessing;
1113
using NUnit.Framework;
1214
using SIL.FieldWorks.Common.Framework;
1315
using SIL.FieldWorks.Common.FwUtils;
@@ -19,6 +21,7 @@
1921
using SIL.TestUtilities;
2022
using XCore;
2123
using static SIL.FieldWorks.XWorks.LcmWordGenerator;
24+
using W14 = DocumentFormat.OpenXml.Office2010.Word;
2225
// ReSharper disable StringLiteralTypo
2326

2427
namespace SIL.FieldWorks.XWorks
@@ -59,6 +62,7 @@ static LcmWordGeneratorTests()
5962
WordNamespaceManager.AddNamespace("w", openXmlSchema);
6063
WordNamespaceManager.AddNamespace("r", openXmlSchema);
6164
WordNamespaceManager.AddNamespace("wp", openXmlSchema);
65+
WordNamespaceManager.AddNamespace("w14", "http://schemas.microsoft.com/office/word/2010/wordml");
6266
}
6367

6468
[OneTimeSetUp]
@@ -210,6 +214,62 @@ public void Setup()
210214
DefaultSettings.StylesGenerator.AddGlobalStyles(null, new ReadOnlyPropertyTable(m_propertyTable));
211215
}
212216

217+
[Test]
218+
public void GenerateCharacterStyleFromLcmStyleSheet_OpenTypeFontFeatures_AddsWordTypographyProperties()
219+
{
220+
var styleName = "WordFeatureStyle" + Guid.NewGuid().ToString("N");
221+
var fontInfo = new FontInfo { m_features = { ExplicitValue = "liga=0,lnum=1,pnum=1,calt=0,ss02=0,cv01=2" } };
222+
var projectStyle = new TestStyle(fontInfo, Cache) { Name = styleName, IsParagraphStyle = false };
223+
FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable).Styles.Add(projectStyle);
224+
225+
var style = WordStylesGenerator.GenerateCharacterStyleFromLcmStyleSheet(styleName, Cache.DefaultVernWs,
226+
new ReadOnlyPropertyTable(m_propertyTable));
227+
228+
var runProps = style.GetFirstChild<StyleRunProperties>();
229+
AssertWordTypographyProperties(runProps, W14.LigaturesValues.None, W14.NumberFormValues.Lining,
230+
W14.NumberSpacingValues.Proportional, false, 2U, false);
231+
}
232+
233+
[Test]
234+
public void GetExplicitFontProperties_OpenTypeFontFeatures_AddsWordTypographyProperties()
235+
{
236+
var fontInfo = new FontInfo { m_features = { ExplicitValue = "liga=1,clig=1,onum=1,tnum=1,calt=1,ss03=1,cv01=2" } };
237+
238+
var runProps = WordStylesGenerator.GetExplicitFontProperties(fontInfo);
239+
240+
AssertWordTypographyProperties(runProps, W14.LigaturesValues.StandardContextual, W14.NumberFormValues.OldStyle,
241+
W14.NumberSpacingValues.Tabular, true, 3U, true);
242+
}
243+
244+
private static void AssertWordTypographyProperties(OpenXmlCompositeElement runProps,
245+
W14.LigaturesValues ligaturesValue, W14.NumberFormValues numberFormValue,
246+
W14.NumberSpacingValues numberSpacingValue, bool contextualAlternativesValue,
247+
uint stylisticSetId, bool stylisticSetValue)
248+
{
249+
Assert.That(runProps, Is.Not.Null);
250+
var ligatures = runProps.GetFirstChild<W14.Ligatures>();
251+
Assert.That(ligatures, Is.Not.Null);
252+
Assert.That(ligatures.Val.Value, Is.EqualTo(ligaturesValue));
253+
254+
var numberForm = runProps.GetFirstChild<W14.NumberingFormat>();
255+
Assert.That(numberForm, Is.Not.Null);
256+
Assert.That(numberForm.Val.Value, Is.EqualTo(numberFormValue));
257+
258+
var numberSpacing = runProps.GetFirstChild<W14.NumberSpacing>();
259+
Assert.That(numberSpacing, Is.Not.Null);
260+
Assert.That(numberSpacing.Val.Value, Is.EqualTo(numberSpacingValue));
261+
262+
var contextualAlternatives = runProps.GetFirstChild<W14.ContextualAlternatives>();
263+
Assert.That(contextualAlternatives, Is.Not.Null);
264+
Assert.That(contextualAlternatives.Val.Value, Is.EqualTo(contextualAlternativesValue));
265+
266+
var stylisticSets = runProps.GetFirstChild<W14.StylisticSets>();
267+
Assert.That(stylisticSets, Is.Not.Null);
268+
var styleSet = stylisticSets.Elements<W14.StyleSet>().Single();
269+
Assert.That(styleSet.Id.Value, Is.EqualTo(stylisticSetId));
270+
Assert.That(styleSet.Val.Value, Is.EqualTo(stylisticSetValue));
271+
}
272+
213273

214274
[Test]
215275
public void GenerateWordDocForEntry_OneSenseWithGlossGeneratesCorrectResult()

openspec/changes/add-opentype-font-features/design.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ The longer product phases are: add OpenType features now, remove Graphite later
7272

7373
**Rationale:** Golden WinForms/Views captures help Phase 1 verification and later Avalonia comparison. Exact pixels are appropriate for same-renderer regressions; tolerant or semantic comparisons are appropriate across GDI/Uniscribe and Skia/HarfBuzz.
7474

75+
### 7. Word DOCX export maps only documented Word typography features
76+
77+
**Decision:** Map FieldWorks `tag=value` font feature strings to Office 2010 WordprocessingML `w14` typography elements only where Microsoft documents a Word representation: ligatures, number form, number spacing, contextual alternatives, and stylistic sets.
78+
79+
**Rationale:** CSS can preserve arbitrary OpenType feature tags, but WordprocessingML does not provide a general `font-feature-settings` equivalent. A best-effort documented subset avoids producing invalid DOCX while preserving the features Word can actually display and round-trip.
80+
81+
**Alternatives considered:** Store arbitrary tags in custom XML or undocumented extension markup. Rejected because Word would not apply those settings to text rendering and the export would give users a false parity signal.
82+
7583
## Risks / Trade-offs
7684

7785
| Risk | Mitigation |
@@ -82,6 +90,7 @@ The longer product phases are: add OpenType features now, remove Graphite later
8290
| OpenType feature labels are incomplete or unlocalized | Use resource-backed labels for common tags and fall back to the four-character tag. |
8391
| Test fonts cannot be redistributed | Confirm SIL Open Font License or another redistributable license before adding binaries. |
8492
| HarfBuzz/Skia visual output differs from GDI/Uniscribe | Compare shaping data first; use tolerant image comparisons for cross-renderer evidence. |
93+
| Word DOCX cannot represent every OpenType feature tag | Map only documented `w14` typography elements and document unsupported tags such as character variants and private features. |
8594

8695
## Migration Plan
8796

@@ -92,6 +101,7 @@ The longer product phases are: add OpenType features now, remove Graphite later
92101
5. Add render snapshot scenarios using the merged render baseline infrastructure.
93102
6. Add test-only HarfBuzzSharp + SkiaSharp comparison tests in FieldWorks test projects.
94103
7. Update help/localized UI text.
104+
8. Add Word DOCX export mapping for the documented WordprocessingML subset and tests that inspect generated Open XML.
95105

96106
Rollback strategy: disable the OpenType provider and native OpenType shaping path behind a feature flag or fallback path if regressions are found; Graphite and old Uniscribe behavior remain available.
97107

openspec/changes/add-opentype-font-features/proposal.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ LT-22324 requires FieldWorks to split Font Features from the Graphite-only UI an
1111
- Update render/cache invalidation rules so feature changes are treated as layout-changing, especially after `001-render-speedup` is merged.
1212
- Add UI/component tests for font-feature controls and high-level visual rendering tests proving feature settings change output.
1313
- Add a test-only HarfBuzzSharp + SkiaSharp comparison path for shaping/rendering confidence toward future Avalonia migration; this path is not a production renderer in Phase 1.
14+
- Add Word DOCX export support for the subset of OpenType font features that Microsoft WordprocessingML can represent, and document unsupported feature tags.
1415
- Document research for later phases: Graphite removal while retaining WinForms, Avalonia alongside WinForms, and eventual WinForms retirement.
1516

1617
## Non-goals
@@ -39,5 +40,6 @@ LT-22324 requires FieldWorks to split Font Features from the Graphite-only UI an
3940
- **Managed rendering bridge:** `Src/Common/SimpleRootSite/RenderEngineFactory.cs` and post-`001-render-speedup` render/cache invalidation paths.
4041
- **Native C++ Views:** `Src/views/lib/UniscribeEngine.cpp`, `UniscribeSegment.cpp`, `Render.idh` only through additive interfaces if needed, and existing Graphite code for regression coverage.
4142
- **Tests:** FwCore dialog/control tests, SimpleRootSite/render-factory tests, native Views tests, and post-`001-render-speedup` render baseline/snapshot tests.
43+
- **Word DOCX export:** `Src/xWorks/WordStylesGenerator.cs`, configured dictionary/reversal DOCX tests in `Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs`, and OpenType export documentation.
4244
- **Test-only dependencies:** HarfBuzzSharp + SkiaSharp in test/comparison projects only.
4345
- **Documentation/help:** FieldWorks Help and localized UI text for the renamed Font Features/Font Options surfaces.

openspec/changes/add-opentype-font-features/research.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,24 @@ Useful references:
110110
- Test-only comparison: HarfBuzzSharp and SkiaSharp remain isolated to `RenderComparisonTests`. HarfBuzzSharp is used only as a test comparison path for shaping data; production rendering remains Uniscribe/Graphite.
111111
- Export audit: CSS already emits `font-feature-settings` and is covered by `GenerateCssForConfiguration_CharStyleFontFeaturesWorks`. Notebook export preserves writing-system `DefaultFontFeatures`. `WordStylesGenerator` did not show a feature-string mapping and should be tracked separately if Word export parity is required.
112112
- Help/docs: no existing FieldWorks help source for Font Options was found in this workspace. Phase 1 adds `Docs/opentype-font-features.md` to document the UI, storage model, temporary Graphite role, and export status.
113+
114+
## Word DOCX Export Analysis
115+
116+
Microsoft Word support for OpenType features is exposed in DOCX through a fixed Office 2010 WordprocessingML typography subset, not through an arbitrary CSS-style `font-feature-settings` property. The relevant Open XML SDK classes live under `DocumentFormat.OpenXml.Office2010.Word` and serialize into the `w14` namespace (`http://schemas.microsoft.com/office/word/2010/wordml`).
117+
118+
Authoritative references gathered for the implementation:
119+
120+
- Microsoft Support: Publisher/Office typography UI covers number styles, ligatures, stylistic sets, swash, stylistic alternates, true small caps, and font-dependent OpenType availability: https://support.microsoft.com/en-us/office/use-typographic-styles-to-increase-the-impact-of-your-publication-10e14096-452f-4d3b-9938-1d537572a377
121+
- Microsoft Support: Word compatibility notes identify ligatures, stylistic sets, contextual alternative characters, font-based kerning, and number forms/spacing as advanced typography features that may be preserved even when older Word versions do not display them: https://support.microsoft.com/en-us/office/about-ligatures-and-compatibility-64ffd007-6e5c-4d38-b87d-0935f37714fe
122+
- OpenType feature tag registry and definitions: https://learn.microsoft.com/en-us/typography/opentype/spec/featuretags, plus registered descriptions for `calt`, `clig`, `cvXX`, `kern`, `liga`, `lnum`, `onum`, `pnum`, `smcp`, `ss01`-`ss20`, and `tnum`.
123+
- Open XML SDK classes: `Ligatures` (`w14:ligatures`), `NumberingFormat` (`w14:numForm`), `NumberSpacing` (`w14:numSpacing`), `ContextualAlternatives` (`w14:cntxtAlts`), `StylisticSets` (`w14:stylisticSets`), and `StyleSet` (`w14:styleSet`).
124+
125+
Planned DOCX subset:
126+
127+
- `liga`, `clig`, `hlig`, and `dlig` map to the aggregate `w14:ligatures` value.
128+
- `lnum` and `onum` map to `w14:numForm` values `lining` and `oldStyle`.
129+
- `pnum` and `tnum` map to `w14:numSpacing` values `proportional` and `tabular`.
130+
- `calt` maps to `w14:cntxtAlts`.
131+
- `ss01` through `ss20` map to `w14:stylisticSets/w14:styleSet` with ids 1 through 20.
132+
133+
Unsupported tags such as `cv01`-`cv99`, `smcp`, `c2sc`, `kern`, `salt`, `swsh`, and private/vendor tags do not have a documented arbitrary WordprocessingML feature-tag representation. They should be ignored by Word export while remaining valid for rendering and CSS export where those paths can consume them.

openspec/changes/add-opentype-font-features/specs/font-feature-settings/spec.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,22 @@ FieldWorks SHALL update user-visible strings and help so Font Features are descr
9696
#### Scenario: Help covers OpenType features
9797
- **WHEN** a user opens the relevant FieldWorks Help topic
9898
- **THEN** the Help content SHALL describe OpenType Font Features and their relationship to Graphite during Phase 1
99+
100+
### Requirement: Word DOCX export preserves supported OpenType font features
101+
FieldWorks SHALL export supported OpenType font feature settings into configured dictionary/reversal Word DOCX output using documented Microsoft WordprocessingML typography elements.
102+
103+
#### Scenario: Character style features are exported to Word typography properties
104+
- **WHEN** a configured Word DOCX export includes a character style with supported OpenType feature settings
105+
- **THEN** the generated Word style run properties SHALL include the equivalent Office 2010 `w14` typography elements for supported features
106+
107+
#### Scenario: Explicit run features are exported to Word typography properties
108+
- **WHEN** direct run font properties include supported OpenType feature settings
109+
- **THEN** generated run properties SHALL include the equivalent Office 2010 `w14` typography elements for supported features
110+
111+
#### Scenario: Unsupported feature tags do not break Word export
112+
- **WHEN** a feature string contains tags that WordprocessingML cannot represent, such as `cv01`, `smcp`, or private feature tags
113+
- **THEN** Word export SHALL ignore those unsupported tags without failing the export or removing supported tags from the same feature string
114+
115+
#### Scenario: Word export uses a documented subset
116+
- **WHEN** documentation describes Word export behavior
117+
- **THEN** it SHALL list the supported WordprocessingML subset and identify arbitrary CSS-style feature tags as unsupported by DOCX export

openspec/changes/add-opentype-font-features/tasks.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,11 @@
8080
- [x] 10.4 Add `RenderEngineFactoryTests` coverage proving writing-system default OpenType features are normalized into `LgCharRenderProps.szFontVar`, equivalent feature strings reuse the renderer cache entry, and different feature strings create separate renderer cache entries. [Managed C# Tests]
8181
- [x] 10.5 Run `./test.ps1 -TestProject SimpleRootSiteTests -StartedBy agent`; result: `Total tests: 113`, `Passed: 113`. [Validation]
8282
- [x] 10.6 Run `./test.ps1 -SkipManaged -TestProject TestViews -StartedBy agent`; result: `Tests [Ok-Fail-Error]: [295-0-0]`. [Validation]
83+
84+
## 11. Word DOCX Export Parity
85+
86+
- [x] 11.1 Add OpenSpec requirements, research analysis, implementation plan, and tasks for Microsoft Word DOCX OpenType feature subset export. [OpenSpec]
87+
- [ ] 11.2 Add failing managed tests proving style and explicit run font features emit documented WordprocessingML `w14` typography elements. [Managed C# Tests]
88+
- [ ] 11.3 Implement a Word DOCX feature mapper that reuses the renderer-neutral parser and maps supported tags to `w14` elements. [Managed C#]
89+
- [ ] 11.4 Keep unsupported tags non-fatal and document Word export subset behavior in `Docs/opentype-font-features.md`. [Docs]
90+
- [ ] 11.5 Run targeted xWorks Word generator tests. [Validation]

0 commit comments

Comments
 (0)