2938 字
约 9 分钟
0
ImageSplit 切图工具 —— 完整流程与技术原理文档

ImageSplit 切图工具 —— 完整流程与技术原理文档

面向想要理解这套切图工具如何运作的读者(含新开发者、产品、非技术协作方)。 本文既讲清本项目(ImageSplit)的真实实现,也提炼出一套通用的切图流程模型,可迁移到其他切图项目。


目录

  1. 项目定位
  2. 一张图看懂整体流程
  3. 六个阶段详解
  4. 三种切法:数学原理
  5. 通用切图流程模型(可复用抽象)
  6. 边界情况与常见坑
  7. 改代码 / 扩展的入手点

1. 项目定位

ImageSplit 是一个 100% 纯前端的图片切分工具。 核心承诺:图片不离开浏览器

  • 无服务器、无数据库、无上传 → 天然隐私
  • 整个应用打包成一个 HTML 文件vite-plugin-singlefile),双击即可运行,零安装
  • 技术栈:React 19 + TypeScript + Zustand 状态管理 + Tailwind CSS 4 + Canvas API + JSZip/FileSaver

它解决什么问题:把一张大图切成若干小图——用于社交媒体九宫格、拼图素材、长图分段、多区块素材等。


2. 一张图看懂整体流程

┌─────────────────────────────────────────────────────────────┐
│                       浏览器(纯前端)                        │
│                                                             │
│  ①接图     ②配置     ③算切位     ④预览      ⑤真切       ⑥下载    │
│  ┌──────┐   ┌──────┐  ┌────────┐  ┌──────┐  ┌────────┐  ┌────┐│
│  │ 拖入 │   │ 选模式 │  │ split  │  │ 画布 │  │ export │  │文件││
│  │ 粘贴 │──▶│ 行列数 │──▶│ Image  │──▶│ 画廊 │──▶│ Slices │──▶│/ZIP││
│  │ 点击 │   │ 画线  │  │  .ts   │  │ 预览 │  │  .ts   │  │    ││
│  └──────┘   └───┬───┘  └────────┘  └──┬───┘  └────────┘  └────┘│
│                 │                      │                       │
│             ▼  唯一真相来源            ▼  读同一套矩形          │
│          ┌───────────────────────────────────────────┐        │
│          │        Zustand Store(useImageStore)      │        │
│          └───────────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────────┘

关键设计:⑤ 真切的矩形 = ④ 预览的矩形(同一套 sliceImage.ts 纯函数),做到"所见即所得"。


3. 六个阶段详解

阶段一:接图(输入)

入口src/components/ImageUploader.tsx

用户从三种通道提供图片:

通道 触发
拖放 onDrop,取 dataTransfer.files[0]

| 点击选择 | 隐藏 <input type="file"> ; | 粘贴 | 全局 document.addEventListener('paste'),任意处 Ctrl+V |

数据读取链路

[File] 文件对象
  └─> FileReader.readAsDataURL(file)      // 文件 → Base64 数据串
        └─> new Image(); img.src = dataURL // 载入图片
              └─> img.onload               // 解码出真实宽高
                    └─> ImageInfo {
                          file, element, 
                          width: naturalWidth, height: naturalHeight,
                          name: 去掉扩展名的文件名
                        }
                          └─> store.setImage(info)

通用要点:图片需要真实宽高才能算切割,所以必须先让浏览器解码(img.onload 后取 naturalWidth/Height),而不能只用文件大小。

阶段二:配置(用户决定怎么切)

入口src/components/ControlPanel.tsx → store 的 set* action

三种切割模式 + 各自的配置,全部写入共享大脑:

模式 配置 写入函数
grid 等分网格 rows × cols 行列数 setGridConfig
fixed 固定尺寸 width × height 像素 setFixedConfig
custom 自定义线 一组 CustomLine[](横/竖线位置) add/remove/updateCustomLine

⚠️ 关键法则(失效规则):任何会改变切分结果的配置变更,都会把已算好的 slices 清空,强制系统重算 —— 详见第 3.4 节。

阶段三:算切位(纯数学)

入口src/utils/splitImage.ts

computeGridSlices / computeFixedSlices / computeCustomSlices
    (图片宽度, 图片高度, 配置)  ──▶  SliceRect[]

产出核心数据结构:

