From d00d192770347752ed700962ef08b5d2494be017 Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Mon, 21 Sep 2026 21:09:22 +0500 Subject: [PATCH] tdf#157028 vcl: write the structure tree into object streams Turning Tagged PDF on added 2.4 MB to the reporter's document, 2.0 MB of that its structure tree: 12562 elements, each its own top-level object, uncompressed, and each costing a further 20 bytes in the cross-reference table. The elements go into /Type/ObjStm streams of 200 now, with the parent and ID trees. The cross-reference has to become a /Type/XRef stream with them, because a table cannot address an object inside another object. PDF 1.5 introduced both, so a PDF/A-1b file keeps the plain form. So does an encrypted file, since a string in an object stream shall not be separately encrypted, and this writer encrypts strings as it builds the object. So does a hybrid file, because sdext reads the embedded original document out of a trailer. The reporter's document exported 3157118 bytes tagged before the change and 1086230 after, against 769448 untagged either way. An 8300-row Writer table exported 4017140 bytes tagged before and 500967 after, against 73183 untagged. testTdf159066 and testTdf173162 grep the exported bytes for something the structure tree carries, /Alt< and the /Namespaces array. They inflate the object streams first now. testListLabelPDFExport counted its List element in the token stream, and asks GetObjects() for it. Signed-off-by: Mike Kaganski Change-Id: I1e27dc3a8f43aa8b6c75ae930615c8d4b4645627 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/13209 Reviewed-by: Miklos Vajna Tested-by: Jenkins CPCI (cherry picked from commit ad4bb8ae080aba98080dfaabd688dfdb48cf7ade) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211556 Reviewed-by: Xisco Fauli Tested-by: Jenkins --- include/vcl/filter/pdfobjectcontainer.hxx | 8 + sc/qa/extras/scpdfexport.cxx | 29 ++- vcl/inc/pdf/pdfwriter_impl.hxx | 39 +++- vcl/qa/cppunit/pdfexport/pdfexport2.cxx | 74 +++++- vcl/source/pdf/pdfwriter_impl.cxx | 271 +++++++++++++++++----- 5 files changed, 359 insertions(+), 62 deletions(-) diff --git a/include/vcl/filter/pdfobjectcontainer.hxx b/include/vcl/filter/pdfobjectcontainer.hxx index f0e29142e771..b8daf00e402b 100644 --- a/include/vcl/filter/pdfobjectcontainer.hxx +++ b/include/vcl/filter/pdfobjectcontainer.hxx @@ -9,6 +9,9 @@ #pragma once +#include + +#include #include #include @@ -35,6 +38,11 @@ public: return writeBufferBytes(aBuffer.data(), aBuffer.size()); } + template bool writeBuffer(const rtl::OStringConcat& rConcat) + { + return writeBuffer(rtl::Concat2View(rConcat)); + } + virtual void checkAndEnableStreamEncryption(sal_Int32 nObject) = 0; virtual void disableStreamEncryption() = 0; diff --git a/sc/qa/extras/scpdfexport.cxx b/sc/qa/extras/scpdfexport.cxx index 3d34a58abf7b..fd1b8fd9fd97 100644 --- a/sc/qa/extras/scpdfexport.cxx +++ b/sc/qa/extras/scpdfexport.cxx @@ -94,10 +94,31 @@ bool ScPDFExportTest::hasTextInPdf(const char* sText, bool& bFound) const std::size_t nRead = pStream->ReadBytes(pBuffer, nFileSize); if (nRead == nFileSize) { - const std::string haystack(pBuffer, pBuffer + nFileSize); - const std::string needle(sText); - const std::size_t n = haystack.find(needle); - bFound = (n != std::string::npos); + std::string haystack(pBuffer, pBuffer + nFileSize); + // what the writer put in an object stream stands in the file as deflated bytes only + vcl::filter::PDFDocument aDocument; + SvMemoryStream aFile(pBuffer, nFileSize, StreamMode::READ); + if (aDocument.Read(aFile)) + { + for (auto* pObject : aDocument.GetObjects()) + { + auto pType + = dynamic_cast(pObject->Lookup("Type"_ostr)); + if (!pType || pType->GetValue() != "ObjStm") + continue; + vcl::filter::PDFStreamElement* pObjStm = pObject->GetStream(); + CPPUNIT_ASSERT(pObjStm); + SvMemoryStream aDecompressed; + ZCodec aCodec; + aCodec.BeginCompression(); + pObjStm->GetMemory().Seek(0); + aCodec.Decompress(pObjStm->GetMemory(), aDecompressed); + CPPUNIT_ASSERT(aCodec.EndCompression() >= 0); + haystack.append(static_cast(aDecompressed.GetData()), + aDecompressed.GetSize()); + } + } + bFound = haystack.find(std::string(sText)) != std::string::npos; } delete[] pBuffer; diff --git a/vcl/inc/pdf/pdfwriter_impl.hxx b/vcl/inc/pdf/pdfwriter_impl.hxx index a4ce2a820c68..65366b43360e 100644 --- a/vcl/inc/pdf/pdfwriter_impl.hxx +++ b/vcl/inc/pdf/pdfwriter_impl.hxx @@ -667,6 +667,29 @@ struct StreamRedirect ResourceDict m_aResourceDict; }; +/** Where the cross-reference has to point to reach an object. + + ISO 32000-2 7.5.8.3 Table 18 calls these the type 1 and type 2 entries: an object written + into the file has a byte offset, one written into an object stream names that stream and its + index in it. + */ +struct ObjectLocation +{ + sal_uInt64 m_nOffset = SAL_MAX_UINT64; + /// the object stream holding it, 0 when the object stands in the file + sal_Int32 m_nObjectStream = 0; + sal_Int32 m_nIndex = 0; + + bool isCompressed() const { return m_nObjectStream != 0; } +}; + +/// An object built but not yet written, held until its object stream is full. +struct PendingObject +{ + sal_Int32 m_nObject; + OString m_aBody; +}; + // graphics state struct GraphicsState { @@ -738,8 +761,10 @@ private: MapMode m_aMapMode; // PDFWriterImpl scaled units StyleSettings m_aWidgetStyleSettings; std::vector< PDFPage > m_aPages; - /* maps object numbers to file offsets (needed for xref) */ - std::vector< sal_uInt64 > m_aObjects; + /* maps object numbers to where they were written (needed for xref) */ + std::vector m_aObjects; + /* objects waiting to be flushed into an object stream, with their numbers */ + std::vector m_aCompressedObjects; /* contains Bitmaps until they are written to the * file stream as XObjects*/ std::list< BitmapEmit > m_aBitmaps; @@ -1067,6 +1092,16 @@ private: sal_Int32 emitEncrypt(); // writes xref and trailer bool emitTrailer(); + /// whether this file may carry object streams and a cross-reference stream + bool useObjectStreams() const; + /// writes a whole object, into an object stream where the file may have one + bool writeObject(sal_Int32 nObject, std::string_view aBody); + /// writes an object's body into the pending object stream rather than into the file + bool writeCompressedObject(sal_Int32 nObject, std::string_view aBody); + /// flushes the objects waiting in m_aCompressedObjects as one /Type/ObjStm + bool emitObjectStream(); + /// writes the cross-reference as a stream, the only form that can reach an object stream + bool emitXRefStream(std::string_view aTrailerEntries); // emits info dict (if applicable) sal_Int32 emitInfoDict( ); diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx index a4658fa3f690..3647fc96b55f 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx @@ -2227,12 +2227,30 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf173162) aNamespaceOf.contains(pRef->LookupObject())); } - // The references have to be separated in the file itself: "1 0 R2 0 R" is not two - // references, and this parser reads it as though it were, so check the bytes. veraPDF is + // The references have to be separated in the bytes themselves: "1 0 R2 0 R" is not two + // references, and this parser reads it as though it were, so check them. veraPDF is // stricter - it fails to parse such a file and reports nothing, which reads as a pass. + // The structure tree rides in an object stream, so the bytes to read are what it holds. + OStringBuffer aBytes; + for (auto* pObject : aDocument.GetObjects()) + { + auto pType = dynamic_cast(pObject->Lookup("Type"_ostr)); + if (!pType || pType->GetValue() != "ObjStm") + continue; + vcl::filter::PDFStreamElement* pObjStm = pObject->GetStream(); + CPPUNIT_ASSERT(pObjStm); + SvMemoryStream aDecompressed; + ZCodec aCodec; + aCodec.BeginCompression(); + pObjStm->GetMemory().Seek(0); + aCodec.Decompress(pObjStm->GetMemory(), aDecompressed); + CPPUNIT_ASSERT(aCodec.EndCompression() >= 0); + aBytes.append(static_cast(aDecompressed.GetData()), aDecompressed.GetSize()); + } SvStream* pStream = maTempFile.GetStream(StreamMode::READ); pStream->Seek(0); - const OString aFile(read_uInt8s_ToOString(*pStream, pStream->remainingSize())); + aBytes.append(read_uInt8s_ToOString(*pStream, pStream->remainingSize())); + const OString aFile(aBytes.makeStringAndClear()); const sal_Int32 nStart(aFile.indexOf("/Namespaces [")); CPPUNIT_ASSERT_GREATER(sal_Int32(-1), nStart); const sal_Int32 nEnd(aFile.indexOf("]", nStart)); @@ -2280,6 +2298,56 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf173162) CPPUNIT_ASSERT_EQUAL(aPDF20, aNamespaceOfType["FENote"_ostr]); } +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testObjectStreams) +{ + loadFromFile(u"SimpleTestDocument.fodt"); + + // the bytes of the export, since what is asserted is the shape of the file itself + auto aExport = [this](const uno::Sequence& rFilterData) { + comphelper::SequenceAsHashMap aMediaDescriptor; + aMediaDescriptor[u"FilterData"_ustr] <<= rFilterData; + // the temp file hands out one stream and keeps it, so the second export needs a new one + maTempFile.CloseStream(); + save(TestFilter::PDF_WRITER, aMediaDescriptor.getAsConstPropertyValueList()); + maTempFile.CloseStream(); + SvStream* pStream = maTempFile.GetStream(StreamMode::READ); + pStream->Seek(0); + return read_uInt8s_ToOString(*pStream, pStream->remainingSize()); + }; + + const OString aTagged(aExport({ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true) })); + CPPUNIT_ASSERT(aTagged.indexOf("/Type/ObjStm") != -1); + CPPUNIT_ASSERT(aTagged.indexOf("/Type/XRef") != -1); + // a cross-reference stream replaces the table, so neither the keyword nor a table is left + CPPUNIT_ASSERT_EQUAL(sal_Int32(-1), aTagged.indexOf("\ntrailer\n")); + + // the structure tree has to stay reachable through all of that + vcl::filter::PDFDocument aDocument; + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + CPPUNIT_ASSERT(aDocument.Read(aStream)); + int nStructElements(0); + for (auto* pObject : aDocument.GetObjects()) + { + auto pType = dynamic_cast(pObject->Lookup("Type"_ostr)); + if (pType && pType->GetValue() == "StructElem") + ++nStructElements; + } + CPPUNIT_ASSERT_GREATER(0, nStructElements); + + // ISO 32000-2 7.5.7 put object streams at PDF 1.5, and PDF/A-1b is built on 1.4 + const OString aArchive( + aExport({ comphelper::makePropertyValue(u"SelectPdfVersion"_ustr, sal_Int32(1)) })); + CPPUNIT_ASSERT_EQUAL(sal_Int32(-1), aArchive.indexOf("/Type/ObjStm")); + CPPUNIT_ASSERT(aArchive.indexOf("\ntrailer\n") != -1); + + // sdext finds the embedded original document through the trailer, so a hybrid file keeps one + const OString aHybrid(aExport({ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true), + comphelper::makePropertyValue(u"IsAddStream"_ustr, true) })); + CPPUNIT_ASSERT_EQUAL(sal_Int32(-1), aHybrid.indexOf("/Type/ObjStm")); + CPPUNIT_ASSERT(aHybrid.indexOf("/AdditionalStreams ") != -1); + CPPUNIT_ASSERT(aHybrid.indexOf("\ntrailer\n") != -1); +} + CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf166963) { // Takes about two minutes locally, most of it in the PDFDocument below, which rescans its diff --git a/vcl/source/pdf/pdfwriter_impl.cxx b/vcl/source/pdf/pdfwriter_impl.cxx index e1717e082ef1..a235af9b7cd8 100644 --- a/vcl/source/pdf/pdfwriter_impl.cxx +++ b/vcl/source/pdf/pdfwriter_impl.cxx @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -127,6 +128,29 @@ namespace void removePlaceholderSE(std::vector & rStructure, PDFStructureElement& rEle); +// a reader inflates a whole object stream to reach any one object in it +constexpr size_t nMaxObjectsPerStream = 200; + +// deflates a buffer for a /FlateDecode stream whose /Length has to be known before it is written +std::vector deflateBuffer(std::string_view aData) +{ + if (g_bDebugDisableCompression) + return { aData.begin(), aData.end() }; + + SvMemoryStream aInput(const_cast(aData.data()), aData.size(), StreamMode::READ); + SvMemoryStream aOutput(aData.size(), 0x4000); + ZCodec aCodec(0x4000, 0x4000); + aCodec.BeginCompression(); + aCodec.Compress(aInput, aOutput); + // it answers with the bytes it took, and a short count would leave a stream that does not + // hold what its /Length claims + if (aCodec.EndCompression() != static_cast(aData.size())) + return {}; + + const sal_uInt8* pBegin = static_cast(aOutput.GetData()); + return { pBegin, pBegin + aOutput.Tell() }; +} + // the types ISO 32000-2 14.8.6 left out of the PDF 2.0 standard structure namespace bool isPDF17OnlyType(StructElement eType) { @@ -795,7 +819,7 @@ void PDFWriterImpl::endPage() sal_Int32 PDFWriterImpl::createObject() { - m_aObjects.push_back( ~0U ); + m_aObjects.emplace_back(); return m_aObjects.size(); } @@ -804,7 +828,7 @@ bool PDFWriterImpl::updateObject( sal_Int32 n ) if( ! m_bOpen ) return false; - sal_uInt64 nOffset = ~0U; + sal_uInt64 nOffset = SAL_MAX_UINT64; osl::File::RC aError = m_aFile.getPos(nOffset); SAL_WARN_IF( aError != osl::File::E_None, "vcl.pdfwriter", "could not register object" ); if (aError != osl::File::E_None) @@ -812,19 +836,94 @@ bool PDFWriterImpl::updateObject( sal_Int32 n ) m_aFile.close(); m_bOpen = false; } - m_aObjects[ n-1 ] = nOffset; + m_aObjects[n - 1].m_nOffset = nOffset; return aError == osl::File::E_None; } +bool PDFWriterImpl::useObjectStreams() const +{ + if (m_aContext.Version < PDFWriter::PDFVersion::PDF_1_5) + return false; + // a string in an object stream shall not be separately encrypted, while this writer + // encrypts every string as it builds the object + if (m_aContext.Encryption.canEncrypt()) + return false; + // sdext reads the embedded original document out of a trailer, and a cross-reference + // stream leaves none + return m_aDocumentAttachedFiles.empty(); +} + +bool PDFWriterImpl::writeObject(sal_Int32 nObject, std::string_view aBody) +{ + if (useObjectStreams()) + return writeCompressedObject(nObject, aBody); + + return updateObject(nObject) && writeBuffer(OString::number(nObject) + " 0 obj\n") + && writeBuffer(aBody) && writeBuffer("\nendobj\n\n"); +} + +bool PDFWriterImpl::writeCompressedObject(sal_Int32 nObject, std::string_view aBody) +{ + m_aCompressedObjects.emplace_back(nObject, OString(aBody)); + + return m_aCompressedObjects.size() < nMaxObjectsPerStream || emitObjectStream(); +} + +bool PDFWriterImpl::emitObjectStream() +{ + if (m_aCompressedObjects.empty()) + return true; + + // ISO 32000-2 7.5.7: /N pairs of object number and offset, then the bodies at /First + OStringBuffer aPairs(12 * m_aCompressedObjects.size()); + OStringBuffer aBodies(4096); + const sal_Int32 nStreamObject = createObject(); + for (const auto& rPending : m_aCompressedObjects) + { + aPairs.append(OString::number(rPending.m_nObject) + " " + + OString::number(aBodies.getLength()) + " "); + aBodies.append(rPending.m_aBody + "\n"); + } + + const sal_Int32 nFirst = aPairs.getLength(); + aPairs.append(aBodies.makeStringAndClear()); + const std::vector aStream(deflateBuffer(aPairs)); + if (aStream.empty()) + return false; + + OStringBuffer aLine(256); + aLine.append(OString::number(nStreamObject) + " 0 obj\n<(m_aCompressedObjects.size())) + "/First " + + OString::number(nFirst) + "/Length " + + OString::number(static_cast(aStream.size()))); + if (!g_bDebugDisableCompression) + aLine.append("/Filter/FlateDecode"); + aLine.append(">>\nstream\n"); + + if (!updateObject(nStreamObject) || !writeBuffer(aLine) + || !writeBufferBytes(aStream.data(), aStream.size()) + || !writeBuffer("\nendstream\nendobj\n\n")) + return false; + + // only now can an entry name the stream, which is in the file + sal_Int32 nIndex = 0; + for (const auto& rPending : m_aCompressedObjects) + { + m_aObjects[rPending.m_nObject - 1] + = { .m_nObjectStream = nStreamObject, .m_nIndex = nIndex }; + ++nIndex; + } + m_aCompressedObjects.clear(); + return true; +} + sal_Int32 PDFWriterImpl::emitStructParentTree( sal_Int32 nObject ) { if( nObject > 0 ) { OStringBuffer aLine( PDFWRITER_IMPL_BUFFERSIZE ); - aLine.append( OString::number(nObject) - + " 0 obj\n" - "<>\nendobj\n\n" ); - if (!updateObject(nObject)) - return 0; - if (!writeBuffer(aLine)) + aLine.append("]>>"); + if (!writeObject(nObject, aLine)) return 0; } return nObject; @@ -862,7 +959,6 @@ sal_Int32 PDFWriterImpl::emitStructIDTree(sal_Int32 const nObject) } OStringBuffer buf; COSWriter aWriter(buf, m_aContext.Encryption.getParams(), m_pPDFEncryptor); - appendObjectID(nObject, buf); buf.append("<>\nendobj\n\n"); + buf.append("] >>"); - if (!updateObject(nObject)) return 0; - if (!writeBuffer(buf)) return 0; + if (!writeObject(nObject, buf)) + return 0; return nObject; } @@ -1179,10 +1275,7 @@ sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle ) OStringBuffer aLine( 512 ); COSWriter aWriter(aLine, m_aContext.Encryption.getParams(), m_pPDFEncryptor); - aLine.append( - OString::number(rEle.m_nObject) - + " 0 obj\n" - "<>\nendobj\n\n" ); + aLine.append(">>"); - if (!updateObject(rEle.m_nObject)) return 0; - if (!writeBuffer(aLine)) return 0; + if (!writeObject(rEle.m_nObject, aLine)) + return 0; if (!emitStructParentTree(nParentTree)) return 0; if (!emitStructIDTree(nIDTree)) return 0; @@ -4551,6 +4644,8 @@ bool PDFWriterImpl::emitCatalog() addInternalStructureContainer(0); nStructureDict = m_aStructure[0].m_nObject = createObject(); emitStructure( m_aStructure[ 0 ] ); + if (!emitObjectStream()) + return false; } // adjust tree node file offset @@ -5425,34 +5520,41 @@ bool PDFWriterImpl::emitTrailer() { nSecObject = emitEncrypt(); } - // emit xref table - // remember start + // a cross-reference table cannot address an object inside an object stream + const bool bXRefStream = std::ranges::any_of(m_aObjects, &ObjectLocation::isCompressed); + sal_uInt64 nXRefOffset = 0; - if (osl::File::E_None != m_aFile.getPos(nXRefOffset)) - return false; - if (!writeBuffer("xref\n")) - return false; - - sal_Int32 nObjects = m_aObjects.size(); OStringBuffer aLine; - aLine.append( "0 " ); - aLine.append( static_cast(nObjects+1) ); - aLine.append( "\n" ); - aLine.append( "0000000000 65535 f \n" ); - if (!writeBuffer(aLine)) - return false; - - for( sal_Int32 i = 0; i < nObjects; i++ ) + if (!bXRefStream) { - aLine.setLength( 0 ); - OString aOffset = OString::number( m_aObjects[i] ); - for( sal_Int32 j = 0; j < (10-aOffset.getLength()); j++ ) - aLine.append( '0' ); - aLine.append( aOffset ); - aLine.append( " 00000 n \n" ); - SAL_WARN_IF( aLine.getLength() != 20, "vcl.pdfwriter", "invalid xref entry" ); + // emit the xref table, remembering where it starts + if (osl::File::E_None != m_aFile.getPos(nXRefOffset)) + return false; + if (!writeBuffer("xref\n")) + return false; + + aLine.append("0 "); + aLine.append(static_cast(m_aObjects.size() + 1)); + aLine.append("\n"); + aLine.append("0000000000 65535 f \n"); if (!writeBuffer(aLine)) return false; + + for (const auto& rLocation : m_aObjects) + { + if (rLocation.m_nOffset == SAL_MAX_UINT64) + { + SAL_WARN("vcl.pdfwriter", "object was created but never written"); + if (!writeBuffer("0000000000 65535 f \n")) + return false; + continue; + } + auto aOffset(OString::number(rLocation.m_nOffset)); + aLine = RepeatedChar('0', 10 - aOffset.length) + aOffset + " 00000 n \n"; + SAL_WARN_IF(aLine.getLength() != 20, "vcl.pdfwriter", "invalid xref entry"); + if (!writeBuffer(aLine)) + return false; + } } // prepare document checksum @@ -5463,9 +5565,6 @@ bool PDFWriterImpl::emitTrailer() // document id set in setDocInfo method // emit trailer aLine.setLength( 0 ); - aLine.append( "trailer\n" - "<(nObjects+1) ); aLine.append( "/Root " ); aLine.append( m_nCatalogObject ); aLine.append( " 0 R\n" ); @@ -5530,12 +5629,78 @@ bool PDFWriterImpl::emitTrailer() // Assertion failure: rv == SECSuccess, at sechash.c:140 m_DocDigest.initialize(); - aLine.append( ">>\n" - "startxref\n" ); - aLine.append( static_cast(nXRefOffset) ); - aLine.append( "\n" - "%%EOF\n" ); - return writeBuffer( aLine ); + if (bXRefStream) + return emitXRefStream(aLine); + + return writeBuffer("trailer\n<>\nstartxref\n" + OString::number(static_cast(nXRefOffset)) + + "\n%%EOF\n"); +} + +bool PDFWriterImpl::emitXRefStream(std::string_view aTrailerEntries) +{ + // ISO 32000-2 7.5.8.3 requires an entry for the stream itself + const sal_Int32 nXRefObject = createObject(); + sal_uInt64 nXRefOffset = 0; + if (osl::File::E_None != m_aFile.getPos(nXRefOffset)) + return false; + m_aObjects[nXRefObject - 1].m_nOffset = nXRefOffset; + + // the second field is a byte offset or an object stream number, so measure both + const auto aSecondField = [](const ObjectLocation& rLocation) -> sal_uInt64 { + if (rLocation.isCompressed()) + return rLocation.m_nObjectStream; + // an unwritten object gets a free entry, which has no offset + return rLocation.m_nOffset == SAL_MAX_UINT64 ? 0 : rLocation.m_nOffset; + }; + sal_Int32 nOffsetBytes = 1; + for (sal_uInt64 nWidest = std::ranges::max(m_aObjects | std::views::transform(aSecondField)); + nWidest > 0xff; nWidest >>= 8) + ++nOffsetBytes; + + OStringBuffer aEntries(static_cast((3 + nOffsetBytes) * (m_aObjects.size() + 1))); + auto appendEntry + = [&aEntries, nOffsetBytes](sal_uInt8 nType, sal_uInt64 nSecond, sal_uInt16 nThird) { + aEntries.append(char(nType)); + for (sal_Int32 nByte = nOffsetBytes; nByte-- > 0;) + aEntries.append(char(nSecond >> (8 * nByte))); + aEntries.append(char(nThird >> 8)); + aEntries.append(char(nThird)); + }; + + // object 0 heads the list of free objects, as it does in a table + appendEntry(0, 0, 0xffff); + for (const auto& rLocation : m_aObjects) + { + if (rLocation.isCompressed()) + appendEntry(2, rLocation.m_nObjectStream, rLocation.m_nIndex); + else if (rLocation.m_nOffset == SAL_MAX_UINT64) + appendEntry(0, 0, 0xffff); + else + appendEntry(1, rLocation.m_nOffset, 0); + } + + const std::vector aStream(deflateBuffer(aEntries)); + if (aStream.empty()) + return false; + + OStringBuffer aLine(512); + // 7.5.8.2: every entry of Table 17 shall be direct, and a reader cannot resolve an + // indirect /Length before it has read this stream + aLine.append(OString::number(nXRefObject) + " 0 obj\n<(m_aObjects.size() + 1)) + "/W[1 " + + OString::number(nOffsetBytes) + " 2]/Length " + + OString::number(static_cast(aStream.size()))); + if (!g_bDebugDisableCompression) + aLine.append("/Filter/FlateDecode"); + aLine.append(aTrailerEntries); + aLine.append(">>\nstream\n"); + + // 7.5.8.2 forbids encrypting this stream, so it goes out as built + return writeBuffer(aLine) && writeBufferBytes(aStream.data(), aStream.size()) + && writeBuffer("\nendstream\nendobj\n\nstartxref\n" + + OString::number(static_cast(nXRefOffset)) + "\n%%EOF\n"); } namespace {