Skip to content

Commit cf8d2f6

Browse files
Kratos2k7Umarnadeem7222dazzatronus
authored
fix: Set ctx.fontVariationSettings = '"wght" 700' in both the node an… (#92)
* fix: Set ctx.fontVariationSettings = '"wght" 700' in both the node and web painters when rendering DrawCaptionWord ops. This explicitly tells the font renderer to use the wght axis at the requested value. --------- Co-authored-by: Kratos2k7 <umernadeem321@gmail.com> Co-authored-by: dazzatronus <derkzomer@gmail.com>
1 parent 321e42c commit cf8d2f6

3 files changed

Lines changed: 54 additions & 49 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
},
9898
"dependencies": {
9999
"@shotstack/schemas": "1.9.3",
100-
"@shotstack/shotstack-canvas": "^2.1.10",
100+
"@shotstack/shotstack-canvas": "^2.1.12",
101101
"howler": "^2.2.4",
102102
"mediabunny": "^1.11.2",
103103
"opentype.js": "^1.3.4",

src/components/canvas/players/rich-caption-player.ts

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import {
1111
generateRichCaptionFrame,
1212
createDefaultGeneratorConfig,
1313
createWebPainter,
14+
buildCaptionLayoutConfig,
1415
parseSubtitleToWords,
1516
CanvasRichCaptionAssetSchema,
1617
type CanvasRichCaptionAsset,
1718
type CaptionLayout,
18-
type CaptionLayoutConfig,
1919
type RichCaptionGeneratorConfig,
2020
type WordTiming
2121
} from "@shotstack/shotstack-canvas";
@@ -217,8 +217,9 @@ export class RichCaptionPlayer extends Player {
217217
this.validatedAsset = canvasValidation.data;
218218

219219
const { width, height } = this.getSize();
220-
const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
221-
const canvasTextMeasurer = this.createCanvasTextMeasurer();
220+
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);
221+
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
222+
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
222223
if (canvasTextMeasurer) {
223224
layoutConfig.measureTextWidth = canvasTextMeasurer;
224225
}
@@ -250,9 +251,10 @@ export class RichCaptionPlayer extends Player {
250251
this.layoutEngine = new CaptionLayoutEngine(this.fontRegistry);
251252

252253
const { width, height } = this.getSize();
253-
const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
254+
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);
254255

255-
const canvasTextMeasurer = this.createCanvasTextMeasurer();
256+
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
257+
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
256258
if (canvasTextMeasurer) {
257259
layoutConfig.measureTextWidth = canvasTextMeasurer;
258260
}
@@ -500,51 +502,16 @@ export class RichCaptionPlayer extends Player {
500502
return payload;
501503
}
502504

503-
private buildLayoutConfig(asset: CanvasRichCaptionAsset, frameWidth: number, frameHeight: number): CaptionLayoutConfig {
504-
const { font, style, align, padding: rawPadding } = asset;
505-
506-
let padding: { top: number; right: number; bottom: number; left: number };
507-
if (typeof rawPadding === "number") {
508-
padding = { top: rawPadding, right: rawPadding, bottom: rawPadding, left: rawPadding };
509-
} else if (rawPadding) {
510-
const p = rawPadding as { top?: number; right?: number; bottom?: number; left?: number };
511-
padding = { top: p.top ?? 0, right: p.right ?? 0, bottom: p.bottom ?? 0, left: p.left ?? 0 };
512-
} else {
513-
padding = { top: 0, right: 0, bottom: 0, left: 0 };
514-
}
515-
516-
const totalHorizontalPadding = padding.left + padding.right;
517-
const availableWidth = totalHorizontalPadding > 0 ? frameWidth - totalHorizontalPadding : frameWidth * 0.9;
518-
519-
const fontSize = font?.size ?? 24;
520-
const lineHeight = style?.lineHeight ?? 1.2;
521-
const availableHeight = frameHeight - padding.top - padding.bottom;
522-
const maxLines = Math.max(1, Math.min(10, Math.floor(availableHeight / (fontSize * lineHeight))));
523-
524-
return {
525-
frameWidth,
526-
frameHeight,
527-
availableWidth,
528-
maxLines,
529-
verticalAlign: align?.vertical ?? "middle",
530-
horizontalAlign: align?.horizontal ?? "center",
531-
padding,
532-
fontSize,
533-
fontFamily: font?.family ?? "Roboto",
534-
fontWeight: String(font?.weight ?? "400"),
535-
letterSpacing: style?.letterSpacing ?? 0,
536-
lineHeight,
537-
textTransform: (style?.textTransform as CaptionLayoutConfig["textTransform"]) ?? "none",
538-
pauseThreshold: this.resolvedPauseThreshold
539-
};
540-
}
541-
542-
private createCanvasTextMeasurer(): ((text: string, font: string) => number) | undefined {
505+
private createCanvasTextMeasurer(letterSpacing?: number): ((text: string, font: string) => number) | undefined {
543506
try {
544507
const measureCanvas = document.createElement("canvas");
545508
const ctx = measureCanvas.getContext("2d");
546509
if (!ctx) return undefined;
547510

511+
if (letterSpacing) {
512+
(ctx as unknown as Record<string, unknown>)["letterSpacing"] = `${letterSpacing}px`;
513+
}
514+
548515
return (text: string, font: string): number => {
549516
ctx.font = font;
550517
return ctx.measureText(text).width;
@@ -684,8 +651,9 @@ export class RichCaptionPlayer extends Player {
684651

685652
if (!this.layoutEngine) return;
686653

687-
const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
688-
const canvasTextMeasurer = this.createCanvasTextMeasurer();
654+
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);
655+
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
656+
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
689657
if (canvasTextMeasurer) {
690658
layoutConfig.measureTextWidth = canvasTextMeasurer;
691659
}

tests/rich-caption-player.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,43 @@ jest.mock("@shotstack/shotstack-canvas", () => {
190190
}),
191191
createWebPainter: (...args: unknown[]) => mockCreateWebPainter(...args),
192192
parseSubtitleToWords: (...args: unknown[]) => mockParseSubtitleToWords(...args),
193+
buildCaptionLayoutConfig: (asset: Record<string, unknown>, frameWidth: number, frameHeight: number) => {
194+
const rawPadding = asset['padding'] as number | { top?: number; right?: number; bottom?: number; left?: number } | undefined;
195+
let padding: { top: number; right: number; bottom: number; left: number };
196+
if (typeof rawPadding === "number") {
197+
padding = { top: rawPadding, right: rawPadding, bottom: rawPadding, left: rawPadding };
198+
} else if (rawPadding) {
199+
padding = { top: rawPadding.top ?? 0, right: rawPadding.right ?? 0, bottom: rawPadding.bottom ?? 0, left: rawPadding.left ?? 0 };
200+
} else {
201+
padding = { top: 0, right: 0, bottom: 0, left: 0 };
202+
}
203+
const font = asset['font'] as { size?: number; family?: string; weight?: number | string } | undefined;
204+
const style = asset['style'] as { letterSpacing?: number; lineHeight?: number; textTransform?: string } | undefined;
205+
const align = asset['align'] as { vertical?: string; horizontal?: string } | undefined;
206+
const fontSize = font?.size ?? 24;
207+
const lineHeight = style?.lineHeight ?? 1.2;
208+
const availableWidth = frameWidth - padding.left - padding.right;
209+
const availableHeight = frameHeight - padding.top - padding.bottom;
210+
const maxLines = Math.max(1, Math.min(10, Math.floor(availableHeight / (fontSize * lineHeight))));
211+
const vertical = align?.vertical;
212+
const horizontal = align?.horizontal;
213+
return {
214+
frameWidth,
215+
frameHeight,
216+
availableWidth,
217+
maxLines,
218+
verticalAlign: (() => { if (vertical === "top") return "top"; if (vertical === "middle") return "middle"; return "bottom"; })(),
219+
horizontalAlign: (() => { if (horizontal === "left") return "left"; if (horizontal === "right") return "right"; return "center"; })(),
220+
padding,
221+
fontSize,
222+
fontFamily: font?.family ?? "Roboto",
223+
fontWeight: String(font?.weight ?? "400"),
224+
letterSpacing: style?.letterSpacing ?? 0,
225+
lineHeight,
226+
textTransform: style?.textTransform ?? "none",
227+
pauseThreshold: (asset['pauseThreshold'] as number) ?? 500,
228+
};
229+
},
193230
CanvasRichCaptionAssetSchema: {
194231
safeParse: jest.fn().mockImplementation((asset: unknown) => ({
195232
success: true,
@@ -1324,7 +1361,7 @@ describe("RichCaptionPlayer", () => {
13241361
await player.load();
13251362

13261363
const layoutConfig = mockLayoutCaption.mock.calls[0]?.[1];
1327-
expect(layoutConfig.availableWidth).toBe(1920 * 0.9);
1364+
expect(layoutConfig.availableWidth).toBe(1920);
13281365
expect(layoutConfig.padding).toEqual({ top: 0, right: 0, bottom: 0, left: 0 });
13291366
});
13301367

0 commit comments

Comments
 (0)