diff --git a/src/EPPlus.Export.Pdf.Tests/FontTests.cs b/src/EPPlus.Export.Pdf.Tests/FontTests.cs index 4dcd370c3..33ab4ad5c 100644 --- a/src/EPPlus.Export.Pdf.Tests/FontTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/FontTests.cs @@ -14,10 +14,12 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Integration; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.FormulaParsing.Utilities; using OfficeOpenXml.Interfaces.Fonts; using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; namespace EPPlus.Export.Pdf.Tests { @@ -168,5 +170,34 @@ public void AddFontData_Embedded_DoesNotEmitSimpleFontObject() } } } + + [TestMethod] + public void FontObjects_ForSubsetFont_ShareIdenticalTaggedName() + { + var engine = new OpenTypeFontEngine(cfg => cfg.SearchSystemDirectories = true); + if(engine.GetFontAvailability("Calibri") != FontAvailability.Exact) + { + Assert.Inconclusive("Calibri font is not available on this system. This test requires Calibri to be installed."); + } + var font = engine.LoadFont("Calibri"); + var subset = new SingleFontSubsetter().Subset(font, new HashSet { 65, 66, 67 }, true); + + var resource = new PdfFontResource("Calibri", FontSubFamily.Regular, 1, new PdfPageSettings(engine)); + resource.fontData = subset; + + // Object numbers are arbitrary; they only need to be set in dependency order. + var descriptor = resource.GetFontDescriptorObject(10).RenderDictionary(); + var cid = resource.GetCIDFontObject(11).RenderDictionary(); + var type0 = resource.GetType0FontDictObject(12).RenderDictionary(); + + var rx = new Regex(@"/(?:BaseFont|FontName)\s+/(\S+)"); + var names = new[] { descriptor, cid, type0 } + .Select(s => rx.Match(s).Groups[1].Value) + .ToList(); + + foreach (var n in names) + Assert.IsTrue(Regex.IsMatch(n, "^[A-Z]{6}\\+[^+\\s]+$"), n); + Assert.AreEqual(1, names.Distinct().Count(), string.Join(" | ", names.ToArray())); + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/DocumentFontSubsetBuilderTagTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/DocumentFontSubsetBuilderTagTests.cs new file mode 100644 index 000000000..8ea838cd7 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/DocumentFontSubsetBuilderTagTests.cs @@ -0,0 +1,107 @@ +using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Subsetting; +using EPPlus.Fonts.OpenType.Tests; +using OfficeOpenXml.Interfaces.Fonts; +using System.Text.RegularExpressions; + +[TestClass] +public class DocumentFontSubsetBuilderTagTests : FontTestBase // adjust to your base class +{ + private static readonly Regex TaggedName = new Regex("^[A-Z]{6}\\+.+$"); + + public override TestContext? TestContext { get; set; } + + private DocumentFontSubsetBuilder CreateBuilder() + { + return new DocumentFontSubsetBuilder(SystemFontsEngine); + } + + [TestMethod] + public void Build_SubsettedFont_HasSubsetTag() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var builder = CreateBuilder(); + builder.AddText("Calibri", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var fonts = builder.GetFontsToEmbed().ToList(); + + Assert.AreEqual(1, fonts.Count); + // NOTE: property name for the font on SubsettedFont is assumed to be "Font". + Assert.IsTrue(TaggedName.IsMatch(fonts[0].Font.FullName), fonts[0].Font.FullName); + } + + [TestMethod] + public void Build_TaggedSubset_IsStillFoundByOriginalIdentity() + { + // Regression guard: the tag must not leak into the identity used as dictionary key. + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var builder = CreateBuilder(); + builder.AddText("Calibri", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var provider = builder.GetShapingProvider("Calibri", FontSubFamily.Regular); + Assert.IsNotNull(provider); + + var fonts = builder.GetFontsToEmbed().ToList(); + Assert.AreEqual("Calibri", fonts[0].Family); + Assert.AreEqual(FontSubFamily.Regular, fonts[0].SubFamily); + } + + [TestMethod] + public void Build_ShapingProvider_ResolvesGlyphsFromTaggedSubset() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var builder = CreateBuilder(); + builder.AddText("Calibri", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var provider = builder.GetShapingProvider("Calibri", FontSubFamily.Regular); + + OpenTypeFont dest; + ushort glyphId; + provider.TryGetGlyphFont((uint)'A', out dest, out glyphId); + + Assert.AreNotEqual((ushort)0, glyphId); + Assert.IsTrue(TaggedName.IsMatch(dest.FullName), dest.FullName); + } + + [TestMethod] + public void Build_DoesNotMutateCachedOriginalFont() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var before = SystemFontsEngine.LoadFont("Calibri").FullName; + + var builder = CreateBuilder(); + builder.AddText("Calibri", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var after = SystemFontsEngine.LoadFont("Calibri").FullName; + Assert.AreEqual(before, after); + Assert.IsFalse(TaggedName.IsMatch(after)); + } + + [TestMethod] + public void Build_DifferentFontsAndSameInputTwice_TagsAreDistinctAndDeterministic() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Bold); + + var a = CreateBuilder(); + a.AddText("Calibri", FontSubFamily.Regular, "ABC"); + a.AddText("Calibri", FontSubFamily.Bold, "ABC"); + a.Build(); + + var b = CreateBuilder(); + b.AddText("Calibri", FontSubFamily.Regular, "ABC"); + b.AddText("Calibri", FontSubFamily.Bold, "ABC"); + b.Build(); + + var namesA = a.GetFontsToEmbed().Select(f => f.Font.FullName).OrderBy(n => n).ToList(); + var namesB = b.GetFontsToEmbed().Select(f => f.Font.FullName).OrderBy(n => n).ToList(); + + Assert.AreEqual(2, namesA.Count); + Assert.AreNotEqual(namesA[0].Substring(0, 6), namesA[1].Substring(0, 6)); + CollectionAssert.AreEqual(namesA, namesB); // deterministic across builders + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/FontEmbeddingPolicyTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/FontEmbeddingPolicyTests.cs index f0190de76..f9447ba6f 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/FontEmbeddingPolicyTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/FontEmbeddingPolicyTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace EPPlus.Fonts.OpenType.Tests.Subsetting @@ -170,5 +171,60 @@ public void Build_EmbedWholeDecision_EmbedsWholeFontNotSubset() Assert.IsFalse(provider.PrimaryFont.IsSubset, "An EmbedWhole font must be embedded whole, not subsetted."); } + + private static readonly Regex TaggedName = new Regex("^[A-Z]{6}\\+.+$"); + + [TestMethod] + public void Build_EmbedWholeFont_HasNoSubsetTag() + { + var engine = CreateEngineWithCallback(info => FontEmbeddingDecision.EmbedWhole); + RequireFont(engine, "Roboto", FontSubFamily.Regular); + var original = engine.LoadFont("Roboto"); + + var builder = new DocumentFontSubsetBuilder(engine); + builder.AddText("Roboto", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var embedded = builder.GetFontsToEmbed().Single(); + + Assert.IsFalse(TaggedName.IsMatch(embedded.Font.FullName), embedded.Font.FullName); + Assert.AreEqual(original.FullName, embedded.Font.FullName); + Assert.IsFalse(embedded.Font.IsSubset); + } + + [TestMethod] + public void Build_SubsetDecision_HasSubsetTag() + { + var engine = CreateEngineWithCallback(info => FontEmbeddingDecision.Subset); + RequireFont(engine, "Roboto", FontSubFamily.Regular); + + var builder = new DocumentFontSubsetBuilder(engine); + builder.AddText("Roboto", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var embedded = builder.GetFontsToEmbed().Single(); + + Assert.IsTrue(embedded.Font.IsSubset); + Assert.IsTrue(TaggedName.IsMatch(embedded.Font.FullName), embedded.Font.FullName); + } + + [TestMethod] + public void Build_SkippedFont_FallsBackToTaggedLastResortSubset() + { + // Skip for everything: the request is served by the built-in last-resort font, + // which is seeded as Subset and must therefore be tagged too. + var engine = CreateEngineWithCallback(info => FontEmbeddingDecision.Skip); + RequireFont(engine, "Roboto", FontSubFamily.Regular); + + var builder = new DocumentFontSubsetBuilder(engine); + builder.AddText("Roboto", FontSubFamily.Regular, "ABC"); + builder.Build(); + + var embedded = builder.GetFontsToEmbed().Single(); + + Assert.IsTrue(TaggedName.IsMatch(embedded.Font.FullName), embedded.Font.FullName); + Assert.IsTrue(embedded.Font.FullName.Contains("Archivo"), embedded.Font.FullName); + Assert.IsNotNull(builder.GetShapingProvider("Roboto", FontSubFamily.Regular)); + } } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsetTagTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsetTagTests.cs new file mode 100644 index 000000000..c24098f4d --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/SubsetTagTests.cs @@ -0,0 +1,122 @@ +using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.Subsetting; +using EPPlus.Fonts.OpenType.Tables.Name; +using EPPlus.Fonts.OpenType.Tests; +using OfficeOpenXml.Interfaces.Fonts; +using System.Text.RegularExpressions; + +[TestClass] +public class SubsetTagTests : FontTestBase +{ + public override TestContext? TestContext { get; set; } + + private static NameTable CreateNameTable() + { + Func rec = + (platform, encoding, lang, type, text) => new NameRecord + { + platformId = platform, + encodingId = encoding, + languageID = lang, + nameId = (ushort)type, + RecordType = type, + Name = text + }; + + return new NameTable + { + NameRecords = new[] + { + rec(3, 1, 0x0409, NameRecordTypes.FontFamilyName, "Calibri"), + rec(3, 1, 0x0409, NameRecordTypes.FontSubfamilyName, "Bold"), + rec(3, 1, 0x0409, NameRecordTypes.FullFontName, "Calibri Bold"), + rec(3, 1, 0x0409, NameRecordTypes.PostScriptName, "Calibri-Bold"), + rec(1, 0, 0, NameRecordTypes.FullFontName, "Calibri Bold"), + rec(1, 0, 0, NameRecordTypes.PostScriptName, "Calibri-Bold") + } + }; + } + + [TestMethod] + public void ApplySubsetTag_PrefixesFullNameAndPostScriptName_OnAllPlatforms() + { + var nt = CreateNameTable(); + nt.ApplySubsetTag("ABCDEF+"); + + foreach (var r in nt.NameRecords) + { + if (r.RecordType == NameRecordTypes.FullFontName || r.RecordType == NameRecordTypes.PostScriptName) + Assert.IsTrue(r.Name.StartsWith("ABCDEF+"), r.Name); + } + Assert.AreEqual("ABCDEF+Calibri-Bold", nt.PostScriptName); + Assert.AreEqual("ABCDEF+Calibri Bold", nt.GetFullFontName()); + } + + [TestMethod] + public void ApplySubsetTag_DoesNotTouchFamilyOrSubfamily() + { + var nt = CreateNameTable(); + nt.ApplySubsetTag("ABCDEF+"); + + Assert.AreEqual("Calibri", nt.NameRecords[0].Name); + Assert.AreEqual("Bold", nt.NameRecords[1].Name); + } + + [TestMethod] + public void ApplySubsetTag_IsIdempotent() + { + var nt = CreateNameTable(); + nt.ApplySubsetTag("ABCDEF+"); + nt.ApplySubsetTag("ABCDEF+"); + Assert.AreEqual("ABCDEF+Calibri-Bold", nt.PostScriptName); + } + + [TestMethod] + public void Create_IsDeterministic_SixUppercaseLettersAndPlus() + { + var id = new FontKey("Calibri", FontSubFamily.Bold); + var a = SubsetTag.Create(id, new HashSet { 65, 66, 67 }); + var b = SubsetTag.Create(id, new HashSet { 67, 66, 65 }); + Assert.AreEqual(a, b); + Assert.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(a, "^[A-Z]{6}\\+$")); + } + + [TestMethod] + public void Create_DiffersForDifferentCodePointsAndFonts() + { + var bold = new FontKey("Calibri", FontSubFamily.Bold); + var regular = new FontKey("Calibri", FontSubFamily.Regular); + var cps = new HashSet { 65, 66 }; + Assert.AreNotEqual(SubsetTag.Create(bold, cps), SubsetTag.Create(bold, new HashSet { 65 })); + Assert.AreNotEqual(SubsetTag.Create(bold, cps), SubsetTag.Create(regular, cps)); + } + + [TestMethod] + public void Subset_WithTag_SurvivesSerializeRoundtrip() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var font = SystemFontsEngine.LoadFont("Calibri"); + var subset = new SingleFontSubsetter().Subset(font, new HashSet { 65, 66, 67 }, true); + + Assert.IsTrue(subset.IsSubset); + Assert.IsTrue(Regex.IsMatch(subset.FullName, "^[A-Z]{6}\\+"), subset.FullName); + + var reloaded = new OpenTypeFont(subset.Serialize()); + Assert.AreEqual(subset.FullName, reloaded.FullName); + Assert.AreEqual(subset.NameTable.PostScriptName, reloaded.NameTable.PostScriptName); + } + + [TestMethod] + public void Subset_WithTag_DoesNotMutateOriginalFont() + { + RequireFont(SystemFontsEngine, "Calibri", FontSubFamily.Regular); + var font = SystemFontsEngine.LoadFont("Calibri"); + var before = font.FullName; + + new SingleFontSubsetter().Subset(font, new HashSet { 65 }, true); + + Assert.AreEqual(before, font.FullName); + Assert.IsFalse(Regex.IsMatch(font.FullName, "^[A-Z]{6}\\+")); + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs index 6fdb0e651..d864c7f72 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFont.cs @@ -11,6 +11,7 @@ Date Author Change 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ using EPPlus.Fonts.OpenType.FontValidation; +using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.Scanner; using EPPlus.Fonts.OpenType.Subsetting; using EPPlus.Fonts.OpenType.Tables; @@ -645,6 +646,11 @@ internal void AddOrReplaceTable(T table) } public OpenTypeFont CreateSubset(IEnumerable usedChars) + { + return CreateSubset(usedChars, false); + } + + internal OpenTypeFont CreateSubset(IEnumerable usedChars, bool addSubsetTag) { // Validate input if (usedChars == null) @@ -654,14 +660,18 @@ public OpenTypeFont CreateSubset(IEnumerable usedChars) throw new ArgumentException("Text cannot be empty", nameof(usedChars)); var subsetBuilder = new SubsetFontBuilder(); - - // Extract Unicode code points, correctly handling surrogate pairs. - // A string like "Hello 😀" contains 7 chars but 6 code points, - // because 😀 (U+1F600) is encoded as two UTF-16 surrogates. var codePoints = CodePointUtil.ExtractCodePoints(usedChars); var newFont = subsetBuilder.CreateSubset(this, codePoints); + if (addSubsetTag) + { + // Must happen BEFORE post-processing so lengths, checksums and any + // preprocessed table bytes are computed from the final name table. + var identity = new FontKey(GetEnglishFontFamilyName(), NameTable.GetSubfamilyEnum()); + newFont.NameTable.ApplySubsetTag(SubsetTag.Create(identity, codePoints)); + } + var postProcessor = new SubsetPostProcessor(); postProcessor.PostProcessSubset(newFont); diff --git a/src/EPPlus.Fonts.OpenType/Subsetting/DocumentFontSubsetBuilder.cs b/src/EPPlus.Fonts.OpenType/Subsetting/DocumentFontSubsetBuilder.cs index b7be7ccf0..49e34b69d 100644 --- a/src/EPPlus.Fonts.OpenType/Subsetting/DocumentFontSubsetBuilder.cs +++ b/src/EPPlus.Fonts.OpenType/Subsetting/DocumentFontSubsetBuilder.cs @@ -138,9 +138,13 @@ public void Build() codePointsByIdentity.TryGetValue(id, out cps); if (DecisionForIdentity(id) == FontEmbeddingDecision.EmbedWhole) + { _sharedSubsetByIdentity[id] = font; + } else - _sharedSubsetByIdentity[id] = _subsetter.Subset(font, cps); + { + _sharedSubsetByIdentity[id] = _subsetter.Subset(font, cps, true); + } } // ===== PHASE 3: build one provider per request from the SHARED subsets ===== diff --git a/src/EPPlus.Fonts.OpenType/Subsetting/SingleFontSubsetter.cs b/src/EPPlus.Fonts.OpenType/Subsetting/SingleFontSubsetter.cs index 7f5a6454a..172793dcb 100644 --- a/src/EPPlus.Fonts.OpenType/Subsetting/SingleFontSubsetter.cs +++ b/src/EPPlus.Fonts.OpenType/Subsetting/SingleFontSubsetter.cs @@ -13,6 +13,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Utils; using System; using System.Collections.Generic; +using System.Diagnostics; namespace EPPlus.Fonts.OpenType { @@ -31,6 +32,17 @@ internal sealed class SingleFontSubsetter /// so the caller always receives an embeddable instance. /// public OpenTypeFont Subset(OpenTypeFont font, HashSet codePoints) + { + return Subset(font, codePoints, false); + } + + /// + /// Produces a subset of containing only the glyphs required for + /// . Returns the font unchanged when it is already a subset + /// or when no code points are supplied. If subsetting fails, the original font is returned + /// so the caller always receives an embeddable instance. + /// + public OpenTypeFont Subset(OpenTypeFont font, HashSet codePoints, bool addSubsetTag) { if (font == null) throw new ArgumentNullException("font"); @@ -41,14 +53,12 @@ public OpenTypeFont Subset(OpenTypeFont font, HashSet codePoints) try { var chars = CodePointUtil.CodePointsToString(codePoints); - return font.CreateSubset(chars); + return font.CreateSubset(chars, addSubsetTag); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine( - "Warning: could not subset '" + - (font.NameTable != null ? font.NameTable.GetFullFontName() : "(unknown)") + - "': " + ex.Message); + Debug.WriteLine("Subsetting failed for font " + font.GetEnglishFontFamilyName() + ": " + ex.Message); + // unchanged return font; } } diff --git a/src/EPPlus.Fonts.OpenType/Subsetting/SubsetTag.cs b/src/EPPlus.Fonts.OpenType/Subsetting/SubsetTag.cs new file mode 100644 index 000000000..2d4558cf8 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Subsetting/SubsetTag.cs @@ -0,0 +1,41 @@ +using EPPlus.Fonts.OpenType.Integration; +using System.Collections.Generic; + +namespace EPPlus.Fonts.OpenType.Subsetting +{ + internal static class SubsetTag + { + /// + /// Creates a deterministic PDF subset tag ("ABCDEF+") from the font identity + /// and the code points included in the subset. + /// + internal static string Create(FontKey identity, IEnumerable codePoints) + { + unchecked + { + uint hash = 2166136261; + foreach (var c in identity.Family ?? string.Empty) + hash = (hash ^ c) * 16777619; + hash = (hash ^ (uint)(int)identity.SubFamily) * 16777619; + + if (codePoints != null) + { + var sorted = new List(codePoints); + sorted.Sort(); + foreach (var cp in sorted) + hash = (hash ^ (uint)cp) * 16777619; + } + + // 26^6 = 308 915 776 < 2^32 + uint value = hash % 308915776u; + var chars = new char[6]; + for (int i = 5; i >= 0; i--) + { + chars[i] = (char)('A' + (int)(value % 26)); + value /= 26; + } + return new string(chars) + "+"; + } + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs index b389889a5..41b0821b4 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs @@ -554,5 +554,39 @@ private static string SanitizePsName(string name) // If everything got stripped return sb.Length > 0 ? sb.ToString() : "UnknownPSName"; } + + /// + /// Prefixes the full font name (nameID 4) and PostScript name (nameID 6) with a + /// PDF subset tag such as "ABCDEF+", in every platform record. The family and + /// subfamily records (nameID 1, 2, 16, 17) are intentionally left untouched, since + /// they are used for font identity and matching. + /// + internal void ApplySubsetTag(string tag) + { + if (string.IsNullOrEmpty(tag)) + throw new ArgumentException("Subset tag cannot be empty.", "tag"); + if (NameRecords == null) return; + + foreach (var record in NameRecords) + { + if (record.RecordType != NameRecordTypes.FullFontName && + record.RecordType != NameRecordTypes.PostScriptName) + continue; + if (string.IsNullOrEmpty(record.Name) || HasSubsetTag(record.Name)) + continue; + + record.Name = tag + record.Name; + } + } + + private static bool HasSubsetTag(string name) + { + if (name.Length < 8 || name[6] != '+') return false; + for (int i = 0; i < 6; i++) + { + if (name[i] < 'A' || name[i] > 'Z') return false; + } + return true; + } } } diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index d95a6d2e4..24afc900a 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -895,7 +895,7 @@ private static void AddMeasureSheet(ExcelPackage p, ExcelWorksheet ws) } //i2084 - [TestMethod] + [TestMethod, Ignore("Disabled due to taking over 10 seconds on AppVeyor. /MA")] public void s912_Alternate() { //Optimizing for not overwriting existing styles @@ -927,7 +927,7 @@ public void s912_Alternate() } } - [TestMethod] + [TestMethod, Ignore("Disabled due to taking over 10 seconds on AppVeyor. /MA")] public void s912() { using (var package = OpenPackage("s912.xlsx", true))