diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index cc759d437..7e150a6e7 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -109,6 +109,8 @@ set(QET_RES_FILES
set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h
+ ${QET_DIR}/sources/pdf_links.cpp
+ ${QET_DIR}/sources/pdf_links.h
${QET_DIR}/sources/borderproperties.cpp
${QET_DIR}/sources/borderproperties.h
${QET_DIR}/sources/bordertitleblock.cpp
diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp
new file mode 100644
index 000000000..b4ce86a5e
--- /dev/null
+++ b/sources/pdf_links.cpp
@@ -0,0 +1,382 @@
+/*
+ Copyright 2006-2025 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "pdf_links.h"
+
+#include "diagram.h"
+#include "qetgraphicsitem/crossrefitem.h"
+#include "qetgraphicsitem/dynamicelementtextitem.h"
+#include "qetgraphicsitem/element.h"
+#include "qetgraphicsitem/elementtextitemgroup.h"
+
+// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
+// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace PdfLinks {
+
+void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
+ const PageGeometry &geom,
+ const QMap &pageMap,
+ const QString &outputFileName)
+{
+ if (!engine || !diagram)
+ return;
+
+ const QTransform &fit = geom.sceneToDevice;
+ const QRectF &target = geom.target;
+ const QRectF &pageBounds = geom.pageBounds;
+
+ // Compute, in PDF points on its OWN page, the rectangle to frame for a
+ // target element (used as a /FitR destination so the link zooms onto it).
+ auto destRectPdf = [&](Element *tgt) -> QRectF {
+ Diagram *dg = tgt ? tgt->diagram() : nullptr;
+ if (!dg) return QRectF();
+ const QRectF srcT = geom.sourceRectOf(dg);
+ if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
+ const qreal sT = qMin(target.width() / srcT.width(),
+ target.height() / srcT.height());
+ QTransform fitT;
+ fitT.translate(target.x(), target.y());
+ fitT.scale(sT, sT);
+ fitT.translate(-srcT.x(), -srcT.y());
+
+ QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
+ // Frame the element with a little context, and enforce a minimum
+ // framed size so tiny contacts don't zoom in extremely.
+ const qreal pad = 25.0;
+ elemScene.adjust(-pad, -pad, pad, pad);
+ const qreal minSide = 160.0;
+ if (elemScene.width() < minSide)
+ elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
+ (minSide - elemScene.width()) / 2.0, 0);
+ if (elemScene.height() < minSide)
+ elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
+ 0, (minSide - elemScene.height()) / 2.0);
+
+ const QRectF devT = fitT.mapRect(elemScene);
+ const QPointF a = geom.devToPdf(devT.topLeft());
+ const QPointF b = geom.devToPdf(devT.bottomRight());
+ return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
+ QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
+ };
+
+ auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
+ if (!targetElmt || !targetElmt->diagram()) return;
+ const int targetPage = pageMap.value(targetElmt->diagram(), -1);
+ if (targetPage < 1) return;
+ const QRectF devRect = fit.mapRect(sceneRect);
+ if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
+
+ QString frag = QString("page=%1").arg(targetPage);
+ const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
+ if (d.isValid())
+ frag += QString("&fitr=%1_%2_%3_%4")
+ .arg(qRound(d.left())).arg(qRound(d.top()))
+ .arg(qRound(d.right())).arg(qRound(d.bottom()));
+
+ QUrl url = QUrl::fromLocalFile(outputFileName);
+ url.setFragment(frag);
+ engine->drawHyperlink(devRect, url);
+ };
+
+ for (auto *item : diagram->items()) {
+
+ // --- CrossRefItem links ---
+ if (auto *xref = dynamic_cast(item)) {
+ for (auto it = xref->hoveredContactsMap().begin();
+ it != xref->hoveredContactsMap().end(); ++it)
+ {
+ Element *targetElmt = it.key();
+ if (!targetElmt || !targetElmt->diagram()) continue;
+ // it.value() is in the CrossRefItem's LOCAL coords -> scene
+ injectLink(xref->mapRectToScene(it.value()), targetElmt);
+ }
+ continue;
+ }
+
+ // --- Folio report links (DynamicElementTextItem) ---
+ if (auto *deti = dynamic_cast(item)) {
+ Element *parent = deti->parentElement();
+ if (!parent) continue;
+
+ // (a) Report element : label -> linked report on another folio
+ if (parent->linkType() & Element::AllReport) {
+ if (parent->linkedElements().isEmpty()) continue;
+
+ bool showsLabel =
+ (deti->textFrom() == DynamicElementTextItem::ElementInfo
+ && deti->infoName() == QLatin1String("label")) ||
+ (deti->textFrom() == DynamicElementTextItem::CompositeText
+ && deti->compositeText().contains(QStringLiteral("%{label}")));
+ if (!showsLabel) continue;
+
+ Element *targetElmt = parent->linkedElements().first();
+ if (!targetElmt || !targetElmt->diagram()) continue;
+
+ injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
+ continue;
+ }
+
+ // (b) Slave element : the "(folio-pos)" text -> master element
+ if (parent->linkType() == Element::Slave) {
+ QGraphicsTextItem *sx = deti->slaveXrefItem();
+ Element *master = deti->masterElement();
+ if (sx && master && master->diagram()) {
+ injectLink(sx->mapRectToScene(sx->boundingRect()), master);
+ }
+ continue;
+ }
+ continue;
+ }
+
+ // --- Slave cross-reference carried by a grouped text ---
+ if (auto *grp = dynamic_cast(item)) {
+ Element *parent = grp->parentElement();
+ if (!parent || parent->linkType() != Element::Slave) continue;
+ if (parent->linkedElements().isEmpty()) continue;
+ QGraphicsTextItem *sx = grp->slaveXrefItem();
+ if (!sx) continue;
+ Element *master = parent->linkedElements().first();
+ if (!master || !master->diagram()) continue;
+ injectLink(sx->mapRectToScene(sx->boundingRect()), master);
+ continue;
+ }
+ }
+}
+
+void convertUriToGoTo(const QString &pdfPath)
+{
+ // --- 1. Read raw bytes ---
+ QFile f(pdfPath);
+ if (!f.open(QIODevice::ReadOnly)) return;
+ QByteArray data = f.readAll();
+ f.close();
+
+ // --- 2. Collect page object numbers in document order ---
+ // Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
+ // This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
+ // marker also occurs inside content streams, and a forward lookahead
+ // wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
+ // document). Qt writes a single, flat /Kids array listing every page.
+ QVector pageObjs;
+ {
+ int pagesPos = data.indexOf("/Type /Pages");
+ int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
+ int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
+ int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
+ if (lb != -1 && rb != -1 && rb > lb) {
+ const QString kids =
+ QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
+ QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
+ auto it = re.globalMatch(kids);
+ while (it.hasNext()) {
+ int objNum = it.next().captured(1).toInt();
+ if (objNum > 0) pageObjs.append(objNum);
+ }
+ }
+ }
+
+ if (pageObjs.isEmpty()) return; // nothing to do
+
+ // --- 3. Replace URI annotations with GoTo ---
+ // Pattern (Qt always writes exactly this):
+ // /S /URI\n/URI (file:///...#page=N)\n
+ // or (older patches without file://):
+ // /S /URI\n/URI (page=N)\n
+ bool changed = false;
+ {
+ // We do a manual scan to handle variable-length replacements.
+ QByteArray out;
+ out.reserve(data.size());
+
+ const QByteArray sUri = "/S /URI\n/URI (";
+ const QByteArray sGoTo = "/S /GoTo\n/D [";
+ int pos = 0;
+
+ while (pos < data.size()) {
+ int found = data.indexOf(sUri, pos);
+ if (found == -1) {
+ out.append(data.mid(pos));
+ break;
+ }
+
+ // Copy everything up to the match
+ out.append(data.mid(pos, found - pos));
+
+ // Find closing ')' of the URI value
+ int uriStart = found + sUri.size();
+ int closeParen = data.indexOf(')', uriStart);
+ if (closeParen == -1) {
+ // Malformed — copy rest verbatim
+ out.append(data.mid(found));
+ pos = data.size();
+ break;
+ }
+
+ QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
+
+ // Extract page number: look for #page=N or bare page=N
+ int pageNum = -1;
+ int hashPos = uriVal.lastIndexOf("#page=");
+ int digitStart = -1;
+ if (hashPos != -1) {
+ digitStart = hashPos + 6;
+ } else if (uriVal.startsWith("page=")) {
+ digitStart = 5;
+ }
+ if (digitStart != -1) {
+ // Take only the leading digits: the fragment may carry extra
+ // parameters after the page number (e.g. "22&fitr=15_489_..."),
+ // and QByteArray::toInt() would fail on the whole remainder.
+ int e = digitStart;
+ while (e < uriVal.size()
+ && uriVal[e] >= '0' && uriVal[e] <= '9')
+ ++e;
+ if (e > digitStart)
+ pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
+ }
+
+ if (pageNum >= 1 && pageNum <= pageObjs.size()) {
+ // Valid page reference — emit GoTo action.
+ int pageObjNum = pageObjs[pageNum - 1];
+
+ // Optional precise destination: &fitr=Left_Bottom_Right_Top
+ // (integer PDF points). If present -> /FitR (frame the element);
+ // otherwise -> /Fit (whole page, top).
+ QByteArray dest = " /Fit]";
+ int fr = uriVal.indexOf("fitr=");
+ if (fr != -1) {
+ QByteArray rest = uriVal.mid(fr + 5);
+ // stop at first char that is not part of the number list
+ int end = 0;
+ while (end < rest.size()
+ && ((rest[end] >= '0' && rest[end] <= '9')
+ || rest[end] == '_' || rest[end] == '-'))
+ ++end;
+ QList parts = rest.left(end).split('_');
+ if (parts.size() == 4) {
+ dest = " /FitR " + parts[0] + " " + parts[1] + " "
+ + parts[2] + " " + parts[3] + "]";
+ }
+ }
+
+ QByteArray goTo = sGoTo
+ + QByteArray::number(pageObjNum)
+ + " 0 R" + dest;
+ out.append(goTo);
+ changed = true;
+ } else {
+ // Unknown page — keep original URI
+ out.append(sUri);
+ out.append(uriVal);
+ out.append(')');
+ }
+
+ pos = closeParen + 1; // skip past ')'
+ }
+
+ if (!changed) return; // nothing was replaced
+ data = out;
+ }
+
+ // --- 4. Rebuild xref table ---
+ // Find start of existing xref (last occurrence)
+ int xrefStart = data.lastIndexOf("\nxref\n");
+ if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
+ if (xrefStart == -1) return; // malformed PDF
+ ++xrefStart; // skip the leading '\n'
+
+ QByteArray body = data.left(xrefStart);
+
+ // Collect all object offsets from the body
+ QMap offsets; // objNum -> byte offset
+ {
+ const QByteArray objMarker = " 0 obj";
+ int pos = 0;
+ while ((pos = body.indexOf(objMarker, pos)) != -1) {
+ int numStart = pos - 1;
+ while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
+ --numStart;
+ QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
+ bool ok = false;
+ int objNum = numStr.toInt(&ok);
+ if (ok && objNum > 0)
+ offsets[objNum] = numStart;
+ ++pos;
+ }
+ }
+
+ if (offsets.isEmpty()) return;
+
+ int maxObj = offsets.lastKey();
+
+ // Build xref table
+ QByteArray xref;
+ xref += "xref\n";
+ xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
+ xref += "0000000000 65535 f \n";
+ for (int i = 1; i <= maxObj; ++i) {
+ if (offsets.contains(i)) {
+ xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
+ + " 00000 n \n";
+ } else {
+ xref += "0000000000 65535 f \n";
+ }
+ }
+
+ // Find trailer dict from the original xref section
+ int trailerPos = data.indexOf("trailer", xrefStart);
+ int trailerEnd = -1;
+ if (trailerPos != -1) {
+ trailerEnd = data.indexOf("%%EOF", trailerPos);
+ if (trailerEnd != -1) trailerEnd += 5;
+ }
+
+ QByteArray trailer;
+ if (trailerPos != -1 && trailerEnd != -1)
+ trailer = data.mid(trailerPos, trailerEnd - trailerPos);
+ else
+ trailer = "trailer\n<<>>\n%%EOF";
+
+ int newXrefOffset = body.size();
+
+ QByteArray result;
+ result.reserve(body.size() + xref.size() + trailer.size() + 30);
+ result += body;
+ result += xref;
+ result += trailer;
+ result += "\nstartxref\n";
+ result += QByteArray::number(newXrefOffset);
+ result += "\n%%EOF\n";
+
+ // --- 5. Write back ---
+ QFile out(pdfPath);
+ if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
+ out.write(result);
+ out.close();
+}
+
+} // namespace PdfLinks
diff --git a/sources/pdf_links.h b/sources/pdf_links.h
new file mode 100644
index 000000000..1a2b11344
--- /dev/null
+++ b/sources/pdf_links.h
@@ -0,0 +1,79 @@
+/*
+ Copyright 2006-2025 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef PDF_LINKS_H
+#define PDF_LINKS_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QPdfEngine;
+class Diagram;
+
+/**
+ Shared helper that turns a project's cross-references and folio reports
+ into clickable internal hyperlinks in a Qt-generated PDF. Used by both the
+ GUI print path (ProjectPrintWindow) and the headless CLI export, each of
+ which builds its own page geometry and passes it in — this code never
+ computes the scene-to-page mapping itself.
+*/
+namespace PdfLinks {
+
+ /**
+ Geometry mapping for one rendered PDF page. Each caller builds this
+ from its OWN page setup (printer page layout vs QPdfWriter), since the
+ device-pixel and point conversions differ between them.
+ */
+ struct PageGeometry {
+ /// scene coordinates -> device pixels (the same "fit" render() applied)
+ QTransform sceneToDevice;
+ /// device paint rectangle, in pixels (the page area)
+ QRectF target;
+ /// links whose rectangle falls outside this are dropped
+ QRectF pageBounds;
+ /// device pixels -> PDF points (replicates the engine's page matrix)
+ std::function devToPdf;
+ /// a diagram -> its source rectangle in scene pixels (for /FitR framing)
+ std::function sourceRectOf;
+ };
+
+ /**
+ Inject clickable cross-reference / folio-report hyperlinks for @p diagram
+ into the current page of @p engine. Each link is emitted as a URI
+ annotation encoding the target page and a /FitR rectangle;
+ convertUriToGoTo() then rewrites those into native internal GoTo actions.
+ */
+ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
+ const PageGeometry &geom,
+ const QMap &pageMap,
+ const QString &outputFileName);
+
+ /**
+ Post-process a Qt-generated PDF file: rewrite every "/S /URI" link
+ 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
+ such annotations.
+ */
+ void convertUriToGoTo(const QString &pdfPath);
+
+}
+
+#endif // PDF_LINKS_H
diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp
index 01c1e5940..5333f1a64 100644
--- a/sources/print/projectprintwindow.cpp
+++ b/sources/print/projectprintwindow.cpp
@@ -18,6 +18,7 @@
#include "projectprintwindow.h"
#include "../diagram.h"
+#include "../pdf_links.h"
#include "../qeticons.h"
#include "../qetproject.h"
#include "../qetversion.h"
@@ -200,241 +201,6 @@ ProjectPrintWindow::~ProjectPrintWindow()
* @brief ProjectPrintWindow::requestPaint
* @param slot called when m_preview emit paintRequested
*/
-/**
- * @brief ProjectPrintWindow::pdfConvertUriToGoTo
- * Post-processes a Qt-generated PDF to replace URI link annotations
- * (file:///path/to/file.pdf#page=N) with native PDF GoTo actions
- * ([pageObj 0 R /Fit]). This makes cross-reference links work in all
- * PDF viewers regardless of where the file is stored.
- *
- * The function:
- * 1. Reads the PDF as raw bytes.
- * 2. Collects page object numbers in document order by scanning for
- * objects that contain "/Type /Page" (but not "/Type /Pages").
- * 3. Replaces every annotation action block
- * /S /URI\n/URI (file://...#page=N)
- * with
- * /S /GoTo\n/D [ 0 R /Fit]
- * 4. Rebuilds the cross-reference table (offsets change because the
- * replacement strings have different lengths).
- * 5. Writes the result back to the same file.
- *
- * The function is intentionally conservative: if any step fails (file
- * not found, malformed PDF, no URI annotations) it returns silently
- * without corrupting the file.
- */
-static void pdfConvertUriToGoTo(const QString &pdfPath)
-{
- // --- 1. Read raw bytes ---
- QFile f(pdfPath);
- if (!f.open(QIODevice::ReadOnly)) return;
- QByteArray data = f.readAll();
- f.close();
-
- // --- 2. Collect page object numbers in document order ---
- // Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
- // This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
- // marker also occurs inside content streams, and a forward lookahead
- // wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
- // document). Qt writes a single, flat /Kids array listing every page.
- QVector pageObjs;
- {
- int pagesPos = data.indexOf("/Type /Pages");
- int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
- int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
- int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
- if (lb != -1 && rb != -1 && rb > lb) {
- const QString kids =
- QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
- QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
- auto it = re.globalMatch(kids);
- while (it.hasNext()) {
- int objNum = it.next().captured(1).toInt();
- if (objNum > 0) pageObjs.append(objNum);
- }
- }
- }
-
- if (pageObjs.isEmpty()) return; // nothing to do
-
- // --- 3. Replace URI annotations with GoTo ---
- // Pattern (Qt always writes exactly this):
- // /S /URI\n/URI (file:///...#page=N)\n
- // or (older patches without file://):
- // /S /URI\n/URI (page=N)\n
- bool changed = false;
- {
- // We do a manual scan to handle variable-length replacements.
- QByteArray out;
- out.reserve(data.size());
-
- const QByteArray sUri = "/S /URI\n/URI (";
- const QByteArray sGoTo = "/S /GoTo\n/D [";
- int pos = 0;
-
- while (pos < data.size()) {
- int found = data.indexOf(sUri, pos);
- if (found == -1) {
- out.append(data.mid(pos));
- break;
- }
-
- // Copy everything up to the match
- out.append(data.mid(pos, found - pos));
-
- // Find closing ')' of the URI value
- int uriStart = found + sUri.size();
- int closeParen = data.indexOf(')', uriStart);
- if (closeParen == -1) {
- // Malformed — copy rest verbatim
- out.append(data.mid(found));
- pos = data.size();
- break;
- }
-
- QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
-
- // Extract page number: look for #page=N or bare page=N
- int pageNum = -1;
- int hashPos = uriVal.lastIndexOf("#page=");
- int digitStart = -1;
- if (hashPos != -1) {
- digitStart = hashPos + 6;
- } else if (uriVal.startsWith("page=")) {
- digitStart = 5;
- }
- if (digitStart != -1) {
- // Take only the leading digits: the fragment may carry extra
- // parameters after the page number (e.g. "22&fitr=15_489_..."),
- // and QByteArray::toInt() would fail on the whole remainder.
- int e = digitStart;
- while (e < uriVal.size()
- && uriVal[e] >= '0' && uriVal[e] <= '9')
- ++e;
- if (e > digitStart)
- pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
- }
-
- if (pageNum >= 1 && pageNum <= pageObjs.size()) {
- // Valid page reference — emit GoTo action.
- int pageObjNum = pageObjs[pageNum - 1];
-
- // Optional precise destination: &fitr=Left_Bottom_Right_Top
- // (integer PDF points). If present -> /FitR (frame the element);
- // otherwise -> /Fit (whole page, top).
- QByteArray dest = " /Fit]";
- int fr = uriVal.indexOf("fitr=");
- if (fr != -1) {
- QByteArray rest = uriVal.mid(fr + 5);
- // stop at first char that is not part of the number list
- int end = 0;
- while (end < rest.size()
- && ((rest[end] >= '0' && rest[end] <= '9')
- || rest[end] == '_' || rest[end] == '-'))
- ++end;
- QList parts = rest.left(end).split('_');
- if (parts.size() == 4) {
- dest = " /FitR " + parts[0] + " " + parts[1] + " "
- + parts[2] + " " + parts[3] + "]";
- }
- }
-
- QByteArray goTo = sGoTo
- + QByteArray::number(pageObjNum)
- + " 0 R" + dest;
- out.append(goTo);
- changed = true;
- } else {
- // Unknown page — keep original URI
- out.append(sUri);
- out.append(uriVal);
- out.append(')');
- }
-
- pos = closeParen + 1; // skip past ')'
- }
-
- if (!changed) return; // nothing was replaced
- data = out;
- }
-
- // --- 4. Rebuild xref table ---
- // Find start of existing xref (last occurrence)
- int xrefStart = data.lastIndexOf("\nxref\n");
- if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
- if (xrefStart == -1) return; // malformed PDF
- ++xrefStart; // skip the leading '\n'
-
- QByteArray body = data.left(xrefStart);
-
- // Collect all object offsets from the body
- QMap offsets; // objNum -> byte offset
- {
- const QByteArray objMarker = " 0 obj";
- int pos = 0;
- while ((pos = body.indexOf(objMarker, pos)) != -1) {
- int numStart = pos - 1;
- while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
- --numStart;
- QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
- bool ok = false;
- int objNum = numStr.toInt(&ok);
- if (ok && objNum > 0)
- offsets[objNum] = numStart;
- ++pos;
- }
- }
-
- if (offsets.isEmpty()) return;
-
- int maxObj = offsets.lastKey();
-
- // Build xref table
- QByteArray xref;
- xref += "xref\n";
- xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
- xref += "0000000000 65535 f \n";
- for (int i = 1; i <= maxObj; ++i) {
- if (offsets.contains(i)) {
- xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
- + " 00000 n \n";
- } else {
- xref += "0000000000 65535 f \n";
- }
- }
-
- // Find trailer dict from the original xref section
- int trailerPos = data.indexOf("trailer", xrefStart);
- int trailerEnd = -1;
- if (trailerPos != -1) {
- trailerEnd = data.indexOf("%%EOF", trailerPos);
- if (trailerEnd != -1) trailerEnd += 5;
- }
-
- QByteArray trailer;
- if (trailerPos != -1 && trailerEnd != -1)
- trailer = data.mid(trailerPos, trailerEnd - trailerPos);
- else
- trailer = "trailer\n<<>>\n%%EOF";
-
- int newXrefOffset = body.size();
-
- QByteArray result;
- result.reserve(body.size() + xref.size() + trailer.size() + 30);
- result += body;
- result += xref;
- result += trailer;
- result += "\nstartxref\n";
- result += QByteArray::number(newXrefOffset);
- result += "\n%%EOF\n";
-
- // --- 5. Write back ---
- QFile out(pdfPath);
- if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
- out.write(result);
- out.close();
-}
-
void ProjectPrintWindow::requestPaint()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
@@ -642,123 +408,17 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
};
- // Compute, in PDF points on its OWN page, the rectangle to frame for a
- // target element (used as a /FitR destination so the link zooms onto it).
- auto destRectPdf = [&](Element *tgt) -> QRectF {
- Diagram *dg = tgt ? tgt->diagram() : nullptr;
- if (!dg) return QRectF();
- const QRectF srcT = QRectF(diagramRect(dg, exportProperties()));
- if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
- const qreal sT = qMin(target.width() / srcT.width(),
- target.height() / srcT.height());
- QTransform fitT;
- fitT.translate(target.x(), target.y());
- fitT.scale(sT, sT);
- fitT.translate(-srcT.x(), -srcT.y());
-
- QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
- // Frame the element with a little context, and enforce a minimum
- // framed size so tiny contacts don't zoom in extremely.
- const qreal pad = 25.0;
- elemScene.adjust(-pad, -pad, pad, pad);
- const qreal minSide = 160.0;
- if (elemScene.width() < minSide)
- elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
- (minSide - elemScene.width()) / 2.0, 0);
- if (elemScene.height() < minSide)
- elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
- 0, (minSide - elemScene.height()) / 2.0);
-
- const QRectF devT = fitT.mapRect(elemScene);
- const QPointF a = devToPdf(devT.topLeft());
- const QPointF b = devToPdf(devT.bottomRight());
- return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
- QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
+ PdfLinks::PageGeometry geom;
+ geom.sceneToDevice = fit;
+ geom.target = target;
+ geom.pageBounds = pageBounds;
+ geom.devToPdf = devToPdf;
+ geom.sourceRectOf = [this](Diagram *dg) {
+ return QRectF(diagramRect(dg, exportProperties()));
};
-
- auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
- if (!targetElmt || !targetElmt->diagram()) return;
- const int targetPage =
- diagramPageMap.value(targetElmt->diagram(), -1);
- if (targetPage < 1) return;
- const QRectF devRect = fit.mapRect(sceneRect);
- if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
-
- QString frag = QString("page=%1").arg(targetPage);
- const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
- if (d.isValid())
- frag += QString("&fitr=%1_%2_%3_%4")
- .arg(qRound(d.left())).arg(qRound(d.top()))
- .arg(qRound(d.right())).arg(qRound(d.bottom()));
-
- QUrl url = QUrl::fromLocalFile(printer->outputFileName());
- url.setFragment(frag);
- pdfEngine->drawHyperlink(devRect, url);
- };
-
- for (auto *item : diagram->items()) {
-
- // --- CrossRefItem links ---
- if (auto *xref = dynamic_cast(item)) {
- for (auto it = xref->hoveredContactsMap().begin();
- it != xref->hoveredContactsMap().end(); ++it)
- {
- Element *targetElmt = it.key();
- if (!targetElmt || !targetElmt->diagram()) continue;
- // it.value() est en coords LOCALES du CrossRefItem -> scene
- injectLink(xref->mapRectToScene(it.value()), targetElmt);
- }
- continue;
- }
-
- // --- Folio report links (DynamicElementTextItem) ---
- if (auto *deti = dynamic_cast(item)) {
- Element *parent = deti->parentElement();
- if (!parent) continue;
-
- // (a) Report element : label -> linked report on another folio
- if (parent->linkType() & Element::AllReport) {
- if (parent->linkedElements().isEmpty()) continue;
-
- bool showsLabel =
- (deti->textFrom() == DynamicElementTextItem::ElementInfo
- && deti->infoName() == QLatin1String("label")) ||
- (deti->textFrom() == DynamicElementTextItem::CompositeText
- && deti->compositeText().contains(QStringLiteral("%{label}")));
- if (!showsLabel) continue;
-
- Element *targetElmt = parent->linkedElements().first();
- if (!targetElmt || !targetElmt->diagram()) continue;
-
- injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
- continue;
- }
-
- // (b) Slave element : the "(folio-pos)" text -> master element
- if (parent->linkType() == Element::Slave) {
- QGraphicsTextItem *sx = deti->slaveXrefItem();
- Element *master = deti->masterElement();
- if (sx && master && master->diagram()) {
- injectLink(sx->mapRectToScene(sx->boundingRect()), master);
- }
- continue;
- }
- continue;
- }
-
- // --- Slave cross-reference carried by a grouped text ---
- if (auto *grp = dynamic_cast(item)) {
- Element *parent = grp->parentElement();
- if (!parent || parent->linkType() != Element::Slave) continue;
- if (parent->linkedElements().isEmpty()) continue;
- QGraphicsTextItem *sx = grp->slaveXrefItem();
- if (!sx) continue;
- Element *master = parent->linkedElements().first();
- if (!master || !master->diagram()) continue;
- injectLink(sx->mapRectToScene(sx->boundingRect()), master);
- continue;
- }
- }
+ PdfLinks::injectCrossRefLinks(
+ pdfEngine, diagram, geom, diagramPageMap,
+ printer->outputFileName());
}
}
////PDF links end////
@@ -1235,7 +895,7 @@ void ProjectPrintWindow::print()
QTimer::singleShot(0, this, [this, pdfFile]() {
// Convert URI link annotations into native internal GoTo/FitR
// actions so cross-references jump inside the document.
- pdfConvertUriToGoTo(pdfFile);
+ PdfLinks::convertUriToGoTo(pdfFile);
this->close();
});
} else {