5

Consider the following MWE:

\documentclass{article}
\usepackage{graphicx}
\usepackage{lipsum}
\usepackage{float}
\usepackage{duckuments}

\begin{document}
    \lipsum[1-4]

    \begin{figure}[H]
    \centering
    \includegraphics{example-image-duck}
    \caption{This is a duck}
    \end{figure}

    \lipsum[4-7]
\end{document}

As you will see after compiling the MWE it will produce 2 pages, moving the image to the second page because it doesn't fit on the first page anymore by just a little bit. And because I have used the H option for the floating environment there is now a big blank space on the first page. I know that I can avoid that by letting the image float, but that's not what this is about.

If I encountered such a situation in one of my documents I would go ahead and start fiddling with the size of the image until it fits on the page eventually. By doing so I will try out different sizes (in this case making it smaller and smaller until it fits) manually. If I had to shrink the image too much I'd just let it be and accept the blank space on the first page.

What I would like to have is a way that does this process automatically with every image:

  1. Check if the floating environment fits on the current page (image and caption have enough space). If this is the case place it right there and exit.
  2. If the remaining space allows the image to be shown at x% (e.g. 50%) of its original height, scale it down and insert it here.
  3. If there isn't enough space place the image at the top of the next page

Additionally it would be really neat to have a floating-like behavior if 3. has to be used: Place the image at the top of the next page and allow text specified in the source after the image to be inserted before it in the document until the blank space is filled.

I have a few other things in mind which would add further refinements to the system but those are the "core-features".

However before I start trying to implement this functionality myself I wanted to ask whether there already are packages that do the described or do similar things that could be used to built the desired functionality upon. Any ideas on how the whole thing could be accomplished in general are welcome as well.

In case there is no solution available out there here are my thoughts on a custom implementation:

Getting the ramaining height should be rather simple and getting the float's height should be doable as well. Therefore the question ultimately comes down to how I can change the included image's size inside the floating environment. A simply approach would be to parse the float until \includegraphics is encountered, and add the necessary optional arguments. If there already are optional arguments they should get changed accordingly (adding a factor). This however is the part that I don't know how I could go about doing.

3

One can use tikzpagenodes and \pgfextracty to determine the remaining space on the page. Note that figure adds a gap of \intextsep above (gap below absorbed by bottom of page).

\documentclass{article}
\usepackage{graphicx}
\usepackage{lipsum}
\usepackage{float}
\usepackage{duckuments}
\usepackage{showframe}

\usepackage{tikzpagenodes}
\newlength{\checkspaceleft}% reserve global name

\newcommand{\checkspace}{\ifvmode\else\par\fi
  \noindent\tikz[remember picture,overlay]{%
    \pgfextracty{\checkspaceleft}{\pgfpointdiff%
      {\pgfpointanchor{current page text area}{south}}%
      {\pgfpointorigin}}%
    \global\checkspaceleft=\checkspaceleft}%
  \hrule height0pt}% not on same line

