// -------------------------------------------------------------------------------------------------------------------- // // 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.Composition; using System.Drawing; using System.Globalization; using Caliburn.Micro; using HandBrake.ApplicationServices.Model; using HandBrake.ApplicationServices.Parsing; using HandBrake.ApplicationServices.Services.Interfaces; using HandBrake.Interop.Model.Encoding; using HandBrakeWPF.ViewModels.Interfaces; /// /// The Picture Settings View Model /// [Export(typeof(IPictureSettingsViewModel))] public class PictureSettingsViewModel : ViewModelBase, IPictureSettingsViewModel { /* * TODO: * Handle Presets when a new title is set * Handle changes in cropping affecting the resolution calcuation. * */ #region Constants and Fields /// /// The display size. /// private string displaySize; /// /// Backing field for for height control enabled /// private bool heightControlEnabled = true; /// /// Backing field for show custom anamorphic controls /// private bool showCustomAnamorphicControls; /// /// The source aspect ratio. /// private double sourceAspectRatio; /// /// The source info. /// private string sourceInfo; /// /// Source Par Values /// private Size sourceParValues; /// /// Source Resolution /// private Size sourceResolution; /// /// Backing field for width control enabled. /// private bool widthControlEnabled = true; #endregion #region Constructors and Destructors /// /// Initializes a new instance of the class. /// /// /// The window manager. /// /// /// The user Setting Service. /// public PictureSettingsViewModel(IWindowManager windowManager, IUserSettingService userSettingService) { this.Task = new EncodeTask(); this.SelectedModulus = 16; this.MaintainAspectRatio = true; } #endregion #region Properties /// /// Gets AnamorphicModes. /// public IEnumerable AnamorphicModes { get { return new List { Anamorphic.None, Anamorphic.Strict, Anamorphic.Loose, Anamorphic.Custom }; } } /// /// Gets or sets CropBottom. /// public int CropBottom { get { return this.Task.Cropping.Bottom; } set { this.Task.Cropping.Bottom = this.CorrectForModulus(this.Task.Cropping.Bottom, value); this.NotifyOfPropertyChange(() => this.CropBottom); } } /// /// Gets or sets CropLeft. /// public int CropLeft { get { return this.Task.Cropping.Left; } set { this.Task.Cropping.Left = this.CorrectForModulus(this.Task.Cropping.Left, value); this.NotifyOfPropertyChange(() => this.CropLeft); } } /// /// Gets or sets CropRight. /// public int CropRight { get { return this.Task.Cropping.Right; } set { this.Task.Cropping.Right = this.CorrectForModulus(this.Task.Cropping.Right, value); this.NotifyOfPropertyChange(() => this.CropRight); } } /// /// Gets or sets CropTop. /// public int CropTop { get { return this.Task.Cropping.Top; } set { this.Task.Cropping.Top = this.CorrectForModulus(this.Task.Cropping.Top, value); this.NotifyOfPropertyChange(() => this.CropTop); } } /// /// Gets or sets DisplaySize. /// public string DisplaySize { get { return this.displaySize; } set { this.displaySize = value; this.NotifyOfPropertyChange(() => this.DisplaySize); } } /// /// Gets or sets DisplayWidth. /// public int DisplayWidth { get { return this.Task.DisplayWidth.HasValue ? int.Parse(Math.Round(this.Task.DisplayWidth.Value, 0).ToString()) : 0; } set { this.Task.DisplayWidth = value; this.CustomAnamorphicAdjust(); this.NotifyOfPropertyChange(() => this.DisplayWidth); } } /// /// Gets or sets Height. /// public int Height { get { return this.Task.Height.HasValue ? this.Task.Height.Value : 0; } set { this.Task.Height = value; this.HeightAdjust(); this.NotifyOfPropertyChange(() => this.Height); } } /// /// Gets or sets a value indicating whether HeightControlEnabled. /// public bool HeightControlEnabled { get { return this.heightControlEnabled; } set { this.heightControlEnabled = value; this.NotifyOfPropertyChange(() => this.HeightControlEnabled); } } /// /// Gets or sets a value indicating whether IsCustomCrop. /// public bool IsCustomCrop { get { return this.Task.HasCropping; } set { this.Task.HasCropping = value; this.NotifyOfPropertyChange(() => this.IsCustomCrop); } } /// /// Gets or sets a value indicating whether MaintainAspectRatio. /// public bool MaintainAspectRatio { get { return this.Task.KeepDisplayAspect; } set { this.Task.KeepDisplayAspect = value; this.WidthAdjust(); this.NotifyOfPropertyChange(() => this.MaintainAspectRatio); } } /// /// Gets ModulusValues. /// public IEnumerable ModulusValues { get { return new List { 16, 8, 4, 2 }; } } /// /// Gets or sets ParHeight. /// public int ParHeight { get { return this.Task.PixelAspectY; } set { this.Task.PixelAspectY = value; this.CustomAnamorphicAdjust(); this.NotifyOfPropertyChange(() => this.ParHeight); } } /// /// Gets or sets ParWidth. /// public int ParWidth { get { return this.Task.PixelAspectX; } set { this.Task.PixelAspectX = value; this.CustomAnamorphicAdjust(); this.NotifyOfPropertyChange(() => this.ParWidth); } } /// /// Gets or sets SelectedAnamorphicMode. /// public Anamorphic SelectedAnamorphicMode { get { return this.Task.Anamorphic; } set { this.Task.Anamorphic = value; this.AnamorphicAdjust(); this.NotifyOfPropertyChange(() => this.SelectedAnamorphicMode); } } /// /// Gets or sets SelectedModulus. /// public int? SelectedModulus { get { return this.Task.Modulus; } set { this.Task.Modulus = value; this.ModulusAdjust(); this.NotifyOfPropertyChange(() => this.SelectedModulus); } } /// /// Gets or sets a value indicating whether ShowCustomAnamorphicControls. /// public bool ShowCustomAnamorphicControls { get { return this.showCustomAnamorphicControls; } set { this.showCustomAnamorphicControls = value; this.NotifyOfPropertyChange(() => this.ShowCustomAnamorphicControls); } } /// /// Gets or sets SourceInfo. /// public string SourceInfo { get { return this.sourceInfo; } set { this.sourceInfo = value; this.NotifyOfPropertyChange(() => this.SourceInfo); } } /// /// Gets or sets Task. /// public EncodeTask Task { get; set; } /// /// Gets or sets Width. /// public int Width { get { return this.Task.Width.HasValue ? this.Task.Width.Value : 0; } set { this.Task.Width = value; this.WidthAdjust(); this.NotifyOfPropertyChange(() => this.Width); } } /// /// Gets or sets a value indicating whether WidthControlEnabled. /// public bool WidthControlEnabled { get { return this.widthControlEnabled; } set { this.widthControlEnabled = value; this.NotifyOfPropertyChange(() => this.WidthControlEnabled); } } /// /// Gets SourceAspect. /// private Size SourceAspect { get { // display aspect = (width * par_width) / (height * par_height) return new Size( (this.sourceParValues.Width * this.sourceResolution.Width), (this.sourceParValues.Height * this.sourceResolution.Height)); } } #endregion #region Implemented Interfaces #region ITabInterface /// /// Setup this tab for the specified preset. /// /// /// The preset. /// /// /// The task. /// public void SetPreset(Preset preset, EncodeTask task) { this.Task = task; // TODO: These all need to be handled correctly. this.SelectedAnamorphicMode = preset.Task.Anamorphic; if (preset.PictureSettingsMode == PresetPictureSettingsMode.SourceMaximum) { this.Task.MaxWidth = preset.Task.MaxWidth; this.Task.MaxHeight = preset.Task.MaxHeight; this.Width = preset.Task.Width ?? (sourceResolution.Width - this.CropLeft - this.CropRight); this.Height = preset.Task.Height ?? (sourceResolution.Height - this.CropTop - this.CropBottom); } else { this.Width = preset.Task.Width ?? (sourceResolution.Width - this.CropLeft - this.CropRight); this.Height = preset.Task.Height ?? (sourceResolution.Height - this.CropTop - this.CropBottom); } if (this.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.MaintainAspectRatio = preset.Task.KeepDisplayAspect; if (this.Task.Modulus.HasValue) { this.SelectedModulus = preset.Task.Modulus; } if (preset.Task.HasCropping) { this.CropLeft = preset.Task.Cropping.Left; this.CropRight = preset.Task.Cropping.Right; this.CropTop = preset.Task.Cropping.Top; this.CropBottom = preset.Task.Cropping.Bottom; } this.NotifyOfPropertyChange(() => this.Task); } /// /// Setup this window for a new source /// /// /// The title. /// /// /// The preset. /// /// /// The task. /// public void SetSource(Title title, Preset preset, EncodeTask task) { this.Task = task; if (title != null) { // Set cached info this.sourceAspectRatio = title.AspectRatio; this.sourceParValues = title.ParVal; this.sourceResolution = title.Resolution; // Set Screen Controls this.SourceInfo = string.Format( "{0}x{1}, Aspect Ratio: {2:0.00}", title.Resolution.Width, title.Resolution.Height, title.AspectRatio); this.CropTop = title.AutoCropDimensions.Top; this.CropBottom = title.AutoCropDimensions.Bottom; this.CropLeft = title.AutoCropDimensions.Left; this.CropRight = title.AutoCropDimensions.Right; // TODO handle preset max width / height this.Width = title.Resolution.Width; this.Height = title.Resolution.Height; this.MaintainAspectRatio = true; } this.NotifyOfPropertyChange(() => this.Task); } #endregion #endregion #region Methods /// /// Adjust other values after the user has altered the anamorphic. /// private void AnamorphicAdjust() { this.DisplaySize = this.sourceResolution.IsEmpty ? "No Title Selected" : string.Format( "{0}x{1}", this.CalculateAnamorphicSizes().Width, this.CalculateAnamorphicSizes().Height); switch (this.SelectedAnamorphicMode) { case Anamorphic.None: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = false; this.Width = this.sourceResolution.Width; this.SetDisplaySize(); break; case Anamorphic.Strict: this.WidthControlEnabled = false; this.HeightControlEnabled = false; this.ShowCustomAnamorphicControls = false; this.Width = 0; this.Height = 0; this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.SetDisplaySize(); break; case Anamorphic.Loose: this.WidthControlEnabled = true; this.HeightControlEnabled = false; this.ShowCustomAnamorphicControls = false; this.Width = this.sourceResolution.Width; this.Height = 0; this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.SetDisplaySize(); break; case Anamorphic.Custom: this.WidthControlEnabled = true; this.HeightControlEnabled = true; this.ShowCustomAnamorphicControls = true; this.Width = this.sourceResolution.Width; this.Height = 0; this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.DisplayWidth = this.CalculateAnamorphicSizes().Width; this.ParWidth = this.sourceParValues.Width; this.ParHeight = this.sourceParValues.Height; this.NotifyOfPropertyChange(() => this.ParHeight); this.NotifyOfPropertyChange(() => this.ParWidth); this.NotifyOfPropertyChange(() => this.DisplayWidth); this.SetDisplaySize(); break; } } /// /// Calculate the Anamorphic Resolution for the selected title. /// /// /// A Size With Width/Height for this title. /// private Size CalculateAnamorphicSizes() { if (this.sourceResolution.IsEmpty) { return new Size(0, 0); } /* Set up some variables to make the math easier to follow. */ int croppedWidth = this.sourceResolution.Width - this.CropLeft - this.CropRight; int croppedHeight = this.sourceResolution.Height - this.CropTop - this.CropBottom; double storageAspect = (double)croppedWidth / croppedHeight; /* Figure out what width the source would display at. */ double sourceDisplayWidth = (double)croppedWidth * this.sourceParValues.Width / this.sourceParValues.Height; /* 3 different ways of deciding output dimensions: - 1: Strict anamorphic, preserve source dimensions - 2: Loose anamorphic, round to mod16 and preserve storage aspect ratio - 3: Power user anamorphic, specify everything */ double calcWidth, calcHeight; switch (this.SelectedAnamorphicMode) { default: case Anamorphic.Strict: /* Strict anamorphic */ double dispWidth = ((double)croppedWidth * this.sourceParValues.Width / this.sourceParValues.Height); dispWidth = Math.Round(dispWidth, 0); Size output = new Size((int)dispWidth, croppedHeight); return output; case Anamorphic.Loose: /* "Loose" anamorphic. - Uses mod16-compliant dimensions, - Allows users to set the width */ calcWidth = this.GetModulusValue(this.Width); /* Time to get picture width that divide cleanly.*/ calcHeight = (calcWidth / storageAspect) + 0.5; calcHeight = this.GetModulusValue(calcHeight); /* Time to get picture height that divide cleanly.*/ /* The film AR is the source's display width / cropped source height. The output display width is the output height * film AR. The output PAR is the output display width / output storage width. */ double pixelAspectWidth = calcHeight * sourceDisplayWidth / croppedHeight; double pixelAspectHeight = calcWidth; double disWidthLoose = (calcWidth * pixelAspectWidth / pixelAspectHeight); if (double.IsNaN(disWidthLoose)) { disWidthLoose = 0; } return new Size((int)disWidthLoose, (int)calcHeight); case Anamorphic.Custom: // Get the User Interface Values double UIdisplayWidth; double.TryParse(this.DisplayWidth.ToString(), out UIdisplayWidth); /* Anamorphic 3: Power User Jamboree - Set everything based on specified values */ calcHeight = this.GetModulusValue(this.Height); if (this.MaintainAspectRatio) { return new Size((int)Math.Truncate(UIdisplayWidth), (int)calcHeight); } return new Size((int)Math.Truncate(UIdisplayWidth), (int)calcHeight); } } /// /// Correct the new value so that the result of the modulus of that value is 0 /// /// /// The old value. /// /// /// The new value. /// /// /// The Value corrected so that for a given modulus the result is 0 /// private int CorrectForModulus(int oldValue, int newValue) { int remainder = newValue % 2; if (remainder == 0) { return newValue; } return newValue > oldValue ? newValue + remainder : newValue - remainder; } /// /// Adjust other values after the user has altered one of the custom anamorphic settings /// private void CustomAnamorphicAdjust() { this.SetDisplaySize(); } /// /// For a given value, correct so that it matches the users currently selected modulus value /// /// /// The value. /// /// /// Value corrected so that value % selected modulus == 0 /// private double GetModulusValue(double value) { if (this.SelectedModulus == null) { return 0; } double remainder = value % this.SelectedModulus.Value; if (remainder == 0) { return value; } return remainder >= ((double)this.SelectedModulus.Value / 2) ? value + (this.SelectedModulus.Value - remainder) : value - remainder; } /// /// Adjust other values after the user has altered the height /// private void HeightAdjust() { if (this.Height > this.sourceResolution.Height) { this.Task.Height = this.sourceResolution.Height; this.NotifyOfPropertyChange(() => this.Task.Height); } switch (this.SelectedAnamorphicMode) { case Anamorphic.None: if (this.MaintainAspectRatio) { double crop_width = this.sourceResolution.Width - this.CropLeft - this.CropRight; double crop_height = this.sourceResolution.Height - this.CropTop - this.CropBottom; double new_width = ((double)this.Height * this.sourceResolution.Height * this.SourceAspect.Width * crop_width) / (this.sourceResolution.Width * this.SourceAspect.Height * crop_height); this.Task.Width = (int)Math.Round(this.GetModulusValue(new_width), 0); this.NotifyOfPropertyChange(() => this.Task.Width); } break; case Anamorphic.Custom: this.SetDisplaySize(); break; } } /// /// Adjust other values after the user has altered the modulus /// private void ModulusAdjust() { this.WidthAdjust(); } /// /// Set the display size text /// private void SetDisplaySize() { this.DisplaySize = this.sourceResolution.IsEmpty ? "No Title Selected" : string.Format( "{0}x{1}", this.CalculateAnamorphicSizes().Width, this.CalculateAnamorphicSizes().Height); } /// /// Adjust other values after the user has altered the width /// private void WidthAdjust() { if (this.Width > this.sourceResolution.Width) { this.Task.Width = this.sourceResolution.Width; this.NotifyOfPropertyChange(() => this.Task.Width); } switch (this.SelectedAnamorphicMode) { case Anamorphic.None: if (this.MaintainAspectRatio) { double crop_width = this.sourceResolution.Width - this.CropLeft - this.CropRight; double crop_height = this.sourceResolution.Height - this.CropTop - this.CropBottom; if (this.SourceAspect.Width == 0 && this.SourceAspect.Height == 0) { break; } double newHeight = ((double)this.Width * this.sourceResolution.Width * this.SourceAspect.Height * crop_height) / (this.sourceResolution.Height * this.SourceAspect.Width * crop_width); this.Task.Height = (int)Math.Round(this.GetModulusValue(newHeight), 0); this.NotifyOfPropertyChange(() => this.Height); } break; case Anamorphic.Strict: this.Task.Width = 0; this.Task.Height = 0; this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.SetDisplaySize(); break; case Anamorphic.Loose: this.Task.Height = 0; this.NotifyOfPropertyChange(() => this.Width); this.NotifyOfPropertyChange(() => this.Height); this.SetDisplaySize(); break; case Anamorphic.Custom: this.SetDisplaySize(); break; } } #endregion } }