blob: f2f54d70962f8379983007ffc687606eedb8138b [file] [log] [blame]
/***************************************************************************
Copyright (c) Microsoft Corporation 2012-2015.
This code is licensed using the Microsoft Public License (Ms-PL). The text of the license can be found here:
http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx
Published at http://OpenXmlDeveloper.org
Resource Center and Documentation: http://openxmldeveloper.org/wiki/w/wiki/powertools-for-open-xml.aspx
Developer: Eric White
Blog: http://www.ericwhite.com
Twitter: @EricWhiteDev
Email: eric@ericwhite.com
***************************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml.Linq;
using DocumentFormat.OpenXml.Packaging;
using System.IO;
namespace OpenXmlPowerTools
{
public partial class SmlDocument
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public XElement ConvertToHtml(SmlToHtmlConverterSettings htmlConverterSettings, string tableName)
{
return SmlToHtmlConverter.ConvertTableToHtml(this, htmlConverterSettings, tableName);
}
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public XElement ConvertTableToHtml(string tableName)
{
SmlToHtmlConverterSettings settings = new SmlToHtmlConverterSettings();
return SmlToHtmlConverter.ConvertTableToHtml(this, settings, tableName);
}
}
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global")]
public class SmlToHtmlConverterSettings
{
public string PageTitle;
public string CssClassPrefix;
public bool FabricateCssClasses;
public string GeneralCss;
public string AdditionalCss;
public SmlToHtmlConverterSettings()
{
PageTitle = "";
CssClassPrefix = "pt-";
FabricateCssClasses = true;
GeneralCss = "span { white-space: pre-wrap; }";
AdditionalCss = "";
}
public SmlToHtmlConverterSettings(SmlToHtmlConverterSettings htmlConverterSettings)
{
PageTitle = htmlConverterSettings.PageTitle;
CssClassPrefix = htmlConverterSettings.CssClassPrefix;
FabricateCssClasses = htmlConverterSettings.FabricateCssClasses;
GeneralCss = htmlConverterSettings.GeneralCss;
AdditionalCss = htmlConverterSettings.AdditionalCss;
}
}
public static class SmlToHtmlConverter
{
// ***********************************************************************************************************************************
#region PublicApis
public static XElement ConvertTableToHtml(SmlDocument smlDoc, SmlToHtmlConverterSettings settings, string tableName)
{
using (MemoryStream ms = new MemoryStream())
{
ms.Write(smlDoc.DocumentByteArray, 0, smlDoc.DocumentByteArray.Length);
using (SpreadsheetDocument sDoc = SpreadsheetDocument.Open(ms, false))
{
var rangeXml = SmlDataRetriever.RetrieveTable(sDoc, tableName);
var xhtml = SmlToHtmlConverter.ConvertToHtmlInternal(sDoc, settings, rangeXml);
return xhtml;
}
}
}
public static XElement ConvertTableToHtml(SpreadsheetDocument sDoc, SmlToHtmlConverterSettings settings, string tableName)
{
var rangeXml = SmlDataRetriever.RetrieveTable(sDoc, tableName);
var xhtml = SmlToHtmlConverter.ConvertToHtmlInternal(sDoc, settings, rangeXml);
return xhtml;
}
#endregion
// ***********************************************************************************************************************************
private static XElement ConvertToHtmlInternal(SpreadsheetDocument sDoc, SmlToHtmlConverterSettings htmlConverterSettings, XElement rangeXml)
{
XElement xhtml = (XElement)ConvertToHtmlTransform(sDoc, htmlConverterSettings, rangeXml);
ReifyStylesAndClasses(htmlConverterSettings, xhtml);
// Note: the xhtml returned by ConvertToHtmlTransform contains objects of type
// XEntity. PtOpenXmlUtil.cs define the XEntity class. See
// http://blogs.msdn.com/ericwhite/archive/2010/01/21/writing-entity-references-using-linq-to-xml.aspx
// for detailed explanation.
//
// If you further transform the XML tree returned by ConvertToHtmlTransform, you
// must do it correctly, or entities will not be serialized properly.
return xhtml;
}
private static XNode ConvertToHtmlTransform(SpreadsheetDocument sDoc, SmlToHtmlConverterSettings htmlConverterSettings, XNode node)
{
var element = node as XElement;
if (element != null)
{
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => ConvertToHtmlTransform(sDoc, htmlConverterSettings, n)));
}
return node;
}
private static void ReifyStylesAndClasses(SmlToHtmlConverterSettings htmlConverterSettings, XElement xhtml)
{
if (htmlConverterSettings.FabricateCssClasses)
{
var usedCssClassNames = new HashSet<string>();
var elementsThatNeedClasses = xhtml
.DescendantsAndSelf()
.Select(d => new
{
Element = d,
Styles = d.Annotation<Dictionary<string, string>>(),
})
.Where(z => z.Styles != null);
var augmented = elementsThatNeedClasses
.Select(p => new
{
p.Element,
p.Styles,
StylesString = p.Element.Name.LocalName + "|" + p.Styles.OrderBy(k => k.Key).Select(s => string.Format("{0}: {1};", s.Key, s.Value)).StringConcatenate(),
})
.GroupBy(p => p.StylesString)
.ToList();
int classCounter = 1000000;
var sb = new StringBuilder();
sb.Append(Environment.NewLine);
foreach (var grp in augmented)
{
string classNameToUse;
var firstOne = grp.First();
var styles = firstOne.Styles;
if (styles.ContainsKey("PtStyleName"))
{
classNameToUse = htmlConverterSettings.CssClassPrefix + styles["PtStyleName"];
if (usedCssClassNames.Contains(classNameToUse))
{
classNameToUse = htmlConverterSettings.CssClassPrefix +
styles["PtStyleName"] + "-" +
classCounter.ToString().Substring(1);
classCounter++;
}
}
else
{
classNameToUse = htmlConverterSettings.CssClassPrefix +
classCounter.ToString().Substring(1);
classCounter++;
}
usedCssClassNames.Add(classNameToUse);
sb.Append(firstOne.Element.Name.LocalName + "." + classNameToUse + " {" + Environment.NewLine);
foreach (var st in firstOne.Styles.Where(s => s.Key != "PtStyleName"))
{
var s = " " + st.Key + ": " + st.Value + ";" + Environment.NewLine;
sb.Append(s);
}
sb.Append("}" + Environment.NewLine);
var classAtt = new XAttribute("class", classNameToUse);
foreach (var gc in grp)
gc.Element.Add(classAtt);
}
var styleValue = htmlConverterSettings.GeneralCss + sb + htmlConverterSettings.AdditionalCss;
SetStyleElementValue(xhtml, styleValue);
}
else
{
// Previously, the h:style element was not added at this point. However,
// at least the General CSS will contain important settings.
SetStyleElementValue(xhtml, htmlConverterSettings.GeneralCss + htmlConverterSettings.AdditionalCss);
foreach (var d in xhtml.DescendantsAndSelf())
{
var style = d.Annotation<Dictionary<string, string>>();
if (style == null)
continue;
var styleValue =
style
.Where(p => p.Key != "PtStyleName")
.OrderBy(p => p.Key)
.Select(e => string.Format("{0}: {1};", e.Key, e.Value))
.StringConcatenate();
XAttribute st = new XAttribute("style", styleValue);
if (d.Attribute("style") != null)
d.Attribute("style").Value += styleValue;
else
d.Add(st);
}
}
}
private static void SetStyleElementValue(XElement xhtml, string styleValue)
{
var styleElement = xhtml
.Descendants(Xhtml.style)
.FirstOrDefault();
if (styleElement != null)
styleElement.Value = styleValue;
else
{
styleElement = new XElement(Xhtml.style, styleValue);
var head = xhtml.Element(Xhtml.head);
if (head != null)
head.Add(styleElement);
}
}
private static object ConvertToHtmlTransform(WordprocessingDocument wordDoc,
WmlToHtmlConverterSettings settings, XNode node)
{
// Ignore element.
return null;
}
private static readonly HashSet<string> UnknownFonts = new HashSet<string>();
private static HashSet<string> _knownFamilies;
private static HashSet<string> KnownFamilies
{
get
{
if (_knownFamilies == null)
{
_knownFamilies = new HashSet<string>();
var families = FontFamily.Families;
foreach (var fam in families)
_knownFamilies.Add(fam.Name);
}
return _knownFamilies;
}
}
private static readonly Dictionary<string, string> FontFallback = new Dictionary<string, string>()
{
{ "Arial", @"'{0}', 'sans-serif'" },
{ "Arial Narrow", @"'{0}', 'sans-serif'" },
{ "Arial Rounded MT Bold", @"'{0}', 'sans-serif'" },
{ "Arial Unicode MS", @"'{0}', 'sans-serif'" },
{ "Baskerville Old Face", @"'{0}', 'serif'" },
{ "Berlin Sans FB", @"'{0}', 'sans-serif'" },
{ "Berlin Sans FB Demi", @"'{0}', 'sans-serif'" },
{ "Calibri Light", @"'{0}', 'sans-serif'" },
{ "Gill Sans MT", @"'{0}', 'sans-serif'" },
{ "Gill Sans MT Condensed", @"'{0}', 'sans-serif'" },
{ "Lucida Sans", @"'{0}', 'sans-serif'" },
{ "Lucida Sans Unicode", @"'{0}', 'sans-serif'" },
{ "Segoe UI", @"'{0}', 'sans-serif'" },
{ "Segoe UI Light", @"'{0}', 'sans-serif'" },
{ "Segoe UI Semibold", @"'{0}', 'sans-serif'" },
{ "Tahoma", @"'{0}', 'sans-serif'" },
{ "Trebuchet MS", @"'{0}', 'sans-serif'" },
{ "Verdana", @"'{0}', 'sans-serif'" },
{ "Book Antiqua", @"'{0}', 'serif'" },
{ "Bookman Old Style", @"'{0}', 'serif'" },
{ "Californian FB", @"'{0}', 'serif'" },
{ "Cambria", @"'{0}', 'serif'" },
{ "Constantia", @"'{0}', 'serif'" },
{ "Garamond", @"'{0}', 'serif'" },
{ "Lucida Bright", @"'{0}', 'serif'" },
{ "Lucida Fax", @"'{0}', 'serif'" },
{ "Palatino Linotype", @"'{0}', 'serif'" },
{ "Times New Roman", @"'{0}', 'serif'" },
{ "Wide Latin", @"'{0}', 'serif'" },
{ "Courier New", @"'{0}'" },
{ "Lucida Console", @"'{0}'" },
};
private static void CreateFontCssProperty(string font, Dictionary<string, string> style)
{
if (FontFallback.ContainsKey(font))
{
style.AddIfMissing("font-family", string.Format(FontFallback[font], font));
return;
}
style.AddIfMissing("font-family", font);
}
private static bool GetBoolProp(XElement runProps, XName xName)
{
var p = runProps.Element(xName);
if (p == null)
return false;
var v = p.Attribute(W.val);
if (v == null)
return true;
var s = v.Value.ToLower();
if (s == "0" || s == "false")
return false;
if (s == "1" || s == "true")
return true;
return false;
}
}
}