├── Rtf2Html ├── RtfToHtmlConverter.cs ├── RtfToPlaintTextConverter.cs ├── Program.cs ├── HtmlResult.cs ├── Properties │ └── AssemblyInfo.cs ├── Rtf2Html.csproj ├── RtfToXamlConverter.cs └── XamlToHtmlConverter.cs ├── testdata ├── revision_tags_2.rtf └── revision_tags.rtf ├── Rtf2Html.sln ├── README.md ├── .gitattributes └── .gitignore /Rtf2Html/RtfToHtmlConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Rtf2Html 2 | { 3 | internal static class RtfToHtmlConverter 4 | { 5 | public static HtmlResult RtfToHtml(string rtf, string contentUriPrefix = null, bool asFullDocument = true) 6 | { 7 | var xamlStream = RtfToXamlConverter.RtfToXamlPackage(rtf); 8 | var htmlConverter = new XamlToHtmlConverter 9 | { 10 | AsFullDocument = asFullDocument, 11 | ContentUriPrefix = contentUriPrefix 12 | }; 13 | 14 | return htmlConverter.ConvertXamlToHtml(xamlStream); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /testdata/revision_tags_2.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\sste16000\ansi\deflang1033\ftnbj\uc1\deff0 2 | {\fonttbl{\f0 \fnil \fcharset0 Arial;}} 3 | {\colortbl ;\red255\green255\blue255 ;\red0\green0\blue0 ;} 4 | {\stylesheet{\f0\fs24 Normal;}{\cs1 Default Paragraph Font;}} 5 | {\*\revtbl{Unknown;}{atiwari1;}} 6 | \paperw12240\paperh15840\margl1800\margr1800\margt1440\margb1440\headery720\footery720\nogrowautofit\deftab720\formshade\fet4\aendnotes\aftnnrlc\pgbrdrhead\pgbrdrfoot 7 | \sectd\pgwsxn12240\pghsxn15840\marglsxn1800\margrsxn1800\margtsxn1440\margbsxn1440\headery720\footery720\sbkpage\pgncont\pgndec 8 | \plain\plain\f0\fs24\pard\plain\f0\fs24\plain\lang1033\hich\f0\dbch\f0\loch\f0\fs20 This is my \plain\lang1033\hich\f0\dbch\f0\loch\f0\fs20\revised\revauth1\revdttm651739769 Second\plain\lang1033\hich\f0\dbch\f0\loch\f0\fs20\deleted\revauthdel1\revdttmdel651739769 9 | first\plain\lang1033\hich\f0\dbch\f0\loch\f0\fs20 line of text.\par 10 | } -------------------------------------------------------------------------------- /Rtf2Html.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rtf2Html", "Rtf2Html\Rtf2Html.csproj", "{B1F1A364-20BD-47A2-BBC9-3D022E065691}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B1F1A364-20BD-47A2-BBC9-3D022E065691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B1F1A364-20BD-47A2-BBC9-3D022E065691}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B1F1A364-20BD-47A2-BBC9-3D022E065691}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B1F1A364-20BD-47A2-BBC9-3D022E065691}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /Rtf2Html/RtfToPlaintTextConverter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Windows; 4 | using System.Windows.Documents; 5 | 6 | namespace Rtf2Html 7 | { 8 | internal static class RtfToPlaintTextConverter 9 | { 10 | public static string RtfToPlainText(string rtf, Encoding encoding = null) 11 | { 12 | var safeEncoding = encoding ?? Encoding.UTF8; 13 | // cf.: http://stackoverflow.com/questions/26123907/convert-rtf-string-to-xaml-string 14 | var doc = new FlowDocument(); 15 | var range = new TextRange(doc.ContentStart, doc.ContentEnd); 16 | using (var stream = new MemoryStream(safeEncoding.GetBytes(rtf))) 17 | using (var outStream = new MemoryStream()) 18 | { 19 | range.Load(stream, DataFormats.Rtf); 20 | range.Save(outStream, DataFormats.Text); 21 | outStream.Seek(0, SeekOrigin.Begin); 22 | return safeEncoding.GetString(outStream.ToArray()).Trim(); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Rtf2Html/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Rtf2Html 5 | { 6 | internal static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var rtfInput = GetArg(args, 0, "Document.rtf"); 11 | 12 | var rtf = File.ReadAllText(rtfInput); 13 | 14 | //var xaml = RtfToXamlConverter.RtfToXaml(rtf); 15 | //File.WriteAllText("xaml.xaml", xaml); 16 | 17 | //var rtf2 = RtfToXamlConverter.XamlToRtf(xaml); 18 | //File.WriteAllText("rtf2.rtf", rtf2); 19 | 20 | //var plainText = RtfToPlaintTextConverter.RtfToPlainText(rtf); 21 | //File.WriteAllText("text.txt", plainText); 22 | 23 | var htmlOutput = GetArg(args, 1, Path.ChangeExtension(rtfInput, ".html")); 24 | var contentUriPrefix = Path.GetFileNameWithoutExtension(htmlOutput); 25 | var htmlResult = RtfToHtmlConverter.RtfToHtml(rtf, contentUriPrefix); 26 | htmlResult.WriteToFile(htmlOutput); 27 | } 28 | 29 | private static string GetArg(IList args, int index, string defaultValue) 30 | { 31 | if (args == null || args.Count <= index) return defaultValue; 32 | return args[index]; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Rtf2Html/HtmlResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Rtf2Html 5 | { 6 | internal class HtmlResult 7 | { 8 | public string Html { get; set; } 9 | public Dictionary Content { get; } 10 | 11 | public HtmlResult() 12 | { 13 | Html = string.Empty; 14 | Content = new Dictionary(); 15 | } 16 | 17 | public void WriteToFile(string fileName) 18 | { 19 | File.WriteAllText(fileName, Html); 20 | var fileInfo = new FileInfo(fileName); 21 | foreach (var content in Content) 22 | { 23 | // GetFullPath converts forward slashes, cf.: 24 | // http://stackoverflow.com/questions/3144492/how-do-i-get-nets-path-combine-to-convert-forward-slashes-to-backslashes 25 | var contentFileName = Path.GetFullPath(Path.Combine(fileInfo.DirectoryName, content.Key)); 26 | var contentPath = Path.GetDirectoryName(contentFileName); 27 | if (!Directory.Exists(contentPath)) 28 | // ReSharper disable once AssignNullToNotNullAttribute 29 | Directory.CreateDirectory(contentPath); 30 | File.WriteAllBytes(contentFileName, content.Value); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Rtf2Html/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Rtf2Html")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Rtf2Html")] 12 | [assembly: AssemblyCopyright("Copyright © 2015")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("05cb6e5a-3ecf-4d65-af2f-dbc6ed3442d1")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rtf2Html 2 | 3 | Convert Rtf via Xaml to Html including images. The code is adapted from [Matthew Manelas Codesample](http://matthewmanela.com/blog/converting-between-rtf-to-html-and-html-to-rtf/) with a few tweaks: 4 | 5 | 1. Usage of [`RichTextBox`](http://msdn.microsoft.com/en-us/library/system.windows.controls.richtextbox.aspx) is avoided so we do not need to work around [STA](https://msdn.microsoft.com/en-us/library/windows/desktop/ms680112%28v=vs.85%29.aspx) issues. Instead we use [`TextEditorCopyPaste.ConvertRtfToXaml`](http://referencesource.microsoft.com/#PresentationFramework/Framework/System/windows/Documents/TextEditorCopyPaste.cs,560) via reflection. This is basically the same as using `TextRangeBase.Save(..., DataFormats.XamlPackage)` with `RichTextBox` but feels a bit cleaner to me. 6 | 7 | 2. Added support for processing the zipped `XamlPackage` stream including images. Requires [`ZipArchive`](https://msdn.microsoft.com/de-de/library/system.io.compression.ziparchive(v=vs.110).aspx) from `System.IO.Compression` in .NET 4.5 8 | 9 | Simple conversion between `Rtf` and `Xaml` is also included by just exposing Microsofts internal implementations. 10 | 11 | ## Examples 12 | 13 | ### Rtf to Html (including image content): 14 | 15 | ```csharp 16 | var rtf = File.ReadAllText("doc.rtf"); 17 | var htmlResult = RtfToHtmlConverter.RtfToHtml(rtf, "doc"); 18 | htmlResult.WriteToFile("doc.html"); 19 | ``` 20 | 21 | ### Rtf to Xaml (without content): 22 | 23 | ```csharp 24 | var rtf = File.ReadAllText("doc.rtf"); 25 | var xaml = RtfToXamlConverter.RtfToXaml(rtf); 26 | File.WriteAllText("doc.xaml", xaml); 27 | ``` 28 | 29 | ### Rtf to Xaml package (zip with content) 30 | 31 | ```csharp 32 | var rtf = File.ReadAllText("doc.rtf"); 33 | using (var xamlStream = RtfToXamlConverter.RtfToXamlPackage(rtf)) 34 | File.WriteAllBytes("doc.xap", xamlStream.ToArray()); 35 | ``` 36 | 37 | ### Rtf to plain text 38 | 39 | ```csharp 40 | var rtf = File.ReadAllText("doc.rtf"); 41 | var plainText = RtfToPlaintTextConverter.RtfToPlainText(rtf); 42 | File.WriteAllText("doc.txt", plainText); 43 | ``` 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /testdata/revision_tags.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\sstern21000\ansi\deflang1033\ftnbj\uc1\deff0 2 | {\fonttbl{\f0 \fmodern \fcharset0 Courier New;}{\f1 \fnil Courier New;}{\f2 \fnil \fcharset0 Calibri;}{\f3 \fnil \fcharset0 Arial;}{\f4 \fswiss \fcharset0 Arial;}} 3 | {\colortbl ;\red255\green255\blue255 ;\red0\green0\blue0 ;} 4 | {\stylesheet{\f1\fs20 Normal;}{\cs1 Default Paragraph Font;}} 5 | {\*\revtbl{Unknown;}{Saware, Nil;}} 6 | \paperw12240\paperh15840\margl1800\margr1800\margt1440\margb1440\headery720\footery720\nogrowautofit\deftab720\formshade\nofeaturethrottle1\dntblnsbdb\fet4\aendnotes\aftnnrlc\pgbrdrhead\pgbrdrfoot 7 | \sectd\pgwsxn12240\pghsxn15840\guttersxn0\marglsxn1800\margrsxn1800\margtsxn1440\margbsxn1440\headery720\footery720\sbkpage\pgncont\pgndec 8 | \plain\plain\f1\fs20\pard\ssparaaux0\s0\ltrpar\ql\widctlpar\plain\f1\fs20\plain\f2\fs22\lang1033\hich\f2\dbch\f2\loch\f2\fs22\ltrch When a Visit Record Audit view is displayed, cancelled Free Text documents will show in strikethrough font.Document Status 9 | History screen.The user who canceled the document, the date/time of cancellation and the reason will also appear. This information will appear at the top of the note under the Authored by & Updated by lines (if Updated by is applicable). This will allow 10 | the cancel information to show when the SN is . Cancel \plain\f2\fs22\lang1033\hich\f2\dbch\f2\loch\f2\fs22\ltrch\deleted\revauthdel1\revdttmdel1733882639 information \plain\f2\fs22\lang1033\hich\f2\dbch\f2\loch\f2\fs22\revised\revauth1\revdttm1733882639 11 | infragistics\plain\f2\fs22\lang1033\hich\f2\dbch\f2\loch\f2\fs22\ltrch will match what is currently found in the Document Status History screen.\par 12 | \pard\ssparaaux0\s0\ql\plain\f1\fs20\plain\f3\fs20\lang1033\hich\f3\dbch\f3\loch\f3\fs20\par 13 | \pard\plain\f1\fs20\plain\f0\fs24\lang1033\hich\f0\dbch\f0\loch\f0\fs24\par 14 | \plain\f4\fs20\lang1033\hich\f4\dbch\f4\loch\f4\fs20\b Electronic Signatures:\plain\f3\fs20\lang1033\hich\f3\dbch\f3\loch\f3\fs20\par 15 | \plain\f4\fs20\lang1033\hich\f4\dbch\f4\loch\f4\fs20\b\ul Config, SCM (Physician)\plain\f4\fs18\lang1033\hich\f4\dbch\f4\loch\f4\fs18 (Signature Pending)\par 16 | \fi-360\li720\ql\plain\f1\fs20\plain\f4\fs18\lang1033\hich\f4\dbch\f4\loch\f4\fs18\tab\plain\f4\fs20\lang1033\hich\f4\dbch\f4\loch\f4\fs20\b\i Authored \plain\f3\fs20\lang1033\hich\f3\dbch\f3\loch\f3\fs20\par 17 | \pard\plain\f1\fs20\plain\f4\fs20\lang1033\hich\f4\dbch\f4\loch\f4\fs20\b\ul Saware, Nil (MD)\plain\f4\fs18\lang1033\hich\f4\dbch\f4\loch\f4\fs18 (Entered on 29-08-2017 23:46)\par 18 | \fi-360\li720\ql\plain\f1\fs20\plain\f4\fs18\lang1033\hich\f4\dbch\f4\loch\f4\fs18\tab\plain\f4\fs20\lang1033\hich\f4\dbch\f4\loch\f4\fs20\b\i Entered \plain\f3\fs20\lang1033\hich\f3\dbch\f3\loch\f3\fs20\par 19 | \pard\plain\f1\fs20\plain\f3\fs20\lang1033\hich\f3\dbch\f3\loch\f3\fs20 \par 20 | } 21 | \par} -------------------------------------------------------------------------------- /Rtf2Html/Rtf2Html.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B1F1A364-20BD-47A2-BBC9-3D022E065691} 8 | Exe 9 | Properties 10 | Rtf2Html 11 | Rtf2Html 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | Rtf2Html.Program 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | -------------------------------------------------------------------------------- /Rtf2Html/RtfToXamlConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Windows; 5 | 6 | namespace Rtf2Html 7 | { 8 | internal static class RtfToXamlConverter 9 | { 10 | public static bool RtfContainsImage(string rtfText) 11 | { 12 | // cf.: http://www.biblioscape.com/rtf15_spec.htm#Heading49 13 | return !string.IsNullOrWhiteSpace(rtfText) && rtfText.Contains(@"\pict"); 14 | } 15 | 16 | /// 17 | /// Converts the specified RTF string into a Xaml package. 18 | /// 19 | /// The RTF content to convert to XAML 20 | /// A zipped stream containing a full xaml package; or null 21 | public static MemoryStream RtfToXamlPackage(string rtfContent) 22 | { 23 | if (string.IsNullOrWhiteSpace(rtfContent)) return null; 24 | return (MemoryStream)TextEditorCopyPaste_ConvertRtfToXaml 25 | .Invoke(null, new object[] { rtfContent }); 26 | } 27 | 28 | /// 29 | /// Converts the specified Xaml content string into an RTF string. 30 | /// 31 | public static string XamlToRtf(string xamlContent, Stream wpfContainerMemory = null) 32 | { 33 | if (string.IsNullOrWhiteSpace(xamlContent)) return string.Empty; 34 | return (string)TextEditorCopyPaste_ConvertXamlToRtf 35 | .Invoke(null, new object[] { xamlContent, wpfContainerMemory }); 36 | } 37 | 38 | /// 39 | /// Converts the specified RTF string into a Xaml Flow document. 40 | /// 41 | /// The RTF content to convert to XAML 42 | /// A Xaml string 43 | /// Images and other content in the RTF are lost in the resulting Xaml content. 44 | public static string RtfToXaml(string rtfContent) 45 | { 46 | if (string.IsNullOrWhiteSpace(rtfContent)) return string.Empty; 47 | return (string)XamlRtfConverterType_ConvertRtfToXaml 48 | .Invoke(XamlRtfConverter, new object[] { rtfContent }); 49 | } 50 | 51 | private static readonly Assembly PresentationFrameworkAssembly = Assembly.GetAssembly(typeof(FrameworkElement)); 52 | private static readonly Type TextEditorCopyPasteType = PresentationFrameworkAssembly 53 | .GetType("System.Windows.Documents.TextEditorCopyPaste"); 54 | 55 | private static readonly Type XamlRtfConverterType = PresentationFrameworkAssembly 56 | .GetType("System.Windows.Documents.XamlRtfConverter"); 57 | 58 | // ReSharper disable InconsistentNaming 59 | private static readonly MethodInfo TextEditorCopyPaste_ConvertRtfToXaml = TextEditorCopyPasteType 60 | .GetMethod("ConvertRtfToXaml", BindingFlags.Static | BindingFlags.NonPublic); 61 | 62 | private static readonly MethodInfo TextEditorCopyPaste_ConvertXamlToRtf = TextEditorCopyPasteType 63 | .GetMethod("ConvertXamlToRtf", BindingFlags.Static | BindingFlags.NonPublic); 64 | 65 | private static readonly MethodInfo XamlRtfConverterType_ConvertRtfToXaml = XamlRtfConverterType 66 | .GetMethod("ConvertRtfToXaml", BindingFlags.Instance | BindingFlags.NonPublic); 67 | 68 | private static readonly object XamlRtfConverter = Activator.CreateInstance(XamlRtfConverterType, 69 | BindingFlags.NonPublic | BindingFlags.Instance, null, null, null); 70 | // ReSharper restore InconsistentNaming 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | ## TODO: Comment the next line if you want to checkin your 127 | ## web deploy settings but do note that will include unencrypted 128 | ## passwords 129 | #*.pubxml 130 | 131 | # NuGet Packages Directory 132 | packages/* 133 | ## TODO: If the tool you use requires repositories.config 134 | ## uncomment the next line 135 | #!packages/repositories.config 136 | 137 | # Enable "build/" folder in the NuGet Packages folder since 138 | # NuGet packages use it for MSBuild targets. 139 | # This line needs to be after the ignore of the build folder 140 | # (and the packages folder if the line above has been uncommented) 141 | !packages/build/ 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | 186 | # LightSwitch generated files 187 | GeneratedArtifacts/ 188 | _Pvt_Extensions/ 189 | ModelManifest.xml 190 | -------------------------------------------------------------------------------- /Rtf2Html/XamlToHtmlConverter.cs: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/mmanela/MarkupConverter 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.IO.Compression; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Xml; 12 | 13 | namespace Rtf2Html 14 | { 15 | internal class XamlToHtmlConverter 16 | { 17 | private ZipArchive _zip; 18 | private XmlTextReader _xamlReader; 19 | private HtmlResult _htmlResult; 20 | private string _contentUriPrefix = string.Empty; 21 | 22 | public bool AsFullDocument { get; set; } 23 | 24 | public string ContentUriPrefix 25 | { 26 | get { return _contentUriPrefix; } 27 | set 28 | { 29 | _contentUriPrefix = value ?? string.Empty; 30 | if (!string.IsNullOrWhiteSpace(_contentUriPrefix) && !_contentUriPrefix.EndsWith("/")) 31 | _contentUriPrefix += "/"; 32 | } 33 | } 34 | 35 | public XamlToHtmlConverter() { AsFullDocument = true; } 36 | 37 | public HtmlResult ConvertXamlToHtml(MemoryStream xamlPackageStream) 38 | { 39 | using (_zip = new ZipArchive(xamlPackageStream)) 40 | { 41 | var entry = _zip.GetEntry(@"Xaml/Document.xaml"); 42 | using (var stream = entry.Open()) 43 | using (var reader = new StreamReader(stream)) 44 | { 45 | var xaml = reader.ReadToEnd(); 46 | _htmlResult = new HtmlResult(); 47 | ConvertXamlToHtml(xaml); 48 | return _htmlResult; 49 | } 50 | } 51 | } 52 | 53 | private void ConvertXamlToHtml(string xamlString) 54 | { 55 | var safeXaml = WrapIntoFlowDocument(xamlString); 56 | 57 | _xamlReader = new XmlTextReader(new StringReader(safeXaml)); 58 | var htmlStringBuilder = new StringBuilder(100); 59 | var htmlWriter = new XmlTextWriter(new StringWriter(htmlStringBuilder)); 60 | 61 | if (!WriteFlowDocument(htmlWriter)) 62 | return; 63 | _htmlResult.Html = htmlStringBuilder.ToString(); 64 | } 65 | 66 | private static string WrapIntoFlowDocument(string xamlString) 67 | { 68 | if (string.IsNullOrWhiteSpace(xamlString)) return string.Empty; 69 | if (xamlString.StartsWith("", 73 | xamlString, 74 | ""); 75 | } 76 | 77 | private bool WriteFlowDocument(XmlTextWriter htmlWriter) 78 | { 79 | // Xaml content is empty - nothing to convert 80 | if (!ReadNextToken()) 81 | return false; 82 | 83 | // Root FlowDocument element is missing 84 | if (_xamlReader.NodeType != XmlNodeType.Element || _xamlReader.Name != "FlowDocument") 85 | return false; 86 | 87 | // Create a buffer StringBuilder for collecting css properties for inline STYLE attributes 88 | // on every element level (it will be re-initialized on every level). 89 | var inlineStyle = new StringBuilder(); 90 | 91 | 92 | if (AsFullDocument) 93 | { 94 | htmlWriter.WriteStartElement("html"); 95 | 96 | WriteHead(htmlWriter); 97 | 98 | htmlWriter.WriteStartElement("body"); 99 | } 100 | 101 | WriteFormattingProperties(htmlWriter, inlineStyle); 102 | WriteElementContent(htmlWriter, inlineStyle); 103 | 104 | if (AsFullDocument) 105 | { 106 | htmlWriter.WriteEndElement(); 107 | htmlWriter.WriteEndElement(); 108 | } 109 | 110 | return true; 111 | } 112 | 113 | private static void WriteHead(XmlWriter htmlWriter) 114 | { 115 | htmlWriter.WriteStartElement("head"); 116 | 117 | htmlWriter.WriteStartElement("meta"); 118 | htmlWriter.WriteAttributeString("http-equiv", "Content-Type"); 119 | htmlWriter.WriteAttributeString("content", "text/html; charset=UTF-8"); 120 | htmlWriter.WriteEndElement(); 121 | 122 | htmlWriter.WriteEndElement(); 123 | } 124 | 125 | private void WriteFormattingProperties(XmlWriter htmlWriter, StringBuilder inlineStyle) 126 | { 127 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.Element); 128 | 129 | // Clear string builder for the inline style 130 | inlineStyle.Remove(0, inlineStyle.Length); 131 | 132 | if (!_xamlReader.HasAttributes) 133 | return; 134 | 135 | var borderSet = false; 136 | 137 | while (_xamlReader.MoveToNextAttribute()) 138 | { 139 | string css = null; 140 | 141 | switch (_xamlReader.Name) 142 | { 143 | // Character fomatting properties 144 | // ------------------------------ 145 | case "Background": 146 | css = "background-color:" + ParseXamlColor(_xamlReader.Value) + ";"; 147 | break; 148 | case "FontFamily": 149 | css = "font-family:" + _xamlReader.Value + ";"; 150 | break; 151 | case "FontStyle": 152 | css = "font-style:" + _xamlReader.Value.ToLower() + ";"; 153 | break; 154 | case "FontWeight": 155 | css = "font-weight:" + _xamlReader.Value.ToLower() + ";"; 156 | break; 157 | case "FontStretch": 158 | break; 159 | case "FontSize": 160 | css = "font-size:" + _xamlReader.Value + ";"; 161 | break; 162 | case "Foreground": 163 | css = "color:" + ParseXamlColor(_xamlReader.Value) + ";"; 164 | break; 165 | case "TextDecorations": 166 | css = _xamlReader.Value.ToLower() == "strikethrough" ? "text-decoration:line-through;" : "text-decoration:underline;"; 167 | break; 168 | case "TextEffects": 169 | break; 170 | case "Emphasis": 171 | break; 172 | case "StandardLigatures": 173 | break; 174 | case "Variants": 175 | break; 176 | case "Capitals": 177 | break; 178 | case "Fraction": 179 | break; 180 | 181 | // Paragraph formatting properties 182 | // ------------------------------- 183 | case "Padding": 184 | css = "padding:" + ParseXamlThickness(_xamlReader.Value) + ";"; 185 | break; 186 | case "Margin": 187 | css = "margin:" + ParseXamlThickness(_xamlReader.Value) + ";"; 188 | break; 189 | case "BorderThickness": 190 | css = "border-width:" + ParseXamlThickness(_xamlReader.Value) + ";"; 191 | borderSet = true; 192 | break; 193 | case "BorderBrush": 194 | css = "border-color:" + ParseXamlColor(_xamlReader.Value) + ";"; 195 | borderSet = true; 196 | break; 197 | case "LineHeight": 198 | break; 199 | case "TextIndent": 200 | css = "text-indent:" + _xamlReader.Value + ";"; 201 | break; 202 | case "TextAlignment": 203 | css = "text-align:" + _xamlReader.Value + ";"; 204 | break; 205 | case "IsKeptTogether": 206 | break; 207 | case "IsKeptWithNext": 208 | break; 209 | case "ColumnBreakBefore": 210 | break; 211 | case "PageBreakBefore": 212 | break; 213 | case "FlowDirection": 214 | break; 215 | 216 | // Table/Image attributes 217 | // ---------------- 218 | case "Width": 219 | css = "width:" + _xamlReader.Value + ";"; 220 | break; 221 | case "Height": 222 | css = "height:" + _xamlReader.Value + ";"; 223 | break; 224 | case "ColumnSpan": 225 | htmlWriter.WriteAttributeString("colspan", _xamlReader.Value); 226 | break; 227 | case "RowSpan": 228 | htmlWriter.WriteAttributeString("rowspan", _xamlReader.Value); 229 | break; 230 | 231 | // Hyperlink Attributes 232 | case "NavigateUri": 233 | htmlWriter.WriteAttributeString("href", _xamlReader.Value); 234 | break; 235 | 236 | case "TargetName": 237 | htmlWriter.WriteAttributeString("target", _xamlReader.Value); 238 | break; 239 | } 240 | 241 | if (css != null) 242 | inlineStyle.Append(css); 243 | } 244 | 245 | if (borderSet) 246 | inlineStyle.Append("border-style:solid;mso-element:para-border-div;"); 247 | 248 | // Return the xamlReader back to element level 249 | _xamlReader.MoveToElement(); 250 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.Element); 251 | } 252 | 253 | private static string ParseXamlColor(string color) 254 | { 255 | // Remove transparency value 256 | if (color.StartsWith("#")) 257 | color = "#" + color.Substring(3); 258 | return color; 259 | } 260 | 261 | private static string ParseXamlThickness(string thickness) 262 | { 263 | var values = thickness.Split(','); 264 | for (var i = 0; i < values.Length; i++) 265 | { 266 | if (double.TryParse(values[i], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double value)) 267 | values[i] = Math.Ceiling(value).ToString(CultureInfo.InvariantCulture); 268 | else 269 | values[i] = "1"; 270 | } 271 | 272 | string cssThickness; 273 | switch (values.Length) 274 | { 275 | case 1: 276 | cssThickness = thickness; 277 | break; 278 | case 2: 279 | cssThickness = values[1] + " " + values[0]; 280 | break; 281 | case 4: 282 | cssThickness = values[1] + " " + values[2] + " " + values[3] + " " + values[0]; 283 | break; 284 | default: 285 | cssThickness = values[0]; 286 | break; 287 | } 288 | 289 | return cssThickness; 290 | } 291 | 292 | private void WriteElementContent(XmlTextWriter htmlWriter, StringBuilder inlineStyle) 293 | { 294 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.Element); 295 | 296 | var elementContentStarted = false; 297 | 298 | if (_xamlReader.IsEmptyElement) 299 | { 300 | if (htmlWriter == null || inlineStyle.Length <= 0) return; 301 | // Output STYLE attribute and clear inlineStyle buffer. 302 | htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); 303 | inlineStyle.Remove(0, inlineStyle.Length); 304 | } 305 | else 306 | { 307 | while (ReadNextToken() && _xamlReader.NodeType != XmlNodeType.EndElement) 308 | { 309 | switch (_xamlReader.NodeType) 310 | { 311 | case XmlNodeType.Element: 312 | if (_xamlReader.Name.Contains(".")) 313 | AddComplexProperty(htmlWriter, inlineStyle); 314 | else 315 | { 316 | if (htmlWriter != null && !elementContentStarted && inlineStyle.Length > 0) 317 | { 318 | // Output STYLE attribute and clear inlineStyle buffer. 319 | htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); 320 | inlineStyle.Remove(0, inlineStyle.Length); 321 | } 322 | elementContentStarted = true; 323 | WriteElement(htmlWriter, inlineStyle); 324 | } 325 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.EndElement || _xamlReader.NodeType == XmlNodeType.Element && _xamlReader.IsEmptyElement); 326 | break; 327 | case XmlNodeType.Comment: 328 | if (htmlWriter != null) 329 | { 330 | if (!elementContentStarted && inlineStyle.Length > 0) 331 | { 332 | htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); 333 | inlineStyle.Remove(0, inlineStyle.Length); 334 | } 335 | htmlWriter.WriteComment(_xamlReader.Value); 336 | } 337 | elementContentStarted = true; 338 | break; 339 | case XmlNodeType.CDATA: 340 | case XmlNodeType.Text: 341 | case XmlNodeType.SignificantWhitespace: 342 | if (htmlWriter != null) 343 | { 344 | if (!elementContentStarted && inlineStyle.Length > 0) 345 | { 346 | htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); 347 | inlineStyle.Remove(0, inlineStyle.Length); 348 | } 349 | htmlWriter.WriteString(_xamlReader.Value); 350 | } 351 | elementContentStarted = true; 352 | break; 353 | } 354 | } 355 | 356 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.EndElement); 357 | } 358 | } 359 | 360 | private void AddComplexProperty(XmlWriter htmlWriter, StringBuilder inlineStyle) 361 | { 362 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.Element); 363 | 364 | if (htmlWriter != null && _xamlReader.Name == "Image.Source") 365 | { 366 | if (ReadNextToken() && _xamlReader.Name == "BitmapImage") 367 | { 368 | var imageUri = _xamlReader.GetAttribute("UriSource"); 369 | if (!string.IsNullOrWhiteSpace(imageUri)) 370 | { 371 | // check new image 372 | if (!_htmlResult.Content.ContainsKey(imageUri)) 373 | { 374 | var entryPath = string.Concat("Xaml/", imageUri).Replace("/./", "/"); 375 | var imageEntry = _zip.GetEntry(entryPath); 376 | using (var stream = imageEntry.Open()) 377 | { 378 | var image = ToByteArray(stream); 379 | var identicalContent = _htmlResult.Content.FirstOrDefault(kvp => image.SequenceEqual(kvp.Value)); 380 | var isIdentical = !default(KeyValuePair).Equals(identicalContent); 381 | if (isIdentical) 382 | imageUri = identicalContent.Key; 383 | else 384 | { 385 | imageUri = string.Concat(ContentUriPrefix, imageUri).Replace("/./", "/"); 386 | _htmlResult.Content[imageUri] = image; 387 | } 388 | } 389 | } 390 | 391 | // width & height 392 | if (inlineStyle != null && inlineStyle.Length > 0) 393 | { 394 | htmlWriter.WriteAttributeString("style", inlineStyle.ToString()); 395 | inlineStyle.Remove(0, inlineStyle.Length); 396 | } 397 | 398 | htmlWriter.WriteAttributeString("src", imageUri); 399 | 400 | while (_xamlReader.NodeType != XmlNodeType.EndElement) 401 | ReadNextToken(); 402 | return; 403 | } 404 | } 405 | } 406 | 407 | if (inlineStyle != null && _xamlReader.Name.EndsWith(".TextDecorations")) 408 | inlineStyle.Append("text-decoration:underline;"); 409 | 410 | // Skip the element representing the complex property 411 | WriteElementContent(null, null); 412 | } 413 | 414 | private void WriteElement(XmlTextWriter htmlWriter, StringBuilder inlineStyle) 415 | { 416 | Debug.Assert(_xamlReader.NodeType == XmlNodeType.Element); 417 | 418 | if (htmlWriter == null) 419 | { 420 | // Skipping mode; recurse into the xaml element without any output 421 | WriteElementContent(null, null); 422 | } 423 | else 424 | { 425 | string htmlElementName; 426 | 427 | switch (_xamlReader.Name) 428 | { 429 | case "Run": 430 | case "Span": 431 | htmlElementName = "span"; 432 | break; 433 | case "InlineUIContainer": 434 | htmlElementName = "span"; 435 | break; 436 | case "Bold": 437 | htmlElementName = "b"; 438 | break; 439 | case "Italic": 440 | htmlElementName = "i"; 441 | break; 442 | case "Paragraph": 443 | htmlElementName = "p"; 444 | break; 445 | case "BlockUIContainer": 446 | htmlElementName = "div"; 447 | break; 448 | case "Section": 449 | htmlElementName = "div"; 450 | break; 451 | case "Table": 452 | htmlElementName = "table"; 453 | break; 454 | case "TableColumn": 455 | htmlElementName = "col"; 456 | break; 457 | case "TableRowGroup": 458 | htmlElementName = "tbody"; 459 | break; 460 | case "TableRow": 461 | htmlElementName = "tr"; 462 | break; 463 | case "TableCell": 464 | htmlElementName = "td"; 465 | break; 466 | case "List": 467 | var marker = _xamlReader.GetAttribute("MarkerStyle"); 468 | if (marker == null || marker == "None" || marker == "Disc" || marker == "Circle" || marker == "Square" || marker == "Box") 469 | htmlElementName = "ul"; 470 | else 471 | htmlElementName = "ol"; 472 | break; 473 | case "ListItem": 474 | htmlElementName = "li"; 475 | break; 476 | case "Hyperlink": 477 | htmlElementName = "a"; 478 | break; 479 | case "Image": 480 | htmlElementName = "img"; 481 | break; 482 | default: 483 | htmlElementName = null; // Ignore the element 484 | break; 485 | } 486 | 487 | if (htmlElementName != null) 488 | { 489 | htmlWriter.WriteStartElement(htmlElementName); 490 | WriteFormattingProperties(htmlWriter, inlineStyle); 491 | WriteElementContent(htmlWriter, inlineStyle); 492 | htmlWriter.WriteEndElement(); 493 | } 494 | else 495 | { 496 | // Skip this unrecognized xaml element 497 | WriteElementContent(null, null); 498 | } 499 | } 500 | } 501 | 502 | private bool ReadNextToken() 503 | { 504 | while (_xamlReader.Read()) 505 | { 506 | Debug.Assert(_xamlReader.ReadState == ReadState.Interactive, "Reader is expected to be in Interactive state (" + _xamlReader.ReadState + ")"); 507 | switch (_xamlReader.NodeType) 508 | { 509 | case XmlNodeType.Element: 510 | case XmlNodeType.EndElement: 511 | case XmlNodeType.None: 512 | case XmlNodeType.CDATA: 513 | case XmlNodeType.Text: 514 | case XmlNodeType.SignificantWhitespace: 515 | return true; 516 | 517 | case XmlNodeType.Whitespace: 518 | if (_xamlReader.XmlSpace == XmlSpace.Preserve) 519 | return true; 520 | // ignore insignificant whitespace 521 | break; 522 | 523 | case XmlNodeType.EndEntity: 524 | case XmlNodeType.EntityReference: 525 | // Implement entity reading 526 | //xamlReader.ResolveEntity(); 527 | //xamlReader.Read(); 528 | //ReadChildNodes( parent, parentBaseUri, xamlReader, positionInfo); 529 | break; // for now we ignore entities as insignificant stuff 530 | 531 | case XmlNodeType.Comment: 532 | return true; 533 | } 534 | } 535 | return false; 536 | } 537 | 538 | private static byte[] ToByteArray(Stream stream) 539 | { 540 | var memoryStream = stream as MemoryStream; 541 | if (memoryStream != null) return memoryStream.ToArray(); 542 | using (memoryStream = new MemoryStream()) 543 | { 544 | stream.CopyTo(memoryStream); 545 | return memoryStream.ToArray(); 546 | } 547 | } 548 | } 549 | } --------------------------------------------------------------------------------