Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-loops-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imgproxy/imgproxy-js-core": minor
---

Add support for [video_thumbnail_animation property](https://docs.imgproxy.net/usage/processing#video-thumbnail-animation)
1 change: 1 addition & 0 deletions src/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export * as style from "./style";
export * as trim from "./trim";
export * as unsharpMasking from "./unsharpMasking";
export * as videoThumbnailSecond from "../optionsShared/videoThumbnailSecond";
export * as videoThumbnailAnimation from "./videoThumbnailAnimation";
export * as watermark from "./watermark";
export * as watermarkShadow from "./watermarkShadow";
export * as watermarkSize from "./watermarkSize";
Expand Down
55 changes: 55 additions & 0 deletions src/options/videoThumbnailAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { VideoThumbnailAnimationOptionsPartial } from "../types/videoThumbnailAnimation";
import { guardIsNotNum, guardIsUndef, normalizeBoolean } from "../utils";

function getOpts(options: VideoThumbnailAnimationOptionsPartial) {
return options.video_thumbnail_animation || options.vta;
}

function test(options: VideoThumbnailAnimationOptionsPartial) {
return Boolean(getOpts(options));
}

function build(options: VideoThumbnailAnimationOptionsPartial) {
const vta = getOpts(options);
guardIsUndef(vta, "video_thumbnail_animation");

guardIsNotNum(vta.step, "video_thumbnail_animation.step");
guardIsNotNum(vta.delay, "video_thumbnail_animation.delay");
guardIsNotNum(vta.frames, "video_thumbnail_animation.frames");

if (vta.frame_width !== undefined) {
guardIsNotNum(vta.frame_width, "video_thumbnail_animation.frame_width");
}

if (vta.frame_height !== undefined) {
guardIsNotNum(vta.frame_height, "video_thumbnail_animation.frame_height");
}

const parts = [];

// Add boolean flags with proper normalization
const extend_frame =
vta.extend_frame !== undefined
? normalizeBoolean(vta.extend_frame)
: undefined;
const trim = vta.trim !== undefined ? normalizeBoolean(vta.trim) : undefined;
const fill = vta.fill !== undefined ? normalizeBoolean(vta.fill) : undefined;

parts.push(extend_frame, trim, fill);

// Add focus coordinates if fill is true and coordinates are defined
if (fill === "t") {
parts.push(vta.focus_x, vta.focus_y);
}

// Remove trailing undefined values
while (parts.length > 0 && parts[parts.length - 1] === undefined) {
parts.pop();
}

const optionsPart = parts.length > 0 ? `:${parts.join(":")}` : "";

return `vta:${vta.step}:${vta.delay}:${vta.frames}:${vta.frame_width}:${vta.frame_height}${optionsPart}`;
}

export { test, build };
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import type { StripMetadataOptionsPartial } from "./stripMetadata";
import type { StyleOptionsPartial } from "./style";
import type { TrimOptionsPartial } from "./trim";
import type { UnsharpMaskingOptionsPartial } from "./unsharpMasking";
import type { VideoThumbnailAnimationOptionsPartial } from "./videoThumbnailAnimation";
import type { VideoThumbnailSecondOptionsPartial } from "../typesShared/videoThumbnailSecond";
import type { WatermarkOptionsPartial } from "./watermark";
import type { WatermarkRotateOptionsPartial } from "./watermarkRotate";
Expand Down Expand Up @@ -128,6 +129,7 @@ export type Options = AdjustOptionsPartial &
StyleOptionsPartial &
TrimOptionsPartial &
UnsharpMaskingOptionsPartial &
VideoThumbnailAnimationOptionsPartial &
VideoThumbnailSecondOptionsPartial &
WatermarkOptionsPartial &
WatermarkShadowOptionsPartial &
Expand Down
63 changes: 63 additions & 0 deletions src/types/videoThumbnailAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* When step is not 0, imgproxy will generate an animated image using the source video frames.
*/
interface VideoThumbnailAnimation {
/**
* step: the step of timestamp (in seconds) between video frames that should be used for the animation generation:
* When step value is positive, imgproxy will use it as an absolute value
* When step value is negative, imgproxy will calculate the actual step as video_duration / frames
*/
step: number;

/**
* the delay between animation frames in milliseconds
*/
delay: number;

/**
* the number of animation frames
*/
frames: number;

/**
* the width and height of animation frames. imgproxy will resize each used frame to fit the provided size
*/
frame_width: number;
frame_height: number;

/**
* when set to 1, t or true, imgproxy will extend each animation frame to the requested size using a black background
*/
extend_frame?: boolean | 1 | string;

/**
* when set to 1, t or true, imgproxy will trim the unused frames from the animation
*/
trim?: boolean | 1 | string;

/**
* when set to 1, t or true, imgproxy will use the fill resizing type for the animation frames
*/
fill?: boolean | 1 | string;

/**
* floating point numbers between 0 and 1 that define the coordinates of the center of the resulting animation frame
* (as in the fp gravity type). Treat 0 and 1 as right/left for x and top/bottom for y.
* Applicable only when fill is set. Default: 0.5:0.5
*/
focus_x?: number;
focus_y?: number;
}
/**
* *Video thumbnail animation options*. **PRO feature**
*
* Allows generating an animated image using the source video frames.
*
* @see https://docs.imgproxy.net/usage/processing#video-thumbnail-animation
*/
interface VideoThumbnailAnimationOptionsPartial {
video_thumbnail_animation?: VideoThumbnailAnimation;
vta?: VideoThumbnailAnimation;
}

export { VideoThumbnailAnimation, VideoThumbnailAnimationOptionsPartial };
184 changes: 184 additions & 0 deletions tests/optionsBasic/videoThumbnailAnimation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { assertType, describe, expect, expectTypeOf, it } from "vitest";
import { test, build } from "../../src/options/videoThumbnailAnimation";
import { VideoThumbnailAnimation } from "../../src/types/videoThumbnailAnimation";
import { Options } from "../../src/types";

describe("videoThumbnailAnimation", () => {
describe("test", () => {
it("should return true if video_thumbnail_animation option is defined", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
};
expect(test({ video_thumbnail_animation: vta })).toEqual(true);
});

it("should return true if vta option is defined", () => {
const vta: VideoThumbnailAnimation = {
step: 2,
delay: 200,
frames: 5,
frame_width: 320,
frame_height: 240,
};
expect(test({ vta })).toEqual(true);
});

it("should return false if video_thumbnail_animation option is undefined", () => {
expect(test({})).toEqual(false);
});
});

describe("build", () => {
it("should throw an error if video_thumbnail_animation option is undefined", () => {
expect(() => build({})).toThrow(
"video_thumbnail_animation option is undefined"
);
});

it("should build basic vta option with required parameters", () => {
const vta: VideoThumbnailAnimation = {
step: 1.5,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
};
expect(build({ video_thumbnail_animation: vta })).toEqual(
"vta:1.5:100:10:320:240"
);
});

it("should build vta option with extend_frame as boolean", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
extend_frame: true,
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
});

it("should build vta option with extend_frame as number", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
extend_frame: 1,
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
});

it("should build vta option with extend_frame as string", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
extend_frame: "t",
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240:t");
});

