Crop rectangle and gravitate to (meveable) point added to preview.

This commit is contained in:
Alexander Shabarshov 2026-05-24 13:35:10 +01:00
parent 61c94d4661
commit c6ca4fcbb6
8 changed files with 479 additions and 95 deletions

View File

@ -7,12 +7,14 @@ public class PreviewData
public Avalonia.Media.Imaging.Bitmap? Frame { get; } public Avalonia.Media.Imaging.Bitmap? Frame { get; }
public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { get; } public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { get; }
public Rect? CropRect { get; } public Rect? CropRect { get; }
public Point2f GravitateTo { get; }
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop) public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop, Point2f gravitateTo)
{ {
Frame = frame; Frame = frame;
DetectedBoxes = boxes; DetectedBoxes = boxes;
CropRect = crop; CropRect = crop;
GravitateTo = gravitateTo;
} }
} }

View File

@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
namespace Splitter_UI.ViewModels; namespace Splitter_UI.ViewModels;
@ -8,6 +9,8 @@ public partial class InspectorPaneViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private JobViewModel? _selected; private JobViewModel? _selected;
public ObservableCollection<JobViewModel> Files { get; set; } = [];
public List<string> DetectModes => public List<string> DetectModes =>
[ [
"face", "body", "none" "face", "body", "none"
@ -24,6 +27,22 @@ public partial class InspectorPaneViewModel : ObservableObject
if (Selected is null) if (Selected is null)
return; return;
foreach (JobViewModel job in Files.Where(x => !ReferenceEquals(x, Selected)))
{
job.Detect = Selected.Detect;
job.Rotate = Selected.Rotate;
job.CropText = Selected.CropText;
job.ForceFixed = Selected.ForceFixed;
job.GravitateText = Selected.GravitateText;
job.Mask = Selected.Mask;
job.OutputFolder = Selected.OutputFolder;
job.OverrideTargetDuration = Selected.OverrideTargetDuration;
job.PassthroughText = Selected.PassthroughText;
job.ParametersList.Clear();
foreach (var param in Selected.ParametersList)
job.ParametersList.Add(param);
}
} }
public IRelayCommand RotateLeftCommand { get; } public IRelayCommand RotateLeftCommand { get; }

View File

@ -16,7 +16,7 @@ public partial class JobViewModel : ObservableObject
private SingleJob Job { get; } private SingleJob Job { get; }
[ObservableProperty] private VideoInfo? _probe; [ObservableProperty] private VideoInfo? _probe;
[ObservableProperty] private PreviewData? _preview = new(null, [], null); [ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f));
[ObservableProperty] private ProgressInfo? _progress; [ObservableProperty] private ProgressInfo? _progress;
[ObservableProperty] private Bitmap? _thumbnail; [ObservableProperty] private Bitmap? _thumbnail;
[ObservableProperty] private string _suggestedAction = ""; [ObservableProperty] private string _suggestedAction = "";
@ -84,6 +84,7 @@ public partial class JobViewModel : ObservableObject
Job.GravitateTo = new Point2f(x, y); Job.GravitateTo = new Point2f(x, y);
} }
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(GravitateTo));
} }
} }
@ -170,6 +171,20 @@ public partial class JobViewModel : ObservableObject
} }
} }
public Point2f GravitateTo
{
get => Job.GravitateTo ?? new Point2f(0.5f, 0.5f);
set
{
if (Job.GravitateTo != null && Math.Abs(Job.GravitateTo.Value.X - value.X) < 0.001 && Math.Abs(Job.GravitateTo.Value.Y - value.Y) < 0.001)
return;
Job.GravitateTo = value;
OnPropertyChanged();
OnPropertyChanged(nameof(GravitateText));
}
}
public double? OverrideTargetDuration public double? OverrideTargetDuration
{ {
get => Job.OverrideTargetDuration; get => Job.OverrideTargetDuration;
@ -188,14 +203,14 @@ public partial class JobViewModel : ObservableObject
_detectorFactory = detectorFactory; _detectorFactory = detectorFactory;
_log = log; _log = log;
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", "")); ParametersList.Add(new ParameterEntry("DropoutToleranceFrames" , ""));
ParametersList.Add(new ParameterEntry("EmaFactor", "")); ParametersList.Add(new ParameterEntry("EmaFactor" , ""));
ParametersList.Add(new ParameterEntry("CameraEasing", "")); ParametersList.Add(new ParameterEntry("CameraEasing" , ""));
ParametersList.Add(new ParameterEntry("LostFreezeFrames", "")); ParametersList.Add(new ParameterEntry("LostFreezeFrames" , ""));
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount", "")); ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount" , ""));
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", "")); ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth", "")); ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth" , ""));
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight", "")); ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight" , ""));
foreach (var entry in ParametersList) foreach (var entry in ParametersList)
{ {
@ -224,13 +239,34 @@ public partial class JobViewModel : ObservableObject
if ( frame == null ) if ( frame == null )
return; return;
Preview = new PreviewData(frame, [], null); Preview = new PreviewData(frame, [], null, Job.GravitateTo ?? new (0.5f, 0.5f));
var detector = _detectorFactory(Job.Detect ?? ""); var detector = _detectorFactory(Job.Detect ?? "");
var detections = detector.DetectAll(frame.ToMatContinuous()); var detections = detector.DetectAll(frame.ToMatContinuous());
var boxes = detections.Select(x => new OpenCvSharp.Rect(x.box.X, x.box.Y, x.box.Width, x.box.Height)).ToList(); Rect? crop = null;
Preview = new PreviewData(frame, boxes, null); if (detections.Count > 0)
{
var primaryDetection = detections
.OrderByDescending(d => d.box.Height * d.box.Width)
.FirstOrDefault();
var w = Probe.Width;
var h = Probe.Height;
var cropWidth = Job.Crop?.width ?? CommandLine.DefaultW;
var cropHeight = Job.Crop?.height ?? CommandLine.DefaultH;
var cx = primaryDetection.center.X - cropWidth / 2f;
var cy = primaryDetection.center.Y - cropHeight / 2f;
var r = new Rect(cx, cy, cropWidth, cropHeight);
crop = ClampCrop(r, w, h);
}
var boxes = detections.Select(x => x.box).ToList();
Preview = new PreviewData(frame, boxes, crop, Job.GravitateTo ?? new (0.5f, 0.5f));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -238,6 +274,26 @@ public partial class JobViewModel : ObservableObject
} }
} }
private static Rect ClampCrop(Rect r, float w, float h)
{
var x = r.X;
var y = r.Y;
var cw = r.Width;
var ch = r.Height;
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x + cw > w) x = w - cw;
if (y + ch > h) y = h - ch;
if (x < 0) x = 0;
if (y < 0) y = 0;
return new Rect(x, y, cw, ch);
}
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e) private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value)) if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))

View File

@ -19,6 +19,8 @@ public partial class MainViewModel : ViewModelBase
Preview.Selected = file; Preview.Selected = file;
Inspector.Selected = file; Inspector.Selected = file;
}; };
Inspector.Files = FileList.Files;
} }
[RelayCommand] [RelayCommand]

View File

@ -12,6 +12,17 @@ public partial class PreviewPaneViewModel : ObservableObject
public PreviewData? Preview => Selected?.Preview; public PreviewData? Preview => Selected?.Preview;
public Point2f? Sar => Selected?.Probe?.Sar; public Point2f? Sar => Selected?.Probe?.Sar;
public int Rotate => Selected?.Rotate ?? 0; public int Rotate => Selected?.Rotate ?? 0;
public Point2f GravitateTo
{
get => Selected?.GravitateTo ?? new Point2f(0.5f, 0.5f);
set
{
if (Selected == null)
return;
Selected.GravitateTo = value;
OnPropertyChanged(nameof(GravitateTo));
}
}
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue) partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
{ {

View File

@ -1,6 +1,7 @@
using System.ComponentModel; using System.ComponentModel;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using splitter.algo; using splitter.algo;
@ -15,6 +16,8 @@ public sealed class PreviewCanvas : Control
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar)); AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
public static readonly StyledProperty<int> RotateAngleProperty = public static readonly StyledProperty<int> RotateAngleProperty =
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle)); AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
public static readonly StyledProperty<Point2f> GravitateToProperty =
AvaloniaProperty.Register<PreviewCanvas, Point2f>(nameof(GravitateTo));
public PreviewData? Preview public PreviewData? Preview
{ {
@ -34,6 +37,17 @@ public sealed class PreviewCanvas : Control
set => SetValue(RotateAngleProperty, value); set => SetValue(RotateAngleProperty, value);
} }
// GravitateTo is normalized (0..1)
public Point2f GravitateTo
{
get => GetValue(GravitateToProperty);
set => SetValue(GravitateToProperty, value);
}
private bool _dragging;
private Avalonia.Point _dragStartCanvas;
private Point2f _dragStartValue;
static PreviewCanvas() static PreviewCanvas()
{ {
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>( PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
@ -42,6 +56,13 @@ public sealed class PreviewCanvas : Control
args.NewValue as PreviewData)); args.NewValue as PreviewData));
} }
public PreviewCanvas()
{
PointerPressed += OnPointerPressed;
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
}
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue) private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
{ {
if (oldValue is INotifyPropertyChanged oldNotify) if (oldValue is INotifyPropertyChanged oldNotify)
@ -50,7 +71,6 @@ public sealed class PreviewCanvas : Control
if (newValue is INotifyPropertyChanged newNotify) if (newValue is INotifyPropertyChanged newNotify)
newNotify.PropertyChanged += PreviewPropertyChanged; newNotify.PropertyChanged += PreviewPropertyChanged;
// Always marshal to UI thread
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
} }
@ -60,7 +80,6 @@ public sealed class PreviewCanvas : Control
e.PropertyName == nameof(PreviewData.DetectedBoxes) || e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
e.PropertyName == nameof(PreviewData.CropRect)) e.PropertyName == nameof(PreviewData.CropRect))
{ {
// Always marshal to UI thread
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
} }
} }
@ -68,80 +87,51 @@ public sealed class PreviewCanvas : Control
protected override Size MeasureOverride(Size availableSize) => availableSize; protected override Size MeasureOverride(Size availableSize) => availableSize;
protected override Size ArrangeOverride(Size finalSize) => finalSize; protected override Size ArrangeOverride(Size finalSize) => finalSize;
public override void Render(DrawingContext context) // ------------------------------------------------------------
// Unified transform helpers
// ------------------------------------------------------------
private (double X, double Y) TransformPoint(
double x, double y,
double rawW, double rawH,
double offsetX, double offsetY,
double scale,
int rotate,
double pixelAspect)
{ {
var preview = Preview; switch (rotate)
if (preview?.Frame is null)
return;
var frame = preview.Frame;
var rawW = frame.PixelSize.Width;
var rawH = frame.PixelSize.Height;
var dispW = Bounds.Width;
var dispH = Bounds.Height;
if (dispW <= 0 || dispH <= 0)
return;
var rotate = RotateAngle; // 0, 90, 180, 270
// SAR (always original, never rotated)
var sar = Sar ?? new Point2f(1, 1);
var sarX = sar.X;
var sarY = sar.Y;
if (sarX <= 0 || sarY <= 0)
{ {
sarX = 1; case 90:
sarY = 1; (x, y) = (rawH - y, x);
break;
case 180:
x = rawW - x;
y = rawH - y;
break;
case 270:
(x, y) = (y, rawW - x);
break;
} }
var pixelAspect = sarX / sarY;
double displayW;
double displayH;
if (rotate == 0 || rotate == 180) if (rotate == 0 || rotate == 180)
{ x *= pixelAspect;
// encoded horizontal axis = rawW
displayW = rawW * pixelAspect;
displayH = rawH;
}
else else
{ y *= pixelAspect;
// encoded horizontal axis = rawH (bitmap already rotated)
displayW = rawW; var sx = offsetX + x * scale;
displayH = rawH * pixelAspect; var sy = offsetY + y * scale;
return (sx, sy);
} }
var scale = Math.Min(dispW / displayW, dispH / displayH); private Rect TransformRect(
double x, double y, double w, double h,
var scaledW = displayW * scale; double rawW, double rawH,
var scaledH = displayH * scale; double offsetX, double offsetY,
double scale,
var offsetX = (dispW - scaledW) / 2; int rotate,
var offsetY = (dispH - scaledH) / 2; double pixelAspect)
// draw frame
context.DrawImage(
frame,
new Rect(0, 0, rawW, rawH),
new Rect(offsetX, offsetY, scaledW, scaledH));
// overlays
if (preview.DetectedBoxes is { Count: > 0 })
{ {
var pen = new Pen(Brushes.Lime, 2);
foreach (var r in preview.DetectedBoxes)
{
double x = r.X;
double y = r.Y;
double w = r.Width;
double h = r.Height;
// rotate overlay coordinates (still using your existing logic)
switch (rotate) switch (rotate)
{ {
case 90: case 90:
@ -160,7 +150,6 @@ public sealed class PreviewCanvas : Control
break; break;
} }
// apply SAR to the axis that originated from encoded width
if (rotate == 0 || rotate == 180) if (rotate == 0 || rotate == 180)
{ {
x *= pixelAspect; x *= pixelAspect;
@ -172,15 +161,319 @@ public sealed class PreviewCanvas : Control
h *= pixelAspect; h *= pixelAspect;
} }
var rr = new Rect( return new Rect(
offsetX + x * scale, offsetX + x * scale,
offsetY + y * scale, offsetY + y * scale,
w * scale, w * scale,
h * scale); h * scale);
}
// ------------------------------------------------------------
// Hit test for gravitate point (normalized)
// ------------------------------------------------------------
private bool HitGravitate(Avalonia.Point p, out Point2f value)
{
value = default;
var preview = Preview;
if (preview?.Frame is null)
return false;
var g = GravitateTo;
var rawW = preview.Frame.PixelSize.Width;
var rawH = preview.Frame.PixelSize.Height;
// normalized → pixel
double px = g.X * rawW;
double py = g.Y * rawH;
var rotate = RotateAngle;
var sar = Sar ?? new Point2f(1, 1);
var pixelAspect = sar.X / sar.Y;
var dispW = Bounds.Width;
var dispH = Bounds.Height;
double displayW, displayH;
if (rotate == 0 || rotate == 180)
{
displayW = rawW * pixelAspect;
displayH = rawH;
}
else
{
displayW = rawW;
displayH = rawH * pixelAspect;
}
var scale = Math.Min(dispW / displayW, dispH / displayH);
var offsetX = (dispW - displayW * scale) / 2;
var offsetY = (dispH - displayH * scale) / 2;
var (cx, cy) = TransformPoint(
px, py,
rawW, rawH,
offsetX, offsetY,
scale,
rotate,
pixelAspect);
const double radius = 10;
var hit = (p.X - cx) * (p.X - cx) + (p.Y - cy) * (p.Y - cy) <= radius * radius;
if (hit)
value = g;
return hit;
}
// ------------------------------------------------------------
// Pointer events
// ------------------------------------------------------------
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var p = e.GetPosition(this);
if (HitGravitate(p, out var g))
{
_dragging = true;
_dragStartCanvas = p;
_dragStartValue = g; // normalized
e.Pointer.Capture(this);
}
}
private void OnPointerMoved(object? sender, PointerEventArgs e)
{
if (!_dragging || Preview?.Frame is null)
return;
var p = e.GetPosition(this);
var dxCanvas = p.X - _dragStartCanvas.X;
var dyCanvas = p.Y - _dragStartCanvas.Y;
var preview = Preview;
var rawW = preview.Frame.PixelSize.Width;
var rawH = preview.Frame.PixelSize.Height;
var rotate = RotateAngle;
var sar = Sar ?? new Point2f(1, 1);
var pixelAspect = sar.X / sar.Y;
var dispW = Bounds.Width;
var dispH = Bounds.Height;
double displayW, displayH;
if (rotate == 0 || rotate == 180)
{
displayW = rawW * pixelAspect;
displayH = rawH;
}
else
{
displayW = rawW;
displayH = rawH * pixelAspect;
}
var scale = Math.Min(dispW / displayW, dispH / displayH);
double dx = dxCanvas / scale;
double dy = dyCanvas / scale;
if (rotate == 0 || rotate == 180)
dx /= pixelAspect;
else
dy /= pixelAspect;
// start normalized → pixel
double gx = _dragStartValue.X * rawW + dx;
double gy = _dragStartValue.Y * rawH + dy;
switch (rotate)
{
case 90:
(gx, gy) = (gy, rawH - gx);
break;
case 180:
gx = rawW - gx;
gy = rawH - gy;
break;
case 270:
(gx, gy) = (rawW - gy, gx);
break;
}
// pixel → normalized
var nx = (float)(gx / rawW);
var ny = (float)(gy / rawH);
if (nx < 0) nx = 0;
if (ny < 0) ny = 0;
if (nx > 1) nx = 1;
if (ny > 1) ny = 1;
GravitateTo = new Point2f(nx, ny);
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_dragging)
{
_dragging = false;
e.Pointer.Capture(null);
}
}
// ------------------------------------------------------------
// Overlay renderers
// ------------------------------------------------------------
private void RenderCropRectangle(
DrawingContext context,
PreviewData preview,
double rawW, double rawH,
double offsetX, double offsetY,
double scale,
int rotate,
double pixelAspect)
{
if (preview.CropRect is not { } crop)
return;
var rr = TransformRect(
crop.X, crop.Y, crop.Width, crop.Height,
rawW, rawH,
offsetX, offsetY,
scale,
rotate,
pixelAspect);
var pen = new Pen(Brushes.Yellow, 2);
context.DrawRectangle(null, pen, rr);
}
private void RenderGravitateTo(
DrawingContext context,
PreviewData preview,
double rawW, double rawH,
double offsetX, double offsetY,
double scale,
int rotate,
double pixelAspect)
{
var g = GravitateTo;
// normalized → pixel
double px = g.X * rawW;
double py = g.Y * rawH;
var (sx, sy) = TransformPoint(
px, py,
rawW, rawH,
offsetX, offsetY,
scale,
rotate,
pixelAspect);
const double radius = 10;
var circle = new EllipseGeometry(
new Rect(sx - radius, sy - radius, radius * 2, radius * 2));
var pen = new Pen(Brushes.Yellow, 2);
var brush = Brushes.Yellow;
context.DrawGeometry(brush, pen, circle);
}
private void RenderDetectedBoxes(
DrawingContext context,
PreviewData preview,
double rawW, double rawH,
double offsetX, double offsetY,
double scale,
int rotate,
double pixelAspect)
{
if (preview.DetectedBoxes is not { Count: > 0 })
return;
var pen = new Pen(Brushes.Lime, 2);
foreach (var r in preview.DetectedBoxes)
{
var rr = TransformRect(
r.X, r.Y, r.Width, r.Height,
rawW, rawH,
offsetX, offsetY,
scale,
rotate,
pixelAspect);
context.DrawRectangle(null, pen, rr); context.DrawRectangle(null, pen, rr);
} }
} }
// ------------------------------------------------------------
// Main Render
// ------------------------------------------------------------
public override void Render(DrawingContext context)
{
var preview = Preview;
if (preview?.Frame is null)
return;
var frame = preview.Frame;
var rawW = frame.PixelSize.Width;
var rawH = frame.PixelSize.Height;
var dispW = Bounds.Width;
var dispH = Bounds.Height;
if (dispW <= 0 || dispH <= 0)
return;
var rotate = RotateAngle;
var sar = Sar ?? new Point2f(1, 1);
var sarX = sar.X <= 0 ? 1 : sar.X;
var sarY = sar.Y <= 0 ? 1 : sar.Y;
var pixelAspect = sarX / sarY;
double displayW, displayH;
if (rotate == 0 || rotate == 180)
{
displayW = rawW * pixelAspect;
displayH = rawH;
}
else
{
displayW = rawW;
displayH = rawH * pixelAspect;
} }
var scale = Math.Min(dispW / displayW, dispH / displayH);
var scaledW = displayW * scale;
var scaledH = displayH * scale;
var offsetX = (dispW - scaledW) / 2;
var offsetY = (dispH - scaledH) / 2;
context.DrawImage(
frame,
new Rect(0, 0, rawW, rawH),
new Rect(offsetX, offsetY, scaledW, scaledH));
RenderDetectedBoxes(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
RenderCropRectangle(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
RenderGravitateTo(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
}
} }

