/* HBPicture.m $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBPicture.h" #import "HBTitle.h" #import "HBCodingUtilities.h" #import "HBMutablePreset.h" #include "hb.h" NSString * const HBPictureChangedNotification = @"HBPictureChangedNotification"; @interface HBPicture () @property (nonatomic, readwrite, getter=isValidating) BOOL validating; @property (nonatomic, readwrite, getter=areNotificationsEnabled) BOOL notificationsEnabled; @property (nonatomic, readwrite) int keep; @property (nonatomic, readwrite) BOOL darUpdated; @property (nonatomic, readonly) int sourceParNum; @property (nonatomic, readonly) int sourceParDen; @property (nonatomic, readonly) int autoCropTop; @property (nonatomic, readonly) int autoCropBottom; @property (nonatomic, readonly) int autoCropLeft; @property (nonatomic, readonly) int autoCropRight; @end @implementation HBPicture - (instancetype)init { self = [super init]; if (self) { // Set some values if we ever need a fake instance _width = 1280; _height = 720; _sourceWidth = 1280; _sourceHeight = 720; _anamorphicMode = HBPictureAnarmophicModeNone; _modulus = 2; _parWidth = 1; _parHeight = 1; _sourceParNum = 1; _sourceParDen = 1; } return self; } - (instancetype)initWithTitle:(HBTitle *)title { self = [self init]; if (self) { _width = title.width; _height = title.height; _sourceWidth = title.width; _sourceHeight = title.height; _sourceParNum = title.parWidth; _sourceParDen = title.parHeight; _autoCropTop = title.autoCropTop; _autoCropBottom = title.autoCropBottom; _autoCropLeft = title.autoCropLeft; _autoCropRight = title.autoCropRight; [self validateSettings]; _notificationsEnabled = YES; } return self; } - (void)postChangedNotification { if (self.areNotificationsEnabled) { [[NSNotificationCenter defaultCenter] postNotification: [NSNotification notificationWithName:HBPictureChangedNotification object:self userInfo:nil]]; } } - (void)setWidth:(int)width { if (width != _width) { [[self.undo prepareWithInvocationTarget:self] setWidth:_width]; } _width = width; if (!self.isValidating) { self.keep |= HB_KEEP_WIDTH; [self validateSettings]; } } - (BOOL)validateWidth:(id *)ioValue error:(NSError * __autoreleasing *)outError { BOOL retval = YES; if (nil != *ioValue) { int value = [*ioValue intValue]; int roundedValue = value - (value % self.modulus); if (value >= self.maxWidth) { *ioValue = @(self.maxWidth); } else if (value <= 32) { *ioValue = @32; } else if (value != roundedValue) { *ioValue = @(roundedValue); } } return retval; } - (void)setHeight:(int)height { if (height != _height) { [[self.undo prepareWithInvocationTarget:self] setHeight:_height]; } _height = height; if (!self.isValidating) { self.keep |= HB_KEEP_HEIGHT; [self validateSettings]; } } - (BOOL)validateHeight:(id *)ioValue error:(NSError * __autoreleasing *)outError { BOOL retval = YES; if (nil != *ioValue) { int value = [*ioValue intValue]; int roundedValue = value - (value % self.modulus); if (value >= self.maxHeight) { *ioValue = @(self.maxHeight); } else if (value <= 32) { *ioValue = @32; } else if (value != roundedValue) { *ioValue = @(roundedValue); } } return retval; } - (void)setDisplayWidth:(int)displayWidth { if (displayWidth != _displayWidth) { [[self.undo prepareWithInvocationTarget:self] setDisplayWidth:_displayWidth]; } _displayWidth = displayWidth; if (!self.isValidating) { self.darUpdated = YES; [self validateSettings]; } } - (void)setParWidth:(int)parWidth { if (parWidth != _parWidth) { [[self.undo prepareWithInvocationTarget:self] setParWidth:_parWidth]; } _parWidth = parWidth; if (!self.isValidating) { [self validateSettings]; } } - (void)setParHeight:(int)parHeight { if (parHeight != _parHeight) { [[self.undo prepareWithInvocationTarget:self] setParHeight:_parHeight]; } _parHeight = parHeight; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropTop:(int)cropTop { if (cropTop != _cropTop) { [[self.undo prepareWithInvocationTarget:self] setCropTop:_cropTop]; } _cropTop = cropTop; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropBottom:(int)cropBottom { if (cropBottom != _cropBottom) { [[self.undo prepareWithInvocationTarget:self] setCropBottom:_cropBottom]; } _cropBottom = cropBottom; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropLeft:(int)cropLeft { if (cropLeft != _cropLeft) { [[self.undo prepareWithInvocationTarget:self] setCropLeft:_cropLeft]; } _cropLeft = cropLeft; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropRight:(int)cropRight { if (cropRight != _cropRight) { [[self.undo prepareWithInvocationTarget:self] setCropRight:_cropRight]; } _cropRight = cropRight; if (!self.isValidating) { [self validateSettings]; } } - (BOOL)validateCropTop:(id *)ioValue error:(NSError * __autoreleasing *)outError { [self validateCrop:ioValue max:self.maxTopCrop]; return YES; } - (BOOL)validateCropBottom:(id *)ioValue error:(NSError * __autoreleasing *)outError { [self validateCrop:ioValue max:self.maxBottomCrop]; return YES; } - (BOOL)validateCropLeft:(id *)ioValue error:(NSError * __autoreleasing *)outError { [self validateCrop:ioValue max:self.maxLeftCrop]; return YES; } - (BOOL)validateCropRight:(id *)ioValue error:(NSError * __autoreleasing *)outError { [self validateCrop:ioValue max:self.maxRightCrop]; return YES; } - (void)validateCrop:(NSNumber **)ioValue max:(int)maxCrop { if (nil != *ioValue) { int value = [*ioValue intValue]; if (value >= maxCrop) { *ioValue = @(maxCrop); } else if (value < 0) { *ioValue = @0; } } } - (void)setAutocrop:(BOOL)autocrop { if (autocrop != _autocrop) { [[self.undo prepareWithInvocationTarget:self] setAutocrop:_autocrop]; } _autocrop = autocrop; if (autocrop && !self.isValidating) { if (!(self.undo.isUndoing || self.undo.isRedoing)) { self.validating = YES; // Reset the crop values to those determined right after scan self.cropTop = self.autoCropTop; self.cropBottom = self.autoCropBottom; self.cropLeft = self.autoCropLeft; self.cropRight = self.autoCropRight; self.validating = NO; } [self validateSettings]; } } - (void)setAnamorphicMode:(HBPictureAnarmophicMode)anamorphicMode { if (anamorphicMode != _anamorphicMode) { [[self.undo prepareWithInvocationTarget:self] setAnamorphicMode:_anamorphicMode]; } _anamorphicMode = anamorphicMode; if (self.anamorphicMode == HB_ANAMORPHIC_AUTO || self.anamorphicMode == HB_ANAMORPHIC_LOOSE) { self.keepDisplayAspect = YES; } if (!self.isValidating) { [self validateSettings]; } } - (void)setKeepDisplayAspect:(BOOL)keepDisplayAspect { if (keepDisplayAspect != _keepDisplayAspect) { [[self.undo prepareWithInvocationTarget:self] setKeepDisplayAspect:_keepDisplayAspect]; } _keepDisplayAspect = keepDisplayAspect; if (!self.isValidating) { [self validateSettings]; } } - (void)setModulus:(int)modulus { if (modulus != _modulus) { [[self.undo prepareWithInvocationTarget:self] setModulus:_modulus]; } _modulus = modulus; if (!self.isValidating) { [self validateSettings]; } } #pragma mark - Max sizes + (NSSet *)keyPathsForValuesAffectingMaxWidth { return [NSSet setWithObjects:@"cropTop", @"cropBottom", @"cropLeft", @"cropRight", nil]; } - (int)maxWidth { return self.sourceWidth - self.cropRight - self.cropLeft; } + (NSSet *)keyPathsForValuesAffectingMaxHeight { return [NSSet setWithObjects:@"cropTop", @"cropBottom", @"cropLeft", @"cropRight", nil]; } - (int)maxHeight { return self.sourceHeight - self.cropTop - self.cropBottom; } + (NSSet *)keyPathsForValuesAffectingMaxTopCrop { return [NSSet setWithObjects:@"cropBottom", nil]; } - (int)maxTopCrop { return self.sourceHeight - self.cropBottom - 32; } + (NSSet *)keyPathsForValuesAffectingMaxBottomCrop { return [NSSet setWithObjects:@"cropTop", nil]; } - (int)maxBottomCrop { return self.sourceHeight - self.cropTop - 32; } + (NSSet *)keyPathsForValuesAffectingMaxLeftCrop { return [NSSet setWithObjects:@"cropRight", nil]; } - (int)maxLeftCrop { return self.sourceWidth - self.cropRight - 32; } + (NSSet *)keyPathsForValuesAffectingMaxRightCrop { return [NSSet setWithObjects:@"cropLeft", nil]; } - (int)maxRightCrop { return self.sourceWidth - self.cropLeft - 32; } - (int)sourceDisplayWidth { return (int) (self.sourceWidth * self.sourceParNum / (double)self.sourceParDen); } - (void)setNilValueForKey:(NSString *)key { if ([key isEqualToString:@"width"] || [key isEqualToString:@"height"]) { [self setValue:@64 forKey:key]; } else { [self setValue:@0 forKey:key]; } } #pragma mark - Picture Update Logic /** * Validates the settings through hb_set_anamorphic_size2, * each setters calls this after setting its value. */ - (void)validateSettings { self.validating = YES; if (!(self.undo.isUndoing || self.undo.isRedoing)) { self.keep |= self.keepDisplayAspect * HB_KEEP_DISPLAY_ASPECT; hb_geometry_t srcGeo, resultGeo; hb_geometry_settings_t uiGeo; srcGeo.width = self.sourceWidth; srcGeo.height = self.sourceHeight; srcGeo.par.num = self.sourceParNum; srcGeo.par.den = self.sourceParDen; uiGeo.mode = (int)self.anamorphicMode; uiGeo.keep = self.keep; uiGeo.itu_par = 0; uiGeo.modulus = self.modulus; int crop[4] = {self.cropTop, self.cropBottom, self.cropLeft, self.cropRight}; memcpy(uiGeo.crop, crop, sizeof(int[4])); uiGeo.geometry.width = self.width; uiGeo.geometry.height = self.height; // Modulus added to maxWidth/maxHeight to allow a small amount of // upscaling to the next mod boundary. uiGeo.maxWidth = self.sourceWidth - crop[2] - crop[3] + self.modulus - 1; uiGeo.maxHeight = self.sourceHeight - crop[0] - crop[1] + self.modulus - 1; hb_rational_t par = {self.parWidth, self.parHeight}; uiGeo.geometry.par = par; if (self.anamorphicMode == HB_ANAMORPHIC_CUSTOM && self.darUpdated) { uiGeo.geometry.par.num = self.displayWidth; uiGeo.geometry.par.den = uiGeo.geometry.width; } hb_set_anamorphic_size2(&srcGeo, &uiGeo, &resultGeo); int display_width; display_width = resultGeo.width * resultGeo.par.num / resultGeo.par.den; self.width = resultGeo.width; self.height = resultGeo.height; self.parWidth = resultGeo.par.num; self.parHeight = resultGeo.par.den; self.displayWidth = display_width; } self.validating = NO; self.keep = 0; self.darUpdated = NO; [self postChangedNotification]; } #pragma mark - NSCopying - (instancetype)copyWithZone:(NSZone *)zone { HBPicture *copy = [[[self class] alloc] init]; if (copy) { copy->_width = _width; copy->_height = _height; copy->_keepDisplayAspect = _keepDisplayAspect; copy->_anamorphicMode = _anamorphicMode; copy->_modulus = _modulus; copy->_displayWidth = _displayWidth; copy->_parWidth = _parWidth; copy->_parHeight = _parHeight; copy->_autocrop = _autocrop; copy->_cropTop = _cropTop; copy->_cropBottom = _cropBottom; copy->_cropLeft = _cropLeft; copy->_cropRight = _cropRight; copy->_autoCropTop = _autoCropTop; copy->_autoCropBottom = _autoCropBottom; copy->_autoCropLeft = _autoCropLeft; copy->_autoCropRight = _autoCropRight; copy->_sourceWidth = _sourceWidth; copy->_sourceHeight = _sourceHeight; copy->_sourceParNum = _sourceParNum; copy->_sourceParDen = _sourceParDen; copy->_notificationsEnabled = _notificationsEnabled; } return copy; } #pragma mark - NSCoding + (BOOL)supportsSecureCoding { return YES; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeInt:1 forKey:@"HBPictureVersion"]; encodeInt(_width); encodeInt(_height); encodeBool(_keepDisplayAspect); encodeInt(_anamorphicMode); encodeInt(_modulus); encodeInt(_displayWidth); encodeInt(_parWidth); encodeInt(_parHeight); encodeBool(_autocrop); encodeInt(_cropTop); encodeInt(_cropBottom); encodeInt(_cropLeft); encodeInt(_cropRight); encodeInt(_autoCropTop); encodeInt(_autoCropBottom); encodeInt(_autoCropLeft); encodeInt(_autoCropRight); encodeInt(_sourceWidth); encodeInt(_sourceHeight); encodeInt(_sourceParNum); encodeInt(_sourceParDen); } - (instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; decodeInt(_width); if (_width < 0) { goto fail; } decodeInt(_height); if (_height < 0) { goto fail; } decodeBool(_keepDisplayAspect); decodeInt(_anamorphicMode); if (_anamorphicMode < HBPictureAnarmophicModeNone || _anamorphicMode > HBPictureAnarmophicModeAuto) { goto fail; } decodeInt(_modulus); if (_modulus < 2 || _modulus > 16) { goto fail; } decodeInt(_displayWidth); if (_displayWidth < 0) { goto fail; } decodeInt(_parWidth); if (_parWidth < 0) { goto fail; } decodeInt(_parHeight); if (_parHeight < 0) { goto fail; } decodeBool(_autocrop); decodeInt(_cropTop); if (_cropTop < 0) { goto fail; } decodeInt(_cropBottom); if (_cropBottom < 0) { goto fail; } decodeInt(_cropLeft); if (_cropLeft < 0) { goto fail; } decodeInt(_cropRight); if (_cropRight < 0) { goto fail; } decodeInt(_autoCropTop); if (_autoCropTop < 0) { goto fail; } decodeInt(_autoCropBottom); if (_autoCropBottom < 0) { goto fail; } decodeInt(_autoCropLeft); if (_autoCropLeft < 0) { goto fail; } decodeInt(_autoCropRight); if (_autoCropRight < 0) { goto fail; } decodeInt(_sourceWidth); if (_sourceWidth < 0) { goto fail; } decodeInt(_sourceHeight); if (_sourceHeight < 0) { goto fail; } decodeInt(_sourceParNum); if (_sourceParNum < 0) { goto fail; } decodeInt(_sourceParDen); if (_sourceParDen < 0) { goto fail; } _notificationsEnabled = YES; return self; fail: return nil; } #pragma mark - Presets - (void)writeToPreset:(HBMutablePreset *)preset { preset[@"PictureKeepRatio"] = @(self.keepDisplayAspect); preset[@"PictureModulus"] = @(self.modulus); switch (self.anamorphicMode) { case HB_ANAMORPHIC_NONE: preset[@"PicturePAR"] = @"off"; break; case HB_ANAMORPHIC_LOOSE: preset[@"PicturePAR"] = @"loose"; break; case HB_ANAMORPHIC_AUTO: preset[@"PicturePAR"] = @"auto"; break; case HB_ANAMORPHIC_CUSTOM: preset[@"PicturePAR"] = @"custom"; break; default: preset[@"PicturePAR"] = @"loose"; break; } // PAR preset[@"PicturePARWidth"] = @(self.parWidth); preset[@"PicturePARHeight"] = @(self.parHeight); // Set crop settings preset[@"PictureAutoCrop"] = @(self.autocrop); preset[@"PictureTopCrop"] = @(self.cropTop); preset[@"PictureBottomCrop"] = @(self.cropBottom); preset[@"PictureLeftCrop"] = @(self.cropLeft); preset[@"PictureRightCrop"] = @(self.cropRight); } - (void)applyPreset:(HBPreset *)preset jobSettings:(NSDictionary *)settings { NSDictionary *par = settings[@"PAR"]; NSDictionary *filterList = settings[@"Filters"][@"FilterList"]; NSDictionary *cropScale = nil; for (NSDictionary *dict in filterList) { if ([dict[@"ID"] intValue] == HB_FILTER_CROP_SCALE) { cropScale = dict[@"Settings"]; } } self.validating = YES; self.notificationsEnabled = NO; // Check to see if UsesPictureSettings is greater than 0, as 0 means use picture sizing "None" // (2 is use max for source and 1 is use exact size when the preset was created) and the // preset completely ignores any picture sizing values in the preset. if (cropScale && [preset[@"UsesPictureSettings"] intValue]) { // If Cropping is set to custom, then recall all four crop values from // when the preset was created and apply them if ([preset[@"PictureAutoCrop"] boolValue]) { self.autocrop = YES; // Here we use the auto crop values determined right after scan self.cropTop = [cropScale[@"crop-top"] intValue]; self.cropBottom = [cropScale[@"crop-bottom"] intValue]; self.cropLeft = [cropScale[@"crop-left"] intValue]; self.cropRight = [cropScale[@"crop-right"] intValue]; } else { self.autocrop = NO; // Here we use the custom crop values saved at the time the preset was saved self.cropTop = [preset[@"PictureTopCrop"] intValue]; self.cropBottom = [preset[@"PictureBottomCrop"] intValue]; self.cropLeft = [preset[@"PictureLeftCrop"] intValue]; self.cropRight = [preset[@"PictureRightCrop"] intValue]; } // Set modulus if (preset[@"PictureModulus"]) { self.modulus = [preset[@"PictureModulus"] intValue]; } else { self.modulus = 16; } if (self.modulus <= 0 || self.modulus > 16) { self.modulus = 2; } // Assume max picture settings initially. self.keepDisplayAspect = [preset[@"PictureKeepRatio"] boolValue]; if ([preset[@"PicturePAR"] isEqualToString:@"off"]) { self.anamorphicMode = HB_ANAMORPHIC_NONE; } else if ([preset[@"PicturePAR"] isEqualToString:@"auto"]) { self.anamorphicMode = HB_ANAMORPHIC_AUTO; } else if ([preset[@"PicturePAR"] isEqualToString:@"custom"]) { self.anamorphicMode = HB_ANAMORPHIC_CUSTOM; } else { self.anamorphicMode = HB_ANAMORPHIC_LOOSE; } self.parWidth = [par[@"Num"] intValue]; self.parHeight = [par[@"Den"] intValue]; self.width = [cropScale[@"width"] intValue]; self.height = [cropScale[@"height"] intValue]; self.displayWidth = self.width * self.parWidth / self.parHeight; } self.validating = NO; self.notificationsEnabled = YES; [self postChangedNotification]; } @end