pgf icon indicating copy to clipboard operation
pgf copied to clipboard

Rotation of pgftext does not affect hyperref

Open lrlunin opened this issue 3 months ago • 7 comments

Brief outline of the bug

Hello,

firstly, I would like to thank you all for the efforts you have made in maintaining this project.

I have encounted the problem when I used matplotlib pgf backend. The problem is that the hyperref links are not rotated with the \pgftext node.

After a quick research I found out that the problem is pretty common and is known for a long time:

All of the proposed solutions are based on use of \rotatebox which is rather poor if the exact placement of the node matters. In particular cases of rotation by 90, 180, 270 degrees the \rotatebox is fine with arguments of the text alignment of \pgftext but does not really work as inteded for arbitary degree.

I am aware that this issue is particularly related to the \hyperref package but this is nowdays not really maintained.

Since the problem is present for pretty long time and I believe that this is important, I would like to address this issue to your project. I would like to ask you if you can see any long-term robust solution for the problem or give any advices regarding the problem.

Best wishes

Minimal working example (MWE)

\documentclass{article}
\usepackage{hyperref}
\usepackage{pgfplots}
\begin{document}
\section{Introduction}
\label{text:intro}
\begin{figure}
    \centering
    \begin{pgfpicture}%
        \begin{pgfscope}%
        \pgftext[x=0.9in,y=0.5in]{Reference to \ref{text:intro}}
        \pgftext[x=0.2in,y=0.2in,rotate=90]{Reference to \ref{text:intro}}
        \end{pgfscope}
    \end{pgfpicture}
\end{figure}
\end{document}
Image

lrlunin avatar Oct 15 '25 21:10 lrlunin

Reproduces with pdfTeX and LuaTeX, but not XeTeX.

When compiled with pdfTeX,

  • \pgftext from pgf package rotates the box using primitives \pdfliteral;
  • \rotatebox from graphic(s|x) package rotates the box using primitives \pdfsave, \pdfsetmatrix, and \pdfrestore (see the pdftex.def driver).

The later 3 primitives are introduced in pdfTeX 1.40.0 (2007) specifically

[...] to save pdfTeX from parsing \pdfliteral contents and to notify pdfTeX about matrix changes to use them in calculating link and anchor positions.

(CTAN announcement for pdfTeX 1.40.0)

\documentclass{article}
\usepackage{hyperref}

\parindent=0pt

\begin{document}
\section{Introduction}\label{text:intro}

\vskip1cm

Emulate \verb|\rotatebox|
\hbox{%
  \setbox0=\hbox{Ref to \ref{text:intro}}%
  \pdfsave
    \pdfsetmatrix{0 1 -1 0}%
    \rlap{\copy0}%
  % Paired \pdfsave and \pdfrestore must be used at the same place,
  % otherwise you got pdftex warning "Misplaced \pdfrestore by (...)".
  % See https://tug.org/pipermail/pdftex/2013-April/008858.html
  \pdfrestore
}

\vskip1cm

Emulate \verb|\pgftext|
\hbox{%
  \pdfliteral{q }%
  \pdfliteral{0 1 -1 0 0 0 cm }%
  Ref to \ref{text:intro}%
  \pdfliteral{Q }%
}
\end{document}
Image

pgf/tikz needs to use \pdfsave and its friends for \pgftext and tikz nodes, in the pdfTeX and LuaTeX drivers.

muzimuzhi avatar Oct 15 '25 22:10 muzimuzhi

pgf/tikz needs to use \pdfsave and its friends for \pgftext and tikz nodes, in the pdfTeX and LuaTeX drivers.

Err, that's not enough if the whole pgfpicture or pgfscope is already rotated.

muzimuzhi avatar Oct 15 '25 23:10 muzimuzhi

I don't think we can easily repair this without some major refactoring of the internals and I have burned my fingers a couple of times already touching pgfsys code. (e.g. https://github.com/pgf-tikz/pgf/commit/8e182a4ac2c4cef5e5e0743911e0ffcfcc51b87d, https://github.com/pgf-tikz/pgf/commit/943a0a0c82ca6e9c3a3f730863178d7744e601d6, https://github.com/pgf-tikz/pgf/commit/5492ecdcfc41f27d9fc00a6eaba1eb0791f73ebb)

hmenke avatar Oct 16 '25 07:10 hmenke

Reproduces with pdfTeX and LuaTeX, but not XeTeX.

Wow, it does indeed work with XeLaTeX as intended. I have never found this solution to the problem before. Thank you so much for your reserach and efforts! I guess this is a pretty decent workaround for end users.

Maybe this can be closed with 'won't fix' for pdfLatex, and anybody else who want to get this right can just switch to the XeLaTeX.

lrlunin avatar Oct 16 '25 08:10 lrlunin

See also comments in l3backend about which primitives are needed to 'track' annotations.

josephwright avatar Oct 16 '25 08:10 josephwright

@josephwright Did you mean the comments for \__kernel_backend_scope_begin:, \__kernel_backend_scope_end:, and \__kernel_backend_matrix:(n|e)?

muzimuzhi avatar Oct 16 '25 14:10 muzimuzhi

Try this. It patches \pgfsys@hboxsynced when compiled "with pdfTeX and LuaTeX in direct PDF output mode" (texdoc l3backend-code, sec. 1.2 "LuaTeX and pdfTeX backends").

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{shapes.multipart} % for the \pgfmultipartnode example
\usepackage{hyperref}

\makeatletter
\ifdefined\pdftexversion
  % pdftex
  \def\pgfsys@save{\pdfsave}
  \def\pgfsys@setmatrix{\pdfsetmatrix}
  \def\pgfsys@restore{\pdfrestore}
\else\ifdefined\directlua
  % luatex
  \def\pgfsys@save{\pdfextension save\relax}
  \def\pgfsys@setmatrix{\pdfextension setmatrix}
  \def\pgfsys@restore{\pdfextension restore\relax}
\fi\fi

\def\pgfsys@hboxsynced@NEW#1{%
  {%
    \pgfsys@begin@idscope%
    \pgfsys@beginscope%
    \setbox\pgf@hbox=\hbox{%
      \hskip\pgf@pt@x
      \raise\pgf@pt@y\hbox{%
        \pgf@pt@x=0pt%
        \pgf@pt@y=0pt%
        %% >>> patch begin
        \ifpgf@pt@identity
          % original case
          \pgflowlevelsynccm
          \pgfsys@hbox#1%
        \else
          \pgfgettransformentries\aa\ab\ba\bb\shiftx\shifty
          \pgftransformresetnontranslations
          \pgflowlevelsynccm
          \pgfsys@save
          \pgfsys@setmatrix{\aa\space\ab\space\ba\space\bb}%
          \pgfsys@hbox#1%
          \pgfsys@restore
        \fi
        %% <<< patch end
      }%
      \hss%
    }%
    \wd\pgf@hbox=0pt%
    \ht\pgf@hbox=0pt%
    \dp\pgf@hbox=0pt%
    \box\pgf@hbox
    \pgfsys@endscope%
    \pgfsys@end@idscope%
  }%
}

%\let\pgfsys@hboxsynced@OLD=\pgfsys@hboxsynced
\ExplSyntaxOn
\sys_ensure_backend: % needed by using \sys_if_output_pdf_p: in preamble

\bool_lazy_and:nnT
  {
    \bool_lazy_or_p:nn
      { \sys_if_engine_pdftex_p: }
      { \sys_if_engine_luatex_p: }
  }
  { \sys_if_output_pdf_p: }
  {
    \let\pgfsys@hboxsynced=\pgfsys@hboxsynced@NEW
  }
\ExplSyntaxOff
\makeatother

\parindent=0pt

\begin{document}
\section{Introduction}\label{text:intro}

\def\drawGrid{%
  \begin{pgfscope}
    \pgfsetcolor{blue}
    \pgfpathgrid{\pgfpointorigin}{\pgfpoint{1cm}{1cm}}
    \pgfusepath{draw}
  \end{pgfscope}
}

\def\drawBoundingBox{%
  \begin{pgfscope}
    \pgftransformreset
    \pgfsetcolor{gray}
    \pgfsetdash{{3pt}{3pt}}{0pt}
    \pgfpathrectanglecorners
      {\pgfpointanchor{current bounding box}{north east}}
      {\pgfpointanchor{current bounding box}{south west}}
    \pgfusepath{draw}
  \end{pgfscope}
}

\begin{pgfpicture}
  \pgfsetbaseline{0cm}
  \drawGrid
  \pgftext[x=0cm, y=0cm, rotate=90] {Ref to \ref{text:intro}}
  \pgftext[x=1cm, y=0cm, rotate=120] {Ref to \ref{text:intro}}
  \drawBoundingBox
\end{pgfpicture}
\qquad
\rotatebox[origin=c]{90}{Ref to \ref{text:intro}}
\qquad
\rotatebox[origin=c]{120}{Ref to \ref{text:intro}}

\begin{pgfpicture}
  % global transformations
  \pgftransformrotate{30}
  \pgftransformscale{1.1}
  \pgftransformshift{\pgfpoint{10pt}{20pt}}

  \pgfsetbaseline{2cm}
  \drawGrid
  \pgftext[x=1cm, y=1cm, rotate=-90] {Ref to \ref{text:intro}}
  \drawBoundingBox
\end{pgfpicture}
\qquad
\rotatebox[origin=c]{-60}{Ref to \ref{text:intro}}

% \pgfnode, \pgfmultipartnode, and tikz nodes
\setbox\pgfnodeparttextbox=\hbox{Ref to}
\setbox\pgfnodepartlowerbox=\hbox{\ref{text:intro}}
\begin{pgfpicture}
  % global transformations
  \pgftransformrotate{60}
  \pgftransformscale{0.8}
  \pgftransformyslant{-.3}
  \drawGrid
  \pgfnode{rectangle}{north}{Ref to \ref{text:intro}}{}{\pgfusepath{stroke}}
  \pgftransformshift{\pgfpoint{1cm}{1cm}}
  \pgfmultipartnode{circle split}{east}{}{\pgfusepath{stroke}}
  \drawBoundingBox
\end{pgfpicture}
\quad
\begin{tikzpicture}[rotate=60, scale=.8, yslant=-.3, nodes={draw, transform shape}]
  \draw[blue] (0,0) grid (1,1);
  \node[anchor=north] {Ref to \ref{text:intro}};
  \node[circle split, anchor=east] at (1,1)
    {Ref to\nodepart{lower}\ref{text:intro}};
  \draw[reset cm, gray, dashed]
    (current bounding box.north east) rectangle
    (current bounding box.south west);
\end{tikzpicture}
\qquad
\scalebox{0.8}{\rotatebox{60}{Ref to \ref{text:intro}}}

\end{document}
Image

muzimuzhi avatar Oct 18 '25 18:10 muzimuzhi