Skip to main content
Skip table of contents

Embedding files and adding custom XMP with PDF/A-3b

📌 Overview

This issue relates to two operations with PDF/A-3b - embedding files and adding custom XMP packets. As of 2025, electronic invoicing based on Zugferd will be mandatory in Germany which means that some customers will have to embed different file types in a PDF/A-3. Additionally, sometimes, when writing files with any of the PDF/A presets or PDFVersion parameter set, Mako will drop XMP properties if they cannot be parsed correctly.

(question) Issue Description

This customer needed to create a PDF/A-3b file with a custom .xml file attachment. They also needed to add custom XMP metadata that is preserved in the PDF output. They used the XmpCore library to generate the custom XMP, which was not being parsed by Mako.

💡 Solution

  • Embedding an xml file is demonstrated in the sample below using the IDocument::addEmbeddedStream() method.

  • XMP generated by XmpCore is slightly edited by removing a rogue question mark at the start.

  • The example also makes use of the setIgnoreXmp(false) feature introduced in Mako 8, which ensures that the custom XMP is preserved in the final output.

⌨️ Sample Code

Here is the C# code that was provided to the customer. You can also find it on our GitHub Gists page, see CustomXmlAndXmp.cs.

Custom XMP C# Example
C#
/* --------------------------------------------------------------------------------
 *  <copyright file="Program.cs" company="Global Graphics Software Ltd">
 *    Copyright (c) 2025 Global Graphics Software Ltd. All rights reserved.
 *  </copyright>
 *  <summary>
 *    This example is provided on an "as is" basis and without warranty of any kind.
 *    Global Graphics Software Ltd. does not warrant or make any representations
 *    regarding the use or results of use of this example.
 *  </summary>
 * ---------------------------------------------------------------------------------
 */

using JawsMako;
using XmpCore;
using XmpCore.Impl;
using XmpCore.Options;

namespace XMPissue;


internal class Program
{
    static int Main(string[] args)
    {
        try
        {
            var testFilepath = "..\\..\\..\\..\\TestFiles\\";
            Directory.CreateDirectory(testFilepath);
            using var mako = IJawsMako.create();
            IJawsMako.enableAllFeatures(mako);
            using var factory = mako.getFactory();
            var assembly = IDocumentAssembly.create(mako);
            var document = IDocument.create(mako);
            var page = IPage.create(mako);
            var content = IDOMFixedPage.create(factory, 210 / 25.4 * 96, 297 / 25.4 * 96); // XPS units
            page.setContent(content);
            document.appendPage(page);
            assembly.appendDocument(document);

            // Create ZugFERD XMP
            var xmp = XmpMetaFactory.Create();
            XmpMetaFactory.SchemaRegistry.RegisterNamespace(@"urn:ferd:pdfa:invoice:rc#", "zf");
            var namespaces = XmpMetaFactory.SchemaRegistry.Namespaces;
            xmp.SetProperty(@"urn:ferd:pdfa:invoice:rc#", "DocumentType", "INVOICE");
            var options = new SerializeOptions();
            options.OmitPacketWrapper = true;
            options.UseCanonicalFormat = true;
            var newXmp = XmpSerializerHelper.SerializeToString((XmpMeta)xmp, options);
            newXmp = newXmp[1..]; //remove leading question mark
            
            // Prepare XMP packet for Mako
            var pair = mako.getTempStore().createTemporaryReaderWriterPair();
            var newXmpStreamReader = pair.inputStream;
            var newXmpStreamWriter = pair.outputStream;
            newXmpStreamWriter.open();
            newXmpStreamWriter.completeWrite(newXmp);
            newXmpStreamReader.close();
            assembly.setXmpPacket(newXmpStreamReader);

            // Add attachment
            IRAInputStream fileStream = IInputStream.createFromFile(mako, testFilepath + "factur-x.xml");
            if (fileStream is not null && fileStream.open())
            { 
                var metadata = new IFileSpecAsEmbeddedData.Data();
                metadata.fileName = "faktur-x.xml";
                metadata.description = "ZUGFeRD-invoice";
                metadata.mimeType = "text/xml";
                metadata.modificationDate = DateTime.Now.ToString();
                metadata.fileSize = (ulong)fileStream.IRAStream_length();
                metadata.fileDataStream = fileStream;
                IFileSpecAsEmbeddedData embeddedFile = IFileSpecAsEmbeddedData.createInstance(mako.getFactory(), metadata);
                if (embeddedFile is not null)
                    document.addEmbeddedStream(embeddedFile);
            }

            var output = IPDFOutput.create(mako);
            output.setPreset("PDF/A-3b");
            output.setIgnoreXmp(false);
            output.setValidateXmp(false);

            output.writeAssembly(assembly, testFilepath + "zugferd.pdf");
        }
        catch (MakoException e)
        {
            Console.WriteLine($"Exception thrown: {e.m_errorCode}: {e.m_msg}");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Exception thrown: {e}");
        }

        return 0;
    }
}

☑️ Conclusion

Incorporating custom XMP metadata and embedding files within a PDF/A-3b document can be crucial for compliance with standards such as Zugferd, especially in regions where electronic invoicing is mandatory. By following the outlined steps and utilizing the provided sample code, users can effectively manage these tasks using Mako.

📚 Additional Resources

If you need additional help, see our API documentation for detailed information on class/method usage, or raise a support ticket via our customer portal.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.