7

I have a problem with TikZ's auto-cropping/auto-calculating the bounding box for a tikzpicture.

Look at the following example:

\documentclass{article}
\usepackage{tikz}
\begin{document}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque massa quis nibh egestas, sed aliquam justo gravida. Integer eget felis vel erat auctor sagittis. In eget ligula eu velit rutrum sodales sed at velit. Proin id blandit ante, tristique bibendum magna.

\begin{center}
\begin{tikzpicture}
\node[draw,circle] (A) at (0,0){A};
\node[draw,circle] (B) at (3,3){B};
\draw (A) to (B);
\end{tikzpicture}
\end{center}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque massa quis nibh egestas, sed aliquam justo gravida. Integer eget felis vel erat auctor sagittis. In eget ligula eu velit rutrum sodales sed at velit. Proin id blandit ante, tristique bibendum magna.

\begin{center}
\begin{tikzpicture}
\node[draw,circle] (A) at (0,0){A};
\node[draw,circle] (B) at (3,3){B};
\draw[bend left=90,looseness=2] (A) to (B);
\draw[bend right=90,looseness=2] (A) to (B);
\end{tikzpicture}
\end{center}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque massa quis nibh egestas, sed aliquam justo gravida. Integer eget felis vel erat auctor sagittis. In eget ligula eu velit rutrum sodales sed at velit. Proin id blandit ante, tristique bibendum magna.
\end{document}

This produces the following output: enter image description here

As you can see the cropping of the picture with the straight lines is perfectly fine. However, for the curved lines there is too much (unnecessary) white-space before and after the picture.

I know I can manually fix this by changing the boundingbox or simply using \vspace, but is there an automatic way to get accurate bounding boxes?

