Files
Rotatory/server/image.js
T
ferdi2goandClaude Fable 5.1 9ed59bcf56 Fotoupload im Personenformular ergänzen und Initialen-Platzhalter aussortieren
Fotofeld mit Dateiauswahl, Einfügen per Strg+V, Zwischenablage-Schaltfläche
und Drag & Drop; sichtbarer Foto-Button je Zeile. Foto wird beim Anlegen
oder Speichern übernommen bzw. entfernt.

Nextcloud meldet generierte Initialen-Avatare teils als eigene Profilbilder.
Der Import erkennt Bilder mit dominanter Hintergrundfarbe und lässt sie aus;
„Platzhalter-Fotos entfernen" bereinigt bereits importierte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 09:57:54 +00:00

71 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Erkennung von Buchstaben-Platzhaltern (eine dominante Hintergrundfarbe, Initialen darauf).
// Nextcloud liefert solche Bilder teils als „eigene" Avatare aus; im Kalender sollen aber nur echte Fotos landen.
import zlib from 'node:zlib';
import { imageMime } from './store.js';
const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
/** Anteil der häufigsten Farbe (0–1) und Zahl unterschiedlicher Farben, per Stichprobe. Nur PNG (8 Bit, RGB/RGBA/Grau). */
export function pngDominantShare(buffer, step = 4) {
if (!buffer || buffer.length < 33 || !buffer.subarray(0, 8).equals(PNG_SIG)) return null;
let pos = 8;
let width = 0; let height = 0; let depth = 0; let colorType = 0;
const idat = [];
while (pos + 8 <= buffer.length) {
const len = buffer.readUInt32BE(pos);
const type = buffer.toString('ascii', pos + 4, pos + 8);
const body = buffer.subarray(pos + 8, pos + 8 + len);
if (type === 'IHDR') { width = body.readUInt32BE(0); height = body.readUInt32BE(4); depth = body[8]; colorType = body[9]; }
else if (type === 'IDAT') idat.push(body);
else if (type === 'IEND') break;
pos += 12 + len;
}
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[colorType];
if (!channels || depth !== 8 || !width || !height) return null;
let raw;
try { raw = zlib.inflateSync(Buffer.concat(idat)); } catch { return null; }
const bpp = channels;
const stride = width * bpp;
let prev = Buffer.alloc(stride);
let offset = 0;
const counts = new Map();
let total = 0;
for (let y = 0; y < height; y += 1) {
const filter = raw[offset];
const line = Buffer.from(raw.subarray(offset + 1, offset + 1 + stride));
offset += 1 + stride;
for (let x = 0; x < stride; x += 1) {
const a = x >= bpp ? line[x - bpp] : 0;
const b = prev[x];
const c = x >= bpp ? prev[x - bpp] : 0;
if (filter === 1) line[x] = (line[x] + a) & 255;
else if (filter === 2) line[x] = (line[x] + b) & 255;
else if (filter === 3) line[x] = (line[x] + ((a + b) >> 1)) & 255;
else if (filter === 4) {
const p = a + b - c;
const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c);
line[x] = (line[x] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255;
}
}
if (y % step === 0) {
for (let x = 0; x < stride; x += bpp * step) {
const key = channels >= 3 ? (line[x] << 16) | (line[x + 1] << 8) | line[x + 2] : line[x] * 0x010101;
counts.set(key, (counts.get(key) || 0) + 1);
total += 1;
}
}
prev = line;
}
if (total === 0) return null;
let max = 0;
for (const n of counts.values()) if (n > max) max = n;
return { share: max / total, colors: counts.size, width, height };
}
/** true, wenn das Bild wie ein generierter Initialen-Avatar aussieht: eine Farbe füllt den Großteil der Fläche. */
export function looksLikePlaceholder(buffer, threshold = 0.75) {
if (imageMime(buffer) !== 'image/png') return false; // JPEG/WebP sind praktisch immer echte Fotos
const info = pngDominantShare(buffer);
return Boolean(info && info.share >= threshold);
}