TX Text Control .NET for WPF

Attachments

Topic

Invoices often travel as a PDF with a machine-readable copy inside: the XML of a ZUGFeRD invoice, the JSON data of a report or the spreadsheet behind a contract. With TX Text Control .NET for WPF, such embedded files are part of the document in the TextControl. Code attaches them before the document is saved as PDF, the DocumentSettings panel of the WPF sidebar lets users manage them, and a loaded PDF brings its attachments along. The sections below cover the classes, the relationship and MIME type that PDF/A-3 asks for, electronic invoices and extracting attachments from received PDF files.

For a first PDF saved from the editor window, see Create a PDF in Getting Started.

How Attachments Work

Each attachment is an EmbeddedFile: a file name, the file's data and optional properties. Only PDF and PDF/A carry embedded files. They are written when the document is saved with StreamType.AdobePDF or StreamType.AdobePDFA, and every other format leaves them out.

PropertyContains
FileNameThe file name, optionally with a relative or absolute path; read only
DataThe content, as a string or a byte array; read only
MIMETypeThe MIME type, for example application/json; application/octet-stream if none is set
RelationshipHow the file relates to the document; PDF/A only
DescriptionA description of the file
CreationDate, LastModificationDateThe dates of the file; if null, the PDF document's dates are used

The EmbeddedFile constructor takes the file name, the data as a byte array or a string, and XMP metadata or null. XMP metadata consists of rdf:Description elements, which are added to the metadata of the PDF, as electronic invoices need. Another overload takes a SafeFileHandle and reads the file only when the document is saved.

Attachments live in three properties:

Choose the Relationship and MIME Type

A PDF/A-3 document needs a MIME type and a relationship for each embedded file. The relationship is a string, with these predefined values:

RelationshipUse it for
SourceThe document the PDF was made from, for example a DOCX file
DataData for software, such as XML, JSON or CSV
AlternativeAnother form of the same content, such as the XML of a ZUGFeRD or Factur-X invoice
SupplementAdditional material, such as appendices
UnspecifiedAnything else

Typical MIME types are application/xml, application/json, text/csv, image/png and image/jpeg. The blog article Why Defining MIME Types for PDF/A Attachments Is Essential describes what validators check.

Attach a File When Saving

The following Click handler goes into MainWindow.xaml.cs, which needs using System;, using System.IO;, using System.Windows; and using TXTextControl;. It embeds data.json from the application folder in the document of textControl1 and saves a PDF/A file:

private void attachDataButton_Click(object sender, RoutedEventArgs e)
{
    // PDF/A requires fonts that can be embedded
    textControl1.FontSettings.EmbeddableFontsOnly = true;

    EmbeddedFile data = new EmbeddedFile("data.json", File.ReadAllBytes("data.json"), null)
    {
        Description = "Invoice data",
        MIMEType = "application/json",
        Relationship = "Data",
        CreationDate = DateTime.Now
    };

    SaveSettings saveSettings = new SaveSettings()
    {
        EmbeddedFiles = new EmbeddedFile[] { data }
    };

    textControl1.Save("invoice.pdf", StreamType.AdobePDFA, saveSettings);
}

The code works as follows:

  • FontSettings.EmbeddableFontsOnly allows only fonts that can be embedded, a requirement of PDF/A. The rules of the format are in PDF/A.
  • EmbeddedFiles is an array, so a single Save embeds as many files as you pass.
  • The array in SaveSettings applies to this file only. Assign it to textControl1.DocumentSettings.EmbeddedFiles instead to make the attachment part of the document for every later save.

A PDF reader lists the attachments in a separate panel. In Adobe Acrobat, it shows each file with its description:

Adobe Acrobat with a contract PDF and its Attachments panel, listing agreement.docx, data.json, data.xlsx and thumbnail.jpg.

Let Users Manage Attachments in the Sidebar

Set the ContentLayout of a WPF Sidebar to DocumentSettings, and the sidebar shows the properties of the document. Its Embedded Files in PDF category lists DocumentSettings.EmbeddedFiles: users add a file with Add..., write one to disk with Save..., delete it with Remove and edit its description, relationship and MIME type with Edit.... For a file added this way, the panel picks the MIME type from the file name extension.

The following XAML docks such a sidebar at the left of the editor, inside the DockPanel of MainWindow.xaml and before the TextControl element. The WPF prefix is the namespace declaration Visual Studio adds with the first TX Text Control element:

<WPF:Sidebar x:Name="sidebarLeft" DockPanel.Dock="Left" HorizontalAlignment="Left"
             ContentLayout="DocumentSettings" IsShown="False" TextControl="textControl1" />

With HorizontalAlignment set to Left, the panel uses its vertical layout. A button opens it:

private void documentSettingsButton_Click(object sender, RoutedEventArgs e)
{
    sidebarLeft.ContentLayout = Sidebar.SidebarContentLayout.DocumentSettings;
    sidebarLeft.IsShown = true;
}

Sidebar is in the namespace TXTextControl.WPF, so MainWindow.xaml.cs needs using TXTextControl.WPF; for this handler. The same panel edits the title, author, subject, keywords and custom properties, as Document Metadata shows. Its elements are named after the values of DocumentSettingsItem, such as TXITEM_EmbeddedFilesPanel, which FrameworkElement.FindName finds when you want to hide one. Sidebar and Panels covers the sidebar in general.

Create an Electronic Invoice

A ZUGFeRD or Factur-X invoice is a PDF/A-3 document that people read, with the invoice data attached as XML that accounting software processes. XRechnung XML can be embedded in the same way since ZUGFeRD 2.1. TX Text Control creates the readable part and embeds the XML, which comes from your ERP system or a library such as ZUGFeRD-csharp.

The following handler attaches factur-x.xml to the invoice in the editor, passes the XMP metadata of metadata.xml that declares the standard, and saves PDF/A-3:

private void createEInvoiceButton_Click(object sender, RoutedEventArgs e)
{
    // PDF/A requires fonts that can be embedded
    textControl1.FontSettings.EmbeddableFontsOnly = true;

    // The XML invoice, created by your ERP system or an e-invoice library
    byte[] invoiceXml = File.ReadAllBytes("factur-x.xml");

    // rdf:Description elements that declare the invoice standard
    string metadata = File.ReadAllText("metadata.xml");

    EmbeddedFile invoice = new EmbeddedFile("factur-x.xml", invoiceXml, metadata)
    {
        Description = "factur-x",
        MIMEType = "application/xml",
        Relationship = "Alternative",
        LastModificationDate = DateTime.Now
    };

    SaveSettings saveSettings = new SaveSettings()
    {
        EmbeddedFiles = new EmbeddedFile[] { invoice }
    };

    textControl1.Save("e-invoice.pdf", StreamType.AdobePDFA, saveSettings);
}

metadata.xml holds the XMP extension schema of ZUGFeRD: conformance level, document type, version and the name of the embedded file. Its full content, and XML created with ZUGFeRD-csharp, are in the blog article Creating ZUGFeRD 2.3 (XRechnung, Factur-X) Documents with .NET C#. Validate the result with a ZUGFeRD or Factur-X validation service, and consider signing the PDF so that changes to the invoice become detectable; see Digital Signatures.

Extract Attachments From a PDF

The static class Attachments of the TXTextControl.DocumentServer.PDF namespace reads the attachments of a PDF file without loading it into the editor. Its namespace contains a second class named EmbeddedFile, so importing it next to TXTextControl makes the name ambiguous. Use an alias instead: using Pdf = TXTextControl.DocumentServer.PDF;.

The following handler asks for a PDF file and a target folder with the dialogs of Microsoft.Win32 (using Microsoft.Win32;) and saves each attachment into the folder:

private void extractAttachmentsButton_Click(object sender, RoutedEventArgs e)
{
    OpenFileDialog openDialog = new OpenFileDialog() { Filter = "PDF documents (*.pdf)|*.pdf" };
    OpenFolderDialog folderDialog = new OpenFolderDialog();

    if (openDialog.ShowDialog(this) != true || folderDialog.ShowDialog(this) != true)
    {
        return;
    }

    Pdf.EmbeddedFile[] files = Pdf.Attachments.GetFromFile(openDialog.FileName)
        ?? Array.Empty<Pdf.EmbeddedFile>();

    foreach (Pdf.EmbeddedFile file in files)
    {
        // The file name can contain a path
        string path = System.IO.Path.Combine(folderDialog.FolderName, System.IO.Path.GetFileName(file.FileName));

        if (file.HasBinaryData)
        {
            File.WriteAllBytes(path, file.BinaryData);
        }
        else if (file.HasStringData)
        {
            File.WriteAllText(path, file.StringData);
        }
    }

    MessageBox.Show(this, $"{files.Length} attachments saved to {folderDialog.FolderName}");
}

The code works as follows:

The XML of an e-invoice is the attachment with the relationship Alternative and a MimeType that ends with xml, because older invoices use text/xml and newer ones application/xml.

Keep, Add and Replace Attachments

A PDF loaded into textControl1 puts its attachments into DocumentSettings.EmbeddedFiles, and saving as PDF embeds them again. A user who opens, edits and saves a PDF therefore keeps its attachments. With SaveSettings.EmbeddedFiles, one save gets exactly the array you pass. To add a file to the existing ones, extend the array of DocumentSettings (the handler needs using System.Collections.Generic;):

private void addAttachmentButton_Click(object sender, RoutedEventArgs e)
{
    // The attachments of the loaded PDF, or none
    List<EmbeddedFile> files = new List<EmbeddedFile>(
        textControl1.DocumentSettings.EmbeddedFiles ?? Array.Empty<EmbeddedFile>());

    files.Add(new EmbeddedFile("line-items.csv", File.ReadAllBytes("line-items.csv"), null)
    {
        Description = "Line items",
        MIMEType = "text/csv"
    });

    // Saved with the document from now on
    textControl1.DocumentSettings.EmbeddedFiles = files.ToArray();

    textControl1.Save("invoice-with-line-items.pdf", StreamType.AdobePDF);
}

Attachments are loaded only if LoadSettings.PDFImportSettings contains LoadEmbeddedFiles. When you set the value yourself, for example to PDFImportSettings.GenerateLines, add LoadEmbeddedFiles to it; the other import settings are described in Loading Documents. The saved file is a new PDF built from the editor's content, carrying the same attachments.

Next Steps