├── 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 | }
--------------------------------------------------------------------------------