MediaRouter / app /operations /resize.py
basyx's picture
Upload 142 files
fba6023 verified
Raw
History Blame Contribute Delete
3.55 kB
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from app.core.exceptions import InputError
from app.models.media import InputMedia, OperationResult
from app.operations.common import (
IMAGE_FORMATS,
VIDEO_FORMATS,
even,
execute,
format_param,
output_path,
require_inputs,
video_codecs,
)
from app.services.ffmpeg_service import FFmpegService
from app.services.validator import positive_int
def _scale(params: dict[str, Any]) -> tuple[int, int, str]:
width = even(positive_int(params, "width", 1280))
height = even(positive_int(params, "height", 720))
if width < 2 or height < 2:
raise InputError("Output dimensions must be at least 2x2")
fit = str(params.get("fit", "contain"))
if fit == "fill":
expression = f"scale={width}:{height}"
elif fit == "cover":
expression = (
f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}"
)
elif fit == "contain":
expression = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2"
else:
raise InputError("'fit' must be contain, cover, or fill")
return width, height, expression
async def resize_video(
ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path
) -> OperationResult:
require_inputs(inputs)
width, height, expression = _scale(params)
extension = format_param(params, "mp4", VIDEO_FORMATS)
output = output_path(output_dir, "resized", extension)
return await execute(
ffmpeg,
["-i", inputs[0].temp_path, "-vf", expression, *video_codecs(extension), output],
output,
"video.resize",
{"width": width, "height": height},
)
async def scale_video(
ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path
) -> OperationResult:
copied = {**params, "fit": params.get("fit", "fill")}
result = await resize_video(ffmpeg, inputs, copied, output_dir)
result.metadata["operation"] = "video.scale"
return result
async def pad_video(
ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path
) -> OperationResult:
require_inputs(inputs)
width = even(positive_int(params, "width", 1920))
height = even(positive_int(params, "height", 1080))
color = str(params.get("color", "black"))
if not color.replace("#", "").isalnum():
raise InputError("Invalid pad color")
output = output_path(output_dir, "padded", "mp4")
vf = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:{color}"
return await execute(
ffmpeg,
["-i", inputs[0].temp_path, "-vf", vf, *video_codecs("mp4"), output],
output,
"video.pad",
{"width": width, "height": height},
)
async def resize_image(
ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path
) -> OperationResult:
require_inputs(inputs)
width, height, expression = _scale(params)
extension = format_param(params, "png", IMAGE_FORMATS)
output = output_path(output_dir, "resized", extension)
return await execute(
ffmpeg,
["-i", inputs[0].temp_path, "-vf", expression, "-frames:v", "1", output],
output,
"image.resize",
{"width": width, "height": height},
)