fix: CLAHE tile size and alpha channel corruption in image enhancement

CLAHE width/height is tile size in pixels, not tile count. A 3px tile on
a 992x1088 image created ~330x360 independent histogram regions, producing
crosshatch/etching artifacts. Now uses image_dimension/8 (clamped 8-256)
for ~8 tiles per axis. Also strips alpha before enhancement and re-joins
after to prevent CLAHE/normalise/linear from corrupting transparency.
This commit is contained in:
SnapOtter
2026-05-09 15:20:55 +08:00
parent 7f131d99a6
commit 7b1f09f5d0
3 changed files with 100 additions and 4 deletions
@@ -530,6 +530,83 @@ describe("HEIC input enhancement", () => {
});
});
// ── Alpha channel preservation ─────────────────────────────────
describe("Alpha channel preservation", () => {
it("preserves alpha channel without crosshatch corruption", async () => {
const rgbaBuffer = await sharp({
create: {
width: 100,
height: 100,
channels: 4,
background: { r: 80, g: 120, b: 60, alpha: 1 },
},
})
.png()
.toBuffer();
const res = await postTool(
{ mode: "auto", intensity: 80 },
rgbaBuffer,
"rgba.png",
"image/png",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
const { data, info } = await sharp(dlRes.rawPayload)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
for (let i = 3; i < data.length; i += 4) {
expect(data[i]).toBe(255);
}
});
it("preserves partial transparency in PNG", async () => {
const semiTransparent = await sharp({
create: {
width: 50,
height: 50,
channels: 4,
background: { r: 100, g: 100, b: 100, alpha: 0.5 },
},
})
.png()
.toBuffer();
const res = await postTool(
{ mode: "auto", intensity: 50 },
semiTransparent,
"semi.png",
"image/png",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.channels).toBe(4);
const { data, info } = await sharp(dlRes.rawPayload)
.raw()
.toBuffer({ resolveWithObject: true });
const alphaValues = new Set<number>();
for (let i = 3; i < data.length; i += info.channels) {
alphaValues.add(data[i]);
}
expect(alphaValues.size).toBe(1);
});
});
// ── Large file handling ─────────────────────────────────────────
describe("Large file handling", () => {
it("enhances a large stress image", async () => {