it("should build vta option with trim flag", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
trim: true,
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240::t");
});

it("should build vta option with fill flag", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
fill: true,
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t");
});

it("should build vta option with focus coordinates", () => {
const vta: VideoThumbnailAnimation = {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
fill: true,
focus_x: 0.3,
focus_y: 0.7,
};
expect(build({ vta })).toEqual("vta:1:100:10:320:240:::t:0.3:0.7");
});

it("should build vta option with all parameters", () => {
const vta: VideoThumbnailAnimation = {
step: -1,
delay: 200,
frames: 15,
frame_width: 640,
frame_height: 480,
extend_frame: true,
trim: true,
fill: true,
focus_x: 0.5,
focus_y: 0.5,
};
expect(build({ video_thumbnail_animation: vta })).toEqual(
"vta:-1:200:15:640:480:t:t:t:0.5:0.5"
);
});
});
});

describe("Check `video_thumbnail_animation` type declarations", () => {
it("video_thumbnail_animation option should have correct type", () => {
expectTypeOf(build).parameter(0).toEqualTypeOf<{
video_thumbnail_animation?: VideoThumbnailAnimation;
vta?: VideoThumbnailAnimation;
}>();
expectTypeOf(build).returns.toEqualTypeOf<string>();
});

it("check TS type declaration", () => {
assertType<Options>({
video_thumbnail_animation: {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
},
});

assertType<Options>({
vta: {
step: 1,
delay: 100,
frames: 10,
frame_width: 320,
frame_height: 240,
extend_frame: true,
trim: true,
fill: true,
focus_x: 0.5,
focus_y: 0.5,
},
});
});
});