.gitignore                              |    1 
 Makefile.fetch                          |    1 
 Makefile.in                             |    1 
 bin/verapdf.sh.in                       |    2 
 config_host.mk.in                       |    1 
 configure.ac                            |   27 +++++++++
 download.lst                            |    5 +
 include/test/unoapi_test.hxx            |    7 +-
 test/source/unoapi_test.cxx             |   87 +++++++++++++++++++-------------
 vcl/qa/cppunit/pdfexport/pdfexport.cxx  |    3 +
 vcl/qa/cppunit/pdfexport/pdfexport2.cxx |   10 +++
 11 files changed, 109 insertions(+), 36 deletions(-)

New commits:
commit 7039373704b2e9c78662c2e44ae81aea522b5f7b
Author:     Xisco Fauli <[email protected]>
AuthorDate: Mon Feb 2 15:38:09 2026 +0100
Commit:     Xisco Fauli <[email protected]>
CommitDate: Fri Feb 20 08:31:47 2026 +0100

    tdf#136822: Add VeraPDF as a PDF validator
    
    For now, only validate those tests that are
    conformant with the validator
    
    verapdf-cli-1.29.0.jar has been generated using
    https://github.com/veraPDF/veraPDF-apps/pull/415
    and running 'mvn clean install'
    Change-Id: Ic172100a120dc0f43f6de526660961d34dc6b415
    
    Change-Id: Icacc8ccbd938f779b506fb5e1820f10a539ab77b
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198556
    Reviewed-by: Xisco Fauli <[email protected]>
    Tested-by: Jenkins

diff --git a/.gitignore b/.gitignore
index ed69ef94f996..e621db8d34ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,7 @@
 /bin/bffvalidator.sh
 /bin/odfvalidator.sh
 /bin/officeotron.sh
+/bin/verapdf.sh
 /hardened_runtime.xcent
 /lo.xcent
 /vs-code.code-workspace.template
