feat(core): add color grading schema and lut parsing

This commit is contained in:
ukimsanov
2026-06-16 13:41:28 -07:00
parent ca3cab340b
commit 3109acb88a
11 changed files with 922 additions and 0 deletions
@@ -17,6 +17,29 @@ function makeTempProject(files: Record<string, string>): string {
return dir;
}
function makeColorGradingProject(lutSrc: string, files: Record<string, string> = {}): string {
return makeTempProject({
"index.html": `<!doctype html>
<html><body>
<div data-composition-id="root" data-width="320" data-height="180">
<video
id="clip"
src="clip.mp4"
data-color-grading='{"lut":{"src":"${lutSrc}","intensity":0.5}}'></video>
</div>
<script>window.__timelines = window.__timelines || {}; window.__timelines.root = {}</script>
</body></html>`,
...files,
});
}
function readBundledColorGradingLutSrc(bundled: string): string | undefined {
const { document } = parseHTML(bundled);
const rawLook = document.getElementById("clip")?.getAttribute("data-color-grading") ?? "";
const parsed = JSON.parse(rawLook) as { lut?: { src?: string } };
return parsed.lut?.src;
}
// Mirror the repo convention (preview.test.ts): skip symlink cases on
// non-symlink-privileged Windows runners rather than crash the suite.
function tryCreateSymlink(target: string, path: string, type: "dir" | "file"): boolean {
@@ -921,6 +944,26 @@ describe("bundleToSingleHtml", () => {
expect(bundled).toContain("margin: 0");
});
it("inlines cube LUT files referenced from data-color-grading", async () => {
const dir = makeColorGradingProject("assets/luts/identity.cube", {
"assets/luts/identity.cube": `LUT_3D_SIZE 2
0 0 0
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1`,
});
const bundled = await bundleToSingleHtml(dir);
const lutSrc = readBundledColorGradingLutSrc(bundled);
expect(lutSrc).toMatch(/^data:text\/plain;base64,/);
expect(lutSrc).not.toContain("assets/luts/identity.cube");
});
it("resolves nested CSS @import chains", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html>
+37
View File
@@ -19,6 +19,7 @@ import { getHyperframeRuntimeScript } from "../generated/runtime-inline";
import { readDeclaredDefaults } from "../runtime/getVariables";
import { inlineSubCompositions } from "./inlineSubCompositions";
import { isSafePath, resolveWithinProject } from "../safePath.js";
import { HF_COLOR_GRADING_ATTR } from "../colorGrading";
const DEFAULT_RUNTIME_SCRIPT_URL = "";
@@ -201,6 +202,7 @@ const INLINE_MIME: Record<string, string> = {
".svg": "image/svg+xml",
".json": "application/json",
".txt": "text/plain",
".cube": "text/plain",
".xml": "application/xml",
};
@@ -219,6 +221,35 @@ function maybeInlineRelativeAssetUrl(urlValue: string, projectDir: string): stri
return appendSuffixToUrl(dataUrl, suffix);
}
function isJsonRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
// fallow-ignore-next-line complexity
function rewriteLookLutWithInlinedAssets(value: string, projectDir: string): string {
if (!value.trim().startsWith("{")) return value;
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
return value;
}
if (!isJsonRecord(parsed)) return value;
const lut = parsed.lut;
if (typeof lut === "string") {
const inlined = maybeInlineRelativeAssetUrl(lut, projectDir);
if (!inlined) return value;
parsed.lut = inlined;
return JSON.stringify(parsed);
}
if (!isJsonRecord(lut) || typeof lut.src !== "string") return value;
const inlined = maybeInlineRelativeAssetUrl(lut.src, projectDir);
if (!inlined) return value;
lut.src = inlined;
return JSON.stringify(parsed);
}
function rewriteSrcsetWithInlinedAssets(srcsetValue: string, projectDir: string): string {
if (!srcsetValue) return srcsetValue;
return srcsetValue
@@ -947,6 +978,12 @@ export async function bundleToSingleHtml(
rewriteCssUrlsWithInlinedAssets(el.getAttribute("style") || "", projectDir),
);
}
for (const el of [...document.querySelectorAll(`[${HF_COLOR_GRADING_ATTR}]`)]) {
const value = el.getAttribute(HF_COLOR_GRADING_ATTR);
if (value) {
el.setAttribute(HF_COLOR_GRADING_ATTR, rewriteLookLutWithInlinedAssets(value, projectDir));
}
}
return document.toString();
}