/* 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" #include "hb.h" NSString * const HBPictureChangedNotification = @"HBPictureChangedNotification"; @interface HBPicture () @property (nonatomic, readwrite, getter=isValidating) BOOL validating; @property (nonatomic, readwrite) int keep; @property (nonatomic, readwrite) BOOL darUpdated; @end @implementation HBPicture - (instancetype)init { self = [super init]; if (self) { // Set some values if we ever need a fake instance _width = 1280; _height = 720; _anamorphicMode = HB_ANAMORPHIC_NONE; _parWidth = 1; _parHeight = 1; } return self; } - (instancetype)initWithTitle:(HBTitle *)title { self = [self init]; if (self) { _title = [title retain]; _width = title.hb_title->geometry.width; _height = title.hb_title->geometry.height; [self validateSettings]; } return self; } - (void)postChangedNotification { [[NSNotificationCenter defaultCenter] postNotification: [NSNotification notificationWithName:HBPictureChangedNotification object:self userInfo:nil]]; } - (void)setWidth:(int)width { _width = width; if (!self.isValidating) { self.keep |= HB_KEEP_WIDTH; [self validateSettings]; } } - (BOOL)validateWidth:(id *)ioValue error:(NSError *)outError { BOOL retval = YES; if (nil != *ioValue) { int value = [*ioValue intValue]; if (value >= self.maxWidth) { *ioValue = @(self.maxWidth); } else if (value <= 32) { *ioValue = @32; } } return retval; } - (void)setHeight:(int)height { _height = height; if (!self.isValidating) { self.keep |= HB_KEEP_HEIGHT; [self validateSettings]; } } - (BOOL)validateHeight:(id *)ioValue error:(NSError *)outError { BOOL retval = YES; if (nil != *ioValue) { int value = [*ioValue intValue]; if (value >= self.maxHeight) { *ioValue = @(self.maxHeight); } else if (value <= 32) { *ioValue = @32; } } return retval; } - (void)setDisplayWidth:(int)displayWidth { _displayWidth = displayWidth; if (!self.isValidating) { self.darUpdated = YES; [self validateSettings]; } } - (void)setParWidth:(int)parWidth { _parWidth = parWidth; if (!self.isValidating) { [self validateSettings]; } } - (void)setParHeight:(int)parHeight { _parHeight = parHeight; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropTop:(int)cropTop { _cropTop = cropTop; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropBottom:(int)cropBottom { _cropBottom = cropBottom; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropLeft:(int)cropLeft { _cropLeft = cropLeft; if (!self.isValidating) { [self validateSettings]; } } - (void)setCropRight:(int)cropRight { _cropRight = cropRight; if (!self.isValidating) { [self validateSettings]; } } - (BOOL)validateCropTop:(id *)ioValue error:(NSError *)outError { [self validateVCrop:ioValue]; return YES; } - (BOOL)validateCropBottom:(id *)ioValue error:(NSError *)outError { [self validateVCrop:ioValue]; return YES; } - (BOOL)validateCropLeft:(id *)ioValue error:(NSError *)outError { [self validateHCrop:ioValue]; return YES; } - (BOOL)validateCropRight:(id *)ioValue error:(NSError *)outError { [self validateHCrop:ioValue]; return YES; } - (void)validateHCrop:(NSNumber **)ioValue { if (nil != *ioValue) { int value = [*ioValue intValue]; if (value >= self.maxHorizontalCrop) { *ioValue = @(self.maxHorizontalCrop); } else if (value < 0) { *ioValue = @0; } } } - (void)validateVCrop:(NSNumber **)ioValue { if (nil != *ioValue) { int value = [*ioValue intValue]; if (value >= self.maxVerticalCrop) { *ioValue = @(self.maxVerticalCrop); } else if (value < 0) { *ioValue = @0; } } } - (void)setAutocrop:(BOOL)autocrop { _autocrop = autocrop; if (self.title && autocrop && !self.isValidating) { hb_title_t *title = self.title.hb_title; self.validating = YES; // Reset the crop values to those determined right after scan self.cropTop = title->crop[0]; self.cropBottom = title->crop[1]; self.cropLeft = title->crop[2]; self.cropRight = title->crop[3]; self.validating = NO; [self validateSettings]; } } - (void)setAnamorphicMode:(int)anamorphicMode { _anamorphicMode = anamorphicMode; if (self.anamorphicMode == HB_ANAMORPHIC_STRICT || self.anamorphicMode == HB_ANAMORPHIC_LOOSE) { self.keepDisplayAspect = YES; } if (!self.isValidating) { [self validateSettings]; } } - (void)setKeepDisplayAspect:(int)keepDisplayAspect { _keepDisplayAspect = keepDisplayAspect; if (!self.isValidating) { [self validateSettings]; } } - (void)setModulus:(int)modulus { _modulus = modulus; if (!self.isValidating) { [self validateSettings]; } } #pragma mark - Max sizes - (int)maxWidth { if (self.title) return self.title.hb_title->geometry.width - self.cropRight - self.cropLeft; return 0; } - (int)maxHeight { if (self.title) return self.title.hb_title->geometry.height - self.cropTop - self.cropBottom; return 0; } - (int)maxVerticalCrop { if (self.title) return self.title.hb_title->geometry.height / 2 - 2; return 0; } - (int)maxHorizontalCrop { if (self.title) return self.title.hb_title->geometry.width / 2 - 2; return 0; } #pragma mark - Editable state - (BOOL)isWidthEditable { return (self.anamorphicMode != HB_ANAMORPHIC_STRICT) ? YES : NO; } - (BOOL)isHeightEditable { return (self.anamorphicMode != HB_ANAMORPHIC_STRICT) ? YES : NO; } - (BOOL)isKeepDisplayAspectEditable { if (self.anamorphicMode == HB_ANAMORPHIC_STRICT || self.anamorphicMode == HB_ANAMORPHIC_LOOSE) { return NO; } else { return YES; } } - (BOOL)isCustomAnamorphicEnabled { return self.anamorphicMode == HB_ANAMORPHIC_CUSTOM; } + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *retval = nil; // Tell KVO to reload the editable state. if ([key isEqualToString:@"keepDisplayAspectEditable"] || [key isEqualToString:@"heightEditable"] || [key isEqualToString:@"widthEditable"] || [key isEqualToString:@"customAnamorphicEnabled"]) { retval = [NSSet setWithObjects:@"anamorphicMode", nil]; } if ([key isEqualToString:@"maxWidth"] || [key isEqualToString:@"maxHeight"]) { retval = [NSSet setWithObjects:@"cropTop", @"cropBottom", @"cropLeft", @"cropRight", nil]; } if ([key isEqualToString:@"info"]) { retval = [NSSet setWithObjects:@"width", @"height",@"anamorphicMode", @"cropTop", @"cropBottom", @"cropLeft", @"cropRight", nil]; } return retval; } - (NSString *)info { if (!self.title) { return @""; } NSString *sizeInfo; hb_title_t *title = self.title.hb_title; if (self.anamorphicMode == HB_ANAMORPHIC_STRICT) // Original PAR Implementation { sizeInfo = [NSString stringWithFormat: @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Strict", title->geometry.width, title->geometry.height, self.width, self.height, self.displayWidth, self.height]; } else if (self.anamorphicMode == HB_ANAMORPHIC_LOOSE) // Loose Anamorphic { sizeInfo = [NSString stringWithFormat: @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Loose", title->geometry.width, title->geometry.height, self.width, self.height, self.displayWidth, self.height]; } else if (self.anamorphicMode == HB_ANAMORPHIC_CUSTOM) // Custom Anamorphic { sizeInfo = [NSString stringWithFormat: @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Custom", title->geometry.width, title->geometry.height, self.width, self.height, self.displayWidth, self.height]; } else // No Anamorphic { sizeInfo = [NSString stringWithFormat: @"Source: %dx%d, Output: %dx%d", title->geometry.width, title->geometry.height, self.width, self.height]; } return sizeInfo; } #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; hb_title_t *title = self.title.hb_title; self.keep |= self.keepDisplayAspect * HB_KEEP_DISPLAY_ASPECT; hb_geometry_t srcGeo, resultGeo; hb_geometry_settings_t uiGeo; srcGeo.width = title->geometry.width; srcGeo.height = title->geometry.height; srcGeo.par = title->geometry.par; uiGeo.mode = 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 = title->geometry.width - crop[2] - crop[3] + self.modulus - 1; uiGeo.maxHeight = title->geometry.height - 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]; } - (void)preparePictureForPreset:(NSMutableDictionary *)preset { preset[@"PictureKeepRatio"] = @(self.keepDisplayAspect); preset[@"PicturePAR"] = @(self.anamorphicMode); preset[@"PictureModulus"] = @(self.modulus); // Set crop settings preset[@"PictureAutoCrop"] = @(self.autocrop); preset[@"PictureTopCrop"] = @(self.cropTop); preset[@"PictureBottomCrop"] = @(self.cropBottom); preset[@"PictureLeftCrop"] = @(self.cropLeft); preset[@"PictureRightCrop"] = @(self.cropRight); } - (void)applySettingsFromPreset:(NSDictionary *)preset { self.validating = YES; hb_title_t *title = self.title.hb_title; /* Note: objectForKey:@"UsesPictureSettings" refers to picture size, which encompasses: * height, width, keep ar, anamorphic and crop settings. * picture filters are handled separately below. */ int maxWidth = title->geometry.width - self.cropLeft - self.cropRight; int maxHeight = title->geometry.height - self.cropTop - self.cropBottom; int jobMaxWidth = 0, jobMaxHeight = 0; /* Check to see if the objectForKey:@"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 ([preset[@"UsesPictureSettings"] intValue] > 0) { // If Cropping is set to custom, then recall all four crop values from // when the preset was created and apply them if ([preset[@"PictureAutoCrop"] intValue] == 0) { 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]; } else // if auto crop has been saved in preset, set to auto and use post scan auto crop { self.autocrop = YES; /* Here we use the auto crop values determined right after scan */ self.cropTop = title->crop[0]; self.cropBottom = title->crop[1]; self.cropLeft = title->crop[2]; self.cropRight = title->crop[3]; } // crop may have changed, reset maxWidth/maxHeight maxWidth = title->geometry.width - self.cropLeft - self.cropRight; maxHeight = title->geometry.height - self.cropTop - self.cropBottom; // Set modulus if (preset[@"PictureModulus"]) { self.modulus = [preset[@"PictureModulus"] intValue]; } else { self.modulus = 16; } // Assume max picture settings initially. self.keepDisplayAspect = [preset[@"PictureKeepRatio"] intValue]; self.anamorphicMode = [preset[@"PicturePAR"] intValue]; self.width = title->geometry.width - self.cropLeft - self.cropRight; self.height = title->geometry.height - self.cropTop - self.cropBottom; // Check to see if the objectForKey:@"UsesPictureSettings" is 2, // which means "Use max. picture size for source" // If not 2 it must be 1 here which means "Use the picture // size specified in the preset" if ([preset[@"UsesPictureSettings"] intValue] != 2 && [preset[@"UsesMaxPictureSettings"] intValue] != 1) { // if the preset specifies neither max. width nor height // (both are 0), use the max. picture size // // if the specified non-zero dimensions exceed those of the // source, also use the max. picture size (no upscaling) if ([preset[@"PictureWidth"] intValue] > 0) { jobMaxWidth = [preset[@"PictureWidth"] intValue]; } if ([preset[@"PictureHeight"] intValue] > 0) { jobMaxHeight = [preset[@"PictureHeight"] intValue]; } } } // Modulus added to maxWidth/maxHeight to allow a small amount of // upscaling to the next mod boundary. This does not apply to // explicit limits set for device compatibility. It only applies // when limiting to cropped title dimensions. maxWidth += self.modulus - 1; maxHeight += self.modulus - 1; if (jobMaxWidth == 0 || jobMaxWidth > maxWidth) jobMaxWidth = maxWidth; if (jobMaxHeight == 0 || jobMaxHeight > maxHeight) jobMaxHeight = maxHeight; hb_geometry_t srcGeo, resultGeo; hb_geometry_settings_t uiGeo; srcGeo.width = title->geometry.width; srcGeo.height = title->geometry.height; srcGeo.par = title->geometry.par; uiGeo.mode = self.anamorphicMode; uiGeo.keep = self.keepDisplayAspect * HB_KEEP_DISPLAY_ASPECT; 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; hb_rational_t par = {self.parWidth, self.parHeight}; uiGeo.geometry.par = par; uiGeo.maxWidth = jobMaxWidth; uiGeo.maxHeight = jobMaxHeight; 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; } - (void)preparePictureForQueueFileJob:(NSMutableDictionary *)queueFileJob { queueFileJob[@"PictureWidth"] = @(self.width); queueFileJob[@"PictureHeight"] = @(self.height); queueFileJob[@"PictureKeepRatio"] = @(self.keepDisplayAspect); queueFileJob[@"PicturePAR"] = @(self.anamorphicMode); queueFileJob[@"PictureModulus"] = @(self.modulus); queueFileJob[@"PicturePARPixelWidth"] = @(self.parWidth); queueFileJob[@"PicturePARPixelHeight"] = @(self.parHeight); queueFileJob[@"PictureAutoCrop"] = @(self.autocrop); queueFileJob[@"PictureTopCrop"] = @(self.cropTop); queueFileJob[@"PictureBottomCrop"] = @(self.cropBottom); queueFileJob[@"PictureLeftCrop"] = @(self.cropLeft); queueFileJob[@"PictureRightCrop"] = @(self.cropRight); } - (void)applyPictureSettingsFromQueue:(NSDictionary *)queueToApply { self.validating = YES; /* If Cropping is set to custom, then recall all four crop values from when the preset was created and apply them */ if ([queueToApply[@"PictureAutoCrop"] intValue] == 0) { self.autocrop = NO; /* Here we use the custom crop values saved at the time the preset was saved */ self.cropTop = [queueToApply[@"PictureTopCrop"] intValue]; self.cropBottom = [queueToApply[@"PictureBottomCrop"] intValue]; self.cropLeft = [queueToApply[@"PictureLeftCrop"] intValue]; self.cropRight = [queueToApply[@"PictureRightCrop"] intValue]; } else /* if auto crop has been saved in preset, set to auto and use post scan auto crop */ { self.autocrop = YES; hb_title_t *title = self.title.hb_title; /* Here we use the auto crop values determined right after scan */ self.cropTop = title->crop[0]; self.cropBottom = title->crop[1]; self.cropLeft = title->crop[2]; self.cropRight = title->crop[3]; } self.anamorphicMode = [[queueToApply objectForKey:@"PicturePAR"] intValue]; self.modulus = [[queueToApply objectForKey:@"PictureModulus"] intValue]; self.keepDisplayAspect = [[queueToApply objectForKey:@"PictureKeepRatio"] intValue]; self.width = [[queueToApply objectForKey:@"PictureWidth"] intValue]; self.height = [[queueToApply objectForKey:@"PictureHeight"] intValue]; self.validating = NO; [self validateSettings]; } @end