这事半个月前就计划好了,本想放到 Photosuite 下个版本里
拖到现在,若不是为了昨天拍的一套图,我也懒得折腾
就是为了这口醋,才包的这顿饺子
我的目的很简单:不引入新的语法,而是通过换行来实现拼图
在 Markdown 中连续插入多张图片,只要它们之间没有非空白内容,就应被视为一个整体自动组合



如何检测相邻图片
拼图分组思路由贪心算法实现:
当扫描到一张图片时,以它为起点向后遍历,将所有连续相邻的图片依次纳入同一组
一旦相邻关系被打断,就停止扩展,并根据组内图片数量决定是否生成拼图(不足两张则原样保留)
难点在于“相邻”的定义:
因为 Markdown 编译为 HTML 后,图片容器之间可能夹杂换行符或空白文本节点
所有,不能直接依赖 nextSibling 进行判断
/** * 检查两个容器是否相邻 * * @param container1 - 第一个容器 * @param container2 - 第二个容器 * @returns 是否相邻 */function areContainersAdjacent(container1: HTMLElement, container2: HTMLElement): boolean { // 获取两个容器的父元素 const parent1 = container1.parentElement; const parent2 = container2.parentElement;
// 如果父元素不同,不是相邻的 if (parent1 !== parent2 || !parent1) return false;
// 获取父元素的所有子元素 const siblings = Array.from(parent1.children); const index1 = siblings.indexOf(container1); const index2 = siblings.indexOf(container2);
// 检查是否相邻(中间只能有文本节点或空白节点) if (index2 !== index1 + 1) { // 检查中间是否只有空白文本节点 let hasNonWhitespace = false; let node = container1.nextSibling; while (node && node !== container2) { if (node.nodeType === Node.ELEMENT_NODE) { hasNonWhitespace = true; break; } if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) { hasNonWhitespace = true; break; } node = node.nextSibling; } return !hasNonWhitespace && node === container2; }
return true;}在解决了相邻判定的问题之后,分组逻辑本身就变得很简单了
当 Photosuite 扫描到一张图片时,会从当前位置向后查找连续相邻的图片,并将它们归为一组
出于美观考虑,我这里将单组图片数量限制为最多三张
function processImageGrids(root: Element) { // 获取所有图片元素 const images = Array.from(root.querySelectorAll("img"));
// 用于标记已处理的图片 const processed = new Set<Element>();
for (let i = 0; i < images.length; i++) { const img = images[i];
// 跳过已处理的图片 if (processed.has(img)) continue;
// 检查是否可以形成拼图 const gridImages = [img]; processed.add(img);
// 查找连续的图片(最多3张) for (let j = i + 1; j < images.length && gridImages.length < 3; j++) { const nextImg = images[j];
// 检查两个图片容器是否相邻 const currentContainer = ensurePhotosuiteContainer(gridImages[gridImages.length - 1]); const nextContainer = ensurePhotosuiteContainer(nextImg);
if (areContainersAdjacent(currentContainer, nextContainer)) { gridImages.push(nextImg); processed.add(nextImg); } else { break; } }
// 如果找到了多张连续的图片(2-3张),创建拼图 if (gridImages.length >= 2) { createImageGrid(gridImages); } }}我用 Boardmix 做了一个流程图,通俗易懂:

如何处理不同比例的图片
把图片简单地放进一个 Flex 容器并不难
如果一张是横图(16:9),另一张是竖图(9:16),直接使用 flex: 1 只会让它们宽度相同,却无法保证高度一致,结果要么高低不齐,要么图片被强行拉伸
要让多张图片在同一行里等高对齐,关键不在 Flex,而在比例关系
直观来说:哪张图片更“扁”,就应该占更宽的位置;哪张更“瘦”,就占得窄一些,这样它们的高度才能最终一致
用更具体的话说:
每张图片在一行中所占的宽度,应该和它本身的宽高比成正比
宽高比越大(越横),分到的宽度就越多;宽高比越小(越竖),分到的宽度就越少
因此,在等高布局下,可以把每张图片的宽度理解为:
每张图片的宽度占比 ≈ 自身宽高比 ÷ 所有图片宽高比之和
其中:
宽高比 = 宽 / 高
基于这个思路,我没有让 Flex 自由分配空间,而是先计算好每张图片应占的宽度,再通过 flex-basis 精确控制布局
具体实现上,会先异步获取每张图片的宽高比,计算出总比例后,将容器宽度按比例拆分。同时考虑到图片之间的间距(gap),使用 calc() 对最终宽度进行修正,避免累计误差
/** * 异步更新拼图项宽度 * 基于图片宽高比计算宽度,使得所有图片高度一致 */async function updateGridDimensions(images: HTMLImageElement[], gridItems: HTMLElement[]) { const ratios: number[] = [];
// 获取所有图片的宽高比 for (const img of images) { const ratio = await resolveImageRatio(img); ratios.push(ratio); }
// 计算总比例 const totalRatio = ratios.reduce((sum, r) => sum + r, 0); const gapCount = gridItems.length - 1;
// 设置每张图片的宽度百分比 gridItems.forEach((item, index) => { if (totalRatio > 0) { const ratio = ratios[index]; const percent = (ratio / totalRatio) * 100;
// 使用 calc 计算实际宽度:(比例% * 100) - (gap总宽 * 比例占比) // 公式: calc(33.33% - (2 * var(--gap) * 0.3333)) const widthCalc = `calc(${percent}% - (${gapCount} * var(--photosuite-grid-gap, 4px)) * ${ratio / totalRatio})`;
// 设置 flex-basis 和 max-width item.style.flex = `0 0 ${widthCalc}`; item.style.maxWidth = widthCalc; } });}这样一来,不管图片比例多么悬殊,它们在拼图中都会自然对齐
高度一致、宽度合理、边缘整齐,同时也避免了任何形式的拉伸或裁剪
布局样式
样式只负责布局:Flex + gap + 响应式间距,没有额外装饰,所有空间都留给图片本身
这里通过 CSS 变量统一管理间距;在移动端则适当缩小 gap,以保证有限屏幕宽度下的视觉紧凑度
/* 拼图容器 */.photosuite-grid { --photosuite-grid-gap: 4px;
display: flex; gap: var(--photosuite-grid-gap); width: 100%; margin: 0; padding: 0; align-items: flex-start;}
/* 移动端保持拼图布局,但减小间距 */@media (max-width: 768px) { .photosuite-grid { --photosuite-grid-gap: 2px; }}在交互层面,加了点人情味,当鼠标悬停在图片上时(拼图状态),会有一个轻微的放大效果
注:使用该功能无需配置,默认为
imageGrid: true拼图状态下不显示 EXIF 和标签,它们在编译阶段并未生成
至此,完成
感谢 Claude Code、Gemini、ChatGPT 对项目的大力支持
安装
Photosuite 已发布至 npm,可直接安装:
pnpm add photosuite# ornpm install photosuite# oryarn add photosuite