[object Object]

← back to CelebritySignatures

wear: fix stale ink-color fallback in reconcile-wear-orders.mjs + its test

8322ca0e39eedc0036bf4ac13fa2a520af72863a · 2026-09-09 20:16:44 -0700 · Steve

test/wear-artwork-flow.test.mjs (committed 07:58 today) asserted the old
fixed #f5f3ee recolor tint. Commit 18cca456 (16:13 today) deliberately
changed server.js's dark-cloth fallback to #ffffff to match the on-shirt
preview's inkHexFor() exactly — but the test was never updated, so it was
failing on HEAD (verified via git blame: the two commits are unrelated,
the test simply predates the ink-choice feature).

Root-caused by reading both implementations rather than guessing:
server.js's #ffffff is correct/intentional (matches public/wear.html's
inkHexFor, "dark cloth, no pick → white"); the old #f5f3ee was pre-feature
scaffolding. So the fix is NOT touching server.js — it's:

1. scripts/reconcile-wear-orders.mjs: renamed recolorSignatureToLight() ->
   recolorSignatureInk(imageUrl, hex) and added safeInkHex(), mirroring
   server.js's current signature/cache-key/fallback exactly (was still on
   the pre-buyer-choice, fixed-#f5f3ee version — the two producers had
   drifted out of parity since the ink-color feature landed).
2. test/wear-artwork-flow.test.mjs: updated the two stale pixel assertions
   from #F5F3EE(FF/00) to #FFFFFF(FF/00) to match current intended
   behavior.

Verified: both producer subtests (success + reconciler) now pass; node
--check clean. Did not touch server.js, public/index.html, or the
unrelated wear-accessories.test.mjs failures (separate, pre-existing,
in another session's active hat/accessory work).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files touched

Diff

commit 8322ca0e39eedc0036bf4ac13fa2a520af72863a
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 20:16:44 2026 -0700

    wear: fix stale ink-color fallback in reconcile-wear-orders.mjs + its test
    
    test/wear-artwork-flow.test.mjs (committed 07:58 today) asserted the old
    fixed #f5f3ee recolor tint. Commit 18cca456 (16:13 today) deliberately
    changed server.js's dark-cloth fallback to #ffffff to match the on-shirt
    preview's inkHexFor() exactly — but the test was never updated, so it was
    failing on HEAD (verified via git blame: the two commits are unrelated,
    the test simply predates the ink-choice feature).
    
    Root-caused by reading both implementations rather than guessing:
    server.js's #ffffff is correct/intentional (matches public/wear.html's
    inkHexFor, "dark cloth, no pick → white"); the old #f5f3ee was pre-feature
    scaffolding. So the fix is NOT touching server.js — it's:
    
    1. scripts/reconcile-wear-orders.mjs: renamed recolorSignatureToLight() ->
       recolorSignatureInk(imageUrl, hex) and added safeInkHex(), mirroring
       server.js's current signature/cache-key/fallback exactly (was still on
       the pre-buyer-choice, fixed-#f5f3ee version — the two producers had
       drifted out of parity since the ink-color feature landed).
    2. test/wear-artwork-flow.test.mjs: updated the two stale pixel assertions
       from #F5F3EE(FF/00) to #FFFFFF(FF/00) to match current intended
       behavior.
    
    Verified: both producer subtests (success + reconciler) now pass; node
    --check clean. Did not touch server.js, public/index.html, or the
    unrelated wear-accessories.test.mjs failures (separate, pre-existing,
    in another session's active hat/accessory work).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 scripts/reconcile-wear-orders.mjs | 32 ++++++++++++++++++++++----------
 test/wear-artwork-flow.test.mjs   |  8 ++++++--
 2 files changed, 28 insertions(+), 12 deletions(-)

diff --git a/scripts/reconcile-wear-orders.mjs b/scripts/reconcile-wear-orders.mjs
index fb605ad..fe3a4be 100644
--- a/scripts/reconcile-wear-orders.mjs
+++ b/scripts/reconcile-wear-orders.mjs
@@ -70,20 +70,27 @@ function wearOrderItems(order) {
   return [{ qid: order.qid, signature_name: order.signature_name, garment: order.garment, garment_label: order.garment_label,
     color: order.color, color_label: order.color_label, size: order.size, placement: order.placement }];
 }
-// Mirrors server.js's wearLuminance()/recolorSignatureToLight() exactly —
-// see that file for the full explanation (Printify prints a design's own
-// pixel colors as-is, so a dark-ink signature on a dark garment needs
-// recoloring to a light ink before it's actually printed). Fails open: any
-// error falls back to the original image rather than blocking reconciliation.
+// Mirrors server.js's wearLuminance()/safeInkHex()/recolorSignatureInk()
+// exactly (see that file for the full explanation) — a dark-ink signature on
+// a dark garment needs recoloring or it prints near-invisible (Printify
+// prints a design's own pixel colors as-is). Recolor ONLY when there's a
+// reason: the buyer picked an ink color, or the cloth is dark (white,
+// matching the on-shirt preview's inkHexFor()) — otherwise the ORIGINAL art
+// ships untouched. Fails open: any recolor error falls back to the original
+// image rather than blocking reconciliation.
 const wearLuminance = hex => {
   const n = parseInt(hex.slice(1), 16), r = (n>>16)&255, g = (n>>8)&255, b = n&255;
   return (0.299*r + 0.587*g + 0.114*b) / 255;
 };
+// Sanitize a stored inkColor before it can reach ImageMagick's -fill; only a
+// literal #rrggbb is allowed, anything else is treated as "no choice made".
+const safeInkHex = v => (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v)) ? v.toLowerCase() : null;
 const RECOLOR_DIR = join(ROOT, 'public', 'assets', 'recolored-signatures');
 const WEAR_SITE_ORIGIN = 'https://celebsignatures.com';
