koishi-plugin-memesluna 0.4.1 → 0.4.3
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.js +94 -10
- package/package.json +6 -2
package/lib/index.js
CHANGED
|
@@ -64,7 +64,7 @@ function hashImageBuffer(buffer) {
|
|
|
64
64
|
return (0, import_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
65
65
|
}
|
|
66
66
|
__name(hashImageBuffer, "hashImageBuffer");
|
|
67
|
-
function
|
|
67
|
+
function loadOptionalSharp() {
|
|
68
68
|
try {
|
|
69
69
|
const packageName = "sharp";
|
|
70
70
|
const sharpModule = require(packageName);
|
|
@@ -73,7 +73,7 @@ function loadSharp() {
|
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
__name(
|
|
76
|
+
__name(loadOptionalSharp, "loadOptionalSharp");
|
|
77
77
|
function countHexBitDistance(a, b) {
|
|
78
78
|
if (!a || !b || a.length !== b.length) return 64;
|
|
79
79
|
let distance = 0;
|
|
@@ -85,6 +85,81 @@ function countHexBitDistance(a, b) {
|
|
|
85
85
|
}
|
|
86
86
|
__name(countHexBitDistance, "countHexBitDistance");
|
|
87
87
|
var DHASH_BITS = 64;
|
|
88
|
+
var DHASH_WIDTH = 9;
|
|
89
|
+
var DHASH_HEIGHT = 8;
|
|
90
|
+
function decodeImagePixels(buffer) {
|
|
91
|
+
try {
|
|
92
|
+
if (buffer.length >= 8 && buffer.readUInt32BE(0) === 2303741511 && buffer.readUInt32BE(4) === 218765834) {
|
|
93
|
+
const png = require("pngjs").PNG.sync.read(buffer);
|
|
94
|
+
return { data: png.data, width: png.width, height: png.height };
|
|
95
|
+
}
|
|
96
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
97
|
+
const jpeg = require("jpeg-js").decode(buffer, { useTArray: true });
|
|
98
|
+
return { data: jpeg.data, width: jpeg.width, height: jpeg.height };
|
|
99
|
+
}
|
|
100
|
+
if (buffer.length >= 6 && buffer.toString("ascii", 0, 3) === "GIF") {
|
|
101
|
+
const { GifReader } = require("omggif");
|
|
102
|
+
const reader = new GifReader(buffer);
|
|
103
|
+
const data = Buffer.alloc(reader.width * reader.height * 4);
|
|
104
|
+
reader.decodeAndBlitFrameRGBA(0, data);
|
|
105
|
+
return { data, width: reader.width, height: reader.height };
|
|
106
|
+
}
|
|
107
|
+
if (buffer.length >= 2 && buffer[0] === 66 && buffer[1] === 77) {
|
|
108
|
+
const bmp = require("bmp-js").decode(buffer);
|
|
109
|
+
const data = Buffer.alloc(bmp.width * bmp.height * 4);
|
|
110
|
+
for (let i = 0; i < bmp.data.length; i += 4) {
|
|
111
|
+
data[i] = bmp.data[i + 3];
|
|
112
|
+
data[i + 1] = bmp.data[i + 2];
|
|
113
|
+
data[i + 2] = bmp.data[i + 1];
|
|
114
|
+
data[i + 3] = bmp.data[i];
|
|
115
|
+
}
|
|
116
|
+
return { data, width: bmp.width, height: bmp.height };
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
__name(decodeImagePixels, "decodeImagePixels");
|
|
124
|
+
function lumaFromRgba(data, offset) {
|
|
125
|
+
const alpha = data[offset + 3] / 255;
|
|
126
|
+
const r = data[offset] * alpha + 255 * (1 - alpha);
|
|
127
|
+
const g = data[offset + 1] * alpha + 255 * (1 - alpha);
|
|
128
|
+
const b = data[offset + 2] * alpha + 255 * (1 - alpha);
|
|
129
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
130
|
+
}
|
|
131
|
+
__name(lumaFromRgba, "lumaFromRgba");
|
|
132
|
+
function averageCellLuma(data, width, height, cellX, cellY) {
|
|
133
|
+
const xStart = Math.floor(cellX * width / DHASH_WIDTH);
|
|
134
|
+
const xEnd = Math.max(xStart + 1, Math.floor((cellX + 1) * width / DHASH_WIDTH));
|
|
135
|
+
const yStart = Math.floor(cellY * height / DHASH_HEIGHT);
|
|
136
|
+
const yEnd = Math.max(yStart + 1, Math.floor((cellY + 1) * height / DHASH_HEIGHT));
|
|
137
|
+
let total = 0;
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (let y = yStart; y < Math.min(yEnd, height); y++) {
|
|
140
|
+
for (let x = xStart; x < Math.min(xEnd, width); x++) {
|
|
141
|
+
total += lumaFromRgba(data, (y * width + x) * 4);
|
|
142
|
+
count++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return count ? total / count : 0;
|
|
146
|
+
}
|
|
147
|
+
__name(averageCellLuma, "averageCellLuma");
|
|
148
|
+
function buildDhashFromLumaGrid(samples) {
|
|
149
|
+
let bits = "";
|
|
150
|
+
for (let y = 0; y < DHASH_HEIGHT; y++) {
|
|
151
|
+
const row = y * DHASH_WIDTH;
|
|
152
|
+
for (let x = 0; x < DHASH_WIDTH - 1; x++) {
|
|
153
|
+
bits += samples[row + x] > samples[row + x + 1] ? "1" : "0";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
let hex = "";
|
|
157
|
+
for (let i = 0; i < bits.length; i += 4) {
|
|
158
|
+
hex += Number.parseInt(bits.slice(i, i + 4), 2).toString(16);
|
|
159
|
+
}
|
|
160
|
+
return hex;
|
|
161
|
+
}
|
|
162
|
+
__name(buildDhashFromLumaGrid, "buildDhashFromLumaGrid");
|
|
88
163
|
var RESERVED_PATHS = /* @__PURE__ */ new Set([
|
|
89
164
|
"config",
|
|
90
165
|
"admin",
|
|
@@ -307,7 +382,17 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
307
382
|
await this.backfillImageFingerprints();
|
|
308
383
|
}
|
|
309
384
|
async getImagePerceptualHash(buffer) {
|
|
310
|
-
const
|
|
385
|
+
const decoded = decodeImagePixels(buffer);
|
|
386
|
+
if (decoded) {
|
|
387
|
+
const samples = [];
|
|
388
|
+
for (let y = 0; y < DHASH_HEIGHT; y++) {
|
|
389
|
+
for (let x = 0; x < DHASH_WIDTH; x++) {
|
|
390
|
+
samples.push(averageCellLuma(decoded.data, decoded.width, decoded.height, x, y));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return buildDhashFromLumaGrid(samples);
|
|
394
|
+
}
|
|
395
|
+
const sharp = loadOptionalSharp();
|
|
311
396
|
if (!sharp) return "";
|
|
312
397
|
try {
|
|
313
398
|
const raw = await sharp(buffer, { animated: false, failOn: "none" }).resize(9, 8, { fit: "fill" }).grayscale().raw().toBuffer();
|
|
@@ -410,18 +495,17 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
410
495
|
}
|
|
411
496
|
async getSimilarStagedImages(threshold = this.config.similarityThreshold || 0.9) {
|
|
412
497
|
const normalizedThreshold = Math.min(1, Math.max(0.5, Number(threshold) || 0.9));
|
|
413
|
-
|
|
414
|
-
|
|
498
|
+
await this.backfillImageFingerprints();
|
|
499
|
+
const rows = await this.ctx.database.get("memesluna_staged_images", {});
|
|
500
|
+
const items = rows.map((row) => this.mapStagedImage(row)).filter((item) => item.perceptualHash);
|
|
501
|
+
if (!items.length) {
|
|
415
502
|
return {
|
|
416
|
-
available:
|
|
503
|
+
available: true,
|
|
417
504
|
threshold: normalizedThreshold,
|
|
418
505
|
groups: [],
|
|
419
|
-
message: "
|
|
506
|
+
message: rows.length ? "暂缓区图片暂未生成可比较的感知哈希" : "暂缓区暂无图片"
|
|
420
507
|
};
|
|
421
508
|
}
|
|
422
|
-
await this.backfillImageFingerprints();
|
|
423
|
-
const rows = await this.ctx.database.get("memesluna_staged_images", {});
|
|
424
|
-
const items = rows.map((row) => this.mapStagedImage(row)).filter((item) => item.perceptualHash);
|
|
425
509
|
const parent = /* @__PURE__ */ new Map();
|
|
426
510
|
const groupSimilarity = /* @__PURE__ */ new Map();
|
|
427
511
|
const find = /* @__PURE__ */ __name((id) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-memesluna",
|
|
3
3
|
"description": "Image Forward service for Koishi with ChatLuna integration",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -41,8 +41,12 @@
|
|
|
41
41
|
"node": ">=18.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"bmp-js": "^0.1.0",
|
|
45
|
+
"jpeg-js": "^0.4.4",
|
|
44
46
|
"koishi-plugin-chatluna": "^1.3.0-alpha.49",
|
|
45
|
-
"koishi-plugin-chatluna-storage-service": "^1.0.6"
|
|
47
|
+
"koishi-plugin-chatluna-storage-service": "^1.0.6",
|
|
48
|
+
"omggif": "^1.0.10",
|
|
49
|
+
"pngjs": "^7.0.0"
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
52
|
"@koishijs/client": "^5.30.11",
|