fix(export): parse native gradient backgrounds

This commit is contained in:
wiiiii123
2026-05-07 00:01:53 +07:00
parent 65e7bd4fff
commit 0814711d47
2 changed files with 101 additions and 14 deletions
@@ -54,6 +54,10 @@ function createExporter(overrides: Record<string, unknown> = {}) {
effectiveDurationSec: number,
) => string | null;
resolveNativeStaticLayoutBackground: () => Promise<unknown>;
createNativeStaticLayoutGradient: (
ctx: CanvasRenderingContext2D,
wallpaper: string,
) => CanvasGradient | null;
};
}
@@ -243,6 +247,26 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
expect(electronAPI.closeExportStream).toHaveBeenCalledWith("background-stream");
});
it("parses rgba color stops in native gradient backgrounds", () => {
const exporter = createExporter();
const gradient = { addColorStop: vi.fn() };
const ctx = {
createLinearGradient: vi.fn(() => gradient),
createRadialGradient: vi.fn(() => gradient),
} as unknown as CanvasRenderingContext2D;
const result = exporter.createNativeStaticLayoutGradient(
ctx,
"linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(249,202,86,1) 86.3% )",
);
expect(result).toBe(gradient);
expect(gradient.addColorStop).toHaveBeenCalledTimes(3);
expect(gradient.addColorStop).toHaveBeenNthCalledWith(1, 0, "rgba(114,167,232,1)");
expect(gradient.addColorStop).toHaveBeenNthCalledWith(2, 0.5, "rgba(253,129,82,1)");
expect(gradient.addColorStop).toHaveBeenNthCalledWith(3, 1, "rgba(249,202,86,1)");
});
it("allows non-tail trim timelines with native static-layout", () => {
const exporter = createExporter({
trimRegions: [{ id: "trim-1", startMs: 10_000, endMs: 12_000 }],
+77 -14
View File
@@ -281,6 +281,7 @@ export class ModernVideoExporter {
private maxNativeWriteInFlight = 1;
private lastNativeExportError: string | null = null;
private nativeStaticLayoutSkipReason: string | null = null;
private nativeStaticLayoutBackgroundSkipReason: string | null = null;
private nativeH264Encoder: VideoEncoder | null = null;
private nativeEncoderError: Error | null = null;
private effectiveDurationSec = 0;
@@ -318,6 +319,7 @@ export class ModernVideoExporter {
this.encoderError = null;
this.nativeEncoderError = null;
this.nativeStaticLayoutSkipReason = null;
this.nativeStaticLayoutBackgroundSkipReason = null;
this.totalExportStartTimeMs = this.getNowMs();
const backendPreference = this.config.backendPreference ?? "auto";
let useNativeEncoder = false;
@@ -1267,6 +1269,7 @@ export class ModernVideoExporter {
}
private async resolveNativeStaticLayoutBackground(): Promise<NativeStaticLayoutBackground | null> {
this.nativeStaticLayoutBackgroundSkipReason = null;
const configuredWallpaper = this.config.wallpaper?.trim() ?? "";
const wallpaper = configuredWallpaper || DEFAULT_WALLPAPER_PATH;
if (/^#?[0-9a-f]{6}$/i.test(wallpaper)) {
@@ -1277,21 +1280,38 @@ export class ModernVideoExporter {
}
if (wallpaper.startsWith("data:image/") || wallpaper.startsWith("blob:")) {
return this.materializeNativeStaticLayoutImageSource(wallpaper);
const materialized = await this.materializeNativeStaticLayoutImageSource(wallpaper);
if (materialized) {
return materialized;
}
this.nativeStaticLayoutBackgroundSkipReason =
"unsupported-background-image-materialize-failed";
return null;
}
if (wallpaper.startsWith("linear-gradient") || wallpaper.startsWith("radial-gradient")) {
return this.materializeNativeStaticLayoutGradientBackground(wallpaper);
const materialized =
await this.materializeNativeStaticLayoutGradientBackground(wallpaper);
if (materialized) {
return materialized;
}
this.nativeStaticLayoutBackgroundSkipReason =
"unsupported-background-gradient-materialize-failed";
return null;
}
if (
isVideoWallpaperSource(wallpaper) ||
wallpaper.startsWith("data:") ||
wallpaper.startsWith("blob:") ||
wallpaper.startsWith("http") ||
wallpaper.startsWith("linear-gradient") ||
wallpaper.startsWith("radial-gradient")
) {
if (isVideoWallpaperSource(wallpaper)) {
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-video";
return null;
}
if (wallpaper.startsWith("data:") || wallpaper.startsWith("blob:")) {
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-data-or-blob";
return null;
}
if (wallpaper.startsWith("http")) {
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-remote";
return null;
}
@@ -1313,7 +1333,12 @@ export class ModernVideoExporter {
}
const localPath = getLocalFilePath(wallpaper);
return localPath ? { backgroundColor: "#101010", backgroundImagePath: localPath } : null;
if (localPath) {
return { backgroundColor: "#101010", backgroundImagePath: localPath };
}
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-local-path";
return null;
}
private async materializeNativeStaticLayoutImageSource(
@@ -1414,9 +1439,12 @@ export class ModernVideoExporter {
}
const [, type, params] = gradientMatch;
const parts = params.split(",").map((part) => part.trim());
const parts = this.splitCssGradientArguments(params).map((part) => part.trim());
const colorStops = parts
.map((part) => part.match(/^(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|[a-z]+)/)?.[1])
.map(
(part) =>
part.match(/^(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-z]+)/i)?.[1],
)
.filter((color): color is string => Boolean(color));
if (colorStops.length === 0) {
return null;
@@ -1446,6 +1474,40 @@ export class ModernVideoExporter {
return gradient;
}
private splitCssGradientArguments(params: string): string[] {
const parts: string[] = [];
let current = "";
let depth = 0;
for (const char of params) {
if (char === "(") {
depth++;
current += char;
continue;
}
if (char === ")") {
depth = Math.max(0, depth - 1);
current += char;
continue;
}
if (char === "," && depth === 0) {
if (current.trim()) {
parts.push(current.trim());
}
current = "";
continue;
}
current += char;
}
if (current.trim()) {
parts.push(current.trim());
}
return parts;
}
private async writeNativeStaticLayoutTempAsset(
bytes: Uint8Array,
extension: string,
@@ -1901,7 +1963,8 @@ export class ModernVideoExporter {
}
const background = await this.resolveNativeStaticLayoutBackground();
if (!background) {
this.nativeStaticLayoutSkipReason = "unsupported-background";
this.nativeStaticLayoutSkipReason =
this.nativeStaticLayoutBackgroundSkipReason ?? "unsupported-background";
return null;
}