Skip to content

Commit debbcab

Browse files
committed
Add videoThumbnailAnimation option
1 parent df3940d commit debbcab

File tree

5 files changed

+301
-0
lines changed

5 files changed

+301
-0
lines changed

src/options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export * as style from "./style";
5858
export * as trim from "./trim";
5959
export * as unsharpMasking from "./unsharpMasking";
6060
export * as videoThumbnailSecond from "../optionsShared/videoThumbnailSecond";
61+
export * as videoThumbnailAnimation from "./videoThumbnailAnimation";
6162
export * as watermark from "./watermark";
6263
export * as watermarkShadow from "./watermarkShadow";
6364
export * as watermarkSize from "./watermarkSize";
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { VideoThumbnailAnimationOptionsPartial } from "../types/videoThumbnailAnimation";
2+
import { guardIsNotNum, guardIsUndef, normalizeBoolean } from "../utils";
3+
4+
function getOpts(options: VideoThumbnailAnimationOptionsPartial) {
5+
return options.video_thumbnail_animation || options.vta;
6+
}
7+
8+
function test(options: VideoThumbnailAnimationOptionsPartial) {
9+
return Boolean(getOpts(options));
10+
}
11+
12+
function build(options: VideoThumbnailAnimationOptionsPartial) {
13+
const vta = getOpts(options);
14+
guardIsUndef(vta, "video_thumbnail_animation");
15+
16+
guardIsNotNum(vta.step, "video_thumbnail_animation.step");
17+
guardIsNotNum(vta.delay, "video_thumbnail_animation.delay");
18+
guardIsNotNum(vta.frames, "video_thumbnail_animation.frames");
19+
20+
if (vta.frame_width !== undefined) {
21+
guardIsNotNum(vta.frame_width, "video_thumbnail_animation.frame_width");
22+
}
23+
24+
if (vta.frame_height !== undefined) {
25+
guardIsNotNum(vta.frame_height, "video_thumbnail_animation.frame_height");
26+
}
27+
28+
const parts = [];
29+
30+
// Add boolean flags with proper normalization
31+
const extend_frame =
32+
vta.extend_frame !== undefined
33+
? normalizeBoolean(vta.extend_frame)
34+
: undefined;
35+
const trim = vta.trim !== undefined ? normalizeBoolean(vta.trim) : undefined;
36+
const fill = vta.fill !== undefined ? normalizeBoolean(vta.fill) : undefined;
37+
38+
parts.push(extend_frame, trim, fill);
39+
40+
// Add focus coordinates if fill is true and coordinates are defined
41+
if (fill === "t") {
42+
parts.push(vta.focus_x, vta.focus_y);
43+
}
44+
45+
// Remove trailing undefined values
46+
while (parts.length > 0 && parts[parts.length - 1] === undefined) {
47+
parts.pop();
48+
}
49+
50+
const optionsPart = parts.length > 0 ? `:${parts.join(":")}` : "";
51+
52+
return `vta:${vta.step}:${vta.delay}:${vta.frames}:${vta.frame_width}:${vta.frame_height}${optionsPart}`;
53+
}
54+
55+
export { test, build };

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import type { StripMetadataOptionsPartial } from "./stripMetadata";
5757
import type { StyleOptionsPartial } from "./style";
5858
import type { TrimOptionsPartial } from "./trim";
5959
import type { UnsharpMaskingOptionsPartial } from "./unsharpMasking";
60+
import type { VideoThumbnailAnimationOptionsPartial } from "./videoThumbnailAnimation";
6061
import type { VideoThumbnailSecondOptionsPartial } from "../typesShared/videoThumbnailSecond";
6162
import type { WatermarkOptionsPartial } from "./watermark";
6263
import type { WatermarkRotateOptionsPartial } from "./watermarkRotate";
@@ -128,6 +129,7 @@ export type Options = AdjustOptionsPartial &
128129
StyleOptionsPartial &
129130
TrimOptionsPartial &
130131
UnsharpMaskingOptionsPartial &
132+
VideoThumbnailAnimationOptionsPartial &
131133
VideoThumbnailSecondOptionsPartial &
132134
WatermarkOptionsPartial &
133135
WatermarkShadowOptionsPartial &
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* When step is not 0, imgproxy will generate an animated image using the source video frames.
3+
*/
4+
interface VideoThumbnailAnimation {
5+
/**
6+
* step: the step of timestamp (in seconds) between video frames that should be used for the animation generation:
7+
* When step value is positive, imgproxy will use it as an absolute value
8+
* When step value is negative, imgproxy will calculate the actual step as video_duration / frames
9+
*/
10+
step: number;
11+
12+
/**
13+
* the delay between animation frames in milliseconds
14+
*/
15+
delay: number;
16+
17+
/**
18+
* the number of animation frames
19+
*/
20+
frames: number;
21+
22+
/**
23+
* the width and height of animation frames. imgproxy will resize each used frame to fit the provided size
24+
*/
25+
frame_width: number;
26+
frame_height: number;
27+
28+
/**
29+
* when set to 1, t or true, imgproxy will extend each animation frame to the requested size using a black background
30+
*/
31+
extend_frame?: boolean | 1 | string;
32+
33+
/**
34+
* when set to 1, t or true, imgproxy will trim the unused frames from the animation
35+
*/
36+
trim?: boolean | 1 | string;
37+
38+
/**
39+
* when set to 1, t or true, imgproxy will use the fill resizing type for the animation frames
40+
*/
41+
fill?: boolean | 1 | string;
42+
43+
/**
44+
* floating point numbers between 0 and 1 that define the coordinates of the center of the resulting animation frame
45+
* (as in the fp gravity type). Treat 0 and 1 as right/left for x and top/bottom for y.
46+
* Applicable only when fill is set. Default: 0.5:0.5
47+
*/
48+
focus_x?: number;
49+
focus_y?: number;
50+
}
51+
/**
52+
* *Video thumbnail animation options*. **PRO feature**
53+
*
54+
* Allows generating an animated image using the source video frames.
55+
*
56+
* @see https://docs.imgproxy.net/usage/processing#video-thumbnail-animation
57+
*/
58+
interface VideoThumbnailAnimationOptionsPartial {
59+
video_thumbnail_animation?: VideoThumbnailAnimation;
60+
vta?: VideoThumbnailAnimation;
61+
}
62+
63+
export { VideoThumbnailAnimation, VideoThumbnailAnimationOptionsPartial };
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { assertType, describe, expect, expectTypeOf, it } from "vitest";
2+
import { test, build } from "../../src/options/videoThumbnailAnimation";
3+
import { VideoThumbnailAnimation } from "../../src/types/videoThumbnailAnimation";
4+
import { Options } from "../../src/types";
5+
6+
describe("videoThumbnailAnimation", () => {
7+
describe("test", () => {
8+
it("should return true if video_thumbnail_animation option is defined", () => {
9+
const vta: VideoThumbnailAnimation = {
10+
step: 1,
11+
delay: 100,
12+
frames: 10,
13+
};
14+
expect(test({ video_thumbnail_animation: vta })).toEqual(true);
15+
});
16+
17+
it("should return true if vta option is defined", () => {
18+
const vta: VideoThumbnailAnimation = {
19+
step: 2,
20+
delay: 200,
21+
frames: 5,
22+
};
23+
expect(test({ vta })).toEqual(true);
24+
});
25+
26+
it("should return false if video_thumbnail_animation option is undefined", () => {
27+
expect(test({})).toEqual(false);
28+
});
29+
});
30+
31+
describe("build", () => {
32+
it("should throw an error if video_thumbnail_animation option is undefined", () => {
33+
expect(() => build({})).toThrow(
34+
"video_thumbnail_animation option is undefined"
35+
);
36+
});
37+
38+
it("should build basic vta option with required parameters", () => {
39+
const vta: VideoThumbnailAnimation = {
40+
step: 1.5,
41+
delay: 100,
42+
frames: 10,
43+
frame_width: 320,
44+
frame_height: 240,
45+
};
46+
expect(build({ video_thumbnail_animation: vta })).toEqual(
47+
"vta:1.5:100:10:320:240"
48+
);
49+
});
50+
51+
it("should build vta option with extend_frame as boolean", () => {
52+
const vta: VideoThumbnailAnimation = {
53+
step: 1,
54+
delay: 100,
55+
frames: 10,
56+
frame_width: 320,
57+
frame_height: 240,
58+
extend_frame: true,
59+
};
60+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
61+
});
62+
63+
it("should build vta option with extend_frame as number", () => {
64+
const vta: VideoThumbnailAnimation = {
65+
step: 1,
66+
delay: 100,
67+
frames: 10,
68+
frame_width: 320,
69+
frame_height: 240,
70+
extend_frame: 1,
71+
};
72+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
73+
});
74+
75+
it("should build vta option with extend_frame as string", () => {
76+
const vta: VideoThumbnailAnimation = {
77+
step: 1,
78+
delay: 100,
79+
frames: 10,
80+
frame_width: 320,
81+
frame_height: 240,
82+
extend_frame: "t",
83+
};
84+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
85+
});
86+
87+
it("should build vta option with trim flag", () => {
88+
const vta: VideoThumbnailAnimation = {
89+
step: 1,
90+
delay: 100,
91+
frames: 10,
92+
frame_width: 320,
93+
frame_height: 240,
94+
trim: true,
95+
};
96+
expect(build({ vta })).toEqual("vta:1:100:10:320:240::t");
97+
});
98+
99+
it("should build vta option with fill flag", () => {
100+
const vta: VideoThumbnailAnimation = {
101+
step: 1,
102+
delay: 100,
103+
frames: 10,
104+
frame_width: 320,
105+
frame_height: 240,
106+
fill: true,
107+
};
108+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t");
109+
});
110+
111+
it("should build vta option with focus coordinates", () => {
112+
const vta: VideoThumbnailAnimation = {
113+
step: 1,
114+
delay: 100,
115+
frames: 10,
116+
frame_width: 320,
117+
frame_height: 240,
118+
fill: true,
119+
focus_x: 0.3,
120+
focus_y: 0.7,
121+
};
122+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t:0.3:0.7");
123+
});
124+
125+
it("should build vta option with all parameters", () => {
126+
const vta: VideoThumbnailAnimation = {
127+
step: -1,
128+
delay: 200,
129+
frames: 15,
130+
frame_width: 640,
131+
frame_height: 480,
132+
extend_frame: true,
133+
trim: true,
134+
fill: true,
135+
focus_x: 0.5,
136+
focus_y: 0.5,
137+
};
138+
expect(build({ video_thumbnail_animation: vta })).toEqual(
139+
"vta:-1:200:15:640:480:t:t:t:0.5:0.5"
140+
);
141+
});
142+
});
143+
});
144+
145+
describe("Check `video_thumbnail_animation` type declarations", () => {
146+
it("video_thumbnail_animation option should have correct type", () => {
147+
expectTypeOf(build).parameter(0).toEqualTypeOf<{
148+
video_thumbnail_animation?: VideoThumbnailAnimation;
149+
vta?: VideoThumbnailAnimation;
150+
}>();
151+
expectTypeOf(build).returns.toEqualTypeOf<string>();
152+
});
153+
154+
it("check TS type declaration", () => {
155+
assertType<Options>({
156+
video_thumbnail_animation: {
157+
step: 1,
158+
delay: 100,
159+
frames: 10,
160+
frame_width: 320,
161+
frame_height: 240,
162+
},
163+
});
164+
165+
assertType<Options>({
166+
vta: {
167+
step: 1,
168+
delay: 100,
169+
frames: 10,
170+
frame_width: 320,
171+
frame_height: 240,
172+
extend_frame: true,
173+
trim: true,
174+
fill: true,
175+
focus_x: 0.5,
176+
focus_y: 0.5,
177+
},
178+
});
179+
});
180+
});

0 commit comments

Comments
 (0)