From 67b1665b3604b0c4bbb9c805f2fe681f142ae4d3 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 13 Aug 2026 11:54:33 +0200 Subject: [PATCH 1/5] Export component info as invisible PDF text annotations --- sources/pdf_links.cpp | 206 +++++++++++++++++++++++++++ sources/pdf_links.h | 21 ++- sources/print/projectprintwindow.cpp | 55 +++++++ sources/print/projectprintwindow.h | 7 +- sources/print/projectprintwindow.ui | 32 +++-- 5 files changed, 301 insertions(+), 20 deletions(-) diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index b4ce86a5e..3cb021220 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -238,6 +239,15 @@ void convertUriToGoTo(const QString &pdfPath) QByteArray uriVal = data.mid(uriStart, closeParen - uriStart); + // Skip component-info annotations — handled by + // convertComponentInfoAnnotations() in a separate pass. + if (uriVal.startsWith("componentinfo://") + || uriVal.startsWith("http://componentinfo.local/")) { + out.append(data.mid(found, closeParen + 1 - found)); + pos = closeParen + 1; + continue; + } + // Extract page number: look for #page=N or bare page=N int pageNum = -1; int hashPos = uriVal.lastIndexOf("#page="); @@ -379,4 +389,200 @@ void convertUriToGoTo(const QString &pdfPath) out.close(); } +void convertComponentInfoAnnotations(const QString &pdfPath, + const QList &annotations) +{ + if (annotations.isEmpty()) return; + + QFile f(pdfPath); + if (!f.open(QIODevice::ReadOnly)) return; + QByteArray data = f.readAll(); + f.close(); + + const QByteArray marker("http://componentinfo.local/"); + if (!data.contains(marker)) return; + + int xrefStart = data.lastIndexOf("\nxref\n"); + if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref "); + if (xrefStart == -1) return; + ++xrefStart; + QByteArray body = data.left(xrefStart); + + // Pre-scan body for highest object number + int maxObjNum = 0; + { + const QByteArray objMarker(" 0 obj"); + int p = 0; + while ((p = body.indexOf(objMarker, p)) != -1) { + int numStart = p - 1; + while (numStart > 0 && body[numStart - 1] != '\n' && body[numStart - 1] != '\r') + --numStart; + QByteArray numStr = body.mid(numStart, p - numStart).trimmed(); + bool ok = false; + int num = numStr.toInt(&ok); + if (ok && num > maxObjNum) + maxObjNum = num; + ++p; + } + } + // Empty Form XObject used as invisible appearance for all annotations. + // All annotations reference this single object. + int emptyXObjNum = maxObjNum + 1; + + QByteArray out; + out.reserve(data.size()); + + int pos = 0; + int annotIdx = 0; + + while (pos < body.size()) { + int markerPos = body.indexOf(marker, pos); + if (markerPos == -1) { + out.append(body.mid(pos)); + break; + } + + int uriOpen = body.lastIndexOf("/URI (", markerPos); + int closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1; + if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) { + out.append(body.mid(pos, markerPos + marker.size() - pos)); + pos = markerPos + marker.size(); + continue; + } + + // Find /S /URI before /URI ( + int sUriPos = body.lastIndexOf("/S /URI", uriOpen); + + // Find the /A << that opens the action dict containing /S /URI. + // Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>> + // We need /Contents at the annotation level, NOT inside /A <<. + int aDictOpen = body.lastIndexOf("/A <<", (sUriPos != -1) ? sUriPos : uriOpen); + int copyEnd = (aDictOpen != -1 && aDictOpen >= pos) ? aDictOpen + : (sUriPos != -1 && sUriPos >= pos) ? sUriPos + : uriOpen; + out.append(body.mid(pos, copyEnd - pos)); + + // Get text from annotations list (matched by order) + if (annotIdx >= annotations.size()) { + // Keep original action dict intact + out.append(body.mid(copyEnd, closeParen + 1 - copyEnd)); + pos = closeParen + 1; + continue; + } + + QByteArray contents = annotations[annotIdx].contents.toUtf8(); + ++annotIdx; + + // Replace /Subtype /Link with /Subtype /Text + int subTypePos = out.lastIndexOf("/Subtype /Link"); + if (subTypePos != -1) { + out.replace(subTypePos, 14, "/Subtype /Text"); + } + + // 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. + QTextCodec *codec = QTextCodec::codecForName("UTF-16BE"); + QByteArray utf16be; + utf16be.append('\xfe'); + utf16be.append('\xff'); + if (codec) { + utf16be += codec->fromUnicode(QString::fromUtf8(contents)); + } else { + // Fallback: manual UTF-16BE encoding + QString str = QString::fromUtf8(contents); + for (int i = 0; i < str.size(); ++i) { + ushort cp = str.at(i).unicode(); + utf16be.append(static_cast((cp >> 8) & 0xFF)); + utf16be.append(static_cast(cp & 0xFF)); + } + } + + out += "/Contents <"; + out += utf16be.toHex(); + out += ">\n/AP << /N " + QByteArray::number(emptyXObjNum) + " 0 R >>\n"; + + // Skip the entire /A << ... >> action dict. + // After closeParen ')' we have: \n>> (closes /A dict) \n>> (closes annot dict) + int actionDictClose = body.indexOf(">>", closeParen + 1); + if (actionDictClose != -1) { + pos = actionDictClose + 2; // skip past >> that closes /A dict + } else { + pos = closeParen + 1; + } + } + + if (annotIdx == 0) return; + + // Append empty Form XObject (shared by all annotations for invisible appearance) + QByteArray emptyXObj; + emptyXObj += QByteArray::number(emptyXObjNum) + " 0 obj\n"; + emptyXObj += "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] /Length 0 >>\n"; + emptyXObj += "stream\nendstream\n"; + emptyXObj += "endobj\n"; + out += emptyXObj; + + // Rebuild xref table + QMap offsets; + { + const QByteArray objMarker(" 0 obj"); + int p = 0; + while ((p = out.indexOf(objMarker, p)) != -1) { + int numStart = p - 1; + while (numStart > 0 && out[numStart - 1] != '\n' && out[numStart - 1] != '\r') + --numStart; + QByteArray numStr = out.mid(numStart, p - numStart).trimmed(); + bool ok = false; + int objNum = numStr.toInt(&ok); + if (ok && objNum > 0) + offsets[objNum] = numStart; + ++p; + } + } + + if (offsets.isEmpty()) return; + + int maxObj = offsets.lastKey(); + + 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"; + } + } + + QByteArray trailer; + { + int tPos = data.indexOf("trailer", xrefStart); + if (tPos != -1) { + int tEnd = data.indexOf("%%EOF", tPos); + if (tEnd != -1) tEnd += 5; + if (tEnd != -1) + trailer = data.mid(tPos, tEnd - tPos); + } + } + if (trailer.isEmpty()) + trailer = "trailer\n<<>>\n%%EOF"; + + QByteArray result; + result.reserve(out.size() + xref.size() + trailer.size() + 64); + result += out; + int newXrefOffset = out.size(); + result += xref; + result += trailer; + result += "\nstartxref\n"; + result += QByteArray::number(newXrefOffset); + result += "\n%%EOF\n"; + + QFile outF(pdfPath); + if (!outF.open(QIODevice::WriteOnly | QIODevice::Truncate)) return; + outF.write(result); + outF.close(); +} + } // namespace PdfLinks diff --git a/sources/pdf_links.h b/sources/pdf_links.h index 1a2b11344..07993f087 100644 --- a/sources/pdf_links.h +++ b/sources/pdf_links.h @@ -66,13 +66,20 @@ namespace PdfLinks { 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); +/** + 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); + +struct ComponentInfo { + QString contents; +}; + +void convertComponentInfoAnnotations(const QString &pdfPath, + const QList &annotations); } diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index b115ad5b7..0edec9cce 100644 --- a/sources/print/projectprintwindow.cpp +++ b/sources/print/projectprintwindow.cpp @@ -20,10 +20,12 @@ #include "../diagram.h" #include "../pdf_links.h" #include "../qeticons.h" +#include "../qetinformation.h" #include "../qetproject.h" #include "../qetversion.h" #include "../qetgraphicsitem/crossrefitem.h" #include "../qetgraphicsitem/dynamicelementtextitem.h" +#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/elementtextitemgroup.h" #include "ui_projectprintwindow.h" @@ -156,6 +158,7 @@ ProjectPrintWindow::ProjectPrintWindow(QETProject *project, QPrinter *printer, Q ui->m_button_box->addButton(pdf_button, QDialogButtonBox::ActionRole); connect(pdf_button, &QPushButton::clicked, this, &ProjectPrintWindow::exportToPDF); } + ui->m_component_info_cb->setVisible(m_printer->outputFormat() == QPrinter::PdfFormat); auto exp = ExportProperties::defaultPrintProperties(); ui->m_draw_border_cb->setChecked(exp.draw_border); @@ -426,6 +429,50 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter PdfLinks::injectCrossRefLinks( pdfEngine, diagram, geom, diagramPageMap, printer->outputFileName()); + + ////Collect component info for popup annotations//// + if (ui->m_component_info_cb->isChecked()) { + for (auto *item : diagram->items()) { + auto *el = dynamic_cast(item); + if (!el) continue; + + // Skip reports and slaves + auto lt = el->linkType(); + if (lt & Element::AllReport) continue; + if (lt == Element::Slave) continue; + + auto info = el->elementInformations(); + if (info.count() == 0) continue; + + // Build info text + QStringList lines; + for (const QString &key : {"label", "manufacturer", "designation", "description"}) { + if (info.contains(key) && !info.value(key).toString().isEmpty()) + lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString(); + } + for (const QString &key : info.keys()) { + if (key == "formula") continue; + QString translated = QETInformation::translatedInfoKey(key); + if (lines.contains(translated + ": " + info.value(key).toString())) continue; + if (info.value(key).toString().isEmpty()) continue; + lines << translated + ": " + info.value(key).toString(); + } + if (lines.isEmpty()) continue; + + // Compute element rect in device pixels + QRectF elemScene = el->mapRectToScene(el->boundingRect()); + QRectF devRect = fit.mapRect(elemScene); + + PdfLinks::ComponentInfo ci; + ci.contents = lines.join("\n"); + m_componentInfoList.append(ci); + + // Create a link annotation as placeholder — post-processing + // will convert it to an invisible text annotation with the actual content + pdfEngine->drawHyperlink(devRect, QUrl("http://componentinfo.local/")); + } + } + ////Component info end//// } } ////PDF links end//// @@ -799,6 +846,7 @@ void ProjectPrintWindow::on_m_draw_titleblock_cb_clicked() { m_preview->upd void ProjectPrintWindow::on_m_keep_conductor_color_cb_clicked() { m_preview->updatePreview(); } void ProjectPrintWindow::on_m_draw_terminal_cb_clicked() { m_preview->updatePreview(); } void ProjectPrintWindow::on_m_draw_terminal_names_cb_clicked() { m_preview->updatePreview(); } +void ProjectPrintWindow::on_m_component_info_cb_clicked() { m_preview->updatePreview(); } void ProjectPrintWindow::on_m_fit_in_page_cb_clicked() { m_preview->updatePreview(); } void ProjectPrintWindow::on_m_use_full_page_cb_clicked() { @@ -903,9 +951,16 @@ void ProjectPrintWindow::print() // which prevents the black screen observed on Apple Silicon under // macOS Sequoia (QPrintPreviewWidget + CALayer timing issue). QTimer::singleShot(0, this, [this, pdfFile]() { + // Convert component-info link annotations into invisible text annotations + if (ui->m_component_info_cb->isChecked() && !m_componentInfoList.isEmpty()) { + PdfLinks::convertComponentInfoAnnotations(pdfFile, m_componentInfoList); + m_componentInfoList.clear(); + } + // Convert URI link annotations into native internal GoTo/FitR // actions so cross-references jump inside the document. PdfLinks::convertUriToGoTo(pdfFile); + this->close(); }); } else { diff --git a/sources/print/projectprintwindow.h b/sources/print/projectprintwindow.h index f290ff4c2..72fa8e41d 100644 --- a/sources/print/projectprintwindow.h +++ b/sources/print/projectprintwindow.h @@ -19,6 +19,7 @@ #define PROJECTPRINTWINDOW_H #include "../exportproperties.h" +#include "../pdf_links.h" #include #include @@ -55,8 +56,9 @@ class ProjectPrintWindow : public QMainWindow void on_m_draw_titleblock_cb_clicked(); void on_m_keep_conductor_color_cb_clicked(); void on_m_draw_terminal_cb_clicked(); - void on_m_draw_terminal_names_cb_clicked(); - void on_m_fit_in_page_cb_clicked(); + void on_m_draw_terminal_names_cb_clicked(); + void on_m_component_info_cb_clicked(); + void on_m_fit_in_page_cb_clicked(); void on_m_use_full_page_cb_clicked(); void on_m_zoom_out_action_triggered(); void on_m_zoom_in_action_triggered(); @@ -102,6 +104,7 @@ class ProjectPrintWindow : public QMainWindow QPrintPreviewWidget *m_preview=nullptr; QColor m_backup_diagram_background_color; QHash m_diagram_list_hash; + QList m_componentInfoList; }; #endif // PROJECTPRINTWINDOW_H diff --git a/sources/print/projectprintwindow.ui b/sources/print/projectprintwindow.ui index 2d1aec28c..a1db8e411 100644 --- a/sources/print/projectprintwindow.ui +++ b/sources/print/projectprintwindow.ui @@ -183,17 +183,27 @@ - - - - Dessiner les noms des bornes - - - false - - - - + + + + Dessiner les noms des bornes + + + false + + + + + + + Inscrire les informations des composants + + + false + + + + From f0348a7cd858129328bc7927ec17c5e5db7e9950 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Fri, 14 Aug 2026 08:58:36 +0200 Subject: [PATCH 2/5] fix --- sources/pdf_links.cpp | 120 ++++++++++++++++++++------- sources/pdf_links.h | 29 ++++--- sources/print/projectprintwindow.cpp | 47 ++++++----- sources/print/projectprintwindow.h | 6 +- 4 files changed, 134 insertions(+), 68 deletions(-) diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 3cb021220..5cf176f76 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include #include @@ -399,8 +398,8 @@ void convertComponentInfoAnnotations(const QString &pdfPath, QByteArray data = f.readAll(); f.close(); - const QByteArray marker("http://componentinfo.local/"); - if (!data.contains(marker)) return; + const QByteArray markerPrefix("http://componentinfo.local/"); + if (!data.contains(markerPrefix)) return; int xrefStart = data.lastIndexOf("\nxref\n"); if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref "); @@ -433,20 +432,34 @@ void convertComponentInfoAnnotations(const QString &pdfPath, out.reserve(data.size()); int pos = 0; - int annotIdx = 0; + bool anyConverted = false; while (pos < body.size()) { - int markerPos = body.indexOf(marker, pos); + // Find next indexed marker: http://componentinfo.local/ + int markerPos = body.indexOf(markerPrefix, pos); if (markerPos == -1) { out.append(body.mid(pos)); 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 closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1; if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) { - out.append(body.mid(pos, markerPos + marker.size() - pos)); - pos = markerPos + marker.size(); + out.append(body.mid(pos, idxEnd - pos)); + pos = idxEnd; continue; } @@ -455,43 +468,63 @@ void convertComponentInfoAnnotations(const QString &pdfPath, // Find the /A << that opens the action dict containing /S /URI. // Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>> - // We need /Contents at the annotation level, NOT inside /A <<. - int aDictOpen = body.lastIndexOf("/A <<", (sUriPos != -1) ? sUriPos : uriOpen); - int copyEnd = (aDictOpen != -1 && aDictOpen >= pos) ? aDictOpen - : (sUriPos != -1 && sUriPos >= pos) ? sUriPos - : uriOpen; - out.append(body.mid(pos, copyEnd - pos)); + int aDictOpen = (sUriPos != -1) + ? body.lastIndexOf("/A <<", sUriPos) + : -1; - // Get text from annotations list (matched by order) - if (annotIdx >= annotations.size()) { - // Keep original action dict intact - out.append(body.mid(copyEnd, closeParen + 1 - copyEnd)); + // If /A << is not found, leave this annotation untouched + if (aDictOpen == -1 || aDictOpen < pos) { + out.append(body.mid(pos, closeParen + 1 - pos)); pos = closeParen + 1; continue; } - QByteArray contents = annotations[annotIdx].contents.toUtf8(); - ++annotIdx; + // Validate index + 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 - int subTypePos = out.lastIndexOf("/Subtype /Link"); + // Copy annotation dict header up to /A << + 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) { out.replace(subTypePos, 14, "/Subtype /Text"); } // 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. - QTextCodec *codec = QTextCodec::codecForName("UTF-16BE"); QByteArray utf16be; utf16be.append('\xfe'); utf16be.append('\xff'); - if (codec) { - utf16be += codec->fromUnicode(QString::fromUtf8(contents)); - } else { - // Fallback: manual UTF-16BE encoding - QString str = QString::fromUtf8(contents); - for (int i = 0; i < str.size(); ++i) { - ushort cp = str.at(i).unicode(); + { + for (int i = 0; i < contents.size(); ) { + ushort cp = 0; + uchar c = static_cast(contents.at(i)); + if (c < 0x80) { + cp = c; + ++i; + } else if ((c & 0xE0) == 0xC0 && i + 1 < contents.size()) { + cp = ((c & 0x1F) << 6) + | (static_cast(contents.at(i + 1)) & 0x3F); + i += 2; + } else if ((c & 0xF0) == 0xE0 && i + 2 < contents.size()) { + cp = ((c & 0x0F) << 12) + | ((static_cast(contents.at(i + 1)) & 0x3F) << 6) + | (static_cast(contents.at(i + 2)) & 0x3F); + i += 3; + } else { + cp = 0xFFFD; // replacement character + ++i; + } utf16be.append(static_cast((cp >> 8) & 0xFF)); utf16be.append(static_cast(cp & 0xFF)); } @@ -509,9 +542,10 @@ void convertComponentInfoAnnotations(const QString &pdfPath, } else { pos = closeParen + 1; } + anyConverted = true; } - if (annotIdx == 0) return; + if (!anyConverted) return; // Append empty Form XObject (shared by all annotations for invisible appearance) QByteArray emptyXObj; @@ -556,6 +590,7 @@ void convertComponentInfoAnnotations(const QString &pdfPath, } } + // Copy trailer and bump /Size to account for the new XObject QByteArray trailer; { int tPos = data.indexOf("trailer", xrefStart); @@ -569,6 +604,31 @@ void convertComponentInfoAnnotations(const QString &pdfPath, if (trailer.isEmpty()) 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; result.reserve(out.size() + xref.size() + trailer.size() + 64); result += out; diff --git a/sources/pdf_links.h b/sources/pdf_links.h index 07993f087..27e266420 100644 --- a/sources/pdf_links.h +++ b/sources/pdf_links.h @@ -66,20 +66,25 @@ namespace PdfLinks { 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); + /** + 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); -struct ComponentInfo { - QString contents; -}; + struct ComponentInfo { + QString contents; + }; -void convertComponentInfoAnnotations(const QString &pdfPath, - const QList &annotations); + /** + Post-process a Qt-generated PDF file: convert component-info placeholder + link annotations (http://componentinfo.local/) into invisible text + annotations with the actual component info as /Contents. + */ + void convertComponentInfoAnnotations(const QString &pdfPath, + const QList &annotations); } diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index 0edec9cce..0215dd44e 100644 --- a/sources/print/projectprintwindow.cpp +++ b/sources/print/projectprintwindow.cpp @@ -432,8 +432,9 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter ////Collect component info for popup annotations//// if (ui->m_component_info_cb->isChecked()) { + int annotIndex = 0; for (auto *item : diagram->items()) { - auto *el = dynamic_cast(item); + auto *el = qgraphicsitem_cast(item); if (!el) continue; // Skip reports and slaves @@ -444,32 +445,32 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter auto info = el->elementInformations(); if (info.count() == 0) continue; - // Build info text - QStringList lines; - for (const QString &key : {"label", "manufacturer", "designation", "description"}) { - if (info.contains(key) && !info.value(key).toString().isEmpty()) - lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString(); - } - for (const QString &key : info.keys()) { - if (key == "formula") continue; - QString translated = QETInformation::translatedInfoKey(key); - if (lines.contains(translated + ": " + info.value(key).toString())) continue; - if (info.value(key).toString().isEmpty()) continue; - lines << translated + ": " + info.value(key).toString(); - } + // Build info text + QStringList lines; + for (const QString &key : {"label", "manufacturer", "designation", "description"}) { + if (info.contains(key) && !info.value(key).toString().isEmpty()) + lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString(); + } + for (const QString &key : info.keys()) { + if (key == "formula") continue; + QString translated = QETInformation::translatedInfoKey(key); + if (lines.contains(translated + ": " + info.value(key).toString())) continue; + if (info.value(key).toString().isEmpty()) continue; + lines << translated + ": " + info.value(key).toString(); + } if (lines.isEmpty()) continue; - // Compute element rect in device pixels - QRectF elemScene = el->mapRectToScene(el->boundingRect()); - QRectF devRect = fit.mapRect(elemScene); + // Compute element rect in device pixels + QRectF elemScene = el->mapRectToScene(el->boundingRect()); + QRectF devRect = fit.mapRect(elemScene); - PdfLinks::ComponentInfo ci; - ci.contents = lines.join("\n"); - m_componentInfoList.append(ci); + PdfLinks::ComponentInfo ci; + ci.contents = lines.join("\n"); + m_componentInfoList.append(ci); - // Create a link annotation as placeholder — post-processing - // will convert it to an invisible text annotation with the actual content - pdfEngine->drawHyperlink(devRect, QUrl("http://componentinfo.local/")); + // Create a link annotation as placeholder — post-processing + // will convert it to an invisible text annotation with the actual content + pdfEngine->drawHyperlink(devRect, QUrl(QString("http://componentinfo.local/%1").arg(annotIndex++))); } } ////Component info end//// diff --git a/sources/print/projectprintwindow.h b/sources/print/projectprintwindow.h index 72fa8e41d..74d9ae7e6 100644 --- a/sources/print/projectprintwindow.h +++ b/sources/print/projectprintwindow.h @@ -56,9 +56,9 @@ class ProjectPrintWindow : public QMainWindow void on_m_draw_titleblock_cb_clicked(); void on_m_keep_conductor_color_cb_clicked(); void on_m_draw_terminal_cb_clicked(); - void on_m_draw_terminal_names_cb_clicked(); - void on_m_component_info_cb_clicked(); - void on_m_fit_in_page_cb_clicked(); + void on_m_draw_terminal_names_cb_clicked(); + void on_m_component_info_cb_clicked(); + void on_m_fit_in_page_cb_clicked(); void on_m_use_full_page_cb_clicked(); void on_m_zoom_out_action_triggered(); void on_m_zoom_in_action_triggered(); From 1780fde458fe81c2530cc0ba200e437d1c656fb0 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Sat, 15 Aug 2026 11:57:19 +0200 Subject: [PATCH 3/5] Fix component-info annotation index collision across pages --- sources/print/projectprintwindow.cpp | 6 +++--- sources/print/projectprintwindow.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index 0215dd44e..2950e4981 100644 --- a/sources/print/projectprintwindow.cpp +++ b/sources/print/projectprintwindow.cpp @@ -256,6 +256,7 @@ void ProjectPrintWindow::requestPaint() bool first = true; QPainter painter(m_printer); + int annotIndex = 0; // A real PDF export uses the QPdfEngine; the on-screen preview uses a // preview paint engine. We only post-process when actually writing a PDF. @@ -269,7 +270,7 @@ void ProjectPrintWindow::requestPaint() for (auto diagram : selectedDiagram()) { first ? first = false : m_printer->newPage(); - printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap); + printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex); } // Note: do NOT call painter.end() or pdfConvertUriToGoTo() here. @@ -287,7 +288,7 @@ void ProjectPrintWindow::requestPaint() * @param fit_page * @param printer */ -void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap) +void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap, int &annotIndex) { ////Prepare the print//// @@ -432,7 +433,6 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter ////Collect component info for popup annotations//// if (ui->m_component_info_cb->isChecked()) { - int annotIndex = 0; for (auto *item : diagram->items()) { auto *el = qgraphicsitem_cast(item); if (!el) continue; diff --git a/sources/print/projectprintwindow.h b/sources/print/projectprintwindow.h index 74d9ae7e6..3d16fce91 100644 --- a/sources/print/projectprintwindow.h +++ b/sources/print/projectprintwindow.h @@ -83,7 +83,7 @@ class ProjectPrintWindow : public QMainWindow private: void requestPaint(); - void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap = {}); + void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap, int &annotIndex); QRect diagramRect(Diagram *diagram, const ExportProperties &option) const; int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; From b8df298953908da85c2d812313ddf2ba43d48c44 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 20 Aug 2026 11:21:22 +0200 Subject: [PATCH 4/5] fix --- sources/pdf_links.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 5cf176f76..810aa7284 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -492,13 +492,17 @@ void convertComponentInfoAnnotations(const QString &pdfPath, 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) { + // Replace /Subtype /Link with /Subtype /Text, bounded to the enclosing + // PDF object. Anchoring to " 0 obj" prevents hitting /Subtype /Link + // from a cross-ref annotation that sits between the previous marker + // and the current annotation on the same page. + int objStart = body.lastIndexOf(" 0 obj", aDictOpen); + int subTypeInBody = (objStart != -1) + ? body.indexOf("/Subtype /Link", objStart) : -1; + int subTypePos = (subTypeInBody != -1 && subTypeInBody < aDictOpen) + ? out.size() - (aDictOpen - subTypeInBody) : -1; + if (subTypePos != -1) out.replace(subTypePos, 14, "/Subtype /Text"); - } // 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. From 4572fca31fe16e3ca2a9cd7903e654fd51318a1f Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 20 Aug 2026 13:30:22 +0200 Subject: [PATCH 5/5] fix2 --- sources/pdf_links.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 810aa7284..c5260846a 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -502,7 +502,11 @@ void convertComponentInfoAnnotations(const QString &pdfPath, int subTypePos = (subTypeInBody != -1 && subTypeInBody < aDictOpen) ? out.size() - (aDictOpen - subTypeInBody) : -1; if (subTypePos != -1) - out.replace(subTypePos, 14, "/Subtype /Text"); + // Acrobat draws its own sticky-note icon for /Subtype /Text + // whatever /AP says (verified on Reader 5.0). /Square with no + // /C and no /IC draws nothing anywhere, and is still a markup + // annotation, so the /Contents popup is unaffected. + out.replace(subTypePos, 14, "/Subtype /Square"); // 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.