vcl/inc/pdf/COSWriter.hxx         |    6 ++
 vcl/inc/pdf/XmpMetadata.hxx       |    2 
 vcl/inc/pdf/pdfwriter_impl.hxx    |    4 +
 vcl/source/gdi/pdfwriter_impl.cxx |   79 +++++++++++++++++++++++++++++++++-----
 vcl/source/pdf/XmpMetadata.cxx    |   10 +++-
 5 files changed, 88 insertions(+), 13 deletions(-)

New commits:
commit d23c5560c13fc10ef367ae1d3bbf6c790dde2a61
Author:     Tomaž Vajngerl <tomaz.vajng...@collabora.co.uk>
AuthorDate: Wed Jan 22 13:19:43 2025 +0900
Commit:     Tomaž Vajngerl <qui...@gmail.com>
CommitDate: Wed Jan 22 10:11:56 2025 +0100

    tdf#160196 add support for PDF/UA-2 and NS for struct. elem.
    
    This adds support for PDF/UA-2 which is used when using PDF 2.0
    (and PDF/UA-1 when using PDF 1.x).
    
    PDF/UA-2 requires that the PDF 2.0 introduced namespaces are set
    for the structure elements, so this also adds support for the
    namespaces with always setting the PDF 2.0 namespace for all the
    structured elements (we don't use others anyway).
    
    Change-Id: Ifbd8f8126d9bcc0d4dcd06a5f56bacdb0804adbd
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/180565
    Reviewed-by: Tomaž Vajngerl <qui...@gmail.com>
    Tested-by: Jenkins

diff --git a/vcl/inc/pdf/COSWriter.hxx b/vcl/inc/pdf/COSWriter.hxx
index 627aa30b780d..18b8ccdeedcb 100644
--- a/vcl/inc/pdf/COSWriter.hxx
+++ b/vcl/inc/pdf/COSWriter.hxx
@@ -117,6 +117,12 @@ public:
         mrBuffer.append(")");
     }
 
+    void writeKeyAndLiteral(std::string_view key, std::string_view value)
+    {
+        mrBuffer.append(key);
+        writeLiteral(value);
+    }
+
     void writeLiteralEncrypt(std::u16string_view value, sal_Int32 nObject,
                              rtl_TextEncoding nEncoding = 
RTL_TEXTENCODING_ASCII_US);
 
diff --git a/vcl/inc/pdf/XmpMetadata.hxx b/vcl/inc/pdf/XmpMetadata.hxx
index 452ad7f85267..a7cf5a223ff8 100644
--- a/vcl/inc/pdf/XmpMetadata.hxx
+++ b/vcl/inc/pdf/XmpMetadata.hxx
@@ -46,7 +46,7 @@ public:
     OString m_sCreateDate;
 
     sal_Int32 mnPDF_A = 0;
-    bool mbPDF_UA = false;
+    sal_Int32 mnPDF_UA = 0;
 
 public:
     XmpMetadata();
diff --git a/vcl/inc/pdf/pdfwriter_impl.hxx b/vcl/inc/pdf/pdfwriter_impl.hxx
index 2d931ba729f8..636973222e8d 100644
--- a/vcl/inc/pdf/pdfwriter_impl.hxx
+++ b/vcl/inc/pdf/pdfwriter_impl.hxx
@@ -835,6 +835,8 @@ private:
 
     ::comphelper::Hash                      m_DocDigest;
 
+    std::unordered_map<std::string_view, sal_Int32> m_aNamespacesMap;
+
     // reduce repeated allocations
     OStringBuffer                           updateGraphicsStateLine{256};
     OStringBuffer                           drawBitmapLine{80};
@@ -989,6 +991,8 @@ private:
     //check if internal dummy container are needed in the structure elements
     void addInternalStructureContainer( PDFStructureElement& rEle );
     //<---i94258
+    // writes namespaces
+    void emitNamespaces();
     // writes document structure
     sal_Int32 emitStructure( PDFStructureElement& rEle );
     // writes structure parent tree
