jphotoshop:用 Java Swing 手写一个 Photoshop——架构与算法全解析
- 约 11 分钟读完 - 全文 4274 字jphotoshop:用 Java Swing 手写一个 Photoshop——架构与算法全解析
把多年前的一个练手项目整理开源了:jphotoshop,一个用纯 Java Swing 手写的 Photoshop 模拟器。不写不知道,写一个"简化版 PS"几乎把计算机图形学、数据结构和设计模式都过了一遍,这篇文章既是功能介绍,也是一次技术复盘。
- 开源地址:https://gitee.com/soulnew/jphotoshop
- 技术栈:Java + Swing(Swing Application Framework / JSR-296),NetBeans Ant 工程
- 规模:197 个 Java 文件,约 1.8 万行(自研约 1.4 万行),开发周期约半年
一、功能概览:一个"简化版 PS"能做什么
UI 形态上和老版 PS 一致:MDI 多文档界面——每张图片一个内部编辑窗口,图层面板、导航缩放面板都是可拖动的浮动内窗。
1.1 图层系统
- 多图层编辑:图像图层、文字图层、浮动选区层(拖动选区内容时的临时层);
- 图层面板带 40×30 实时缩略图,支持拖拽换序(基于 Swing TransferHandler 的 DnD)、复制粘贴为新图层;
- 透明区域用经典的灰白棋盘格打底。
1.2 选区工具(7 个)
| 工具 | 说明 |
|---|---|
| 魔棒 | 容差可调(默认 23),支持多连通区域一次选中 |
| 色彩范围 | 全图相似色选取 |
| 多边形套索 | 逐点勾勒 |
| 自由套索 | 鼠标轨迹勾勒 |
| 磁性套索 | 局部窗口内吸附边缘(完成度不高) |
| 贝塞尔选区 | 双击闭合时自动做曲线拟合,生成平滑选区 |
| 移动工具 | 把选区内容提成浮动层,拖动实时合成,松手落回底层 |
选区还支持:羽化(可调半径)、反选、平移/缩放(AffineTransform)、以及标志性的蚂蚁线动画。
1.3 滤镜(24 个,分 5 大类)
- 模糊:均值平滑、高斯模糊(3×3 高斯核
{1,2,1;2,4,2;1,2,1}/16)、中值滤波; - 锐化:Sharpen / SharpenMore(拉普拉斯卷积核
{0,-1,0;-1,5,-1;0,-1,0}); - 艺术:曝光过度、冰晶、熔岩、暗调(平方衰减)、漫画等 8 种风格化效果;
- 简单:反色、怀旧(sepia 矩阵
0.393/0.769/0.189...)、灰度; - 扭曲:挤压、球面化、正/反旋转、正弦波浪、摩尔纹——扭曲类全部带多段滑块实时预览,取消可还原。
1.4 图像调整与文字
- HSB 调整:先把整图 RGB 转成 HSB 三维 float 数组,色相/饱和度/亮度三个滑块实时调节再转回 RGB;
- 对比度调整;
- 文字工具:点击图片任意位置就地出现透明输入框 + 字体选择面板,完成后生成为独立的文字图层。
1.5 文件与工程
- 打开 jpg / png / gif / bmp;
- 自定义
.jps工程格式(类似 PSD):整个图层模型实现Serializable,用ObjectOutputStream套GZIPOutputStream序列化保存,可以随时回来继续编辑; - 复制 / 剪切 / 粘贴、导航缩放窗口、中文界面。
二、架构总览
graph TD
A["JPhotoShop 入口<br/>EDT 中构建主窗口"] --> B["JDesktopPane MDI 主框架"]
B --> C["EditFrame 编辑窗口<br/>每张图一个"]
B --> D["LayerControl 图层面板"]
B --> E["ViewSizeFrame 导航/缩放"]
C --> F["EditModel<br/>像素模型 + MemoryImageSource 渲染"]
F --> G["ImageModel<br/>LinkedList 图层栈"]
G --> H["ImagePaintLayer / TextPaintLayer / TempImageLayer"]
H --> I["LayerPlug 混合样式<br/>Alpha 合成"]
C --> J["ToolManager<br/>工具代理/状态切换"]
J --> K["Tool 抽象类<br/>魔棒/套索/贝塞尔/移动/文字..."]
C --> L["FilterManager<br/>properties + 反射装配"]
L --> M["PicFilter 滤镜即 Action"]
M --> N["Math 包<br/>CoverOp 卷积 / FeatherSide 羽化<br/>FindSide 轮廓追踪 / PointConnection 并查集"]
整体分四层:UI 层(MDI 窗口与面板)、模型层(图层栈与像素数据)、工具/滤镜层(操作入口)、算法层(jphotoshop.Math 包)。工具和滤镜都通过 properties 配置 + 反射装配,下面详细说。
三、技术亮点
3.1 自绘像素管线:int[][] + MemoryImageSource
核心存储没有用 BufferedImage,而是直接操作 int[][] ARGB 二维数组——所有算法(卷积、混合、羽化)都在裸数组上做,直观且没有额外封装开销。
显示侧用了一个当年很典型的 Swing 高性能技巧:MemoryImageSource + setAnimated(true),像素变化后调 newPixels() 增量刷新屏幕图像,避免每次重绘都重建整个 Image 对象。编辑、拖选区、调滤镜滑块时的实时反馈都靠它撑着。
3.2 链式图层合成缓存
多图层合成是最频繁的操作,朴素实现是每次重绘把所有层从头合一遍。这里的做法是给每个图层挂一个"本层与下面所有层合成结果的缓存 cacheData",层与层之间用 backLayer 引用连成链:
graph LR
BG["背景层<br/>棋盘格"] -->|"合成"| L1["图层1<br/>cacheData"]
L1 -->|"合成"| L2["图层2<br/>cacheData"]
L2 -->|"合成"| L3["图层3<br/>cacheData"]
L3 --> OUT["最终显示图像"]
style L2 fill:#f96
L2 -. "仅从此层向上重建" .-> L3
某一层像素变化时,只从该层向上依次 reBuild,下面的层缓存不动。层数多、图大时,这个收益非常明显。
3.3 手写 Alpha 混合
AlphaLayerStyle 实现了逐像素的经典混合公式 C = α·Cf + (1-α)·Cb,并且处理了任意偏移(含负坐标裁剪)——浮动选区拖到画布边缘外也能正确合成。羽化后的选区掩码是 short 型 alpha 权重,抠图粘贴时按权重衰减,边缘不会出现生硬的锯齿。
3.4 魔棒:一条完整的算法链
魔棒是整个项目里算法含量最高的功能,完整链路分三步:
graph TD
A["用户点击图片某点"] --> B["Flood Fill 泛洪填充<br/>8邻域 + 容差判断<br/>ΔR+ΔG+ΔB < range"]
B --> C["PointConnection 并查集<br/>id 数组 + sz 数组按秩合并<br/>分离多个互不连通的区域"]
C --> D["FindSide 8方向轮廓追踪<br/>沿区域边界走一圈<br/>每3步抽稀路径点"]
D --> E["GeneralPath 选区路径"]
E --> F["蚂蚁线动画<br/>Timer 500ms 切换两条相位差3的虚线"]
几个细节:
- 泛洪填充用
LinkedList当队列,容差判断用曼哈顿距离(|ΔR|+|ΔG|+|ΔB|),比欧氏距离省开方; - 一次点击可能选中多个互不连通的区域(比如图片里分散的几块蓝天),用并查集把连通域分组,每个域独立生成轮廓;
- 轮廓追踪把面状的 boolean 掩码转成线状的路径点列,这一步直接决定蚂蚁线的平滑程度;
- 蚂蚁线动画是两条相位差为 3 的虚线
BasicStroke用 500ms 的Timer交替绘制——朴素但效果完全对味。
3.5 通用卷积引擎 CoverOp
所有卷积类滤镜(模糊、锐化背后是同一个引擎 CoverOp,提供三种重载:
- 全图卷积;
- 掩码限定区域卷积——滤镜只在选区内生效,这是 PS 的基本体验;
- short alpha 掩码卷积——配合羽化选区做渐变处理。
RGB 三通道分离卷积 + 边界 clamp,滤镜类只需要给一个卷积核就完事。新增一个模糊变体的成本是十几行代码。
3.6 贝塞尔选区:Schneider 曲线拟合
自由手绘的选区轨迹是抖动的折线,直接当选区很难看。贝塞尔选区工具在双击闭合时调用 JHotDraw 的 Bezier.fitCurve(Schneider 的经典拟合算法),把鼠标点列拟合成平滑的三次贝塞尔曲线段——这就是 PS 里"钢笔工具"的底层原理。
3.7 声明式装配:properties + 反射的原始"插件化"
工具和滤镜都不写死在代码里,而是从 properties 读类名、Class.forName 反射实例化:
# resource/filters.properties
filters.Kinds=模糊,锐化,艺术效果,简单,扭曲
模糊=Smooth,GaussBlur,Median加一个新滤镜 = 写一个类 + 改一行配置,菜单自动按分类分组生成。工具栏同理(ToolManager.initTool)。项目里还专门有一个 plug 包在探索真正的外部插件机制(虽然只是个雏形)。在 2007 年没有 OSGi、没有 IoC 框架依赖的纯 Swing 工程里,这是最务实的可扩展方案。
3.8 工具架构:Proxy + State 的混合
Tool 抽象类把底层 MouseEvent 翻译成语义化回调(click / pressed / dragged / release / rightClick / clickDouble / move),子类只关心语义。ToolManager 本身也是 Tool 的子类,作为代理把事件转发给当前激活的工具——选中哪个工具,事件流就切到哪个状态机上。每个工具还可以自带 getPlugPanel() 参数面板,选中时挂到选项栏上,和 PS 的交互完全一致。
3.9 扭曲滤镜的"预览-取消"模式
扭曲类滤镜(球面、旋转、波浪……)都是坐标重映射:对每个目标像素做逆极坐标变换,找它在原图的取样点。为了支持滑块实时预览又能取消还原,ContortFilter 在打开对话框时先备份 tempData,滑块每次变化都在备份基础上重算,点取消直接把备份拷回——一个廉价的 Memento 模式。配套的 MultiSlider 多段滑块控件也是自绘的。
3.10 滤镜即 Action
PicFilter extends AbstractAction,滤镜本身就是可以挂进菜单/工具栏的 Action:actionPerformed 里自动取当前窗口、当前图层、当前选区掩码,还处理了选区坐标系到图层坐标系的换算(start -= layerStart),执行完触发 rePaintEdit 刷新。框架把脏活都干了,滤镜作者只需要关注像素算法。
四、诚实的复盘:留下了什么遗憾
开源的代码就是当年的样子,包括未完成的部分,如实列出:
- 撤销/重做是空架子:
UndoRedoManager的骨架搭好了,但propertyChange三个分支全是空的——这是当时没啃下来的硬骨头,现在看应该用 Command 模式重做,而不是当初设想的 property 监听方案; - 历史面板、通道面板、外部插件机制都只有雏形;
- 磁性套索完成度不高;
- 若干小 bug,比如
FindSide里找最近点写成了找最远点(if (temp > MinLength)应为<)、"undo".endsWith(...)应为startsWith、几处类名 typo(WaveFiler、getSize拼写)——权当留给读者的"考古彩蛋"。
五、写在最后
写 jphotoshop 的半年收获远超预期:并查集、轮廓追踪、卷积、贝塞尔拟合这些算法不再是教科书上的名词,而是要亲手调到能跑的东西;合成缓存、声明式装配、Action 抽象这些设计,也都是被真实需求逼出来的。如果你也想扎实练一次"算法 + 工程"的综合功,手写一个图像编辑器是我能想到的最好题目之一。
源码在 https://gitee.com/soulnew/jphotoshop,欢迎 Star、Fork,更欢迎把那个空着的 UndoRedoManager 补上。