返回 JoyAI-Echo
cropImageToVideoSize.ts
根目录 / echo_longvideo / Director_Agent / webui / src / lib / cropImageToVideoSize.ts
1 /** Center-crop an image to a target aspect ratio, then scale to pixel size. */
2
3 export type VideoSize = { width: number; height: number };
4
5 export type CropRect = {
6 sx: number;
7 sy: number;
8 sw: number;
9 sh: number;
10 };
11
12 /** Largest centered source rect that matches ``targetW/targetH`` aspect. */
13 export function computeCenterCropRect(
14 srcWidth: number,
15 srcHeight: number,
16 targetWidth: number,
17 targetHeight: number,
18 ): CropRect {
19 if (srcWidth <= 0 || srcHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
20 return { sx: 0, sy: 0, sw: Math.max(0, srcWidth), sh: Math.max(0, srcHeight) };
21 }
22 const targetRatio = targetWidth / targetHeight;
23 const srcRatio = srcWidth / srcHeight;
24 if (srcRatio > targetRatio) {
25 const sw = srcHeight * targetRatio;
26 return { sx: (srcWidth - sw) / 2, sy: 0, sw, sh: srcHeight };
27 }
28 const sh = srcWidth / targetRatio;
29 return { sx: 0, sy: (srcHeight - sh) / 2, sw: srcWidth, sh };
30 }
31
32 function loadBitmap(source: Blob): Promise<ImageBitmap> {
33 if (typeof createImageBitmap === "function") {
34 return createImageBitmap(source);
35 }
36 return new Promise((resolve, reject) => {
37 const url = URL.createObjectURL(source);
38 const img = new Image();
39 img.onload = () => {
40 const canvas = document.createElement("canvas");
41 canvas.width = img.naturalWidth || img.width;
42 canvas.height = img.naturalHeight || img.height;
43 const ctx = canvas.getContext("2d");
44 if (!ctx) {
45 URL.revokeObjectURL(url);
46 reject(new Error("canvas unavailable"));
47 return;
48 }
49 ctx.drawImage(img, 0, 0);
50 URL.revokeObjectURL(url);
51 createImageBitmap(canvas).then(resolve, reject);
52 };
53 img.onerror = () => {
54 URL.revokeObjectURL(url);
55 reject(new Error("decode_failed"));
56 };
57 img.src = url;
58 });
59 }
60
61 function blobFromCanvas(
62 canvas: HTMLCanvasElement,
63 type: string,
64 ): Promise<Blob> {
65 return new Promise((resolve, reject) => {
66 canvas.toBlob(
67 (blob) => {
68 if (!blob) {
69 reject(new Error("toBlob failed"));
70 return;
71 }
72 resolve(blob);
73 },
74 type,
75 0.92,
76 );
77 });
78 }
79
80 /**
81 * Center-crop ``source`` to ``target`` aspect ratio and draw at target pixels.
82 * Resizes when the source is smaller than the target.
83 */
84 export async function cropImageToVideoSize(
85 source: File | Blob,
86 target: VideoSize,
87 ): Promise<{ blob: Blob; width: number; height: number }> {
88 const bitmap = await loadBitmap(source);
89 try {
90 const { sx, sy, sw, sh } = computeCenterCropRect(
91 bitmap.width,
92 bitmap.height,
93 target.width,
94 target.height,
95 );
96 const canvas = document.createElement("canvas");
97 canvas.width = target.width;
98 canvas.height = target.height;
99 const ctx = canvas.getContext("2d");
100 if (!ctx) {
101 throw new Error("canvas unavailable");
102 }
103 ctx.imageSmoothingEnabled = true;
104 ctx.imageSmoothingQuality = "high";
105 ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, target.width, target.height);
106
107 // Always JPEG so dense PNG/WebP sources stay under gateway body limits.
108 const blob = await blobFromCanvas(canvas, "image/jpeg");
109 return { blob, width: target.width, height: target.height };
110 } finally {
111 bitmap.close?.();
112 }
113 }
114
114 lines TYPESCRIPT