[Tex/LaTex] Obtaining the behavior of both subfigure and subcaptionbox in the subcaption package

subfloats

In the subcaption package, the default way to create subfigures is when the subfigure environment:

\documentclass{article}
\usepackage{caption}
\usepackage{subcaption}
\begin{document}
\begin{figure}
 \centering
 \begin{subfigure}{0.4\textwidth}
  \centering
  Subfigure1 is located here
  \caption{Subfigure1 caption}
  \label{subfig:1}
 \end{subfigure}
 \begin{subfigure}{0.4\textwidth}
  \centering
  Subfigure2 is located here
  \caption{Subfigure2 caption}
  \label{subfig:2}
 \end{subfigure}
 \caption{Figure caption}
 \label{fig}
\end{figure}
\end{document}

I like this syntax, especially in that the figure comes first, then the caption, then the label for the caption. However, I don't like having to specify the \textwidth within the environment. I would prefer that this parameter be optional with the default value being the width of the figure.

One solution to fix this problem is to use a savebox, but then source for the figure is (slightly) divorced from its primary location of use.

The subcaption package provides a solution for this problem via the \subcaptionbox command:

\documentclass{article}
\usepackage{caption}
\usepackage{subcaption}
\begin{document}
\begin{figure}
 \centering
 \subcaptionbox{Subfigure1 caption\label{subfig:1}}{
  \centering
  Subfigure1 is located here}
 \subcaptionbox{Subfigure2 caption\label{subfig:2}}{
  \centering
  Subfigure2 is located here}
 \caption{Figure caption}
 \label{fig}
\end{figure}
\end{document}

Now specifying \textwidth is optional and defaults to the width of the figure, but the source is much less readable. I dislike this solution for several reasons:

  1. It is easier to write, read, and understand large amounts of code placed within \begin{}...\end{} then within \someCommand{...}. (It is so easy to use the wrong number of curly braces.)
  2. The figure, caption, and label out written out of order from the way they are typeset (by default), which is the order in which I think about them.
  3. The location to place the caption is not explicit. (It is just a required value to the \subcaptionbox placed between some {}'s, so a fellow coauthor that doesn't know this syntax won't know how to add a caption if it is currently blank.)

Question:

Is there a way to get the best of both worlds? How can I get behavior equivalent to using the subfigure environment but with the specification of \textwidth being optional (and defaulting to the width of the figure)?

(This question is about the subcaption package because it seems that this is the best package for subfigures. I am open to suggestions that another package is better and fulfills my requirements.)

Best Answer

This doesn't support all the features of \subcaptionbox: the optional argument <inner-pos> can't be expressed.

The optional argument to the xsubcaption environment is the explicit width of the subfigure.

\documentclass{article}
\usepackage{caption}
\usepackage{subcaption}
\usepackage{environ}

\makeatletter
\environfinalcode{}
\NewEnviron{xsubcaption}[1][-1sp]
 {\let\xsc@theoptcaption\@empty
  \let\xsc@thecaption\@empty
  \let\xsc@thelabel\@empty
  \expandafter\xsc@checkcaption\BODY\caption\xsc@checkcaption
  \expandafter\xsc@checklabel\BODY\label\xsc@checklabel
  \begingroup\edef\x{\endgroup
    \noexpand\subcaptionbox
      \ifx\xsc@theoptcaption\@empty\else
        [\unexpanded\expandafter{\xsc@theoptcaption}]%
      \fi
      {\unexpanded\expandafter{\xsc@thecaption}%
       \ifx\xsc@thelabel\@empty\else
         \noexpand\label{\unexpanded\expandafter{\xsc@thelabel}}%
       \fi} 
      \ifdim#1=-1sp % no optional argument
      \else
        [#1]%
      \fi
      {\unexpanded{\renewcommand\caption[2][]{}\renewcommand{\label}[1]{}}%
       \unexpanded\expandafter{\BODY}}}\x
}

\long\def\xsc@checkcaption#1\caption#2\xsc@checkcaption{%
  \if\relax\detokenize{#2}\relax
    \expandafter\@gobble
  \else
    \expandafter\@firstofone
  \fi
  {\expandafter\xsc@getcaption\BODY\xsc@getcaption}%
}
\long\def\xsc@getcaption#1\caption#2#3\xsc@getcaption{%
  \if[\detokenize{#2}%
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
  {\expandafter\xsc@getcaptionopt\BODY\xsc@getcaptionopt}%
  {\def\xsc@thecaption{#2}}%
}
\long\def\xsc@getcaptionopt#1\caption[#2]#3#4\xsc@getcaptionopt{%
  \def\xsc@theoptcaption{#2}%
  \def\xsc@thecaption{#3}%
}
\long\def\xsc@checklabel#1\label#2\xsc@checklabel{%
  \if\relax\detokenize{#2}\relax
    \expandafter\@gobble
  \else
    \expandafter\@firstofone
  \fi
  {\expandafter\xsc@getlabel\BODY\xsc@getlabel}%
}
\long\def\xsc@getlabel#1\label#2#3\xsc@getlabel{%
  \def\xsc@thelabel{#2}%
}
\makeatother

\begin{document}
\begin{figure}
\centering
\begin{xsubcaption}
\centering
Subfigure1 is located here
\caption{Subfigure1 caption}\label{subfig:1}
\end{xsubcaption}
\begin{xsubcaption}
\centering
Subfigure2 is located here
\end{xsubcaption}
\begin{xsubcaption}[3cm]
\centering
Subfigure3 is located here
\caption[A]{BBB}
\end{xsubcaption}
\caption{Figure caption}
\label{fig}
\end{figure}

\ref{subfig:1} \ref{fig}
\end{document}

enter image description here

I use the environ package to collect the environment's body in the macro \BODY. Then I check whether \caption appears in the body; if so I check whether it has an optional argument. In caption (and the "list caption", if present) is stored in a macro and then the whole \subcaptionbox instruction is built from the various parts (when the body is typeset, \caption is redefined to do nothing).

(Added) The same checks are performed to catch a \label command outside the argument of \caption (both ways are supported, of course).

Finally the \subcaptionbox macro with all arguments determined with the code above is executed. It appears to be complicated, but it's not really difficult: it's a standard way to catch the presence of some token (\caption and \label) in a list of tokens, in this case the contents of the environment, which is available as the expansion of \BODY.