fix(icons): generate non-transparent PNGs\n\n- prefer rsvg-convert/inkscape with fallback to ImageMagick\n- inject explicit stroke/fill on <path> to avoid inheritance issues\n- document rasterizer behavior in icons/README
This commit is contained in:
		
					parent
					
						
							
								16b422fc66
							
						
					
				
			
			
				commit
				
					
						6ba833769c
					
				
			
		
					 2 changed files with 51 additions and 19 deletions
				
			
		| 
						 | 
					@ -15,7 +15,7 @@ Run `npm run build` (or `npm run build:icons`) to regenerate. The manifest is wi
 | 
				
			||||||
Notes
 | 
					Notes
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
- Source: `icons/file-search.svg` uses `stroke="currentColor"`.
 | 
					- Source: `icons/file-search.svg` uses `stroke="currentColor"`.
 | 
				
			||||||
- The build script replaces `currentColor` with configured colors and rasterizes with a transparent background.
 | 
					- The build script replaces `currentColor` with configured colors and rasterizes with a transparent background. It also injects explicit `stroke`/`fill` on each path for better compatibility across renderers.
 | 
				
			||||||
- Default stroke colors use the project emphasis palette:
 | 
					- Default stroke colors use the project emphasis palette:
 | 
				
			||||||
  - Add‑on icons (48/96/128): Primary 2 (deep) `#223544`.
 | 
					  - Add‑on icons (48/96/128): Primary 2 (deep) `#223544`.
 | 
				
			||||||
  - Toolbar icons (16/32): Primary 1 (light) `#5AC3D6`.
 | 
					  - Toolbar icons (16/32): Primary 1 (light) `#5AC3D6`.
 | 
				
			||||||
| 
						 | 
					@ -24,3 +24,4 @@ Notes
 | 
				
			||||||
  - `ICON_COLOR_TOOLBAR` (16/32); default `#5AC3D6`.
 | 
					  - `ICON_COLOR_TOOLBAR` (16/32); default `#5AC3D6`.
 | 
				
			||||||
- Firefox does not require .ico; PNG is recommended.
 | 
					- Firefox does not require .ico; PNG is recommended.
 | 
				
			||||||
- Theme variants can be added later via `browser_action.theme_icons`.
 | 
					- Theme variants can be added later via `browser_action.theme_icons`.
 | 
				
			||||||
 | 
					 - Rasterizer: the script will use `rsvg-convert` if available, then `inkscape`, and falls back to ImageMagick (`magick`/`convert`). Install one of these locally.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,12 +28,22 @@ const TOOLBAR_SIZES = [16, 32];
 | 
				
			||||||
const COLOR_ADDON = process.env.ICON_COLOR_ADDON || '#223544';
 | 
					const COLOR_ADDON = process.env.ICON_COLOR_ADDON || '#223544';
 | 
				
			||||||
