From 67b1665b3604b0c4bbb9c805f2fe681f142ae4d3 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 13 Aug 2026 11:54:33 +0200 Subject: [PATCH] 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 + + + +