Introduction/Background
Some time ago, I needed to construct Rich Text Format reports, and I decided to write my own library from scratch as I couldn't find any suitable solution on the web.
The main idea was to create a library that could be easily expanded, so that developers could add any feature from the RTF Specification that is not implemented by default. Look at the second part of this article for details.
Using the Code
The RtfDocument
class has a constructor that takes a RtfCodepage
enum value as parameter. I tested the library with Windows-1251 Cyrillic encoding, and I'm quite sure it works just as well with others. Unicode is supported too.
RtfDocument rtf = new RtfDocument();
We go on with adding fonts and colors to the specified tables. Later, we will use indexes to refer to them (that's certainly the simplest but not the best implementation). RtfDocument.DefaultFont
defines the index of the font used for paragraphs with no FontIndex
set explicitly.
rtf.FontTable.Add(new RtfFont("Calibri"));
rtf.FontTable.Add(new RtfFont("Constantia"));
rtf.ColorTable.AddRange(new RtfColor[] {
new RtfColor(Color.Red),
new RtfColor(0, 0, 255)
});
The contents of the document are paragraphs, formatted paragraphs, table rows, and tables. Let's create a header paragraph with centered 16pt text.
RtfFormattedParagraph header =
new RtfFormattedParagraph(new RtfParagraphFormatting(16, RtfTextAlign.Center));
Add some text, formatted text, and an empty paragraph to the header:
header.AppendText("Calibri");
header.AppendText(new RtfFormattedText(" Bold", RtfCharacterFormatting.Bold));
header.AppendParagraph();
Add another paragraph with a different formatting. We set FontIndex
to 1 and IndentLeft
to 6.05cm. Most of the indents and widths are set in twips. TwipConverter
converts millimeters, centimeters, and points to twips, and vice versa.
RtfFormattedParagraph p =
new RtfFormattedParagraph(new RtfParagraphFormatting(12, RtfTextAlign.Left));
p.Formatting.FontIndex = 1;
p.Formatting.IndentLeft = TwipConverter.ToTwip(6.05F, MetricUnit.Centimeter);
p.AppendText("Constantia");
p.AppendText(new RtfFormattedText("Superscript", RtfCharacterFormatting.Superscript));
Here is an example demonstrating inline font size change. The font index -1 to be ignored.
p.AppendParagraph(new RtfFormattedText("Inline", -1, 8));
p.AppendText(new RtfFormattedText(" font size ", -1, 14));
p.AppendText(new RtfFormattedText("change", -1, 8));
Pictures are supported in different output formats. JPEG and PNG cannot be read by WordPad, so it's better to use WMF for compatibility. The conversion to WMF is done with P/Invoke calls, and the credits for this part go to David Bennett.
RtfImage picture = new RtfImage(Properties.Resources.lemon, RtfImageFormat.Wmf);
picture.ScaleX = 50;
picture.ScaleY = 50;
p.AppendParagraph(picture);
A hyperlink with common formatting:
RtfFormattedText linkText =
new RtfFormattedText("View article", RtfCharacterFormatting.Underline, 2);
p.AppendParagraph(new RtfHyperlink("RtfConstructor.aspx", linkText));
A centered table with 2 columns and 3 rows:
RtfTable t1 = new RtfTable(RtfTableAlign.Center, 2, 3);
The cells can be merged both horizontally and vertically:
t1.MergeCellsVertically(1, 0, 2);
Formatting to use within cells:
RtfParagraphFormatting LeftAligned12 = new RtfParagraphFormatting(12, RtfTextAlign.Left);
RtfParagraphFormatting Centered10 = new RtfParagraphFormatting(10, RtfTextAlign.Center);
The table cell class derives from formatted paragraph, and has some additional properties.
t1[0, 0].Definition.Style =
new RtfTableCellStyle(RtfBorderSetting.None, LeftAligned12,
RtfTableCellVerticalAlign.Bottom);
t1[0, 0].AppendText("Bottom");
t1[1, 0].Definition.Style =
new RtfTableCellStyle(RtfBorderSetting.Left, Centered10,
RtfTableCellVerticalAlign.Center,
RtfTableCellTextFlow.BottomToTopLeftToRight);
t1[1, 1].Definition.Style = t1[1, 0].Definition.Style;
t1[1, 0].AppendText("Vertical");
We set TextColorIndex
of the cell to 1, and add RtfFormattedText
with different colors.
t1[0, 1].Formatting = new RtfParagraphFormatting(10, RtfTextAlign.Center);
t1[0, 1].Formatting.TextColorIndex = 1;
t1[0, 1].AppendText(new RtfFormattedText("Black", 0));
t1[0, 1].AppendText(" Red ");
t1[0, 1].AppendText(new RtfFormattedText("Blue", 2));
This part shows an example of bitwise operations on RtfCharacterFormatting
:
t1[0, 2].AppendText("Normal");
t1[1, 2].AppendText(new RtfFormattedText("Italic",
RtfCharacterFormatting.Caps | RtfCharacterFormatting.Italic));
t1[1, 2].AppendParagraph("+");
t1[1, 2].AppendParagraph(new RtfFormattedText("Caps",
RtfCharacterFormatting.Caps | RtfCharacterFormatting.Italic));
Adding the contents to the document:
rtf.Contents.AddRange(new IRtfDocumentPart[] {
header,
t1,
p,
});
When the document is complete, we use RtfWriter
to convert it to RTF code:
RtfWriter rtfWriter = new RtfWriter();
TextWriter writer = new StreamWriter("test.rtf");
rtfWriter.Write(writer, rtf);
And voila, the resulting file as seen in Microsoft Word:
And that's the RTF code:
{\rtf1\ansi\ansicpg1252\deffont0\deflang1033
{\fonttbl {\f0\fnil\fcharset1\fprq0 Calibri;}{\f1\fnil\fcharset1\fprq0 Constantia;}}
{\colortbl ;\red255\green0\blue0;\red0\green0\blue255;}
\pard\plain\qc\fi0\li0\ri0\sl0\sb0\sa240\fs32 Calibri
{\b Bold}\par\trowd\trrh1134\trqc\clvertalb\cltxlrtb\cellx1701\clvmgf
\clvertalc\clbrdrl\brdrw10\brdrs\cltxbtlr\cellx2835\pard\
intbl\plain\ql\fi0\li0\ri0\sl0\sb0\sa0\fs24 Bottom\cell\pard\intbl
\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20 Vertical\cell\row\
trowd\trrh1134\trqc\clvertalc\cltxlrtb\cellx1701\clvmrg\clvertalc
\clbrdrl\brdrw10\brdrs\cltxbtlr\cellx2835\pard\intbl\plain\
qc\fi0\li0\ri0\sl0\sb0\sa0\cf1\fs20{\cf0 Black } Red {\cf2 Blue}\cell
\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20\cell\row\
trowd\trrh1134\trqc\clvertalc\cltxlrtb\cellx1701\clvertalc\cltxlrtb
\cellx2835\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20 Normal\
cell\pard\intbl\plain\qc\fi0\li0\ri0\sl0\sb0\sa0\fs20{\i\caps I
talic}\par +\par{\i\caps Caps}\cell\row\pard\plain\ql\fi0\li3430\
ri0\sl0\sb120\sa0\f1\fs24 Constantia {\super Superscript}\par{
\fs16 Inline}{\fs28 font size }{\fs16 change}\par{\pict\picscalex50\
picscaley50\picw7938\pich11509\picwgoal4500\pichgoal6525
\wmetafile8
...
}
\par{\field{\fldinst HYPERLINK "http://www.codeproject.com/KB/
cs/RtfConstructor.aspx"}{\fldrslt{\cf2\cb1\ul View article}}}}
The code above has been wrapped to prevent scrolling.
Reflection Part
The conversion of RtfDocument
to RTF code is done using Reflection.
Each class representing a control word has a specific attribute which is recognized by the RtfWriter
. And that's what makes it easy to expand the library and add your own classes to support more control words. It is somehow similar to what you do when you add System.Xml.Serialization
attributes to classes and members for serialization purposes. Except that you will have to study the RTF specification.
It comes as no surprise that the most used attribute is RtfControlWord
. If its RtfControlWord.Name
property is not set, RtfWriter
uses the member name for the control word.
[RtfControlWord]
public int Red { get; set; }
Values of int
members are appended to control words. RtfWriter
ignores members marked with the RtfIndex
attribute if their value is -1.
[RtfControlWord("cf"), RtfIndex]
public int TextColorIndex { get; set; }
The enums are special case, and they need the RtfEnumAsControlWord
attribute as there are different ways they can be treated.
[RtfEnumAsControlWord(RtfEnumConversion.UseAttribute)]
public enum RtfTableAlign
{
[RtfControlWord("trql")]
Left,
[RtfControlWord("trqc")]
Center,
[RtfControlWord("trqr")]
Right
}
[RtfEnumAsControlWord(RtfEnumConversion.UseValue, Prefix = "fprq")]
public enum RtfFontPitch
{
Default,
Fixed,
Variable
}
[RtfEnumAsControlWord(RtfEnumConversion.UseName)]
public enum RtfDocumentCharacterSet
{
ANSI,
Mac,
PC,
PCa
}
Some members not marked with RtfControlWord
must be included, and RtfInclude
tells the RtfWriter
to do that.
public class RtfTable
{
[RtfInclude]
public RtfTableRowCollection Rows
{
get { return _rows; }
}
}
Some members should be included only if some conditions are met, and that's where the RtfInclude.ConditionMember
property comes in handy.
[RtfControlWord("clbrdrt"), RtfInclude(ConditionMember = "IsTopBorderSet")]
public RtfBorder Top
{
get { return top; }
}
public bool IsTopBorderSet
{
get { return Top.Width > 0; }
}
A bool
member marked with RtfControlWord
is included only if its value is true
.
[RtfControlWord("b")]
public bool Bold = false;
Some control words are paired, like /trowd to start a table row, and /row to end it. The RtfControlWordDenotingEnd
attribute is used to define the second one.
[RtfControlWord("pard"), RtfControlWordDenotingEnd("cell")]
public class RtfTableCell {
}
Some members must be ignored, and RtfIgnore
is used for that.
public class RtfTableCell
{
[RtfIgnore]
public RtfTable Table
{
get { return RowInternal.Table; }
}
}
The RtfEnclosingBraces
attribute and its RtfEnclosingBraces.ClosingSemicolon
property speak for themselves.
[RtfControlWord("rtf1"), RtfEnclosingBraces]
public class RtfDocument {
}
[RtfControlWord("f", IsIndexed = true),
RtfEnclosingBraces(ClosingSemicolon = true)]
public class RtfFont {
}
Two attributes left unmentioned are RtfTextData
to mark text, and RtfControlGroup
which was added for some reason I don't remember. Only font and color tables are marked with it.
Points of Interest
Writing the RtfDocument
with a lot of content takes a considerable time, but only once as the information about the types' attributes and members is stored in specific classes. Please take a look at the RtfDocumentInfo
, RtfTypeInfo
, and RtfAttributeInfo
classes.
History
- 16.08.10: Code revised, added support for tabs (not shown in the article).
- 07.08.10: Added support for images, hyperlinks, inline color, and font formatting.
- 05.08.10: Reflection Part added.
- 02.08.10: Text revised, RTF code added.
- 30.07.10: Initial release.