diff --git a/docs/checklist-manual-testing.md b/docs/checklist-manual-testing.md index 8c8db06..27ade45 100644 --- a/docs/checklist-manual-testing.md +++ b/docs/checklist-manual-testing.md @@ -12,6 +12,9 @@ ## A. Краевые PNG-файлы +> Фикстуры для этого раздела генерируются скриптом: `pnpm generate:fixtures` → +> `tests/fixtures/manual-edge-cases/`. + - [ ] Огромное изображение (мегапиксели) — загрузка не висит, прогресс есть. - [ ] PNG 1×1 — инструменты не падают (проверено авто: flip-png, см. suite). - [ ] PNG с полупрозрачностью/чётким альфа-краёв (чёрно-белая шахматка) — @@ -23,6 +26,31 @@ - [ ] PNG с битой CRCh / 16-bit — как ведут себя анализаторы (ориентация, размер, прозрачность). +### Файлы (`tests/fixtures/manual-edge-cases/`) + +| Файл | Описание | +| ------------------------ | ------------------------------------------------------------ | +| `1x1.png` | Минимальное изображение 1×1, сплошной красный | +| `1x1-transparent.png` | 1×1, полностью прозрачный (alpha=0) | +| `2x2-extreme.png` | 2×2: чёрный, белый, полупрозрачный красный, прозрачный синий | +| `1024x1024-checker.png` | 1024×1024, шахматка ≈1 мегапиксель | +| `1920x1080-gradient.png` | 1920×1080, градиент ≈2 мегапикселя | +| `alpha-checkerboard.png` | 64×64, чёрно-белая шахматка с чередующейся альфой | +| `alpha-gradient.png` | 128×64, альфа-градиент 0→255 по ширине | +| `sparse-alpha.png` | 100×100, 10% пикселей с alpha=0 | +| `all-black-opaque.png` | 64×64, сплошной чёрный | +| `all-white-opaque.png` | 64×64, сплошной белый | +| `all-transparent.png` | 64×64, полностью прозрачное | +| `palette-4colors.png` | 32×32, палитровый 4 цвета (RGBW) | +| `palette-256.png` | 64×64, палитровый полная палитра 256 цветов | +| `16bit-subtle.png` | 32×32, 16-bit RGBA, тонкий градиент | +| `noise-pattern.png` | 100×100, шумовая текстура | +| `1x4000-strip.png` | 1×4000, длинная вертикальная полоса | +| `4000x1-strip.png` | 4000×1, широкая горизонтальная полоса | +| `corrupt-bad-crc.png` | 4×4, повреждённый CRC в IDAT | +| `truncated.png` | Обрезанный файл (только PNG-заголовок + часть IHDR) | +| `fake-png.txt` | Текстовый файл «.png» — не-PNG | + ## B. Края параметров - [ ] Экстремальные значения слайдеров (0 и max) в blur/sharpen/pixelate — не diff --git a/package.json b/package.json index 901e5bf..9462b3f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "build": "pnpm --dir web build", "format": "pnpm format:docs && pnpm --dir web format", "format:docs": "prettier --write docs --log-level warn", - "check:docs": "prettier --check docs" + "check:docs": "prettier --check docs", + "generate:fixtures": "node tests/fixtures/manual-edge-cases/generate.mjs" }, "keywords": [], "author": "Ku6epXBOCTuK", diff --git a/tests/fixtures/manual-edge-cases/1024x1024-checker.png b/tests/fixtures/manual-edge-cases/1024x1024-checker.png new file mode 100644 index 0000000..7377e3c Binary files /dev/null and b/tests/fixtures/manual-edge-cases/1024x1024-checker.png differ diff --git a/tests/fixtures/manual-edge-cases/16bit-subtle.png b/tests/fixtures/manual-edge-cases/16bit-subtle.png new file mode 100644 index 0000000..237e2dc Binary files /dev/null and b/tests/fixtures/manual-edge-cases/16bit-subtle.png differ diff --git a/tests/fixtures/manual-edge-cases/1920x1080-gradient.png b/tests/fixtures/manual-edge-cases/1920x1080-gradient.png new file mode 100644 index 0000000..8e5c67b Binary files /dev/null and b/tests/fixtures/manual-edge-cases/1920x1080-gradient.png differ diff --git a/tests/fixtures/manual-edge-cases/1x1-transparent.png b/tests/fixtures/manual-edge-cases/1x1-transparent.png new file mode 100644 index 0000000..35a340b Binary files /dev/null and b/tests/fixtures/manual-edge-cases/1x1-transparent.png differ diff --git a/tests/fixtures/manual-edge-cases/1x1.png b/tests/fixtures/manual-edge-cases/1x1.png new file mode 100644 index 0000000..cf5d589 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/1x1.png differ diff --git a/tests/fixtures/manual-edge-cases/1x4000-strip.png b/tests/fixtures/manual-edge-cases/1x4000-strip.png new file mode 100644 index 0000000..71bc5f5 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/1x4000-strip.png differ diff --git a/tests/fixtures/manual-edge-cases/2x2-extreme.png b/tests/fixtures/manual-edge-cases/2x2-extreme.png new file mode 100644 index 0000000..fce1969 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/2x2-extreme.png differ diff --git a/tests/fixtures/manual-edge-cases/4000x1-strip.png b/tests/fixtures/manual-edge-cases/4000x1-strip.png new file mode 100644 index 0000000..ee2dd6e Binary files /dev/null and b/tests/fixtures/manual-edge-cases/4000x1-strip.png differ diff --git a/tests/fixtures/manual-edge-cases/all-black-opaque.png b/tests/fixtures/manual-edge-cases/all-black-opaque.png new file mode 100644 index 0000000..2d0b013 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/all-black-opaque.png differ diff --git a/tests/fixtures/manual-edge-cases/all-transparent.png b/tests/fixtures/manual-edge-cases/all-transparent.png new file mode 100644 index 0000000..0858c19 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/all-transparent.png differ diff --git a/tests/fixtures/manual-edge-cases/all-white-opaque.png b/tests/fixtures/manual-edge-cases/all-white-opaque.png new file mode 100644 index 0000000..5400ae8 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/all-white-opaque.png differ diff --git a/tests/fixtures/manual-edge-cases/alpha-checkerboard.png b/tests/fixtures/manual-edge-cases/alpha-checkerboard.png new file mode 100644 index 0000000..31e7c4d Binary files /dev/null and b/tests/fixtures/manual-edge-cases/alpha-checkerboard.png differ diff --git a/tests/fixtures/manual-edge-cases/alpha-gradient.png b/tests/fixtures/manual-edge-cases/alpha-gradient.png new file mode 100644 index 0000000..1ea28da Binary files /dev/null and b/tests/fixtures/manual-edge-cases/alpha-gradient.png differ diff --git a/tests/fixtures/manual-edge-cases/corrupt-bad-crc.png b/tests/fixtures/manual-edge-cases/corrupt-bad-crc.png new file mode 100644 index 0000000..6ff7570 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/corrupt-bad-crc.png differ diff --git a/tests/fixtures/manual-edge-cases/fake-png.txt b/tests/fixtures/manual-edge-cases/fake-png.txt new file mode 100644 index 0000000..003e76d --- /dev/null +++ b/tests/fixtures/manual-edge-cases/fake-png.txt @@ -0,0 +1 @@ +this is definitely not a png file \ No newline at end of file diff --git a/tests/fixtures/manual-edge-cases/generate.mjs b/tests/fixtures/manual-edge-cases/generate.mjs new file mode 100644 index 0000000..3e0b2db --- /dev/null +++ b/tests/fixtures/manual-edge-cases/generate.mjs @@ -0,0 +1,329 @@ +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); diff --git a/tests/fixtures/manual-edge-cases/noise-pattern.png b/tests/fixtures/manual-edge-cases/noise-pattern.png new file mode 100644 index 0000000..9ffbe24 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/noise-pattern.png differ diff --git a/tests/fixtures/manual-edge-cases/palette-256.png b/tests/fixtures/manual-edge-cases/palette-256.png new file mode 100644 index 0000000..92ae1ac Binary files /dev/null and b/tests/fixtures/manual-edge-cases/palette-256.png differ diff --git a/tests/fixtures/manual-edge-cases/palette-4colors.png b/tests/fixtures/manual-edge-cases/palette-4colors.png new file mode 100644 index 0000000..2396aff Binary files /dev/null and b/tests/fixtures/manual-edge-cases/palette-4colors.png differ diff --git a/tests/fixtures/manual-edge-cases/sparse-alpha.png b/tests/fixtures/manual-edge-cases/sparse-alpha.png new file mode 100644 index 0000000..5d07093 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/sparse-alpha.png differ diff --git a/tests/fixtures/manual-edge-cases/truncated.png b/tests/fixtures/manual-edge-cases/truncated.png new file mode 100644 index 0000000..029ace0 Binary files /dev/null and b/tests/fixtures/manual-edge-cases/truncated.png differ