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:
committed by
Xisco Fauli
parent
e4efd9faf2
commit
d00d192770
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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( );
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user