\newcommand{\myfigure}[2]{% #1 = image, #2 = caption
  \checkspace
  \begin{figure}[H]
    \sbox0{\begin{minipage}[b]{\textwidth}
      #2
      \end{minipage}}% measure caption
    \dimen0=\dimexpr \checkspaceleft-\ht0-\dp0-\intextsep\relax
    \sbox1{#1}% measure image
    \centering
    \ifdim \dimen0<\ht1\relax
      \resizebox{!}{\dimen0}{\usebox1}
    \else
      \usebox1
    \fi
    \usebox0
  \end{figure}}

\begin{document}
    \lipsum[1-4]

    \myfigure{\includegraphics{example-image-duck}}%
      {\caption{This is a duck}}

    \lipsum[4-7]
\end{document}
  • Thank you for your answer. This approach does make the image fille the complete remaining space which is not what I want. I want to display the image with its set size (if omitted the image's original size) unless it does no longer fit on the page but would fit if it was shrunk by at most x% – Raven Sep 13 '18 at 16:25
  • Slightly more complicated, but doable. One might want to create a new command for all this, using \caption and \includegraphics as the two arguments. – John Kormylo Sep 13 '18 at 17:15
  • Yeah introducing a new command is probably the easiest way to go. I'll give it a shot tomorrow and use your snippet as a base :) – Raven Sep 13 '18 at 19:37
  • I would also appreciate a command to invoke a dynamic method to size figures. – Jeffrey J Weimer Sep 14 '18 at 1:29
  • 1
    IIRC, \pagetotal is not accurate. LaTeX itself only uses it for \clearpage to see if the page has started yet. – John Kormylo Sep 14 '18 at 14:36
2

Okay so I went ahead and built something myself. I decided to create a package so when you want to copy&paste my code you have to create a file dynimage.sty inside your working directory and paste the following code in it:

\NeedsTeXFormat{LaTeX2e}

\ProvidesPackage{dynimage}[2018/09/14 Package for dynamic images that will fit on current page]

%%%%%%%%%%%%%%%%%%%%%%%%%%%%% REQUIREMENTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\RequirePackage{pgfkeys}
\RequirePackage{etoolbox}
\RequirePackage{xstring}
\RequirePackage{graphicx}
\RequirePackage{pgf}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%% INTERNAL VARS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\newtoggle{@dynimageDebug}
\def\@dyinmageMinScaleFactor{0.75}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%% KEY SETUP %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\pgfkeys{%
    /dynimage/.cd,%
    min scale factor/.estore in = {\@dyinmageMinScaleFactor},%
    min scale factor/.value required,%
    reckless/.style = {/dynimage/min scale factor=0.1},%
    reckless/.value forbidden,%
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%% CONFIGURATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\DeclareOption{debug}{\toggletrue{@dynimageDebug}}

\DeclareOption*{%
    \pgfkeys{%
        /dynimage/.cd,%
        \CurrentOption,%
    }%
}

\ProcessOptions\relax


%%%%%%%%%%%%%%%%%%%%%%%%%%%%% IMPLEMENTATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\DeclareRobustCommand{\dynImage}[2][]{%
    \iftoggle{@dynimageDebug}{%
        \PackageInfo{dynimage}{Minimum scale factor set to \@dyinmageMinScaleFactor}
    }{}%
    %
    % Get the height of the image if it was placed without any further processing
    \newlength{\@nativeImageHeight}%
    \settoheight{\@nativeImageHeight}{\hbox{\includegraphics[#1]{#2}}}%
    %
    % Get the remaining page height
    \newlength{\@remainingPageHeight}%
    \@remainingPageHeight=\dimexpr\textheight-\pagetotal\relax%
    %
    % Calculate the scaling-factor that needs to be used in order to keep the image on the current page
    \pgfmathparse{(floor(\@remainingPageHeight))/ceil(max(\@nativeImageHeight,0.1))}%
    \edef\@scaleFactor{\pgfmathresult}
    %
    % Log all gathered sizes until this point if in debug mode
    \iftoggle{@dynimageDebug}{\PackageInfo{dynimage}{The scale for “#2” is \the\@remainingPageHeight /\the\@nativeImageHeight =\@scaleFactor}}{}
    %
    % don't enlarge images (no up-scaling)
    \ifdimgreater{\@scaleFactor pt}{1pt}{\gdef\@scaleFactor{1}}{}%
    %
    % don't scale down more than allowed
    \ifdimgreater{\@scaleFactor pt}{\@dyinmageMinScaleFactor pt}{}{\gdef\@scaleFactor{1}\gdef\@tooMuchScaling{}}%
    %
    % scale if the scale isn't equal to 1
    \ifdimequal{\@scaleFactor pt}{1pt}{%
        % Output appropriate info/warning
        \ifdef{\@tooMuchScaling}{%
            \PackageWarning{dynimage}{The image "#2" would require too much down-scaling and is therefore placed on the next side}%
        }{%
            \PackageInfo{dynimage}{The image "#2" fits on the page without height-adjustments}%
        }%
        %
        % insert image as is
        \includegraphics[#1]{#2}%
    }{%
        % scale down
        %
        % calculate the scaled image height
        \pgfmathparse{\@scaleFactor * \@nativeImageHeight}
        \let\@scaledImageHeight\pgfmathresult
        %
        % Add some difference between remaining height and scaled image height in order to
        % avoid rounding inaccuracies
        \pgfmathparse{(\@remainingPageHeight - \@scaledImageHeight) < 4}
        \ifnumequal{\pgfmathresult}{1}{\pgfmathparse{\@scaledImageHeight - 4} \edef\@scaledImageHeight{\pgfmathresult}}{}
        %
        % Set up macro for all non-height specifications
        \def\@graphicOptions{}\relax%
        %
        % Set up dummy-keys for processing the \includegraphics-options
        \pgfkeys{%
            /dynimage/dummy/.cd,%
            height/.code = {\gdef\specifiesHeight{}},%
            /dynimage/dummy/.unknown/.code = {%
                % get unchanhed key-path
                \edef\keypath{\pgfkeyscurrentpath}%
                \IfBeginWith{\keypath}{/dynimage/dummy}{%
                    % trim away the local dummy path in order to get unchanged key-path
                    \StrBehind{\keypath}{/dynimage/dummy}[\keypath]
                    \xdef\keypath{\keypath}
                }{}%
                %
                % Add the respective key to \@graphicOptions but don't introduce equals where there shouldn't be any
                \edef\@val{##1}
                \ifdefequal{\@val}{\pgfkeysnovalue{}}{%
                    \xdef\@graphicOptions{\@graphicOptions,\keypath\pgfkeyscurrentname}
                }{%
                    \xdef\@graphicOptions{\@graphicOptions,\keypath\pgfkeyscurrentname=##1}%
                }%
            },%
        }%
        %
        % process the keys originally intended for the \includegraphics[]{imagefile} in order
        % to check for a possible height-definition. All other keys are stored in \@graphicOptions
        \pgfkeys{/dynimage/dummy/.cd, #1}%      
        %
        % Check if an explicit height for the image is given
        \ifdef{\specifiesHeight}{%
            % Create warning that the use of height prvented scaling
            \PackageWarning{dynimage}{Inserting image "#2" with specified height instead of adjusted height}%
            %
            % insert image as is
            \includegraphics[#1]{#2}
        }{%
            % log the scaled size of the image if in debug mode
            \iftoggle{@dynimageDebug}{%
                \PackageInfo{dynimage}{Inserting image "#2" with a height of \@scaledImageHeight pt}%
            }{}%
            %
            % add a height-specification to the keys for the image
            \xdef\@graphicOptions{height=\@scaledImageHeight pt,\@graphicOptions}
            %
            % Gobble away any leading and trailing commas as the keyvals-package used by
            % graphics can't deal with them
            \IfBeginWith{\@graphicOptions}{,}{%
                % removing leading comma
                \StrGobbleLeft{\@graphicOptions}{1}[\@graphicOptions]%
                \xdef\@graphicOptions{\@graphicOptions}%
            }{}%
            \IfEndWith{\@graphicOptions}{,}{%
                % remove trailing comma
                \StrGobbleRight{\@graphicOptions}{1}[\@graphicOptions]%
                \xdef\@graphicOptions{\@graphicOptions}%
            }{}%
            %
            % Fully expand options
            \edef\doPrintImage{\noexpand\includegraphics[\@graphicOptions]{#2}}%
            %
            % print out the scaled image
            \doPrintImage%
        }%
    }%
}

You can then go ahead and \usepackage{dynimage} in your document which will allow you to use the \dynImage[<options>{<image>} macro. <option> are the options you want to pass to the \includegraphics-command (Note: Specifying an explicit height will disable any scaling) and <image> is the path to the image file that should be included.

Using this macro instead of \includegraphics will have the following effect:
If there is enough space left on the page the image will be inserted without any modifications made to it (is if directly using \includegraphics). However if this is not the case, a scaling-factor is computed the image had to be scaled with in order to fit on the current page (in terms of its height). If the scaling factor isn't lower than the minimum scaling factor (configurable with the min scale factor-option for the package - the default value is 0.75) and if no explicit height was specified in the options, the image's height will be reduced so that it will fit on the current page and filling it (besides a few points maybe).
If you do not care about your images getting shrunken, you can pass the reckless-option to the package which will cause the min scale factor to be set to 0.1.

Taking the (slightly modified) MWE from my question it would then be used as

\documentclass{article}
\usepackage{graphicx}
\usepackage{lipsum}
\usepackage{duckuments}
\usepackage{showframe}
\usepackage{dynimage}

\begin{document}
    \lipsum[1-4]

    Here comes the image:

    \dynImage{example-image-duck}
%    \includegraphics{example-image-duck}

    \lipsum[4-7]
\end{document}

Notes

As you can see the package does not yet deal with floats (containing captions) or similar constructs. I am intending to expand the functionality of this package to tackle those things as well and might update this answer at some point.

I faced a few cases in which the image's size was set to be smaller than the calculated remaining size but it still got put on the next page. I am unsure why that might be. I think it is either caused by rounding issues or by an inaccurate way of computing the remaining page size (or a combination of both). This is why I make sure that the scaled image's size is always at least 4pt smaller than the computed remaining page height. This seems to be working reasonably well but it's not ideal.
I'll look into that as well but if you have an idea how this could be fixed I'd be very grateful for a comment ;)

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.