diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 4e5a7d354..0e7d131e8 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -238,6 +238,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 +388,269 @@ 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 markerPrefix("http://componentinfo.local/"); + if (!data.contains(markerPrefix)) 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; + bool anyConverted = false; + + while (pos < body.size()) { + // 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, idxEnd - pos)); + pos = idxEnd; + 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>> + int aDictOpen = (sUriPos != -1) + ? body.lastIndexOf("/A <<", sUriPos) + : -1; + + // 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; + } + + // 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; + } + + // 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 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) + // 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. + QByteArray utf16be; + utf16be.append('\xfe'); + utf16be.append('\xff'); + { + 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)); + } + } + + 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; + } + anyConverted = true; + } + + if (!anyConverted) 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"; + } + } + + // Copy trailer and bump /Size to account for the new XObject + 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"; + + // 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; + 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..27e266420 100644 --- a/sources/pdf_links.h +++ b/sources/pdf_links.h @@ -74,6 +74,18 @@ namespace PdfLinks { */ void convertUriToGoTo(const QString &pdfPath); + struct ComponentInfo { + QString contents; + }; + + /** + 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); + } #endif // PDF_LINKS_H diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index 537c7ad77..797117e00 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" @@ -153,6 +155,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); @@ -248,6 +251,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. @@ -261,7 +265,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. @@ -279,7 +283,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//// @@ -415,6 +419,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 = qgraphicsitem_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(QString("http://componentinfo.local/%1").arg(annotIndex++))); + } + } + ////Component info end//// } } ////PDF links end//// @@ -715,6 +763,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() { @@ -819,9 +868,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..3d16fce91 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 @@ -56,6 +57,7 @@ class ProjectPrintWindow : public QMainWindow 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_use_full_page_cb_clicked(); void on_m_zoom_out_action_triggered(); @@ -81,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; @@ -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 + + + +