From d8a612fbc432356af42bbad99f313129c603cc5e Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Fri, 11 Sep 2026 16:37:48 +0500 Subject: [PATCH] test: add png fixtures for manual tests" --- docs/checklist-manual-testing.md | 28 ++ package.json | 3 +- .../manual-edge-cases/1024x1024-checker.png | Bin 0 -> 9193 bytes .../manual-edge-cases/16bit-subtle.png | Bin 0 -> 2021 bytes .../manual-edge-cases/1920x1080-gradient.png | Bin 0 -> 165765 bytes .../manual-edge-cases/1x1-transparent.png | Bin 0 -> 68 bytes tests/fixtures/manual-edge-cases/1x1.png | Bin 0 -> 70 bytes .../manual-edge-cases/1x4000-strip.png | Bin 0 -> 1253 bytes .../manual-edge-cases/2x2-extreme.png | Bin 0 -> 79 bytes .../manual-edge-cases/4000x1-strip.png | Bin 0 -> 1103 bytes .../manual-edge-cases/all-black-opaque.png | Bin 0 -> 225 bytes .../manual-edge-cases/all-transparent.png | Bin 0 -> 96 bytes .../manual-edge-cases/all-white-opaque.png | Bin 0 -> 203 bytes .../manual-edge-cases/alpha-checkerboard.png | Bin 0 -> 290 bytes .../manual-edge-cases/alpha-gradient.png | Bin 0 -> 601 bytes .../manual-edge-cases/corrupt-bad-crc.png | Bin 0 -> 73 bytes tests/fixtures/manual-edge-cases/fake-png.txt | 1 + tests/fixtures/manual-edge-cases/generate.mjs | 329 ++++++++++++++++++ .../manual-edge-cases/noise-pattern.png | Bin 0 -> 1625 bytes .../manual-edge-cases/palette-256.png | Bin 0 -> 1160 bytes .../manual-edge-cases/palette-4colors.png | Bin 0 -> 879 bytes .../manual-edge-cases/sparse-alpha.png | Bin 0 -> 722 bytes .../fixtures/manual-edge-cases/truncated.png | Bin 0 -> 16 bytes 23 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/manual-edge-cases/1024x1024-checker.png create mode 100644 tests/fixtures/manual-edge-cases/16bit-subtle.png create mode 100644 tests/fixtures/manual-edge-cases/1920x1080-gradient.png create mode 100644 tests/fixtures/manual-edge-cases/1x1-transparent.png create mode 100644 tests/fixtures/manual-edge-cases/1x1.png create mode 100644 tests/fixtures/manual-edge-cases/1x4000-strip.png create mode 100644 tests/fixtures/manual-edge-cases/2x2-extreme.png create mode 100644 tests/fixtures/manual-edge-cases/4000x1-strip.png create mode 100644 tests/fixtures/manual-edge-cases/all-black-opaque.png create mode 100644 tests/fixtures/manual-edge-cases/all-transparent.png create mode 100644 tests/fixtures/manual-edge-cases/all-white-opaque.png create mode 100644 tests/fixtures/manual-edge-cases/alpha-checkerboard.png create mode 100644 tests/fixtures/manual-edge-cases/alpha-gradient.png create mode 100644 tests/fixtures/manual-edge-cases/corrupt-bad-crc.png create mode 100644 tests/fixtures/manual-edge-cases/fake-png.txt create mode 100644 tests/fixtures/manual-edge-cases/generate.mjs create mode 100644 tests/fixtures/manual-edge-cases/noise-pattern.png create mode 100644 tests/fixtures/manual-edge-cases/palette-256.png create mode 100644 tests/fixtures/manual-edge-cases/palette-4colors.png create mode 100644 tests/fixtures/manual-edge-cases/sparse-alpha.png create mode 100644 tests/fixtures/manual-edge-cases/truncated.png 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 0000000000000000000000000000000000000000..7377e3c06b318779185bb3a87e0187373fae4a81 GIT binary patch literal 9193 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&zE~RK2WrGXsP222U5qkcv5PZXM)2z#zcn z_&eXK@r!~--k&q&+y?%iYLq=Esd!G>Z^|tQq*OdNWs50!fJ9QR>$pq+37uRg#{ia+ zGk~dC&CUSU#eT+#1P=z?!NgSP4A5u#WYHn{$7G_|OXiNP2 z|5cnIhs)$J`2^5-&zV;Y3ZMjCbRfF}%$O7@3t4T!SHTPE0~)PnZU80vlZ@*Xo4~0* ztrW7h!t#MBv_xFP#{e=-g?&#O2beMQDzppTa2-0hy4i{Wysks$gA{auM;Nq1VUBZV i0l8t+F@qsw?2b%5>wjD(}dx8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..237e2dcb4bdc5d0c37872237b732a0a49a0e9cdb GIT binary patch literal 2021 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fce1969a6ee69db540a2c4adf1b1cffee09a44d6 GIT binary patch literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nRF;5rAkcv6U2?+^5_?dYAOG-#d bG^j9m_^?-T&)FIYRKeis>gTe~DWM4fGIS8{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ee2dd6ec6f703209f7a4072611a39bca26c9d18f GIT binary patch literal 1103 zcmeAS@N?(olHy`uVBq!ia0y~y;9me_Gjgy2$y4QlKn{zTr;B4q#hka7Z#r>AF*saQ zDHfWfqT;zs#dDI1r;)0nrw5RRAfNxnlk%jXxi4?* zln#nr`tr(N@wrQXUvOO}ADb%r;##`G-KT7qo$E9%ajp6iBGcG&Nu!Eau6xZUwY{Ng z7u-YJ_hn6<7<_&C{S}!f`VANDKjtH1;w!npl->31Po6809h26nHh-CB6mfr+BAa%A zSr6V%dHr5w=?&p zXVRRvjwa!)M)%%vM12$VDX{3Bj1h zn2VKH&nSOn{CRO!q`+U^&okJ>j{Q5Lv~H&B$NCLE8Oeo8I-ZkKUOx4Uh<$r%(-XOf zv{K@-c;UrUh?p*JB1ccGB@hgEA_GN zTBARwDR?5Ibl397F-d2RU5eZ>W#-0}HFE1jpXIEJX^Flnv@Jyc@ZMu>Yqx5DT(LPh z`frqtw9L(WI~doiiIOHsQJbyHaPbJc`@Z>J;>AAa#k8V>B4?kIS zcAfsoZ=YhyB^Zi_m+V|sd^*=;`(4u=j}7OQ_~*W>@>Mz<-Cu1N zeDaIwlYRGjkjSJBLg!j{IQ$;Avv# zcHcg(;ctKNcTaFg%CQd*4@PQje)MxjZR4srDt1rf7Fy}gJpa>tg{H@2?}xHk-9|s6 zYUb~nq_c0T-Lic;)89WmpRq#Yf%{W$snuOSBmcH7n`KmYb>H%x>n7d*IzJi_@$^6ON$?3T|_d;lyc7(8A5 KT-G@yGywpD3^E@8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2d0b01303da4b7297d35071f73ef0633c4a3b280 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=D?D8sLn`JxJ*UXXz`(<@@OAtz tac0BD=88|9xb&q^8mV;!PC{xWt~$(695|AP@Vt) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0858c19f0521b74f4a058fbddc280ea48a5751bb GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=>Ygr+Ar*7p9%SSN@(wNd?w`wN fz5vKX1`XxR4h{^A{hv+-f`mO?{an^LB{Ts5qcarg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5400ae85ce5c32942f07f92069d88987a429ac29 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=lRRARb1z|85K4ZbW84ddmv3xzk^5qDa6jfU>a7M|0V($#SDR%7#(9;Z_u6{1- HoD!M<`Sol& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1ea28daf681ded1618bbe8b0920563cf1a8bd805 GIT binary patch literal 601 zcmd5)ze^iY9Dfb1Xi(~0kO;P&1c#g6P;yBwxtvYokEXf)5Om8TL^2q+Cpy?tCj)nb z0l`9-%4t2?4jpbN9h%C6+M^ICbnt=0L#oIVJbdrl)X~2{`26C-XZ*YxFPeSvJ8^_i zpOrTzJHFVV-rd#F{a=J26dSaRRB54kv_H|+A4kRyYwhk;G}zVVa`UZmxqSP~*r@lc z4B<6Rr#fC{y^dzkar}k#x>}m%@f)`4XgT^6AFx$dOHl(iSi#YokpDvth?K=p_EmhFw2RNAWwh<@h=J9CNni`YD>j>ulEXvy{|6w%YeA zpPK`<`BwX0ap1Hpdc$eX~TIm5|a1o zp_ukyRN6c!gNUjj@zo3Q$AhA*^S}lfB|_Lyrbv2V$uUmKa2idAu&Znlc<@M8IRvUM zf^Epi1cwqFMnMQ4R8^2|FlCa%6Zjj!OLTJ(-RRa)tbg&k$K#RM#jW2HLtm#h=l-7v MS=plDrDvA@0jhEDaR2}S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ff757062926aa072b3046a4f7d3e15197a1e0c6 GIT binary patch literal 73 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1SHiab7}%90Z$jlkcv6UKh7UuaOXKv$gplQ V|HljQ%85WZ22WQ%mvv4FO#t~b5qSUr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9ffbe24dbafcb95b4b5388d69bb2c20c154923b3 GIT binary patch literal 1625 zcmXAqdpwhS9LHBDtkzsQk?Te>WRjF3y1C34a;XWWnY-f>$1Q9x)Cna~ZnL#6BDv)n zPAD8T*G8?gWYaXMTpO}D>v(?s@w}hkAJ6Cg{d|A_JQ*jQaL6sHTVOC45^rbiy8iZq z9=P;6BL#WxFqpI^-uk$EJbi}ap9qI-KlZ`kFXPf;Qi!>3S#de5+<0t^OeTbdTyu;w zF&kryn2i|^yk5{<{y=FyDo7qwP|#WyxchdFMAhOp$@G5oC#Mcv@`(xz=_c7OSK_kk z*bVkmjx&AE`t=Wus@vLgILYdpx9JEc^3GUz(=R);5hEXalT3AZc zrcQm<3d>z8nay%;u2FVu-;iPFlOUa;ZZa>P+=i;bKFdoPJv8l-V#vy_T{&$PP}w?I zcwA%ZtWYY(`RS}G{h_*#;nAsY zdF1I=)r;j4)SP=)IaRK=ruak(Dfp#_wz^Cpv7N{79$E5lF<+C%g>wA8z8GMWU*+FG zI?t=Iue3L|5AVrBBYksEk|qdSuJ;p4-}T{ho8P@ZueVxEE;&Z^_o7*)K9qQG zv?#PE3q|N??a09*pX6er6DSr(d#}zQn~f!^x~h$VxcpC*@oXmdFlBshZV2X#GY?>- zed_zeo!64txxt3@F8<(k?xSy_H!A05LwW`_$C+`v3m9B}?YE`9lhYF1vu;gkQ%A=# zbwuzQMh=x|>bmRkRxV9rSHhrqcUxP3#Y5Bgw8cWVfyDYgA)wC6OQ6)6$L_ovVlKa*}TMJni}nr`M)y zg^AcYKb2_7`nX1IoF2-XN#_yqXYOcF+mB4luqp46;z4fgM8@vIYmNK>lb;@^F$3PL zSIai@F{3u3xl<1*Yx{Kq4*c_=OT$(1OD7*4kr3yr_dVdJ(E9~z+HktRm5G2KE-Ge4 zn`%CojNA2O&cU@n8BU#8i45N;*XbI0H&k!cVe))e#{sN`4_zN|Pnlh1KIXupjO5HSY)i1sv^G^c`U5TpdBAP}5$qL+M zbup!|VI>E%pm2}GuqO`-aI_%LBR69=vKx{xVeoswVuB~idgNPCLlDIMRB#rgmkLdS zjRBtn<|#P#1rGCe((;E&E^9-4gWGS&#II&Rx+Wz^SEUH)Xum;x=LB>zk^%G$vt?APcTEY%^q_=>iWL zc(QH5UVx;ofXi@gJtBvISz<^6-5G_0FbkQ Z?x9$ZzraV+)_)l=yp5A}^@;PT{{xX|CoKQ~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..92ae1ace93d08550158e4a1668316d25339318c4 GIT binary patch literal 1160 zcmV;31b6$1P)I4g(1l1PmMn4POY_NZ9yE+y6@5220@$Oym?z=NnGyB2VopQ1CEO^EXoVK2!NcRQya;|5H{1T2}>O zSO{oY3vXHuc3TmBToi;|7mHpRl3yK}U?8AjBd1~}u45^+WGuX8FT-Xs%4apvXgJtu zJKt(P=4(OiY((^JNBeF{0&h(Ta8M9&Qx|ep9&=eGbX+WTUo&=MI(KD3cxXs@YfpM^ zR(o+>d~{@ecWZuna({h!fPjF3gNK5Kj)RGngp8bpkE4c?s)v=ah?uyEo4<;l#*3lN zjHJ|!r`wLI;*YKAkg)KPv-gs={*$={l)Ma;zY~_i8kfZ(n8+xZ%P*SFHk;8soYX{} z*Grz-QlH&fpx|Jk<7cAgZlmdSr0jg9?}MiDil_CFsQ8$v`=6@*rmF$1tOT^J2fMBc z!mkafcUAM>^%`nM$kxF`s@D-XIZ7P~PWyfh@eH!Hq6GQT}J zz(7F3Lr20!PQyu6#7tbpPh-YXYR6S^$XIyETYt)4hRb1%%w&|!XPeGyqR(xq&~UKP zbGOoVzSDWd)O^g`{}mkZvS65pK};GiJkqbK5~ zF5{^+(Bb`*8B0@{Pg7g_v`-p^8fw$S0`5r0003)Nklh9_7>z^=j(&Q;qr%j(R zbJpxRbLY)puyE1hB}NRWEt>3V5)8;K(w{73CbJy-Yd-v^UIB@XL;UhSj*GJF{ENnazX=>n?^ztQ<_GCgd!K)*2gbaFidChdF&C`;LWtGutkG` YA>|4iFVdQ&MBb@0P9^ODgXcg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d07093c8e1ef570fb7e45a5b4263b8dfef97a1c GIT binary patch literal 722 zcmV;@0xkWCP)>@{R^p*GO;M{{D5V_b=l3{zes;{HVOlyC6Do|E zN2v~F5`f0+x}c%}H0EV6n}A)+`!h)dAW08%DhZ&GzN%%REyE%`ntyCrOv@Kt}1|jMOrOIX~y8g|YNG%&wMY z>Fb%7mSyRCneC5~r~ou(nR#JG0ccFwmc`1XZdr;<_?DH0dAcFH!(?GLomUosDm}_M zwPo#IhDCZf!yrISdc>?hHl)iaCt@H zve=eU$eHH#Oe{HXhZ&b)$r;VlEyI#?bpEkrS^B%oV(D1S_--W-fW}PIvS^r4Va&t| zVCw#6mSySvIsJMjw4A5{q@}mCEQMthay||*EyI?xHb13hS^7L? zbIY>ycbMB>&$L?r9MjUVm{4KN?0j*6q5w1|ZOc++Lbt3e%=7OP3fL_@oKY13E&Yv_ zg|G~Z^l%PcfTHwh=F+m5mr=;s{wNVF!Qec=cS zKx1BOS26);OzD=z$fR#sYD|ijm4$h_A-lt5VLqN!7l0@|$~nDdS^9T5hgufuGAz== zc_aZ@q=)B}mSN9XlwQxolJjYPxMf-T8fMtCEd71v9}M{?W;fdHO8@`>07*qoM6N<$ Ef)=`5ng9R* literal 0 HcmV?d00001 diff --git a/tests/fixtures/manual-edge-cases/truncated.png b/tests/fixtures/manual-edge-cases/truncated.png new file mode 100644 index 0000000000000000000000000000000000000000..029ace0fcbb58feb758971feed0457fd34dbb60b GIT binary patch literal 16 XcmeAS@N?(olHy`uVBq!ia0vnc8m