Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/canvas/players/caption-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Player, PlayerType } from "@canvas/players/player";
import { type Cue, findActiveCue } from "@core/captions";
import type { Edit } from "@core/edit-session";
import { parseFontFamily, resolveFontPath } from "@core/fonts/font-config";
import { sanitizeColor } from "@core/shared/color-utils";
import { isAliasReference } from "@core/timing/types";
import { type Size, type Vector } from "@layouts/geometry";
import { SubtitleLoadParser, type SubtitleAsset } from "@loaders/subtitle-load-parser";
Expand Down Expand Up @@ -48,7 +49,7 @@ export class CaptionPlayer extends Player {
if (captionAsset.stroke?.width && captionAsset.stroke.width > 0 && captionAsset.stroke.color) {
const strokeFilter = new pixiFilters.OutlineFilter({
thickness: captionAsset.stroke.width,
color: captionAsset.stroke.color
color: sanitizeColor(captionAsset.stroke.color)
});
this.text.filters = [strokeFilter];
}
Expand Down Expand Up @@ -146,7 +147,7 @@ export class CaptionPlayer extends Player {
return new pixi.TextStyle({
fontFamily: baseFontFamily,
fontSize,
fill: captionAsset.font?.color ?? "#ffffff",
fill: sanitizeColor(captionAsset.font?.color),
fontWeight: fontWeight.toString() as pixi.TextStyleFontWeight,
wordWrap: true,
wordWrapWidth: width * 0.9,
Expand Down Expand Up @@ -219,7 +220,7 @@ export class CaptionPlayer extends Player {

this.background.clear();
this.background.fillStyle = {
color: bgConfig.color,
color: sanitizeColor(bgConfig.color),
alpha: bgConfig.opacity ?? 0.8
};

Expand Down
9 changes: 5 additions & 4 deletions src/components/canvas/players/text-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Player, PlayerType } from "@canvas/players/player";
import { TextEditor } from "@canvas/text/text-editor";
import type { Edit } from "@core/edit-session";
import { parseFontFamily, resolveFontPath } from "@core/fonts/font-config";
import { sanitizeColor } from "@core/shared/color-utils";
import { type Size, type Vector } from "@layouts/geometry";
import { type ResolvedClip, type TextAsset } from "@schemas";
import * as pixiFilters from "pixi-filters";
Expand Down Expand Up @@ -43,7 +44,7 @@ export class TextPlayer extends Player {
if (textAsset.stroke?.width && textAsset.stroke.width > 0 && textAsset.stroke.color) {
const textStrokeFilter = new pixiFilters.OutlineFilter({
thickness: textAsset.stroke.width,
color: textAsset.stroke.color
color: sanitizeColor(textAsset.stroke.color)
});
this.text.filters = [textStrokeFilter];
}
Expand Down Expand Up @@ -85,7 +86,7 @@ export class TextPlayer extends Player {
if (textAsset.stroke?.width && textAsset.stroke.width > 0 && textAsset.stroke.color) {
const textStrokeFilter = new pixiFilters.OutlineFilter({
thickness: textAsset.stroke.width,
color: textAsset.stroke.color
color: sanitizeColor(textAsset.stroke.color)
});
this.text.filters = [textStrokeFilter];
} else {
Expand Down Expand Up @@ -157,7 +158,7 @@ export class TextPlayer extends Player {
return new pixi.TextStyle({
fontFamily: baseFontFamily,
fontSize: textAsset.font?.size ?? 32,
fill: textAsset.font?.color ?? "#ffffff",
fill: sanitizeColor(textAsset.font?.color),
fontWeight: fontWeight.toString() as pixi.TextStyleFontWeight,
wordWrap: true,
wordWrapWidth: width,
Expand Down Expand Up @@ -198,7 +199,7 @@ export class TextPlayer extends Player {
const { width, height } = this.getSize();
this.background.clear();
this.background.fillStyle = {
color: textAsset.background.color,
color: sanitizeColor(textAsset.background.color),
alpha: textAsset.background.opacity ?? 1
};
this.background.rect(0, 0, width, height);
Expand Down
30 changes: 30 additions & 0 deletions src/core/shared/color-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Color helpers for Pixi text/style construction.
*/

/** Matches Pixi Color HEX_PATTERN (3/4/6/8 hex digits, optional # or 0x). */
const HEX_PATTERN = /^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;
const FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla)\(/i;
const CSS_COLOR_NAME = /^[a-z]+$/i;

/**
* Return a Pixi-safe color string, or `fallback` when the value is missing/invalid.
* Incomplete hex (e.g. `#00`, `#0`) and empty strings fall back instead of throwing in TextStyle.
* @internal
*/
export function sanitizeColor(color: string | undefined | null, fallback = "#ffffff"): string {
if (typeof color !== "string") {
return fallback;
}

const value = color.trim();
if (!value) {
return fallback;
}

if (HEX_PATTERN.test(value) || FUNCTIONAL_COLOR.test(value) || CSS_COLOR_NAME.test(value)) {
return value;
}

return fallback;
}
27 changes: 27 additions & 0 deletions tests/color-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { sanitizeColor } from "@core/shared/color-utils";

describe("sanitizeColor", () => {
it("falls back for incomplete hex colors that crash Pixi TextStyle", () => {
expect(sanitizeColor("#00")).toBe("#ffffff");
expect(sanitizeColor("#0")).toBe("#ffffff");
expect(sanitizeColor("")).toBe("#ffffff");
expect(sanitizeColor(" ")).toBe("#ffffff");
});

it("falls back for nullish and non-string values", () => {
expect(sanitizeColor(undefined)).toBe("#ffffff");
expect(sanitizeColor(null)).toBe("#ffffff");
});

it("preserves valid hex, named, and functional colors", () => {
expect(sanitizeColor("#ffffff")).toBe("#ffffff");
expect(sanitizeColor("#fff")).toBe("#fff");
expect(sanitizeColor("#ff00ff00")).toBe("#ff00ff00");
expect(sanitizeColor("red")).toBe("red");
expect(sanitizeColor("rgb(255, 0, 0)")).toBe("rgb(255, 0, 0)");
});

it("uses a custom fallback when provided", () => {
expect(sanitizeColor("#00", "#000000")).toBe("#000000");
});
});
Loading