koishi-plugin-memesluna 0.4.1 → 0.4.2

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 +76 -10
  2. package/package.json +2 -1
package/lib/index.js CHANGED
@@ -64,7 +64,16 @@ 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 loadJimp() {
68
+ try {
69
+ const jimpModule = require("jimp");
70
+ return jimpModule.Jimp || jimpModule.default?.Jimp || null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ __name(loadJimp, "loadJimp");
76
+ function loadOptionalSharp() {
68
77
  try {
69
78
  const packageName = "sharp";
70
79
  const sharpModule = require(packageName);
@@ -73,7 +82,7 @@ function loadSharp() {
73
82
  return null;
74
83
  }
75
84
  }
76
- __name(loadSharp, "loadSharp");
85
+ __name(loadOptionalSharp, "loadOptionalSharp");
77
86
  function countHexBitDistance(a, b) {
78
87
  if (!a || !b || a.length !== b.length) return 64;
79
88
  let distance = 0;
@@ -85,6 +94,47 @@ function countHexBitDistance(a, b) {
85
94
  }
86
95
  __name(countHexBitDistance, "countHexBitDistance");
87
96
  var DHASH_BITS = 64;
97
+ var DHASH_WIDTH = 9;
98
+ var DHASH_HEIGHT = 8;
99
+ function lumaFromRgba(data, offset) {
100
+ const alpha = data[offset + 3] / 255;
101
+ const r = data[offset] * alpha + 255 * (1 - alpha);
102
+ const g = data[offset + 1] * alpha + 255 * (1 - alpha);
103
+ const b = data[offset + 2] * alpha + 255 * (1 - alpha);
104
+ return 0.299 * r + 0.587 * g + 0.114 * b;
105
+ }
106
+ __name(lumaFromRgba, "lumaFromRgba");
107
+ function averageCellLuma(data, width, height, cellX, cellY) {
108
+ const xStart = Math.floor(cellX * width / DHASH_WIDTH);
109
+ const xEnd = Math.max(xStart + 1, Math.floor((cellX + 1) * width / DHASH_WIDTH));
110
+ const yStart = Math.floor(cellY * height / DHASH_HEIGHT);
111
+ const yEnd = Math.max(yStart + 1, Math.floor((cellY + 1) * height / DHASH_HEIGHT));
112
+ let total = 0;
113
+ let count = 0;
114
+ for (let y = yStart; y < Math.min(yEnd, height); y++) {
115
+ for (let x = xStart; x < Math.min(xEnd, width); x++) {
116
+ total += lumaFromRgba(data, (y * width + x) * 4);
117
+ count++;
118
+ }
119
+ }
120
+ return count ? total / count : 0;
121
+ }
122
+ __name(averageCellLuma, "averageCellLuma");
123
+ function buildDhashFromLumaGrid(samples) {
124
+ let bits = "";
125
+ for (let y = 0; y < DHASH_HEIGHT; y++) {
126
+ const row = y * DHASH_WIDTH;
127
+ for (let x = 0; x < DHASH_WIDTH - 1; x++) {
128
+ bits += samples[row + x] > samples[row + x + 1] ? "1" : "0";
129
+ }
130
+ }
131
+ let hex = "";
132
+ for (let i = 0; i < bits.length; i += 4) {
133
+ hex += Number.parseInt(bits.slice(i, i + 4), 2).toString(16);
134
+ }
135
+ return hex;
136
+ }
137
+ __name(buildDhashFromLumaGrid, "buildDhashFromLumaGrid");
88
138
  var RESERVED_PATHS = /* @__PURE__ */ new Set([
89
139
  "config",
90
140
  "admin",
@@ -307,7 +357,24 @@ var MemesLunaService = class extends import_koishi.Service {
307
357
  await this.backfillImageFingerprints();
308
358
  }
309
359
  async getImagePerceptualHash(buffer) {
310
- const sharp = loadSharp();
360
+ const Jimp = loadJimp();
361
+ if (Jimp) {
362
+ try {
363
+ const image = await Jimp.read(buffer);
364
+ const { data, width, height } = image.bitmap;
365
+ if (!data || !width || !height) return "";
366
+ const samples = [];
367
+ for (let y = 0; y < DHASH_HEIGHT; y++) {
368
+ for (let x = 0; x < DHASH_WIDTH; x++) {
369
+ samples.push(averageCellLuma(data, width, height, x, y));
370
+ }
371
+ }
372
+ return buildDhashFromLumaGrid(samples);
373
+ } catch (error) {
374
+ this.ctx.logger("memesluna").debug(`Jimp failed to calculate perceptual hash: ${error.message}`);
375
+ }
376
+ }
377
+ const sharp = loadOptionalSharp();
311
378
  if (!sharp) return "";
312
379
  try {
313
380
  const raw = await sharp(buffer, { animated: false, failOn: "none" }).resize(9, 8, { fit: "fill" }).grayscale().raw().toBuffer();
@@ -410,18 +477,17 @@ var MemesLunaService = class extends import_koishi.Service {
410
477
  }
411
478
  async getSimilarStagedImages(threshold = this.config.similarityThreshold || 0.9) {
412
479
  const normalizedThreshold = Math.min(1, Math.max(0.5, Number(threshold) || 0.9));
413
- const sharp = loadSharp();
414
- if (!sharp) {
480
+ await this.backfillImageFingerprints();
481
+ const rows = await this.ctx.database.get("memesluna_staged_images", {});
482
+ const items = rows.map((row) => this.mapStagedImage(row)).filter((item) => item.perceptualHash);
483
+ if (!items.length) {
415
484
  return {
416
- available: false,
485
+ available: true,
417
486
  threshold: normalizedThreshold,
418
487
  groups: [],
419
- message: "sharp 未安装,暂时无法筛选相似图片"
488
+ message: rows.length ? "暂缓区图片暂未生成可比较的感知哈希" : "暂缓区暂无图片"
420
489
  };
421
490
  }
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
491
  const parent = /* @__PURE__ */ new Map();
426
492
  const groupSimilarity = /* @__PURE__ */ new Map();
427
493
  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.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",
@@ -41,6 +41,7 @@
41
41
  "node": ">=18.0.0"
42
42
  },
43
43
  "dependencies": {
44
+ "jimp": "^1.6.1",
44
45
  "koishi-plugin-chatluna": "^1.3.0-alpha.49",
45
46
  "koishi-plugin-chatluna-storage-service": "^1.0.6"
46
47
  },