📌 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.
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.