-async function recolorSignatureToLight(imageUrl) {
+async function recolorSignatureInk(imageUrl, hex) {
+  const ink = safeInkHex(hex) || '#f5f3ee';   // fall back to the old light ink
   try {
-    const hash = createHash('sha256').update(imageUrl).digest('hex').slice(0, 24);
+    const hash = createHash('sha256').update(imageUrl + '|' + ink).digest('hex').slice(0, 24);
     const outPath = join(RECOLOR_DIR, `${hash}.png`);
     // FULLY QUALIFIED — flows into the Printify order payload as images[0].src,
     // which Printify fetches from the public internet (caught via a2a from
@@ -96,7 +103,7 @@ async function recolorSignatureToLight(imageUrl) {
     const out = await new Promise((resolve, reject) => {
       // 'convert' not 'magick': the Kamatera prod box only has ImageMagick 6
       // (convert), not the IMv7 'magick' unified CLI — verified via SSH.
-      const proc = spawn('convert', ['-background', 'none', '-', '-channel', 'RGB', '-fill', '#f5f3ee', '-colorize', '100%', 'png:-']);
+      const proc = spawn('convert', ['-background', 'none', '-', '-channel', 'RGB', '-fill', ink, '-colorize', '100%', 'png:-']);
       const chunks = []; let errText = '';
       const timer = setTimeout(() => { proc.kill(); reject(new Error('magick timeout')); }, 8000);
       proc.stdout.on('data', c => chunks.push(c));
@@ -109,7 +116,7 @@ async function recolorSignatureToLight(imageUrl) {
     await writeFile(outPath, out);
     return outUrl;
   } catch (e) {
-    console.error('recolorSignatureToLight failed, using original image:', e.message);
+    console.error('recolorSignatureInk failed, using original image:', e.message);
     return imageUrl;
   }
 }
@@ -162,7 +169,12 @@ async function main() {
         if (designUrl) {
           const g = (draftTpl.garments || []).find(x => x.id === it.garment);
           const c = g && (g.colors || []).find(x => x.id === it.color);
-          if (c && wearLuminance(c.hex) < 0.4) designUrl = await recolorSignatureToLight(designUrl);
+          const darkCloth = !!(c && wearLuminance(c.hex) < 0.4);
+          // Recolor ONLY when there's a reason: the buyer picked a color, or
+          // the cloth is dark (a dark signature would vanish → white).
+          // Otherwise keep the ORIGINAL art in its true source colors.
+          const ink = safeInkHex(it.inkColor) || (darkCloth ? '#ffffff' : null);
+          if (ink) designUrl = await recolorSignatureInk(designUrl, ink);
         }
         const draft = { draftedAt: new Date().toISOString(), orderId: compositeId, status: 'DRAFT_UNSENT', provider,
           recipient_email: order.email, recipient: order.recipient, garment: it.garment, color: it.color, size: it.size,
diff --git a/test/wear-artwork-flow.test.mjs b/test/wear-artwork-flow.test.mjs
index 343c220..f2cf4f0 100644
--- a/test/wear-artwork-flow.test.mjs
+++ b/test/wear-artwork-flow.test.mjs
@@ -108,8 +108,12 @@ globalThis.fetch = async (input, options = {}) => {
           assert.equal(png.subarray(1, 4).toString(), 'PNG');
         }
         const pixels = await exec('convert', [join(work, 'public', new URL(drafts[0].design_image_url).pathname), 'txt:-']);
-        assert.match(pixels.stdout, /10,5:.*#F5F3EEFF/i, 'opaque signature pixel has light ink');
-        assert.match(pixels.stdout, /0,0:.*#F5F3EE00/i, 'transparent background preserved');
+        // No buyer inkColor on this fixture + dark (black) cloth → white ink,
+        // matching public/wear.html's inkHexFor() exactly (server.js's
+        // recolorSignatureInk call site, 18cca456: darkCloth with no explicit
+        // pick recolors to '#ffffff', not the old fixed '#f5f3ee').
+        assert.match(pixels.stdout, /10,5:.*#FFFFFFFF/i, 'opaque signature pixel has light ink');
+        assert.match(pixels.stdout, /0,0:.*#FFFFFF00/i, 'transparent background preserved');
         if (producer === 'success') {
           for (const id of [91001, 91002]) await fetch(`${base}/wear-success?sid=cs_test_${id}`);
         } else await reconcile();

← 71f5347 wear: surface the Signature Edit — ✦ badge + 'Signature Edit  ·  back to CelebritySignatures  ·  signature-edit: expand The Signature Edit lookbook from 10 → 7bce96c →