mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 03:43:03 +00:00
fix: preserve SVGs whose filters recolor the source
This commit is contained in:
parent
6f9dbd6229
commit
b3fbcccdee
2 changed files with 86 additions and 5 deletions
|
|
@ -549,6 +549,30 @@ describe('isMonochromeSvg', () => {
|
|||
'<svg viewBox="0 0 24 24"><style>.fx{filter:url(#dropShadow)}</style><defs><filter id="dropShadow"><feFlood flood-color="#e00" /></filter></defs><path class="fx" fill="#000" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves a glyph recolored by a feColorMatrix filter', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><defs><filter id="r"><feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" /></filter></defs><path filter="url(#r)" fill="#000" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves a glyph recolored by a feComponentTransfer filter', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><defs><filter id="ct"><feComponentTransfer><feFuncR type="linear" slope="0" intercept="1" /></feComponentTransfer></filter></defs><path filter="url(#ct)" fill="#000" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('tints a grayscale glyph with a color-neutral blur filter', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><defs><filter id="b"><feGaussianBlur stdDeviation="1" /></filter></defs><path filter="url(#b)" fill="#333" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores an unreferenced recoloring filter', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><defs><filter id="r"><feColorMatrix type="hueRotate" values="90" /></filter></defs><path fill="#333" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('opaque background (not tintable as a mask)', () => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,34 @@ const FILTER_COLOR_DEFAULTS = new Map<string, string>([
|
|||
['lighting-color', 'white'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Filter primitives whose color contribution we already model (the flood/lighting
|
||||
* color sources) or that only move, blur, or combine existing pixels without
|
||||
* introducing or remapping color, plus their light-source/merge children. Any
|
||||
* primitive outside this set (`feColorMatrix`, `feComponentTransfer`, `feImage`,
|
||||
* `feTurbulence`, ...) can recolor the source into a fixed color a mask would
|
||||
* discard, so an SVG whose rendered content references such a filter is preserved
|
||||
* rather than tinted. A safelist keeps unknown/new primitives on the safe side.
|
||||
*/
|
||||
const COLOR_SAFE_FILTER_PRIMITIVES = new Set([
|
||||
'feflood',
|
||||
'fedropshadow',
|
||||
'fediffuselighting',
|
||||
'fespecularlighting',
|
||||
'fedistantlight',
|
||||
'fepointlight',
|
||||
'fespotlight',
|
||||
'femerge',
|
||||
'femergenode',
|
||||
'fegaussianblur',
|
||||
'feoffset',
|
||||
'fetile',
|
||||
'femorphology',
|
||||
'fecomposite',
|
||||
'feblend',
|
||||
'fedisplacementmap',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Shapes that render with SVG's default black fill when none is supplied. Includes
|
||||
* `polyline` (SVG closes it for fill painting) but not `line`, which has no area.
|
||||
|
|
@ -568,11 +596,13 @@ function collectFilterPrimitiveColors(filter: Element, rules: StyleRule[]): stri
|
|||
return colors;
|
||||
}
|
||||
|
||||
function collectFilterColors(
|
||||
/** The set of `<filter>` elements referenced by rendered (visible, non-template)
|
||||
* content, so only filters that actually paint are inspected. */
|
||||
function referencedFilters(
|
||||
root: Element,
|
||||
rules: StyleRule[],
|
||||
referenceUses: Map<Element, Element[][]>,
|
||||
): string[] {
|
||||
): Set<Element> {
|
||||
const filters = new Set<Element>();
|
||||
for (const el of [root, ...Array.from(root.querySelectorAll('*'))]) {
|
||||
if (
|
||||
|
|
@ -590,11 +620,33 @@ function collectFilterColors(
|
|||
}
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
function filterColors(filters: Set<Element>, rules: StyleRule[]): string[] {
|
||||
return normalizeColors(
|
||||
Array.from(filters).flatMap((filter) => collectFilterPrimitiveColors(filter, rules)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a referenced filter contains a primitive that can recolor its input
|
||||
* beyond the flood/lighting sources we collect, since we cannot predict the tone it
|
||||
* produces (e.g. `feColorMatrix` mapping a black glyph to a fixed red logo). Such an
|
||||
* SVG must keep its colors rather than be flattened to a `currentColor` mask.
|
||||
*/
|
||||
function filtersRecolor(filters: Set<Element>): boolean {
|
||||
for (const filter of filters) {
|
||||
for (const el of Array.from(filter.querySelectorAll('*'))) {
|
||||
const name = el.nodeName.toLowerCase();
|
||||
if (name.startsWith('fe') && !COLOR_SAFE_FILTER_PRIMITIVES.has(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximates a selector's CSS specificity as one comparable number (ids, then
|
||||
* classes/attributes/pseudo-classes, then type selectors), so the cascade can pick
|
||||
|
|
@ -1004,8 +1056,9 @@ function hasDefaultBlackUse(root: Element, rules: StyleRule[]): boolean {
|
|||
* raster (`<image>`) or foreign (`<foreignObject>`) content, has no opaque
|
||||
* background, and resolves to a single grayscale tone (or relies on the default
|
||||
* black fill / `currentColor`). Anything with a second tone (a background shape,
|
||||
* an accent, or a second shade), a chromatic color, or that is unparseable
|
||||
* returns false, since a CSS mask would flatten it to a solid block.
|
||||
* an accent, or a second shade), a chromatic color, a filter that recolors its
|
||||
* input, or that is unparseable returns false, since a CSS mask would flatten it
|
||||
* to a solid block.
|
||||
*/
|
||||
export function isMonochromeSvg(svg: string): boolean {
|
||||
const root = parseSvgRoot(svg);
|
||||
|
|
@ -1020,10 +1073,14 @@ export function isMonochromeSvg(svg: string): boolean {
|
|||
return false;
|
||||
}
|
||||
const referenceUses = referenceMap(root, rules);
|
||||
const filters = referencedFilters(root, rules, referenceUses);
|
||||
if (filtersRecolor(filters)) {
|
||||
return false;
|
||||
}
|
||||
const levels = new Set<number>();
|
||||
for (const color of [
|
||||
...collectColors(root, rules, referenceUses),
|
||||
...collectFilterColors(root, rules, referenceUses),
|
||||
...filterColors(filters, rules),
|
||||
]) {
|
||||
if (color === CURRENT_COLOR) {
|
||||
levels.add(CURRENT_COLOR_TONE);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue