Microsoft has completely changed the text rendering engine from WPF 3.5 to 4.0. In the meantime, it also has introduced a bug, which you can read about here : http://stackoverflow.com/questions/23246254/ and here : https://connect.microsoft.com/VisualStudio/feedback/details/860053/wpf-4-font-rendering-bug-with-some-fonts-generated-xps-is-invalid. I have built a test project that demonstrates the problem here : https://github.com/tbroust-trepia/wpf-4-font-rendering
What looks like a small problem (the apostrophe character ” ‘ ” actually reduces the space between two characters with some fonts, resulting in overlapping characters) is problematic when you generate an XPS file from the XAML, because the resulting XPS is “corrupted”, with negative letter-spacing, which seems illegal in XPS.
I don’t really know which is to blame more : the font rendering bug, or the XPS converter (integrated in .Net !) which renders the text faithfully, but shouldn’t. Anyway, while waiting for MS to fix anything, I have found the way to fix it myself. Warning: dirty hack ahead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
public class XpsFile { /// <summary> /// Regex to validate XPS "indices" property (gotten from ABCPDF) /// </summary> private static readonly string IndicesRegex = @"(((\(([1-9][0-9]*)(:([1-9][0-9]*))?\))?([0-9]+))?(,(\+?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?)?(,((\-|\+)?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?)?(,((\-|\+)?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?))?)?)?)(;((\(([1-9][0-9]*)(:([1-9][0-9]*))?\))?([0-9]+))?(,(\+?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?)?(,((\-|\+)?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?)?(,((\-|\+)?(([0-9]+(\.[0-9]+)?)|(\.[0-9]+))((e|E)(\-|\+)?[0-9]+)?))?)?)?)*"; /// <summary> /// Fixes the XPS problems it encounters and knows about /// </summary> public static void FixXps(string filePath) { // first we'll load the XPS file using (var currentPackage = Package.Open(filePath, FileMode.Open)) { var pageUri = new Uri("/Documents/1/Pages/1.fpage", UriKind.Relative); // check that the file we'll modify exists if (!currentPackage.PartExists(pageUri)) { throw new Exception(string.Format("Unable to find first page in XPS {0} - unable to fix this XPS !", filePath)); } // assume the broken part is in the first page var firstPage = currentPackage.GetPart(pageUri); var relationships = firstPage.GetRelationships(); var pageContent = XDocument.Load(firstPage.GetStream()); // then we'll look up each glyph and check if their "Indices" property is valid XNamespace ns = pageContent.Root.GetDefaultNamespace(); var glyphs = (from g in pageContent.Descendants(ns + "Glyphs") where g.Attribute("Indices") != null select g).ToList(); for (var i = 0; i < glyphs.Count(); i ++) { glyphs[i] = FixGlyph(glyphs[i]); } // remove the current (corrupted) file from the package currentPackage.DeletePart(pageUri); // add the new (shiny) file to the package var newPage = currentPackage.CreatePart(pageUri, "application/vnd.ms-package.xps-fixedpage+xml", CompressionOption.NotCompressed); using (var ms = new MemoryStream()) { // we need to remove XML declaration, so we need to use the XmlWriter var settings = new XmlWriterSettings(); settings.Indent = false; settings.NewLineChars = string.Empty; settings.NewLineHandling = NewLineHandling.Replace; settings.OmitXmlDeclaration = true; using (var xw = XmlWriter.Create(ms, settings)) { pageContent.WriteTo(xw); } ms.Seek(0, SeekOrigin.Begin); CopyStream(newPage.GetStream(), ms); } // now we need to re-create the relationships between the Page file and the fonts foreach (var relation in relationships) { newPage.CreateRelationship(relation.TargetUri, relation.TargetMode, relation.RelationshipType, relation.Id); } } } /// <summary> /// Tries to load the XPS, and returns false if it fails /// </summary> public static bool IsValidXps(string filePath) { try { using (var xpsOld = new XpsDocument(filePath, FileAccess.Read)) { var unused = xpsOld.GetFixedDocumentSequence(); } return true; } catch (System.Windows.Markup.XamlParseException) { return false; } } /// <summary> /// Writes the whole content of a stream into another /// </summary> /// <remarks> /// http://stackoverflow.com/a/18885954/2354542 /// </remarks> private static void CopyStream(Stream target, Stream source) { const int bufSize = 0x1000; byte[] buf = new byte[bufSize]; int bytesRead = 0; while ((bytesRead = source.Read(buf, 0, bufSize)) > 0) { target.Write(buf, 0, bytesRead); } } /// <summary> /// Fixes the glyph, if necessary /// </summary> private static XElement FixGlyph(XElement g) { var matchAttribute = Regex.Match(g.Attribute("Indices").Value, IndicesRegex); if (!matchAttribute.Success) { return g; } var hasProblem = false; foreach (var token in matchAttribute.Value.Split(";".ToCharArray())) { if (token == ",") { hasProblem = true; break; } } if (hasProblem) { // the Indices attribute is not well-formed: let's try to fix the one(s) that are wrong var fixedTokens = new List<string>(); foreach (var token in g.Attribute("Indices").Value.Split(";".ToCharArray())) { var newToken = token; var matchToken = Regex.Match(token, @",(-\d+)"); if (matchToken.Success) // negative number, yay ! it's not allowed :-( { newToken = ",0"; // it should be zero, I believe } fixedTokens.Add(newToken); } g.Attribute("Indices").Value = string.Join(";", fixedTokens); } return g; } } |
Update: Microsoft has closed the ticket, and, obviously, they have decided to not resolve the issue, since it impacts such a small number of people (maybe just us ?). I always thinks it’s sad when a company this big does not close such a small and quickly-fixed bug.