Clean up cropping logic

This commit is contained in:
Vanilagy
2025-09-12 16:48:35 +02:00
parent 4aca172d14
commit eba1f360d5
10 changed files with 204 additions and 350 deletions
+30 -42
View File
@@ -47,7 +47,7 @@ import {
} from './misc';
import { Output, TrackType } from './output';
import { Mp4OutputFormat } from './output-format';
import { AudioSample, VideoSample } from './sample';
import { AudioSample, clampCropRectangle, validateCropRectangle, VideoSample } from './sample';
import { MetadataTags, validateMetadataTags } from './tags';
import { NullTarget } from './target';
@@ -106,22 +106,6 @@ export type ConversionOptions = {
export type ConversionVideoOptions = {
/** If `true`, all video tracks will be discarded and will not be present in the output. */
discard?: boolean;
/**
* Specifies a rectangular region of the input video to crop to, in the coordinate
* system of the original, unrotated frame. Parts of the crop rectangle that extend
* beyond the frame will be filled with black. Cropping is performed before rotation
* and resizing.
*/
crop?: {
/** The distance in pixels from the left edge of the source frame to the left edge of the crop rectangle. */
left: number;
/** The distance in pixels from the top edge of the source frame to the top edge of the crop rectangle. */
top: number;
/** The width in pixels of the crop rectangle. */
width: number;
/** The height in pixels of the crop rectangle. */
height: number;
};
/**
* The desired width of the output video in pixels, defaulting to the video's natural display width. If height
* is not set, it will be deduced automatically based on aspect ratio.
@@ -142,10 +126,24 @@ export type ConversionVideoOptions = {
*/
fit?: 'fill' | 'contain' | 'cover';
/**
* The angle in degrees to rotate the input video by, clockwise. Rotation is applied before resizing. This
* rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
* The angle in degrees to rotate the input video by, clockwise. Rotation is applied before cropping and resizing.
* This rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
*/
rotate?: Rotation;
/**
* Specifies the rectangular region of the input video to crop to. The crop region will automatically be clamped to
* the dimensions of the input video track. Cropping is performed after rotation but before resizing.
*/
crop?: {
/** The distance in pixels from the left edge of the source frame to the left edge of the crop rectangle. */
left: number;
/** The distance in pixels from the top edge of the source frame to the top edge of the crop rectangle. */
top: number;
/** The width in pixels of the crop rectangle. */
width: number;
/** The height in pixels of the crop rectangle. */
height: number;
};
/**
* The desired frame rate of the output video, in hertz. If not specified, the original input frame rate will
* be used (which may be variable).
@@ -189,24 +187,6 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
if (videoOptions?.forceTranscode !== undefined && typeof videoOptions.forceTranscode !== 'boolean') {
throw new TypeError('options.video.forceTranscode, when provided, must be a boolean.');
}
if (videoOptions?.crop !== undefined) {
if (typeof videoOptions.crop !== 'object') {
throw new TypeError('options.video.crop, when provided, must be an object.');
}
const { left, top, width, height } = videoOptions.crop;
if (!Number.isInteger(left)) {
throw new TypeError('options.video.crop.left must be an integer.');
}
if (!Number.isInteger(top)) {
throw new TypeError('options.video.crop.top must be an integer.');
}
if (!Number.isInteger(width) || width <= 0) {
throw new TypeError('options.video.crop.width must be a positive integer.');
}
if (!Number.isInteger(height) || height <= 0) {
throw new TypeError('options.video.crop.height must be a positive integer.');
}
}
if (videoOptions?.codec !== undefined && !VIDEO_CODECS.includes(videoOptions.codec)) {
throw new TypeError(
`options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(', ')}.`,
@@ -247,6 +227,9 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
if (videoOptions?.rotate !== undefined && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
}
if (videoOptions?.crop !== undefined) {
validateCropRectangle(videoOptions.crop);
}
if (
videoOptions?.frameRate !== undefined
&& (!Number.isFinite(videoOptions.frameRate) || videoOptions.frameRate <= 0)
@@ -635,13 +618,18 @@ export class Conversion {
const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0
? [track.codedWidth, track.codedHeight]
: [track.codedHeight, track.codedWidth];
const crop = trackOptions.crop;
const [croppedWidth, croppedHeight] = crop
if (crop) {
clampCropRectangle(crop, rotatedWidth, rotatedHeight);
}
const [originalWidth, originalHeight] = crop
? [crop.width, crop.height]
: [track.codedWidth, track.codedHeight];
const [originalWidth, originalHeight] = totalRotation % 180 === 0
? [croppedWidth, croppedHeight]
: [croppedHeight, croppedWidth];
: [rotatedWidth, rotatedHeight];
let width = originalWidth;
let height = originalHeight;