diff --git a/packages/cli/src/utils/projectMedia.ts b/packages/cli/src/utils/projectMedia.ts index 3f9235ffa7..20990d76cd 100644 --- a/packages/cli/src/utils/projectMedia.ts +++ b/packages/cli/src/utils/projectMedia.ts @@ -9,6 +9,7 @@ import { loadProducer } from "./producer.js"; export interface ProjectMedia { videos: readonly Pick[]; images: readonly { src: string; graded?: boolean }[]; + gradedCanvas?: boolean; audios: readonly AudioElement[]; /** The bundled document the lists were read from. */ html: string; diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index c9474c783b..af21e5d9d4 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -219,6 +219,7 @@ export { // ── Media processing ─────────────────────────────────────────────────────────── export { parseVideoElements, + hasGradedCanvas, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, diff --git a/packages/engine/src/services/videoFrameExtractor.test.ts b/packages/engine/src/services/videoFrameExtractor.test.ts index 75ab91ac28..b067e8704d 100644 --- a/packages/engine/src/services/videoFrameExtractor.test.ts +++ b/packages/engine/src/services/videoFrameExtractor.test.ts @@ -16,6 +16,7 @@ import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; import { parseVideoElements, + hasGradedCanvas, parseImageElements, extractAllVideoFrames, extractVideoFramesRange, @@ -751,6 +752,10 @@ describe("parseVideoElements", () => { ["gi", true], ["pi", undefined], ]); + expect(hasGradedCanvas(html)).toBe(false); + expect( + hasGradedCanvas(``), + ).toBe(true); }); it.each([ diff --git a/packages/engine/src/services/videoFrameExtractor.ts b/packages/engine/src/services/videoFrameExtractor.ts index 2086350f1a..e885a7bcb0 100644 --- a/packages/engine/src/services/videoFrameExtractor.ts +++ b/packages/engine/src/services/videoFrameExtractor.ts @@ -105,7 +105,6 @@ export interface VideoElement { loop: boolean; hasAudio: boolean; hidden?: boolean; - /** Carries a `data-color-grading` treatment, which grades in SDR. */ graded?: boolean; } @@ -858,10 +857,16 @@ export interface ImageElement { src: string; start: number; end: number; - /** Carries a `data-color-grading` treatment, which grades in SDR. */ graded?: boolean; } +export function hasGradedCanvas(html: string): boolean { + return ( + parseHTML(unwrapTemplate(html)).document.querySelector(`canvas[${HF_COLOR_GRADING_ATTR}]`) !== + null + ); +} + export function parseImageElements(html: string): ImageElement[] { const images: ImageElement[] = []; const { document } = parseHTML(unwrapTemplate(html)); diff --git a/packages/engine/src/utils/hdr.test.ts b/packages/engine/src/utils/hdr.test.ts index 2ed982a5d4..5addacfca0 100644 --- a/packages/engine/src/utils/hdr.test.ts +++ b/packages/engine/src/utils/hdr.test.ts @@ -233,5 +233,6 @@ describe("gradingKeepsSdr", () => { expect(gradingKeepsSdr("auto", graded)).toBe(true); expect(gradingKeepsSdr("force-hdr", graded)).toBe(false); expect(gradingKeepsSdr("auto", { videos: [{}], images: [] })).toBe(false); + expect(gradingKeepsSdr("auto", { videos: [], images: [], gradedCanvas: true })).toBe(true); }); }); diff --git a/packages/engine/src/utils/hdr.ts b/packages/engine/src/utils/hdr.ts index 6e4812cac2..9448743d35 100644 --- a/packages/engine/src/utils/hdr.ts +++ b/packages/engine/src/utils/hdr.ts @@ -145,11 +145,16 @@ export function findHdrAutoPromotion( /** Grading is SDR-only, so in auto mode a composition with graded media renders SDR, as `--sdr` does. */ export function gradingKeepsSdr( hdrMode: "auto" | "force-hdr" | "force-sdr" | undefined, - media: { videos: readonly { graded?: boolean }[]; images: readonly { graded?: boolean }[] }, + media: { + videos: readonly { graded?: boolean }[]; + images: readonly { graded?: boolean }[]; + gradedCanvas?: boolean; + }, ): boolean { return ( (hdrMode ?? "auto") === "auto" && - [...media.videos, ...media.images].some(({ graded }) => graded === true) + (media.gradedCanvas === true || + [...media.videos, ...media.images].some(({ graded }) => graded === true)) ); } diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 72857aab2c..6ed1ea5ba0 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -118,6 +118,7 @@ export interface CompiledComposition { audios: AudioElement[]; images: ImageElement[]; droppedMediaIds: Set; + gradedCanvas: boolean; masterFxChain?: string; unresolvedCompositions: UnresolvedElement[]; /** Assets that resolve outside projectDir. Keys are the path used in HTML, values are absolute filesystem paths. */ @@ -2236,7 +2237,7 @@ export async function compileForRender( // within one composition file: two scenes declaring `