This adds native, clickable hyperlinks to PDF exports: cross-references jump

directly to the related component on its folio, framing the target element.

When a project is exported to PDF, every cross-reference becomes an internal
link. Four kinds are covered:

- **Master → contact**: the contact list on a coil/relay (`CrossRefItem`)
- **Folio report → report**: report element labels (`DynamicElementTextItem`)
- **Slave → master**: the `(folio-position)` reference shown on a slave
  (both standalone `DynamicElementTextItem` and grouped `ElementTextItemGroup`)

Clicking a link navigates **inside** the open document (no new viewer
instance) and zooms to frame the target element.

1. **Injection** (`printDiagram`, only when the paint engine is a `QPdfEngine`):
   link rectangles are added with `QPdfEngine::drawHyperlink()`. The scene→page
   mapping is rebuilt to match exactly what `QGraphicsScene::render()` does
   (top-left anchored, `KeepAspectRatio`, **no centering**), and rectangles are
   passed in device pixels — `pageMatrix()` already applies the 72/resolution
   scale and Y-flip internally.

2. Each link URL encodes the target page and the target element's rectangle, in
   PDF points on its own page: `#page=N&fitr=L_B_R_T`.

3. **Post-processing** (`pdfConvertUriToGoTo`, run after the painter is closed):
   the `/S /URI` annotations are rewritten to native `/S /GoTo` actions with a
   `/D [pageObj 0 R /FitR L B R T]` destination, and the xref table is rebuilt.
   Pages are enumerated from the `/Pages /Kids` tree (reliable), not by scanning
   for `/Type /Page` in raw bytes.

- `sources/print/projectprintwindow.{cpp,h}` — injection + post-processing
- `sources/qetgraphicsitem/crossrefitem.{cpp,h}` — `hoveredContactsMap()` accessor; store text rect for hit area
- `sources/qetgraphicsitem/dynamicelementtextitem.h` — `slaveXrefItem()` / `masterElement()` accessors
- `sources/qetgraphicsitem/elementtextitemgroup.h` — `slaveXrefItem()` accessor
- `qelectrotech.pro`, `cmake/qet_compilation_vars.cmake` — enable Qt gui-private headers (`<private/qpdf_p.h>`)

- **Fit-to-page mode only.** Links are not injected in tiled mode (multiple
  pages per folio), which would require a per-tile transform.
- Uses Qt private API (`QPdfEngine::drawHyperlink`), stable since Qt 4 but not
  part of the public API; the build links against `gui-private`.
- Page-tree enumeration assumes the flat `/Kids` array Qt produces (no nested
  page trees).
- The frame zoom is controlled by two constants in `destRectPdf` (`pad`,
  `minSide`) and can be tuned.
- Tested on Qt5; the `/Kids` parsing and `pageMatrix` behaviour are identical on
  Qt6.
This commit is contained in:
Laurent Trinques
2026-05-30 18:48:28 +02:00
parent b522a94556
commit cd76b6a1d6
8 changed files with 470 additions and 7 deletions
+3 -3
View File
@@ -928,7 +928,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect);
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
m_hovered_contacts_map.insert(elmt, text_rect);
++m_drawed_contacts;
}
@@ -1012,7 +1012,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect);
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
m_hovered_contacts_map.insert(elmt, text_rect);
//a switch contact take place of two normal contact
m_drawed_contacts += 2;
@@ -1044,7 +1044,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
bounding_rect = bounding_rect.united(text_rect);
if (m_update_map)
m_hovered_contacts_map.insert(elmt, bounding_rect);
m_hovered_contacts_map.insert(elmt, text_rect);
++m_drawed_contacts;
}
return bounding_rect;
+6
View File
@@ -126,6 +126,12 @@ class CrossRefItem : public QGraphicsObject
ElementTextItemGroup *m_group = nullptr;
QList <QMetaObject::Connection> m_slave_connection;
QList <QMetaObject::Connection> m_update_connection;
public:
/// Returns the map of linked elements and their clickable rects (local coords).
/// Used by the PDF export to inject hyperlink annotations.
const QMultiMap<Element *, QRectF> &hoveredContactsMap() const
{ return m_hovered_contacts_map; }
};
#endif // CROSSREFITEM_H
@@ -86,6 +86,9 @@ class DynamicElementTextItem : public DiagramTextItem
void fromXml(const QDomElement &dom_elmt) override;
Element *parentElement() const;
/// PDF export: slave cross-reference text item ("(folio-pos)") and its master target.
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
Element *masterElement() const { return m_master_element.data(); }
ElementTextItemGroup *parentGroup() const;
Element *elementUseForInfo() const;
void refreshLabelConnection();
@@ -76,6 +76,8 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
QList<DynamicElementTextItem *> texts() const;
Diagram *diagram() const;
Element *parentElement() const;
/// PDF export: slave cross-reference text item of the group, if any.
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
QDomElement toXml(QDomDocument &dom_document) const;
void fromXml(QDomElement &dom_element);