TX Text Control .NET for WPF

Saving and Converting Documents

Topic

Saving is also how a WPF application converts: a document loaded as DOCX, RTF or HTML becomes a PDF file with one call of Save. The choices around that call make the difference, such as saving the whole document or only a part, keeping HTML styles in a separate file, shrinking the images of a PDF or leaving comments out. The Click handlers below convert a folder, save parts of a document and control the output with SaveSettings.

The basics are in Save as DOCX and PDF: saving the editor's document, converting a single file, byte arrays and PDF/A. PDF Processing covers PDF passwords, attachments and metadata. MainWindow.xaml.cs needs these using directives for the examples:

using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using Microsoft.Win32;
using TXTextControl;

Convert a Folder of Documents

The following handler lets the user pick a folder with the OpenFolderDialog of WPF, converts each DOCX, DOC, RTF and HTML file in it to PDF in the subfolder PDF and lists the files that failed:

// The load format for each file extension
private static readonly Dictionary<string, StreamType> SourceFormats =
    new Dictionary<string, StreamType>(StringComparer.OrdinalIgnoreCase)
    {
        { ".docx", StreamType.WordprocessingML },
        { ".doc", StreamType.MSWord },
        { ".rtf", StreamType.RichTextFormat },
        { ".html", StreamType.HTMLFormat }
    };

private void ConvertFolderButton_Click(object sender, RoutedEventArgs e)
{
    OpenFolderDialog dialog = new OpenFolderDialog();

    if (dialog.ShowDialog(this) != true)
    {
        return;
    }

    string outputFolder = System.IO.Path.Combine(dialog.FolderName, "PDF");
    Directory.CreateDirectory(outputFolder);

    int converted = 0;
    List<string> failed = new List<string>();

    using (ServerTextControl tx = new ServerTextControl())
    {
        tx.Create();

        foreach (string file in Directory.GetFiles(dialog.FolderName))
        {
            if (!SourceFormats.TryGetValue(System.IO.Path.GetExtension(file), out StreamType format))
            {
                continue;
            }

            try
            {
                // Load replaces the previous document
                tx.Load(file, format);
                tx.Save(System.IO.Path.Combine(outputFolder,
                    System.IO.Path.GetFileNameWithoutExtension(file) + ".pdf"), StreamType.AdobePDF);
                converted++;
            }
            catch (FilterException exception)
            {
                failed.Add($"{System.IO.Path.GetFileName(file)}: {exception.Reason}");
            }
        }
    }

    MessageBox.Show($"{converted} documents converted.\r\n" + string.Join("\r\n", failed));
}

The code works as follows:

  • OpenFolderDialog of the Microsoft.Win32 namespace is available in WPF on .NET 8 and higher, the versions the TX Text Control package targets. ShowDialog returns true when the user chose a folder.
  • A ServerTextControl converts the files, so the document in the window stays as it is. Its Load replaces the previous file, and one instance handles the whole folder.
  • Save writes the complete document and overwrites a file of the same name.
  • A FilterException with its Reason reports a file that couldn't be converted, and the loop continues.

The handler runs on the UI thread of the window, which doesn't respond until the last file is saved. Background Operations and Using ServerTextControl in Desktop Applications show how to run conversions in the background. For very large batches, Text Control recommends separate processes, because threads that use TX Text Control can wait for each other.

Save With the Save As Dialog Box

With a StreamType and a SaveSettings object, Save opens the Save As dialog box; Open and Save With the Built-In Dialog Boxes describes its file filters, its return value and the options it can offer. When the user has saved, the object tells you what was written:

private void SaveAsButton_Click(object sender, RoutedEventArgs e)
{
    SaveSettings saveSettings = new SaveSettings();

    // Opens the Save As dialog box with the filters of these formats
    TXTextControl.WPF.DialogResult result = textControl1.Save(
        StreamType.WordprocessingML | StreamType.AdobePDF, saveSettings);

    if (result == TXTextControl.WPF.DialogResult.OK)
    {
        MessageBox.Show($"{saveSettings.SavedFile}: {saveSettings.BytesWritten} bytes, " +
                        $"format {saveSettings.SavedStreamType}");
    }
}

SavedFile, BytesWritten and SavedStreamType are the file with its path, its size and its format; SavedFile is empty when the document was saved to a byte array or a string.

Save Part of a Document

Selection.Save saves only the selected text to a file, a byte array or a string, and Paragraph.Save saves a single paragraph to a byte array or a string:

private void SavePartButton_Click(object sender, RoutedEventArgs e)
{
    // The paragraph at the input position as an HTML string
    TXTextControl.Paragraph paragraph = textControl1.Paragraphs.GetItem(textControl1.Selection.Start);

    string paragraphHtml;
    paragraph.Save(out paragraphHtml, StringStreamType.HTMLFormat);

    // The selected text as RTF and as a DOCX file
    if (textControl1.Selection.Length > 0)
    {
        string rtf;
        textControl1.Selection.Save(out rtf, StringStreamType.RichTextFormat);

        textControl1.Selection.Save("excerpt.docx", StreamType.WordprocessingML);
    }

    MessageBox.Show(paragraphHtml);
}

