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 <mike.kaganski@collabora.com>
Change-Id: I1e27dc3a8f43aa8b6c75ae930615c8d4b4645627
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/13209
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit ad4bb8ae080aba98080dfaabd688dfdb48cf7ade)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211556
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Mike Kaganski
2026-09-30 13:25:11 +02:00
committed by Xisco Fauli
parent e4efd9faf2
commit d00d192770
5 changed files with 359 additions and 62 deletions
@@ -9,6 +9,9 @@
#pragma once
#include <sal/config.h>
#include <rtl/stringconcat.hxx>
#include <sal/types.h>
#include <string_view>
@@ -35,6 +38,11 @@ public:
return writeBufferBytes(aBuffer.data(), aBuffer.size());
}
template <typename T1, typename T2> bool writeBuffer(const rtl::OStringConcat<T1, T2>& rConcat)
{
return writeBuffer(rtl::Concat2View(rConcat));
}
virtual void checkAndEnableStreamEncryption(sal_Int32 nObject) = 0;
virtual void disableStreamEncryption() = 0;
+25 -4
View File
@@ -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<vcl::filter::PDFNameElement*>(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<const char*>(aDecompressed.GetData()),
aDecompressed.GetSize());
}
}
bFound = haystack.find(std::string(sText)) != std::string::npos;
}
delete[] pBuffer;
+37 -2
View File
@@ -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<ObjectLocation> m_aObjects;
/* objects waiting to be flushed into an object stream, with their numbers */
std::vector<PendingObject> 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( );
+71 -3
View File
@@ -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<vcl::filter::PDFNameElement*>(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<const char*>(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<beans::PropertyValue>& 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<vcl::filter::PDFNameElement*>(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
+218 -53
View File
@@ -24,6 +24,7 @@
#include <math.h>
#include <algorithm>
#include <ranges>
#include <string_view>
#include <lcms2.h>
@@ -127,6 +128,29 @@ namespace
void removePlaceholderSE(std::vector<PDFStructureElement> & 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<sal_uInt8> deflateBuffer(std::string_view aData)
{
if (g_bDebugDisableCompression)
return { aData.begin(), aData.end() };
SvMemoryStream aInput(const_cast<char*>(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<tools::Long>(aData.size()))
return {};
const sal_uInt8* pBegin = static_cast<const sal_uInt8*>(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<sal_uInt8> aStream(deflateBuffer(aPairs));
if (aStream.empty())
return false;
OStringBuffer aLine(256);
aLine.append(OString::number(nStreamObject) + " 0 obj\n<</Type/ObjStm/N "
+ OString::number(static_cast<sal_Int32>(m_aCompressedObjects.size())) + "/First "
+ OString::number(nFirst) + "/Length "
+ OString::number(static_cast<sal_Int32>(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"
"<</Nums[\n" );
aLine.append("<</Nums[\n");
sal_Int32 nTreeItems = m_aStructParentTree.size();
for( sal_Int32 n = 0; n < nTreeItems; n++ )
{
@@ -832,10 +931,8 @@ sal_Int32 PDFWriterImpl::emitStructParentTree( sal_Int32 nObject )
+ m_aStructParentTree[n]
+ "\n" );
}
aLine.append( "]>>\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("<</Names [\n");
for (auto const& it : ids)
{
@@ -871,10 +967,10 @@ sal_Int32 PDFWriterImpl::emitStructIDTree(sal_Int32 const nObject)
appendObjectReference(it.second, buf);
buf.append("\n");
}
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"
"<</Type" );
aLine.append("<</Type");
sal_Int32 nParentTree = -1;
sal_Int32 nIDTree = -1;
if( rEle.m_nOwnElement == rEle.m_nParentElement )
@@ -1387,10 +1480,10 @@ sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle )
}
aLine.append( "]\n" );
}
aLine.append( ">>\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<sal_Int32>(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<sal_Int32>(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"
"<</Size " );
aLine.append( static_cast<sal_Int32>(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<sal_Int64>(nXRefOffset) );
aLine.append( "\n"
"%%EOF\n" );
return writeBuffer( aLine );
if (bXRefStream)
return emitXRefStream(aLine);
return writeBuffer("trailer\n<</Size " + OString::number(m_aObjects.size() + 1))
&& writeBuffer(aLine)
&& writeBuffer(">>\nstartxref\n" + OString::number(static_cast<sal_Int64>(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<sal_Int32>((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<sal_uInt8> 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<</Type/XRef/Size "
+ OString::number(static_cast<sal_Int32>(m_aObjects.size() + 1)) + "/W[1 "
+ OString::number(nOffsetBytes) + " 2]/Length "
+ OString::number(static_cast<sal_Int32>(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<sal_Int64>(nXRefOffset)) + "\n%%EOF\n");
}
namespace {