Fit text into given box by adjusting the fontsize

Using the suggestion in Fitting and centering text (both!) in a constrained area, together with Martin's answer that uses the environ package, the following provides the environment

\begin{fitbox}{<width>}{<height>}
  <stuff>
\end{fitbox}

which typesets <stuff> using a form of binary search to fit the text within the given height <height> while under a fixed width <width> constraint set by a minipage. This is required in order to maintain a proportionate scaling of the font and leading (\baselineskip).

\documentclass{article}
\usepackage{lmodern}
\usepackage{environ}% http://ctan.org/pkg/environ
\usepackage{lipsum}% http://ctan.org/pkg/lipsum
\newdimen\fontdim
\newdimen\upperfontdim
\newdimen\lowerfontdim
\newif\ifmoreiterations
\fontdim12pt

\makeatletter
\NewEnviron{fitbox}[2]{% \begin{fitbox}{<width>}{<height>} stuff \end{fitbox}
  \def\buildbox{%
    \setbox0\vbox{\hbox{\minipage{#1}%
      \fontsize{\fontdim}{1.2\fontdim}%
      \selectfont%
      \stuff%
    \endminipage}}%
    \dimen@\ht0
    \advance\dimen@\dp0
  }
  \def\stuff{\BODY}% Store environment body
  \buildbox
  % Compute upper and lower bounds
  \ifdim\dimen@>#2
    \loop
      \fontdim.5\fontdim % Reduce font size by half
      \buildbox
    \ifdim\dimen@>#2 \repeat
    \lowerfontdim\fontdim
    \upperfontdim2\fontdim
    \fontdim1.5\fontdim
  \else
    \loop
      \fontdim2\fontdim % Double font size
      \buildbox
    \ifdim\dimen@<#2 \repeat
    \upperfontdim\fontdim
    \lowerfontdim.5\fontdim
    \fontdim.75\fontdim
  \fi
  % Now try to find the optimum size
  \loop
    %\message{Bounds: \the\lowerfontdim\space
    %         \the\fontdim\space \the\upperfontdim^^J}
    \buildbox
    \ifdim\dimen@>#2
      \moreiterationstrue
      \upperfontdim\fontdim
      \advance\fontdim\lowerfontdim
      \fontdim.5\fontdim
    \else
      \advance\dimen@-#2
      \ifdim\dimen@<10pt
        \lowerfontdim\fontdim
        \advance\fontdim\upperfontdim
        \fontdim.5\fontdim
        \dimen@\upperfontdim
        \advance\dimen@-\lowerfontdim
        \ifdim\dimen@<.2pt
          \moreiterationsfalse
        \else
          \moreiterationstrue
        \fi
      \else
        \moreiterationsfalse
      \fi
    \fi
  \ifmoreiterations \repeat
  \box0% Typeset content
}
\makeatother
\begin{document}
\lipsum[1]
\begin{fitbox}{.5\textwidth}{0.5\textheight}
  \lipsum[1-2]
\end{fitbox}
\lipsum[2]

\clearpage

\lipsum[1]
\begin{fitbox}{300pt}{300pt}
  \lipsum[1-2]
\end{fitbox}
\lipsum[2]
\end{document}​

In the figure below, two pages are typeset, each starting with \lipsum[1] and ending with \lipsum[2] to provide some frame of reference. The left page has a fitbox of dimension .5\textwidth x .5\textwidth while the page on the right is set at 300pt x 300pt (square).

Fixed-width-height box

Interestingly enough, I'm having trouble compiling this under TeXLive 2011. Although, there is no problem compiling it using the online LaTeX compiler ScribTeX, which runs TeXLive 2009. I don't know what the cause behind this is... This has been fixed by the replacement of \protected@edef\stuff{\BODY} with \def\stuff{\BODY}. The original code used this form since it provided two macros - one for parsing the content (called \fillthepage{<stuff>}) and another for updating a resized version of the content (called \buildbox). I assume the coding structure required this. However, with everything contained in a single environment fitbox above, this is not needed anymore.


I found a way to solve the problem - almost. It builds on the approach outlined by @Werner. Essentially, it adds a test for the badness of the box as a measure for text running over the edge. The test is a recursion that unfortunately only works for the last paragraph of the text in the environment. So there is still some refinement required.

\documentclass{article}
\usepackage{xltxtra}
\usepackage{fontspec}
\setmainfont[Mapping=tex-text]{Times New Roman}

\sloppypar

\usepackage{environ}% http://ctan.org/pkg/environ
\newdimen\fontdim
\newdimen\upperfontdim
\newdimen\lowerfontdim
\newif\ifmoreiterations
\fontdim12pt

\newbox\trialbox
\newbox\linebox
\global\newcount\maxbad
\newcount\linebad
\newcount\currenthbadness


\makeatletter
\NewEnviron{fitbox}[2]{% \begin{fitbox}{<width>}{<height>} stuff \end{fitbox}
    % Store environment body
    \def\stuff{%
        \BODY%
    }%
    % prepare badness box
    \def\badnessbox{%
        \global\maxbad=0\relax%
        \currenthbadness=\hbadness% save old \hbadness
        \hbadness=10000000\relax% make sure, TeX reports overfull boxes
        \message{Starting measureline recursion with width #1^^J}%
        \setbox\trialbox=\vbox{%
            \hsize#1\relax%
            \fontsize{\fontdim}{1.2\fontdim}%
            \selectfont%
            \stuff\par%
            \measurelines% start recursion
        }%
%       \noindent\usebox\trialbox\par
        \hbadness=\currenthbadness% restore old \hbadness
    }
    % prepare recursion to measure line badness
    \def\measurelines{%
        \message{Iteration of measurelines^^J}%
        \begingroup%
            \setbox\linebox=\lastbox% get the last line
            \setbox0=\hbox to \hsize{\unhcopy\linebox}% put the last line into box0 to provoke badness calculation
            \linebad=\the\badness\relax% \badness now reflects the last typeset box, i.e. box0
            \message{Badness: \the\badness\space\the\linebad\space with max \the\maxbad\space at Fontsize: \the\fontdim\space^^J}%
            \ifnum\linebad>\maxbad% store the maximum badness
                \global\maxbad=\linebad% Uncomment this line to ignore overfull hboxes!
            \fi%
            \ifvoid% end of recursion
                \linebox%
            \else%
                \unskip\unpenalty\measurelines% do the recursion
                \ifhmode%
                    \newline%
                \fi%
                \noindent\box\linebox% do the output
            \fi%
        \endgroup%
    }%
    % Prepare measurement box
    \def\buildbox{%
        \badnessbox% measure badness
        \setbox0\vbox{% measure height
            \hbox{%
                \fontsize{\fontdim}{1.2\fontdim}%
                \selectfont%
                \minipage{#1}%
                    \vbox{%                     
                        \stuff\par%
                    }%
                \endminipage%
            }%
        }%
        \message{Measured badness: \the\maxbad\space at Fontsize: \the\fontdim\space^^J}%
        \dimen@\ht0
        \advance\dimen@\dp0
        \message{Measured box height: \the\dimen@\space^^J}%
    }%
    \def\shrinkheight{%
        \loop
            \fontdim.5\fontdim % Reduce font size by half
            \buildbox
            \message{Shrinking, new box height: \the\dimen@\space at Fontsize: \the\fontdim\space^^J}%
        \ifdim\dimen@>#2 \repeat
        \lowerfontdim\fontdim
        \upperfontdim2\fontdim
        \fontdim1.5\fontdim
    }%
    \def\shrinkwidth{%
        \loop
            \fontdim.5\fontdim % Reduce font size by half
            \buildbox
        \ifnum\maxbad>10000 \repeat
        \lowerfontdim\fontdim
        \upperfontdim2\fontdim
        \fontdim1.5\fontdim
    }%
    \def\growheight{%
        \loop
            \fontdim2\fontdim % Double font size
            \buildbox
            \message{Growing, new box height: \the\dimen@\space at Fontsize: \the\fontdim\space^^J}%
        \ifdim\dimen@<#2 \repeat
        \upperfontdim\fontdim
        \lowerfontdim.5\fontdim
        \fontdim.75\fontdim
    }%
    \buildbox
    % Compute upper and lower bounds
    \ifdim\dimen@>#2
        \message{Need to shrink box height: \the\dimen@\space^^J}%
        \shrinkheight
    \else
        \message{Need to grow box height: \the\dimen@\space to target: #2^^J}%
        \growheight
    \fi
    \message{Max font: \the\upperfontdim\space^^J}%
    \message{Min font: \the\lowerfontdim\space^^J}%
    % Potentially further reduce bounds for overfull box
    \ifnum\maxbad>10000
        \shrinkwidth
    \fi 
    \message{Max font adjusted: \the\upperfontdim\space^^J}%
    \message{Min font adjusted: \the\lowerfontdim\space^^J}%
    % Now try to find the optimum height and width
    \loop
        \buildbox
        \message{Height: \the\dimen@\space^^J}%
        \ifdim\dimen@>#2
            \moreiterationstrue
            \upperfontdim\fontdim
            \advance\fontdim\lowerfontdim
            \fontdim.5\fontdim
        \else
            \ifnum\maxbad>10000
                \moreiterationstrue
                \upperfontdim\fontdim
                \advance\fontdim\lowerfontdim
                \fontdim.5\fontdim
            \else
                \advance\dimen@-#2
                \ifdim\dimen@<10pt
                    \lowerfontdim\fontdim
                    \advance\fontdim\upperfontdim
                    \fontdim.5\fontdim
                    \dimen@\upperfontdim
                    \advance\dimen@-\lowerfontdim
                    \ifdim\dimen@<.2pt
                        \moreiterationsfalse
                    \else
                        \moreiterationstrue
                    \fi
                \else
                    \moreiterationsfalse
                \fi
            \fi
        \fi
    \ifmoreiterations \repeat
    \message{Selected font: \the\fontdim\space^^J}%
    \vbox to #2{\box0\hbox{}}% Typeset content
}%
\makeatother

\usepackage{parskip}

\setlength\fboxsep{0pt}
\begin{document}

(2cm x 2cm):

\fbox{%
\begin{fitbox}{2cm}{2cm}%
xx
\end{fitbox}
}%
\fbox{\vbox to 2cm{\hbox to 2cm{}}}%
\fbox{\vbox to 2cm{\hbox to 2cm{}}}%
\fbox{\vbox to 2cm{\hbox to 2cm{}}}%

(8cm x 2cm):

\fbox{%
\begin{fitbox}{8cm}{2cm}
xxxxxxxxxx
\end{fitbox}
}%

(8cm x 1cm):

\fbox{%
\begin{fitbox}{8cm}{1cm}
This box should be 8x1 cm and actually, that's exactly the size it has.
\end{fitbox}
}
\end{document}​

This produces the following output:

Fitbox illustration

There is no longer any text running over the edge for the cases discussed in the other answers and the question. Instead, the font shrinks and part of the box remains empty.


Here my idea: Capture the content using the environ package, place it in a box which uses a minibox with the given width. Then measure the ratio between the requested height and the total height of the box and use relsize and its \relscale macro to typeset the content again with this size. This depends on the use of a scalable font. It doesn't work for me for some input, because there seem to be scaling limits.

\documentclass{article}

\usepackage[T1]{fontenc}
\usepackage{lmodern}

\usepackage{graphicx}
\usepackage{environ}
\usepackage{relsize}

\makeatletter
\NewEnviron{fitbox}[2]{%
    \minipage{#1}%
        \sbox0{\minipage{#1}\strut\BODY\strut\endminipage}%
        \Gscale@div\factor{#2}{\dimexpr\ht0+\dp0\relax}%
        \relscale{\factor}%
        \BODY
    \endminipage
}
\makeatother

\usepackage{lipsum}

\begin{document}

\begin{fitbox}{300pt}{300pt}
    \lipsum[1-2]
\end{fitbox}

\end{document}