PDF writer: implement support for document internal links
QPdfEngine already supported links to external URIs, but not internal
links for navigation in a document.
For internal links, PDF supports "named destinations" instead of
direct coordinates. That way, we avoid the need for a two-pass
implementation. Instead, we just store the name and position of all
anchors as we enounter them. At document finishing time, we export the
list of named destinations in a PDF "name tree" structure.
The PDF named destinations feature uses the same main catalog item
("Names") as the attached files feature. Hence, this commit must
slightly change the implementation of file attachment support also, so
that the structure supports both. Now, we always add a reference to a
Names object in the catalog when we start the document. When we
finish, we write that object, making it reference the attached
files-structure and/or the named destinations-structure, as needed.
Fixes: QTBUG-83458
Change-Id: I9c43e7b423062d3f21965ab8a0d81a53c4dd72cb
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
bb10
parent
a9f94d078a
commit
5d95205bae
|
|
@ -1505,8 +1505,9 @@ bool QPdfEngine::begin(QPaintDevice *pdev)
|
|||
|
||||
d->xrefPositions.clear();
|
||||
d->pageRoot = 0;
|
||||
d->embeddedfilesRoot = 0;
|
||||
d->namesRoot = 0;
|
||||
d->destsRoot = 0;
|
||||
d->attachmentsRoot = 0;
|
||||
d->catalog = 0;
|
||||
d->info = 0;
|
||||
d->graphicsState = 0;
|
||||
|
|
@ -1543,6 +1544,7 @@ bool QPdfEngine::end()
|
|||
d->outDevice = nullptr;
|
||||
}
|
||||
|
||||
d->destCache.clear();
|
||||
d->fileCache.clear();
|
||||
|
||||
setActive(false);
|
||||
|
|
@ -1591,10 +1593,7 @@ void QPdfEnginePrivate::writeHeader()
|
|||
|
||||
catalog = addXrefEntry(-1);
|
||||
pageRoot = requestObject();
|
||||
if (!fileCache.isEmpty()) {
|
||||
namesRoot = requestObject();
|
||||
embeddedfilesRoot = requestObject();
|
||||
}
|
||||
namesRoot = requestObject();
|
||||
|
||||
// catalog
|
||||
{
|
||||
|
|
@ -1602,11 +1601,8 @@ void QPdfEnginePrivate::writeHeader()
|
|||
QPdf::ByteStream s(&catalog);
|
||||
s << "<<\n"
|
||||
<< "/Type /Catalog\n"
|
||||
<< "/Pages " << pageRoot << "0 R\n";
|
||||
|
||||
// Embedded files, if any
|
||||
if (!fileCache.isEmpty())
|
||||
s << "/Names " << embeddedfilesRoot << "0 R\n";
|
||||
<< "/Pages " << pageRoot << "0 R\n"
|
||||
<< "/Names " << namesRoot << "0 R\n";
|
||||
|
||||
if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty())
|
||||
s << "/Metadata " << metaDataObj << "0 R\n";
|
||||
|
|
@ -1620,12 +1616,6 @@ void QPdfEnginePrivate::writeHeader()
|
|||
write(catalog);
|
||||
}
|
||||
|
||||
if (!fileCache.isEmpty()) {
|
||||
addXrefEntry(embeddedfilesRoot);
|
||||
xprintf("<</EmbeddedFiles %d 0 R>>\n"
|
||||
"endobj\n", namesRoot);
|
||||
}
|
||||
|
||||
// graphics state
|
||||
graphicsState = addXrefEntry(-1);
|
||||
xprintf("<<\n"
|
||||
|
|
@ -1793,6 +1783,39 @@ void QPdfEnginePrivate::writePageRoot()
|
|||
"endobj\n");
|
||||
}
|
||||
|
||||
void QPdfEnginePrivate::writeDestsRoot()
|
||||
{
|
||||
if (destCache.isEmpty())
|
||||
return;
|
||||
|
||||
QHash<QString, int> destObjects;
|
||||
QByteArray xs, ys;
|
||||
for (const DestInfo &destInfo : qAsConst(destCache)) {
|
||||
int destObj = addXrefEntry(-1);
|
||||
xs.setNum(static_cast<double>(destInfo.coords.x()), 'f');
|
||||
ys.setNum(static_cast<double>(destInfo.coords.y()), 'f');
|
||||
xprintf("[%d 0 R /XYZ %s %s 0]\n", destInfo.pageObj, xs.constData(), ys.constData());
|
||||
xprintf("endobj\n");
|
||||
destObjects.insert(destInfo.anchor, destObj);
|
||||
}
|
||||
|
||||
// names
|
||||
destsRoot = addXrefEntry(-1);
|
||||
QStringList anchors = destObjects.keys();
|
||||
anchors.sort();
|
||||
xprintf("<<\n/Limits [");
|
||||
printString(anchors.constFirst());
|
||||
xprintf(" ");
|
||||
printString(anchors.constLast());
|
||||
xprintf("]\n/Names [\n");
|
||||
for (const QString &anchor : qAsConst(anchors)) {
|
||||
printString(anchor);
|
||||
xprintf(" %d 0 R\n", destObjects[anchor]);
|
||||
}
|
||||
xprintf("]\n>>\n"
|
||||
"endobj\n");
|
||||
}
|
||||
|
||||
void QPdfEnginePrivate::writeAttachmentRoot()
|
||||
{
|
||||
if (fileCache.isEmpty())
|
||||
|
|
@ -1832,7 +1855,7 @@ void QPdfEnginePrivate::writeAttachmentRoot()
|
|||
}
|
||||
|
||||
// names
|
||||
addXrefEntry(namesRoot);
|
||||
attachmentsRoot = addXrefEntry(-1);
|
||||
xprintf("<</Names[");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
auto attachment = fileCache.at(i);
|
||||
|
|
@ -1843,6 +1866,21 @@ void QPdfEnginePrivate::writeAttachmentRoot()
|
|||
"endobj\n");
|
||||
}
|
||||
|
||||
void QPdfEnginePrivate::writeNamesRoot()
|
||||
{
|
||||
addXrefEntry(namesRoot);
|
||||
xprintf("<<\n");
|
||||
|
||||
if (attachmentsRoot)
|
||||
xprintf("/EmbeddedFiles %d 0 R\n", attachmentsRoot);
|
||||
|
||||
if (destsRoot)
|
||||
xprintf("/Dests %d 0 R\n", destsRoot);
|
||||
|
||||
xprintf(">>\n");
|
||||
xprintf("endobj\n");
|
||||
}
|
||||
|
||||
void QPdfEnginePrivate::embedFont(QFontSubset *font)
|
||||
{
|
||||
//qDebug() << "embedFont" << font->object_id;
|
||||
|
|
@ -2099,7 +2137,9 @@ void QPdfEnginePrivate::writeTail()
|
|||
writePage();
|
||||
writeFonts();
|
||||
writePageRoot();
|
||||
writeDestsRoot();
|
||||
writeAttachmentRoot();
|
||||
writeNamesRoot();
|
||||
|
||||
addXrefEntry(xrefPositions.size(),false);
|
||||
xprintf("xref\n"
|
||||
|
|
@ -2951,7 +2991,9 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti)
|
|||
{
|
||||
Q_Q(QPdfEngine);
|
||||
|
||||
if (ti.charFormat.hasProperty(QTextFormat::AnchorHref)) {
|
||||
const bool isLink = ti.charFormat.hasProperty(QTextFormat::AnchorHref);
|
||||
const bool isAnchor = ti.charFormat.hasProperty(QTextFormat::AnchorName);
|
||||
if (isLink || isAnchor) {
|
||||
qreal size = ti.fontEngine->fontDef.pixelSize;
|
||||
int synthesized = ti.fontEngine->synthesized();
|
||||
qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.;
|
||||
|
|
@ -2971,32 +3013,47 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti)
|
|||
trans.map(0, 0, &x1, &y1);
|
||||
trans.map(ti.width.toReal()/size, (ti.ascent.toReal()-ti.descent.toReal())/size, &x2, &y2);
|
||||
|
||||
uint annot = addXrefEntry(-1);
|
||||
QByteArray x1s, y1s, x2s, y2s;
|
||||
x1s.setNum(static_cast<double>(x1), 'f');
|
||||
y1s.setNum(static_cast<double>(y1), 'f');
|
||||
x2s.setNum(static_cast<double>(x2), 'f');
|
||||
y2s.setNum(static_cast<double>(y2), 'f');
|
||||
QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s;
|
||||
xprintf("<<\n/Type /Annot\n/Subtype /Link\n");
|
||||
if (isLink) {
|
||||
uint annot = addXrefEntry(-1);
|
||||
QByteArray x1s, y1s, x2s, y2s;
|
||||
x1s.setNum(static_cast<double>(x1), 'f');
|
||||
y1s.setNum(static_cast<double>(y1), 'f');
|
||||
x2s.setNum(static_cast<double>(x2), 'f');
|
||||
y2s.setNum(static_cast<double>(y2), 'f');
|
||||
QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s;
|
||||
xprintf("<<\n/Type /Annot\n/Subtype /Link\n");
|
||||
|
||||
if (pdfVersion == QPdfEngine::Version_A1b)
|
||||
xprintf("/F 4\n"); // enable print flag, disable all other
|
||||
if (pdfVersion == QPdfEngine::Version_A1b)
|
||||
xprintf("/F 4\n"); // enable print flag, disable all other
|
||||
|
||||
xprintf("/Rect [");
|
||||
xprintf(rectData.constData());
|
||||
xprintf("/Rect [");
|
||||
xprintf(rectData.constData());
|
||||
#ifdef Q_DEBUG_PDF_LINKS
|
||||
xprintf("]\n/Border [16 16 1]\n/A <<\n");
|
||||
xprintf("]\n/Border [16 16 1]\n");
|
||||
#else
|
||||
xprintf("]\n/Border [0 0 0]\n/A <<\n");
|
||||
xprintf("]\n/Border [0 0 0]\n");
|
||||
#endif
|
||||
xprintf("/Type /Action\n/S /URI\n/URI (%s)\n",
|
||||
ti.charFormat.anchorHref().toLatin1().constData());
|
||||
xprintf(">>\n>>\n");
|
||||
xprintf("endobj\n");
|
||||
const QString link = ti.charFormat.anchorHref();
|
||||
const bool isInternal = link.startsWith(QLatin1Char('#'));
|
||||
if (!isInternal) {
|
||||
xprintf("/A <<\n");
|
||||
xprintf("/Type /Action\n/S /URI\n/URI (%s)\n", link.toLatin1().constData());
|
||||
xprintf(">>\n");
|
||||
} else {
|
||||
xprintf("/Dest ");
|
||||
printString(link.sliced(1));
|
||||
xprintf("\n");
|
||||
}
|
||||
xprintf(">>\n");
|
||||
xprintf("endobj\n");
|
||||
|
||||
if (!currentPage->annotations.contains(annot)) {
|
||||
currentPage->annotations.append(annot);
|
||||
if (!currentPage->annotations.contains(annot)) {
|
||||
currentPage->annotations.append(annot);
|
||||
}
|
||||
} else {
|
||||
const QString anchor = ti.charFormat.anchorNames().constFirst();
|
||||
const uint curPage = pages.last();
|
||||
destCache.append(DestInfo({ anchor, curPage, QPointF(x1, y2) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@ private:
|
|||
int writeXmpDcumentMetaData();
|
||||
int writeOutputIntent();
|
||||
void writePageRoot();
|
||||
void writeDestsRoot();
|
||||
void writeAttachmentRoot();
|
||||
void writeNamesRoot();
|
||||
void writeFonts();
|
||||
void embedFont(QFontSubset *font);
|
||||
qreal calcUserUnit() const;
|
||||
|
|
@ -305,11 +307,20 @@ private:
|
|||
QString mimeType;
|
||||
};
|
||||
|
||||
struct DestInfo
|
||||
{
|
||||
QString anchor;
|
||||
uint pageObj;
|
||||
QPointF coords;
|
||||
};
|
||||
|
||||
// various PDF objects
|
||||
int pageRoot, embeddedfilesRoot, namesRoot, catalog, info, graphicsState, patternColorSpace;
|
||||
int pageRoot, namesRoot, destsRoot, attachmentsRoot, catalog, info;
|
||||
int graphicsState, patternColorSpace;
|
||||
QList<uint> pages;
|
||||
QHash<qint64, uint> imageCache;
|
||||
QHash<QPair<uint, uint>, uint > alphaCache;
|
||||
QList<DestInfo> destCache;
|
||||
QList<AttachmentInfo> fileCache;
|
||||
QByteArray xmpDocumentMetadata;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue