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 ;)