Bug 167290 - Since version 24 of LibreOffice, the PDF/A-1 export is invalid with java API
Summary: Since version 24 of LibreOffice, the PDF/A-1 export is invalid with java API
Status: UNCONFIRMED
Alias: None
Product: LibreOffice
Classification: Unclassified
Component: Writer (show other bugs)
Version:
(earliest affected)
24.2.4.2 release
Hardware: x86-64 (AMD64) Windows (All)
: medium normal
Assignee: Not Assigned
URL:
Whiteboard:
Keywords:
Depends on:
Blocks: PDF-Export
  Show dependency treegraph
 
Reported: 2025-06-30 07:26 UTC by d.maurel
Modified: 2025-06-30 14:22 UTC (History)
0 users

See Also:
Crash report or crash signature:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description d.maurel 2025-06-30 07:26:37 UTC
Description:
A code that worked in previous versions no longer works in LibreOffice 24 and 25. The method that checks the PDF/A-1 format returns the following errors:

Le fichier n'est pas valide PDF/A-1b voici les 1 erreur(s) :
7.2 : Error on MetaData, CreationDate present in the document catalog dictionary doesn't match with XMP information

Steps to Reproduce:
1.To generate pdf file from odt file

StringBuffer _sbUrl = new StringBuffer( fileDest.startsWith( "file:///" ) ? "" : "file:///" );

_sbUrl.append( fileDest );

PropertyValue[] mediaDescriptorProperties = new PropertyValue[4];
PropertyValue[] filtersProperties = new PropertyValue[4];

// Utiliser la norme A1
filtersProperties[0] = new PropertyValue();
filtersProperties[0].Name = "SelectPdfVersion";
filtersProperties[0].Value = 1;

filtersProperties[1] = new PropertyValue();
filtersProperties[1].Name = "UseTaggedPDF";
filtersProperties[1].Value = Boolean.TRUE;

filtersProperties[2] = new PropertyValue();
filtersProperties[2].Name = "UseLosslessCompression";
filtersProperties[2].Value = Boolean.TRUE;

filtersProperties[3] = new PropertyValue();
filtersProperties[3].Name = "Quality";
filtersProperties[3].Value = 100;

PropertyValue[] filterData = null;
if (pages != null && pages.length() > 0 && !"All".equals( pages )) {
	filterData = new PropertyValue[1];
	filterData[0] = new PropertyValue();
	filterData[0].Name = "PageRange";
	filterData[0].Value = pages;
}

mediaDescriptorProperties[0] = new PropertyValue();
mediaDescriptorProperties[0].Name = "FilterName";
mediaDescriptorProperties[0].Value = "writer_pdf_Export";

mediaDescriptorProperties[1] = new PropertyValue();
mediaDescriptorProperties[1].Name = "FilterData";
mediaDescriptorProperties[1].Value = filtersProperties;

mediaDescriptorProperties[3] = new PropertyValue();
mediaDescriptorProperties[3].Name = "Overwrite";
mediaDescriptorProperties[3].Value = new Boolean( true );

if (filterData != null) {
	mediaDescriptorProperties[2] = new PropertyValue();
	mediaDescriptorProperties[2].Name = "FilterData";
	mediaDescriptorProperties[2].Value = filterData;
} else {
	mediaDescriptorProperties[2] = new PropertyValue();
	mediaDescriptorProperties[2].Name = "PageRange";
	mediaDescriptorProperties[2].Value = pages;
}

String adresseFichier = _sbUrl.toString().replace( ".odt", ".pdf" ).replace( ".rtf", ".pdf" );

try {
           XStorable xstorable = (XStorable) UnoRuntime.queryInterface( XStorable.class, _xcomponent );
	   xstorable.storeToURL( adresseFichier, mediaDescriptorProperties );// On enregistre le pdf
     }catch(Exception e){
         e.printStackTrace();
     }


