/* HBSubtitles.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 "HBSubtitles.h" #import "HBSubtitlesDefaults.h" #import "HBSubtitlesTrack.h" #import "HBJob.h" #import "HBJob+HBJobConversion.h" #import "HBTitle.h" #import "HBCodingUtilities.h" #import "HBUtilities.h" #import "HBJob+Private.h" #import "HBSecurityAccessToken.h" #include "common.h" extern NSString *keySubTrackName; extern NSString *keySubTrackLanguageIsoCode; extern NSString *keySubTrackType; extern NSString *keySubTrackSrtFileURL; extern NSString *keySubTrackSrtFileURLBookmark; #define NONE_TRACK_INDEX 0 #define FOREIGN_TRACK_INDEX 1 @interface HBSubtitles () @property (nonatomic, readwrite) NSArray *sourceTracks; @property (nonatomic, readonly) NSMutableArray *tokens; @property (nonatomic, readwrite) NSInteger *accessCount; @property (nonatomic, readwrite, weak) HBJob *job; @property (nonatomic, readwrite) int container; /// Used to aovid circular dependecy validation. @property (nonatomic, readwrite) BOOL validating; @end @implementation HBSubtitles - (instancetype)initWithJob:(HBJob *)job { self = [super init]; if (self) { _job = job; _container = HB_MUX_MP4; _tracks = [[NSMutableArray alloc] init]; _defaults = [[HBSubtitlesDefaults alloc] init]; _tokens = [NSMutableArray array]; NSMutableArray *sourceTracks = [job.title.subtitlesTracks mutableCopy]; NSMutableSet *forcedSourceNamesArray = [NSMutableSet set]; int foreignAudioType = VOBSUB; for (NSDictionary *dict in _sourceTracks) { enum subsource source = [dict[keySubTrackType] intValue]; NSString *name = @(hb_subsource_name(source)); // if the subtitle track can be forced, add its source name to the array if (hb_subtitle_can_force(source) && name.length) { [forcedSourceNamesArray addObject:name]; } } // now set the name of the Foreign Audio Search track NSMutableString *foreignAudioSearchTrackName = [@"Foreign Audio Search (Bitmap)" mutableCopy]; if (forcedSourceNamesArray.count) { [foreignAudioSearchTrackName appendFormat:@" ("]; for (NSString *name in forcedSourceNamesArray) { [foreignAudioSearchTrackName appendFormat:@"%@, ", name]; } [foreignAudioSearchTrackName deleteCharactersInRange:NSMakeRange(foreignAudioSearchTrackName.length - 2, 2)]; [foreignAudioSearchTrackName appendFormat:@")"]; } // Add the none and foreign track to the source array NSDictionary *none = @{ keySubTrackName: NSLocalizedString(@"None", nil)}; [sourceTracks insertObject:none atIndex:0]; NSDictionary *foreign = @{ keySubTrackName: [foreignAudioSearchTrackName copy], keySubTrackType: @(foreignAudioType) }; [sourceTracks insertObject:foreign atIndex:1]; _sourceTracks = [sourceTracks copy]; } return self; } #pragma mark - Data Source - (NSDictionary *)sourceTrackAtIndex:(NSUInteger)idx; { return self.sourceTracks[idx]; } - (NSArray *)sourceTracksArray { NSMutableArray *sourceNames = [NSMutableArray array]; for (NSDictionary *track in self.sourceTracks) { [sourceNames addObject:track[keySubTrackName]]; } return sourceNames; } #pragma mark - Delegate - (void)track:(HBSubtitlesTrack *)track didChangeSourceFrom:(NSUInteger)oldSourceIdx; { // If the source was changed to None, remove the track if (track.sourceTrackIdx == NONE_TRACK_INDEX) { NSUInteger idx = [self.tracks indexOfObject:track]; [self removeObjectFromTracksAtIndex:idx]; } // If the source was changed to Foreign Audio Track, // insert it at top if it wasn't already there else if (track.sourceTrackIdx == FOREIGN_TRACK_INDEX) { NSUInteger idx = [self.tracks indexOfObject:track]; if (idx != 0) { [self removeObjectFromTracksAtIndex:idx]; if (self.tracks[0].sourceTrackIdx != FOREIGN_TRACK_INDEX) { [self insertObject:track inTracksAtIndex:0]; } } } // Else add a new None track if (oldSourceIdx == NONE_TRACK_INDEX) { [self addNoneTrack]; } [self validatePassthru]; } - (BOOL)canSetBurnedInOption:(HBSubtitlesTrack *)track { BOOL result = YES; for (HBSubtitlesTrack *subTrack in self.tracks) { if (subTrack != track && subTrack.isEnabled && subTrack.sourceTrackIdx > FOREIGN_TRACK_INDEX && !subTrack.canPassthru) { result = NO; } } return result; } - (void)didSetBurnedInOption:(HBSubtitlesTrack *)track { if (self.validating == NO && track.sourceTrackIdx != FOREIGN_TRACK_INDEX) { self.validating = YES; NSUInteger idx = [self.tracks indexOfObject:track]; [self validateBurned:idx]; self.validating = NO; } } - (void)didSetDefaultOption:(HBSubtitlesTrack *)track { if (self.validating == NO && track.sourceTrackIdx != FOREIGN_TRACK_INDEX) { self.validating = YES; NSUInteger idx = [self.tracks indexOfObject:track]; [self validateDefault:idx]; self.validating = NO; } } - (void)addNoneTrack { HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:NONE_TRACK_INDEX]; [self addTrack:track]; } #pragma mark - Public methods - (void)addAllTracks { [self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]]; // Add the remainings tracks for (NSUInteger idx = 1; idx < self.sourceTracksArray.count; idx++) { [self addTrack:[self trackFromSourceTrackIndex:idx]]; } [self addNoneTrack]; [self validatePassthru]; } - (void)removeAll { [self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]]; [self addNoneTrack]; } - (void)reloadDefaults { [self addDefaultTracksFromJobSettings:self.job.jobDict]; } - (void)addSrtTrackFromURL:(NSURL *)srtURL { #ifdef __SANDBOX_ENABLED__ // Create the security scoped bookmark NSData *bookmark = [HBUtilities bookmarkFromURL:srtURL options:NSURLBookmarkCreationWithSecurityScope | NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess]; #endif // Create a new entry for the subtitle source array so it shows up in our subtitle source list NSMutableArray *sourceTrack = [self.sourceTracks mutableCopy]; #ifdef __SANDBOX_ENABLED__ [sourceTrack addObject:@{keySubTrackName: srtURL.lastPathComponent, keySubTrackType: @(SRTSUB), keySubTrackSrtFileURL: srtURL, keySubTrackSrtFileURLBookmark: bookmark}]; #else [sourceTrack addObject:@{keySubTrackName: srtURL.lastPathComponent, keySubTrackType: @(SRTSUB), keySubTrackSrtFileURL: srtURL}]; #endif self.sourceTracks = [sourceTrack copy]; HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:self.sourceTracksArray.count - 1]; [self insertObject:track inTracksAtIndex:[self countOfTracks] - 1]; } - (void)setContainer:(int)container { _container = container; for (HBSubtitlesTrack *track in self.tracks) { track.container = container; } if (!(self.undo.isUndoing || self.undo.isRedoing)) { [self validatePassthru]; } } - (void)setUndo:(NSUndoManager *)undo { _undo = undo; for (HBSubtitlesTrack *track in self.tracks) { track.undo = undo; } self.defaults.undo = undo; } - (void)setDefaults:(HBSubtitlesDefaults *)defaults { if (defaults != _defaults) { [[self.undo prepareWithInvocationTarget:self] setDefaults:_defaults]; } _defaults = defaults; _defaults.undo = self.undo; } /** * Convenience method to add a track to subtitlesArray. * * @param track the track to add. */ - (void)addTrack:(HBSubtitlesTrack *)newTrack { [self insertObject:newTrack inTracksAtIndex:[self countOfTracks]]; } /** * Creates a new track dictionary from a source track. * * @param index the index of the source track in the subtitlesSourceArray */ - (HBSubtitlesTrack *)trackFromSourceTrackIndex:(NSInteger)index { HBSubtitlesTrack *track = [[HBSubtitlesTrack alloc] initWithTrackIdx:index container:self.container dataSource:self delegate:self]; track.undo = self.undo; return track; } #pragma mark - Defaults - (void)addDefaultTracksFromJobSettings:(NSDictionary *)settings { NSMutableArray *tracks = [NSMutableArray array]; NSArray *> *settingsTracks = settings[@"Subtitle"][@"SubtitleList"]; NSDictionary *search = settings[@"Subtitle"][@"Search"]; // Reinitialize the configured list of audio tracks [self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]]; // Add the foreign audio search pass if ([search[@"Enable"] boolValue]) { HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:FOREIGN_TRACK_INDEX]; track.burnedIn = [search[@"Burn"] boolValue]; track.forcedOnly = [search[@"Forced"] boolValue]; track.def = [search[@"Default"] boolValue]; [tracks addObject:track]; } // Add the tracks for (NSDictionary *trackDict in settingsTracks) { HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:[trackDict[@"Track"] unsignedIntegerValue] + 2]; track.burnedIn = [trackDict[@"Burn"] boolValue]; track.forcedOnly = [trackDict[@"Forced"] boolValue]; [tracks addObject:track]; } [self insertTracks:tracks atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, tracks.count)]]; // Add an None item [self addNoneTrack]; } #pragma mark - Validation /** * Checks whether any subtitles in the list cannot be passed through. * Set the first of any such subtitles to burned-in, remove the others. */ - (void)validatePassthru { BOOL convertToBurnInUsed = NO; NSMutableIndexSet *tracksToDelete = [[NSMutableIndexSet alloc] init]; // convert any non-None incompatible tracks to burn-in or remove them NSUInteger idx = 0; for (HBSubtitlesTrack *track in self.tracks) { if (track.isEnabled && track.sourceTrackIdx > FOREIGN_TRACK_INDEX && !track.canPassthru) { if (convertToBurnInUsed == NO) { //we haven't set any track to burned-in yet, so we can track.burnedIn = YES; convertToBurnInUsed = YES; //remove any additional tracks } else { //we already have a burned-in track, we must remove others [tracksToDelete addIndex:idx]; } } idx++; } //if we converted a track to burned-in, unset it for tracks that support passthru if (convertToBurnInUsed == YES) { for (HBSubtitlesTrack *track in self.tracks) { if (track.isEnabled && track.sourceTrackIdx > FOREIGN_TRACK_INDEX && track.canPassthru) { track.burnedIn = NO; } } } // Delete the tracks NSUInteger currentIndex = [tracksToDelete lastIndex]; while (currentIndex != NSNotFound) { [self removeObjectFromTracksAtIndex:currentIndex]; currentIndex = [tracksToDelete indexLessThanIndex:currentIndex]; } } - (void)validateBurned:(NSUInteger)index { [self.tracks enumerateObjectsUsingBlock:^(HBSubtitlesTrack *track, NSUInteger idx, BOOL *stop) { if (idx != index && track.sourceTrackIdx != FOREIGN_TRACK_INDEX) { track.burnedIn = NO; } }]; } - (void)validateDefault:(NSUInteger)index { [self.tracks enumerateObjectsUsingBlock:^(HBSubtitlesTrack *obj, NSUInteger idx, BOOL *stop) { if (idx != index && obj.sourceTrackIdx != FOREIGN_TRACK_INDEX) { obj.def = NO; } }]; } - (BOOL)startAccessingSecurityScopedResource { #ifdef __SANDBOX_ENABLED__ if (self.accessCount == 0) { for (NSDictionary *sourceTrack in self.sourceTracks) { if (sourceTrack[keySubTrackSrtFileURLBookmark]) { [self.tokens addObject:[HBSecurityAccessToken tokenWithObject:sourceTrack[keySubTrackSrtFileURL]]]; } } } self.accessCount += 1; return YES; #else return NO; #endif } - (void)stopAccessingSecurityScopedResource { #ifdef __SANDBOX_ENABLED__ self.accessCount -= 1; NSAssert(self.accessCount >= 0, @"[HBSubtitles stopAccessingSecurityScopedResource:] unbalanced call"); if (self.accessCount == 0) { [self.tokens removeAllObjects]; } #endif } #pragma mark - NSCopying - (instancetype)copyWithZone:(NSZone *)zone { HBSubtitles *copy = [[[self class] alloc] init]; if (copy) { copy->_container = _container; copy->_sourceTracks = [_sourceTracks copy]; copy->_tracks = [[NSMutableArray alloc] init]; for (HBSubtitlesTrack *track in _tracks) { HBSubtitlesTrack *trackCopy = [track copy]; [copy->_tracks addObject:trackCopy]; trackCopy.dataSource = copy; trackCopy.delegate = copy; } copy->_defaults = [_defaults copy]; copy->_tokens = [NSMutableArray array]; } return copy; } #pragma mark - NSCoding + (BOOL)supportsSecureCoding { return YES; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeInt:1 forKey:@"HBSubtitlesVersion"]; encodeInt(_container); encodeObject(_sourceTracks); encodeObject(_tracks); encodeObject(_defaults); } - (instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; _tokens = [NSMutableArray array]; decodeInt(_container); decodeCollectionOfObjects3(_sourceTracks, NSArray, NSDictionary, NSURL, NSData); #ifdef __SANDBOX_ENABLED__ NSMutableArray *sourceTracks = [_sourceTracks mutableCopy]; for (NSDictionary *sourceTrack in _sourceTracks) { if (sourceTrack[keySubTrackSrtFileURLBookmark]) { NSMutableDictionary *copy = [sourceTrack mutableCopy]; NSURL *srtURL = [HBUtilities URLFromBookmark:sourceTrack[keySubTrackSrtFileURLBookmark]]; if (srtURL) { copy[keySubTrackSrtFileURL] = srtURL; } [sourceTracks addObject:copy]; } else { [sourceTracks addObject:sourceTrack]; } } _sourceTracks = [sourceTracks copy]; #endif decodeCollectionOfObjects(_tracks, NSMutableArray, HBSubtitlesTrack); for (HBSubtitlesTrack *track in _tracks) { track.dataSource = self; track.delegate = self; } decodeObject(_defaults, HBSubtitlesDefaults); return self; } #pragma mark - Presets - (void)writeToPreset:(HBMutablePreset *)preset { [self.defaults writeToPreset:preset]; } - (void)applyPreset:(HBPreset *)preset jobSettings:(NSDictionary *)settings { [self.defaults applyPreset:preset jobSettings:settings]; [self addDefaultTracksFromJobSettings:settings]; } #pragma mark - #pragma mark KVC - (NSUInteger)countOfTracks { return self.tracks.count; } - (HBSubtitlesTrack *)objectInTracksAtIndex:(NSUInteger)index { return self.tracks[index]; } - (void)insertObject:(HBSubtitlesTrack *)track inTracksAtIndex:(NSUInteger)index; { [[self.undo prepareWithInvocationTarget:self] removeObjectFromTracksAtIndex:index]; [self.tracks insertObject:track atIndex:index]; } - (void)insertTracks:(NSArray *)array atIndexes:(NSIndexSet *)indexes { [[self.undo prepareWithInvocationTarget:self] removeTracksAtIndexes:indexes]; [self.tracks insertObjects:array atIndexes:indexes]; } - (void)removeObjectFromTracksAtIndex:(NSUInteger)index { HBSubtitlesTrack *track = self.tracks[index]; [[self.undo prepareWithInvocationTarget:self] insertObject:track inTracksAtIndex:index]; [self.tracks removeObjectAtIndex:index]; } - (void)removeTracksAtIndexes:(NSIndexSet *)indexes { NSArray *tracks = [self.tracks objectsAtIndexes:indexes]; [[self.undo prepareWithInvocationTarget:self] insertTracks:tracks atIndexes:indexes]; [self.tracks removeObjectsAtIndexes:indexes]; } @end