mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 08:13:46 +00:00
fix: do not tint custom SVGs that have an opaque background
A grayscale SVG with an opaque full-canvas background (for example an exported logo with a white background rect and a black glyph) passed the monochrome check and was drawn through a CSS mask. Masks key off the alpha channel, so the opaque background filled the whole area with the tint color and the icon collapsed into a solid block. Detect a full-canvas opaque background rect and exclude such SVGs from tinting, rendering them with their own colors instead. Transparent single-color and multi-shade glyphs remain tintable. Add tests for the background cases.
This commit is contained in:
parent
7c4e06a372
commit
85260671fc
2 changed files with 104 additions and 3 deletions
|
|
@ -81,6 +81,44 @@ describe('isMonochromeSvg', () => {
|
|||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('opaque background (not tintable as a mask)', () => {
|
||||
it('rejects a white background rect with a black glyph', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><rect width="24" height="24" fill="#ffffff" /><path fill="#000" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a black background rect with a white glyph', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 48 48"><rect width="48" height="48" fill="black" /><path fill="white" d="M0 0h10v10H0z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a full-size percentage background rect', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><rect x="0" y="0" width="100%" height="100%" fill="#eee" /><path fill="#222" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a default-fill (opaque black) background rect', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 16 16"><rect width="16" height="16" /><path fill="#fff" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('still tints a glyph drawn with small rects on a transparent background', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" fill="#333" /><rect x="14" y="14" width="6" height="6" fill="#333" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores a transparent background rect', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><rect width="24" height="24" fill="none" /><path fill="#333" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeSvg', () => {
|
||||
|
|
|
|||
|
|
@ -103,12 +103,75 @@ function extractColors(svg: string): string[] {
|
|||
return colors;
|
||||
}
|
||||
|
||||
const VIEWBOX_REGEX = /viewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)/i;
|
||||
const RECT_REGEX = /<rect\b[^>]*>/gi;
|
||||
|
||||
function getAttr(tag: string, name: string): string | null {
|
||||
const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, 'i'));
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function coversCanvas(value: string | null, canvas: number | null): boolean {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value.trim() === '100%') {
|
||||
return true;
|
||||
}
|
||||
const size = parseFloat(value);
|
||||
if (Number.isNaN(size)) {
|
||||
return false;
|
||||
}
|
||||
return canvas != null && size >= canvas * 0.98;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when an SVG only contains grayscale colors (or relies on the
|
||||
* default black fill / `currentColor`), meaning it can be safely tinted to match
|
||||
* the theme. Multi-color logos return false so their colors are preserved.
|
||||
* Detects a full-canvas opaque background (a `<rect>` at the origin spanning the
|
||||
* viewBox). Such SVGs cannot be tinted via a CSS mask, since the opaque
|
||||
* background fills the whole area with the tint color instead of the glyph.
|
||||
*/
|
||||
function hasOpaqueBackground(svg: string): boolean {
|
||||
const viewBox = svg.match(VIEWBOX_REGEX);
|
||||
const width = viewBox ? parseFloat(viewBox[1]) : null;
|
||||
const height = viewBox ? parseFloat(viewBox[2]) : null;
|
||||
|
||||
const rects = svg.match(RECT_REGEX) ?? [];
|
||||
for (const rect of rects) {
|
||||
const fill = getAttr(rect, 'fill');
|
||||
if (fill === 'none' || fill === 'transparent') {
|
||||
continue;
|
||||
}
|
||||
const fillOpacity = getAttr(rect, 'fill-opacity');
|
||||
const opacity = getAttr(rect, 'opacity');
|
||||
if (fillOpacity != null && parseFloat(fillOpacity) === 0) {
|
||||
continue;
|
||||
}
|
||||
if (opacity != null && parseFloat(opacity) === 0) {
|
||||
continue;
|
||||
}
|
||||
if (parseFloat(getAttr(rect, 'x') ?? '0') > 0 || parseFloat(getAttr(rect, 'y') ?? '0') > 0) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
coversCanvas(getAttr(rect, 'width'), width) &&
|
||||
coversCanvas(getAttr(rect, 'height'), height)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when an SVG can be safely tinted to match the theme: it only
|
||||
* contains grayscale colors (or relies on the default black fill /
|
||||
* `currentColor`) and has no opaque background. Multi-color logos and icons with
|
||||
* an opaque background return false so they are rendered with their own colors.
|
||||
*/
|
||||
export function isMonochromeSvg(svg: string): boolean {
|
||||
if (hasOpaqueBackground(svg)) {
|
||||
return false;
|
||||
}
|
||||
const colors = extractColors(svg);
|
||||
if (colors.length === 0) {
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue