diff --git a/CHANGELOG.md b/CHANGELOG.md index 6672e8b2..e2f93e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,11 @@ 82 instead of 95 ms), with identical output: the predictors run as one loop per mode instead of a switch per pixel, residual costs come from a table, and the LZ77 search hashes every pixel pair once. +### Fixes + +- The outline of `Image.renderText` (`strokeWidth`) showed through translucent text, it now only shows around the + glyphs (opaque text renders as before). + ## 3.2.1 ### Performance diff --git a/src/image.ts b/src/image.ts index aa403ddc..d9ebc6f9 100644 --- a/src/image.ts +++ b/src/image.ts @@ -2095,14 +2095,14 @@ export class Image { // the outline goes around the color glyphs as well let shape = coverage; - if (colored && strokeWidth) { + if (colored) { shape = coverage.slice(); for (let i = 0; i < shape.length; i++) shape[i] = Math.max(shape[i], colored[i * 4 + 3]); } const stroke = strokeWidth ? outline(shape, width, height, strokeWidth) : null; const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor); - // color glyphs over the text (premultiplied, with the text's alpha), both over their outline + // color glyphs over the text (premultiplied, with the text's alpha), both within their outline for (let i = 0; i < coverage.length; i++) { let af = coverage[i] / 255 * a / 255, pr = r * af, pg = g * af, pb = b * af; if (colored && colored[i * 4 + 3]) { @@ -2113,7 +2113,9 @@ export class Image { af = ca + af * keep; } if (stroke) { - const as = stroke[i] * sa / 255 * (1 - af); + // the outline shows around the glyphs only, not through translucent ones (for opaque text it's the + // same as drawing the glyphs over it) + const as = stroke[i] * sa / 255 * (1 - shape[i] / 255); pr += sr * as; pg += sg * as; pb += sb * as; diff --git a/tests/regressions.js b/tests/regressions.js index 8bcb7226..f0b5660d 100644 --- a/tests/regressions.js +++ b/tests/regressions.js @@ -457,6 +457,18 @@ const rgba = (image, x, y) => Image.colorToRGBA(image.getPixelAt(x, y)); if (color === 0xffffffff) white++; } assert(red > 100 && white > 100, `outlined text should contain both colors (${red} red, ${white} white)`); + + // the stroke goes around translucent text, it doesn't show through it + const translucent = await Image.renderText(font, 40, 'Hi', 0x0000ff80, undefined, {strokeWidth: 3, strokeColor: 0xff0000ff}); + const fill = await Image.renderText(font, 40, 'Hi', 0x0000ff80); + let inside = 0, tinted = 0; + // pixels the glyphs cover fully (the stroked image is 3 pixels larger on every side) + for (const [x, y, color] of fill.iterateWithColors()) { + if (color !== 0x0000ff80) continue; + inside++; + if (translucent.getPixelAt(x + 3, y + 3) !== 0x0000ff80) tinted++; + } + assert(inside > 100 && !tinted, `translucent text should show no stroke inside (${tinted} of ${inside} pixels tinted)`); } // PNGs are oriented by their eXIf chunk, ImageMagick's Exif text or XMP, in that order