interface SliceRect {
  x: number       // 左上角 X
  y: number       // 左上角 Y
  width: number   // 宽度
  height: number  // 高度
  row: number     // 所在行(命名用)
  col: number     // 所在列(命名用)
}

通用要点:这一步纯粹算坐标、不切图。把它做成纯函数(同输入 → 同输出、无副作用)是保证"预览=导出"的关键,因为预览、画廊、导出三处都能复用同一个函数。

阶段四:预览(所见)

两个观察者都读同一套矩形:

  1. 画布 CanvasPreview.tsx:把切割线(虚线)画在原图上,支持缩放/平移、自定义线编辑(双击加线/拖拽移线/右键删线)。
  2. 画廊 SlicePreview.tsx:底部横向缩略图条,逐块 URL.createObjectURL(blob) 生成预览图,点击可全屏查看。

🧠 内存:画廊为每个切片生成 blob URL,必须在组件卸载/变更时 URL.revokeObjectURL 释放,否则泄漏内存。

阶段五:真切(裁剪成文件)

入口src/utils/exportSlices.ts

对所有矩形 rect:
   sliceToBlob(img, rect, format, quality):
     1. 新建空白 canvas,尺寸 = rect 的宽高
     2. ctx.drawImage(原图, rect.x, rect.y, rect.width, rect.height,
                        0, 0, rect.width, rect.height)
        // 从原图"抠"出该区域,贴到画布左上角
     3. canvas.toBlob(...)          // 编码成图片数据 Blob
        // PNG 无损;JPEG/WebP 传 quality/100 压缩

exportAllSlices(...):
   1 张  → saveAs(单文件)                    → 直接下载
   多张  → JSZip:逐块 zip.file() → 打包     → saveAs(压缩包)

通用要点:裁剪的本质 = 离屏 canvas + drawImage 源区域拷贝。图片自始至终在浏览器内存,导出只是"抠出来再让浏览器下载"。

阶段六:下载(输出)

  • 单个切片:{文件名}_{row}_{col}.{格式} 直接下载
  • 多切片:打包成 {文件名}_slices.zip
  • 支持格式:PNG(无损)、JPEG / WebP(有损,可调质量)

文件名中的 row/col 正是矩形里携带的,一箭双雕:既定义裁剪位置,又决定下载文件名。


4. 三种切法:数学原理

4.1 等分网格(Grid)—— 末列/末行吸收余量

sliceW = Math.floor(imgWidth  / cols)   // 每列"理想"宽,向下取整
sliceH = Math.floor(imgHeight / rows)   // 每行"理想"高
遍历 r, c:
   width  = isLastCol ? imgWidth  - c*sliceW : sliceW   // 最后一列,拉伸到右缘
   height = isLastRow ? imgHeight - r*sliceH : sliceH   // 最后一行,拉伸到底缘

例子:1000px 宽 / 3 列 → floor(1000/3)=333,得 333+333+334(末列吸收 1px 余量)。不留缝、不溢出。

4.2 固定尺寸(Fixed)—— 整数列 + 边缘钳制

cols = Math.ceil(imgWidth  / width)    // 向上取整求列数
rows = Math.ceil(imgHeight / height)   // 保证覆盖整图
遍历:x = c*width; y = r*height
   width  = Math.min(width,  imgWidth  - x)   // 到右缘就钳制
   height = Math.min(height, imgHeight - y)   // 到底缘就钳制

例子:350px 宽 / 200px 块 → ceil(350/200)=2 列;第 2 列宽度 = min(200, 350-200)=150(被钳制,绝不溢出)。

4.3 自定义线(Custom)—— 线的位置 → 网格 + 去重护栏

xPositions = [0, ...竖线位置(四舍五入, 排序), imgWidth]
uniqueX    = [...new Set(xPositions)]   // 去重:防两条线在同像素 → 零宽片
遍历相邻两 x(以及相邻两 y):
   w = xNext - x;  h = yNext - y
   if (w > 0 && h > 0) 加入矩形            // 护栏:拒绝零/负尺寸

🛡️ 两条线拖到几乎同一点,四舍五入后可能是同像素 → 用 Set 去重 + w>0 && h>0 护栏,永不产生 0×0 不可下载切片


5. 通用切图流程模型(可复用抽象)

把 ImageSplit 抽象成一整套任何切图工具都适用的流程。这六个阶段是切图类软件的普遍骨架:

