%%% ---------------------------------------------------------------------------- %%% joinbox: Join figures to same height or width with LaTeX3 %%% Author : Nan Geng %%% 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