// -------------------------------------------------------------------------------------------------------------------- // // This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License. // // // The Picture Settings View Model // // -------------------------------------------------------------------------------------------------------------------- namespace HandBrakeWPF.ViewModels { using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using HandBrake.Interop.Interop; using HandBrake.Interop.Interop.Model; using HandBrake.Interop.Interop.Model.Encoding; using HandBrakeWPF.EventArgs; using HandBrakeWPF.Helpers; using HandBrakeWPF.Model.Picture; using HandBrakeWPF.Properties; using HandBrakeWPF.Services.Presets.Model; using HandBrakeWPF.Services.Scan.Model; using HandBrakeWPF.Utilities; using HandBrakeWPF.ViewModelItems.Filters; using HandBrakeWPF.ViewModels.Interfaces; using EncodeTask = HandBrakeWPF.Services.Encode.Model.EncodeTask; public class PictureSettingsViewModel : ViewModelBase, IPictureSettingsViewModel { private string displaySize; private bool heightControlEnabled = true; private bool showCustomAnamorphicControls; private string sourceInfo; private Size sourceParValues; private Size sourceResolution; private bool widthControlEnabled = true; private bool showModulus; private bool showDisplaySize; private bool showKeepAr = true; private DelayedActionProcessor delayedPreviewprocessor = new DelayedActionProcessor(); private Title currentTitle; private Source scannedSource; private PictureSettingsResLimitModes selectedPictureSettingsResLimitMode; public PictureSettingsViewModel(IStaticPreviewViewModel staticPreviewViewModel) { this.StaticPreviewViewModel = staticPreviewViewModel; this.sourceResolution = new Size(0, 0); this.Task = new EncodeTask(); this.PaddingFilter = new PadFilter(this.Task, () => this.OnTabStatusChanged(null)); this.RotateFlipFilter = new RotateFlipFilter(this.Task, () => this.OnTabStatusChanged(null)); this.Init(); } public event EventHandler TabStatusChanged; /* Properties */ public IStaticPreviewViewModel StaticPreviewViewModel { get; set; } public IEnumerable AnamorphicModes { get; } = new List { Anamorphic.None, Anamorphic.Automatic, Anamorphic.Loose, Anamorphic.Custom }; public PadFilter PaddingFilter { get; set; } public RotateFlipFilter RotateFlipFilter { get; set; } public string DisplaySize { get => this.displaySize; set { this.displaySize = value; this.NotifyOfPropertyChange(() => this.DisplaySize); } } public bool HeightControlEnabled { get => this.heightControlEnabled; set { this.heightControlEnabled = value; this.NotifyOfPropertyChange(() => this.HeightControlEnabled); } } public IEnumerable ModulusValues { get; } = new List { 16, 8, 4, 2 }; public bool ShowCustomAnamorphicControls { get => this.showCustomAnamorphicControls; set { this.showCustomAnamorphicControls = value; this.NotifyOfPropertyChange(() => this.ShowCustomAnamorphicControls); } } public string SourceInfo { get => this.sourceInfo; set { this.sourceInfo = value; this.NotifyOfPropertyChange(() => this.SourceInfo); } } public EncodeTask Task { get; set; } public bool WidthControlEnabled { get => this.widthControlEnabled; set { this.widthControlEnabled = value; this.NotifyOfPropertyChange(() => this.WidthControlEnabled); } } public bool ShowModulus { get => this.showModulus; set { this.showModulus = value; this.NotifyOfPropertyChange(() => this.ShowModulus); } } public bool ShowDisplaySize { get => this.showDisplaySize; set { this.showDisplaySize = value; this.NotifyOfPropertyChange(() => this.ShowDisplaySize); } } public int? MaxHeight { get => this.Task.MaxHeight; set { if (value != this.Task.MaxHeight) { this.Task.MaxHeight = value; this.NotifyOfPropertyChange(() => this.MaxHeight); } } } public int? MaxWidth { get => this.Task.MaxWidth; set { if (value != this.Task.MaxWidth) { this.Task.MaxWidth = value; this.NotifyOfPropertyChange(() => this.MaxWidth); } } } public bool ShowKeepAR { get => this.showKeepAr; set { this.showKeepAr = value; this.NotifyOfPropertyChange(() => this.ShowKeepAR); } } public BindingList ResolutionLimitModes => new BindingList { PictureSettingsResLimitModes.Size8K, PictureSettingsResLimitModes.Size4K, PictureSettingsResLimitModes.Size1080p, PictureSettingsResLimitModes.Size720p, PictureSettingsResLimitModes.Size576p, PictureSettingsResLimitModes.Size480p, PictureSettingsResLimitModes.Custom, }; public PictureSettingsResLimitModes SelectedPictureSettingsResLimitMode { get => this.selectedPictureSettingsResLimitMode; set { if (value == this.selectedPictureSettingsResLimitMode) { return; } this.selectedPictureSettingsResLimitMode = value; this.NotifyOfPropertyChange(() => this.SelectedPictureSettingsResLimitMode); this.IsCustomMaxRes = value == PictureSettingsResLimitModes.Custom; this.NotifyOfPropertyChange(() => this.IsCustomMaxRes); // Enforce the new limit ResLimit limit = EnumHelper.GetAttribute(value); if (value == PictureSettingsResLimitModes.Custom && this.Task.MaxWidth == null && this.Task.MaxHeight == null) { // Default to 4K if null! limit = EnumHelper.GetAttribute(PictureSettingsResLimitModes.Size4K); } if (limit != null) { this.Task.MaxWidth = limit.Width; this.Task.MaxHeight = limit.Height; if (this.MaxWidth.HasValue && this.Width > this.MaxWidth) { this.Width = this.MaxWidth.Value; } if (this.MaxHeight.HasValue && this.Height > this.MaxHeight) { this.Height = this.MaxHeight.Value; } } if (value == PictureSettingsResLimitModes.None) { this.Task.MaxWidth = null; this.Task.MaxHeight = null; } this.NotifyOfPropertyChange(() => this.MaxWidth); this.NotifyOfPropertyChange(() => this.MaxHeight); } } public bool IsCustomMaxRes { get; private set; } /* Task Properties */ public int CropBottom { get => this.Task.Cropping.Bottom; set { this.Task.Cropping.Bottom = value; this.NotifyOfPropertyChange(() => this.CropBottom); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } public int CropLeft { get => this.Task.Cropping.Left; set { this.Task.Cropping.Left = value; this.NotifyOfPropertyChange(() => this.CropLeft); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } public int CropRight { get => this.Task.Cropping.Right; set { this.Task.Cropping.Right = value; this.NotifyOfPropertyChange(() => this.CropRight); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } public int CropTop { get => this.Task.Cropping.Top; set { this.Task.Cropping.Top = value; this.NotifyOfPropertyChange(() => this.CropTop); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Crop); } } public bool IsCustomCrop { get => this.Task.HasCropping; set { this.Task.HasCropping = value; this.NotifyOfPropertyChange(() => this.IsCustomCrop); if (!value && this.currentTitle != null) { this.CropTop = currentTitle.AutoCropDimensions.Top; this.CropBottom = currentTitle.AutoCropDimensions.Bottom; this.CropLeft = currentTitle.AutoCropDimensions.Left; this.CropRight = currentTitle.AutoCropDimensions.Right; } } } public long DisplayWidth { get => this.Task.DisplayWidth.HasValue ? int.Parse(Math.Round(this.Task.DisplayWidth.Value, 0).ToString(CultureInfo.InvariantCulture)) : 0; set { if (!object.Equals(this.Task.DisplayWidth, value)) { this.Task.DisplayWidth = value; this.NotifyOfPropertyChange(() => this.DisplayWidth); this.RecaulcatePictureSettingsProperties(ChangedPictureField.DisplayWidth); } } } public int Width { get => this.Task.Width.HasValue ? this.Task.Width.Value : 0; set { if (!object.Equals(this.Task.Width, value)) { this.Task.Width = value; this.NotifyOfPropertyChange(() => this.Width); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); } } } public int Height { get => this.Task.Height.HasValue ? this.Task.Height.Value : 0; set { if (!object.Equals(this.Task.Height, value)) { this.Task.Height = value; this.NotifyOfPropertyChange(() => this.Height); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Height); } } } public bool MaintainAspectRatio { get => this.Task.KeepDisplayAspect; set { this.Task.KeepDisplayAspect = value; this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); this.RecaulcatePictureSettingsProperties(ChangedPictureField.MaintainAspectRatio); } } public int ParHeight { get => this.Task.PixelAspectY; set { if (!object.Equals(this.Task.PixelAspectY, value)) { this.Task.PixelAspectY = value; this.NotifyOfPropertyChange(() => this.ParHeight); this.RecaulcatePictureSettingsProperties(ChangedPictureField.ParH); } } } public int ParWidth { get => this.Task.PixelAspectX; set { if (!object.Equals(this.Task.PixelAspectX, value)) { this.Task.PixelAspectX = value; this.NotifyOfPropertyChange(() => this.ParWidth); this.RecaulcatePictureSettingsProperties(ChangedPictureField.ParW); } } } public Anamorphic SelectedAnamorphicMode { get => this.Task.Anamorphic; set { if (!object.Equals(this.SelectedAnamorphicMode, value)) { this.Task.Anamorphic = value; this.NotifyOfPropertyChange(() => this.SelectedAnamorphicMode); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Anamorphic); this.OnTabStatusChanged(null); } } } public int? SelectedModulus { get => this.Task.Modulus; set { this.Task.Modulus = value; this.NotifyOfPropertyChange(() => this.SelectedModulus); this.RecaulcatePictureSettingsProperties(ChangedPictureField.Modulus); this.OnTabStatusChanged(null); } } /* Public Tab API Implementation */ public void SetPreset(Preset preset, EncodeTask task) { this.Task = task; // Cropping if (preset.Task.HasCropping) { this.IsCustomCrop = true; this.Task.Cropping.Left = preset.Task.Cropping.Left; this.Task.Cropping.Right = preset.Task.Cropping.Right; this.Task.Cropping.Top = preset.Task.Cropping.Top; this.Task.Cropping.Bottom = preset.Task.Cropping.Bottom; this.NotifyOfPropertyChange(() => this.CropLeft); this.NotifyOfPropertyChange(() => this.CropRight); this.NotifyOfPropertyChange(() => this.CropTop); this.NotifyOfPropertyChange(() => this.CropBottom); } else { this.IsCustomCrop = false; } // Padding and Rotate Filters this.PaddingFilter.SetPreset(preset, task); this.RotateFlipFilter?.SetPreset(preset, task); // Picture Sizes and Anamorphic this.SelectedAnamorphicMode = preset.Task.Anamorphic; // Modulus if (preset.Task.Modulus.HasValue) { this.SelectedModulus = preset.Task.Modulus; } // Set the Maintain Aspect ratio. this.MaintainAspectRatio = preset.Task.KeepDisplayAspect; // Setup the Maximum Width / Height with sane 4K fallback. this.MaxWidth = preset.Task.MaxWidth ?? 3840; this.MaxHeight = preset.Task.MaxHeight ?? 2160; this.SetSelectedPictureSettingsResLimitMode(); // Set the width, then check the height doesn't breach the max height and correct if necessary. int width = this.GetModulusValue(this.GetRes((this.sourceResolution.Width - this.CropLeft - this.CropRight), this.MaxWidth)); int height = this.GetModulusValue(this.GetRes((this.sourceResolution.Height - this.CropTop - this.CropBottom), this.MaxHeight)); // Set the backing fields to avoid triggering re-calculation until both are set. this.Task.Width = width; this.Task.Height = height; // Trigger a calculation this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); // Update the UI this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); // Custom Anamorphic if (preset.Task.Anamorphic == Anamorphic.Custom) { this.DisplayWidth = preset.Task.DisplayWidth != null ? int.Parse(preset.Task.DisplayWidth.ToString()) : 0; this.ParWidth = preset.Task.PixelAspectX; this.ParHeight = preset.Task.PixelAspectY; } this.NotifyOfPropertyChange(() => this.Task); this.UpdateVisibileControls(); } public void UpdateTask(EncodeTask task) { this.Task = task; this.SetSelectedPictureSettingsResLimitMode(); this.PaddingFilter.UpdateTask(task); this.RotateFlipFilter?.UpdateTask(task); this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.NotifyOfPropertyChange(() => this.SelectedAnamorphicMode); this.NotifyOfPropertyChange(() => this.SelectedModulus); this.NotifyOfPropertyChange(() => this.CropTop); this.NotifyOfPropertyChange(() => this.CropBottom); this.NotifyOfPropertyChange(() => this.CropLeft); this.NotifyOfPropertyChange(() => this.CropRight); this.NotifyOfPropertyChange(() => this.IsCustomCrop); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); this.NotifyOfPropertyChange(() => this.DisplayWidth); this.NotifyOfPropertyChange(() => this.ParWidth); this.NotifyOfPropertyChange(() => this.ParHeight); this.NotifyOfPropertyChange(() => this.MaxWidth); this.NotifyOfPropertyChange(() => this.MaxHeight); this.UpdateVisibileControls(); } public void SetSource(Source source, Title title, Preset preset, EncodeTask task) { this.currentTitle = title; this.Task = task; this.PaddingFilter.SetSource(source, title, preset, task); this.RotateFlipFilter?.SetSource(source, title, preset, task); this.scannedSource = source; if (title != null) { // Set cached info this.sourceParValues = title.ParVal; this.sourceResolution = title.Resolution; // Update the cropping values, preffering those in the presets. if (!preset.Task.HasCropping) { this.Task.Cropping.Top = title.AutoCropDimensions.Top; this.Task.Cropping.Bottom = title.AutoCropDimensions.Bottom; this.Task.Cropping.Left = title.AutoCropDimensions.Left; this.Task.Cropping.Right = title.AutoCropDimensions.Right; this.IsCustomCrop = false; } else { this.Task.Cropping.Left = preset.Task.Cropping.Left; this.Task.Cropping.Right = preset.Task.Cropping.Right; this.Task.Cropping.Top = preset.Task.Cropping.Top; this.Task.Cropping.Bottom = preset.Task.Cropping.Bottom; this.IsCustomCrop = true; } // Setup the Maximum Width / Height with sane 4K fallback. this.MaxWidth = preset.Task.MaxWidth ?? 3840; this.MaxHeight = preset.Task.MaxHeight ?? 2160; this.SetSelectedPictureSettingsResLimitMode(); // Set the W/H // Set the width, then check the height doesn't breach the max height and correct if necessary. this.Task.Width = this.GetModulusValue(this.GetRes((this.sourceResolution.Width - this.CropLeft - this.CropRight), this.MaxWidth)); this.Task.Height = this.GetModulusValue(this.GetRes((this.sourceResolution.Height - this.CropTop - this.CropBottom), this.MaxHeight)); this.MaintainAspectRatio = preset.Task.KeepDisplayAspect; // Set Screen Controls this.SourceInfo = string.Format( "{0}x{1}, PAR: {2}/{3}", title.Resolution.Width, title.Resolution.Height, title.ParVal.Width, title.ParVal.Height); // Force a re-calc. This will handle MaxWidth / Height corrections. this.RecaulcatePictureSettingsProperties(ChangedPictureField.Width); } this.NotifyOfPropertyChange(() => this.Task); } public bool MatchesPreset(Preset preset) { if (preset.Task.Anamorphic != this.SelectedAnamorphicMode) { return false; } if (preset.Task.Modulus != this.SelectedModulus) { return false; } if (!PaddingFilter.MatchesPreset(preset)) { return false; } if (!RotateFlipFilter.MatchesPreset(preset)) { return false; } return true; } /* Protected and Private Methods */ protected virtual void OnTabStatusChanged(TabStatusEventArgs e) { this.TabStatusChanged?.Invoke(this, e); } private void Init() { this.Task.Modulus = 16; this.Task.KeepDisplayAspect = true; this.NotifyOfPropertyChange(() => this.SelectedModulus); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); // Default the Max Width / Height to 4K format this.MaxHeight = 2160; this.MaxWidth = 3480; this.SetSelectedPictureSettingsResLimitMode(); } private PictureSize.PictureSettingsTitle GetPictureTitleInfo() { PictureSize.PictureSettingsTitle title = new PictureSize.PictureSettingsTitle { Width = this.sourceResolution.Width, Height = this.sourceResolution.Height, ParW = this.sourceParValues.Width, ParH = this.sourceParValues.Height, }; return title; } private PictureSize.PictureSettingsJob GetPictureSettings(ChangedPictureField changedField) { PictureSize.PictureSettingsJob job = new PictureSize.PictureSettingsJob { Width = this.Width, Height = this.Height, ItuPar = false, Modulus = this.SelectedModulus, ParW = this.SelectedAnamorphicMode == Anamorphic.None ? 1 : this.ParWidth, ParH = this.SelectedAnamorphicMode == Anamorphic.None ? 1 : this.ParHeight, MaxWidth = this.MaxWidth.HasValue ? this.MaxWidth.Value : 0, MaxHeight = this.MaxHeight.HasValue ? this.MaxHeight.Value : 0, KeepDisplayAspect = this.MaintainAspectRatio, AnamorphicMode = this.SelectedAnamorphicMode, Crop = new Cropping(this.CropTop, this.CropBottom, this.CropLeft, this.CropRight), }; if (this.SelectedAnamorphicMode == Anamorphic.Custom) { if (changedField == ChangedPictureField.DisplayWidth) { var displayWidth = this.DisplayWidth; job.ParW = (int)displayWidth; // num job.ParH = job.Width; // den } } // Reduce the Par W/H if we can. Don't do it while the user is altering the PAR controls through as it will mess with the result. if (changedField != ChangedPictureField.ParH && changedField != ChangedPictureField.ParW) { long x, y; HandBrakeUtils.Reduce(job.ParW, job.ParH, out x, out y); job.ParW = (int)y; job.ParH = (int)x; } return job; } /// /// Recalculate the picture settings when the user changes a particular field defined in the ChangedPictureField enum. /// The properties in this class are dumb. They simply call this method if there is a change. /// It is the job of this method to update all affected private fields and raise change notifications. /// /// /// The changed field. /// private void RecaulcatePictureSettingsProperties(ChangedPictureField changedField) { // Sanity Check if (this.currentTitle == null) { return; } // Step 1, Update what controls are visible. this.UpdateVisibileControls(); // Step 2, Set sensible defaults if (changedField == ChangedPictureField.Anamorphic && (this.SelectedAnamorphicMode == Anamorphic.None || this.SelectedAnamorphicMode == Anamorphic.Loose)) { this.Task.Width = this.sourceResolution.Width > this.MaxWidth ? this.MaxWidth : this.sourceResolution.Width; this.Task.KeepDisplayAspect = true; } // Choose which setting to keep. PictureSize.KeepSetting setting = PictureSize.KeepSetting.HB_KEEP_WIDTH; switch (changedField) { case ChangedPictureField.Width: setting = PictureSize.KeepSetting.HB_KEEP_WIDTH; break; case ChangedPictureField.Height: setting = PictureSize.KeepSetting.HB_KEEP_HEIGHT; break; } // Step 2, For the changed field, call hb_set_anamorphic_size and process the results. PictureSize.AnamorphicResult result = PictureSize.hb_set_anamorphic_size2(this.GetPictureSettings(changedField), this.GetPictureTitleInfo(), setting); double dispWidth = Math.Round((result.OutputWidth * result.OutputParWidth / result.OutputParHeight), 0); this.Task.Width = result.OutputWidth; this.Task.Height = result.OutputHeight; long x, y; HandBrakeUtils.Reduce((int)Math.Round(result.OutputParWidth, 0), (int)Math.Round(result.OutputParHeight, 0), out x, out y); this.Task.PixelAspectX = (int)Math.Round(result.OutputParWidth, 0); this.Task.PixelAspectY = (int)Math.Round(result.OutputParHeight, 0); this.Task.DisplayWidth = dispWidth; // Step 3, Set the display width label to indicate the output. this.DisplaySize = this.sourceResolution == null || this.sourceResolution.IsEmpty ? string.Empty : string.Format(Resources.PictureSettingsViewModel_StorageDisplayLabel, dispWidth, result.OutputHeight, this.ParWidth, this.ParHeight); // Step 4, Force an update on all the UI elements. this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.NotifyOfPropertyChange(() => this.DisplayWidth); this.NotifyOfPropertyChange(() => this.ParWidth); this.NotifyOfPropertyChange(() => this.ParHeight); this.NotifyOfPropertyChange(() => this.CropTop); this.NotifyOfPropertyChange(() => this.CropBottom); this.NotifyOfPropertyChange(() => this.CropLeft); this.NotifyOfPropertyChange(() => this.CropRight); this.NotifyOfPropertyChange(() => this.SelectedModulus); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); // Step 5, Update the Preview if (delayedPreviewprocessor != null && this.Task != null && this.StaticPreviewViewModel != null && this.StaticPreviewViewModel.IsOpen) { delayedPreviewprocessor.PerformTask(() => this.StaticPreviewViewModel.UpdatePreviewFrame(this.Task, this.scannedSource), 800); } } private void UpdateVisibileControls() { this.ShowDisplaySize = true; this.ShowKeepAR = true; switch (this.SelectedAnamorphicMode) { case Anamorphic.None: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowDisplaySize = true; this.ShowKeepAR = true; break; case Anamorphic.Automatic: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowKeepAR = false; break; case Anamorphic.Loose: this.WidthControlEnabled = true; this.HeightControlEnabled = false; this.ShowCustomAnamorphicControls = false; this.ShowModulus = true; this.ShowKeepAR = false; break; case Anamorphic.Custom: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = true; this.ShowModulus = true; this.ShowDisplaySize = true; this.ShowKeepAR = true; break; } } /// /// For a given value, correct so that it matches the users currently selected modulus value /// /// /// Value corrected so that value % selected modulus == 0 /// private int GetModulusValue(double value) { if (this.SelectedModulus == null) { return 0; } double remainder = value % this.SelectedModulus.Value; if (remainder.Equals(0.0d)) { return (int)Math.Abs(value); } double result = remainder >= ((double)this.SelectedModulus.Value / 2) ? value + (this.SelectedModulus.Value - remainder) : value - remainder; return (int)Math.Abs(result); } private int GetRes(int value, int? max) { return max.HasValue ? (value > max.Value ? max.Value : value) : value; } private void SetSelectedPictureSettingsResLimitMode() { // Look for a matching resolution. foreach (PictureSettingsResLimitModes limit in EnumHelper.GetEnumList()) { ResLimit resLimit = EnumHelper.GetAttribute(limit); if (resLimit != null) { if (resLimit.Width == this.MaxWidth && resLimit.Height == this.MaxHeight) { this.SelectedPictureSettingsResLimitMode = limit; return; } } } if (this.MaxWidth.HasValue || this.MaxHeight.HasValue) { this.SelectedPictureSettingsResLimitMode = PictureSettingsResLimitModes.Custom; } } } public enum ChangedPictureField { Width, Height, ParW, ParH, DisplayWidth, Crop, Anamorphic, MaintainAspectRatio, Modulus } }