插件功能说明

ASCII / 像素艺术生成器是一款轻量化图片转换插件,核心支持将任意上传图片快速解析、转换为标准ASCII字符画字符串或复古风格像素艺术图像。

  • ASCII字符串画:支持 ASCII字符串画,适配字符串艺术照使用场景

  • 高清还原转换:智能识别原图色彩、明暗、光影、轮廓细节,通过字符灰度、像素色块精准替代,最大程度还原原图画面结构

  • 自定义参数调节:支持调整输出尺寸、字符密度、像素精度、明暗对比度、色彩模式等参数,灵活定制生成效果

  • 多格式兼容:支持主流图片格式上传,包含 JPG、PNG、BMP 等,适配静态图片全场景转换

  • 极速生成导出:轻量化运算逻辑,秒级完成转换

代码组织规定

正所谓学习的第一步就是模仿,而这也是我们的第一个自己实现的dsh插件。所以有必要借鉴一下dsh官方是怎么组织插件的代码。

我们以packages\plan 为例,来分析一下dsh插件的代码组织规定。

plan-mode-代码组织图.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
packages/<group>/<pkg>/                ← 例如 packages/todo/tool-todo/
├── package.json
├── tsconfig.json
├── README.md ← 英文
├── README.zh.md ← 中文(双语对齐)
├── README.i18n.yaml ← 双语行对齐元数据
├── src/
│ ├── index.ts ← 唯一主入口:name/inject/Config/apply
│ ├── types.ts ← 纯类型 + declare module 声明合并,无运行时代码
│
└── tests/ ← 包级 tests/(不是 src/__tests__/)
├── tool-todo.spec.ts ← 单元/行为测试
├── loader-composition.spec.ts ← 加载组合测试
├── projection.spec.ts
└── integration.spec.ts ← REAL-composition 非单元测试


因为是自己实现一个工具类的插件,如果不是想要发布给大家用的话,其中说明性以及测试类的代码是可选项(如需发布,请务必写好说明和测试,确保插件的稳定性和可靠性)

去繁化简之后,我们只需要关注的只有两个东西,分别就是src下面的index以及type。

关键实现要点

1. 图片处理

对一张图片进行转换的前提是先获取图片的基本信息,以及像素数据。而JIMP这个库可以很好的帮助我们实现这个功能。Jimp 是一个零依赖、纯 JavaScript 的图像处理库,支持读取、编辑、合成、滤镜、文字等丰富功能,适用于 Node.js 与浏览器环境。

在我们的插件目录下执行:

1
2
pnpm install jimp

读取图片

1
2
3
4
5
6
7
8
9
10
11
12
import Jimp from 'jimp';

// 从本地文件读取
const image = await Jimp.read('input.jpg');

// 从 URL 读取
const image2 = await Jimp.read('https://example.com/a.png');

// 从 Buffer 读取
const image3 = await Jimp.read(buffer);


对照片做灰度处理,得到图片的灰度像素值。

1
2
3
4
5
6
// 对图片进行灰度处理
const grayImage = image.greyscale()
// 通过bitmap得到每个像素的rgba信息
const { data, width, height } = grayImage.bitmap


2. 灰度转换

图片是由像素依次排列组成的,每一个像素都是用rgba来描述。所以grayImage的data每四位表示一个像素的情况,由于灰度了,所以rgb这三通道的像素是一致的,a通道在该应用中不涉及。

因此通过每四个采点,即可得到该像素应该的像素值。字符串画需要凸显明暗的对比,从而还原画作。所以我们需要建立一个灰度值映射表,将灰度值映射到字符上。原理是找到字符越是密集,所对应的灰度值则越小,字符视觉上就越暗。

取灰度值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 04 根据每个像素的灰度值映射字符表:CharacterGrayLib 前密后疏,
// 暗像素(gray 小)用靠前的密字符,亮像素用靠后的稀疏字符
// density 为采样步长:每隔 step 个像素取一个点,降低字符密度
// lightDarkDensity 为明暗指数:<1 整体调亮(更多像素映射到稀疏字符),>1 调暗
const { data, width, height } = grayImage.bitmap
const step = Math.max(1, Math.round(param.density))
const lines: string[] = []
for (let y = 0; y < height; y += step) {
let row = ''
for (let x = 0; x < width; x += step) {
const gray = data[(y * width + x) * 4] || 0
const charIndex = Math.round(((gray / 255) ** param.lightDarkDensity) * (CharacterGrayLen - 1))
row += CharacterGrayLib[charIndex]
}
lines.push(row)
}


映射表

1
2
3
4

// 字符灰度表 每个字符的灰度呈现递减
export const CharacterGrayLib : string = '@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^`\''

3.输出

将结果使用txt存储起来,方便后续查看。由于要和dsh进行互动,我们不可以使用node自带的fs模块。因为dsh使用的过程中有可能在sandbox,或者其他我们未知的环境下,所以我们需要与dsh官方写好的fs模型进行交互,该模块隐藏了这些细节以及差异,实现了文件读写的功能。 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs'

export interface ImageDeps {
fs: FileSystem
signal?: AbortSignal
}

const ascii = lines.join('\n')
const outputTarget = await deps.fs.resolve(param.outputPath)
await deps.fs.writeText(outputTarget, ascii, undefined, deps.signal)



3. 连接dsh

  1. 加入pnpm-workspace.yaml

加入yaml.png

  1. 编写cords.yml,注入dsh的cords配置

注入cords.png

使用起来

编写完毕,通过 pnpm dsh web --patch ./my-plugin-to-do/cords.yml 启动插件。和dsh对话,尝试调用。

检查插件是否存在.png

插件调用测试.png

测试图片预览.png

测试结果预览.png

发布注意事项

写cords.yml的注入是通过绝对路径的,所以要是换了台机器或者目录结构发生改变都不行。这是不对的。并且根据dsh官方doc的publish指南,我们不应该预设用户的所有操作环境或者环境变量,所以需要通过打包成mjs或者zip,保证服务与环境和变量的无关性。

优化写法以及注入

一个可安装的插件是声明了dsh.bundle 的 npm 包,包含两部分.

  • package.json 里声明"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
  • 一个cordis.patch.yml 配置层文件。

将cordis.yml的绝对路径改掉

cordis.png

package.json对齐包名

package.json.png

补齐dsh字段,让dsh能够解析以及发现,

package.json.png

打包发布

登录自己的npm账号(去npm官方注册)

1
pnpm login

补上prepack,发布的时候自动走一遍构建

1
2
3
// package.json script
"prepack": "pnpm run build"

发布

1
pnpm publish --access=public --no-git-checks

在插件目录下执行 pnpm run build

验证

安装

1
2
3
4

pnpm run dsh plugin --profile web add @lgh_ccc/ascii-gen


安装截图.png

验证.png

总结

经过这次实践,我们成功的开发了一个纯作为工具调用的dsh插件。并且将他打包进行发布,使得别人也可以通过安装之后使用我们的插件。