(Note: This is very similar to this question, but the answers there do not seem to help with the automatic calculation, mostly checking first what the bounding box is and then applying some sort of clipping.)

  • I think there's no automatic way because as you already read, all control points are included into the automatically computed bounding box. May be with externalizing and cropping the results you could get better adjusted results, although I don't know if it's possible to join both tasks in an automatic way. – Ignasi Feb 1 '16 at 10:34
  • Already feared it to be so. Well, unless a magic answer comes about, it's back to manual cropping. – senegrom Feb 1 '16 at 14:44
  • 1
    I cannot test it because pdfcrop doesn't work for me now but according to pgfplots manual ` (v1.13, end of p. 511) it's possible to define external/system call like two different commands with && in between. Thus I understand you could use externalization with pdflatex ... && pdfcrop ... and if there's no problem with names, you could get automatically cropped figures. – Ignasi Feb 1 '16 at 19:11
  • Thank you, that sounds like an interesting idea. Of course, it will destroy any portability of the document :-( Is there any way of extracting the actual points of the path and not the control points (in some discretized way) ? – senegrom Feb 2 '16 at 11:17
  • How will it destroy portability? – Ignasi Feb 2 '16 at 12:02
13

Here is my attempt to get an automatic method. Read this page to know how to split Bézier curves.

I define the new limit bb style with two arguments:

  1. the maximum distance between actual bounding box and perfect bounding box.
  2. the action (draw, fill...) applied to the path.

This new style splits automatically and recursively all Bézier curves to remove too distant control points.

enter image description here

\documentclass[tikz]{standalone}
\usetikzlibrary{calc,decorations.pathreplacing}
\tikzset{
  bezier/controls/.code args={(#1) and (#2)}{
    \def\mystartcontrol{#1}
    \def\mytargetcontrol{#2}
  },
  bezier/limit/.store in=\mylimit,
  bezier/limit=1cm,
  bezier/.code={
    \tikzset{bezier/.cd,#1}
    \tikzset{
      to path={
        let
        \p0=(\tikztostart),    \p1=(\mystartcontrol),
        \p2=(\mytargetcontrol), \p3=(\tikztotarget),
        \n0={veclen(\x1-\x0,\y1-\y0)},
        \n1={veclen(\x3-\x2,\y3-\y2)},
        \n2={\mylimit}
        in  \pgfextra{
          \pgfmathtruncatemacro\ok{max((\n0>\n2),(\n1>\n2))}
        }
        \ifnum\ok=1 %
        let
        \p{01}=($(\p0)!.5!(\p1)$), \p{12}=($(\p1)!.5!(\p2)$), \p{23}=($(\p2)!.5!(\p3)$),
        \p{0112}=($(\p{01})!.5!(\p{12})$), \p{1223}=($(\p{12})!.5!(\p{23})$),
        \p{01121223}=($(\p{0112})!.5!(\p{1223})$)
        in
        to[bezier={controls={(\p{01}) and (\p{0112})}}]
        (\p{01121223})
        to[bezier={controls={(\p{1223}) and (\p{23})}}]
        (\p3)
        \else
        [overlay=false] .. controls (\p1) and (\p2) ..  (\p3) [overlay=true]
        \fi
      },
    },
  },
  limit bb/.style n args={2}{
    overlay,
    decorate,
    decoration={
      show path construction,
      moveto code={},
      lineto code={\path[#2] (\tikzinputsegmentfirst) -- (\tikzinputsegmentlast);},
      curveto code={
        \path[#2]
        (\tikzinputsegmentfirst)
        to[bezier={limit=#1,controls={(\tikzinputsegmentsupporta) and (\tikzinputsegmentsupportb)}}]
        (\tikzinputsegmentlast);
      },
      closepath code={\path[#2] (\tikzinputsegmentfirst) -- (\tikzinputsegmentlast);},
    },
  },
  limit bb/.default={1mm}{draw},
}

\begin{document}
\begin{tikzpicture}
  \node[draw,circle] (A) at (0,0){A};
  \node[draw,circle] (B) at (3,3){B};
  \draw[limit bb={1mm}{draw=red},bend left=90,looseness=2] (A) to (B);
  \draw[limit bb={1mm}{draw=blue},bend right=90,looseness=2] (A) to (B);
  \draw[green] (current bounding box.south west) rectangle (current bounding box.north east);
\end{tikzpicture}
\end{document}
  • Wow. Thanks a lot for the effort. That looks really good! – senegrom Feb 12 '16 at 14:04
3

By default, part of the bounding box comes from

\def\pgf@lt@moveto#1#2{%
  \pgf@protocolsizes{#1}{#2}%
  \pgfsyssoftpath@moveto{\the#1}{\the#2}%
}
\def\pgf@lt@lineto#1#2{%
  \pgf@protocolsizes{#1}{#2}%
  \pgfsyssoftpath@lineto{\the#1}{\the#2}%
}
\def\pgf@lt@curveto#1#2#3#4#5#6{%
  \pgf@protocolsizes{#1}{#2}%
  \pgf@protocolsizes{#3}{#4}%
  \pgf@protocolsizes{#5}{#6}%
  \pgfsyssoftpath@curveto{\the#1}{\the#2}{\the#3}{\the#4}{\the#5}{\the#6}%
}

That is the reason we saw that all control points are involved: control points are directly passed to bounding-box-calculation (\pgf@protocolsizes). To solve this, one can only do the math inside \pgf@lt@curveto. This topic is definitely a duplicate if you count programming languages other than TeX. For instance An algorithm to find bounding box of closed bezier curves? in stack overflow.

But in TeX it is hard to do math. But still it is possible to sacrifice some efficiency to get a fairly acceptable result. For example: since 3(1-t)t^2,3(1-t)^2t≤4/9, we know

xmax ≤ max(xA,xD)+4/9|xB-max(xA,xD)|+4/9|xC-max(xA,xD)|

so the right hand side could improve the calculation.

1

Here comes a very late brute force solution. WARNING: This is a very slow solution. BUT: It works for all paths. The idea is just to move along the path and record the coordinates. (This turns out somewhat trickier than I originally thought because TikZ is using a co-moving frame along the path. This is good in all situations except the one here. In this example, this "problem" is "solved" used by issuing a \pgftransformreset.) This answer comes with a style get path extrema, which determines the extremal points, and the Bezier curves are an example where the otherwise great answer fails. Here is an application to the scenario described in the question.

\documentclass[tikz,border=3.14pt]{standalone}
\usetikzlibrary{calc,decorations,decorations.markings}
\tikzset{get path extrema/.style={decorate,decoration={markings, 
    mark=between positions 0 and 1 step 0.3pt with
      {\xdef\mypos{\pgfkeysvalueof{/pgf/decoration/mark info/distance from
      start}}
      \begin{pgfinterruptpath}
      \ifdim\mypos=0pt
      \coordinate (start) at (0,0);
      \fi
      \coordinate (here) at (0,0);
      \pgftransformreset
      \path let \p1 = ($(here) - (start)$) in \pgfextra{\xdef\myx{\x1}
      \xdef\myy{\y1}}; 
      \ifdim\mypos=0pt
      \xdef\myxmin{\myx}
      \xdef\myymin{\myy}
      \xdef\myxmax{\myx}
      \xdef\myymax{\myx}
      \xdef\mypostop{(\myx,\myy)}
      \xdef\myposbottom{(\myx,\myy)}
      \xdef\myposleft{(\myx,\myy)}
      \xdef\myposright{(\myx,\myy)}
      \fi
      \ifdim\myx<\myxmin
      \xdef\myposleft{(\myx,\myy)}
      \xdef\myxmin{\myx}
      \fi
      \ifdim\myx>\myxmax
      \xdef\myposright{(\myx,\myy)}
      \xdef\myxmax{\myx}
      \fi
      \ifdim\myy<\myymin      
      \xdef\myposbottom{(\myx,\myy)}
      \xdef\myymin{\myy}
      \fi
      \ifdim\myy>\myymax
      \xdef\mypostop{(\myx,\myy)}
      \xdef\myymax{\myy}
      \fi
      \end{pgfinterruptpath}
      }},
      path picture={
      \path[shift=(start)] \myposleft coordinate (#1-left) --
    \mypostop coordinate (#1-top) --
    \myposright coordinate (#1-right) --
    \myposbottom coordinate (#1-bottom) -- cycle;
    }}}
\begin{document}
\begin{tikzpicture}
\node[draw,circle] (A) at (0,0){A};
\node[draw,circle] (B) at (3,3){B};
\begin{pgfinterruptboundingbox}
\draw[bend left=90,looseness=2,postaction={get path extrema=test1}] (A) to (B);
\draw[bend right=90,looseness=2,postaction={get path extrema=test2}] (A) to (B);
\end{pgfinterruptboundingbox}
\path[red] (test1-left)  -- (test1-top)
-- (test1-right)-- (test1-bottom) -- cycle;
\draw[red] (test2-left)  -- (test2-top)
-- (test2-right)-- (test2-bottom) -- cycle;
\end{tikzpicture}
\end{document}

enter image description here

The red contour is just an illustration. I know that this is a very late answer, and I also think that there must be a way to make the thing faster. Essentially one would need to tell TikZ to issue some pgfinterruptboundingbox whenever it draws the auxiliary paths that pop up in the path constructions. But doing this is far beyond my capabilities.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.