阶段 通用职责 本项目的实现 关键原则
① 输入 拿到底图 + 真实尺寸 ImageUploader / FileReader 必须先解码得到宽高
② 配置 收集"怎么切"的参数 ControlPanel → store 配置是后续切割的唯一输入
③ 规划 把尺寸+配置 → 切割矩形 splitImage.ts(纯函数) 做成纯函数,可被多处复用
④ 预览 用同一矩形可视化 CanvasPreview / SlicePreview 预览与最终结果共源
⑤ 裁剪 按矩形真正切出文件 exportSlices.ts(离屏 canvas) 裁剪 = drawImage 源区拷贝
⑥ 输出 下载单文件 / 打包 FileSaver + JSZip 包名体现布局 行列

可迁移的三条设计经验(换其他语言/框架也成立):

  1. 切割数学与渲染解耦:切割算法是纯函数,不依赖 UI —— 前端/后端都能用。
  2. 预览与导出共源:同一个矩形集合,杜绝"预览是一套、导出是另一套"的 bug。
  3. 共享状态放"唯一来源":用一层集中状态(Zustand/Redux/vuex……)统一读写,避免组件各自为政不同步。

6. 边界情况与常见坑

场景 现象 原因 / 对策
图片尺寸除不尽 会留一截空白 Grid 末列/末行吸收余量;Fixed 用 ceil + 钳制
两条线几乎重合 0×0 切片 自定义线用 Set 去重 + w>0&&h>0 护栏
改了行列但画廊没变 显示旧切片 store 忘了 slices: [] 失效,或依赖数组漏了 gridConfig
画廊切多张后卡/内存涨 blob 泄漏 忘了 URL.revokeObjectURL;或预览数量上限(本项目 100)
放大后线条很粗 线宽没随缩放调整 绘制时除以 scale2/scale),保持屏幕上的固定粗细
下载文件名 _0_0.png 只有一张 实际只切了 1 块 检查 rects 数量 / 配置是否被正确传入导出函数
中英文案切换不彻底 部分文案没变 组件硬编码了语言;或 i18n 两套对象没对齐
pnpm build 报类型错但 dev 正常 类型只被 build 检查 tsc -b 做严格全量类型检查,dev 是转译

7. 改代码 / 扩展的入手点

你要做什么 改哪里 备注
改某一种切法 src/utils/splitImage.ts 纯函数,最安全,不碰 UI
新增一种切割模式 类型 + splitImage + store + 三个 switch(mode) + i18n 见上文"四周动手点"
改切分相关状态行为 src/store/useImageStore.ts 必须保留 slices: [] 失效
改画布交互/缩放平移 src/components/CanvasPreview.tsx 新指针交互必须走 toImageCoords
改导出格式/打包 src/utils/exportSlices.ts 注意浏览器格式兼容
改界面/文案/主题 src/components/* + src/i18n/index.ts + src/index.css i18n 两套要同时改
更新双击可用的成品 重新 pnpm build,复制 dist/index.html → 根目录 ImageSplit.html 不要手改成品

构建命令

pnpm install   # 安装(仅 pnpm)
pnpm dev       # 开发
pnpm build     # = tsc -b(类型检查)+ vite build → dist/index.html(单文件)
pnpm lint      # ESLint


附:一次"所见即所得"的完整闭环示例

用户把一张 600×400 的图切成 2×2 网格,然后下载全部 4 张。

  1. 接图ImageUploader 解码 → ImageInfo{width:600, height:400}store.setImage
  2. 配置:用户选 grid 模式,填 rows=2, cols=2 → store.setMode('grid') + setGridConfig({rows:2,cols:2})slices:[] 失效
  3. computeGridSlices(600,400,{2,2}) → 4 个矩形,每个 300×200(整除,无余量需吸收)
  4. 预览:画布画 1 竖 + 1 横切割线;画廊生成 4 张缩略图
  5. :对 4 个矩形 sliceToBlob → 4 个 PNG blob
  6. :4 张 → 打包 → 下载 照片_slices.zip,内含 照片_0_0.png照片_1_1.png

全程图片只存在于浏览器内存与你的磁盘,从不上传。

ImageSplit 切图工具 —— 完整流程与技术原理文档
http://www.clxhxhhr.top/posts/549/
作者
clxstart
发布于
2026-09-08
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。