%%% ----------------------------------------------------------------------------
%%% joinbox: Join figures to same height or width with LaTeX3
%%% Author    : Nan Geng <nangeng@nwafu.edu.cn>
%%% Repository: https://gitee.com/nwafu_nan/joinfigs
%%% License   : The LaTeX Project Public License 1.3c
%%% ----------------------------------------------------------------------------

\NeedsTeXFormat{LaTeX2e}
\RequirePackage{expl3}
\ProvidesExplPackage{joinbox}{2026-06-25}{v1.0.4}
  {Join figures to same height or width with LaTeX3}

\RequirePackage{xparse}
\RequirePackage{graphicx}

%% \tl_if_eq:NnTF 与texlive 2020的兼容性设置
\cs_if_exist:NF \tl_if_eq:NnTF
  {
    \tl_new:N \l__tblr_backport_b_tl
    \prg_new_protected_conditional:Npnn \tl_if_eq:Nn #1 #2 { T, F, TF }
      {
        \group_begin:
          \tl_set:Nn \l__tblr_backport_b_tl { #2 }
          \exp_after:wN
        \group_end:
        \if_meaning:w #1 \l__tblr_backport_b_tl
          \prg_return_true:
        \else:
          \prg_return_false:
        \fi:
      }
    \prg_generate_conditional_variant:Nnn \tl_if_eq:Nn { c } { TF, T, F }
  }

\cs_if_exist:NF \seq_map_indexed_function:NN
  {
    \cs_set_eq:NN \seq_map_indexed_function:NN \seq_indexed_map_function:NN
  }

% 错误信息输出函数
\cs_new:Npn \__joinbox_error:n { \msg_error:nn { joinbox } }

% 空盒子对象列表出错信息
\msg_new:nnn { joinbox } { empty-contents }
  { The~contents~list~is~empty. }

% 被拼接对象同时为空，无需拼接
\msg_new:nnn { joinbox } { empty-objs }
  { The~two~objects~which~were~joined~are~empty. }

% 空图像文件名列表出错信息
\msg_new:nnn { joinbox } { empty-fignames }
  { The~figure~namelist~is~empty. }

% 函数变体
\cs_generate_variant:Nn \hcoffin_set:Nn { Nx }

% 定义变量
\bool_new:N   \l__joinbox_vertical_bool    % 是否为垂直拼接模式
\bool_new:N   \l__joinbox_out_scale_bool   % 是否缩放
\bool_new:N   \l__joinbox_only_first_bool  % 是否仅有第1个盒子
\bool_new:N   \l__joinbox_only_second_bool % 是否仅有第2个盒子

\int_new:N    \l__joinbox_baseline_int     % 基线位置(1-t, 2-vc, 3-H, 4-b)

\clist_new:N  \l__joinbox_name_clist       % 图片文件名列表(可带路径)
\clist_new:N  \l__joinbox_contents_clist   % 拼接内容列表

\coffin_new:N \l__joinbox_out_coffin       % 结果盒子
\coffin_new:N \l__joinbox_tmpa_coffin      % 临时盒子a
\coffin_new:N \l__joinbox_tmpb_coffin      % 临时盒子b

\dim_new:N \l__joinbox_out_length_dim      % 实际输出尺寸
\dim_new:N \l__joinbox_sep_dim             % 间隔尺寸
\dim_new:N \l__joinbox_min_width_dim       % 最小宽度
\dim_new:N \l__joinbox_min_height_dim      % 最小高度

\dim_new:N \l__joinbox_tmpa_dim            % 临时尺寸a
\dim_new:N \l__joinbox_tmpb_dim            % 临时尺寸b

% 初值置假
\bool_set_false:N \l__joinbox_only_first_bool
\bool_set_false:N \l__joinbox_only_second_bool

% 设置outlen选项值的辅助函数（必须在keys_define之前定义）
% #1: 输出实际尺寸，>0则缩放，<=0则不作处理
\cs_new_protected:Npn \__joinbox_set_outlen:n #1
  {
    \dim_compare:nNnTF { \dim_eval:n { #1 } } > \c_zero_dim
      {
        \bool_set_true:N \l__joinbox_out_scale_bool
        \dim_set:Nn \l__joinbox_out_length_dim { \dim_eval:n { #1 } }
      }
      {
        \bool_set_false:N \l__joinbox_out_scale_bool
      }
  }

%% 选项设计
\keys_define:nn { joinbox }
  {
    % 输出结果基线位置
    baseline .choice:,
    baseline .value_required:n = true,
    baseline .choices:nn = { t, vc, H, b }
                           {
                             \int_set_eq:NN \l__joinbox_baseline_int
                                            \l_keys_choice_int
                           },
    baseline .default:n = b,
    baseline .initial:n = b,

    % 输出尺寸(垂直拼接：宽度，水平拼接：高度)
    outlen   .code:n = { \__joinbox_set_outlen:n { #1 } },
    outlen   .default:n = 0pt,
    outlen   .initial:n = 0pt,

    % 拼接间距
    sep      .dim_set:N  = \l__joinbox_sep_dim,
    sep      .default:n = 0pt,
    sep      .initial:n = 0pt,

    unknown .code:n = \__joinbox_unknown_key:V \l_keys_key_str,
  }

% 默认参数处理
\cs_new_protected:Npn \__joinbox_unknown_key:n #1
  {
    \str_case:nnF { #1 }
      {
          { t  } { \int_set:Nn \l__joinbox_baseline_int { 1 } } % 盒子顶端对齐
          { vc } { \int_set:Nn \l__joinbox_baseline_int { 2 } } % 盒子中心对齐
          { H  } { \int_set:Nn \l__joinbox_baseline_int { 3 } } % 盒子内容基线对齐
          { b  } { \int_set:Nn \l__joinbox_baseline_int { 4 } } % 盒子底端对齐
      }{
        % 其它参数默认为输出尺寸，转换为token
        \tl_set_rescan:Nnn \l_tmpa_tl {} { #1 }
        \__joinbox_set_outlen:n { \l_tmpa_tl }
      }
  }
\cs_generate_variant:Nn \__joinbox_unknown_key:n { V }

%% 参数设置用户接口
\NewDocumentCommand \joinset { m }
  { \keys_set:nn { joinbox } { #1 } }

% 计算box盒子的总高度
% #1: 盒子变量
\cs_if_free:NT \box_ht_plus_dp:N
  {
    \cs_new_protected:Npn \box_ht_plus_dp:N #1
      { \tex_dimexpr:D \box_ht:N #1 + \box_dp:N #1 \scan_stop: }
  }

% 计算coffin盒子的总高度
% #1: coffin变量
\cs_new_nopar:Npn \__joinbox_coffin_ht_plus_dp:N #1
  {
    \coffin_ht:N #1 + \coffin_dp:N #1
  }

% 计算两个盒子的最小宽度和最小高度
% 注意：最小宽度和最小高度不一定属于同一个盒子
% #1: 第1个盒子的内容
% #2: 第2个盒子的内容
\cs_new_protected:Npn \__joinbox_calc_min_size:nn #1#2
  {
    \bool_if:NTF \l__joinbox_only_first_bool
      {
        % 仅有第1个盒子，直接使用其尺寸
        \hbox_set:Nn \l_tmpa_box { #1 }
        \dim_set_eq:NN \l__joinbox_min_width_dim  { \box_wd:N \l_tmpa_box }
        \dim_set_eq:NN \l__joinbox_min_height_dim { \box_ht_plus_dp:N \l_tmpa_box }
      }
      {
        \bool_if:NTF \l__joinbox_only_second_bool
          {
            % 仅有第2个盒子，直接使用其尺寸
            \hbox_set:Nn \l_tmpa_box { #2 }
            \dim_set_eq:NN \l__joinbox_min_width_dim  { \box_wd:N \l_tmpa_box }
            \dim_set_eq:NN \l__joinbox_min_height_dim { \box_ht_plus_dp:N \l_tmpa_box }
          }
          {
            % 两个盒子都有，计算最小尺寸
            \hbox_set:Nn \l_tmpa_box { #1 }
            \dim_set:Nn \l__joinbox_min_width_dim  { \box_wd:N \l_tmpa_box }
            \dim_set:Nn \l__joinbox_min_height_dim { \box_ht_plus_dp:N \l_tmpa_box }

            \hbox_set:Nn \l_tmpa_box { #2 }
            \dim_set:Nn \l_tmpa_dim { \box_wd:N \l_tmpa_box }
            \dim_set:Nn \l_tmpb_dim { \box_ht_plus_dp:N \l_tmpa_box }

            \dim_set:Nn \l__joinbox_min_width_dim
              { \dim_min:nn { \l__joinbox_min_width_dim }{ \l_tmpa_dim } }
            \dim_set:Nn \l__joinbox_min_height_dim
              { \dim_min:nn { \l__joinbox_min_height_dim }{ \l_tmpb_dim } }
          }
      }
  }

% 输出盒子
% #1: 需要输出的coffin变量
\cs_new_protected:Npn \__joinbox_typeout_coffin:N #1
  {
    \int_case:nn { \l__joinbox_baseline_int }
      {
        { 1 } { \coffin_typeset:Nnnnn #1 { l } { t  } { 0pt } { 0pt } } % 顶端对齐
        { 2 } { \coffin_typeset:Nnnnn #1 { l } { vc } { 0pt } { 0pt } } % 中心对齐
        { 3 } { \coffin_typeset:Nnnnn #1 { l } { H  } { 0pt } { 0pt } } % 基线对齐
        { 4 } { \coffin_typeset:Nnnnn #1 { l } { b  } { 0pt } { 0pt } } % 底端对齐
      }
  }

% 缩放coffin盒子的辅助函数
% #1: coffin变量
% #2: 目标尺寸
% #3: 当前尺寸
\cs_new_protected:Npn \__joinbox_scale_coffin:Nnn #1#2#3
  {
    \dim_compare:nNnF { #2 } = \c_zero_dim
      {
        \coffin_scale:Nnn #1
          { \dim_ratio:nn { #2 } { #3 } }
          { \dim_ratio:nn { #2 } { #3 } }
      }
  }

% 将 coffin 缩放到最小尺寸
% #1: coffin 变量
\cs_new_protected:Npn \__joinbox_scale_to_min:N #1
  {
    \bool_if:NTF \l__joinbox_vertical_bool
      {
        \__joinbox_scale_coffin:Nnn #1
          { \l__joinbox_min_width_dim } { \coffin_wd:N #1 }
      }
      {
        \__joinbox_scale_coffin:Nnn #1
          { \l__joinbox_min_height_dim }
          { \__joinbox_coffin_ht_plus_dp:N #1 }
      }
  }

% 两个盒子拼接函数
% 将指定的两个内容构成的盒子拼接成一个盒子
% #1: 第1个盒子的内容
% #2: 第2个盒子的内容
\cs_new_protected:Npn \__joinbox_handle:nn #1#2
  {
    \group_begin:
      \hcoffin_set:Nn \l__joinbox_out_coffin  { #1 }
      \hcoffin_set:Nn \l__joinbox_tmpa_coffin { #2 }

      \bool_if:NTF \l__joinbox_vertical_bool
        {
          % 垂直拼接
          \bool_if:NF \l__joinbox_only_second_bool
            {
              \coffin_scale:Nnn \l__joinbox_out_coffin
                {
                  \dim_ratio:nn { \l__joinbox_min_width_dim }
                                { \coffin_wd:N \l__joinbox_out_coffin }
                }{
                  \dim_ratio:nn { \l__joinbox_min_width_dim }
                                { \coffin_wd:N \l__joinbox_out_coffin }
                }
            }

          \bool_if:NF \l__joinbox_only_first_bool
            {
              \coffin_scale:Nnn \l__joinbox_tmpa_coffin
                {
                  \dim_ratio:nn { \l__joinbox_min_width_dim }
                                { \coffin_wd:N \l__joinbox_tmpa_coffin }
                }{
                  \dim_ratio:nn { \l__joinbox_min_width_dim }
                                { \coffin_wd:N \l__joinbox_tmpa_coffin }
                }
            }

          \bool_if:NT \l__joinbox_only_second_bool
            {
              \coffin_set_eq:NN \l__joinbox_out_coffin \l__joinbox_tmpa_coffin
            }

          \bool_if:nT { !(\l__joinbox_only_first_bool) && !(\l__joinbox_only_second_bool) }
            {
              \coffin_join:NnnNnnnn \l__joinbox_out_coffin
                { hc } { b } \l__joinbox_tmpa_coffin { hc } { t }
                { 0pt } { -\l__joinbox_sep_dim }
            }

          \bool_if:NT \l__joinbox_out_scale_bool
            {
              \__joinbox_scale_coffin:Nnn \l__joinbox_out_coffin
                { \l__joinbox_out_length_dim }
                { \l__joinbox_min_width_dim }
            }
        }
        {
          % 水平拼接
          \__joinbox_scale_coffin:Nnn \l__joinbox_out_coffin
            { \l__joinbox_min_height_dim }
            { \__joinbox_coffin_ht_plus_dp:N \l__joinbox_out_coffin }

          \hcoffin_set:Nn \l__joinbox_tmpa_coffin { #2 }

          \__joinbox_scale_coffin:Nnn \l__joinbox_tmpa_coffin
            { \l__joinbox_min_height_dim }
            { \__joinbox_coffin_ht_plus_dp:N \l__joinbox_tmpa_coffin }

          \coffin_join:NnnNnnnn \l__joinbox_out_coffin
            { vc } { r } \l__joinbox_tmpa_coffin { vc } { l }
            { \l__joinbox_sep_dim } { 0pt }

          \bool_if:NT \l__joinbox_out_scale_bool
            {
              \__joinbox_scale_coffin:Nnn \l__joinbox_out_coffin
                { \l__joinbox_out_length_dim }
                { \l__joinbox_min_height_dim }
            }
        }

      \hcoffin_set:Nn \l__joinbox_out_coffin
        {
          \coffin_typeset:Nnnnn \l__joinbox_out_coffin
            { l } { b } { 0pt } { 0pt }
        }
      \hcoffin_set_end:

      \__joinbox_typeout_coffin:N \l__joinbox_out_coffin
    \group_end:
  }

% 多个对象拼接的通用核心函数
% #1: 内容输出命令（如 \includegraphics 或 \use:n）
% #2: 对象列表（clist变量）
\cs_new_protected:Npn \__joinbox_join_multiple:Nn #1#2
  {
    \dim_set:Nn \l__joinbox_min_width_dim  { \c_max_dim }
    \dim_set:Nn \l__joinbox_min_height_dim { \c_max_dim }

    % 收集所有对象并计算最小尺寸
    \clist_map_inline:Nn #2
      {
        \hcoffin_set:Nn \l__joinbox_tmpa_coffin { #1 { ##1 } }

        \dim_set:Nn \l__joinbox_tmpa_dim { \coffin_wd:N \l__joinbox_tmpa_coffin }
        \dim_set:Nn \l__joinbox_tmpb_dim { \__joinbox_coffin_ht_plus_dp:N \l__joinbox_tmpa_coffin }

        \dim_compare:nNnT { \l__joinbox_tmpa_dim } < { \l__joinbox_min_width_dim }
          { \dim_set_eq:NN \l__joinbox_min_width_dim \l__joinbox_tmpa_dim }
        \dim_compare:nNnT { \l__joinbox_tmpb_dim } < { \l__joinbox_min_height_dim }
          { \dim_set_eq:NN \l__joinbox_min_height_dim \l__joinbox_tmpb_dim }
      }

    % 取得第1个对象
    \clist_pop:NN #2 \l_tmpa_tl
    \hcoffin_set:Nn \l__joinbox_out_coffin { #1 { \l_tmpa_tl } }

    % 缩放第1个对象
    \__joinbox_scale_to_min:N \l__joinbox_out_coffin

    % 拼接其它对象
    \clist_map_inline:Nn #2
      {
        \hcoffin_set:Nn \l__joinbox_tmpa_coffin { #1 { ##1 } }
        \__joinbox_scale_to_min:N \l__joinbox_tmpa_coffin

        \bool_if:NTF \l__joinbox_vertical_bool
          {
            \coffin_join:NnnNnnnn \l__joinbox_out_coffin
              { hc } { b } \l__joinbox_tmpa_coffin { hc } { t }
              { 0pt } { -\l__joinbox_sep_dim }
          }
          {
            \coffin_join:NnnNnnnn \l__joinbox_out_coffin
              { vc } { r } \l__joinbox_tmpa_coffin { vc } { l }
              { \l__joinbox_sep_dim } { 0pt }
          }
      }

    % 按指定尺寸缩放输出
    \bool_if:NT \l__joinbox_out_scale_bool
      {
        \bool_if:NTF \l__joinbox_vertical_bool
          {
            \__joinbox_scale_coffin:Nnn \l__joinbox_out_coffin
              { \l__joinbox_out_length_dim }
              { \coffin_wd:N \l__joinbox_out_coffin }
          }
          {
            \__joinbox_scale_coffin:Nnn \l__joinbox_out_coffin
              { \l__joinbox_out_length_dim }
              { \__joinbox_coffin_ht_plus_dp:N \l__joinbox_out_coffin }
          }
      }

    \hcoffin_set:Nn \l__joinbox_out_coffin
      {
        \coffin_typeset:Nnnnn \l__joinbox_out_coffin
          { l } { b } { 0pt } { 0pt }
      }
    \hcoffin_set_end:

    \__joinbox_typeout_coffin:N \l__joinbox_out_coffin
  }

% 多个盒子拼接函数
\cs_new_protected:Npn \__joinbox_boxes:
  {
    \__joinbox_join_multiple:Nn \use:n \l__joinbox_contents_clist
  }

% 多个图像拼接函数
\cs_new_protected:Npn \__joinbox_figs:
  {
    \__joinbox_join_multiple:Nn \includegraphics \l__joinbox_name_clist
  }

% 盒子拼接用户接口
% 将两个盒子按指定方式拼接成一个盒子并将基线调整为选项设定的基线位置
% #1: 是否为*命令，如有*则采用水平拼接，无*则采用垂直拼接
% #2: 可选参数，用key-value选项指定拼接参数
% #3: 第1个盒子的内容
% #4: 第2个盒子的内容
\NewDocumentCommand{\joinbox}{ s O{} +m +m}
  {
    \IfBooleanTF{#1}
      { \bool_set_false:N \l__joinbox_vertical_bool }
      { \bool_set_true:N  \l__joinbox_vertical_bool }

    \group_begin:
      \keys_set:nn { joinbox } { #2 }

      \tl_set:Nn \l_tmpa_tl { #3 }
      \tl_if_empty:VT \l_tmpa_tl
        { \bool_set_true:N \l__joinbox_only_second_bool }

      \tl_set:Nn \l_tmpa_tl { #4 }
      \tl_if_empty:VT \l_tmpa_tl
        { \bool_set_true:N \l__joinbox_only_first_bool }

      \bool_if:nT { \l__joinbox_only_first_bool && \l__joinbox_only_second_bool }
        { \__joinbox_error:n { empty-objs } }

      \__joinbox_calc_min_size:nn { #3 } { #4 }
      \__joinbox_handle:nn { #3 } { #4 }
    \group_end:
  }

% 两个以上盒子拼接用户接口
% 将逗号分隔的内容构成的各个盒子拼接成一个盒子
% #1: 是否为*命令，如有*则采用水平拼接，无*则采用垂直拼接
% #2: 可选参数，用key-value选项指定拼接参数
% #3: 必选参数，用逗号分隔的，需要拼接的内容（各个内容应该置于大括号内）
\NewDocumentCommand{\joinboxes}{ s O{} +m}
  {
    \IfBooleanTF{ #1 }
      { \bool_set_false:N \l__joinbox_vertical_bool }
      { \bool_set_true:N  \l__joinbox_vertical_bool }

    \group_begin:
      \keys_set:nn { joinbox } { #2 }
      \clist_set:Nn \l__joinbox_contents_clist { #3 }

      \clist_if_empty:NT \l__joinbox_contents_clist
        { \__joinbox_error:n { empty-contents } }

      \int_compare:nNnT { \clist_count:N \l__joinbox_contents_clist } = { 1 }
        { \bool_set_true:N \l__joinbox_only_first_bool }

      \__joinbox_boxes:
    \group_end:
  }

% 图像拼接用户接口
% 将指定文件名列表中的图像拼接成一个盒子
% #1: 是否为*命令，如有*则采用水平拼接，无*则采用垂直拼接
% #2: 可选参数，用key-value选项指定拼接参数
% #3: 必选参数，用逗号分隔的，需要拼接的图像文件名称（可以带有路径）
\NewDocumentCommand{\joinfigs}{ s O{} m}
  {
    \IfBooleanTF{#1}
      { \bool_set_false:N \l__joinbox_vertical_bool }
      { \bool_set_true:N  \l__joinbox_vertical_bool }

    \group_begin:
      \keys_set:nn { joinbox } { #2 }
      \clist_set:Nn \l__joinbox_name_clist { #3 }

      \clist_if_empty:NT \l__joinbox_name_clist
        { \__joinbox_error:n { empty-fignames } }

      \int_compare:nNnT { \clist_count:N \l__joinbox_name_clist } = { 1 }
        { \bool_set_true:N \l__joinbox_only_first_bool }

      \__joinbox_figs:
    \group_end:
  }
\endinput
