diff --git a/docs/guides/color-grading.mdx b/docs/guides/color-grading.mdx index 7abca7425ac..5bc45a0be71 100644 --- a/docs/guides/color-grading.mdx +++ b/docs/guides/color-grading.mdx @@ -98,6 +98,9 @@ labelled `original` unless you pass `--no-baseline`. Up to sixteen cells land four to a row in `grade-compare.png`, or wherever `--out` points. Swap `--grades` for `--luts looks/a.cube,looks/b.cube` to compare LUT files the same way. +Phone photos keep their displayed orientation and aspect ratio in the sheet; +the source image file stays unchanged. + {/* VISUAL NEEDED (4): a real grade-compare.png sheet — the labelled grid with `original` in the first cell. */} diff --git a/packages/cli/src/commands/grade-compare.test.ts b/packages/cli/src/commands/grade-compare.test.ts index a321a859ad0..bb94c7b00e7 100644 --- a/packages/cli/src/commands/grade-compare.test.ts +++ b/packages/cli/src/commands/grade-compare.test.ts @@ -3,11 +3,13 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { HF_COLOR_GRADING_ATTR, serializeHfColorGrading } from "@hyperframes/core"; +import sharp from "sharp"; import { describe, expect, it, vi } from "vitest"; import { buildGradeCompareHtml, buildGradeCompareSuccessPayload, capCandidateCells, + loadReferenceFrame, parseGradeCompareArgs, parseGradesFile, prepareGradeCompareTempProject, @@ -134,6 +136,69 @@ describe("buildGradeCompareHtml", () => { }); }); +describe("loadReferenceFrame", () => { + it.each([ + { orientation: 1, width: 160, height: 80 }, + { orientation: 2, width: 160, height: 80 }, + { orientation: 3, width: 160, height: 80 }, + { orientation: 4, width: 160, height: 80 }, + { orientation: 5, width: 80, height: 160 }, + { orientation: 6, width: 80, height: 160 }, + { orientation: 7, width: 80, height: 160 }, + { orientation: 8, width: 80, height: 160 }, + ])("uses displayed dimensions for JPEG orientation $orientation", async (expected) => { + const dir = tempDir(); + try { + const framePath = join(dir, "phone.jpg"); + const buffer = await sharp({ + create: { width: 160, height: 80, channels: 3, background: "red" }, + }) + .jpeg() + .withMetadata({ orientation: expected.orientation }) + .toBuffer(); + writeFileSync(framePath, buffer); + + const frame = await loadReferenceFrame(framePath); + + expect(frame).toMatchObject({ width: expected.width, height: expected.height }); + expect(frame.buffer).toEqual(buffer); + expect(readFileSync(framePath)).toEqual(buffer); + const html = buildGradeCompareHtml({ + cells: [{ label: "warm", grading: { preset: "warm-daylight" } }], + frameSrc: frame.stagedName, + frameWidth: frame.width, + frameHeight: frame.height, + }); + const sheetHeight = expected.width === 80 ? 1184 : 344; + expect(html).toContain(`data-height="${sheetHeight}"`); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("keeps the dimensions and bytes of a PNG without orientation metadata", async () => { + const dir = tempDir(); + try { + const framePath = join(dir, "frame.png"); + const buffer = await sharp({ + create: { width: 160, height: 80, channels: 3, background: "red" }, + }) + .png() + .toBuffer(); + writeFileSync(framePath, buffer); + + expect(await loadReferenceFrame(framePath)).toEqual({ + buffer, + width: 160, + height: 80, + stagedName: "frame.png", + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + describe("prepareGradeCompareTempProject", () => { it("copies the frame and LUTs into a temp project and rewrites LUT src values", async () => { const dir = tempDir(); diff --git a/packages/cli/src/commands/grade-compare.ts b/packages/cli/src/commands/grade-compare.ts index c6501119af6..3d3cc144f3c 100644 --- a/packages/cli/src/commands/grade-compare.ts +++ b/packages/cli/src/commands/grade-compare.ts @@ -515,7 +515,7 @@ async function extractVideoFrameToBuffer(videoPath: string): Promise { +export async function loadReferenceFrame(framePath: string): Promise { if (!existsSync(framePath)) { throw new Error(`Reference frame not found: ${framePath}`); } @@ -528,15 +528,15 @@ async function loadReferenceFrame(framePath: string): Promise { } const sharp = await loadSharp("grade-compare"); - const metadata = await sharp(buffer).metadata(); - if (!metadata.width || !metadata.height) { + const { width, height } = (await sharp(buffer).metadata()).autoOrient; + if (!width || !height) { throw new Error(`Could not read reference frame dimensions: ${framePath}`); } return { buffer, - width: metadata.width, - height: metadata.height, + width, + height, stagedName: isVideoPath(framePath) ? "frame.png" : frameFileNameForPath(framePath), }; }