
1. 一个拖了十二年的移植执念为什么这次不一样Paint.NET 要上 Linux 这件事老用户应该都不陌生。这是一款从 2004 年就开始迭代的 Windows 图像编辑软件定位介于画图和 Photoshop 之间轻量、启动快、插件生态成熟长期是 Windows 平台上装机量最大的免费修图工具之一。它的技术栈有个很关键的特点界面和渲染层深度绑定Direct2D也就是微软自家的一套 2D 图形加速接口。这个绑定就是它十几年没能跨出 Windows 的根本原因。过去十二年里社区尝试过好几条路。最主流的是走Wine兼容层让 Windows 程序在 Linux 上跑起来。但 Paint.NET 对 Direct2D 的依赖太深Wine 的 D2D 实现长期不完整跑起来要么界面错乱要么直接崩能用但不好用。另一条路是重写可一个成熟软件的 UI 层重写成本极高个人开发者根本扛不住。于是这事就一直卡着成了 Linux 桌面用户心里的一根刺。这次不一样的地方在于有人开始用Claude这类大模型辅助做代码移植。注意不是让 AI 从零写一个替代品而是让它去啃 Paint.NET 的现有代码把 Direct2D 相关的调用逐块翻译成 Linux 上能用的图形接口比如Cairo、Skia或者基于 Vulkan 的渲染层。这个思路的转变很关键以前是重写现在是翻译 适配工作量从造一栋楼变成了改造一栋楼可行性完全不是一个量级。这篇文章我想聊的不是AI 能不能写代码这种泛泛的话题而是具体到 Paint.NET 移植 Linux 这件事上Direct2D 到底难在哪、Wine 路线为什么走了十二年还是别扭、用 Claude 做代码翻译的真实工作流长什么样、中间会踩哪些坑、以及这套方法能不能复制到其他 Windows 软件的移植上。如果你手上也有类似的Windows 老软件想在 Linux 上用的需求这篇应该能给你一套可参考的思路。2. Direct2D 这道墙为什么 Paint.NET 死活离不开 Windows2.1 Direct2D 不是一个库是一整套渲染哲学很多人以为 Direct2D 就是个画图 API替换掉就行。实际不是。Direct2D 是建立在 Direct3D 之上的 2D 渲染层它和 Windows 的显示驱动模型、硬件加速管线、DPI 缩放机制、字体渲染DirectWrite是绑在一起的。Paint.NET 用它做的不只是画几个矩形而是整个画布的实时合成、图层混合、选区抗锯齿、缩放插值全都走 D2D 的硬件加速路径。这意味着什么意味着你没法简单地把 D2D 调用换成 Cairo 调用。因为两者的抽象层级不一样。Cairo 是软件渲染为主的 2D 库虽然也有 GPU 后端但它的合成模型、坐标系处理、混合模式支持和 D2D 不是一一对应的。你翻译一个ID2D1RenderTarget::DrawBitmap背后可能牵扯到 D2D 的图层栈、裁剪区域、变换矩阵、像素格式转换一整套状态机。2.2 十二年里 Wine 路线到底卡在哪Wine 的思路是在 Linux 上模拟 Windows 的 API。它对 Direct2D 的支持是靠wined3d把 D3D 调用转成 OpenGL 或 Vulkan再在上面实现一层 D2D。问题在于D2D 的很多行为是实现定义的微软的文档写得含糊Wine 只能靠逆向和试错去猜。结果就是简单的图形能画复杂的图层合成会出偏差字体渲染和 Windows 下不一致中文尤其明显硬件加速时好时坏某些显卡驱动下直接黑屏插件调用 D2D 高级特性时容易崩我实测过用 Wine 跑 Paint.NET 的几个版本基本结论是打开小图能用一旦图层多了、用了特效滤镜卡顿和渲染错误就上来了。这不是 Wine 不努力而是 D2D 这套东西的隐式契约太多靠兼容层去完美复现成本高到不现实。2.3 为什么翻译源码比模拟 API更靠谱这里有个认知转折。Wine 是在运行时模拟 API 行为它不知道程序的意图只能被动响应调用。而用 Claude 做源码翻译是在编译前理解代码意图把我想画一个带透明度的图层这个意图用 Linux 原生接口重新表达一遍。打个比方Wine 像是一个同声传译你说一句它翻一句遇到方言和俚语就抓瞎源码翻译像是把整本书重新用另一种语言写一遍译者能理解上下文能调整表达方式。前者快但粗糙后者慢但准确。对于 Paint.NET 这种渲染逻辑复杂的软件后者的上限明显更高。3. 用 Claude 啃 Paint.NET 源码真实工作流拆解3.1 第一步不是让 AI 写代码是让它画地图很多人一上来就让 Claude 把这个文件翻译成 Linux 版本结果得到一堆编译不过的代码。正确的第一步是让 AI 帮你梳理依赖关系。Paint.NET 的代码库里D2D 相关调用分布在渲染层、控件层、特效层好几个地方你得先知道哪些是核心、哪些是边缘。我的做法是把关键文件喂给 Claude让它输出一份D2D API 使用清单按调用频率和耦合度排序。比如D2D 接口用途替换难度建议方案ID2D1RenderTarget画布渲染目标高Skia SurfaceID2D1Bitmap位图对象中Skia ImageID2D1Layer图层合成高Skia SaveLayerIDWriteTextLayout文字排版中Pango HarfBuzzID2D1Effect特效滤镜极高自研或 CPU 回退这张表不是让 AI 一次生成的是反复对话、让它读代码后逐步修正出来的。关键是先有地图再动手否则你会在几千个调用点里迷路。3.2 翻译的粒度一个函数一个函数地过地图有了之后进入实际翻译。这里有个经验不要让 AI 一次翻译整个文件而是以函数为单位。原因很简单D2D 的调用往往有状态依赖一个函数里的BeginDraw/EndDraw配对跨函数翻译容易丢状态。具体操作是把单个函数连同它的上下文类定义、成员变量、相关辅助函数一起给 Claude明确告诉它目标接口是 Skia 还是 Cairo让它输出替换后的函数体并附上这里为什么这么改的说明。这个说明很重要因为 AI 有时会给出能编译但语义不对的代码你得靠它的解释来判断。举个例子D2D 里创建一个带透明度的位图笔刷翻译到 Skia 大概是这样的思路// 原 D2D 代码简化 ID2D1BitmapBrush* brush; renderTarget-CreateBitmapBrush(bitmap, brushProps, brush); // 翻译到 Skia示意 sk_spSkShader shader bitmap-makeShader( SkTileMode::kClamp, SkTileMode::kClamp); SkPaint paint; paint.setShader(shader); paint.setAlphaf(opacity);注意setAlphaf这一步D2D 的透明度可能在 brush 属性里也可能在 render target 的全局 alpha 里翻译时必须确认原始语义不能想当然。3.3 编译错误的处理把报错原样喂回去翻译出来的代码第一次编译报错是必然的。这时候别自己硬啃把完整的编译错误信息 相关代码片段一起丢给 Claude让它分析。实测下来AI 对编译错误的定位能力相当强尤其是类型不匹配、头文件缺失、命名空间这类问题基本一两轮就能修好。但有个坑要注意AI 有时会为了消除报错而改错逻辑。比如某个类型转换报错它可能直接加个强制转换把错误压下去而不是去查为什么类型不对。所以每次它改完你都要问一句这个修改会不会改变运行时行为逼它解释。3.4 一个真实的翻译节奏参考我按自己的节奏给个参考不是标准答案但能让你对工作量有个概念第 1 周梳理依赖产出 API 映射表确定目标图形栈第 2-3 周翻译核心渲染层跑通打开图片 显示这条最小链路第 4-6 周翻译图层、选区、基础绘图工具第 7 周起处理特效滤镜、插件接口、字体渲染细节这个节奏的前提是你对 C 和图形编程有基本了解。如果完全不懂图形光靠 AI 喂代码遇到渲染结果不对的时候你连从哪查都不知道。4. 那些 AI 不会主动告诉你的坑4.1 颜色空间和像素格式的隐性差异D2D 默认工作在premultiplied alpha的 RGBA 空间而很多 Linux 图形库默认是 straight alpha。这个差异在简单绘图时看不出来一旦做图层混合、半透明叠加颜色就会偏。我踩过这个坑一个半透明红色图层叠在蓝色背景上Windows 下是紫色移植后变成了偏灰的紫。解决办法是在数据进出渲染层时统一做 alpha 预乘/反预乘转换。这个转换有性能开销所以要在管线设计时就规划好别等到最后发现颜色不对再回头改。4.2 字体渲染中文是最难的那一关DirectWrite 的字体渲染和 Linux 上的 FreeType Fontconfig 是两套体系。英文可能看着差不多中文的 hinting、字重、行距差异会非常明显。Paint.NET 里文字工具是核心功能这块不能糊弄。我的建议是不要试图复刻DirectWrite 的效果而是接受 Linux 下的渲染风格把精力放在排版逻辑的正确性上换行位置、对齐方式、字距调整这些要和原来一致。视觉上的细微差异用户其实能接受但排版错乱是绝对不能忍的。4.3 插件的二进制兼容是个死结Paint.NET 有大量第三方插件很多是编译好的 DLL直接调用 D2D 接口。这部分没法用源码翻译解决因为源码不在你手里。现实的做法是核心功能自己移植插件生态要么等社区跟进要么提供一个兼容层。这一点必须提前跟用户说清楚别让人以为移植完就万事大吉。4.4 AI 会幻觉出不存在的 API这是最需要警惕的。Claude 有时会自信地写出一个看起来合理、实际不存在的函数签名尤其是 Skia 和 Cairo 这种 API 面很广的库。防范方法很简单每个 AI 给出的 API 调用都去官方文档或头文件里核对一遍。别嫌麻烦这一步能省掉你后面几小时的调试。提示让 AI 翻译代码时明确要求它如果不确定某个 API 是否存在标注出来让我核实比让它硬编一个答案要好得多。5. 这套方法能复制到别的软件上吗5.1 什么样的软件适合AI 辅助移植不是所有 Windows 软件都值得这么干。我总结了几条判断标准渲染层相对独立像 Paint.NET 这样图形调用集中在一个模块翻译边界清晰源码可得闭源软件没法翻译只能走 WineUI 框架不是死结如果软件深度绑定 WPF 或 WinUI那 UI 层本身就是个大工程有持续维护的价值移植一次要能长期跟进上游更新否则很快过时按这几条筛下来其实适合的软件不多。Paint.NET 算是比较典型的值得一试的案例。5.2 和 Wine 路线不是二选一这里要澄清一个误区源码移植和 Wine 兼容不是互斥的。实际项目里常见的是混合策略核心渲染走原生移植边缘功能比如某个小工具窗口、某个文件格式解析继续用 Wine 跑。这样能把工作量控制在可接受范围内又不牺牲太多功能。5.3 移植之后谁来维护这是最容易被忽略的问题。AI 帮你翻译完第一版不代表事情结束了。上游 Paint.NET 每次更新你都得把新代码再翻译一遍。如果上游改动频繁维护成本会很高。所以移植项目最好能建立一个自动化流程把翻译规则、API 映射表沉淀成文档和脚本下次更新时能半自动处理而不是每次从头来。6. 给想动手的人的几条实操建议如果你看完想自己试试不管是 Paint.NET 还是别的软件这几条是我踩过坑之后觉得最该先知道的。第一先跑通最小链路再铺开。别一上来就翻译整个渲染层先做到能打开一张图并显示出来这条链路通了后面的工作才有参照。最小链路里会暴露大部分架构性问题早发现早调整。第二给 AI 的上下文要精准。Claude 的上下文窗口虽然大但塞太多无关代码反而会干扰它。每次只给它当前函数 直接相关的定义效果比丢一整个文件好得多。第三建立自己的验证清单。每翻译完一个模块用固定的测试用例跑一遍打开特定图片、执行特定操作、对比输出结果。图形编程的 bug 很隐蔽没有系统化验证你根本不知道哪里错了。第四别追求 100% 还原。移植的目标是在 Linux 上能用、好用不是和 Windows 版像素级一致。有些平台差异是正常的把精力花在核心体验上比死磕细节划算。第五文档和映射表要边做边记。AI 翻译的过程会产生大量这个 API 对应那个 API的知识这些如果不记下来下次遇到同样的调用还得重新问一遍。我习惯用一个 Markdown 文件维护映射表越用越顺手。最后说个我自己的判断Paint.NET 移植 Linux 这件事十二年的卡点不在于技术难度本身而在于重写成本和兼容层精度这两条路都走不通。AI 辅助源码翻译打开的是第三条路它的价值不在于 AI 多聪明而在于它把理解代码意图这件事的成本降下来了。这个思路对很多困在 Windows 生态里的老软件来说可能比 Paint.NET 本身更有意义。