View File

@ -13,7 +13,8 @@
Grid.Row="0" Grid.Row="0"
Preview="{Binding Preview}" Preview="{Binding Preview}"
Sar="{Binding Sar}" Sar="{Binding Sar}"
RotateAngle="{Binding Rotate}" /> RotateAngle="{Binding Rotate}"
GravitateTo="{Binding GravitateTo}"/>
<Grid Grid.Row="1" <Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto"

View File

@ -6,6 +6,10 @@ namespace splitter;
public sealed class CommandLine public sealed class CommandLine
{ {
// Default vertical Full HD for YouTube Shorts
public const int DefaultW = 607;
public const int DefaultH = 1080;
public SingleJob Master { get; } = new SingleJob(); public SingleJob Master { get; } = new SingleJob();
public SingleJob[] Jobs { get; } public SingleJob[] Jobs { get; }
@ -231,13 +235,9 @@ public sealed class CommandLine
private static (int width, int height)? ParseCrop(string v) private static (int width, int height)? ParseCrop(string v)
{ {
// Default vertical Full HD for YouTube Shorts
const int defaultW = 607;
const int defaultH = 1080;
// Empty or whitespace → default crop // Empty or whitespace → default crop
if (string.IsNullOrWhiteSpace(v)) if (string.IsNullOrWhiteSpace(v))
return (defaultW, defaultH); return (DefaultW, DefaultH);
var s = v.Trim().ToLowerInvariant(); var s = v.Trim().ToLowerInvariant();