[Tex/LaTex] Automatic “Zoom in” hypertext boxes in PDF

hyperrefjavascriptpdfviewerszooming

I remember long time ago, seeing PDF documents (such as online PDF versions of newspapers) that had hypertext boxes around paragraphs, such that when you clicked inside those boxes the surrounded paragraph will be zoomed in to exactly that paragraph in the Acrobat Reader window. I am not sure what tool was used to create those PDFs, and unfortunately I can't find one example at the moment.

Is it possible to reproduce such zoom-on-click feature with some pdflatex code?
To repeat: the requested feature is to create hypertext boxes around paragraphs or figures that when clicked the PDF viewer would zoom in into that region.

This feature can be useful in PDFs with lots of structure in a given page, such as small paragraphs or figures or figure captions scattered around the page.

(Beamer has a "zoom in" feature (\framezoom) but it is not the same thing because that creates a new page with the zoomed region, and I just want to force the PDF reader to enclose the area in the same page by only zooming. I am also aware of marquee zoom in Acrobat but it is not what I am looking for because it is not aware of the content.)

Best Answer

The following code (for use with pdfLaTeX) defines the command \zoombox[box line width]{contents}. It creates a click-to-zoom box for fitting the second argument 'contents' into the AdobeReader window. A dotted line is drawn around the box if the optional 'box line width' is >0.

EDIT: The code has been optimized for presentation PDFs (such as beamer generated ones) looked at in full screen mode. The zoom-to box is now centred on the screen when clicked, as requested by @alfC.

\documentclass{beamer}

\usepackage{graphicx}
\usepackage{mwe}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  \zoombox[box line width]{contents}
%
%  optimized version for beamer: in full screen, zoom boxes are centred
%  in the viewer; useable with any documenclass
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\makeatletter
\newsavebox\zb@x
\newcounter{z@@m}
\usepackage{calc}
\newdimen\B@r\newdimen\P@r
\newdimen\@zw\newdimen\@zh\newdimen\@zd

\newcommand{\zoombox}[2][0]{%
  \leavevmode%
  \sbox\zb@x{#2}%
  \setlength\B@r{1pt*\ratio{\wd\zb@x}{\ht\zb@x+\dp\zb@x}}%
  \setlength\P@r{1pt*\ratio{\paperwidth}{\paperheight}}%
  \ifdim\B@r>\P@r\relax%
    \setlength\@zw{\wd\zb@x}\setlength\@zh{\@zw*\ratio{\paperheight}{\paperwidth}}%
    \setlength\@zd{(\@zh-\ht\zb@x-\dp\zb@x)*\real{0.5}+\dp\zb@x}%
    \setlength\@zh{\@zh-\@zd}%
  \else%
    \setlength\@zh{\ht\zb@x+\dp\zb@x}%
    \setlength\@zw{\@zh*\ratio{\paperwidth}{\paperheight}}%
    \setlength\@zh{\ht\zb@x}\setlength\@zd{\dp\zb@x}%
  \fi%
  \makebox[0pt][l]{\makebox[\wd\zb@x][c]{\makebox[\@zw][l]{%
    \pdfdest name {zbfs\thez@@m} fitr
      width  \@zw\space
      height \@zh\space
      depth  \@zd\space
  }}}%
  \pdfdest name {zb\thez@@m} fitr
    width  \wd\zb@x\space
    height \ht\zb@x\space
    depth  \dp\zb@x\space
  \immediate\pdfannot 
    width  \wd\zb@x\space
    height \ht\zb@x\space
    depth  \dp\zb@x\space
  {%
    /Subtype/Link/H/N
    /Border [0 0 #1 [1 2]]
    /A <<
      /S/JavaScript
      /JS (
        if(typeof(zoomed)=='undefined'||!zoomed){
          var lastView=this.viewState;
          if(app.fs.isFullScreen) this.gotoNamedDest('zbfs\thez@@m');
          else this.gotoNamedDest('zb\thez@@m');
          zoomed=true;
        }else{
          this.viewState=lastView;
          zoomed=false;
        }
      )
    >>
  }%
  \usebox{\zb@x}%
  \stepcounter{z@@m}%
} 
\makeatother
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\begin{document}
\begin{frame}
\zoombox{\includegraphics[width=1cm]{example-image-a}}
\zoombox{\includegraphics[width=1cm]{example-image-b}}
\zoombox{\includegraphics[width=1cm]{example-image-c}}
\zoombox{\includegraphics[height=1cm]{example-image-golden}}
\zoombox{\includegraphics[width=1cm]{example-image-golden-upright}}
\end{frame}
\end{document}

The second example defines pairs of commands for marking opposing corners (lower left & upper right .OR. upper left & lower right) of a zoom box, and commands for marking text sequences/entire paragraphs.

\documentclass{article}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\makeatletter
\InputIfFileExists{\jobname.zom}{}{}                                                                         
\newwrite\zoomdat
\immediate\openout\zoomdat=\jobname.zom                                                                      
\newcommand{\startzoombox}[2][0]{%                                                                           
  \leavevmode%                                                                                               
  \pdfsavepos%
  \protected@write\zoomdat{}{%
  \string\expandafter\string\def\string\csname\space zb#2.ulx\string\endcsname{%                             
    \noexpand\number\pdflastxpos}%
  \string\expandafter\string\def\string\csname\space zb#2.uly\string\endcsname{%                             
    \noexpand\number\pdflastypos}%                                                                           
  }%
  \ifcsname zb#2.ulx\endcsname\ifcsname zb#2.lrx\endcsname%
    \edef\zoomwd{\dimexpr \csname zb#2.lrx\endcsname sp- \csname zb#2.ulx\endcsname sp\relax}%               
    \edef\zoomdp{\dimexpr \csname zb#2.uly\endcsname sp- \csname zb#2.lry\endcsname sp\relax}%               
    \pdfdest name {zb#2.in} fitr                                                                             
      width \zoomwd                                                                                          
      height 0pt
      depth \zoomdp
    \immediate\pdfannot                                                                                      
      width \zoomwd                                                                                          
      height 0pt
      depth \zoomdp                                                                                          
    {%
      /Subtype/Link/H/N 
      /Border [0 0 1 [1 2]]                                                                                  
      /A <<
        /S/JavaScript                                                                                        
        /JS (
          if(typeof(zoomed)=='undefined'||!zoomed){                                                          
            var lastView=this.viewState;                                                                     
            zoomed=true;
            this.gotoNamedDest('zb#2.in');                                                                   
          }else{
            this.viewState=lastView;                                                                         
            zoomed=false;                                                                                    
          }                                                                                                  
        )                                                                                                    
      >>                                                                                                     
    }%
  \fi\fi%                                                                                                    
}
\def\stopzoombox#1{%\leavevmode%                                                                             
  \leavevmode%                                                                                               
  \pdfsavepos%
  \protected@write\zoomdat{}{%
  \string\expandafter\string\def\string\csname\space zb#1.lrx\string\endcsname{%                             
    \noexpand\number\pdflastxpos}%
  \string\expandafter\string\def\string\csname\space zb#1.lry\string\endcsname{%                             
    \noexpand\number\pdflastypos}%                                                                           
  }%                                                                                                         
}
\def\startzoom{%
  \stepcounter{@zb@id}%
  \xdef\@lblStack{\the@zb@id.\@lblStack}%
  \@ifstar\@startzoomstar\@startzoom%                                                                        
}

