using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using DocumentFormat.OpenXml; namespace DCIT.Core.Utilities { internal static class VmlTextMetadataUtility { private static readonly Regex Base64WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled); public static bool IsVmlShapeElement(OpenXmlElement element) { return element != null && string.Equals(element.LocalName, "shape", StringComparison.OrdinalIgnoreCase) && string.Equals(element.NamespaceUri, "urn:schemas-microsoft-com:vml", StringComparison.OrdinalIgnoreCase); } public static IEnumerable CreateBindings(OpenXmlElement shape) { if (!IsVmlShapeElement(shape)) { yield break; } foreach (var binding in CreateEquationBindings(shape)) { yield return binding; } foreach (var binding in CreateChartBindings(shape)) { yield return binding; } } private static IEnumerable CreateEquationBindings(OpenXmlElement shape) { var attribute = FindAttribute(shape, "equationxml"); if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value)) { yield break; } var actualAttribute = attribute.Value; XDocument document; try { var decoded = WebUtility.HtmlDecode(actualAttribute.Value); if (string.IsNullOrWhiteSpace(decoded)) { yield break; } document = XDocument.Parse(decoded, LoadOptions.PreserveWhitespace); } catch { yield break; } var textNodes = document .Descendants() .Where(item => string.Equals(item.Name.LocalName, "t", StringComparison.OrdinalIgnoreCase)) .ToList(); if (textNodes.Count == 0) { yield break; } foreach (var textNode in textNodes) { var initialText = textNode.Value ?? string.Empty; if (initialText.Length == 0) { continue; } yield return new VmlTextBinding { Text = initialText, IsEquationText = true, Apply = updatedText => { textNode.Value = updatedText ?? string.Empty; SetAttribute(shape, actualAttribute, SerializeXml(document)); }, }; } } private static IEnumerable CreateChartBindings(OpenXmlElement shape) { var attribute = FindAttribute(shape, "gfxdata"); if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value)) { yield break; } var actualAttribute = attribute.Value; ChartPackageContext context; try { context = ChartPackageContext.TryCreate(shape, actualAttribute); } catch { context = null; } if (context == null || context.XmlEntries.Count == 0) { yield break; } foreach (var entry in context.XmlEntries) { foreach (var textNode in entry.Document .Descendants() .Where(item => IsChartVisibleTextNode(item)) .ToList()) { var initialText = textNode.Value ?? string.Empty; if (initialText.Length == 0) { continue; } yield return new VmlTextBinding { Text = initialText, IsChartText = true, Apply = updatedText => { textNode.Value = updatedText ?? string.Empty; context.Save(); }, }; } } } private static bool IsChartVisibleTextNode(XElement element) { if (element == null) { return false; } var localName = element.Name.LocalName; return string.Equals(localName, "t", StringComparison.OrdinalIgnoreCase) || string.Equals(localName, "v", StringComparison.OrdinalIgnoreCase); } private static OpenXmlAttribute? FindAttribute(OpenXmlElement element, string localName) { if (element == null || string.IsNullOrWhiteSpace(localName)) { return null; } foreach (var attribute in element.GetAttributes()) { if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase)) { return attribute; } } return null; } private static void SetAttribute(OpenXmlElement element, OpenXmlAttribute sourceAttribute, string value) { element.SetAttribute(new OpenXmlAttribute( sourceAttribute.Prefix, sourceAttribute.LocalName, sourceAttribute.NamespaceUri, value ?? string.Empty)); } private static string SerializeXml(XDocument document) { using (var writer = new Utf8StringWriter()) using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings { OmitXmlDeclaration = document.Declaration == null, Encoding = new UTF8Encoding(false), Indent = false, NewLineHandling = NewLineHandling.None, })) { document.Save(xmlWriter); xmlWriter.Flush(); return writer.ToString(); } } private sealed class ChartPackageContext { private readonly OpenXmlElement _shape; private readonly OpenXmlAttribute _attribute; private readonly List _allEntries; private ChartPackageContext(OpenXmlElement shape, OpenXmlAttribute attribute, List allEntries, List xmlEntries) { _shape = shape; _attribute = attribute; _allEntries = allEntries; XmlEntries = xmlEntries; } public List XmlEntries { get; private set; } public static ChartPackageContext TryCreate(OpenXmlElement shape, OpenXmlAttribute attribute) { var decoded = WebUtility.HtmlDecode(attribute.Value ?? string.Empty); if (string.IsNullOrWhiteSpace(decoded)) { return null; } var normalizedBase64 = Base64WhitespaceRegex.Replace(decoded, string.Empty); if (normalizedBase64.Length == 0) { return null; } var bytes = Convert.FromBase64String(normalizedBase64); var allEntries = new List(); var xmlEntries = new List(); using (var input = new MemoryStream(bytes)) using (var zip = new ZipArchive(input, ZipArchiveMode.Read, false)) { foreach (var entry in zip.Entries) { var state = new EntryState { FullName = entry.FullName, LastWriteTime = entry.LastWriteTime, }; using (var entryStream = entry.Open()) using (var memory = new MemoryStream()) { entryStream.CopyTo(memory); state.RawBytes = memory.ToArray(); } if (entry.FullName.IndexOf("drs/charts/", StringComparison.OrdinalIgnoreCase) >= 0 && entry.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { try { state.Document = XDocument.Parse( Encoding.UTF8.GetString(state.RawBytes), LoadOptions.PreserveWhitespace); } catch { state.Document = null; } if (state.Document != null) { xmlEntries.Add(state); } } allEntries.Add(state); } } return new ChartPackageContext(shape, attribute, allEntries, xmlEntries); } public void Save() { using (var output = new MemoryStream()) { using (var zip = new ZipArchive(output, ZipArchiveMode.Create, true)) { foreach (var entry in _allEntries) { var targetEntry = zip.CreateEntry(entry.FullName, CompressionLevel.Optimal); targetEntry.LastWriteTime = entry.LastWriteTime; using (var targetStream = targetEntry.Open()) { var bytes = entry.Document == null ? (entry.RawBytes ?? Array.Empty()) : Encoding.UTF8.GetBytes(SerializeXml(entry.Document)); targetStream.Write(bytes, 0, bytes.Length); } } } SetAttribute(_shape, _attribute, Convert.ToBase64String(output.ToArray())); } } } private sealed class Utf8StringWriter : StringWriter { public override Encoding Encoding { get { return new UTF8Encoding(false); } } } private sealed class EntryState { public string FullName { get; set; } public DateTimeOffset LastWriteTime { get; set; } public byte[] RawBytes { get; set; } public XDocument Document { get; set; } } } internal sealed class VmlTextBinding { public string Text { get; set; } public bool IsEquationText { get; set; } public bool IsChartText { get; set; } public Action Apply { get; set; } } }