Skip to content

Commit db5b6e8

Browse files
authored
Add video thumbnail animation property (#39)
* Add videoThumbnailAnimation option * Add changesets
1 parent c11cdad commit db5b6e8

File tree

6 files changed

+310
-0
lines changed

6 files changed

+310
-0
lines changed

.changeset/tall-loops-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@imgproxy/imgproxy-js-core": minor
3+
---
4+
5+
Add support for [video_thumbnail_animation property](https://docs.imgproxy.net/usage/processing#video-thumbnail-animation)

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: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
frame_width: 320,
14+
frame_height: 240,
15+
};
16+
expect(test({ video_thumbnail_animation: vta })).toEqual(true);
17+
});
18+
19+
it("should return true if vta option is defined", () => {
20+
const vta: VideoThumbnailAnimation = {
21+
step: 2,
22+
delay: 200,
23+
frames: 5,
24+
frame_width: 320,
25+
frame_height: 240,
26+
};
27+
expect(test({ vta })).toEqual(true);
28+
});
29+
30+
it("should return false if video_thumbnail_animation option is undefined", () => {
31+
expect(test({})).toEqual(false);
32+
});
33+
});
34+
35+
describe("build", () => {
36+
it("should throw an error if video_thumbnail_animation option is undefined", () => {
37+
expect(() => build({})).toThrow(
38+
"video_thumbnail_animation option is undefined"
39+
);
40+
});
41+
42+
it("should build basic vta option with required parameters", () => {
43+
const vta: VideoThumbnailAnimation = {
44+
step: 1.5,
45+
delay: 100,
46+
frames: 10,
47+
frame_width: 320,
48+
frame_height: 240,
49+
};
50+
expect(build({ video_thumbnail_animation: vta })).toEqual(
51+
"vta:1.5:100:10:320:240"
52+
);
53+
});
54+
55+
it("should build vta option with extend_frame as boolean", () => {
56+
const vta: VideoThumbnailAnimation = {
57+
step: 1,
58+
delay: 100,
59+
frames: 10,
60+
frame_width: 320,
61+
frame_height: 240,
62+
extend_frame: true,
63+
};
64+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
65+
});
66+
67+
it("should build vta option with extend_frame as number", () => {
68+
const vta: VideoThumbnailAnimation = {
69+
step: 1,
70+
delay: 100,
71+
frames: 10,
72+
frame_width: 320,
73+
frame_height: 240,
74+
extend_frame: 1,
75+
};
76+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
77+
});
78+
79+
it("should build vta option with extend_frame as string", () => {
80+
const vta: VideoThumbnailAnimation = {
81+
step: 1,
82+
delay: 100,
83+
frames: 10,
84+
frame_width: 320,
85+
frame_height: 240,
86+
extend_frame: "t",
87+
};
88+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
89+
});
90+
91+
it("should build vta option with trim flag", () => {
92+
const vta: VideoThumbnailAnimation = {
93+
step: 1,
94+
delay: 100,
95+
frames: 10,
96+
frame_width: 320,
97+
frame_height: 240,
98+
trim: true,
99+
};
100+
expect(build({ vta })).toEqual("vta:1:100:10:320:240::t");
101+
});
102+
103+
it("should build vta option with fill flag", () => {
104+
const vta: VideoThumbnailAnimation = {
105+
step: 1,
106+
delay: 100,
107+
frames: 10,
108+
frame_width: 320,
109+
frame_height: 240,
110+
fill: true,
111+
};
112+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t");
113+
});
114+
115+
it("should build vta option with focus coordinates", () => {
116+
const vta: VideoThumbnailAnimation = {
117+
step: 1,
118+
delay: 100,
119+
frames: 10,
120+
frame_width: 320,
121+
frame_height: 240,
122+
fill: true,
123+
focus_x: 0.3,
124+
focus_y: 0.7,
125+
};
126+
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t:0.3:0.7");
127+
});
128+
129+
it("should build vta option with all parameters", () => {
130+
const vta: VideoThumbnailAnimation = {
131+
step: -1,
132+
delay: 200,
133+
frames: 15,
134+
frame_width: 640,
135+
frame_height: 480,
136+
extend_frame: true,
137+
trim: true,
138+
fill: true,
139+
focus_x: 0.5,
140+
focus_y: 0.5,
141+
};
142+
expect(build({ video_thumbnail_animation: vta })).toEqual(
143+
"vta:-1:200:15:640:480:t:t:t:0.5:0.5"
144+
);
145+
});
146+
});
147+
});
148+
149+
describe("Check `video_thumbnail_animation` type declarations", () => {
150+
it("video_thumbnail_animation option should have correct type", () => {
151+
expectTypeOf(build).parameter(0).toEqualTypeOf<{
152+
video_thumbnail_animation?: VideoThumbnailAnimation;
153+
vta?: VideoThumbnailAnimation;
154+
}>();
155+
expectTypeOf(build).returns.toEqualTypeOf<string>();
156+
});
157+
158+
it("check TS type declaration", () => {
159+
assertType<Options>({
160+
video_thumbnail_animation: {
161+
step: 1,
162+
delay: 100,
163+
frames: 10,
164+
frame_width: 320,
165+
frame_height: 240,
166+
},
167+
});
168+
169+
assertType<Options>({
170+
vta: {
171+
step: 1,
172+
delay: 100,
173+
frames: 10,
174+
frame_width: 320,
175+
frame_height: 240,
176+
extend_frame: true,
177+
trim: true,
178+
fill: true,
179+
focus_x: 0.5,
180+
focus_y: 0.5,
181+
},
182+
});
183+
});
184+
});

0 commit comments

Comments
 (0)