Skip to content

Commit 85a5cf9

Browse files
fishwww-wwFinleyGe
andauthored
feat: add base64 to image plugin (#194)
* feat: add base64 to image plugin * perf: auto detect the image encoding --------- Co-authored-by: FinleyGe <[email protected]>
1 parent bcdf318 commit 85a5cf9

File tree

7 files changed

+248
-0
lines changed

7 files changed

+248
-0
lines changed

bun.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@
121121
"typescript": "^5.0.0",
122122
},
123123
},
124+
"modules/tool/packages/base64ToImage": {
125+
"name": "@fastgpt-plugins/tool-base64to-image",
126+
"dependencies": {
127+
"zod": "^3.24.2",
128+
},
129+
"devDependencies": {
130+
"@types/bun": "latest",
131+
},
132+
"peerDependencies": {
133+
"typescript": "^5.0.0",
134+
},
135+
},
124136
"modules/tool/packages/blackForestLab": {
125137
"name": "fastgpt-tools-blackForestLab",
126138
"dependencies": {
@@ -646,6 +658,8 @@
646658

647659
"@fastgpt-plugins/tool-ali-model-studio": ["@fastgpt-plugins/tool-ali-model-studio@workspace:modules/tool/packages/aliModelStudio"],
648660

661+
"@fastgpt-plugins/tool-base64to-image": ["@fastgpt-plugins/tool-base64to-image@workspace:modules/tool/packages/base64ToImage"],
662+
649663
"@fastgpt-plugins/tool-link-memo": ["@fastgpt-plugins/tool-link-memo@workspace:modules/tool/packages/linkMemo"],
650664

651665
"@fastgpt-plugins/tool-mineru": ["@fastgpt-plugins/tool-mineru@workspace:modules/tool/packages/mineru"],
@@ -2106,6 +2120,8 @@
21062120

21072121
"@fast-csv/parse/@types/node": ["@types/[email protected]", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
21082122

2123+
"@fastgpt-plugins/tool-base64to-image/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
2124+
21092125
"@fastgpt-plugins/tool-link-memo/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
21102126

21112127
"@fastgpt-plugins/tool-mineru/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
@@ -2270,6 +2286,8 @@
22702286

22712287
"zip-stream/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
22722288

2289+
"@fastgpt-plugins/tool-base64to-image/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
2290+
22732291
"@fastgpt-plugins/tool-link-memo/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
22742292

22752293
"@fastgpt-plugins/tool-mineru/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defineTool } from '@tool/type';
2+
import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt';
3+
import { ToolTypeEnum } from '@tool/type/tool';
4+
5+
export default defineTool({
6+
name: {
7+
'zh-CN': 'Base64 转图片',
8+
en: 'Base64 to Image'
9+
},
10+
type: ToolTypeEnum.tools,
11+
description: {
12+
'zh-CN': '输入 Base64 编码的图片,输出图片可访问链接。',
13+
en: 'Enter a Base64-encoded image and get a directly accessible image link.'
14+
},
15+
toolDescription: 'Enter a Base64-encoded image and get a directly accessible image link.',
16+
versionList: [
17+
{
18+
value: '0.1.0',
19+
description: 'Default version',
20+
inputs: [
21+
{
22+
key: 'base64',
23+
label: 'Base64 字符串',
24+
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
25+
valueType: WorkflowIOValueTypeEnum.string
26+
}
27+
],
28+
outputs: [
29+
{
30+
valueType: WorkflowIOValueTypeEnum.string,
31+
key: 'url',
32+
label: '图片 URL',
33+
description: '可访问的图片地址: http://example.com'
34+
},
35+
{
36+
valueType: WorkflowIOValueTypeEnum.string,
37+
key: 'type',
38+
label: 'MIME 类型',
39+
description: 'MIME 类型'
40+
},
41+
{
42+
valueType: WorkflowIOValueTypeEnum.number,
43+
key: 'size',
44+
label: '图片大小(B)',
45+
description: '图片大小(B)'
46+
}
47+
]
48+
}
49+
]
50+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import config from './config';
2+
import { InputType, OutputType, tool as toolCb } from './src';
3+
import { exportTool } from '@tool/utils/tool';
4+
5+
export default exportTool({
6+
toolCb,
7+
InputType,
8+
OutputType,
9+
config
10+
});
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@fastgpt-plugins/tool-base64to-image",
3+
"module": "index.ts",
4+
"type": "module",
5+
"scripts": {
6+
"build": "bun ../../../../scripts/build.ts"
7+
},
8+
"devDependencies": {
9+
"@types/bun": "latest"
10+
},
11+
"peerDependencies": {
12+
"typescript": "^5.0.0"
13+
},
14+
"dependencies": {
15+
"zod": "^3.24.2"
16+
}
17+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { z } from 'zod';
2+
import { uploadFile } from '@tool/utils/uploadFile';
3+
4+
/**
5+
* Detect image MIME type from base64 binary data by checking file signatures
6+
* Supports JPEG, PNG, GIF, BMP, and WebP formats
7+
*/
8+
function detectImageMimeType(base64Data: string) {
9+
try {
10+
// Remove data URL prefix if exists and decode base64
11+
const base64Content = base64Data.replace(/^data:[^;]+;base64,/, '');
12+
const binaryString = atob(base64Content);
13+
const bytes = new Uint8Array(binaryString.length);
14+
for (let i = 0; i < binaryString.length; i++) {
15+
bytes[i] = binaryString.charCodeAt(i);
16+
}
17+
18+
// Check for common image file signatures
19+
// JPEG: FF D8 FF
20+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
21+
return 'image/jpeg';
22+
}
23+
24+
// PNG: 89 50 4E 47 0D 0A 1A 0A
25+
if (
26+
bytes.length >= 8 &&
27+
bytes[0] === 0x89 &&
28+
bytes[1] === 0x50 &&
29+
bytes[2] === 0x4e &&
30+
bytes[3] === 0x47 &&
31+
bytes[4] === 0x0d &&
32+
bytes[5] === 0x0a &&
33+
bytes[6] === 0x1a &&
34+
bytes[7] === 0x0a
35+
) {
36+
return 'image/png';
37+
}
38+
39+
// GIF: 47 49 46 38 (GIF8)
40+
if (
41+
bytes.length >= 4 &&
42+
bytes[0] === 0x47 &&
43+
bytes[1] === 0x49 &&
44+
bytes[2] === 0x46 &&
45+
bytes[3] === 0x38
46+
) {
47+
return 'image/gif';
48+
}
49+
50+
// BMP: 42 4D
51+
if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
52+
return 'image/bmp';
53+
}
54+
55+
// WebP: RIFF + WEBP
56+
if (
57+
bytes.length >= 12 &&
58+
bytes[0] === 0x52 &&
59+
bytes[1] === 0x49 &&
60+
bytes[2] === 0x46 &&
61+
bytes[3] === 0x46 &&
62+
bytes[8] === 0x57 &&
63+
bytes[9] === 0x45 &&
64+
bytes[10] === 0x42 &&
65+
bytes[11] === 0x50
66+
) {
67+
return 'image/webp';
68+
}
69+
70+
// Default to PNG if no signature matches
71+
return null;
72+
} catch {
73+
// If any error occurs during detection, default to PNG
74+
return null;
75+
}
76+
}
77+
78+
export const InputType = z.object({
79+
base64: z.string().nonempty()
80+
});
81+
82+
export const OutputType = z.object({
83+
url: z.string(),
84+
type: z.string(),
85+
size: z.number()
86+
});
87+
88+
/**
89+
* Convert base64 image data to a file and return its URL, type, and size
90+
* Supports both data URL format (with MIME type) and raw base64 (auto-detected)
91+
*/
92+
export async function tool({
93+
base64
94+
}: z.infer<typeof InputType>): Promise<z.infer<typeof OutputType>> {
95+
// First try to get MIME type from data URL
96+
const mime = (() => {
97+
const match = base64.match(/^data:([^;]+);base64,/);
98+
if (match?.[1]) {
99+
return match[1];
100+
}
101+
const detectedType = detectImageMimeType(base64);
102+
if (!detectedType) {
103+
throw new Error('Image Type unknown');
104+
}
105+
return detectedType;
106+
})();
107+
108+
const ext = (() => {
109+
const m = mime.split('/')[1];
110+
return m && m.length > 0 ? m : 'png';
111+
})();
112+
113+
// Generate filename with appropriate extension
114+
const filename = `image.${ext}`;
115+
116+
const meta = await uploadFile({ base64, defaultFilename: filename });
117+
118+
return {
119+
url: meta.accessUrl,
120+
type: meta.contentType,
121+
size: meta.size
122+
};
123+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expect, test } from 'vitest';
2+
import tool from '..';
3+
4+
test(async () => {
5+
expect(tool.name).toBeDefined();
6+
expect(tool.description).toBeDefined();
7+
expect(tool.cb).toBeDefined();
8+
9+
const v = tool.versionList?.[0];
10+
expect(v).toBeDefined();
11+
const inputKeys = (v?.inputs || []).map((i: any) => i.key);
12+
const outputKeys = (v?.outputs || []).map((o: any) => o.key);
13+
expect(inputKeys).toContain('base64');
14+
expect(outputKeys).toContain('url');
15+
expect(outputKeys).toContain('type');
16+
expect(outputKeys).toContain('size');
17+
});

0 commit comments

Comments
 (0)