mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
330 lines
8.5 KiB
JavaScript
330 lines
8.5 KiB
JavaScript
import { deflateSync } from "node:zlib";
|
||
import { writeFileSync, mkdirSync } from "node:fs";
|
||
import { join, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
|
||
const CRC_TABLE = (() => {
|
||
const table = new Uint32Array(256);
|
||
for (let n = 0; n < 256; n++) {
|
||
let c = n;
|
||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||
table[n] = c >>> 0;
|
||
}
|
||
return table;
|
||
})();
|
||
|
||
function crc32(data) {
|
||
let c = 0xffffffff;
|
||
for (let i = 0; i < data.length; i++)
|
||
c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
|
||
return (c ^ 0xffffffff) >>> 0;
|
||
}
|
||
|
||
function chunk(type, data) {
|
||
const out = Buffer.alloc(8 + data.length + 4);
|
||
out.writeUInt32BE(data.length, 0);
|
||
out.write(type, 4, "ascii");
|
||
data.copy(out, 8);
|
||
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
|
||
return out;
|
||
}
|
||
|
||
function makePng(width, height, pixelAt) {
|
||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(width, 0);
|
||
ihdr.writeUInt32BE(height, 4);
|
||
ihdr[8] = 8; // bit depth
|
||
ihdr[9] = 6; // color type RGBA
|
||
ihdr[10] = 0; // compression
|
||
ihdr[11] = 0; // filter
|
||
ihdr[12] = 0; // interlace
|
||
const stride = width * 4 + 1;
|
||
const raw = Buffer.alloc(height * stride);
|
||
for (let y = 0; y < height; y++) {
|
||
raw[y * stride] = 0; // filter: none
|
||
for (let x = 0; x < width; x++) {
|
||
const [r, g, b, a] = pixelAt(x, y);
|
||
const p = y * stride + 1 + x * 4;
|
||
raw[p] = r;
|
||
raw[p + 1] = g;
|
||
raw[p + 2] = b;
|
||
raw[p + 3] = a;
|
||
}
|
||
}
|
||
return Buffer.concat([
|
||
sig,
|
||
chunk("IHDR", ihdr),
|
||
chunk("IDAT", deflateSync(raw)),
|
||
chunk("IEND", Buffer.alloc(0)),
|
||
]);
|
||
}
|
||
|
||
function makePalettePng(width, height, palette, pixelAt) {
|
||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(width, 0);
|
||
ihdr.writeUInt32BE(height, 4);
|
||
ihdr[8] = 8; // bit depth
|
||
ihdr[9] = 3; // color type: indexed
|
||
ihdr[10] = 0;
|
||
ihdr[11] = 0;
|
||
ihdr[12] = 0;
|
||
|
||
// PLTE: palette entries (R,G,B × count), padded to multiple of 3
|
||
const plteData = Buffer.alloc(256 * 3);
|
||
for (let i = 0; i < palette.length && i < 256; i++) {
|
||
plteData[i * 3] = palette[i][0];
|
||
plteData[i * 3 + 1] = palette[i][1];
|
||
plteData[i * 3 + 2] = palette[i][2];
|
||
}
|
||
|
||
// Raw image data: 1 byte per pixel (index)
|
||
const stride = width + 1;
|
||
const raw = Buffer.alloc(height * stride);
|
||
for (let y = 0; y < height; y++) {
|
||
raw[y * stride] = 0;
|
||
for (let x = 0; x < width; x++) {
|
||
raw[y * stride + 1 + x] = pixelAt(x, y);
|
||
}
|
||
}
|
||
|
||
return Buffer.concat([
|
||
sig,
|
||
chunk("IHDR", ihdr),
|
||
chunk("PLTE", plteData),
|
||
chunk("IDAT", deflateSync(raw)),
|
||
chunk("IEND", Buffer.alloc(0)),
|
||
]);
|
||
}
|
||
|
||
function make16bitPng(width, height, pixelAt) {
|
||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(width, 0);
|
||
ihdr.writeUInt32BE(height, 4);
|
||
ihdr[8] = 16; // bit depth
|
||
ihdr[9] = 6; // color type RGBA
|
||
ihdr[10] = 0;
|
||
ihdr[11] = 0;
|
||
ihdr[12] = 0;
|
||
|
||
const stride = width * 8 + 1;
|
||
const raw = Buffer.alloc(height * stride);
|
||
for (let y = 0; y < height; y++) {
|
||
raw[y * stride] = 0;
|
||
for (let x = 0; x < width; x++) {
|
||
const [r, g, b, a] = pixelAt(x, y);
|
||
const p = y * stride + 1 + x * 8;
|
||
raw.writeUInt16BE(r, p);
|
||
raw.writeUInt16BE(g, p + 2);
|
||
raw.writeUInt16BE(b, p + 4);
|
||
raw.writeUInt16BE(a, p + 6);
|
||
}
|
||
}
|
||
|
||
return Buffer.concat([
|
||
sig,
|
||
chunk("IHDR", ihdr),
|
||
chunk("IDAT", deflateSync(raw)),
|
||
chunk("IEND", Buffer.alloc(0)),
|
||
]);
|
||
}
|
||
|
||
function makeCorruptPng() {
|
||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(4, 0);
|
||
ihdr.writeUInt32BE(4, 4);
|
||
ihdr[8] = 8;
|
||
ihdr[9] = 2; // RGB
|
||
ihdr[10] = 0;
|
||
ihdr[11] = 0;
|
||
ihdr[12] = 0;
|
||
|
||
const stride = 4 * 3 + 1;
|
||
const raw = Buffer.alloc(4 * stride);
|
||
for (let y = 0; y < 4; y++) {
|
||
raw[y * stride] = 0;
|
||
for (let x = 0; x < 4; x++) {
|
||
const p = y * stride + 1 + x * 3;
|
||
raw[p] = 255;
|
||
raw[p + 1] = 0;
|
||
raw[p + 2] = 0;
|
||
}
|
||
}
|
||
|
||
const idat = chunk("IDAT", deflateSync(raw));
|
||
// Corrupt CRC: flip a byte in the CRC field
|
||
idat[idat.length - 1] ^= 0xff;
|
||
|
||
return Buffer.concat([sig, chunk("IHDR", ihdr), idat, chunk("IEND", Buffer.alloc(0))]);
|
||
}
|
||
|
||
function makeTruncatedPng() {
|
||
return Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82]);
|
||
}
|
||
|
||
const outDir = join(__dirname);
|
||
|
||
// 1. PNG 1×1
|
||
writeFileSync(join(outDir, "1x1.png"), makePng(1, 1, () => [255, 0, 0, 255]));
|
||
|
||
// 2. PNG 1×1 fully transparent
|
||
writeFileSync(
|
||
join(outDir, "1x1-transparent.png"),
|
||
makePng(1, 1, () => [0, 0, 0, 0]),
|
||
);
|
||
|
||
// 3. Large 1024×1024 checkerboard (≈1 megapixel RGBA)
|
||
writeFileSync(
|
||
join(outDir, "1024x1024-checker.png"),
|
||
makePng(1024, 1024, (x, y) =>
|
||
x % 2 === y % 2 ? [255, 0, 0, 255] : [255, 255, 255, 255],
|
||
),
|
||
);
|
||
|
||
// 4. 1920×1080 gradient (≈2 megapixels, tests larger displays)
|
||
writeFileSync(
|
||
join(outDir, "1920x1080-gradient.png"),
|
||
makePng(1920, 1080, (x, y) => [
|
||
Math.floor((x / 1920) * 255),
|
||
Math.floor((y / 1080) * 255),
|
||
128,
|
||
255,
|
||
]),
|
||
);
|
||
|
||
// 5. Transparency checkerboard (black/white with alpha pattern)
|
||
writeFileSync(
|
||
join(outDir, "alpha-checkerboard.png"),
|
||
makePng(64, 64, (x, y) => {
|
||
const alpha = (x + y) % 3 === 0 ? 0 : 255;
|
||
const base = (x % 2 === y % 2) ? 0 : 255;
|
||
return [base, base, base, alpha];
|
||
}),
|
||
);
|
||
|
||
// 6. Palette / indexed-color PNG (4 colors: red, green, blue, white)
|
||
const palette = [
|
||
[255, 0, 0],
|
||
[0, 255, 0],
|
||
[0, 0, 255],
|
||
[255, 255, 255],
|
||
];
|
||
writeFileSync(
|
||
join(outDir, "palette-4colors.png"),
|
||
makePalettePng(32, 32, palette, (x, y) => {
|
||
const idx = (Math.floor(x / 8) + Math.floor(y / 8)) % palette.length;
|
||
return idx;
|
||
}),
|
||
);
|
||
|
||
// 7. Palette with many colors (256-color full palette)
|
||
const fullPalette = Array.from({ length: 256 }, (_, i) => [
|
||
i,
|
||
(i * 3) % 256,
|
||
(i * 7) % 256,
|
||
]);
|
||
writeFileSync(
|
||
join(outDir, "palette-256.png"),
|
||
makePalettePng(64, 64, fullPalette, (x, y) => (x + y * 64) % 256),
|
||
);
|
||
|
||
// 8. 16-bit RGBA PNG (subtle gradient to test high bit depth)
|
||
writeFileSync(
|
||
join(outDir, "16bit-subtle.png"),
|
||
make16bitPng(32, 32, (x, y) => [
|
||
Math.floor((x / 32) * 65535),
|
||
Math.floor((y / 32) * 65535),
|
||
32768,
|
||
65535,
|
||
]),
|
||
);
|
||
|
||
// 9. Corrupt PNG (bad CRC in IDAT)
|
||
writeFileSync(join(outDir, "corrupt-bad-crc.png"), makeCorruptPng());
|
||
|
||
// 10. Truncated PNG (only header, no data)
|
||
writeFileSync(join(outDir, "truncated.png"), makeTruncatedPng());
|
||
|
||
// 11. Non-PNG file disguised as PNG
|
||
writeFileSync(
|
||
join(outDir, "fake-png.txt"),
|
||
Buffer.from("this is definitely not a png file"),
|
||
);
|
||
|
||
// 12. All-black fully opaque
|
||
writeFileSync(
|
||
join(outDir, "all-black-opaque.png"),
|
||
makePng(64, 64, () => [0, 0, 0, 255]),
|
||
);
|
||
|
||
// 13. All-white fully opaque
|
||
writeFileSync(
|
||
join(outDir, "all-white-opaque.png"),
|
||
makePng(64, 64, () => [255, 255, 255, 255]),
|
||
);
|
||
|
||
// 14. All-transparent
|
||
writeFileSync(
|
||
join(outDir, "all-transparent.png"),
|
||
makePng(64, 64, () => [0, 0, 0, 0]),
|
||
);
|
||
|
||
// 15. Semi-transparent gradient (alpha 0→255 across width)
|
||
writeFileSync(
|
||
join(outDir, "alpha-gradient.png"),
|
||
makePng(128, 64, (x, y) => [
|
||
0,
|
||
100,
|
||
255,
|
||
Math.floor((x / 128) * 255),
|
||
]),
|
||
);
|
||
|
||
// 16. Noise pattern (random-ish pixels)
|
||
writeFileSync(
|
||
join(outDir, "noise-pattern.png"),
|
||
makePng(100, 100, (x, y) => {
|
||
const v = ((x * 7919 + y * 104729) % 256);
|
||
return [v, (v * 3) % 256, (v * 7) % 256, 255];
|
||
}),
|
||
);
|
||
|
||
// 17. Minimal size: 2×2 with extreme colors
|
||
writeFileSync(
|
||
join(outDir, "2x2-extreme.png"),
|
||
makePng(2, 2, (x, y) => {
|
||
if (x === 0 && y === 0) return [0, 0, 0, 255];
|
||
if (x === 1 && y === 0) return [255, 255, 255, 255];
|
||
if (x === 0 && y === 1) return [255, 0, 0, 128];
|
||
return [0, 0, 255, 0];
|
||
}),
|
||
);
|
||
|
||
// 18. Long strip (1×4000) — tests scroll / vertical tools
|
||
writeFileSync(
|
||
join(outDir, "1x4000-strip.png"),
|
||
makePng(1, 4000, (x, y) => [y % 256, (y * 2) % 256, (y * 3) % 256, 255]),
|
||
);
|
||
|
||
// 19. Wide strip (4000×1) — tests horizontal handling
|
||
writeFileSync(
|
||
join(outDir, "4000x1-strip.png"),
|
||
makePng(4000, 1, (x, y) => [x % 256, (x * 2) % 256, (x * 3) % 256, 255]),
|
||
);
|
||
|
||
// 20. PNG with very high alpha variance (sparse transparency)
|
||
writeFileSync(
|
||
join(outDir, "sparse-alpha.png"),
|
||
makePng(100, 100, (x, y) => {
|
||
const sparse = ((x * 31 + y * 17) % 100) < 10;
|
||
return [sparse ? 0 : 100, sparse ? 0 : 200, 255, sparse ? 0 : 255];
|
||
}),
|
||
);
|
||
|
||
console.log("Generated 20 edge case PNG files in:", outDir);
|