Dr0ptp4kt has submitted this change and it was merged. Change subject: Move lead image inflation/face detection to background thread. ......................................................................
Move lead image inflation/face detection to background thread. Listens in on URLCache notification that the web view retrieved an image and sees if this image is a variant of the lead image. Shows this image if the variant is larger than the one already being shown. Fixed performance issue in data layer related to seeing if an image binary exists. Improved fileSizePrefix error handling. Fixed bug in imageSizeVariants sorting. Added c methods for unit rectangle conversion. Face detection more efficient - since face rect is stored as unit rectangle we don't have to re-detect the face if we intercept a higher res variant of lead image. Change-Id: I979f3a07c7d37febd5b7d9f99a8105223f2525b3 --- M MediaWikiKit/MediaWikiKit/MWKDataStore.m M MediaWikiKit/MediaWikiKit/MWKImage.h M MediaWikiKit/MediaWikiKit/MWKImage.m M MediaWikiKit/MediaWikiKit/MWKImageList.m M Wikipedia.xcodeproj/project.pbxproj A wikipedia/C Methods/WMFGeometry.c A wikipedia/C Methods/WMFGeometry.h A wikipedia/Categories/UIImage+WMFFocalImageDrawing.h A wikipedia/Categories/UIImage+WMFFocalImageDrawing.m A wikipedia/Custom Objects/WMFFaceDetector.h A wikipedia/Custom Objects/WMFFaceDetector.m R wikipedia/Custom Views/MenuButton.h R wikipedia/Custom Views/MenuButton.m R wikipedia/Custom Views/MenuLabel.h R wikipedia/Custom Views/MenuLabel.m R wikipedia/Custom Views/PaddedLabel.h R wikipedia/Custom Views/PaddedLabel.m R wikipedia/Custom Views/TabularScrollView.h R wikipedia/Custom Views/TabularScrollView.m R wikipedia/Custom Views/WMFCenteredPathView.h R wikipedia/Custom Views/WMFCenteredPathView.m R wikipedia/Custom Views/WikiGlyphButton.h R wikipedia/Custom Views/WikiGlyphButton.m R wikipedia/Custom Views/WikiGlyphLabel.h R wikipedia/Custom Views/WikiGlyphLabel.m M wikipedia/Networking/Fetchers/SavedArticlesFetcher.h M wikipedia/Networking/Fetchers/SavedArticlesFetcher.m M wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m D wikipedia/View Controllers/LeadImage/FocalImage.h D wikipedia/View Controllers/LeadImage/FocalImage.m M wikipedia/View Controllers/LeadImage/LeadImageContainer.m M wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m M wikipedia/View Controllers/SavedPages/SavedPagesViewController.m M wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.h M wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.m M wikipedia/View Controllers/ShareCard/WMFShareCardViewController.m M wikipedia/Web Image Interception/URLCache.h M wikipedia/Web Image Interception/URLCache.m M wikipedia/en.lproj/Main_iPhone.strings 39 files changed, 722 insertions(+), 412 deletions(-) Approvals: Dr0ptp4kt: Looks good to me, approved Fjalapeno: Looks good to me, but someone else must approve diff --git a/MediaWikiKit/MediaWikiKit/MWKDataStore.m b/MediaWikiKit/MediaWikiKit/MWKDataStore.m index 86b7f15..2bf60cc 100644 --- a/MediaWikiKit/MediaWikiKit/MWKDataStore.m +++ b/MediaWikiKit/MediaWikiKit/MWKDataStore.m @@ -312,9 +312,7 @@ NSLog(@"nil image passed to imageDataWithImage"); return nil; } - NSString* path = [self pathForImage:image]; - NSString* fileName = [@"Image" stringByAppendingPathExtension:image.extension]; - NSString* filePath = [path stringByAppendingPathComponent:fileName]; + NSString* filePath = [image fullImageBinaryPath]; NSError* err; NSData* data = [NSData dataWithContentsOfFile:filePath options:0 error:&err]; diff --git a/MediaWikiKit/MediaWikiKit/MWKImage.h b/MediaWikiKit/MediaWikiKit/MWKImage.h index 0f6603b..c31ae02 100644 --- a/MediaWikiKit/MediaWikiKit/MWKImage.h +++ b/MediaWikiKit/MediaWikiKit/MWKImage.h @@ -48,8 +48,10 @@ - (void)save; - (UIImage*)asUIImage; +- (NSData*) asNSData; - (MWKImage*)largestVariant; +- (MWKImage*)largestCachedVariant; /// Return the folder containing the image file from receiver's @c sourceURL. - (NSString*)basename; @@ -77,7 +79,7 @@ /// The name of the image "file" associatd with the receiver, with percent encodings replaced. + (NSString*)canonicalFilenameFromSourceURL:(NSString*)sourceURL; -+ (int)fileSizePrefix:(NSString*)sourceURL; ++ (NSInteger)fileSizePrefix:(NSString*)sourceURL; /** * Checks if two images are variants of each other <b>but not exactly the same image</b>. @@ -93,4 +95,6 @@ */ - (BOOL)isVariantOfImage:(MWKImage*)otherImage; +- (NSString*)fullImageBinaryPath; + @end diff --git a/MediaWikiKit/MediaWikiKit/MWKImage.m b/MediaWikiKit/MediaWikiKit/MWKImage.m index ca55ba8..251bbfb 100644 --- a/MediaWikiKit/MediaWikiKit/MWKImage.m +++ b/MediaWikiKit/MediaWikiKit/MWKImage.m @@ -74,11 +74,15 @@ return [[self fileNameNoSizePrefix:sourceURL] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; } -+ (int)fileSizePrefix:(NSString*)sourceURL { - NSString* fileName = [sourceURL lastPathComponent]; - NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:@"^(\\d+)px-" options:0 error:nil]; ++ (NSInteger)fileSizePrefix:(NSString*)sourceURL { + NSString* fileName = [sourceURL lastPathComponent]; + if (!fileName) { + return -1; + } + NSError* error = nil; + NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:@"^(\\d+)px-" options:0 error:&error]; NSArray* matches = [re matchesInString:fileName options:0 range:NSMakeRange(0, [fileName length])]; - if ([matches count]) { + if (!error && [matches count]) { return [[fileName substringWithRange:[matches[0] rangeAtIndex:0]] intValue]; } else { return -1; @@ -159,15 +163,23 @@ return [UIImage imageWithData:imageData scale:1.0]; } +- (NSData*)asNSData { + return [self.article.dataStore imageDataWithImage:self]; +} + - (MWKImage*)largestVariant { NSString* largestURL = [self.article.images largestImageVariant:self.sourceURL]; return [self.article imageWithURL:largestURL]; } +- (MWKImage*)largestCachedVariant { + return [self.article.images largestImageVariantForURL:self.sourceURL cachedOnly:YES]; +} + - (BOOL)isCached { - // @fixme maybe make this more efficient - NSData* data = [self.article.dataStore imageDataWithImage:self]; - return (data != nil); + NSString* fullPath = [self fullImageBinaryPath]; + BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath]; + return fileExists; } - (BOOL)isEqual:(id)object { @@ -199,4 +211,11 @@ [super description], self.article.title, self.sourceURL]; } +- (NSString*)fullImageBinaryPath { + NSString* path = [self.article.dataStore pathForImage:self]; + NSString* fileName = [@"Image" stringByAppendingPathExtension:self.extension]; + NSString* filePath = [path stringByAppendingPathComponent:fileName]; + return filePath; +} + @end diff --git a/MediaWikiKit/MediaWikiKit/MWKImageList.m b/MediaWikiKit/MediaWikiKit/MWKImageList.m index fda21cd..3d27b02 100644 --- a/MediaWikiKit/MediaWikiKit/MWKImageList.m +++ b/MediaWikiKit/MediaWikiKit/MWKImageList.m @@ -94,8 +94,19 @@ if (arr) { NSMutableArray* arr2 = [NSMutableArray arrayWithArray:arr]; [arr2 sortUsingComparator:^NSComparisonResult (NSString* url1, NSString* url2) { - int width1 = [MWKImage fileSizePrefix:[url1 lastPathComponent]]; - int width2 = [MWKImage fileSizePrefix:[url2 lastPathComponent]]; + NSInteger width1 = [MWKImage fileSizePrefix:[url1 lastPathComponent]]; + NSInteger width2 = [MWKImage fileSizePrefix:[url2 lastPathComponent]]; + + // Canonical image won't have "fileSizePrefix" at beginning of file name. + // ie: "cat.jpg" is larger than "200px-cat.jpg". Set to NSIntegerMax in + // these cases so canonical will correctly sort as largest. + if (width1 == -1) { + width1 = NSIntegerMax; + } + if (width2 == -1) { + width2 = NSIntegerMax; + } + if (width1 > width2) { return NSOrderedDescending; } else if (width1 < width2) { diff --git a/Wikipedia.xcodeproj/project.pbxproj b/Wikipedia.xcodeproj/project.pbxproj index e869cbb..e35b6ba 100644 --- a/Wikipedia.xcodeproj/project.pbxproj +++ b/Wikipedia.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 040892641935ABBD004CF254 /* UIViewController+StatusBarHeight.m in Sources */ = {isa = PBXBuildFile; fileRef = 040892631935ABBD004CF254 /* UIViewController+StatusBarHeight.m */; }; 04090A33187F53E400577EDF /* clear.png in Resources */ = {isa = PBXBuildFile; fileRef = 04090A32187F53E400577EDF /* clear.png */; }; 04090A3B187FB7D000577EDF /* UIView+Debugging.m in Sources */ = {isa = PBXBuildFile; fileRef = 04090A3A187FB7D000577EDF /* UIView+Debugging.m */; }; + 040D83591AB0ECFD000896D5 /* WMFCenteredPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 040D83581AB0ECFD000896D5 /* WMFCenteredPathView.m */; }; + 040D835A1AB0ECFD000896D5 /* WMFCenteredPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 040D83581AB0ECFD000896D5 /* WMFCenteredPathView.m */; }; + 040D835E1AB0EE45000896D5 /* WMFGeometry.c in Sources */ = {isa = PBXBuildFile; fileRef = 040D835C1AB0EE45000896D5 /* WMFGeometry.c */; }; + 040D835F1AB0EE45000896D5 /* WMFGeometry.c in Sources */ = {isa = PBXBuildFile; fileRef = 040D835C1AB0EE45000896D5 /* WMFGeometry.c */; }; 0412362E189C29EA00E0CF8E /* abuse-filter-disallowed.png in Resources */ = {isa = PBXBuildFile; fileRef = 04123624189C29EA00E0CF8E /* abuse-filter-disallowed.png */; }; 04123630189C29EA00E0CF8E /* abuse-filter-disallo...@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 04123625189C29EA00E0CF8E /* abuse-filter-disallo...@2x.png */; }; 04123636189C29EA00E0CF8E /* abuse-filter-flag-white.png in Resources */ = {isa = PBXBuildFile; fileRef = 04123628189C29EA00E0CF8E /* abuse-filter-flag-white.png */; }; @@ -75,12 +79,10 @@ 04530AF81935C07500022512 /* ModalContentViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04530AF71935C07500022512 /* ModalContentViewController.m */; }; 04530AFB1935C2B500022512 /* EmptySegue.m in Sources */ = {isa = PBXBuildFile; fileRef = 04530AFA1935C2B500022512 /* EmptySegue.m */; }; 045374881A35834D00CE1A56 /* LeadImageTitleAttributedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 045374871A35834D00CE1A56 /* LeadImageTitleAttributedString.m */; }; - 045424461A429E8800F1DF2F /* FocalImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 045424451A429E8800F1DF2F /* FocalImage.m */; }; 045A9F0D18F6090E0057EA85 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 045A9F0C18F6090E0057EA85 /* assets */; }; 045D872119FAD2FA0035C1F9 /* AboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 045D872019FAD2FA0035C1F9 /* AboutViewController.m */; }; 045EFF1A19A25FEB00D0EDBB /* logo-placeholder-search.png in Resources */ = {isa = PBXBuildFile; fileRef = 045EFF1819A25FEB00D0EDBB /* logo-placeholder-search.png */; }; 045EFF1B19A25FEB00D0EDBB /* logo-placeholder-sea...@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 045EFF1919A25FEB00D0EDBB /* logo-placeholder-sea...@2x.png */; }; - 0460F8DC19B0F932001BC59B /* CenteredPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0460F8DB19B0F932001BC59B /* CenteredPathView.m */; }; 0462A6D11A1FE016009412D4 /* SearchResultAttributedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 0462A6D01A1FE016009412D4 /* SearchResultAttributedString.m */; }; 0463639818A844570049EE4F /* KeychainCredentials.m in Sources */ = {isa = PBXBuildFile; fileRef = 0463639718A844570049EE4F /* KeychainCredentials.m */; }; 0472BC18193AD88C00C40BDA /* MWKSection+DisplayHtml.m in Sources */ = {isa = PBXBuildFile; fileRef = 0472BC17193AD88C00C40BDA /* MWKSection+DisplayHtml.m */; }; @@ -125,7 +127,6 @@ 0487048E19F8262600B7D307 /* WikipediaZeroMessageFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 0487047919F8262600B7D307 /* WikipediaZeroMessageFetcher.m */; }; 0487048F19F8262600B7D307 /* WikiTextSectionFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 0487047B19F8262600B7D307 /* WikiTextSectionFetcher.m */; }; 0487049019F8262600B7D307 /* WikiTextSectionUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 0487047D19F8262600B7D307 /* WikiTextSectionUploader.m */; }; - 048A26771906268100395F53 /* PaddedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048A26761906268100395F53 /* PaddedLabel.m */; }; 0493C2CC1952373100EBB973 /* DataHousekeeping.m in Sources */ = {isa = PBXBuildFile; fileRef = 0493C2CB1952373100EBB973 /* DataHousekeeping.m */; }; 0493C2D419526A0100EBB973 /* WikiFont-Glyphs.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0493C2D319526A0100EBB973 /* WikiFont-Glyphs.ttf */; }; 049566C218F5F4CB0058EA12 /* ZeroConfigState.m in Sources */ = {isa = PBXBuildFile; fileRef = 049566C118F5F4CB0058EA12 /* ZeroConfigState.m */; }; @@ -138,13 +139,10 @@ 04B0EA47190B2319007458AF /* PreviewLicenseView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 04B0EA46190B2319007458AF /* PreviewLicenseView.xib */; }; 04B0EA4A190B2348007458AF /* PreviewLicenseView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B0EA49190B2348007458AF /* PreviewLicenseView.m */; }; 04B162F119284A6F00B1ABC2 /* BottomMenuContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B162F019284A6F00B1ABC2 /* BottomMenuContainerView.m */; }; - 04B6050C193522650007185A /* WikiGlyphButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B6050B193522650007185A /* WikiGlyphButton.m */; }; - 04B605101935236C0007185A /* WikiGlyphLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B6050F1935236C0007185A /* WikiGlyphLabel.m */; }; 04B6925018E77B2A00F88D8A /* UIWebView+HideScrollGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B6924F18E77B2A00F88D8A /* UIWebView+HideScrollGradient.m */; }; 04B7B9BD18B5570E00A63551 /* CaptchaViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B7B9BC18B5570E00A63551 /* CaptchaViewController.m */; }; 04B91AA718E34BBC00FFAA1C /* UIView+TemporaryAnimatedXF.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B91AA618E34BBC00FFAA1C /* UIView+TemporaryAnimatedXF.m */; }; 04B91AAB18E3D9E200FFAA1C /* NSString+FormattedAttributedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B91AAA18E3D9E200FFAA1C /* NSString+FormattedAttributedString.m */; }; - 04B91AB718E4D5B200FFAA1C /* TabularScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B91AB618E4D5B200FFAA1C /* TabularScrollView.m */; }; 04BA48A11A80062F00CB5CAE /* UIFont+WMFStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 04BA48A01A80062E00CB5CAE /* UIFont+WMFStyle.m */; }; 04C0A0781936786000D55325 /* UIViewController+ModalPresent.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C0A0771936786000D55325 /* UIViewController+ModalPresent.m */; }; 04C43AA4183440C1006C643B /* MWNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C43AA1183440C1006C643B /* MWNetworkActivityIndicatorManager.m */; }; @@ -171,8 +169,6 @@ 04CCCFF61935094000E3F60C /* PrimaryMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CCCFF31935094000E3F60C /* PrimaryMenuViewController.m */; }; 04CCCFF71935094000E3F60C /* PrimaryMenuTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CCCFF51935094000E3F60C /* PrimaryMenuTableViewCell.m */; }; 04CFA120194900D50088269A /* TopMenuTextFieldContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CFA11F194900D50088269A /* TopMenuTextFieldContainer.m */; }; - 04CFA123194B94980088269A /* MenuButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CFA122194B94980088269A /* MenuButton.m */; }; - 04CFA126194B94A10088269A /* MenuLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CFA125194B94A10088269A /* MenuLabel.m */; }; 04D122321899B8AC006B9A30 /* AlertWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D122311899B8AC006B9A30 /* AlertWebView.m */; }; 04D149DD18877343006B4104 /* AlertLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D149DA18877343006B4104 /* AlertLabel.m */; }; 04D149DF18877343006B4104 /* UIViewController+Alert.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D149DC18877343006B4104 /* UIViewController+Alert.m */; }; @@ -180,6 +176,22 @@ 04D3082B19991CB60034F106 /* logo-placeholder-nearby.png in Resources */ = {isa = PBXBuildFile; fileRef = 04D3082919991CB60034F106 /* logo-placeholder-nearby.png */; }; 04D3082C19991CB60034F106 /* logo-placeholder-nea...@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 04D3082A19991CB60034F106 /* logo-placeholder-nea...@2x.png */; }; 04D34DB21863D39000610A87 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 04D34DB11863D39000610A87 /* libxml2.dylib */; }; + 04D686C91AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686C81AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m */; }; + 04D686CA1AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686C81AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m */; }; + 04D686CE1AB292160009B44A /* WMFFaceDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686CD1AB292160009B44A /* WMFFaceDetector.m */; }; + 04D686CF1AB292160009B44A /* WMFFaceDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686CD1AB292160009B44A /* WMFFaceDetector.m */; }; + 04D686F41AB2949C0009B44A /* MenuButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686E91AB2949C0009B44A /* MenuButton.m */; }; + 04D686F51AB2949C0009B44A /* MenuButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686E91AB2949C0009B44A /* MenuButton.m */; }; + 04D686F61AB2949C0009B44A /* MenuLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686EB1AB2949C0009B44A /* MenuLabel.m */; }; + 04D686F71AB2949C0009B44A /* MenuLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686EB1AB2949C0009B44A /* MenuLabel.m */; }; + 04D686F81AB2949C0009B44A /* PaddedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686ED1AB2949C0009B44A /* PaddedLabel.m */; }; + 04D686F91AB2949C0009B44A /* PaddedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686ED1AB2949C0009B44A /* PaddedLabel.m */; }; + 04D686FA1AB2949C0009B44A /* TabularScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686EF1AB2949C0009B44A /* TabularScrollView.m */; }; + 04D686FB1AB2949C0009B44A /* TabularScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686EF1AB2949C0009B44A /* TabularScrollView.m */; }; + 04D686FC1AB2949C0009B44A /* WikiGlyphButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686F11AB2949C0009B44A /* WikiGlyphButton.m */; }; + 04D686FD1AB2949C0009B44A /* WikiGlyphButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686F11AB2949C0009B44A /* WikiGlyphButton.m */; }; + 04D686FE1AB2949C0009B44A /* WikiGlyphLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686F31AB2949C0009B44A /* WikiGlyphLabel.m */; }; + 04D686FF1AB2949C0009B44A /* WikiGlyphLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D686F31AB2949C0009B44A /* WikiGlyphLabel.m */; }; 04DB0BEA18BD37F900B4BCF3 /* UIScrollView+ScrollSubviewToLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 04DB0BE918BD37F900B4BCF3 /* UIScrollView+ScrollSubviewToLocation.m */; }; 04DD89B118BFE63A00DD5DAD /* PreviewAndSaveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04DD89B018BFE63A00DD5DAD /* PreviewAndSaveViewController.m */; }; 04E106341A3560030046DC27 /* LeadImageContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E106321A3560030046DC27 /* LeadImageContainer.m */; }; @@ -372,6 +384,10 @@ 04090A32187F53E400577EDF /* clear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = clear.png; sourceTree = "<group>"; }; 04090A39187FB7D000577EDF /* UIView+Debugging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+Debugging.h"; sourceTree = "<group>"; }; 04090A3A187FB7D000577EDF /* UIView+Debugging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+Debugging.m"; sourceTree = "<group>"; }; + 040D83571AB0ECFD000896D5 /* WMFCenteredPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WMFCenteredPathView.h; sourceTree = "<group>"; }; + 040D83581AB0ECFD000896D5 /* WMFCenteredPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WMFCenteredPathView.m; sourceTree = "<group>"; }; + 040D835C1AB0EE45000896D5 /* WMFGeometry.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = WMFGeometry.c; sourceTree = "<group>"; }; + 040D835D1AB0EE45000896D5 /* WMFGeometry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WMFGeometry.h; sourceTree = "<group>"; }; 040E5C4E184566F4007AFE6F /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 04123624189C29EA00E0CF8E /* abuse-filter-disallowed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "abuse-filter-disallowed.png"; sourceTree = "<group>"; }; 04123625189C29EA00E0CF8E /* abuse-filter-disallo...@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "abuse-filter-disallo...@2x.png"; sourceTree = "<group>"; }; @@ -486,15 +502,11 @@ 04530AFA1935C2B500022512 /* EmptySegue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EmptySegue.m; sourceTree = "<group>"; }; 045374861A35834D00CE1A56 /* LeadImageTitleAttributedString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LeadImageTitleAttributedString.h; sourceTree = "<group>"; }; 045374871A35834D00CE1A56 /* LeadImageTitleAttributedString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LeadImageTitleAttributedString.m; sourceTree = "<group>"; }; - 045424441A429E8800F1DF2F /* FocalImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FocalImage.h; sourceTree = "<group>"; }; - 045424451A429E8800F1DF2F /* FocalImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FocalImage.m; sourceTree = "<group>"; }; 045A9F0C18F6090E0057EA85 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = "<group>"; }; 045D871F19FAD2FA0035C1F9 /* AboutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AboutViewController.h; sourceTree = "<group>"; }; 045D872019FAD2FA0035C1F9 /* AboutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AboutViewController.m; sourceTree = "<group>"; }; 045EFF1819A25FEB00D0EDBB /* logo-placeholder-search.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-placeholder-search.png"; sourceTree = "<group>"; }; 045EFF1919A25FEB00D0EDBB /* logo-placeholder-sea...@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-placeholder-sea...@2x.png"; sourceTree = "<group>"; }; - 0460F8DA19B0F932001BC59B /* CenteredPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CenteredPathView.h; sourceTree = "<group>"; }; - 0460F8DB19B0F932001BC59B /* CenteredPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CenteredPathView.m; sourceTree = "<group>"; }; 0462A6CF1A1FE016009412D4 /* SearchResultAttributedString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SearchResultAttributedString.h; sourceTree = "<group>"; }; 0462A6D01A1FE016009412D4 /* SearchResultAttributedString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SearchResultAttributedString.m; sourceTree = "<group>"; }; 0463639618A844570049EE4F /* KeychainCredentials.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeychainCredentials.h; sourceTree = "<group>"; }; @@ -571,8 +583,6 @@ 0487047B19F8262600B7D307 /* WikiTextSectionFetcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiTextSectionFetcher.m; sourceTree = "<group>"; }; 0487047C19F8262600B7D307 /* WikiTextSectionUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WikiTextSectionUploader.h; sourceTree = "<group>"; }; 0487047D19F8262600B7D307 /* WikiTextSectionUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiTextSectionUploader.m; sourceTree = "<group>"; }; - 048A26751906268100395F53 /* PaddedLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PaddedLabel.h; sourceTree = "<group>"; }; - 048A26761906268100395F53 /* PaddedLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PaddedLabel.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 0493C2CA1952373100EBB973 /* DataHousekeeping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataHousekeeping.h; sourceTree = "<group>"; }; 0493C2CB1952373100EBB973 /* DataHousekeeping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataHousekeeping.m; sourceTree = "<group>"; }; 0493C2D319526A0100EBB973 /* WikiFont-Glyphs.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "WikiFont-Glyphs.ttf"; sourceTree = "<group>"; }; @@ -595,10 +605,6 @@ 04B0EA49190B2348007458AF /* PreviewLicenseView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreviewLicenseView.m; sourceTree = "<group>"; }; 04B162EF19284A6F00B1ABC2 /* BottomMenuContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BottomMenuContainerView.h; sourceTree = "<group>"; }; 04B162F019284A6F00B1ABC2 /* BottomMenuContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BottomMenuContainerView.m; sourceTree = "<group>"; }; - 04B6050A193522650007185A /* WikiGlyphButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WikiGlyphButton.h; sourceTree = "<group>"; }; - 04B6050B193522650007185A /* WikiGlyphButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiGlyphButton.m; sourceTree = "<group>"; }; - 04B6050E1935236C0007185A /* WikiGlyphLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WikiGlyphLabel.h; sourceTree = "<group>"; }; - 04B6050F1935236C0007185A /* WikiGlyphLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiGlyphLabel.m; sourceTree = "<group>"; }; 04B6924E18E77B2A00F88D8A /* UIWebView+HideScrollGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWebView+HideScrollGradient.h"; sourceTree = "<group>"; }; 04B6924F18E77B2A00F88D8A /* UIWebView+HideScrollGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWebView+HideScrollGradient.m"; sourceTree = "<group>"; }; 04B7B9BB18B5570E00A63551 /* CaptchaViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CaptchaViewController.h; sourceTree = "<group>"; }; @@ -607,8 +613,6 @@ 04B91AA618E34BBC00FFAA1C /* UIView+TemporaryAnimatedXF.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+TemporaryAnimatedXF.m"; sourceTree = "<group>"; }; 04B91AA918E3D9E200FFAA1C /* NSString+FormattedAttributedString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FormattedAttributedString.h"; sourceTree = "<group>"; }; 04B91AAA18E3D9E200FFAA1C /* NSString+FormattedAttributedString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FormattedAttributedString.m"; sourceTree = "<group>"; }; - 04B91AB518E4D5B200FFAA1C /* TabularScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TabularScrollView.h; sourceTree = "<group>"; }; - 04B91AB618E4D5B200FFAA1C /* TabularScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TabularScrollView.m; sourceTree = "<group>"; }; 04BA489F1A80062E00CB5CAE /* UIFont+WMFStyle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+WMFStyle.h"; sourceTree = "<group>"; }; 04BA48A01A80062E00CB5CAE /* UIFont+WMFStyle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+WMFStyle.m"; sourceTree = "<group>"; }; 04C0A0761936786000D55325 /* UIViewController+ModalPresent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ModalPresent.h"; sourceTree = "<group>"; }; @@ -655,10 +659,6 @@ 04CCCFF51935094000E3F60C /* PrimaryMenuTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrimaryMenuTableViewCell.m; sourceTree = "<group>"; }; 04CFA11E194900D50088269A /* TopMenuTextFieldContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TopMenuTextFieldContainer.h; sourceTree = "<group>"; }; 04CFA11F194900D50088269A /* TopMenuTextFieldContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TopMenuTextFieldContainer.m; sourceTree = "<group>"; }; - 04CFA121194B94980088269A /* MenuButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuButton.h; sourceTree = "<group>"; }; - 04CFA122194B94980088269A /* MenuButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuButton.m; sourceTree = "<group>"; }; - 04CFA124194B94A10088269A /* MenuLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuLabel.h; sourceTree = "<group>"; }; - 04CFA125194B94A10088269A /* MenuLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuLabel.m; sourceTree = "<group>"; }; 04D122301899B8AC006B9A30 /* AlertWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AlertWebView.h; sourceTree = "<group>"; }; 04D122311899B8AC006B9A30 /* AlertWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AlertWebView.m; sourceTree = "<group>"; }; 04D149D918877343006B4104 /* AlertLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AlertLabel.h; sourceTree = "<group>"; }; @@ -670,6 +670,22 @@ 04D3082919991CB60034F106 /* logo-placeholder-nearby.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-placeholder-nearby.png"; sourceTree = "<group>"; }; 04D3082A19991CB60034F106 /* logo-placeholder-nea...@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-placeholder-nea...@2x.png"; sourceTree = "<group>"; }; 04D34DB11863D39000610A87 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; + 04D686C71AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+WMFFocalImageDrawing.h"; sourceTree = "<group>"; }; + 04D686C81AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+WMFFocalImageDrawing.m"; sourceTree = "<group>"; }; + 04D686CC1AB292160009B44A /* WMFFaceDetector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WMFFaceDetector.h; sourceTree = "<group>"; }; + 04D686CD1AB292160009B44A /* WMFFaceDetector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WMFFaceDetector.m; sourceTree = "<group>"; }; + 04D686E81AB2949C0009B44A /* MenuButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuButton.h; sourceTree = "<group>"; }; + 04D686E91AB2949C0009B44A /* MenuButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuButton.m; sourceTree = "<group>"; }; + 04D686EA1AB2949C0009B44A /* MenuLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuLabel.h; sourceTree = "<group>"; }; + 04D686EB1AB2949C0009B44A /* MenuLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuLabel.m; sourceTree = "<group>"; }; + 04D686EC1AB2949C0009B44A /* PaddedLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PaddedLabel.h; sourceTree = "<group>"; }; + 04D686ED1AB2949C0009B44A /* PaddedLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaddedLabel.m; sourceTree = "<group>"; }; + 04D686EE1AB2949C0009B44A /* TabularScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TabularScrollView.h; sourceTree = "<group>"; }; + 04D686EF1AB2949C0009B44A /* TabularScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TabularScrollView.m; sourceTree = "<group>"; }; + 04D686F01AB2949C0009B44A /* WikiGlyphButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WikiGlyphButton.h; sourceTree = "<group>"; }; + 04D686F11AB2949C0009B44A /* WikiGlyphButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiGlyphButton.m; sourceTree = "<group>"; }; + 04D686F21AB2949C0009B44A /* WikiGlyphLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WikiGlyphLabel.h; sourceTree = "<group>"; }; + 04D686F31AB2949C0009B44A /* WikiGlyphLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WikiGlyphLabel.m; sourceTree = "<group>"; }; 04DB0BE818BD37F900B4BCF3 /* UIScrollView+ScrollSubviewToLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+ScrollSubviewToLocation.h"; sourceTree = "<group>"; }; 04DB0BE918BD37F900B4BCF3 /* UIScrollView+ScrollSubviewToLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+ScrollSubviewToLocation.m"; sourceTree = "<group>"; }; 04DD89AF18BFE63A00DD5DAD /* PreviewAndSaveViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreviewAndSaveViewController.h; sourceTree = "<group>"; }; @@ -1128,6 +1144,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 040D835B1AB0EE14000896D5 /* C Methods */ = { + isa = PBXGroup; + children = ( + 040D835D1AB0EE45000896D5 /* WMFGeometry.h */, + 040D835C1AB0EE45000896D5 /* WMFGeometry.c */, + ); + path = "C Methods"; + sourceTree = "<group>"; + }; 040E5C50184673F2007AFE6F /* Data */ = { isa = PBXGroup; children = ( @@ -1381,15 +1406,6 @@ path = About; sourceTree = "<group>"; }; - 0460F8D919B0F90E001BC59B /* CenteredPathView */ = { - isa = PBXGroup; - children = ( - 0460F8DA19B0F932001BC59B /* CenteredPathView.h */, - 0460F8DB19B0F932001BC59B /* CenteredPathView.m */, - ); - path = CenteredPathView; - sourceTree = "<group>"; - }; 0463639518A844380049EE4F /* Keychain */ = { isa = PBXGroup; children = ( @@ -1577,15 +1593,6 @@ path = BaseFetcher; sourceTree = "<group>"; }; - 048A26741906268100395F53 /* PaddedLabel */ = { - isa = PBXGroup; - children = ( - 048A26751906268100395F53 /* PaddedLabel.h */, - 048A26761906268100395F53 /* PaddedLabel.m */, - ); - path = PaddedLabel; - sourceTree = "<group>"; - }; 0493C2C91952373100EBB973 /* Housekeeping */ = { isa = PBXGroup; children = ( @@ -1632,28 +1639,6 @@ path = ../Importer; sourceTree = SOURCE_ROOT; }; - 04B60509193522650007185A /* MenuButton */ = { - isa = PBXGroup; - children = ( - 04B6050A193522650007185A /* WikiGlyphButton.h */, - 04B6050B193522650007185A /* WikiGlyphButton.m */, - 04CFA121194B94980088269A /* MenuButton.h */, - 04CFA122194B94980088269A /* MenuButton.m */, - ); - path = MenuButton; - sourceTree = "<group>"; - }; - 04B6050D1935236C0007185A /* MenuLabel */ = { - isa = PBXGroup; - children = ( - 04B6050E1935236C0007185A /* WikiGlyphLabel.h */, - 04B6050F1935236C0007185A /* WikiGlyphLabel.m */, - 04CFA124194B94A10088269A /* MenuLabel.h */, - 04CFA125194B94A10088269A /* MenuLabel.m */, - ); - path = MenuLabel; - sourceTree = "<group>"; - }; 04B7B9BA18B5569600A63551 /* Captcha */ = { isa = PBXGroup; children = ( @@ -1661,15 +1646,6 @@ 04B7B9BC18B5570E00A63551 /* CaptchaViewController.m */, ); path = Captcha; - sourceTree = "<group>"; - }; - 04B91AB418E4D58D00FFAA1C /* TabularScrollView */ = { - isa = PBXGroup; - children = ( - 04B91AB518E4D5B200FFAA1C /* TabularScrollView.h */, - 04B91AB618E4D5B200FFAA1C /* TabularScrollView.m */, - ); - path = TabularScrollView; sourceTree = "<group>"; }; 04C43A9F183440C1006C643B /* mw-network */ = { @@ -1780,6 +1756,8 @@ 04B91AA618E34BBC00FFAA1C /* UIView+TemporaryAnimatedXF.m */, 043F8BF01A11699A00D1AE44 /* UIView+WMFRoundCorners.h */, 043F8BF11A11699A00D1AE44 /* UIView+WMFRoundCorners.m */, + 04D686C71AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.h */, + 04D686C81AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m */, 044396211A3D33030081557D /* UICollectionViewCell+DynamicCellHeight.h */, 044396221A3D33030081557D /* UICollectionViewCell+DynamicCellHeight.m */, 0433542018A023FE009305F0 /* UIViewController+HideKeyboard.h */, @@ -1911,6 +1889,15 @@ path = Alerts; sourceTree = "<group>"; }; + 04D686CB1AB291DE0009B44A /* Custom Objects */ = { + isa = PBXGroup; + children = ( + 04D686CC1AB292160009B44A /* WMFFaceDetector.h */, + 04D686CD1AB292160009B44A /* WMFFaceDetector.m */, + ); + path = "Custom Objects"; + sourceTree = "<group>"; + }; 04DD89AE18BFE63A00DD5DAD /* Preview */ = { isa = PBXGroup; children = ( @@ -1929,11 +1916,9 @@ 04E106301A3560030046DC27 /* LeadImage */ = { isa = PBXGroup; children = ( - 04E106331A3560030046DC27 /* LeadImageContainer.xib */, 04E106311A3560030046DC27 /* LeadImageContainer.h */, 04E106321A3560030046DC27 /* LeadImageContainer.m */, - 045424441A429E8800F1DF2F /* FocalImage.h */, - 045424451A429E8800F1DF2F /* FocalImage.m */, + 04E106331A3560030046DC27 /* LeadImageContainer.xib */, 04E106361A3560A90046DC27 /* LeadImageTitleLabel.h */, 04E106371A3560A90046DC27 /* LeadImageTitleLabel.m */, 045374861A35834D00CE1A56 /* LeadImageTitleAttributedString.h */, @@ -2156,8 +2141,22 @@ C42D94811A937DE000A4871A /* Custom Views */ = { isa = PBXGroup; children = ( + 04D686E81AB2949C0009B44A /* MenuButton.h */, + 04D686E91AB2949C0009B44A /* MenuButton.m */, + 04D686EA1AB2949C0009B44A /* MenuLabel.h */, + 04D686EB1AB2949C0009B44A /* MenuLabel.m */, + 04D686EC1AB2949C0009B44A /* PaddedLabel.h */, + 04D686ED1AB2949C0009B44A /* PaddedLabel.m */, + 04D686EE1AB2949C0009B44A /* TabularScrollView.h */, + 04D686EF1AB2949C0009B44A /* TabularScrollView.m */, + 04D686F01AB2949C0009B44A /* WikiGlyphButton.h */, + 04D686F11AB2949C0009B44A /* WikiGlyphButton.m */, + 04D686F21AB2949C0009B44A /* WikiGlyphLabel.h */, + 04D686F31AB2949C0009B44A /* WikiGlyphLabel.m */, C42D94821A937DE000A4871A /* WMFBorderButton.h */, C42D94831A937DE000A4871A /* WMFBorderButton.m */, + 040D83571AB0ECFD000896D5 /* WMFCenteredPathView.h */, + 040D83581AB0ECFD000896D5 /* WMFCenteredPathView.m */, C963358F1AA92AAC00A1EB2C /* WMFCrashAlertView.h */, C96335901AA92AAC00A1EB2C /* WMFCrashAlertView.m */, C42D94841A937DE000A4871A /* WMFProgressLineView.h */, @@ -2187,6 +2186,7 @@ isa = PBXGroup; children = ( C98990321A699DE000AF44FC /* WMFShareCardViewController.h */, + C98990331A699DE000AF44FC /* WMFShareCardViewController.m */, C91A86F21A8BCB680088A801 /* WMFShareCardImageContainer.h */, C91A86F31A8BCB680088A801 /* WMFShareCardImageContainer.m */, C90799B81A8564C60044E13C /* WMFShareOptionsViewController.h */, @@ -2196,7 +2196,6 @@ C97972791A731EAA00C6ED7A /* ShareOptions.xib */, C979727B1A731F2D00C6ED7A /* WMFShareOptionsView.h */, C979727C1A731F2D00C6ED7A /* WMFShareOptionsView.m */, - C98990331A699DE000AF44FC /* WMFShareCardViewController.m */, C98990351A699DFB00AF44FC /* ShareCard.xib */, ); name = ShareCard; @@ -2265,8 +2264,10 @@ D46CD8C218A1AC4F0042959E /* Localizable.strings */, 045A9F0C18F6090E0057EA85 /* assets */, 04272E771940EEBC00CC682F /* AssetsFile */, + 040D835B1AB0EE14000896D5 /* C Methods */, + C42D94811A937DE000A4871A /* Custom Views */, + 04D686CB1AB291DE0009B44A /* Custom Objects */, 04C43AB7183442FC006C643B /* Categories */, - 0460F8D919B0F90E001BC59B /* CenteredPathView */, 040E5C50184673F2007AFE6F /* Data */, 04292FFB185FC026002A13FC /* Defines */, D4B0ADFF19365F4600F0AC90 /* EventLogging */, @@ -2274,13 +2275,8 @@ 0493C2C91952373100EBB973 /* Housekeeping */, 0466F44C183A30CC00EA1FD7 /* Images */, 0463639518A844380049EE4F /* Keychain */, - 04B60509193522650007185A /* MenuButton */, - 04B6050D1935236C0007185A /* MenuLabel */, - C42D94811A937DE000A4871A /* Custom Views */, 0487041519F824D700B7D307 /* Networking */, - 048A26741906268100395F53 /* PaddedLabel */, 0447866C1852B5010050563B /* Session */, - 04B91AB418E4D58D00FFAA1C /* TabularScrollView */, 04C43AB0183441A4006C643B /* View Controllers */, 04A70FD4185BB6C300E24515 /* Web Image Interception */, D499143F181D51DE00E6073C /* Supporting Files */, @@ -2808,11 +2804,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 04D686FD1AB2949C0009B44A /* WikiGlyphButton.m in Sources */, BCDB75C41AB0E8300005593F /* WMFSubstringUtilsTests.m in Sources */, BC0FED6D1AAA0268002488D7 /* MWKHistoryListTests.m in Sources */, BC0FED731AAA026C002488D7 /* NSMutableDictionary+MaybeSetTests.m in Sources */, BC0FED6E1AAA0268002488D7 /* MWKImageListTests.m in Sources */, + 04D686F51AB2949C0009B44A /* MenuButton.m in Sources */, BC0FED701AAA026C002488D7 /* NSArray+PredicateTests.m in Sources */, + 04D686F91AB2949C0009B44A /* PaddedLabel.m in Sources */, BC0FED771AAA026C002488D7 /* WMFImageURLParsingTests.m in Sources */, BC0FED6B1AAA0268002488D7 /* MWKDataStoreStorageTests.m in Sources */, BC0FED751AAA026C002488D7 /* NSArray+BKIndexTests.m in Sources */, @@ -2822,13 +2821,20 @@ BC0FED621AAA0263002488D7 /* WMFCodingStyle.m in Sources */, BC0FED741AAA026C002488D7 /* CircularBitwiseRotationTests.m in Sources */, BC0FED681AAA0268002488D7 /* MWKUserTests.m in Sources */, + 04D686F71AB2949C0009B44A /* MenuLabel.m in Sources */, BC0FED661AAA0268002488D7 /* MWKSiteTests.m in Sources */, + 040D835F1AB0EE45000896D5 /* WMFGeometry.c in Sources */, BC0FED691AAA0268002488D7 /* MWKProtectionStatusTests.m in Sources */, + 040D835A1AB0ECFD000896D5 /* WMFCenteredPathView.m in Sources */, BC0FED6C1AAA0268002488D7 /* MWKImageStorageTests.m in Sources */, BC0FED721AAA026C002488D7 /* WMFErrorForApiErrorObjectTests.m in Sources */, + 04D686CA1AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m in Sources */, + 04D686FF1AB2949C0009B44A /* WikiGlyphLabel.m in Sources */, BC0FED711AAA026C002488D7 /* WMFJoinedPropertyParametersTests.m in Sources */, BC0FED6A1AAA0268002488D7 /* MWKDataStorePathTests.m in Sources */, BC0FED761AAA026C002488D7 /* NSString+WMFHTMLParsingTests.m in Sources */, + 04D686CF1AB292160009B44A /* WMFFaceDetector.m in Sources */, + 04D686FB1AB2949C0009B44A /* TabularScrollView.m in Sources */, BC0FED671AAA0268002488D7 /* MWKTitleTests.m in Sources */, BC0FED631AAA0263002488D7 /* MWKTestCase.m in Sources */, ); @@ -2857,7 +2863,6 @@ BCB669A91A83F6C400C7B1FE /* MWKSection.m in Sources */, BCB669B61A83F6C400C7B1FE /* MWKImageList.m in Sources */, BCB848831AAE0C5C0077EC24 /* WMFImageGalleryCollectionViewCell.m in Sources */, - 04CFA126194B94A10088269A /* MenuLabel.m in Sources */, 04B0EA4A190B2348007458AF /* PreviewLicenseView.m in Sources */, 0462A6D11A1FE016009412D4 /* SearchResultAttributedString.m in Sources */, 0487047F19F8262600B7D307 /* AccountCreator.m in Sources */, @@ -2876,19 +2881,19 @@ 0463639818A844570049EE4F /* KeychainCredentials.m in Sources */, BCA96E771AAA35EE009A61FA /* UIView+WMFDefaultNib.m in Sources */, 04414DDB1A140FAF00A41B4E /* SearchDidYouMeanButton.m in Sources */, + 040D83591AB0ECFD000896D5 /* WMFCenteredPathView.m in Sources */, 04530AFB1935C2B500022512 /* EmptySegue.m in Sources */, BCB669B41A83F6C400C7B1FE /* MWKArticle.m in Sources */, 044BD6B618849AD000FFE4BE /* SectionEditorViewController.m in Sources */, 042B3996192EAEEA0066B270 /* ShareMenuSavePageActivity.m in Sources */, 0429301018604898002A13FC /* SavedPagesViewController.m in Sources */, 04D149DF18877343006B4104 /* UIViewController+Alert.m in Sources */, - 04B605101935236C0007185A /* WikiGlyphLabel.m in Sources */, BCA96E731AAA354D009A61FA /* WMFGradientView.m in Sources */, - 048A26771906268100395F53 /* PaddedLabel.m in Sources */, 04B91AAB18E3D9E200FFAA1C /* NSString+FormattedAttributedString.m in Sources */, 043F18E518D9691D00D8489A /* UINavigationController+TopActionSheet.m in Sources */, 044396231A3D33030081557D /* UICollectionViewCell+DynamicCellHeight.m in Sources */, 04414DDF1A1420EB00A41B4E /* WikiDataShortDescriptionFetcher.m in Sources */, + 04D686F61AB2949C0009B44A /* MenuLabel.m in Sources */, 04D308281998A8AA0034F106 /* NearbyThumbnailView.m in Sources */, 042E3B931AA16D6700BF8D66 /* UIViewController+WMFChildViewController.m in Sources */, 045374881A35834D00CE1A56 /* LeadImageTitleAttributedString.m in Sources */, @@ -2898,6 +2903,7 @@ 0447866F1852B5010050563B /* SessionSingleton.m in Sources */, BCB669AA1A83F6C400C7B1FE /* MWKImage.m in Sources */, D4E8A8A4190835C100DA4765 /* DataMigrator.m in Sources */, + 04D686F41AB2949C0009B44A /* MenuButton.m in Sources */, 0443961B1A3C11A30081557D /* NearbyResultCollectionCell.m in Sources */, 0487048519F8262600B7D307 /* EditTokenFetcher.m in Sources */, 04CCCFF71935094000E3F60C /* PrimaryMenuTableViewCell.m in Sources */, @@ -2921,6 +2927,7 @@ 04A70FD7185BB6C300E24515 /* URLCache.m in Sources */, C90799BA1A8564C60044E13C /* WMFShareOptionsViewController.m in Sources */, 04C91CEB195517250035ED1B /* OnboardingViewController.m in Sources */, + 04D686FA1AB2949C0009B44A /* TabularScrollView.m in Sources */, 041A3B5E18E11ED90079FF1C /* LanguagesCell.m in Sources */, 042487521A54BECD00A5C905 /* MWKArticle+Convenience.m in Sources */, 043DAC4B1901C3EE001CD17C /* CreditsViewController.m in Sources */, @@ -2931,17 +2938,18 @@ 04D149DD18877343006B4104 /* AlertLabel.m in Sources */, BCB669AE1A83F6C400C7B1FE /* MWKHistoryEntry.m in Sources */, 0433542618A093C5009305F0 /* UIView+RemoveConstraints.m in Sources */, + 04D686FC1AB2949C0009B44A /* WikiGlyphButton.m in Sources */, C98990341A699DE000AF44FC /* WMFShareCardViewController.m in Sources */, C42D94861A937DE000A4871A /* WMFBorderButton.m in Sources */, 047801BE18AE987900DBB747 /* UIButton+ColorMask.m in Sources */, BC86B93D1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m in Sources */, + 04D686C91AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m in Sources */, 0429300A18604898002A13FC /* SavedPagesResultCell.m in Sources */, 0487048419F8262600B7D307 /* CaptchaResetter.m in Sources */, D407E6411A51DBDA00CCC8B1 /* SchemaConverter.m in Sources */, BCB669A71A83F6C400C7B1FE /* MWKSiteDataObject.m in Sources */, BC7DFCD61AA4E5FE000035C3 /* WMFImageURLParsing.m in Sources */, 04821CD119895EDC007558F6 /* ReferenceGradientView.m in Sources */, - 0460F8DC19B0F932001BC59B /* CenteredPathView.m in Sources */, 0472BC18193AD88C00C40BDA /* MWKSection+DisplayHtml.m in Sources */, 047ED63918C13E4900442BE3 /* PreviewWebView.m in Sources */, 0480AEA01AA4F4DA00A9950C /* WMFIntrinsicContentSizeAwareTableView.m in Sources */, @@ -2971,7 +2979,6 @@ 04224501197F5E09005DD0BF /* BulletedLabel.m in Sources */, C979727D1A731F2D00C6ED7A /* WMFShareOptionsView.m in Sources */, C91A86F41A8BCB680088A801 /* WMFShareCardImageContainer.m in Sources */, - 04B6050C193522650007185A /* WikiGlyphButton.m in Sources */, BCB58F781A8D081E00465627 /* NSArray+BKIndex.m in Sources */, 042A5B2C19253E690095E172 /* BottomMenuViewController.m in Sources */, 0433542218A023FE009305F0 /* UIViewController+HideKeyboard.m in Sources */, @@ -2985,11 +2992,12 @@ BCB58F441A890D9700465627 /* MWKImageInfo+MWKImageComparison.m in Sources */, BCB669AD1A83F6C400C7B1FE /* MWKSavedPageList.m in Sources */, BCC185E81A9FA498005378F8 /* UICollectionViewFlowLayout+AttributeUtils.m in Sources */, - 04B91AB718E4D5B200FFAA1C /* TabularScrollView.m in Sources */, + 04D686F81AB2949C0009B44A /* PaddedLabel.m in Sources */, 04C695D218ED213000D9F2DA /* UIScrollView+NoHorizontalScrolling.m in Sources */, 04E106381A3560A90046DC27 /* LeadImageTitleLabel.m in Sources */, 04F0E2EE186FB2D100468738 /* TOCSectionCellView.m in Sources */, 04D122321899B8AC006B9A30 /* AlertWebView.m in Sources */, + 040D835E1AB0EE45000896D5 /* WMFGeometry.c in Sources */, 04C0A0781936786000D55325 /* UIViewController+ModalPresent.m in Sources */, BC86B9361A92966B00B4C039 /* AFHTTPRequestOperationManager+UniqueRequests.m in Sources */, D4991449181D51DE00E6073C /* AppDelegate.m in Sources */, @@ -3021,7 +3029,6 @@ BC955BC71A82BEFD000EF9E4 /* MWKImageInfoFetcher.m in Sources */, 04C7576E1A1AA2D00084AC39 /* RecentSearchCell.m in Sources */, BCB58F541A894D3E00465627 /* WMFImageGalleryDetailOverlayView.m in Sources */, - 045424461A429E8800F1DF2F /* FocalImage.m in Sources */, 04292FF8185FBB0B002A13FC /* SearchResultsController.m in Sources */, 04478633185145090050563B /* HistoryViewController.m in Sources */, BCB669B21A83F6C400C7B1FE /* MWKImageInfo.m in Sources */, @@ -3045,11 +3052,12 @@ 04272E7B1940EEBC00CC682F /* WMFAssetsFile.m in Sources */, 042A5B2919253E570095E172 /* TopMenuViewController.m in Sources */, D4991445181D51DE00E6073C /* main.m in Sources */, - 04CFA123194B94980088269A /* MenuButton.m in Sources */, 0480AE9C1AA4F01600A9950C /* WMFWebViewFooterContainerView.m in Sources */, D47BF5D4197870390067C3BC /* SavedPagesFunnel.m in Sources */, 04C43AC0183442FC006C643B /* NSString+Extras.m in Sources */, + 04D686FE1AB2949C0009B44A /* WikiGlyphLabel.m in Sources */, 04CCCFEE1935093A00E3F60C /* SecondaryMenuRowView.m in Sources */, + 04D686CE1AB292160009B44A /* WMFFaceDetector.m in Sources */, 0442F57B19006DCC00F55DF9 /* PageHistoryLabel.m in Sources */, 041C6206199ED2A20061516F /* MWKSection+TOC.m in Sources */, BC2CBB8E1AA10F400079A313 /* UIView+WMFFrameUtils.m in Sources */, diff --git a/wikipedia/C Methods/WMFGeometry.c b/wikipedia/C Methods/WMFGeometry.c new file mode 100644 index 0000000..d33f69d --- /dev/null +++ b/wikipedia/C Methods/WMFGeometry.c @@ -0,0 +1,25 @@ +// Created by Monte Hurd on 3/11/15. +// Copyright (c) 2015 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#include "WMFGeometry.h" + +CGRect WMFUnitRectFromRectForReferenceSize(CGRect rect, CGSize refSize){ + if (CGSizeEqualToSize(refSize, CGSizeZero) || CGRectIsEmpty(rect)) { + return CGRectZero; + } + return CGRectMake( + CGRectGetMinX(rect) / refSize.width, + CGRectGetMinY(rect) / refSize.height, + CGRectGetWidth(rect) / refSize.width, + CGRectGetHeight(rect) / refSize.height + ); +} + +CGRect WMFRectFromUnitRectForReferenceSize(CGRect unitRect, CGSize refSize){ + return CGRectMake( + unitRect.origin.x * refSize.width, + unitRect.origin.y * refSize.height, + unitRect.size.width * refSize.width, + unitRect.size.height * refSize.height + ); +} \ No newline at end of file diff --git a/wikipedia/C Methods/WMFGeometry.h b/wikipedia/C Methods/WMFGeometry.h new file mode 100644 index 0000000..225e8c4 --- /dev/null +++ b/wikipedia/C Methods/WMFGeometry.h @@ -0,0 +1,17 @@ +// Created by Monte Hurd on 3/11/15. +// Copyright (c) 2015 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#ifndef __Wikipedia__WMFGeometry__ +#define __Wikipedia__WMFGeometry__ + +#include <stdio.h> + +#include <CoreGraphics/CGGeometry.h> + +// Convert rect to a unit rect for reference size. +CG_EXTERN CGRect WMFUnitRectFromRectForReferenceSize(CGRect rect, CGSize referenceSize); + +// Convert unit rect back to rect for reference size. +CG_EXTERN CGRect WMFRectFromUnitRectForReferenceSize(CGRect unitRect, CGSize referenceSize); + +#endif /* defined(__Wikipedia__WMFGeometry__) */ diff --git a/wikipedia/Categories/UIImage+WMFFocalImageDrawing.h b/wikipedia/Categories/UIImage+WMFFocalImageDrawing.h new file mode 100644 index 0000000..5712753 --- /dev/null +++ b/wikipedia/Categories/UIImage+WMFFocalImageDrawing.h @@ -0,0 +1,22 @@ +// Created by Monte Hurd on 3/12/15. +// Copyright (c) 2015 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#import <UIKit/UIKit.h> + +@interface UIImage (WMFFocalImageDrawing) + +/* + Draws image with: + + - Aspect fill. + - Horizontal center if horizontal overlap. + - Align top if no focalBounds, else vertical centered on focalBounds. + - Optional focalBounds highlight. + */ +- (void)wmf_drawInRect:(CGRect)rect + focalBounds:(CGRect)focalBounds + focalHighlight:(BOOL)focalHighlight + blendMode:(CGBlendMode)blendMode + alpha:(CGFloat)alpha; + +@end diff --git a/wikipedia/Categories/UIImage+WMFFocalImageDrawing.m b/wikipedia/Categories/UIImage+WMFFocalImageDrawing.m new file mode 100644 index 0000000..0363588 --- /dev/null +++ b/wikipedia/Categories/UIImage+WMFFocalImageDrawing.m @@ -0,0 +1,73 @@ +// Created by Monte Hurd on 3/12/15. +// Copyright (c) 2015 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#import "UIImage+WMFFocalImageDrawing.h" + +@implementation UIImage (WMFFocalImageDrawing) + +- (void)wmf_drawInRect:(CGRect)rect + focalBounds:(CGRect)focalBounds + focalHighlight:(BOOL)focalHighlight + blendMode:(CGBlendMode)blendMode + alpha:(CGFloat)alpha { + if ((self.size.width == 0) || (self.size.height == 0)) { + return; + } + + // Aspect fill. + float xScale = rect.size.width / self.size.width; + float yScale = rect.size.height / self.size.height; + float scale = MAX(xScale, yScale); + CGSize size = CGSizeMake(self.size.width * scale, self.size.height * scale); + + // Align top. + CGRect r = (CGRect){{0, 0}, size}; + + // Center horizontally. + CGFloat m1 = CGRectGetMidX(r); + CGFloat m2 = CGRectGetMidX(rect); + CGFloat offset = (m2 - m1); + r = CGRectOffset(r, offset, 0.0); + + // Figure out bottom overlap so we can know how much we can move the image up. + CGFloat bottomOverlap = r.size.height - rect.size.height; + if (bottomOverlap > 0.0) { + if (!CGRectIsEmpty(focalBounds)) { + // Move image up to vertically center focal bounds (as much as possible). + CGFloat yMidSelf = CGRectGetMidY(rect); + CGFloat yMidFocalBounds = CGRectGetMidY(focalBounds) * scale; + CGFloat yShift = fminf(yMidFocalBounds - yMidSelf, bottomOverlap); + if (yShift > 0) { + r = CGRectOffset(r, 0.0, -yShift); + } + } else { + // If no focalBounds, move the image up a bit, if possible. + CGFloat quarterOverlap = (bottomOverlap * 0.25); + r = CGRectOffset(r, 0.0, -quarterOverlap); + } + } + + [self drawInRect:r blendMode:blendMode alpha:alpha]; + + // Draw a box over the focal bounds. + if (focalHighlight) { + CGRect scaledfocalBounds = + CGRectMake( + (focalBounds.origin.x * scale) + r.origin.x, + (focalBounds.origin.y * scale) + r.origin.y, + focalBounds.size.width * scale, + focalBounds.size.height * scale + ); + [self fillFocalBounds:scaledfocalBounds]; + } +} + +- (void)fillFocalBounds:(CGRect)bounds { + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 0.3); + CGContextFillRect(context, bounds); + CGColorSpaceRelease(colorSpace); +} + +@end diff --git a/wikipedia/Custom Objects/WMFFaceDetector.h b/wikipedia/Custom Objects/WMFFaceDetector.h new file mode 100644 index 0000000..fe3773a --- /dev/null +++ b/wikipedia/Custom Objects/WMFFaceDetector.h @@ -0,0 +1,21 @@ +// Created by Monte Hurd on 12/17/14. +// Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#import <UIKit/UIKit.h> + +@interface WMFFaceDetector : NSObject + +@property (nonatomic, strong) UIImage* image; + +/* + "detectFace" returns rect for largest face in "self.image". + + Subsequent calls to "detectFace" return next largest face rect, + rolling back to first face after last face. + + It only actually runs face detection on the first call. + Internally cached results are returned on subsequent calls. + */ +- (CGRect)detectFace; + +@end diff --git a/wikipedia/Custom Objects/WMFFaceDetector.m b/wikipedia/Custom Objects/WMFFaceDetector.m new file mode 100644 index 0000000..b0acd60 --- /dev/null +++ b/wikipedia/Custom Objects/WMFFaceDetector.m @@ -0,0 +1,80 @@ +// Created by Monte Hurd on 12/17/14. +// Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! + +#import "WMFFaceDetector.h" + +@interface WMFFaceDetector () + +@property (strong, atomic) CIDetector* detector; +@property (strong, atomic) NSArray* faces; +@property (atomic) NSInteger nextFaceIndex; +@property (atomic, readwrite) CGRect faceBounds; + +@end + +@implementation WMFFaceDetector + +- (instancetype)init { + self = [super init]; + if (self) { + self.detector = + [CIDetector detectorOfType:CIDetectorTypeFace + context:nil + options:@{ + CIDetectorAccuracy: CIDetectorAccuracyLow, + CIDetectorMinFeatureSize: @(0.15) + }]; + } + return self; +} + +- (CGRect)detectFace { + // Optimized for repeated calls (for easy cycle through all faces). + if (!self.image) { + return CGRectZero; + } + + // No need to set faces more than once (for repeated call cycling). + if (!self.faces) { + NSAssert(self.image.CIImage, @"Attempted to use a UIImage w/o CIImage backing: Create the UIImage with 'imageWithCIImage' so face detection doesn't have to alloc/init a new CIImage to run detection. See: http://stackoverflow.com/a/15651358/135557"); + self.faces = [self.detector featuresInImage:self.image.CIImage]; + } + + CGRect widestFaceRect = CGRectZero; + + // Index overrun protection. + if (self.nextFaceIndex >= self.faces.count) { + return CGRectZero; + } + + // Get face for nextFaceIndex. + widestFaceRect = ((CIFaceFeature*)self.faces[self.nextFaceIndex]).bounds; + + if (CGRectIsEmpty(widestFaceRect)) { + return CGRectZero; + } + + // Increment so next call will return next face. + self.nextFaceIndex++; + + // Reset if last face so next call shows first face. + if (self.nextFaceIndex == self.faces.count) { + self.nextFaceIndex = 0; + } + + CGRect faceImageCoords = + (CGRect){ + {widestFaceRect.origin.x, self.image.size.height - widestFaceRect.origin.y - widestFaceRect.size.height}, + widestFaceRect.size + }; + + return faceImageCoords; +} + +- (void)setImage:(UIImage*)image { + _image = image; + self.nextFaceIndex = 0; + self.faces = nil; +} + +@end diff --git a/wikipedia/MenuButton/MenuButton.h b/wikipedia/Custom Views/MenuButton.h similarity index 100% rename from wikipedia/MenuButton/MenuButton.h rename to wikipedia/Custom Views/MenuButton.h diff --git a/wikipedia/MenuButton/MenuButton.m b/wikipedia/Custom Views/MenuButton.m similarity index 100% rename from wikipedia/MenuButton/MenuButton.m rename to wikipedia/Custom Views/MenuButton.m diff --git a/wikipedia/MenuLabel/MenuLabel.h b/wikipedia/Custom Views/MenuLabel.h similarity index 100% rename from wikipedia/MenuLabel/MenuLabel.h rename to wikipedia/Custom Views/MenuLabel.h diff --git a/wikipedia/MenuLabel/MenuLabel.m b/wikipedia/Custom Views/MenuLabel.m similarity index 100% rename from wikipedia/MenuLabel/MenuLabel.m rename to wikipedia/Custom Views/MenuLabel.m diff --git a/wikipedia/PaddedLabel/PaddedLabel.h b/wikipedia/Custom Views/PaddedLabel.h similarity index 100% rename from wikipedia/PaddedLabel/PaddedLabel.h rename to wikipedia/Custom Views/PaddedLabel.h diff --git a/wikipedia/PaddedLabel/PaddedLabel.m b/wikipedia/Custom Views/PaddedLabel.m similarity index 100% rename from wikipedia/PaddedLabel/PaddedLabel.m rename to wikipedia/Custom Views/PaddedLabel.m diff --git a/wikipedia/TabularScrollView/TabularScrollView.h b/wikipedia/Custom Views/TabularScrollView.h similarity index 100% rename from wikipedia/TabularScrollView/TabularScrollView.h rename to wikipedia/Custom Views/TabularScrollView.h diff --git a/wikipedia/TabularScrollView/TabularScrollView.m b/wikipedia/Custom Views/TabularScrollView.m similarity index 100% rename from wikipedia/TabularScrollView/TabularScrollView.m rename to wikipedia/Custom Views/TabularScrollView.m diff --git a/wikipedia/CenteredPathView/CenteredPathView.h b/wikipedia/Custom Views/WMFCenteredPathView.h similarity index 90% rename from wikipedia/CenteredPathView/CenteredPathView.h rename to wikipedia/Custom Views/WMFCenteredPathView.h index fe13fd7..8e226fa 100644 --- a/wikipedia/CenteredPathView/CenteredPathView.h +++ b/wikipedia/Custom Views/WMFCenteredPathView.h @@ -5,7 +5,7 @@ #import <UIKit/UIKit.h> -@interface CenteredPathView : UIView +@interface WMFCenteredPathView : UIView - (id)initWithPath:(CGPathRef)newPath strokeWidth:(CGFloat)strokeWidth diff --git a/wikipedia/CenteredPathView/CenteredPathView.m b/wikipedia/Custom Views/WMFCenteredPathView.m similarity index 96% rename from wikipedia/CenteredPathView/CenteredPathView.m rename to wikipedia/Custom Views/WMFCenteredPathView.m index 529a25e..242ce8a 100644 --- a/wikipedia/CenteredPathView/CenteredPathView.m +++ b/wikipedia/Custom Views/WMFCenteredPathView.m @@ -1,8 +1,8 @@ // Created by Monte Hurd on 8/26/14. -#import "CenteredPathView.h" +#import "WMFCenteredPathView.h" -@interface CenteredPathView () +@interface WMFCenteredPathView () @property (nonatomic) CGPathRef path; @property (nonatomic) CGFloat strokeWidth; @@ -11,7 +11,7 @@ @end -@implementation CenteredPathView +@implementation WMFCenteredPathView - (id)initWithPath:(CGPathRef)newPath strokeWidth:(CGFloat)strokeWidth diff --git a/wikipedia/MenuButton/WikiGlyphButton.h b/wikipedia/Custom Views/WikiGlyphButton.h similarity index 100% rename from wikipedia/MenuButton/WikiGlyphButton.h rename to wikipedia/Custom Views/WikiGlyphButton.h diff --git a/wikipedia/MenuButton/WikiGlyphButton.m b/wikipedia/Custom Views/WikiGlyphButton.m similarity index 100% rename from wikipedia/MenuButton/WikiGlyphButton.m rename to wikipedia/Custom Views/WikiGlyphButton.m diff --git a/wikipedia/MenuLabel/WikiGlyphLabel.h b/wikipedia/Custom Views/WikiGlyphLabel.h similarity index 100% rename from wikipedia/MenuLabel/WikiGlyphLabel.h rename to wikipedia/Custom Views/WikiGlyphLabel.h diff --git a/wikipedia/MenuLabel/WikiGlyphLabel.m b/wikipedia/Custom Views/WikiGlyphLabel.m similarity index 100% rename from wikipedia/MenuLabel/WikiGlyphLabel.m rename to wikipedia/Custom Views/WikiGlyphLabel.m diff --git a/wikipedia/Networking/Fetchers/SavedArticlesFetcher.h b/wikipedia/Networking/Fetchers/SavedArticlesFetcher.h index 2c2b6c1..ba67495 100644 --- a/wikipedia/Networking/Fetchers/SavedArticlesFetcher.h +++ b/wikipedia/Networking/Fetchers/SavedArticlesFetcher.h @@ -4,7 +4,7 @@ @class MWKArticle, MWKSavedPageList, AFHTTPRequestOperationManager; @class SavedArticlesFetcher; -typedef void (^WMFSavedArticlesFetcherProgress)(CGFloat progress); +typedef void (^ WMFSavedArticlesFetcherProgress)(CGFloat progress); @protocol SavedArticlesFetcherDelegate <FetchFinishedDelegate> diff --git a/wikipedia/Networking/Fetchers/SavedArticlesFetcher.m b/wikipedia/Networking/Fetchers/SavedArticlesFetcher.m index 42cadcb..14ff4ad 100644 --- a/wikipedia/Networking/Fetchers/SavedArticlesFetcher.m +++ b/wikipedia/Networking/Fetchers/SavedArticlesFetcher.m @@ -70,19 +70,15 @@ }); } -- (void)getProgress:(WMFSavedArticlesFetcherProgress)progressBlock{ - +- (void)getProgress:(WMFSavedArticlesFetcherProgress)progressBlock { dispatch_async(self.accessQueue, ^{ - CGFloat progress = [self progress]; - + dispatch_async(dispatch_get_main_queue(), ^{ - progressBlock(progress); }); }); } - - (CGFloat)progress { if ([self.savedPageList length] == 0) { @@ -117,7 +113,7 @@ dispatch_async(dispatch_get_main_queue(), ^{ [self.fetchFinishedDelegate savedArticlesFetcher:self didFetchArticle:article progress:progress status:status error:error]; - + dispatch_async(self.accessQueue, ^{ if ([self.fetchersByArticleTitle count] == 0) { [self notifyDelegate]; diff --git a/wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m b/wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m index fddcc3a..9825300 100644 --- a/wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m +++ b/wikipedia/View Controllers/Image Gallery/WMFImageGalleryViewController.m @@ -246,7 +246,7 @@ duration:(NSTimeInterval)duration { [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; NSUInteger const currentImageIndex = [self mostVisibleItemIndex]; - ImgGalleryLog(@"Will scroll to %u after rotation animation finishes.", currentImageIndex); + ImgGalleryLog(@"Will scroll to %lu after rotation animation finishes.", currentImageIndex); [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowAnimatedContent diff --git a/wikipedia/View Controllers/LeadImage/FocalImage.h b/wikipedia/View Controllers/LeadImage/FocalImage.h deleted file mode 100644 index ee6b73f..0000000 --- a/wikipedia/View Controllers/LeadImage/FocalImage.h +++ /dev/null @@ -1,26 +0,0 @@ -// Created by Monte Hurd on 12/17/14. -// Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! - -#import <UIKit/UIKit.h> - -@interface FocalImage : UIImage - -/* - This draws image with: - - - Aspect fill. - - Horizontal center if horizontal overlap. - - Align top if no focalBounds, else vertical centered on focalBounds. - - Optional focalBounds highlight. - */ -- (void)drawInRect:(CGRect)rect - focalBounds:(CGRect)focalBounds - focalHighlight:(BOOL)focalHighlight - blendMode:(CGBlendMode)blendMode - alpha:(CGFloat)alpha; - -// Repeated calls to "getFaceBounds" will return the next face -// rect each time (rolling back to first face after last face). -- (CGRect)getFaceBounds; - -@end diff --git a/wikipedia/View Controllers/LeadImage/FocalImage.m b/wikipedia/View Controllers/LeadImage/FocalImage.m deleted file mode 100644 index ca05737..0000000 --- a/wikipedia/View Controllers/LeadImage/FocalImage.m +++ /dev/null @@ -1,133 +0,0 @@ -// Created by Monte Hurd on 12/17/14. -// Copyright (c) 2014 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! - -#import "FocalImage.h" - -@interface FocalImage () - -@property (strong, nonatomic) CIDetector* detector; -@property (strong, nonatomic) NSArray* faces; -@property (nonatomic) NSInteger nextFaceIndex; - -@end - - -@implementation FocalImage - -- (void)drawInRect:(CGRect)rect - focalBounds:(CGRect)focalBounds - focalHighlight:(BOOL)focalHighlight - blendMode:(CGBlendMode)blendMode - alpha:(CGFloat)alpha { - if ((self.size.width == 0) || (self.size.height == 0)) { - return; - } - - // Aspect fill. - float xScale = rect.size.width / self.size.width; - float yScale = rect.size.height / self.size.height; - float scale = MAX(xScale, yScale); - CGSize size = CGSizeMake(self.size.width * scale, self.size.height * scale); - - // Align top. - CGRect r = (CGRect){{0, 0}, size}; - - // Center horizontally. - CGFloat m1 = CGRectGetMidX(r); - CGFloat m2 = CGRectGetMidX(rect); - CGFloat offset = (m2 - m1); - r = CGRectOffset(r, offset, 0.0); - - // Figure out bottom overlap so we can know how much we can move the image up. - CGFloat bottomOverlap = r.size.height - rect.size.height; - if (bottomOverlap > 0.0) { - if (!CGRectIsEmpty(focalBounds)) { - // Move image up to vertically center focal bounds (as much as possible). - CGFloat yMidSelf = CGRectGetMidY(rect); - CGFloat yMidFocalBounds = CGRectGetMidY(focalBounds) * scale; - CGFloat yShift = fminf(yMidFocalBounds - yMidSelf, bottomOverlap); - if (yShift > 0) { - r = CGRectOffset(r, 0.0, -yShift); - } - } else { - // If no focalBounds, move the image up a bit, if possible. - CGFloat quarterOverlap = (bottomOverlap * 0.25); - r = CGRectOffset(r, 0.0, -quarterOverlap); - } - } - - [self drawInRect:r blendMode:blendMode alpha:alpha]; - - // Draw a box over the focal bounds. - if (focalHighlight) { - CGRect scaledfocalBounds = - CGRectMake( - (focalBounds.origin.x * scale) + r.origin.x, - (focalBounds.origin.y * scale) + r.origin.y, - focalBounds.size.width * scale, - focalBounds.size.height * scale - ); - [self fillFocalBounds:scaledfocalBounds]; - } -} - -- (void)fillFocalBounds:(CGRect)bounds { - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 0.3); - CGContextFillRect(context, bounds); - CGColorSpaceRelease(colorSpace); -} - -- (CGRect)getFaceBounds { - // Optimized for repeated calls (for easy cycle through all faces). - - // No need to make the detector more than once. - if (!self.detector) { - self.detector = - [CIDetector detectorOfType:CIDetectorTypeFace - context:nil - options:@{ - CIDetectorAccuracy: CIDetectorAccuracyLow, - CIDetectorMinFeatureSize: @(0.05) - }]; - } - - // No need to set faces more than once. - if (!self.faces) { - CIImage* cgImage = [CIImage imageWithCGImage:self.CGImage]; - self.faces = [self.detector featuresInImage:cgImage]; - } - - CGRect widestFaceRect = CGRectZero; - - // Index overrun protection. - if (self.nextFaceIndex >= self.faces.count) { - return CGRectZero; - } - - // Get face for nextFaceIndex. - widestFaceRect = ((CIFaceFeature*)self.faces[self.nextFaceIndex]).bounds; - - if (CGRectIsEmpty(widestFaceRect)) { - return CGRectZero; - } - - // Increment so next call will return next face. - self.nextFaceIndex++; - - // Reset if last face so next call shows first face. - if (self.nextFaceIndex == self.faces.count) { - self.nextFaceIndex = 0; - } - - CGRect faceImageCoords = - (CGRect){ - {widestFaceRect.origin.x, self.size.height - widestFaceRect.origin.y - widestFaceRect.size.height}, - widestFaceRect.size - }; - - return faceImageCoords; -} - -@end diff --git a/wikipedia/View Controllers/LeadImage/LeadImageContainer.m b/wikipedia/View Controllers/LeadImage/LeadImageContainer.m index c746ecb..2281414 100644 --- a/wikipedia/View Controllers/LeadImage/LeadImageContainer.m +++ b/wikipedia/View Controllers/LeadImage/LeadImageContainer.m @@ -10,11 +10,16 @@ #import "LeadImageTitleLabel.h" #import "UIScreen+Extras.h" #import "QueuesSingleton.h" -#import "FocalImage.h" +#import "WMFFaceDetector.h" #import "MWKArticle+isMain.h" #import "UIView+Debugging.h" +#import "WebViewController.h" +#import "URLCache.h" +#import "WMFGeometry.h" +#import "UIImage+WMFFocalImageDrawing.h" -#define PLACEHOLDER_IMAGE_ALPHA 0.3f +static const CGFloat kPlaceHolderImageAlpha = 0.3f; +static const CGFloat kMinimumAcceptableCachedVariantThreshold = 0.6f; /* When YES this causes lead image faces to be highlighted in green and @@ -23,32 +28,49 @@ Do *not* leave this set to YES for release. */ #if DEBUG -#define HIGHLIGHT_FOCAL_FACE 0 +#define ENABLE_FACE_DETECTION_DEBUGGING 0 #else // disable in release builds -#define HIGHLIGHT_FOCAL_FACE 0 +#define ENABLE_FACE_DETECTION_DEBUGGING 0 #endif @interface LeadImageContainer () +#pragma mark Private properties + @property (weak, nonatomic) IBOutlet UIView* titleDescriptionContainer; @property (weak, nonatomic) IBOutlet LeadImageTitleLabel* titleLabel; -@property (strong, nonatomic) FocalImage* image; -@property (nonatomic) CGRect focalFaceBounds; +@property (strong, nonatomic) UIImage* image; @property(strong, nonatomic) MWKArticle* article; @property (nonatomic) BOOL isPlaceholder; @property(strong, nonatomic) id rotationObserver; @property (nonatomic) CGFloat height; +@property (nonatomic) BOOL isFaceDetectionNeeded; +@property (strong, nonatomic) WMFFaceDetector* faceDetector; +@property (strong, nonatomic) NSData* placeholderImageData; +@property (nonatomic, strong) dispatch_queue_t serialFaceDetectionQueue; +@property (nonatomic) CGRect focalFaceBounds; +@property (nonatomic) BOOL shouldHighlightFace; @end @implementation LeadImageContainer +#pragma mark Setup + - (void)awakeFromNib { - self.height = LEAD_IMAGE_CONTAINER_HEIGHT; - self.isPlaceholder = NO; - self.clipsToBounds = YES; - self.backgroundColor = [UIColor clearColor]; + [self setupSerialFaceDetectionQueue]; + + self.focalFaceBounds = CGRectZero; + self.shouldHighlightFace = ENABLE_FACE_DETECTION_DEBUGGING; + self.image = nil; + self.faceDetector = [[WMFFaceDetector alloc] init]; + self.isFaceDetectionNeeded = NO; + self.height = LEAD_IMAGE_CONTAINER_HEIGHT; + self.isPlaceholder = NO; + self.clipsToBounds = YES; + self.backgroundColor = [UIColor clearColor]; + self.placeholderImageData = UIImagePNGRepresentation([UIImage imageNamed:@"lead-default"]); [self adjustConstraintsScaleForViews:@[self.titleLabel]]; self.rotationObserver = @@ -58,27 +80,66 @@ usingBlock:^(NSNotification* notification) { [self updateNonImageElements]; }]; - #if HIGHLIGHT_FOCAL_FACE - // Testing code so we can hit "Command-Shift-M" to toggle through focal images. - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification* note) { - // Repeated calls to getFaceBounds returns next face bounds each time. - self.focalFaceBounds = [self.image getFaceBounds]; - [self setNeedsDisplay]; - }]; + + #if ENABLE_FACE_DETECTION_DEBUGGING + [self debugSetupToggle]; #endif // Important! "clipsToBounds" must be "NO" so super long titles lay out properly! self.clipsToBounds = NO; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(webViewRetrievedAnImage:) name:@"SectionImageRetrieved" object:nil]; + //[self randomlyColorSubviews]; } -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self.rotationObserver]; +- (void)setupSerialFaceDetectionQueue { + // Low priority serial dispatch queue. From http://stackoverflow.com/a/17690878/135557 + // Images intercepted from web view need to have face detection ran + // serially to avoid running face detection more than necessary. + self.serialFaceDetectionQueue = dispatch_queue_create("org.wikimedia.wikipedia.LeadImageContainer.faceDetector.queue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_t low = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_set_target_queue(self.serialFaceDetectionQueue, low); } + +#pragma mark WebView image retrieval interception + +- (void)webViewRetrievedAnImage:(NSNotification*)notification { + // Notification received each time the web view retrieves an image. + + if (![NAV.topViewController isMemberOfClass:[WebViewController class]]) { + return; + } + + BOOL (^ notificationContainsImage)(NSNotification*) = ^BOOL (NSNotification* n) { + return ( + n.userInfo[kURLCacheKeyFileNameNoSizePrefix] + && + n.userInfo[kURLCacheKeyWidth] + && + n.userInfo[kURLCacheKeyData] + ); + }; + + if (notificationContainsImage(notification)) { + if ([self isRetrievedImageVariantOfLeadImage:notification.userInfo[kURLCacheKeyFileNameNoSizePrefix]]) { + if (self.isPlaceholder || [self isRetrievedImageWiderThanLeadImage:notification.userInfo[kURLCacheKeyWidth]]) { + NSLog(@"INTERCEPTED WEBVIEW IMAGE of width: %@", notification.userInfo[kURLCacheKeyWidth]); + [self showImage:notification.userInfo[kURLCacheKeyData] isPlaceHolder:NO]; + } + } + } +} + +- (BOOL)isRetrievedImageWiderThanLeadImage:(NSString*)retrievedImageWidth { + return (retrievedImageWidth.floatValue > self.image.size.width); +} + +- (BOOL)isRetrievedImageVariantOfLeadImage:(NSString*)retrievedImageNameNoSizePrefix { + return ([self.article.image.fileNameNoSizePrefix isEqualToString:retrievedImageNameNoSizePrefix]); +} + +#pragma mark Drawing - (void)drawRect:(CGRect)rect { [super drawRect:rect]; @@ -94,15 +155,15 @@ // the gradient will look smooth. [self drawGradientBackground]; - CGFloat alpha = self.isPlaceholder ? PLACEHOLDER_IMAGE_ALPHA : 1.0; + CGFloat alpha = self.isPlaceholder ? kPlaceHolderImageAlpha : 1.0; // Draw lead image, aspect fill, align top, vertically centering // focalFaceBounds face if necessary. - [self.image drawInRect:rect - focalBounds:self.focalFaceBounds - focalHighlight:HIGHLIGHT_FOCAL_FACE - blendMode:kCGBlendModeMultiply - alpha:alpha]; + [self.image wmf_drawInRect:rect + focalBounds:WMFRectFromUnitRectForReferenceSize(self.focalFaceBounds, self.image.size) + focalHighlight:self.shouldHighlightFace + blendMode:kCGBlendModeMultiply + alpha:alpha]; } - (void)drawGradientBackground { @@ -164,6 +225,8 @@ CGColorSpaceRelease(colorSpace); } +#pragma mark Layout + - (void)updateNonImageElements { // Updates title/description text color. [self updateTitleColors]; @@ -172,6 +235,14 @@ [self updateHeights]; [self setNeedsDisplay]; +} + +- (void)updateNonImageElementsIfNecessary { + if (!(self.isFaceDetectionNeeded && !self.isPlaceholder)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateNonImageElements]; + }); + } } - (void)updateHeights { @@ -207,6 +278,8 @@ self.titleLabel.shadowColor = shadowColor; } +#pragma mark Flags + - (BOOL)shouldHideImage { return UIInterfaceOrientationIsLandscape([[UIScreen mainScreen] interfaceOrientation]) @@ -222,79 +295,94 @@ return (url.pathExtension && [url.pathExtension isEqualToString:@"gif"]) ? YES : NO; } -- (void)showForArticle:(MWKArticle*)article { - self.article = article; - self.focalFaceBounds = CGRectZero; +#pragma mark Show +- (void)showForArticle:(MWKArticle*)article { + self.article = article; + self.focalFaceBounds = CGRectZero; self.titleLabel.imageExists = [self imageExists]; + self.image = nil; + self.isFaceDetectionNeeded = YES; if (self.article.isMain) { [self.titleLabel setTitle:@"" description:@""]; + [self updateNonImageElements]; + return; } else { NSString* title = [self.article.displaytitle getStringWithoutHTML]; [self.titleLabel setTitle:title description:[self getCurrentArticleDescription]]; } - if (!self.article.image.asUIImage && [self imageExists]) { - [self loadPlaceholderImage]; + // Show largest cached variant of lead image, or placeholder, immediately. + // This image is shown until the webview (potentially) retrieves higher resolution variants. + MWKImage* largestCachedVariant = self.article.image.largestCachedVariant; + if (largestCachedVariant) { + NSLog(@"SHOWING LARGEST CACHED VARIANT of width: %f", largestCachedVariant.width.floatValue); + [self showImage:[largestCachedVariant asNSData] isPlaceHolder:NO]; + } else { + [self showImage:self.placeholderImageData isPlaceHolder:YES]; + } + if (![self isLargestCachedVariantSufficient:largestCachedVariant]) { (void)[[ThumbnailFetcher alloc] initAndFetchThumbnailFromURL:[@"http:" stringByAppendingString:self.article.imageURL] withManager:[QueuesSingleton sharedInstance].articleFetchManager thenNotifyDelegate:self]; - } else { - [self showImage]; } } -- (void)loadPlaceholderImage { - FocalImage* placeholderImage = - [[FocalImage alloc] initWithCGImage:[UIImage imageNamed:@"lead-default"].CGImage]; - self.isPlaceholder = YES; - self.image = placeholderImage; - [self updateNonImageElements]; -} - -- (void)showImage { - UIImage* img = self.article.image.asUIImage; - - FocalImage* image = [[FocalImage alloc] initWithCGImage:img.CGImage]; - - // Biggest face. - self.focalFaceBounds = [image getFaceBounds]; - - //NSLog(@"Requested lead image width = %d", LEAD_IMAGE_WIDTH); - //NSLog(@"Returned lead image width = %d", self.article.image.width.integerValue); - //NSLog(@"percent = %f", self.article.image.width.floatValue / LEAD_IMAGE_WIDTH); - - /* - Note: this doesn't work as-is. Would need to listen for "SectionImageRetrieved" - notifications because article images are late-arriving. - - Use first article image if no image at this point. - - Should probably only do this if the image above is going to be greatly - cropped *and* no faces were detected. Then, instead of loading first image - of minimum size, as noted below, use first image of sufficient area which - would not need to be aggresively cropped. - - Note: commented this out because I'm not sure it ever gets called. - Would also probably want to only use the first article image if it's - bigger than some minimum size. - - if(!img){ - MWKImage *firstArticleImage = self.article.sections[0].images[0]; - UIImage *firstUIImage = firstArticleImage.asUIImage; - if(firstUIImage){ - img = firstUIImage; +- (BOOL)isLargestCachedVariantSufficient:(MWKImage*)largestCachedVariant { + if (![largestCachedVariant isEqualToImage:self.article.image]) { + CGFloat okMinimumWidth = LEAD_IMAGE_WIDTH * kMinimumAcceptableCachedVariantThreshold; + if (largestCachedVariant.width.floatValue < okMinimumWidth) { + if (self.article.imageURL) { + NSInteger widestExpectedImageWidth = [self widthOfWidestVariantWebViewWillDownload]; + if (widestExpectedImageWidth < okMinimumWidth) { + return NO; + } + } } - } - */ - - self.isPlaceholder = NO; - self.image = image; - - [self updateNonImageElements]; + } + return YES; } + +- (void)showImage:(NSData*)retrievedImageData isPlaceHolder:(BOOL)isPlaceHolder { + self.isPlaceholder = isPlaceHolder; + + // Face detection is faster if the UIImage has CIImage backing. + CIImage* ciImage = [[CIImage alloc] initWithData:retrievedImageData]; + self.image = [UIImage imageWithCIImage:ciImage]; + + [self detectFaceIfNecessary]; + [self updateNonImageElementsIfNecessary]; +} + +- (NSInteger)widthOfWidestVariantWebViewWillDownload { + MWKImage* widestUncachedVariant = nil; + NSArray* arr = [self.article.images imageSizeVariants:self.article.imageURL]; + for (NSString* variantURL in [arr reverseObjectEnumerator]) { + MWKImage* image = [self.article imageWithURL:variantURL]; + // Must exclude article.image because it is not retrieved by the web view + // (it's the thing we're deciding if we need to download!) + if (![image isEqualToImage:self.article.image]) { + if (!image.isCached) { + widestUncachedVariant = image; + break; + } + } + } + if (widestUncachedVariant) { + // Parse the width out of the url - necessary because the image probably hasn't been + // retrieved yet, so width and height properties won't be set yet. + // Note: occasionally images don't have size prefix in their file name, so for these + // images we won't be able to divine ahead of time whether among the images to be + // downloaded by the webview there will be one of sufficient resolution. In these + // cases it's ok because the higher res image will be fetched with the ThumbnailFetcher. + return [MWKImage fileSizePrefix:widestUncachedVariant.sourceURL]; + } + return -1; +} + +#pragma mark Fetch finished - (void)fetchFinished:(id)sender fetchedData:(id)fetchedData @@ -304,14 +392,18 @@ switch (status) { case FETCH_FINAL_STATUS_SUCCEEDED: { - // Associate the image retrieved with article.image. - ThumbnailFetcher* fetcher = (ThumbnailFetcher*)sender; - NSString* thumbnailURL = [fetcher.url getUrlWithoutScheme]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + // Associate the image retrieved with article.image. + ThumbnailFetcher* fetcher = (ThumbnailFetcher*)sender; + NSString* thumbnailURL = [fetcher.url getUrlWithoutScheme]; - MWKImage* articleImage = [[MWKImage alloc] initWithArticle:self.article sourceURL:thumbnailURL]; - [articleImage importImageData:fetchedData]; + MWKImage* articleImage = [[MWKImage alloc] initWithArticle:self.article sourceURL:thumbnailURL]; + [articleImage importImageData:fetchedData]; - [self showImage]; + NSLog(@"FETCHED HIGHER RES VARIANT of width: %f", articleImage.width.floatValue); + + [self showImage:[articleImage asNSData] isPlaceHolder:NO]; + }); } break; case FETCH_FINAL_STATUS_FAILED: @@ -326,6 +418,8 @@ } } +#pragma mark Description + - (NSString*)getCurrentArticleDescription { NSString* description = self.article.entityDescription; if (description) { @@ -335,4 +429,72 @@ return description; } +#pragma mark Face detection + +- (void)detectFaceIfNecessary { + if (self.isFaceDetectionNeeded && !self.isPlaceholder) { + UIImage* imageToDetect = self.image; // Ensure async block is working on this size variant. + + dispatch_async(self.serialFaceDetectionQueue, ^{ // Important that this is a serial queue! + //NSLog(@"Face detection block ran for image of size = %@", NSStringFromCGSize(imageToDetect.size)); + + if (self.isFaceDetectionNeeded) { // Re-check in case it changed since block was dispatched. + self.faceDetector.image = imageToDetect; + CGRect faceBounds = [self.faceDetector detectFace]; + + //NSLog(@"FACE DETECTION ACTUALLY RAN FOR IMAGE SIZE = %@", NSStringFromCGSize(imageToDetect.size)); + + BOOL faceDetected = !CGRectIsEmpty(faceBounds); + + // Store as unit rect so we don't have to re-run face detection on subsequent size + self.focalFaceBounds = WMFUnitRectFromRectForReferenceSize(faceBounds, imageToDetect.size); + + if (faceDetected) { + //NSLog(@"FACE FOUND FOR IMAGE SIZE = %@", NSStringFromCGSize(imageToDetect.size)); + + self.isFaceDetectionNeeded = NO; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateNonImageElements]; + }); + }); + } +} + +#pragma mark Easy face detection debugging + +- (void)debugSetupToggle { + // Testing code so we can hit "Command-Shift-M" to toggle through focal images. + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* notification) { + [self debugDetectNextFace]; + }]; +} + +- (void)debugDetectNextFace { + // Ensure detector is set to last image retrieved. Detector may have + // successfully detected large face in an earlier low res image, but + // current image may be higher res. See "Madonna del Granduca" enwiki + // article. Without this only the mother's face available in cycle as + // it is the only one detected when the first low-res variant is + // retrieved. + if (self.faceDetector.image != self.image) { + self.faceDetector.image = self.image; + } + + // Repeated calls to detectNextFace returns next face bounds each time. + self.focalFaceBounds = WMFUnitRectFromRectForReferenceSize([self.faceDetector detectFace], self.faceDetector.image.size); + [self setNeedsDisplay]; +} + +#pragma mark Dealloc + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self.rotationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"SectionImageRetrieved" object:nil]; +} + @end diff --git a/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m b/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m index 81e46be..6f16178 100644 --- a/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m +++ b/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m @@ -499,7 +499,7 @@ break; } } else if ([sender isKindOfClass:[WikiTextSectionUploader class]]) { - WikiTextSectionUploader* uploader = (WikiTextSectionUploader*)sender; + //WikiTextSectionUploader* uploader = (WikiTextSectionUploader*)sender; switch (status) { case FETCH_FINAL_STATUS_SUCCEEDED: { diff --git a/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m b/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m index ce4055d..82f1031 100644 --- a/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m +++ b/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m @@ -429,11 +429,8 @@ } - (void)resumeRefresh { - [[SavedArticlesFetcher sharedInstance] getProgress:^(CGFloat progress) { - - if(progress < 100){ - + if (progress < 100) { self.progressView.progress = progress; [SavedArticlesFetcher sharedInstance].fetchFinishedDelegate = self; diff --git a/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.h b/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.h index 154f417..8745e0f 100644 --- a/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.h +++ b/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.h @@ -2,9 +2,7 @@ #import <UIKit/UIKit.h> -@class FocalImage; - @interface WMFShareCardImageContainer : UIView -@property (strong, nonatomic) FocalImage* image; +@property (strong, nonatomic) UIImage* image; @end diff --git a/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.m b/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.m index 73cd08b..3dcf4f6 100644 --- a/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.m +++ b/wikipedia/View Controllers/ShareCard/WMFShareCardImageContainer.m @@ -1,19 +1,41 @@ #import "WMFShareCardImageContainer.h" -#import "FocalImage.h" +#import "UIImage+WMFFocalImageDrawing.h" +#import "WMFFaceDetector.h" +@interface WMFShareCardImageContainer () + +@property(nonatomic, strong) WMFFaceDetector* faceDetector; +@property(nonatomic) CGRect faceBounds; + +@end @implementation WMFShareCardImageContainer + +- (instancetype)initWithCoder:(NSCoder*)coder { + self = [super initWithCoder:coder]; + if (self) { + self.faceDetector = [[WMFFaceDetector alloc] init]; + } + return self; +} + +- (void)setImage:(UIImage*)image { + _image = image; + self.faceDetector.image = image; + self.faceBounds = [self.faceDetector detectFace]; +} - (void)drawRect:(CGRect)rect { [super drawRect:rect]; [self drawGradientBackground]; - [self.image drawInRect:rect - focalBounds:[self.image getFaceBounds] - focalHighlight:NO - blendMode:kCGBlendModeMultiply - alpha:1.0]; + + [self.image wmf_drawInRect:rect + focalBounds:self.faceBounds + focalHighlight:NO + blendMode:kCGBlendModeMultiply + alpha:1.0]; } // TODO: in follow-up patch, factor drawGradientBackground from diff --git a/wikipedia/View Controllers/ShareCard/WMFShareCardViewController.m b/wikipedia/View Controllers/ShareCard/WMFShareCardViewController.m index 481d89f..6d14137 100644 --- a/wikipedia/View Controllers/ShareCard/WMFShareCardViewController.m +++ b/wikipedia/View Controllers/ShareCard/WMFShareCardViewController.m @@ -7,7 +7,6 @@ // #import "WMFShareCardViewController.h" -#import "FocalImage.h" #import "NSString+Extras.h" #import "WMFShareCardImageContainer.h" #import "MWLanguageInfo.h" @@ -59,11 +58,13 @@ self.shareArticleDescription.text = [[article.entityDescription getStringWithoutHTML] capitalizeFirstLetter]; self.shareArticleDescription.textAlignment = subtextAlignment; - UIImage* leadImage = [article.image asUIImage]; - if (leadImage) { + NSData* leadImageData = [article.image.largestCachedVariant asNSData]; + if (leadImageData) { // in case the image has transparency, make its container white self.shareCardImageContainer.backgroundColor = [UIColor whiteColor]; - self.shareCardImageContainer.image = [[FocalImage alloc] initWithCGImage:leadImage.CGImage]; + // Face detection is faster if the image has CIImage backing. + CIImage* ciImage = [[CIImage alloc] initWithData:leadImageData]; + self.shareCardImageContainer.image = [UIImage imageWithCIImage:ciImage]; } } diff --git a/wikipedia/Web Image Interception/URLCache.h b/wikipedia/Web Image Interception/URLCache.h index b9d33b0..148fdef 100644 --- a/wikipedia/Web Image Interception/URLCache.h +++ b/wikipedia/Web Image Interception/URLCache.h @@ -3,6 +3,13 @@ #import <Foundation/Foundation.h> +extern NSString* const kURLCacheKeyFileName; +extern NSString* const kURLCacheKeyData; +extern NSString* const kURLCacheKeyWidth; +extern NSString* const kURLCacheKeyHeight; +extern NSString* const kURLCacheKeyURL; +extern NSString* const kURLCacheKeyFileNameNoSizePrefix; + @interface URLCache : NSURLCache @end diff --git a/wikipedia/Web Image Interception/URLCache.m b/wikipedia/Web Image Interception/URLCache.m index acf7723..d64248f 100644 --- a/wikipedia/Web Image Interception/URLCache.m +++ b/wikipedia/Web Image Interception/URLCache.m @@ -2,11 +2,15 @@ // Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; please copy and modify! #import "URLCache.h" -//#import "ArticleDataContextSingleton.h" -//#import "ArticleCoreDataObjects.h" -//#import "NSManagedObjectContext+SimpleFetch.h" #import "NSString+Extras.h" #import "SessionSingleton.h" + +NSString* const kURLCacheKeyFileName = @"fileName"; +NSString* const kURLCacheKeyData = @"data"; +NSString* const kURLCacheKeyWidth = @"width"; +NSString* const kURLCacheKeyHeight = @"height"; +NSString* const kURLCacheKeyURL = @"url"; +NSString* const kURLCacheKeyFileNameNoSizePrefix = @"fileNameNoSizePrefix"; #if 0 #define URLCacheLog(...) NSLog(__VA_ARGS__) @@ -133,8 +137,12 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"SectionImageRetrieved" object:nil userInfo:@{ - @"fileName": image.fileName, - @"data": imageDataToUse, + kURLCacheKeyFileName: image.fileName, + kURLCacheKeyData: imageDataToUse, + kURLCacheKeyWidth: image.width, + kURLCacheKeyHeight: image.height, + kURLCacheKeyURL: image.sourceURL, + kURLCacheKeyFileNameNoSizePrefix: image.fileNameNoSizePrefix }]; } diff --git a/wikipedia/en.lproj/Main_iPhone.strings b/wikipedia/en.lproj/Main_iPhone.strings index 9349c6d..f670b7e 100644 --- a/wikipedia/en.lproj/Main_iPhone.strings +++ b/wikipedia/en.lproj/Main_iPhone.strings @@ -1,75 +1,75 @@ -/* Class = "IBUIButton"; normalTitle = "Show another captcha"; ObjectID = "21c-U6-yfo"; */ +/* Class = "UIButton"; normalTitle = "Show another captcha"; ObjectID = "21c-U6-yfo"; */ "21c-U6-yfo.normalTitle" = "Show another captcha"; -/* Class = "IBUITextField"; placeholder = "User name"; ObjectID = "5cT-2Y-0Ie"; */ +/* Class = "UITextField"; placeholder = "User name"; ObjectID = "5cT-2Y-0Ie"; */ "5cT-2Y-0Ie.placeholder" = "User name"; -/* Class = "IBUILabel"; text = "Saved pages are pretty awesome. Think of them as bookmarks that you can read even when you are offline."; ObjectID = "GiD-Rj-wb7"; */ +/* Class = "UILabel"; text = "Saved pages are pretty awesome. Think of them as bookmarks that you can read even when you are offline."; ObjectID = "GiD-Rj-wb7"; */ "GiD-Rj-wb7.text" = "Saved pages are pretty awesome. Think of them as bookmarks that you can read even when you are offline."; -/* Class = "IBUILabel"; text = "Preview"; ObjectID = "LfM-01-aCF"; */ +/* Class = "UILabel"; text = "Preview"; ObjectID = "LfM-01-aCF"; */ "LfM-01-aCF.text" = "Preview"; -/* Class = "IBUILabel"; text = "Skip"; ObjectID = "P6J-IE-CiO"; */ +/* Class = "UILabel"; text = "Skip"; ObjectID = "P6J-IE-CiO"; */ "P6J-IE-CiO.text" = "Skip"; -/* Class = "IBUITextField"; placeholder = "Password"; ObjectID = "PCr-0J-fBj"; */ +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "PCr-0J-fBj"; */ "PCr-0J-fBj.placeholder" = "Password"; -/* Class = "IBUILabel"; text = "Canonical Language"; ObjectID = "SER-n4-DZC"; */ +/* Class = "UILabel"; text = "Canonical Language"; ObjectID = "SER-n4-DZC"; */ "SER-n4-DZC.text" = "Canonical Language"; -/* Class = "IBUITextField"; placeholder = "Re-type password"; ObjectID = "UgH-77-lyp"; */ +/* Class = "UITextField"; placeholder = "Re-type password"; ObjectID = "UgH-77-lyp"; */ "UgH-77-lyp.placeholder" = "Re-type password"; -/* Class = "IBUILabel"; text = "No saved pages yet."; ObjectID = "W1c-wQ-1pa"; */ +/* Class = "UILabel"; text = "No saved pages yet."; ObjectID = "W1c-wQ-1pa"; */ "W1c-wQ-1pa.text" = "No saved pages yet."; -/* Class = "IBUILabel"; text = "Log in"; ObjectID = "Wff-o9-AdH"; */ +/* Class = "UILabel"; text = "Log in"; ObjectID = "Wff-o9-AdH"; */ "Wff-o9-AdH.text" = "Log in"; -/* Class = "IBUILabel"; text = "Label"; ObjectID = "XkB-Xo-Xq0"; */ +/* Class = "UILabel"; text = "Label"; ObjectID = "XkB-Xo-Xq0"; */ "XkB-Xo-Xq0.text" = "Label"; -/* Class = "IBUILabel"; text = "Test Zero Label Text"; ObjectID = "aCV-ih-PXn"; */ +/* Class = "UILabel"; text = "Test Zero Label Text"; ObjectID = "aCV-ih-PXn"; */ "aCV-ih-PXn.text" = "Test Zero Label Text"; -/* Class = "IBUILabel"; text = "No recent pages here."; ObjectID = "aUp-0e-F6i"; */ +/* Class = "UILabel"; text = "No recent pages here."; ObjectID = "aUp-0e-F6i"; */ "aUp-0e-F6i.text" = "No recent pages here."; -/* Class = "IBUILabel"; text = "Create Account"; ObjectID = "c3c-PU-Exz"; */ +/* Class = "UILabel"; text = "Create Account"; ObjectID = "c3c-PU-Exz"; */ "c3c-PU-Exz.text" = "Create Account"; -/* Class = "IBUILabel"; text = "Label"; ObjectID = "cbH-8H-z54"; */ +/* Class = "UILabel"; text = "Label"; ObjectID = "cbH-8H-z54"; */ "cbH-8H-z54.text" = "Label"; -/* Class = "IBUITextField"; placeholder = "Enter CAPTCHA text from image above"; ObjectID = "gPg-cg-Yjy"; */ +/* Class = "UITextField"; placeholder = "Enter CAPTCHA text from image above"; ObjectID = "gPg-cg-Yjy"; */ "gPg-cg-Yjy.placeholder" = "Enter CAPTCHA text from image above"; -/* Class = "IBUILabel"; text = "Already have an account? Log in"; ObjectID = "heA-3K-nhS"; */ +/* Class = "UILabel"; text = "Already have an account? Log in"; ObjectID = "heA-3K-nhS"; */ "heA-3K-nhS.text" = "Already have an account? Log in"; -/* Class = "IBUILabel"; text = "You probably deleted all of them. Next time you go to a page you can get back to it from here."; ObjectID = "huU-kO-aYI"; */ +/* Class = "UILabel"; text = "You probably deleted all of them. Next time you go to a page you can get back to it from here."; ObjectID = "huU-kO-aYI"; */ "huU-kO-aYI.text" = "You probably deleted all of them. Next time you go to a page you can get back to it from here."; -/* Class = "IBUILabel"; text = "Create Account"; ObjectID = "jiW-Cg-oL3"; */ +/* Class = "UILabel"; text = "Create Account"; ObjectID = "jiW-Cg-oL3"; */ "jiW-Cg-oL3.text" = "Create Account"; -/* Class = "IBUILabel"; text = "Language"; ObjectID = "jxY-ej-I9e"; */ +/* Class = "UILabel"; text = "Language"; ObjectID = "jxY-ej-I9e"; */ "jxY-ej-I9e.text" = "Language"; -/* Class = "IBUITextField"; placeholder = "Password"; ObjectID = "kVb-lx-d6C"; */ +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "kVb-lx-d6C"; */ "kVb-lx-d6C.placeholder" = "Password"; -/* Class = "IBUITextField"; placeholder = "User name"; ObjectID = "mAk-1N-jPC"; */ +/* Class = "UITextField"; placeholder = "User name"; ObjectID = "mAk-1N-jPC"; */ "mAk-1N-jPC.placeholder" = "User name"; -/* Class = "IBUILabel"; text = "Label"; ObjectID = "nI1-bn-0Ii"; */ +/* Class = "UILabel"; text = "Label"; ObjectID = "nI1-bn-0Ii"; */ "nI1-bn-0Ii.text" = "Label"; -/* Class = "IBUITextField"; placeholder = "Email address (optional)"; ObjectID = "rKI-nq-3p7"; */ +/* Class = "UITextField"; placeholder = "Email address (optional)"; ObjectID = "rKI-nq-3p7"; */ "rKI-nq-3p7.placeholder" = "Email address (optional)"; -/* Class = "IBUILabel"; text = "Create account"; ObjectID = "wkl-j8-wLX"; */ +/* Class = "UILabel"; text = "Create account"; ObjectID = "wkl-j8-wLX"; */ "wkl-j8-wLX.text" = "Create account"; -- To view, visit https://gerrit.wikimedia.org/r/193323 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I979f3a07c7d37febd5b7d9f99a8105223f2525b3 Gerrit-PatchSet: 23 Gerrit-Project: apps/ios/wikipedia Gerrit-Branch: master Gerrit-Owner: Mhurd <mh...@wikimedia.org> Gerrit-Reviewer: Bgerstle <bgers...@wikimedia.org> Gerrit-Reviewer: Dr0ptp4kt <ab...@wikimedia.org> Gerrit-Reviewer: Fjalapeno <cfl...@wikimedia.org> Gerrit-Reviewer: Mhurd <mh...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits