Handling Untrusted Documents
A WPF application that lets users open any file they receive will sooner or later meet a document that is damaged, mislabeled, encrypted or full of links to places nobody checked. None of this has to reach your users unfiltered. TX Text Control lets your code limit the formats of the Open dialog box, leave out images and links, report files that don't load, decide what a clicked link opens and keep file references in a folder of your choice.
The samples go into MainWindow.xaml.cs of a window with a TextControl named textControl1, as in Create Your First WPF Application. Add using System.Diagnostics;, using System.IO; and using TXTextControl; to the top of the file.
Limit What the Open Dialog Box Offers
WPF.TextControl.Load, called with a StreamType and LoadSettings, shows the Open dialog box with the formats you combine with | as its file filters. This handler of an Open button accepts DOCX, RTF and PDF, loads neither images nor hypertext links and catches files that can't be loaded:
private void OpenButton_Click(object sender, RoutedEventArgs e)
{
LoadSettings loadSettings = new LoadSettings()
{
// Don't load images and links of the document
LoadImages = false,
LoadHypertextLinks = false
};
try
{
// The Open dialog box offers only these formats
if (textControl1.Load(StreamType.WordprocessingML | StreamType.RichTextFormat | StreamType.AdobePDF,
loadSettings) == TXTextControl.WPF.DialogResult.OK)
{
Title = System.IO.Path.GetFileName(loadSettings.LoadedFile);
}
}
catch (FilterException exception)
{
// For example WrongFileType, NotWellFormed or InvalidPassword
MessageBox.Show(this, "The document can't be opened: " + exception.Reason);
}
}
The code works as follows:
- Only the combined
StreamTypevalues appear as file filters. Leave out formats your application has no use for. - LoadImages and LoadHypertextLinks set to
falseleave out images and links where only the text counts, such as in a preview; Control the Import With LoadSettings lists these and the other load settings. - In WPF,
Loadreturns a TXTextControl.WPF.DialogResult, not theDialogResultof Windows Forms. AfterOK, LoadedFile holds the path of the file. - A file that can't be loaded throws a FilterException; the next section covers its reasons.
React to Files That Fail to Load
Reason tells why a file failed. With documents from outside, expect reasons such as WrongFileType for a file that isn't what its extension says, NotWellFormed or BadToken for a damaged file, FileTooBig, Encrypted for an encryption scheme TX Text Control doesn't support and InvalidPassword for a PDF that needs a user password; Read the Reason of a FilterException lists them all.
Put every Load of a document from outside into a try block, and tell the user what went wrong instead of letting the exception end the application. A PDF with a user password is loaded again with the password, as Load Password-Protected PDF Documents shows; the encryption TX Text Control can import is listed in Encrypted PDF Documents. Document Conversion helps with files that should load and don't.
Decide What a Link Opens
Hypertext links in a document can lead to websites, local files or other programs. A click on a link in the TextControl has no effect until your application handles HypertextLinkClicked. Attach the handler in MainWindow.xaml:
<WPF:TextControl x:Name="textControl1" HypertextLinkClicked="textControl1_HypertextLinkClicked" />
The handler opens web addresses only, after asking the user:
private void textControl1_HypertextLinkClicked(object sender, HypertextLinkEventArgs e)
{
// Open web addresses only, and only when the user agrees
if (Uri.TryCreate(e.HypertextLink.Target, UriKind.Absolute, out Uri uri)
&& (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp))
{
if (MessageBox.Show(this, "Open " + uri.AbsoluteUri + "?", "Open Link",
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true });
}
}
}
The code works as follows:
- HypertextLinkEventArgs.HypertextLink is the link that was clicked, and Target is where it points, exactly as the document says.
- The message box shows the real target, which can differ from the text of the link.
- Any other target, such as a path to a file or a program, is ignored. The TX Words sample Open Hyperlink opens linked files with a new instance of the application; if you do the same, decide first which files your application may open.
Links to bookmarks in the same document raise DocumentLinkClicked instead and stay inside the document; Fields, Hyperlinks, and Bookmarks covers both kinds of links.
Keep File References in One Folder
Images and link targets can be stored in a document as absolute file paths of the computer it was created on. ImageSearchPath sets a folder that replaces all absolute file paths used in the document, so that these resources are looked for in that folder only:
LoadSettings loadSettings = new LoadSettings()
{
// Resolve the file paths of the document in this folder only
ImageSearchPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Resources")
};
textControl1.Load("received.rtf", StreamType.RichTextFormat, loadSettings);
Redact Text
Covering a name with a black rectangle hides it only on screen: the name stays in the document, where a search finds it and a copy brings it back. Real redaction deletes the text and puts a black box of the same size where it was. The following handler redacts the text the user has selected, one line at a time. Add using System.Collections.Generic; and using System.Text; to the file as well:
private void RedactButton_Click(object sender, RoutedEventArgs e)
{
int start = textControl1.Selection.Start;
int end = start + textControl1.Selection.Length;
// One black box for each line of the selection
List<(int Start, int Length, System.Drawing.Rectangle Bounds)> boxes =
new List<(int, int, System.Drawing.Rectangle)>();
int boxStart = -1, boxEnd = -1, boxLine = -1;
System.Drawing.Rectangle boxBounds = System.Drawing.Rectangle.Empty;
for (int position = start; position <= end; position++)
{
// TextChars counts from 1, text positions from 0
TextChar textChar = position < end ? textControl1.TextChars[position + 1] : null;
TXTextControl.Line line = position < end ? textControl1.Lines.GetItem(position) : null;
// Close the box at the end of the selection, at a break and at a new line
if (boxStart >= 0 && (textChar == null || char.IsControl(textChar.Char) || line.Number != boxLine))
{
boxes.Add((boxStart, boxEnd - boxStart + 1, boxBounds));
boxStart = -1;
}
// Spaces and breaks don't start a box
if (textChar == null || char.IsWhiteSpace(textChar.Char) || char.IsControl(textChar.Char))
{
continue;
}
// From the top of the character to the baseline of its line, in twips
System.Drawing.Rectangle bounds = textChar.Bounds;
bounds.Height = line.Baseline - bounds.Top;
if (boxStart < 0)
{
boxStart = position;
boxLine = line.Number;
boxBounds = bounds;
}
else
{
boxBounds = System.Drawing.Rectangle.Union(boxBounds, bounds);
}
boxEnd = position;
}
// Replace the text from the last box to the first, so that the positions stay valid
for (int i = boxes.Count - 1; i >= 0; i--)
{
textControl1.Select(boxes[i].Start, boxes[i].Length);
textControl1.Selection.Text = "";
byte[] svg = CreateBlackBox(boxes[i].Bounds.Size);
using (MemoryStream stream = new MemoryStream(svg, 0, svg.Length, false, true))
{
// -1 inserts the image at the input position, where the text was
textControl1.Images.Add(new TXTextControl.Image(stream: stream), -1);
}
}
}
private static byte[] CreateBlackBox(System.Drawing.Size size)
{
// 20 twips are one point
string svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" " +
$"width=\"{size.Width / 20f}pt\" height=\"{size.Height / 20f}pt\">" +
"<rect width=\"100%\" height=\"100%\" fill=\"black\" /></svg>";
return Encoding.UTF8.GetBytes(svg);
}
The code works as follows:
- Selection.Start and Selection.Length give the selected range. TextChars holds the characters of the main text, counted from 1: text position 0 is
TextChars[1]. - TextChar.Bounds is a
System.Drawing.Rectanglein twips, measured from the upper left corner of the document; the full type namesSystem.Drawing.RectangleandTXTextControl.Lineavoid a clash with theRectangleandLineshapes ofSystem.Windows.Shapes, which the Visual Studio template imports. LineCollection.GetItem returns the Line of a position: a new Number starts a new box, and the Baseline is the bottom of the box. - Selection.Text set to an empty string deletes the text of a box, and ImageCollection.Add with
-1inserts the SVG image inline at that position. CreateBlackBoxgives the SVG its size in points, a twentieth of the twips. The Image constructor gets the stream as the named argumentstream:, because its overload for aSystem.Drawing.Imagewould otherwise need the System.Drawing.Common library, as Images, Text Frames, and Shapes explains.
In the editor, black boxes take the place of the selected names and addresses:

Save the result as PDF, for example with textControl1.Save("contract-redacted.pdf", StreamType.AdobePDF), before it leaves your application. The text behind the boxes isn't in the PDF, so nobody can select, copy or find it:

The handler follows the technique of the blog article True Document and PDF Text Redaction in .NET C#. In its sample, the user marks several passages as SubTextPart objects named tx_redact first, and all of them are redacted together; the repository TextControl/Redaction holds its Redaction class, written for ServerTextControl. A Rule-Based PHI and PII Risk Scanner for Documents Using C# .NET helps to find what to redact: it checks the text of a document against regular expressions defined in JSON rules and reports each match with its location.
Next Steps
- PDF Passwords and Permissions: encrypted PDF documents and their permissions.
- Loading and Saving Documents: all load and save options of the WPF
TextControl. - Document Protection and User Permissions: control what users change in the documents you hand out.
- Security Overview: the security model of a WPF application with TX Text Control.