koishi-plugin-img-tool 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +7 -0
- package/lib/index.js +239 -0
- package/package.json +1 -1
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Context, Schema } from "koishi";
|
|
2
|
+
export declare const name = "img-tool";
|
|
3
|
+
export declare const using: readonly ["canvas"];
|
|
4
|
+
export interface Config {
|
|
5
|
+
}
|
|
6
|
+
export declare const Config: Schema<Config>;
|
|
7
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Config: () => Config,
|
|
34
|
+
apply: () => apply,
|
|
35
|
+
name: () => name,
|
|
36
|
+
using: () => using
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
var import_koishi = require("koishi");
|
|
40
|
+
var import_gif_encoder_2 = __toESM(require("gif-encoder-2"));
|
|
41
|
+
var import_gifuct_js = require("gifuct-js");
|
|
42
|
+
var name = "img-tool";
|
|
43
|
+
var using = ["canvas"];
|
|
44
|
+
var Config = import_koishi.Schema.object({});
|
|
45
|
+
function selectFirstImageElement(content) {
|
|
46
|
+
if (!content) return;
|
|
47
|
+
const nodes = import_koishi.h.select(content.elements, "img");
|
|
48
|
+
return nodes?.[0];
|
|
49
|
+
}
|
|
50
|
+
__name(selectFirstImageElement, "selectFirstImageElement");
|
|
51
|
+
function getImageSrc(el) {
|
|
52
|
+
const src = el?.attrs?.src ?? el?.attrs?.url;
|
|
53
|
+
return typeof src === "string" && src.length ? src : void 0;
|
|
54
|
+
}
|
|
55
|
+
__name(getImageSrc, "getImageSrc");
|
|
56
|
+
function findImageSrc(session) {
|
|
57
|
+
const direct = selectFirstImageElement(session.event.message);
|
|
58
|
+
const directSrc = getImageSrc(direct);
|
|
59
|
+
if (directSrc) return directSrc;
|
|
60
|
+
const quote = session.quote;
|
|
61
|
+
const quoteEl = selectFirstImageElement(quote);
|
|
62
|
+
const quoteSrc = getImageSrc(quoteEl);
|
|
63
|
+
if (quoteSrc) return quoteSrc;
|
|
64
|
+
const quoteElements = quote?.elements;
|
|
65
|
+
if (quoteElements) {
|
|
66
|
+
const quoteEl2 = import_koishi.h.select(quoteElements, "img")?.[0];
|
|
67
|
+
const quoteSrc2 = getImageSrc(quoteEl2);
|
|
68
|
+
if (quoteSrc2) return quoteSrc2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
__name(findImageSrc, "findImageSrc");
|
|
72
|
+
function isGif(buffer) {
|
|
73
|
+
if (buffer.length < 6) return false;
|
|
74
|
+
const head = buffer.subarray(0, 6).toString("ascii");
|
|
75
|
+
return head === "GIF87a" || head === "GIF89a";
|
|
76
|
+
}
|
|
77
|
+
__name(isGif, "isGif");
|
|
78
|
+
function toArrayBuffer(buffer) {
|
|
79
|
+
return buffer.buffer.slice(
|
|
80
|
+
buffer.byteOffset,
|
|
81
|
+
buffer.byteOffset + buffer.byteLength
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
__name(toArrayBuffer, "toArrayBuffer");
|
|
85
|
+
function mirrorPixels(src, width, height, mode) {
|
|
86
|
+
const dst = new Uint8ClampedArray(src.length);
|
|
87
|
+
const ceilHalfW = Math.ceil(width / 2);
|
|
88
|
+
const floorHalfW = Math.floor(width / 2);
|
|
89
|
+
const ceilHalfH = Math.ceil(height / 2);
|
|
90
|
+
const floorHalfH = Math.floor(height / 2);
|
|
91
|
+
for (let y = 0; y < height; y++) {
|
|
92
|
+
for (let x = 0; x < width; x++) {
|
|
93
|
+
let sx = x;
|
|
94
|
+
let sy = y;
|
|
95
|
+
if (mode === "left") {
|
|
96
|
+
sx = x < ceilHalfW ? x : width - 1 - x;
|
|
97
|
+
} else if (mode === "right") {
|
|
98
|
+
sx = x < floorHalfW ? width - 1 - x : x;
|
|
99
|
+
} else if (mode === "up") {
|
|
100
|
+
sy = y < ceilHalfH ? y : height - 1 - y;
|
|
101
|
+
} else {
|
|
102
|
+
sy = y < floorHalfH ? height - 1 - y : y;
|
|
103
|
+
}
|
|
104
|
+
const si = (sy * width + sx) * 4;
|
|
105
|
+
const di = (y * width + x) * 4;
|
|
106
|
+
dst[di] = src[si];
|
|
107
|
+
dst[di + 1] = src[si + 1];
|
|
108
|
+
dst[di + 2] = src[si + 2];
|
|
109
|
+
dst[di + 3] = src[si + 3];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dst;
|
|
113
|
+
}
|
|
114
|
+
__name(mirrorPixels, "mirrorPixels");
|
|
115
|
+
async function mirrorStaticImage(canvas, buffer, mode) {
|
|
116
|
+
let img;
|
|
117
|
+
let srcCanvas;
|
|
118
|
+
let outCanvas;
|
|
119
|
+
try {
|
|
120
|
+
img = await canvas.loadImage(buffer);
|
|
121
|
+
const w = img.naturalWidth ?? img.width;
|
|
122
|
+
const h2 = img.naturalHeight ?? img.height;
|
|
123
|
+
srcCanvas = await canvas.createCanvas(w, h2);
|
|
124
|
+
const src = srcCanvas.getContext("2d");
|
|
125
|
+
src.drawImage(img, 0, 0, w, h2);
|
|
126
|
+
outCanvas = await canvas.createCanvas(w, h2);
|
|
127
|
+
const out = outCanvas.getContext("2d");
|
|
128
|
+
const srcImageData = src.getImageData(0, 0, w, h2);
|
|
129
|
+
const outImageData = out.createImageData(w, h2);
|
|
130
|
+
outImageData.data.set(mirrorPixels(srcImageData.data, w, h2, mode));
|
|
131
|
+
out.putImageData(outImageData, 0, 0);
|
|
132
|
+
return {
|
|
133
|
+
buffer: await outCanvas.toBuffer("image/png"),
|
|
134
|
+
mime: "image/png"
|
|
135
|
+
};
|
|
136
|
+
} finally {
|
|
137
|
+
await Promise.allSettled([
|
|
138
|
+
img?.dispose?.(),
|
|
139
|
+
srcCanvas?.dispose?.(),
|
|
140
|
+
outCanvas?.dispose?.()
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
__name(mirrorStaticImage, "mirrorStaticImage");
|
|
145
|
+
async function mirrorGifImage(canvas, buffer, mode) {
|
|
146
|
+
const gif = (0, import_gifuct_js.parseGIF)(toArrayBuffer(buffer));
|
|
147
|
+
const frames = (0, import_gifuct_js.decompressFrames)(gif, true);
|
|
148
|
+
if (!frames.length) throw new Error("GIF has no frames");
|
|
149
|
+
const width = gif.lsd?.width ?? frames[0].dims?.width;
|
|
150
|
+
const height = gif.lsd?.height ?? frames[0].dims?.height;
|
|
151
|
+
if (!width || !height) throw new Error("Unable to determine GIF size");
|
|
152
|
+
const screenCanvas = await canvas.createCanvas(width, height);
|
|
153
|
+
const screen = screenCanvas.getContext("2d");
|
|
154
|
+
const outCanvas = await canvas.createCanvas(width, height);
|
|
155
|
+
const out = outCanvas.getContext("2d");
|
|
156
|
+
try {
|
|
157
|
+
const encoder = new import_gif_encoder_2.default(width, height);
|
|
158
|
+
encoder.start();
|
|
159
|
+
encoder.setRepeat(0);
|
|
160
|
+
encoder.setQuality?.(10);
|
|
161
|
+
for (const frame of frames) {
|
|
162
|
+
const patch = frame.patch;
|
|
163
|
+
const dims = frame.dims || {};
|
|
164
|
+
const left = dims.left ?? 0;
|
|
165
|
+
const top = dims.top ?? 0;
|
|
166
|
+
const pw = dims.width ?? width;
|
|
167
|
+
const ph = dims.height ?? height;
|
|
168
|
+
if (!patch || patch.length !== pw * ph * 4) {
|
|
169
|
+
throw new Error("Unsupported GIF frame patch format");
|
|
170
|
+
}
|
|
171
|
+
const disposalType = frame.disposalType ?? 0;
|
|
172
|
+
const restore = disposalType === 3 ? screen.getImageData(0, 0, width, height) : null;
|
|
173
|
+
const region = screen.getImageData(left, top, pw, ph);
|
|
174
|
+
for (let i = 0; i < patch.length; i += 4) {
|
|
175
|
+
if (patch[i + 3] === 0) continue;
|
|
176
|
+
region.data[i] = patch[i];
|
|
177
|
+
region.data[i + 1] = patch[i + 1];
|
|
178
|
+
region.data[i + 2] = patch[i + 2];
|
|
179
|
+
region.data[i + 3] = patch[i + 3];
|
|
180
|
+
}
|
|
181
|
+
screen.putImageData(region, left, top);
|
|
182
|
+
const screenData = screen.getImageData(0, 0, width, height);
|
|
183
|
+
const outData = out.createImageData(width, height);
|
|
184
|
+
outData.data.set(mirrorPixels(screenData.data, width, height, mode));
|
|
185
|
+
out.putImageData(outData, 0, 0);
|
|
186
|
+
const delayMs = typeof frame.delay === "number" ? frame.delay : 100;
|
|
187
|
+
encoder.setDelay(Math.max(20, delayMs));
|
|
188
|
+
encoder.addFrame(out);
|
|
189
|
+
if (disposalType === 2) {
|
|
190
|
+
screen.clearRect(left, top, pw, ph);
|
|
191
|
+
} else if (disposalType === 3 && restore) {
|
|
192
|
+
screen.putImageData(restore, 0, 0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
encoder.finish();
|
|
196
|
+
const outBuffer = Buffer.from(encoder.out.getData());
|
|
197
|
+
return { buffer: outBuffer, mime: "image/gif" };
|
|
198
|
+
} finally {
|
|
199
|
+
await Promise.allSettled([screenCanvas.dispose?.(), outCanvas.dispose?.()]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
__name(mirrorGifImage, "mirrorGifImage");
|
|
203
|
+
async function handleMirror(ctx, session, mode) {
|
|
204
|
+
const src = findImageSrc(session);
|
|
205
|
+
if (!src) return "未找到图片:请在消息中附带图片,或引用一条包含图片的消息。";
|
|
206
|
+
const canvas = ctx.canvas;
|
|
207
|
+
if (!canvas?.createCanvas || !canvas?.loadImage) {
|
|
208
|
+
return "未检测到 canvas 服务:请安装并启用 Koishi 的 canvas 服务插件后再使用本功能。";
|
|
209
|
+
}
|
|
210
|
+
const arrayBuffer = await ctx.http.get(src, {
|
|
211
|
+
responseType: "arraybuffer"
|
|
212
|
+
});
|
|
213
|
+
const input = Buffer.from(arrayBuffer);
|
|
214
|
+
const result = isGif(input) ? await mirrorGifImage(canvas, input, mode) : await mirrorStaticImage(canvas, input, mode);
|
|
215
|
+
return import_koishi.h.img(result.buffer, result.mime);
|
|
216
|
+
}
|
|
217
|
+
__name(handleMirror, "handleMirror");
|
|
218
|
+
function apply(ctx, config) {
|
|
219
|
+
ctx.command("左对称", "以左侧为基准生成左右对称图片").action(async ({ session }) => {
|
|
220
|
+
return handleMirror(ctx, session, "left");
|
|
221
|
+
});
|
|
222
|
+
ctx.command("右对称", "以右侧为基准生成左右对称图片").action(async ({ session }) => {
|
|
223
|
+
return handleMirror(ctx, session, "right");
|
|
224
|
+
});
|
|
225
|
+
ctx.command("上对称", "以上侧为基准生成上下对称图片").action(async ({ session }) => {
|
|
226
|
+
return handleMirror(ctx, session, "up");
|
|
227
|
+
});
|
|
228
|
+
ctx.command("下对称", "以下侧为基准生成上下对称图片").action(async ({ session }) => {
|
|
229
|
+
return handleMirror(ctx, session, "down");
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
__name(apply, "apply");
|
|
233
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
234
|
+
0 && (module.exports = {
|
|
235
|
+
Config,
|
|
236
|
+
apply,
|
|
237
|
+
name,
|
|
238
|
+
using
|
|
239
|
+
});
|