Skip to content

Commit 1beb7e1

Browse files
authored
Add Video thumbnail tile option (#47)
* Add video thumbnail tile option * Changesets
1 parent 3f7b58e commit 1beb7e1

File tree

6 files changed

+295
-0
lines changed

6 files changed

+295
-0
lines changed

.changeset/moody-coins-wink.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 [Video thumbnail tile](https://docs.imgproxy.net/usage/processing#video-thumbnail-tile) option

src/options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export * as trim from "./trim";
6262
export * as unsharpMasking from "./unsharpMasking";
6363
export * as videoThumbnailSecond from "../optionsShared/videoThumbnailSecond";
6464
export * as videoThumbnailAnimation from "./videoThumbnailAnimation";
65+
export * as videoThumbnailTile from "./videoThumbnailTile";
6566
export * as watermark from "./watermark";
6667
export * as watermarkShadow from "./watermarkShadow";
6768
export * as watermarkSize from "./watermarkSize";

src/options/videoThumbnailTile.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { VideoThumbnailTileOptionsPartial } from "../types/videoThumbnailTile";
2+
import { guardIsNotNum, guardIsUndef, normalizeBoolean } from "../utils";
3+
4+
function getOpts(options: VideoThumbnailTileOptionsPartial) {
5+
return options.video_thumbnail_tile || options.vtt;
6+
}
7+
8+
function test(options: VideoThumbnailTileOptionsPartial) {
9+
return Boolean(getOpts(options));
10+
}
11+
12+
function build(options: VideoThumbnailTileOptionsPartial) {
13+
const vtt = getOpts(options);
14+
guardIsUndef(vtt, "video_thumbnail_tile");
15+
16+
guardIsNotNum(vtt.step, "video_thumbnail_tile.step");
17+
guardIsNotNum(vtt.columns, "video_thumbnail_tile.columns");
18+
guardIsNotNum(vtt.rows, "video_thumbnail_tile.rows");
19+
guardIsNotNum(vtt.tile_width, "video_thumbnail_tile.tile_width");
20+
guardIsNotNum(vtt.tile_height, "video_thumbnail_tile.tile_height");
21+
22+
const parts = [];
23+
24+
// Add boolean flags with proper normalization
25+
const extend_tile =
26+
vtt.extend_tile !== undefined
27+
? normalizeBoolean(vtt.extend_tile)
28+
: undefined;
29+
const trim = vtt.trim !== undefined ? normalizeBoolean(vtt.trim) : undefined;
30+
const fill = vtt.fill !== undefined ? normalizeBoolean(vtt.fill) : undefined;
31+
32+
parts.push(extend_tile, trim, fill);
33+
34+
// Add focus coordinates if fill is true and coordinates are defined
35+
if (fill === "t") {
36+
parts.push(vtt.focus_x, vtt.focus_y);
37+
}
38+
39+
// Remove trailing undefined values
40+
while (parts.length > 0 && parts[parts.length - 1] === undefined) {
41+
parts.pop();
42+
}
43+
44+
const optionsPart = parts.length > 0 ? `:${parts.join(":")}` : "";
45+
46+
return `vtt:${vtt.step}:${vtt.columns}:${vtt.rows}:${vtt.tile_width}:${vtt.tile_height}${optionsPart}`;
47+
}
48+
49+
export { test, build };

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import type { StyleOptionsPartial } from "./style";
6161
import type { TrimOptionsPartial } from "./trim";
6262
import type { UnsharpMaskingOptionsPartial } from "./unsharpMasking";
6363
import type { VideoThumbnailAnimationOptionsPartial } from "./videoThumbnailAnimation";
64+
import type { VideoThumbnailTileOptionsPartial } from "./videoThumbnailTile";
6465
import type { VideoThumbnailSecondOptionsPartial } from "../typesShared/videoThumbnailSecond";
6566
import type { WatermarkOptionsPartial } from "./watermark";
6667
import type { WatermarkRotateOptionsPartial } from "./watermarkRotate";
@@ -136,6 +137,7 @@ export type Options = AdjustOptionsPartial &
136137
TrimOptionsPartial &
137138
UnsharpMaskingOptionsPartial &
138139
VideoThumbnailAnimationOptionsPartial &
140+
VideoThumbnailTileOptionsPartial &
139141
VideoThumbnailSecondOptionsPartial &
140142
WatermarkOptionsPartial &
141143
WatermarkShadowOptionsPartial &

src/types/videoThumbnailTile.ts

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

0 commit comments

Comments
 (0)