\newcommand{\@startzoom}[1][0]{%
  \raisebox{\baselineskip}[0pt][0pt]{\startzoombox[#1]{.\the@zb@id}}%                                                 
}
\newcommand{\@startzoomstar}[1][0]{%
  \makebox[0pt][r]{\raisebox{\baselineskip}[0pt][0pt]{\startzoombox[#1]{.\the@zb@id}}%                                
  \hspace{\parindent}}%                                                                                      
}
\def\stopzoom{%
  \@ifstar\@stopzoomstar\@stopzoom%                                                                          
}
\def\@stopzoom{%
  \@popStack\@lblStack%
  \raisebox{-1ex}[0pt][0pt]{\stopzoombox{.\@lblCur}}%
  \xspace%
}
\def\@stopzoomstar{%
  \@popStack\@lblStack%
  \hfill\raisebox{-1ex}[0pt][0pt]{\stopzoombox{.\@lblCur}}%
  \xspace%
}
\newcounter{@zb@id}
\def\@lblStack{}
\def\@popStack#1{\expandafter\@@popStack#1\nil}
\def\@@popStack#1.#2\nil{\gdef\@lblCur{#1}\gdef\@lblStack{#2}}
\RequirePackage{xspace}
\makeatother
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\begin{document}
\section{Paragraph zooming}
\startzoom[1]First paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first \startzoom[1]\emph{Do not miss this one.}\stopzoom First paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph first paragraph.\stopzoom*

\startzoom*[1]Second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph second paragraph.\stopzoom*
\end{document}

The two commands for marking the corners of a zoom area are

\startzoombox[line width]{label}

and

\stopzoombox{label}

They are meant to be used in picture-making environments, such as pspicture or tikzpicture. Associated command pairs must be identified by unique labels.

For marking text and paragraphs the following commands have been provided:

\startzoom[lwidth]
\startzoom*[lwidth]
\stopzoom
\stopzoom*

The starred versions insert horizontal space (negative \parindent on first line, \hfill at the end of the paragraph).

Zoom areas may be nested. However, smaller ones should be placed on top of bigger ones (i.e. to appear later in the code), in order not to be obscured.


Note that at least two pdflatex runs are necessary.


EDIT:

A dvipdfmx/XeLaTeX version of the \zoombox macro is given in: How do I include a click-to-zoom thumbnail picture in a non-beamer document?

Related Question