*******************************************************************************************************************
2.To control the pdf a1 validity
package isc.template.pdf;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
import org.apache.pdfbox.preflight.Format;
import org.apache.pdfbox.preflight.PreflightDocument;
import org.apache.pdfbox.preflight.ValidationResult;
import org.apache.pdfbox.preflight.ValidationResult.ValidationError;
import org.apache.pdfbox.preflight.exception.SyntaxValidationException;
import org.apache.pdfbox.preflight.parser.PreflightParser;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.DublinCoreSchema;
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
import org.apache.xmpbox.schema.XMPBasicSchema;
import org.apache.xmpbox.type.BadFieldValueException;
import org.apache.xmpbox.xml.XmpSerializer;

import isc.appli.template.ooo.OooWriter;
import isc.appli.util.AppliInitializer;
import isc.appli.util.AppliToolkit;
import isc.appli.web.spring.ServeurSpringConnector;
import isc.util.text.TextUtil;

public class PdfA1 {
	public static boolean controleValidite( String fileName ) {

		ValidationResult result = null;

		try {
			PreflightParser parser = new PreflightParser( fileName );

			/*
			 * Parse the PDF file with PreflightParser that inherits from the NonSequentialParser. Some additional controls are present to check a set of PDF/A requirements. (Stream length
			 * consistency, EOL after some Keyword...)
			 */
			PDDocument pd = parser.parse( Format.PDF_A1A );

			/*
			 * Once the syntax validation is done, the parser can provide a PreflightDocument (that inherits from PDDocument) This document process the end of PDF/A validation.
			 */
			PreflightDocument document = (PreflightDocument) pd;

			// Get validation result
			result = document.validate();

			document.close();

		} catch (SyntaxValidationException e) {
			/*
			 * the parse method can throw a SyntaxValidationException if the PDF file can't be parsed. In this case, the exception contains an instance of ValidationResult
			 */
			result = e.getResult();
		} catch (Exception e) {
			/*
			 * the parse method can throw a SyntaxValidationException if the PDF file can't be parsed. In this case, the exception contains an instance of ValidationResult
			 */
			e.printStackTrace();
			return false;
		}

		AppliToolkit.afficher( "¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤" );

		// display validation result
		if (result.isValid()) {
			AppliToolkit.afficher( "Le fichier " + fileName + " est valide PDF/A-1b" );
		} else {

			System.out.println( "***************************************************" );

			AppliToolkit
					.afficher( "Le fichier " + fileName + " n'est pas valide PDF/A-1b voici les " + result.getErrorsList().size() + " erreur(s) :" );

			for (ValidationError error : result.getErrorsList()) {
				AppliToolkit.afficher( error.getErrorCode() + " : " + error.getDetails() );
			}
			System.out.println( "***************************************************" );
		}

		AppliToolkit.afficher( "¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤" );

		return result.isValid();
	}