ParagraphCollection.GetItem returns the paragraph at a zero-based text position. Only complete documents can be saved as PDF and PDF/A.

Save HTML With Stylesheets and Images

HTML output depends on two settings. SaveSettings.CssSaveMode decides where the formatting is written:

ValueThe formatting is written
NoneInto the HTML tags of the body
InlineAs stylesheets in the head of the HTML document
CreateFileInto a CSS file with the name of the HTML file, or the name in CssFileName, saved beside it; an existing file is kept
OverwriteFileLike CreateFile, but an existing CSS file is overwritten

With SaveSettings.ImageSaveMode set to SaveAsData, the images are embedded in the HTML. The following handler writes an HTML file with a separate CSS file and an HTML string with the styles in its head:

private void SaveHtmlButton_Click(object sender, RoutedEventArgs e)
{
    // An HTML file with a stylesheet file of the same name, newsletter.css
    SaveSettings fileSettings = new SaveSettings()
    {
        CssSaveMode = CssSaveMode.OverwriteFile,
        ImageSaveMode = ImageSaveMode.SaveAsData
    };

    textControl1.Save("newsletter.html", StreamType.HTMLFormat, fileSettings);

    // An HTML string can't have a stylesheet file, so the styles go into the head
    SaveSettings stringSettings = new SaveSettings()
    {
        CssSaveMode = CssSaveMode.Inline,
        ImageSaveMode = ImageSaveMode.SaveAsData
    };

    string html;
    textControl1.Save(out html, StringStreamType.HTMLFormat, stringSettings);
}

When HTML is saved to a string or a byte array, CreateFile and OverwriteFile can't be used.

Reduce the Size of PDF Files

Most of the size of a PDF file with photos is image data. Two settings reduce it:

private void SmallPdfButton_Click(object sender, RoutedEventArgs e)
{
    SaveSettings saveSettings = new SaveSettings()
    {
        ImageCompressionQuality = 70, // 1 to 100
        ImageMaxResolution = 150      // dpi; larger images are reduced
    };

    textControl1.Save("invoice.pdf", StreamType.AdobePDF, saveSettings);
}
  • ImageCompressionQuality sets the quality of lossy compression from 1 to 100 for images compressed with a lossy algorithm; 0 selects a default.
  • ImageMaxResolution reduces images whose resolution is higher. The default, 0, keeps each image's resolution.

Suitable values for documents read on screen and printed are compared in the blog article Best Practices for Image Compression when Exporting to PDF in .NET C#.

Leave Out Content and Add Properties

SaveSettings.OmittedContent keeps parts of the document out of the saved file, such as the comments of an invoice that goes to a customer. The document in the window keeps them:

private void SaveForCustomerButton_Click(object sender, RoutedEventArgs e)
{
    UserDefinedPropertyDictionary properties = new UserDefinedPropertyDictionary();
    properties.Add("InvoiceNumber", "INV-2025-001");
    properties.Add("AmountDue", 299.97);

    SaveSettings saveSettings = new SaveSettings()
    {
        DocumentTitle = "Invoice INV-2025-001",
        UserDefinedDocumentProperties = properties,
        OmittedContent = OmittedContent.Comments
    };

    textControl1.Save("invoice-customer.docx", StreamType.WordprocessingML, saveSettings);
}

OmittedContent takes these values, and none of them can be used with PDF and PDF/A:

ValueLeaves out
Comments, FootnotesComments and footnotes
TextFieldsAll kinds of text fields
TrackedChangesThe markup of tracked changes; text and formatting are saved
SubTextParts, EditableRegions, TablesOfContentsThe markers or data of these elements; text and formatting are saved
SectionsThe section data

UserDefinedDocumentProperties takes strings, Boolean values, integers and double values, and DocumentTitle replaces the title of DocumentSettings for this save.

Save to Streams

The stream overload of Save takes a FileStream. For any other stream, save to a byte array first:

private void SaveToStreamButton_Click(object sender, RoutedEventArgs e)
{
    // A FileStream is written at its current position
    using (FileStream fileStream = new FileStream("invoice.rtf", FileMode.Create))
    {
        textControl1.Save(fileStream, StreamType.RichTextFormat);
    }

    // Other streams: save to a byte array and wrap it
    byte[] pdf;
    textControl1.Save(out pdf, BinaryStreamType.AdobePDF);

    using (MemoryStream memoryStream = new MemoryStream(pdf))
    {
        MessageBox.Show($"Stream length: {memoryStream.Length}");
    }
}

What Changes When Converting

Each format stores different things, so a conversion can change a document:

  • Plain text keeps the text only, with a carriage return and a line feed after each paragraph.
  • Word fields are imported as fields only when LoadSettings.ApplicationFieldFormat is set before loading; see Loading Documents.
  • Images keep their original format unless SaveSettings.ImageExportFilterIndex selects another one. When the target format can't store it, the image is saved in the default image format of the target, as Control How Images Are Saved lists per document format.
  • Fonts missing on the computer that converts are replaced, which can move line and page breaks. Font Embedding and Substitution shows how to detect this.
  • PDF/A embeds its fonts, so they must be embeddable, as PDF/A describes.

When a converted document doesn't look right, see Document Conversion.

Next Steps