const COLOR_TOOLBAR = process.env.ICON_COLOR_TOOLBAR || '#5AC3D6';
 | 
					const COLOR_TOOLBAR = process.env.ICON_COLOR_TOOLBAR || '#5AC3D6';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ensureMagick() {
 | 
					function which(cmd, args = ['--version']) {
 | 
				
			||||||
  // Prefer calling binaries directly to avoid shell init output polluting stdout.
 | 
					  try {
 | 
				
			||||||
  let ok = spawnSync('magick', ['-version']);
 | 
					    const res = spawnSync(cmd, args, { stdio: 'ignore' });
 | 
				
			||||||
  if (ok && ok.status === 0) return 'magick';
 | 
					    return res && res.status === 0;
 | 
				
			||||||
  ok = spawnSync('convert', ['-version']);
 | 
					  } catch (_) {
 | 
				
			||||||
  if (ok && ok.status === 0) return 'convert';
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ensureRasterizer() {
 | 
				
			||||||
 | 
					  // Prefer rsvg-convert (librsvg), then Inkscape, then ImageMagick.
 | 
				
			||||||
 | 
					  if (which('rsvg-convert', ['-v'])) return { kind: 'rsvg', bin: 'rsvg-convert' };
 | 
				
			||||||
 | 
					  // Inkscape 1.x CLI: --pipe reads from stdin
 | 
				
			||||||
 | 
					  if (which('inkscape', ['--version'])) return { kind: 'inkscape', bin: 'inkscape' };
 | 
				
			||||||
 | 
					  if (which('magick', ['-version'])) return { kind: 'magick', bin: 'magick' };
 | 
				
			||||||
 | 
					  if (which('convert', ['-version'])) return { kind: 'magick', bin: 'convert' };
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,24 +56,45 @@ function readSvg() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function colorize(svg, color) {
 | 
					function colorize(svg, color) {
 | 
				
			||||||
  // Replace currentColor with explicit hex color. Keep case-insensitive safety.
 | 
					  // Replace currentColor with explicit hex color and inject explicit stroke attributes on each path
 | 
				
			||||||
  return svg.replace(/currentColor/gi, color);
 | 
					  let s = svg.replace(/currentColor/gi, color);
 | 
				
			||||||
 | 
					  // Ensure each <path> has explicit stroke and fill to avoid inheritance issues in some renderers
 | 
				
			||||||
 | 
					  s = s.replace(/<path\b([^>]*?)\/>/g, (m, attrs) => {
 | 
				
			||||||
 | 
					    let a = attrs;
 | 
				
			||||||
 | 
					    if (!/\bstroke\s*=/.test(a)) a = ` stroke=\"${color}\"` + a;
 | 
				
			||||||
 | 
					    if (!/\bstroke-width\s*=/.test(a)) a = ` stroke-width=\"1.8\"` + a;
 | 
				
			||||||
 | 
					    if (!/\bstroke-linecap\s*=/.test(a)) a = ` stroke-linecap=\"round\"` + a;
 | 
				
			||||||
 | 
					    if (!/\bstroke-linejoin\s*=/.test(a)) a = ` stroke-linejoin=\"round\"` + a;
 | 
				
			||||||
 | 
					    if (!/\bfill\s*=/.test(a)) a = ` fill=\"none\"` + a;
 | 
				
			||||||
 | 
					    return `<path${a}/>`;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function rasterize(magickBin, svgString, size, outPath) {
 | 
					function rasterize(raster, svgString, size, outPath) {
 | 
				
			||||||
  // Use ImageMagick, transparent background, high density for crisp vector rasterization.
 | 
					  let args;
 | 
				
			||||||
  const args = ['-background', 'none', '-density', '384', 'svg:-', '-resize', `${size}x${size}`, outPath];
 | 
					  let cmd = raster.bin;
 | 
				
			||||||
  const proc = spawnSync(magickBin, args, { input: svgString, stdio: ['pipe', 'inherit', 'inherit'] });
 | 
					  if (raster.kind === 'rsvg') {
 | 
				
			||||||
 | 
					    // rsvg-convert reads SVG and outputs PNG; specify size and transparent background
 | 
				
			||||||
 | 
					    args = ['-f', 'png', '-w', String(size), '-h', String(size), '--background-color=transparent', '-o', outPath, '-'];
 | 
				
			||||||
 | 
					  } else if (raster.kind === 'inkscape') {
 | 
				
			||||||
 | 
					    // Inkscape 1.x: --export-type=png --export-filename=out --export-width/height, read from stdin via --pipe
 | 
				
			||||||
 | 
					    args = ['--export-type=png', `--export-filename=${outPath}`, `--export-width=${size}`, `--export-height=${size}`, '--pipe'];
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    // ImageMagick
 | 
				
			||||||
 | 
					    args = ['-background', 'none', '-density', '384', 'svg:-', '-resize', `${size}x${size}`, outPath];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const proc = spawnSync(cmd, args, { input: svgString, stdio: ['pipe', 'inherit', 'inherit'] });
 | 
				
			||||||
  if (proc.status !== 0) {
 | 
					  if (proc.status !== 0) {
 | 
				
			||||||
    console.error(`Failed to generate ${outPath}`);
 | 
					    console.error(`Failed to generate ${outPath} using ${raster.kind} (${cmd}).`);
 | 
				
			||||||
    process.exit(proc.status || 1);
 | 
					    process.exit(proc.status || 1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function main() {
 | 
					function main() {
 | 
				
			||||||
  const magick = ensureMagick();
 | 
					  const raster = ensureRasterizer();
 | 
				
			||||||
  if (!magick) {
 | 
					  if (!raster) {
 | 
				
			||||||
    console.error('ImageMagick not found. Please install it or adjust this script to use inkscape/rsvg-convert.');
 | 
					    console.error('No SVG rasterizer found. Install one of: rsvg-convert, inkscape, or ImageMagick (magick/convert).');
 | 
				
			||||||
    process.exit(1);
 | 
					    process.exit(1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!fs.existsSync(ICONS_DIR)) fs.mkdirSync(ICONS_DIR, { recursive: true });
 | 
					  if (!fs.existsSync(ICONS_DIR)) fs.mkdirSync(ICONS_DIR, { recursive: true });
 | 
				
			||||||
| 
						 | 
					@ -74,7 +105,7 @@ function main() {
 | 
				
			||||||
  const addonSvg = colorize(base, COLOR_ADDON);
 | 
					  const addonSvg = colorize(base, COLOR_ADDON);
 | 
				
			||||||
  for (const s of ADDON_SIZES) {
 | 
					  for (const s of ADDON_SIZES) {
 | 
				
			||||||
    const out = path.join(ICONS_DIR, `icon-${s}.png`);
 | 
					    const out = path.join(ICONS_DIR, `icon-${s}.png`);
 | 
				
			||||||
    rasterize(magick, addonSvg, s, out);
 | 
					    rasterize(raster, addonSvg, s, out);
 | 
				
			||||||
    console.log(`Generated ${path.relative(REPO_ROOT, out)}`);
 | 
					    console.log(`Generated ${path.relative(REPO_ROOT, out)}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,7 +113,7 @@ function main() {
 | 
				
			||||||
  const toolbarSvg = colorize(base, COLOR_TOOLBAR);
 | 
					  const toolbarSvg = colorize(base, COLOR_TOOLBAR);
 | 
				
			||||||
  for (const s of TOOLBAR_SIZES) {
 | 
					  for (const s of TOOLBAR_SIZES) {
 | 
				
			||||||
    const out = path.join(ICONS_DIR, `icon-${s}.png`);
 | 
					    const out = path.join(ICONS_DIR, `icon-${s}.png`);
 | 
				
			||||||
    rasterize(magick, toolbarSvg, s, out);
 | 
					    rasterize(raster, toolbarSvg, s, out);
 | 
				
			||||||
    console.log(`Generated ${path.relative(REPO_ROOT, out)}`);
 | 
					    console.log(`Generated ${path.relative(REPO_ROOT, out)}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue