Prepare scanner branch for merge

This commit is contained in:
AzuTear
2026-07-09 08:44:50 +02:00
parent f791d1464c
commit 8b73c01e46
69 changed files with 6700 additions and 3773 deletions
+92
View File
@@ -0,0 +1,92 @@
import { inflateSync } from "node:zlib";
import type { Bitmap } from "../../src/lib/ocrPreprocess.js";
interface PngChunk {
type: string;
data: Buffer;
}
function readChunks(buffer: Buffer): PngChunk[] {
const signature = buffer.subarray(0, 8);
if (!signature.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
throw new Error("Invalid PNG signature.");
}
const chunks: PngChunk[] = [];
let offset = 8;
while (offset + 12 <= buffer.length) {
const length = buffer.readUInt32BE(offset);
const type = buffer.toString("ascii", offset + 4, offset + 8);
const dataStart = offset + 8;
const dataEnd = dataStart + length;
if (dataEnd + 4 > buffer.length) throw new Error("Invalid PNG chunk length.");
chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) });
offset = dataEnd + 4;
if (type === "IEND") break;
}
return chunks;
}
function paethPredictor(left: number, up: number, upperLeft: number) {
const estimate = left + up - upperLeft;
const leftDistance = Math.abs(estimate - left);
const upDistance = Math.abs(estimate - up);
const upperLeftDistance = Math.abs(estimate - upperLeft);
if (leftDistance <= upDistance && leftDistance <= upperLeftDistance) return left;
if (upDistance <= upperLeftDistance) return up;
return upperLeft;
}
function unfilterScanlines(raw: Buffer, width: number, height: number, bytesPerPixel: number) {
const stride = width * bytesPerPixel;
const output = Buffer.alloc(stride * height);
let rawOffset = 0;
for (let row = 0; row < height; row++) {
const filter = raw[rawOffset++];
const rowOffset = row * stride;
const previousRowOffset = rowOffset - stride;
for (let col = 0; col < stride; col++) {
const value = raw[rawOffset++];
const left = col >= bytesPerPixel ? output[rowOffset + col - bytesPerPixel] : 0;
const up = row > 0 ? output[previousRowOffset + col] : 0;
const upperLeft = row > 0 && col >= bytesPerPixel ? output[previousRowOffset + col - bytesPerPixel] : 0;
let restored = value;
if (filter === 1) restored = value + left;
else if (filter === 2) restored = value + up;
else if (filter === 3) restored = value + Math.floor((left + up) / 2);
else if (filter === 4) restored = value + paethPredictor(left, up, upperLeft);
else if (filter !== 0) throw new Error(`Unsupported PNG filter: ${filter}`);
output[rowOffset + col] = restored & 0xff;
}
}
return output;
}
export function pngBufferToBitmap(buffer: Buffer): Bitmap {
const chunks = readChunks(buffer);
const ihdr = chunks.find((chunk) => chunk.type === "IHDR")?.data;
if (!ihdr) throw new Error("PNG missing IHDR.");
const width = ihdr.readUInt32BE(0);
const height = ihdr.readUInt32BE(4);
const bitDepth = ihdr[8];
const colorType = ihdr[9];
const compression = ihdr[10];
const filter = ihdr[11];
const interlace = ihdr[12];
if (bitDepth !== 8 || compression !== 0 || filter !== 0 || interlace !== 0) {
throw new Error("Unsupported PNG format.");
}
const sourceBytesPerPixel = colorType === 6 ? 4 : colorType === 2 ? 3 : 0;
if (!sourceBytesPerPixel) throw new Error(`Unsupported PNG color type: ${colorType}`);
const idat = Buffer.concat(chunks.filter((chunk) => chunk.type === "IDAT").map((chunk) => chunk.data));
const unfiltered = unfilterScanlines(inflateSync(idat), width, height, sourceBytesPerPixel);
if (colorType === 6) return { data: unfiltered, width, height };
const rgba = Buffer.alloc(width * height * 4);
for (let pixel = 0; pixel < width * height; pixel++) {
rgba[pixel * 4] = unfiltered[pixel * 3];
rgba[pixel * 4 + 1] = unfiltered[pixel * 3 + 1];
rgba[pixel * 4 + 2] = unfiltered[pixel * 3 + 2];
rgba[pixel * 4 + 3] = 255;
}
return { data: rgba, width, height };
}