From d924a66c506523c089003bf2dd039310c77ed37a Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 17 Apr 2026 11:58:55 +0700 Subject: [PATCH] =?UTF-8?q?feat(importer):=20phase=206=20=E2=80=94=20skip-?= =?UTF-8?q?white=20+=20paint-transparent=20toggles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two quick quality-of-life options for logo and background-heavy imports: - Skip-white: near-white pixels (r,g,b >= threshold, default 230) are marked transparent in the output so they don't consume credits. Useful when the source has a white background you don't want to paint. - Paint-transparent: fully-transparent source pixels are treated as opaque white before quantization. Useful if the canvas background is white and you want empty regions of a logo to blend in. Both toggles compose with every existing dither method; paint-transparent runs first, so combining the two leaves originally-transparent pixels as skip (since they're synthesized white, then the skip-white pass drops them). UI adds the two checkboxes and a threshold slider shown only when skip-white is on. CLI gains --skip-white, --white-threshold, --paint-transparent. 4 new unit tests pin the behavior including the paint+skip interaction. --- .../phase-06-skip-white.md | 10 ++-- .../plan.md | 2 +- scripts/image-to-colors.js | 11 +++- src/client/components/ImageImporter.svelte | 28 +++++++++- src/lib/image-to-palette.js | 53 ++++++++++++++++--- test/lib/image-to-palette.test.js | 40 ++++++++++++++ 6 files changed, 129 insertions(+), 15 deletions(-) diff --git a/plans/260417-1125-image-importer-enhancements/phase-06-skip-white.md b/plans/260417-1125-image-importer-enhancements/phase-06-skip-white.md index 5ed2f12..f02bfd0 100644 --- a/plans/260417-1125-image-importer-enhancements/phase-06-skip-white.md +++ b/plans/260417-1125-image-importer-enhancements/phase-06-skip-white.md @@ -2,7 +2,7 @@ ## Overview - **Priority:** P3 (low, easy win) -- **Status:** Todo +- **Status:** Done - Two cheap toggles: treat near-white pixels as transparent (useful for logos on white backgrounds), and optionally paint pixels with alpha < threshold as white instead of skipping. ## Requirements @@ -40,10 +40,10 @@ Logic: 5. Tests: near-white pixel → -1 iff skipWhite on; low-alpha pixel → -1 iff paintTransparent off. ## Todo -- [ ] Extend `rgbaToPalette` options -- [ ] UI toggles + threshold -- [ ] CLI flags -- [ ] Tests +- [x] Extend `rgbaToPalette` options (`skipWhite`, `whiteThreshold`, `paintTransparent`) +- [x] UI toggles + threshold slider +- [x] CLI `--skip-white`, `--white-threshold`, `--paint-transparent` +- [x] Tests (4 new cases in image-to-palette.test.js) ## Success Criteria - With defaults, output matches pre-change output. diff --git a/plans/260417-1125-image-importer-enhancements/plan.md b/plans/260417-1125-image-importer-enhancements/plan.md index a3fc357..655f9ab 100644 --- a/plans/260417-1125-image-importer-enhancements/plan.md +++ b/plans/260417-1125-image-importer-enhancements/plan.md @@ -20,7 +20,7 @@ Grow the in-app Image Import panel into a full pre-placement pipeline: resize, t | 3 | [Transforms (flip / rotate)](phase-03-transforms.md) | Done | Medium — quick fixes without re-editing source | | 4 | [More dithering algorithms](phase-04-dithering-algorithms.md) | Done | Medium — different looks per source | | 5 | [Color correction sliders](phase-05-color-correction.md) | Done | Medium — photos benefit most | -| 6 | [Skip-white + paint-transparent toggles](phase-06-skip-white.md) | Todo | Low — easy win, often useful for logos | +| 6 | [Skip-white + paint-transparent toggles](phase-06-skip-white.md) | Done | Low — easy win, often useful for logos | Later / parked: multi-algorithm color distance (Lab/Oklab), Kuwahara smoothing, template save/load, repair mode. Revisit after Phase 6. diff --git a/scripts/image-to-colors.js b/scripts/image-to-colors.js index 48bca1f..483c892 100644 --- a/scripts/image-to-colors.js +++ b/scripts/image-to-colors.js @@ -35,6 +35,9 @@ const { values, positionals } = parseArgs({ contrast: { type: 'string', default: '0' }, saturation: { type: 'string', default: '0' }, gamma: { type: 'string', default: '1' }, + 'skip-white': { type: 'boolean', default: false }, + 'white-threshold': { type: 'string', default: '230' }, + 'paint-transparent': { type: 'boolean', default: false }, }, allowPositionals: true, }); @@ -100,7 +103,13 @@ const working = hasCorrection // sharp raw buffer has channels=4 after ensureAlpha; same layout as Canvas ImageData. const ditherMethod = values['dither-method'] ?? (values.dither ? 'floyd' : 'none'); -const indices = rgbaToPalette(working, outW, outH, { alphaThreshold, method: ditherMethod }); +const indices = rgbaToPalette(working, outW, outH, { + alphaThreshold, + method: ditherMethod, + skipWhite: values['skip-white'], + whiteThreshold: parseInt(values['white-threshold'], 10), + paintTransparent: values['paint-transparent'], +}); const pixels = Array.from(indices); const opaque = pixels.reduce((n, p) => n + (p >= 0 ? 1 : 0), 0); diff --git a/src/client/components/ImageImporter.svelte b/src/client/components/ImageImporter.svelte index c96658c..dda5bcf 100644 --- a/src/client/components/ImageImporter.svelte +++ b/src/client/components/ImageImporter.svelte @@ -26,6 +26,9 @@ let originY = $state(0); let skipMatching = $state(true); let ditherMethod = $state('none'); // see DITHER_METHODS + let skipWhite = $state(false); + let whiteThreshold = $state(230); + let paintTransparent = $state(false); // Resize controls — init to source dims on file load let resizeW = $state(0); @@ -108,7 +111,11 @@ const corrected = (brightness !== 0 || contrast !== 0 || saturation !== 0 || gamma !== 1) ? applyColorCorrection(resized, resizeW, resizeH, { brightness, contrast, saturation, gamma }) : resized; - const idx = rgbaToPalette(corrected, resizeW, resizeH, { method: ditherMethod }); + const idx = rgbaToPalette(corrected, resizeW, resizeH, { + method: ditherMethod, + skipWhite, whiteThreshold, + paintTransparent, + }); paletteIndices = idx; let count = 0; for (let i = 0; i < idx.length; i++) if (idx[i] >= 0) count++; @@ -362,6 +369,25 @@ Skip pixels that already match + +
+ + {#if skipWhite} + + {/if} +
+ +