Ship composition families, optional background images for lower-corner templates, and an optional blurred cover fill for classic letterbox.
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import {
|
|
BLUR_AMOUNT_MAX,
|
|
INVALID_LAYOUT_TEMPLATE_MESSAGE,
|
|
blurToBoxblur,
|
|
boxblurFilterSegment,
|
|
buildArtTrackFilterComplex,
|
|
buildClassicBlurFillFilterComplex,
|
|
clampBlurAmount,
|
|
clampTextPadding,
|
|
clampTitleArtistGap,
|
|
computeLayoutGeometry,
|
|
normalizeLayoutSettings,
|
|
requiresArtTrackLayoutEntitlement,
|
|
} from "../lib/layout";
|
|
import { sanitizeDrawtext } from "../lib/watermark";
|
|
|
|
function testEnumsAndClamp() {
|
|
assert.equal(clampBlurAmount(150), BLUR_AMOUNT_MAX);
|
|
assert.equal(clampBlurAmount(-1), 0);
|
|
assert.equal(clampTextPadding(999), 120);
|
|
assert.equal(clampTextPadding(1), 16);
|
|
assert.equal(clampTitleArtistGap(100), 64);
|
|
assert.equal(clampTitleArtistGap(-5), 0);
|
|
|
|
assert.equal(blurToBoxblur(0), null);
|
|
const mid = blurToBoxblur(50);
|
|
assert.ok(mid && mid.radius >= 1 && mid.power >= 1);
|
|
assert.ok(boxblurFilterSegment(60).includes("boxblur="));
|
|
assert.equal(boxblurFilterSegment(0), "");
|
|
}
|
|
|
|
function testNormalize() {
|
|
const classic = normalizeLayoutSettings(null);
|
|
assert.equal(classic.template, null);
|
|
assert.equal(classic.blurFill, false);
|
|
assert.equal(classic.titleArtistGap, 10);
|
|
assert.equal(classic.textOffsetX, 0);
|
|
assert.equal(classic.titleBold, true);
|
|
|
|
assert.equal(
|
|
normalizeLayoutSettings({ blur_fill: true }).blurFill,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
normalizeLayoutSettings({
|
|
template: "CENTERED_COMPACT",
|
|
blurFill: true,
|
|
}).blurFill,
|
|
false,
|
|
);
|
|
|
|
assert.equal(
|
|
normalizeLayoutSettings({
|
|
layout_template: "CENTERED_COMPACT",
|
|
blur_amount: 80,
|
|
title_artist_gap: 24,
|
|
text_offset_y: -20,
|
|
}).titleArtistGap,
|
|
24,
|
|
);
|
|
|
|
assert.throws(
|
|
() => normalizeLayoutSettings({ template: "NOT_A_TEMPLATE" }),
|
|
(err: Error) => err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE,
|
|
);
|
|
assert.throws(
|
|
() => normalizeLayoutSettings({ template: "COVER_LEFT_TEXT_RIGHT", x: 10 }),
|
|
(err: Error) => err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE,
|
|
);
|
|
|
|
assert.equal(
|
|
requiresArtTrackLayoutEntitlement(normalizeLayoutSettings({ template: null })),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
requiresArtTrackLayoutEntitlement(
|
|
normalizeLayoutSettings({ template: "COVER_TOP_TEXT_BOTTOM" }),
|
|
),
|
|
true,
|
|
);
|
|
}
|
|
|
|
function testGeometryAndFilter() {
|
|
const geo = computeLayoutGeometry("COVER_TOP_TEXT_BOTTOM", 1920, 1080, 48, 16, 10, -5);
|
|
assert.ok(geo.coverMaxW > 1600, "top layout cover should be near full width");
|
|
assert.ok(geo.artistY !== geo.titleY);
|
|
|
|
const withGap = computeLayoutGeometry("COVER_LEFT_TEXT_RIGHT", 1280, 720, 40, 40, 0, 0);
|
|
const tight = computeLayoutGeometry("COVER_LEFT_TEXT_RIGHT", 1280, 720, 40, 0, 0, 0);
|
|
assert.ok(Number(withGap.artistY) - Number(withGap.titleY) > Number(tight.artistY) - Number(tight.titleY));
|
|
|
|
const lowerLeft = computeLayoutGeometry("LOWER_LEFT_COVER_TEXT", 1280, 720, 48, 10, 0, 0);
|
|
assert.equal(lowerLeft.coverX, "48");
|
|
assert.equal(lowerLeft.coverY, "H-h-48");
|
|
assert.ok(Number(lowerLeft.titleX) > Number(lowerLeft.coverX));
|
|
assert.ok(Number(lowerLeft.titleY) > 300, "title should sit in the lower half with the cover");
|
|
|
|
const lowerRight = computeLayoutGeometry("LOWER_RIGHT_COVER_TEXT", 1280, 720, 48, 10, 0, 0);
|
|
assert.equal(lowerRight.coverX, "W-w-48");
|
|
assert.equal(lowerRight.coverY, "H-h-48");
|
|
assert.ok(String(lowerRight.titleX).startsWith("W-text_w-"));
|
|
|
|
const fc = buildArtTrackFilterComplex({
|
|
width: 1280,
|
|
height: 720,
|
|
layout: {
|
|
template: "CENTERED_COMPACT",
|
|
blurFill: false,
|
|
blurAmount: 40,
|
|
blurOpacity: 70,
|
|
textPadding: 40,
|
|
titleArtistGap: 18,
|
|
textOffsetX: 5,
|
|
textOffsetY: -8,
|
|
titleBold: true,
|
|
},
|
|
titleEscaped: sanitizeDrawtext("Hello:World"),
|
|
artistEscaped: sanitizeDrawtext("Artist"),
|
|
fontfileEscaped: null,
|
|
titleFontfileEscaped: "C\\:/fonts/Arimo-Bold.ttf",
|
|
});
|
|
assert.ok(fc.includes("split=2"));
|
|
assert.ok(fc.includes("blend=") || fc.includes("[blurred]"));
|
|
assert.ok(fc.includes("overlay="));
|
|
assert.ok(fc.includes("drawtext="));
|
|
assert.ok(fc.includes("fontfile='C\\:/fonts/Arimo-Bold.ttf'"));
|
|
assert.ok(fc.endsWith("[laid]"));
|
|
|
|
const regular = buildArtTrackFilterComplex({
|
|
width: 1280,
|
|
height: 720,
|
|
layout: {
|
|
template: "CENTERED_COMPACT",
|
|
blurFill: false,
|
|
blurAmount: 40,
|
|
blurOpacity: 70,
|
|
textPadding: 40,
|
|
titleArtistGap: 18,
|
|
textOffsetX: 0,
|
|
textOffsetY: 0,
|
|
titleBold: false,
|
|
},
|
|
titleEscaped: "Hi",
|
|
artistEscaped: null,
|
|
fontfileEscaped: "C\\:/fonts/Arimo-Regular.ttf",
|
|
});
|
|
assert.ok(regular.includes("fontfile='C\\:/fonts/Arimo-Regular.ttf'"));
|
|
assert.equal(regular.includes("borderw="), false);
|
|
|
|
const withSeparateBg = buildArtTrackFilterComplex({
|
|
width: 1280,
|
|
height: 720,
|
|
layout: {
|
|
template: "LOWER_LEFT_COVER_TEXT",
|
|
blurFill: false,
|
|
blurAmount: 40,
|
|
blurOpacity: 100,
|
|
textPadding: 48,
|
|
titleArtistGap: 10,
|
|
textOffsetX: 0,
|
|
textOffsetY: 0,
|
|
titleBold: true,
|
|
},
|
|
titleEscaped: "Song",
|
|
artistEscaped: "Artist",
|
|
separateBackgroundInputIndex: 2,
|
|
});
|
|
assert.equal(withSeparateBg.includes("[0:v]split=2"), false);
|
|
assert.ok(withSeparateBg.includes("[2:v]"));
|
|
assert.ok(withSeparateBg.includes("[0:v]scale="));
|
|
assert.ok(withSeparateBg.includes("overlay="));
|
|
|
|
const withoutSeparateBg = buildArtTrackFilterComplex({
|
|
width: 1280,
|
|
height: 720,
|
|
layout: {
|
|
template: "LOWER_LEFT_COVER_TEXT",
|
|
blurFill: false,
|
|
blurAmount: 40,
|
|
blurOpacity: 100,
|
|
textPadding: 48,
|
|
titleArtistGap: 10,
|
|
textOffsetX: 0,
|
|
textOffsetY: 0,
|
|
titleBold: true,
|
|
},
|
|
titleEscaped: "Song",
|
|
artistEscaped: null,
|
|
});
|
|
assert.ok(withoutSeparateBg.includes("[0:v]split=2"));
|
|
|
|
const classicBlur = buildClassicBlurFillFilterComplex({
|
|
width: 1280,
|
|
height: 720,
|
|
blurAmount: 55,
|
|
blurOpacity: 100,
|
|
});
|
|
assert.ok(classicBlur.includes("[0:v]split=2"));
|
|
assert.ok(classicBlur.includes("overlay=(W-w)/2:(H-h)/2"));
|
|
assert.equal(classicBlur.includes("pad="), false);
|
|
assert.equal(classicBlur.includes("drawtext="), false);
|
|
assert.ok(classicBlur.endsWith("[laid]"));
|
|
}
|
|
|
|
testEnumsAndClamp();
|
|
testNormalize();
|
|
testGeometryAndFilter();
|
|
console.log("layout.test.ts: ok");
|