This commit is contained in:
Kellermorph
2026-08-14 08:58:36 +02:00
parent 67b1665b36
commit f0348a7cd8
4 changed files with 134 additions and 68 deletions
+89 -29
View File
@@ -32,7 +32,6 @@
#include <QGraphicsTextItem> #include <QGraphicsTextItem>
#include <QList> #include <QList>
#include <QRegularExpression> #include <QRegularExpression>
#include <QTextCodec>
#include <QUrl> #include <QUrl>
#include <QVector> #include <QVector>
@@ -399,8 +398,8 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
QByteArray data = f.readAll(); QByteArray data = f.readAll();
f.close(); f.close();
const QByteArray marker("http://componentinfo.local/"); const QByteArray markerPrefix("http://componentinfo.local/");
if (!data.contains(marker)) return; if (!data.contains(markerPrefix)) return;
int xrefStart = data.lastIndexOf("\nxref\n"); int xrefStart = data.lastIndexOf("\nxref\n");
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref "); if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
@@ -433,20 +432,34 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
out.reserve(data.size()); out.reserve(data.size());
int pos = 0; int pos = 0;
int annotIdx = 0; bool anyConverted = false;
while (pos < body.size()) { while (pos < body.size()) {
int markerPos = body.indexOf(marker, pos); // Find next indexed marker: http://componentinfo.local/<N>
int markerPos = body.indexOf(markerPrefix, pos);
if (markerPos == -1) { if (markerPos == -1) {
out.append(body.mid(pos)); out.append(body.mid(pos));
break; break;
} }
// Extract index from marker URL
int idxStart = markerPos + markerPrefix.size();
int idxEnd = idxStart;
while (idxEnd < body.size() && body[idxEnd] >= '0' && body[idxEnd] <= '9')
++idxEnd;
if (idxEnd == idxStart) {
// No index — skip malformed marker
out.append(body.mid(pos, markerPos + markerPrefix.size() - pos));
pos = markerPos + markerPrefix.size();
continue;
}
int annotIndex = body.mid(idxStart, idxEnd - idxStart).toInt();
int uriOpen = body.lastIndexOf("/URI (", markerPos); int uriOpen = body.lastIndexOf("/URI (", markerPos);
int closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1; int closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1;
if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) { if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) {
out.append(body.mid(pos, markerPos + marker.size() - pos)); out.append(body.mid(pos, idxEnd - pos));
pos = markerPos + marker.size(); pos = idxEnd;
continue; continue;
} }
@@ -455,43 +468,63 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
// Find the /A << that opens the action dict containing /S /URI. // Find the /A << that opens the action dict containing /S /URI.
// Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>> // Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>>
// We need /Contents at the annotation level, NOT inside /A <<. int aDictOpen = (sUriPos != -1)
int aDictOpen = body.lastIndexOf("/A <<", (sUriPos != -1) ? sUriPos : uriOpen); ? body.lastIndexOf("/A <<", sUriPos)
int copyEnd = (aDictOpen != -1 && aDictOpen >= pos) ? aDictOpen : -1;
: (sUriPos != -1 && sUriPos >= pos) ? sUriPos
: uriOpen;
out.append(body.mid(pos, copyEnd - pos));
// Get text from annotations list (matched by order) // If /A << is not found, leave this annotation untouched
if (annotIdx >= annotations.size()) { if (aDictOpen == -1 || aDictOpen < pos) {
// Keep original action dict intact out.append(body.mid(pos, closeParen + 1 - pos));
out.append(body.mid(copyEnd, closeParen + 1 - copyEnd));
pos = closeParen + 1; pos = closeParen + 1;
continue; continue;
} }
QByteArray contents = annotations[annotIdx].contents.toUtf8(); // Validate index
++annotIdx; if (annotIndex < 0 || annotIndex >= annotations.size()) {
// Index out of range — keep original annotation intact
out.append(body.mid(pos, closeParen + 1 - pos));
pos = closeParen + 1;
continue;
}
// Replace /Subtype /Link with /Subtype /Text // Copy annotation dict header up to /A <<
int subTypePos = out.lastIndexOf("/Subtype /Link"); out.append(body.mid(pos, aDictOpen - pos));
QByteArray contents = annotations[annotIndex].contents.toUtf8();
// Replace /Subtype /Link with /Subtype /Text, bounded to current object.
// Search only the bytes we just appended (the current annotation header).
int searchStart = out.size() - (aDictOpen - pos);
int subTypePos = out.indexOf("/Subtype /Link", searchStart);
if (subTypePos != -1) { if (subTypePos != -1) {
out.replace(subTypePos, 14, "/Subtype /Text"); out.replace(subTypePos, 14, "/Subtype /Text");
} }
// Encode as UTF-16BE hex with BOM for proper Unicode support (Umlauten etc.). // Encode as UTF-16BE hex with BOM for proper Unicode support (Umlauten etc.).
// PDF spec: hex strings starting with FE FF are interpreted as UTF-16BE. // PDF spec: hex strings starting with FE FF are interpreted as UTF-16BE.
QTextCodec *codec = QTextCodec::codecForName("UTF-16BE");
QByteArray utf16be; QByteArray utf16be;
utf16be.append('\xfe'); utf16be.append('\xfe');
utf16be.append('\xff'); utf16be.append('\xff');
if (codec) { {
utf16be += codec->fromUnicode(QString::fromUtf8(contents)); for (int i = 0; i < contents.size(); ) {
ushort cp = 0;
uchar c = static_cast<uchar>(contents.at(i));
if (c < 0x80) {
cp = c;
++i;
} else if ((c & 0xE0) == 0xC0 && i + 1 < contents.size()) {
cp = ((c & 0x1F) << 6)
| (static_cast<uchar>(contents.at(i + 1)) & 0x3F);
i += 2;
} else if ((c & 0xF0) == 0xE0 && i + 2 < contents.size()) {
cp = ((c & 0x0F) << 12)
| ((static_cast<uchar>(contents.at(i + 1)) & 0x3F) << 6)
| (static_cast<uchar>(contents.at(i + 2)) & 0x3F);
i += 3;
} else { } else {
// Fallback: manual UTF-16BE encoding cp = 0xFFFD; // replacement character
QString str = QString::fromUtf8(contents); ++i;
for (int i = 0; i < str.size(); ++i) { }
ushort cp = str.at(i).unicode();
utf16be.append(static_cast<char>((cp >> 8) & 0xFF)); utf16be.append(static_cast<char>((cp >> 8) & 0xFF));
utf16be.append(static_cast<char>(cp & 0xFF)); utf16be.append(static_cast<char>(cp & 0xFF));
} }
@@ -509,9 +542,10 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
} else { } else {
pos = closeParen + 1; pos = closeParen + 1;
} }
anyConverted = true;
} }
if (annotIdx == 0) return; if (!anyConverted) return;
// Append empty Form XObject (shared by all annotations for invisible appearance) // Append empty Form XObject (shared by all annotations for invisible appearance)
QByteArray emptyXObj; QByteArray emptyXObj;
@@ -556,6 +590,7 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
} }
} }
// Copy trailer and bump /Size to account for the new XObject
QByteArray trailer; QByteArray trailer;
{ {
int tPos = data.indexOf("trailer", xrefStart); int tPos = data.indexOf("trailer", xrefStart);
@@ -569,6 +604,31 @@ void convertComponentInfoAnnotations(const QString &pdfPath,
if (trailer.isEmpty()) if (trailer.isEmpty())
trailer = "trailer\n<<>>\n%%EOF"; trailer = "trailer\n<<>>\n%%EOF";
// Bump /Size: original was maxObjNum+1, now it's emptyXObjNum+1
{
int sizePos = trailer.indexOf("/Size ");
if (sizePos != -1) {
int numStart = sizePos + 6;
int numEnd = numStart;
while (numEnd < trailer.size() && trailer[numEnd] >= '0' && trailer[numEnd] <= '9')
++numEnd;
if (numEnd > numStart) {
trailer.replace(numStart, numEnd - numStart,
QByteArray::number(emptyXObjNum + 1));
}
}
}
// Remove duplicate startxref if present in copied trailer
{
int stPos = trailer.indexOf("\nstartxref\n");
if (stPos != -1)
trailer = trailer.left(stPos);
// Ensure trailer ends with %%EOF
if (!trailer.endsWith("%%EOF\n"))
trailer += "\n%%EOF\n";
}
QByteArray result; QByteArray result;
result.reserve(out.size() + xref.size() + trailer.size() + 64); result.reserve(out.size() + xref.size() + trailer.size() + 64);
result += out; result += out;
+11 -6
View File
@@ -66,19 +66,24 @@ namespace PdfLinks {
const QMap<Diagram *, int> &pageMap, const QMap<Diagram *, int> &pageMap,
const QString &outputFileName); const QString &outputFileName);
/** /**
Post-process a Qt-generated PDF file: rewrite every "/S /URI" link Post-process a Qt-generated PDF file: rewrite every "/S /URI" link
annotation into a native internal "/S /GoTo" action (page + /FitR or annotation into a native internal "/S /GoTo" action (page + /FitR or
/Fit destination) and rebuild the xref table. No-op if the file has no /Fit destination) and rebuild the xref table. No-op if the file has no
such annotations. such annotations.
*/ */
void convertUriToGoTo(const QString &pdfPath); void convertUriToGoTo(const QString &pdfPath);
struct ComponentInfo { struct ComponentInfo {
QString contents; QString contents;
}; };
void convertComponentInfoAnnotations(const QString &pdfPath, /**
Post-process a Qt-generated PDF file: convert component-info placeholder
link annotations (http://componentinfo.local/<N>) into invisible text
annotations with the actual component info as /Contents.
*/
void convertComponentInfoAnnotations(const QString &pdfPath,
const QList<ComponentInfo> &annotations); const QList<ComponentInfo> &annotations);
} }
+3 -2
View File
@@ -432,8 +432,9 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
////Collect component info for popup annotations//// ////Collect component info for popup annotations////
if (ui->m_component_info_cb->isChecked()) { if (ui->m_component_info_cb->isChecked()) {
int annotIndex = 0;
for (auto *item : diagram->items()) { for (auto *item : diagram->items()) {
auto *el = dynamic_cast<Element*>(item); auto *el = qgraphicsitem_cast<Element*>(item);
if (!el) continue; if (!el) continue;
// Skip reports and slaves // Skip reports and slaves
@@ -469,7 +470,7 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
// Create a link annotation as placeholder — post-processing // Create a link annotation as placeholder — post-processing
// will convert it to an invisible text annotation with the actual content // will convert it to an invisible text annotation with the actual content
pdfEngine->drawHyperlink(devRect, QUrl("http://componentinfo.local/")); pdfEngine->drawHyperlink(devRect, QUrl(QString("http://componentinfo.local/%1").arg(annotIndex++)));
} }
} }
////Component info end//// ////Component info end////