	private static void createPDF( String file ) {
		try {
			// Creating PDF document object

			PDDocument document = Loader.loadPDF( new File( "c:\\fichiers_travail\\testa1.pdf" ) );

			isc.util.log.LogUtil.getLogger().info( "NOMBRE de pages: " + document.getNumberOfPages() );

			// Color profile
			// Create output intent
			InputStream colorProfile = PdfA1.class.getResourceAsStream( "/pdfbox/pdfa/sRGB.icc" );
			PDOutputIntent intent = new PDOutputIntent( document, colorProfile );
			intent.setInfo( "sRGB IEC61966-2.1" );
			intent.setOutputCondition( "sRGB IEC61966-2.1" );
			intent.setOutputConditionIdentifier( "sRGB IEC61966-2.1" );
			intent.setRegistryName( "http://www.color.org" );
			document.getDocumentCatalog().addOutputIntent( intent );

			// load the font as this needs to be embedded
			InputStream arialFont = PdfA1.class.getResourceAsStream( "/pdfbox/ttf/arial.ttf" );
			PDFont font = PDType0Font.load( document, arialFont );

			// A PDF/A file needs to have the font embedded if the font is used for text rendering
			// in rendering modes other than text rendering mode 3.
			//
			// This requirement includes the PDF standard fonts, so don't use their static PDFType1Font classes such as
			// PDFType1Font.HELVETICA.
			//
			// As there are many different font licenses it is up to the developer to check if the license terms for the
			// font loaded allows embedding in the PDF.
			//
			if (!font.isEmbedded()) {
				throw new IllegalStateException( "PDF/A compliance requires that all fonts used for"
						+ " text rendering in rendering modes other than rendering mode 3 are embedded." );
			}

			// add XMP metadata
			XMPMetadata xmp = XMPMetadata.createXMPMetadata();

			try {
				DublinCoreSchema dc = xmp.createAndAddDublinCoreSchema();
				dc.setTitle( file );
				// Format A1

				PDFAIdentificationSchema id = xmp.createAndAddPDFAIdentificationSchema();
				id.setPart( 1 );
				id.setConformance( "B" );
				// Creation date doit apparaitre si elle est dans les infos du doc, idem pour le producer
				XMPBasicSchema basicSchema = xmp.createAndAddXMPBasicSchema();
				basicSchema.setCreateDate( document.getDocumentInformation().getCreationDate() );

				// Le producer gène, soit on le place sur un schéma soit on le retire
				document.getDocumentInformation().setProducer( null );

				XmpSerializer serializer = new XmpSerializer();
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				serializer.serialize( xmp, baos, true );

				PDMetadata metadata = new PDMetadata( document );
				metadata.importXMPMetadata( baos.toByteArray() );
				document.getDocumentCatalog().setMetadata( metadata );
			} catch (BadFieldValueException e) {
				// won't happen here, as the provided value is valid
				throw new IllegalArgumentException( e );
			}

			// Saving the document
			document.save( file );
			System.out.println( "PDF created" );
			// Closing the document
			document.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static String getODTAsPDFA1( String file ) {
		OooWriter ooo = new OooWriter();

		ooo.open( "file:///" + file, false, true, false, true, false );
		return getWriterAsPDFA1( ooo, file );
	}

	public static String getWriterAsPDFA1( OooWriter ooo, String file ) {
		try {
			String fileDest = TextUtil.replace( TextUtil.replace( TextUtil.replace( file, ".odt", ".pdf" ), "ODT", "pdf" ), "\\", "/" );
			ooo.saveAsPdf( fileDest, null );
			ooo.close();
			boolean valide = PdfA1.controleValidite( fileDest );
			if (!valide) {
				return null;
			}
			isc.util.log.LogUtil.getLogger().info( "transformation en pdf " );
			isc.util.log.LogUtil.getLogger().info( "source " + file );
			isc.util.log.LogUtil.getLogger().info( "dest " + fileDest );
			return fileDest;
		} catch (Exception e) {
			isc.util.log.LogUtil.getLogger().error( e.getMessage(), e );
			return null;
		}

	}

	/**
	 * Obtenir un PDF A1 depuis un PDF: utilise le serveur de webservice qui utilise ghostscript en ligne de commande
	 * 
	 * @param file
	 * @return
	 */
	private static String getPDFAsPDFA1( String file ) {

		if (controleValidite( file )) {
			return file;
		}

		String fichierConverti = ServeurSpringConnector.pdf2pdfA1Service( file );// Creating PDF document object

		if (controleValidite( fichierConverti )) {
			return fichierConverti;
		}

		return null;
	}

	/**
	 * Méthode pour ODT et PDF qui renvoie dans tous les cas un PDFA1
	 * 
	 * @param file
	 * @return
	 */
	public static String getFileAsPDFA1( String file ) {
		if (file.toUpperCase().endsWith( ".PDF" )) {
			return getPDFAsPDFA1( file );
		} else {
			return getODTAsPDFA1( file );
		}
	}

	/**
	 * @param file
	 *            Le nom du fichier d'origine qui n'est pas PDF/A1 : courrier_genere_timestamp.pdf
	 * @return Le nom du fichier qui est PDF/A1 et avec une date création ok : courrier_genere_timestamp_corrected.pdf
	 */
	public static String correctCreationDateGetPDFA1( String file ) {

		String adresseFichierPdf = excludeFilePrefixGetPath( file );
		String destFile = TextUtil.replace( adresseFichierPdf, ".pdf", "_corrected.pdf" );

		Calendar creationDate = null;
		try (FileInputStream fis = new FileInputStream( adresseFichierPdf ); BufferedReader br = new BufferedReader( new InputStreamReader( fis ) )) {

			String ligne;
			while ((creationDate == null) && (ligne = br.readLine()) != null) {
				String buffer = ligne;

				if (buffer.trim().startsWith( "<xmp:CreateDate>" )) {
					String xmpCreateDate = buffer.substring( buffer.indexOf( "<xmp:CreateDate>" ) + "<xmp:CreateDate>".length(),
							buffer.indexOf( "</xmp:CreateDate>" ) );
					System.out.println( "xmp create date trouvée : " + xmpCreateDate );

					xmpCreateDate = xmpCreateDate.replace( "-", "" ).replace( "T", "" ).replace( ":", "" ).replace( "+0200", "" ).replace( "+0100",
							"" );
					System.out.println( "xmp create date après replace : " + xmpCreateDate );

					Date d = new SimpleDateFormat( "yyyyMMddHHmmss" ).parse( xmpCreateDate );
					creationDate = Calendar.getInstance();
					creationDate.setTimeInMillis( d.getTime() );
				}

			}
		} catch (FileNotFoundException e) {
			isc.util.log.LogUtil.getLogger().error( e.getMessage(), e );
		} catch (IOException e) {
			isc.util.log.LogUtil.getLogger().error( e.getMessage(), e );
		} catch (ParseException e) {
			isc.util.log.LogUtil.getLogger().error( e.getMessage(), e );
		}
		if (creationDate != null) {

			try {

				PreflightParser parser = new PreflightParser( adresseFichierPdf );

				/*
				 * Parse the PDF file with PreflightParser that inherits from the NonSequentialParser. Some additional controls are present to check a set of PDF/A requirements. (Stream length
				 * consistency, EOL after some Keyword...)
				 */
				PDDocument pd = parser.parse( Format.PDF_A1A );

				/*
				 * Once the syntax validation is done, the parser can provide a PreflightDocument (that inherits from PDDocument) This document process the end of PDF/A validation.
				 */
				PreflightDocument document = (PreflightDocument) pd;

				PDDocumentInformation info = document.getDocumentInformation();
				info.setCreationDate( creationDate );
				document.setDocumentInformation( info );
				document.save( destFile );
				return destFile;
			} catch (Exception e) {
				isc.util.log.LogUtil.getLogger().error( e.getMessage(), e );
			}
		}
		return null;
	}

	public static String excludeFilePrefixGetPath( String file ) {
		return TextUtil.replace( TextUtil.replace( TextUtil.replace( file, "file:///", "" ), "file://", "" ), "file:/", "" );
	}

	// PdfA1.java
	public static void main( String[] args ) {
		AppliInitializer.initializeMyApplication();
		controleValidite( "C:/temp/courrier_genere_1729858583829_2.pdf" );
		System.exit( 0 );
	}
}





Actual Results:
Le fichier toto.pdf n'est pas valide PDF/A-1b voici les 1 erreur(s) :
7.2 : Error on MetaData, CreationDate present in the document catalog dictionary doesn't match with XMP information

Expected Results:
Le fichier toto.pdf est valide PDF/A-1b


Reproducible: Always


User Profile Reset: No

Additional Info:
Does not work with the latest version either (Libre office 25.2.4)
Comment 1 Mike Kaganski 2025-06-30 07:53:28 UTC
Is using Java an important part here? Did you check, if doing this from UI gives the same problem? Because in the current form, your report limits who can reproduce and work on this.
Comment 2 d.maurel 2025-06-30 13:29:20 UTC
Hello

thank you for your reply. Yes, using Java is important because our application relies on this API, but I believe the same issue occurs even with macros. If we use the GUI and export through the menu, the generated PDF file produces the same error.
Best regards