diff --git a/vcl/source/gdi/pdfwriter_impl.cxx 
b/vcl/source/gdi/pdfwriter_impl.cxx
index 7e048c4d6dd3..5945e43f0460 100644
--- a/vcl/source/gdi/pdfwriter_impl.cxx
+++ b/vcl/source/gdi/pdfwriter_impl.cxx
@@ -111,6 +111,8 @@ static bool g_bDebugDisableCompression = 
getenv("VCL_DEBUG_DISABLE_PDFCOMPRESSIO
 namespace
 {
 
+constexpr std::string_view constNamespacePDF2("http://iso.org/pdf2/ssn";);
+
 constexpr sal_Int32 nLog10Divisor = 3;
 constexpr double fDivisor = 1000.0;
 
@@ -1334,6 +1336,12 @@ PDFWriterImpl::PDFWriterImpl( const 
PDFWriter::PDFWriterContext& rContext,
         m_aContext.Tagged = true;
     }
 
+    // Add common PDF 2.0 namespace when we are using PDF 2.0
+    if (m_aContext.Version == PDFWriter::PDFVersion::PDF_2_0)
+    {
+        m_aNamespacesMap.emplace(constNamespacePDF2, createObject());
+    }
+
     if( m_aContext.DPIx == 0 || m_aContext.DPIy == 0 )
         SetReferenceDevice( VirtualDevice::RefDevMode::PDF1 );
     else
@@ -1933,6 +1941,30 @@ OString PDFWriterImpl::emitStructureAttributes( 
PDFStructureElement& i_rEle )
     return aRet.makeStringAndClear();
 }
 
+// Write the namespace objects to the stream
+void PDFWriterImpl::emitNamespaces()
+{
+    if (m_aContext.Version < PDFWriter::PDFVersion::PDF_2_0)
+        return;
+
+    for (auto&[sNamespace, nObject] : m_aNamespacesMap)
+    {
+        if (!updateObject(nObject))
+            return;
+
+        COSWriter aWriter(m_aContext.Encryption.getParams(), m_pPDFEncryptor);
+        aWriter.startObject(nObject);
+        aWriter.startDict();
+        aWriter.write("/Type", "/Namespace");
+        aWriter.writeKeyAndLiteral("/NS", sNamespace);
+        aWriter.endDict();
+        aWriter.endObject();
+
+        if (!writeBuffer(aWriter.getLine()))
+            m_aNamespacesMap[sNamespace] = 0;
+    }
+}
+
 sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle )
 {
     assert(rEle.m_nOwnElement == 0 || rEle.m_oType);
@@ -1979,10 +2011,21 @@ sal_Int32 PDFWriterImpl::emitStructure( 
PDFStructureElement& rEle )
         nParentTree = createObject();
         if (!nParentTree)
             return 0;
-        aLine.append( "/StructTreeRoot
"
-            "/ParentTree "
-            + OString::number(nParentTree)
-            + " 0 R
" );
+        aLine.append("/StructTreeRoot
");
+        aWriter.writeKeyAndReference("/ParentTree", nParentTree);
+
+        // Write the reference to the PDF 2.0 namespace
+        if (m_aContext.Version >= PDFWriter::PDFVersion::PDF_2_0)
+        {
+            auto iterator = m_aNamespacesMap.find(constNamespacePDF2);
+            if (iterator != m_aNamespacesMap.end())
+            {
+                aLine.append("/Namespaces [");
+                aWriter.writeReference(iterator->second);
+                aLine.append("]");
+            }
+        }
+
         if( ! m_aRoleMap.empty() )
         {
             aLine.append( "/RoleMap<<" );
@@ -2002,8 +2045,16 @@ sal_Int32 PDFWriterImpl::emitStructure( 
PDFStructureElement& rEle )
     }
     else
     {
-        aLine.append( "/StructElem
"
-                      "/S/" );
+        aLine.append("/StructElem");
+
+        // Write the reference to the PDF 2.0 namespace
+        if (m_aContext.Version >= PDFWriter::PDFVersion::PDF_2_0)
+        {
+            auto iterator = m_aNamespacesMap.find(constNamespacePDF2);
+            if (iterator != m_aNamespacesMap.end())
+                aWriter.writeKeyAndReference("/NS", iterator->second);
+        }
+        aLine.append("/S/");
         if( !rEle.m_aAlias.isEmpty() )
             aLine.append( rEle.m_aAlias );
         else
@@ -5149,6 +5200,8 @@ bool PDFWriterImpl::emitCatalog()
     // emit metadata
     sal_Int32 nMetadataObject = emitDocumentMetadata();
 
+    emitNamespaces();
+
     sal_Int32 nStructureDict = 0;
     if(m_aStructure.size() > 1)
     {
@@ -5341,12 +5394,13 @@ bool PDFWriterImpl::emitCatalog()
     }
 
     // viewer preferences, if we had some, then emit
-    if( m_aContext.HideViewerToolbar ||
+    if (m_aContext.HideViewerToolbar ||
         (!m_aContext.DocumentInfo.Title.isEmpty() && 
m_aContext.DisplayPDFDocumentTitle) ||
         m_aContext.HideViewerMenubar ||
         m_aContext.HideViewerWindowControls || m_aContext.FitWindow ||
         m_aContext.CenterWindow || (m_aContext.FirstPageLeft  &&  
m_aContext.PageLayout == PDFWriter::ContinuousFacing ) ||
-        m_aContext.OpenInFullScreenMode )
+        m_aContext.OpenInFullScreenMode ||
+        m_bIsPDF_UA)
     {
         aLine.append( "/ViewerPreferences<<" );
         if( m_aContext.HideViewerToolbar )
@@ -5359,7 +5413,8 @@ bool PDFWriterImpl::emitCatalog()
             aLine.append( "/FitWindow true
" );
         if( m_aContext.CenterWindow )
             aLine.append( "/CenterWindow true
" );
-        if (!m_aContext.DocumentInfo.Title.isEmpty() && 
m_aContext.DisplayPDFDocumentTitle)
+        // PDF/UA-2 requires /DisplayDocTitle set to true
+        if ((!m_aContext.DocumentInfo.Title.isEmpty() && 
m_aContext.DisplayPDFDocumentTitle) || m_bIsPDF_UA)
             aLine.append( "/DisplayDocTitle true
" );
         if( m_aContext.FirstPageLeft &&  m_aContext.PageLayout == 
PDFWriter::ContinuousFacing )
             aLine.append( "/Direction/R2L
" );
@@ -5877,7 +5932,11 @@ sal_Int32 PDFWriterImpl::emitDocumentMetadata()
     if (m_nPDFA_Version > 0)
         aMetadata.mnPDF_A = m_nPDFA_Version;
 
-    aMetadata.mbPDF_UA = m_bIsPDF_UA;
+    if (m_bIsPDF_UA)
+    {
+        // If PDF 2.0+ we need to use PDF/UA-2 otherwise PDF/UA-1
+        aMetadata.mnPDF_UA = (m_aContext.Version >= 
PDFWriter::PDFVersion::PDF_2_0) ? 2 : 1;
+    }
 
     lcl_assignMeta(m_aContext.DocumentInfo.Title, aMetadata.msTitle);
     lcl_assignMeta(m_aContext.DocumentInfo.Author, aMetadata.msAuthor);
diff --git a/vcl/source/pdf/XmpMetadata.cxx b/vcl/source/pdf/XmpMetadata.cxx
index c7e6b17deb3d..0bc84404321e 100644
--- a/vcl/source/pdf/XmpMetadata.cxx
+++ b/vcl/source/pdf/XmpMetadata.cxx
@@ -206,7 +206,7 @@ void XmpMetadata::write()
         }
 
         // PDF/UA
-        if (mbPDF_UA)
+        if (mnPDF_UA > 0)
         {
             if (mnPDF_A != 0)
             { // tdf#157517 PDF/A extension schema is required
@@ -289,7 +289,7 @@ void XmpMetadata::write()
                 aXmlWriter.endElement(); // pdfaExtension:schemas
                 aXmlWriter.endElement(); // rdf:Description
             }
-            OString sPdfUaVersion = OString::number(1);
+            OString sPdfUaVersion = OString::number(mnPDF_UA);
             aXmlWriter.startElement("rdf:Description");
             aXmlWriter.attribute("rdf:about", ""_ostr);
             aXmlWriter.attribute("xmlns:pdfuaid", 
"http://www.aiim.org/pdfua/ns/id/"_ostr);
@@ -298,6 +298,12 @@ void XmpMetadata::write()
             aXmlWriter.content(sPdfUaVersion);
             aXmlWriter.endElement();
 
+            if (mnPDF_UA == 2)
+            {
+                aXmlWriter.startElement("pdfuaid:rev");
+                aXmlWriter.content("2024"_ostr);
+                aXmlWriter.endElement();
+            }
             aXmlWriter.endElement();
         }
 

Reply via email to