diff --git a/Makefile.fetch b/Makefile.fetch
index caed43c085d5..29a9fc6e4b14 100644
--- a/Makefile.fetch
+++ b/Makefile.fetch
@@ -252,6 +252,7 @@ $(WORKDIR)/download: $(BUILDDIR)/config_$(gb_Side).mk 
$(SRCDIR)/download.lst $(S
                $(call fetch_Optional,OPENSYMBOL,OPENSYMBOL_TTF) \
                $(call fetch_Optional,ODFVALIDATOR,ODFVALIDATOR_JAR) \
                $(call fetch_Optional,OFFICEOTRON,OFFICEOTRON_JAR) \
+               $(call fetch_Optional,VERAPDF,VERAPDF_JAR) \
        ,$(call 
fetch_Download_item,https://dev-www.libreoffice.org/extern,$(item)))
        -@mkdir -p $(TARFILE_LOCATION)/cargo
        $(if $(call fetch_Optional,YRS,1),\
diff --git a/Makefile.in b/Makefile.in
index 340ebf7321bd..f754c4c3ec76 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -226,6 +226,7 @@ distclean : clean compilerplugins-clean 
mac-app-store-package.clean
         $(BUILDDIR)/bin/bffvalidator.sh \
         $(BUILDDIR)/bin/odfvalidator.sh \
         $(BUILDDIR)/bin/officeotron.sh \
+        $(BUILDDIR)/bin/verapdf.sh \
         $(BUILDDIR)/config.Build.log \
         $(BUILDDIR)/config.Build.warn \
         $(BUILDDIR)/config.log \
diff --git a/bin/verapdf.sh.in b/bin/verapdf.sh.in
new file mode 100644
index 000000000000..bc536eeb9e4e
--- /dev/null
+++ b/bin/verapdf.sh.in
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+java -jar @TARFILE_LOCATION_NATIVE@/@VERAPDF_JAR@ --nonpdfext "$@"
diff --git a/config_host.mk.in b/config_host.mk.in
index 2103814da8d0..45940b6b258b 100644
--- a/config_host.mk.in
+++ b/config_host.mk.in
@@ -766,6 +766,7 @@ export VCL_PLUGIN_INFO=@VCL_PLUGIN_INFO@
 export VCTOOLSET=@VCTOOLSET@
 export VCVER=@VCVER@
 export DEVENV=@DEVENV@
+export VERAPDF=@VERAPDF@
 export VISIO_CFLAGS=$(gb_SPACE)@VISIO_CFLAGS@
 export VISIO_LIBS=$(gb_SPACE)@VISIO_LIBS@
 export WGET=@WGET@
diff --git a/configure.ac b/configure.ac
index ccfcb348c26e..2693dfde888b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6197,6 +6197,7 @@ if test "$cross_compiling" = "yes"; then
         bin/bffvalidator.sh.in \
         bin/odfvalidator.sh.in \
         bin/officeotron.sh.in \
+        bin/verapdf.sh.in \
         instsetoo_native/util/openoffice.lst.in \
         config_host/*.in \
         sysui/desktop/macosx/Info.plist.in \
@@ -9653,6 +9654,31 @@ if test "$with_export_validation" != "no"; then
         else
             OFFICEOTRON="sh $OFFICEOTRON"
         fi
+
+        AC_PATH_PROGS(VERAPDF, verapdf)
+        if test -z "$VERAPDF"; then
+            # remember to download the verapdf with validator later
+            AC_MSG_NOTICE([no verapdf found, will download it])
+            BUILD_TYPE="$BUILD_TYPE VERAPDF"
+            VERAPDF="$BUILDDIR/bin/verapdf.sh"
+
+            # and fetch name of verapdf jar name from download.lst
+            VERAPDF_JAR=`$SED -n -e "s/^VERAPDF_JAR *:= *\(.*\) *//p" 
$SRC_ROOT/download.lst`
+            AC_SUBST(VERAPDF_JAR)
+
+            if test -z "$VERAPDF_JAR"; then
+                AC_MSG_ERROR([cannot determine verapdf jar location 
(--with-export-validation)])
+            fi
+        fi
+        if test "$build_os" = "cygwin"; then
+            # In case of Cygwin it will be executed from Windows,
+            # so we need to run bash and absolute path to validator
+            # so instead of "odfvalidator" it will be
+            # something like "bash.exe C:+            VERAPDF="bash.exe 
`cygpath -m "$VERAPDF"`"
+        else
+            VERAPDF="sh $VERAPDF"
+        fi
     fi
     AC_SUBST(OFFICEOTRON)
 else
@@ -16274,6 +16300,7 @@ AC_CONFIG_FILES([
                  bin/bffvalidator.sh
                  bin/odfvalidator.sh
                  bin/officeotron.sh
+                 bin/verapdf.sh
                  instsetoo_native/util/openoffice.lst
                  sysui/desktop/macosx/Info.plist
                  sysui/desktop/macosx/LaunchConstraint.plist
diff --git a/download.lst b/download.lst
index 4c41f1d1292b..a4f21573ec82 100644
--- a/download.lst
+++ b/download.lst
@@ -539,6 +539,11 @@ OFFICEOTRON_JAR := officeotron-0.8.8.jar
 # three static lines
 # so that git cherry-pick
 # will not run into conflicts
+VERAPDF_SHA256SUM := 
bdeef807f7e883fe3ff4e0a4712dc216064ca670d5c857fbc94408266719f0f4
+VERAPDF_JAR := verapdf-cli-1.29.0.jar
+# three static lines
+# so that git cherry-pick
+# will not run into conflicts
 ONLINEUPDATE_SHA256SUM := 
37206cf981e8409d048b59ac5839621ea107ff49af72beb9d7769a2f41da8d90
 ONLINEUPDATE_TARBALL := 
onlineupdate-c003be8b9727672e7d30972983b375f4c200233f-2.tar.xz
 # three static lines
diff --git a/include/test/unoapi_test.hxx b/include/test/unoapi_test.hxx
index 82baa824d2b0..664d12994a71 100644
--- a/include/test/unoapi_test.hxx
+++ b/include/test/unoapi_test.hxx
@@ -28,7 +28,8 @@ enum ValidationFormat
 {
     OOXML,
     ODF,
-    MSBINARY
+    MSBINARY,
+    PDF
 };
 
 enum class TestFilter
@@ -188,9 +189,9 @@ protected:
 
     rtl::Reference<TestInteractionHandler> xInteractionHandler;
 
-private:
-    void validate(const OUString& rURL, TestFilter eFilter) const;
+    void validate(TestFilter eFilter);
 
+private:
     bool mbSkipValidation;
     OUString m_aBaseString;
 
diff --git a/test/source/unoapi_test.cxx b/test/source/unoapi_test.cxx
index 476005d89be2..ebd4a3f00cff 100644
--- a/test/source/unoapi_test.cxx
+++ b/test/source/unoapi_test.cxx
@@ -100,7 +100,7 @@ constexpr std::u16string_view grand_total = u"Grand total 
of errors in submitted
 }
 #endif
 
-void UnoApiTest::validate(const OUString& rPath, TestFilter eFilter) const
+void UnoApiTest::validate(TestFilter eFilter)
 {
     ValidationFormat eFormat = ValidationFormat::ODF;
     if (eFilter == TestFilter::XLSX)
@@ -123,6 +123,8 @@ void UnoApiTest::validate(const OUString& rPath, TestFilter 
eFilter) const
         eFormat = ValidationFormat::MSBINARY;
     else if (eFilter == TestFilter::PPT)
         eFormat = ValidationFormat::MSBINARY;
+    else if (eFilter == TestFilter::PDF_WRITER)
+        eFormat = ValidationFormat::PDF;
     else
     {
         SAL_INFO("test", "UnoApiTest::validate: unknown filter");
@@ -139,6 +141,10 @@ void UnoApiTest::validate(const OUString& rPath, 
TestFilter eFilter) const
     {
         var = "ODFVALIDATOR";
     }
+    else if (eFormat == ValidationFormat::PDF)
+    {
+        var = "VERAPDF";
+    }
     else if (eFormat == ValidationFormat::MSBINARY)
     {
 #if HAVE_BFFVALIDATOR
@@ -175,7 +181,7 @@ void UnoApiTest::validate(const OUString& rPath, TestFilter 
eFilter) const
     utl::TempFileNamed aOutput;
     aOutput.EnableKillingFile();
     OUString aOutputFile = aOutput.GetFileName();
-    OUString aCommand = aValidator + " " + rPath + " > " + aOutputFile + " 
2>&1";
+    OUString aCommand = aValidator + " " + maTempFile.GetFileName() + " > " + 
aOutputFile + " 2>&1";
 
 #if !defined _WIN32
     // For now, this is only needed by some Linux ASan builds, so keep it 
simply and disable it on
@@ -199,45 +205,59 @@ void UnoApiTest::validate(const OUString& rPath, 
TestFilter eFilter) const
     SAL_INFO("test", "UnoApiTest::validate: executing '" << aCommand << "'");
     int returnValue = system(OUStringToOString(aCommand, 
RTL_TEXTENCODING_UTF8).getStr());
 
-    OString aContentString = loadFile(aOutput.GetURL());
-    OUString aContentOUString = OStringToOUString(aContentString, 
RTL_TEXTENCODING_UTF8);
-
-    if (eFormat == ValidationFormat::OOXML && !aContentOUString.isEmpty())
+    if (eFormat == ValidationFormat::PDF)
+    {
+        SvMemoryStream aStream;
+        SvFileStream aFileStream(aOutput.GetURL(), StreamMode::READ);
+        aStream.WriteStream(aFileStream);
+        aStream.Seek(0);
+        xmlDocUniquePtr pXmlDoc = parseXmlStream(&aStream);
+        // Make sure the output is well-formed.
+        CPPUNIT_ASSERT(pXmlDoc);
+        assertXPath(pXmlDoc, "/report/jobs/job/validationReport", 
"isCompliant", u"true");
+    }
+    else
     {
-        // check for validation errors here
-        sal_Int32 nIndex = aContentOUString.lastIndexOf(grand_total);
-        if (nIndex == -1)
+        OString aContentString = loadFile(aOutput.GetURL());
+        OUString aContentOUString = OStringToOUString(aContentString, 
RTL_TEXTENCODING_UTF8);
+
+        if (eFormat == ValidationFormat::OOXML && !aContentOUString.isEmpty())
         {
-            SAL_WARN("test", "no summary line");
+            // check for validation errors here
+            sal_Int32 nIndex = aContentOUString.lastIndexOf(grand_total);
+            if (nIndex == -1)
+            {
+                SAL_WARN("test", "no summary line");
+            }
+            else
+            {
+                sal_Int32 nStartOfNumber = nIndex + grand_total.size();
+                std::u16string_view aNumber = 
aContentOUString.subView(nStartOfNumber);
+                sal_Int32 nErrors = o3tl::toInt32(aNumber);
+                OString aMsg
+                    = "validation error in OOXML export: Errors: " + 
OString::number(nErrors);
+                if (nErrors)
+                {
+                    SAL_WARN("test", aContentOUString);
+                }
+                CPPUNIT_ASSERT_EQUAL_MESSAGE(aMsg.getStr(), sal_Int32(0), 
nErrors);
+            }
         }
-        else
+        else if (eFormat == ValidationFormat::ODF && 
!aContentOUString.isEmpty())
         {
-            sal_Int32 nStartOfNumber = nIndex + grand_total.size();
-            std::u16string_view aNumber = 
aContentOUString.subView(nStartOfNumber);
-            sal_Int32 nErrors = o3tl::toInt32(aNumber);
-            OString aMsg = "validation error in OOXML export: Errors: " + 
OString::number(nErrors);
-            if (nErrors)
+            if (aContentOUString.indexOf("Error") != -1 || 
aContentOUString.indexOf("Fatal") != -1)
             {
                 SAL_WARN("test", aContentOUString);
+                CPPUNIT_FAIL(aContentString.getStr());
             }
-            CPPUNIT_ASSERT_EQUAL_MESSAGE(aMsg.getStr(), sal_Int32(0), nErrors);
-        }
-    }
-    else if (eFormat == ValidationFormat::ODF && !aContentOUString.isEmpty())
-    {
-        if (aContentOUString.indexOf("Error") != -1 || 
aContentOUString.indexOf("Fatal") != -1)
-        {
-            SAL_WARN("test", aContentOUString);
-            CPPUNIT_FAIL(aContentString.getStr());
         }
+        CPPUNIT_ASSERT_EQUAL_MESSAGE(
+            OString("failed to execute: " + OUStringToOString(aCommand, 
RTL_TEXTENCODING_UTF8)
+                    + "
" + OUStringToOString(aContentOUString, RTL_TEXTENCODING_UTF8))
+                .getStr(),
+            0, returnValue);
     }
-    CPPUNIT_ASSERT_EQUAL_MESSAGE(
-        OString("failed to execute: " + OUStringToOString(aCommand, 
RTL_TEXTENCODING_UTF8) + "
"
-                + OUStringToOString(aContentOUString, RTL_TEXTENCODING_UTF8))
-            .getStr(),
-        0, returnValue);
 #else
-    (void)rPath;
     (void)eFormat;
 #endif
 }
@@ -347,8 +367,9 @@ void UnoApiTest::save(TestFilter eFilter, const 
uno::Sequence<beans::PropertyVal
     css::uno::Reference<frame::XStorable> xStorable(mxComponent, 
css::uno::UNO_QUERY_THROW);
     xStorable->storeToURL(maTempFile.GetURL(), 
aMediaDescriptor.getAsConstPropertyValueList());
 
-    if (!mbSkipValidation)
-        validate(maTempFile.GetFileName(), eFilter);
+    // FIXME: Don't validate pdf files by default for now
+    if (!mbSkipValidation && eFilter != TestFilter::PDF_WRITER)
+        validate(eFilter);
 }
 
 void UnoApiTest::saveAndReload(TestFilter eFilter,
diff --git a/vcl/qa/cppunit/pdfexport/pdfexport.cxx 
b/vcl/qa/cppunit/pdfexport/pdfexport.cxx
index 8a5c73c6f7c4..c41988e4d28f 100644
--- a/vcl/qa/cppunit/pdfexport/pdfexport.cxx
+++ b/vcl/qa/cppunit/pdfexport/pdfexport.cxx
@@ -2385,6 +2385,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf157816)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"tdf157816.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -2787,6 +2788,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf157816Link)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"LinkWithFly.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3178,6 +3180,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf142806)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"LinkPages.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx 
b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
index 4cf9cafce23a..efef680693b4 100644
--- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
+++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
@@ -1473,6 +1473,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf139736)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"tdf139736-1.odt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -1779,6 +1780,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf149140)
     vcl::filter::PDFDocument aDocument;
     
loadFromFile(u"TableTH_test_LibreOfficeWriter7.3.3_HeaderRow-HeadersInTopRow.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -1839,6 +1841,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testNestedSection)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"nestedsection.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -2976,6 +2979,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf154982)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"tdf154982.odt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3386,6 +3390,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf135192)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"tdf135192-1.fodp");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3518,6 +3523,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf154955)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"grouped-shape.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3653,6 +3659,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf155190)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"tdf155190.odt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3743,6 +3750,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testMediaShapeAnnot)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"vid.odt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -3883,6 +3891,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, 
testFlyFrameHyperlinkAnnot)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"image-hyperlink-alttext.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
@@ -4019,6 +4028,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testFormControlAnnot)
     vcl::filter::PDFDocument aDocument;
     loadFromFile(u"formcontrol.fodt");
     save(TestFilter::PDF_WRITER, 
aMediaDescriptor.getAsConstPropertyValueList());
+    validate(TestFilter::PDF_WRITER);
 
     // Parse the export result.
     SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);

Reply via email to