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.
Files changed (2) hide show
  1. package/lib/index.js +94 -10
  2. 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 loadSharp() {
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(loadSharp, "loadSharp");
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 sharp = loadSharp();
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
- const sharp = loadSharp();
414
- if (!sharp) {
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: false,
503
+ available: true,
417
504
  threshold: normalizedThreshold,
418
505
  groups: [],
419
- message: "sharp 未安装,暂时无法筛选相似图片"
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.1",
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",