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 {