using System.Globalization; using System.Text.RegularExpressions; using AmiReel.Models; namespace AmiReel.Services; /// Shared FFmpeg crop filters used by both Video and Shorts pipelines. internal static class SourceCropFilters { internal const string CenterFourByThreeFilter = "crop='min(iw,ih*4/3)':'min(ih,iw*3/4)':'(iw-ow)/2':'(ih-oh)/2'"; private static readonly Regex CropSpecRegex = new( @"crop=(\d+):(\d+):(\d+):(\d+)", RegexOptions.CultureInvariant); internal static string BuildManualCropFilter(int left, int right, int top, int bottom) => $"crop=iw-{left}-{right}:ih-{top}-{bottom}:{left}:{top}"; internal static bool HasManualCrop(int left, int right, int top, int bottom) => left > 0 || right > 0 || top > 0 || bottom > 0; internal static string Resolve( ShortSourceCropMode mode, int left, int right, int top, int bottom, string? autoDetected) { if (HasManualCrop(left, right, top, bottom)) return BuildManualCropFilter(left, right, top, bottom); return mode switch { ShortSourceCropMode.None => "", ShortSourceCropMode.Auto when !string.IsNullOrWhiteSpace(autoDetected) => autoDetected, ShortSourceCropMode.Auto => CenterFourByThreeFilter, _ => CenterFourByThreeFilter, }; } internal static string Prefix(string cropFilter, string nextFilter) => string.IsNullOrWhiteSpace(cropFilter) ? nextFilter : $"{cropFilter},{nextFilter}"; internal static List BuildHorizontalCropDetectArguments(string inputFile, string start, double durationSeconds) => [ "-hide_banner", "-ss", string.IsNullOrWhiteSpace(start) ? "00:00:00" : start, "-t", durationSeconds.ToString("0.###", CultureInfo.InvariantCulture), "-i", inputFile, "-an", "-vf", "crop=iw:trunc(ih*0.70/2)*2:0:trunc(ih*0.15/2)*2,cropdetect=limit=28:round=2:reset=0", "-f", "null", "-", ]; internal static string? SelectMostCommonCropSpec(string ffmpegOutput) { Dictionary counts = new(StringComparer.Ordinal); foreach (Match match in CropSpecRegex.Matches(ffmpegOutput)) { string spec = match.Value; counts[spec] = counts.GetValueOrDefault(spec) + 1; } return counts.Count == 0 ? null : counts.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key; } internal static bool TryParseCropSpec(string spec, out int width, out int height, out int x, out int y) { Match match = CropSpecRegex.Match(spec); if (!match.Success) { width = height = x = y = 0; return false; } width = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); height = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); x = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); y = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture); return true; } internal static bool TryBuildAutoHorizontalCrop(int detectedWidth, int detectedX, out int width, out int x) { int inset = (int)(detectedWidth * 0.005 / 2) * 2; width = ((detectedWidth - (2 * inset)) / 2) * 2; x = ((detectedX + inset) / 2) * 2; if (width < 16) return false; return true; } internal static string? TryBuildAutoHorizontalFilter(string ffmpegOutput) { string? spec = SelectMostCommonCropSpec(ffmpegOutput); if (spec is null || !TryParseCropSpec(spec, out int width, out _, out int x, out _)) return null; if (!TryBuildAutoHorizontalCrop(width, x, out int cropWidth, out int cropX)) return null; return $"crop={cropWidth}:ih:{cropX}:0"; } }