| /* |
| * Copyright (C) 2014, 2016 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "WKFileUploadPanel.h" |
| |
| #if PLATFORM(IOS) |
| |
| #import "APIArray.h" |
| #import "APIData.h" |
| #import "APIOpenPanelParameters.h" |
| #import "APIString.h" |
| #import "PhotosSPI.h" |
| #import "UIKitSPI.h" |
| #import "WKContentViewInteraction.h" |
| #import "WKData.h" |
| #import "WKStringCF.h" |
| #import "WKURLCF.h" |
| #import "WebOpenPanelResultListenerProxy.h" |
| #import "WebPageProxy.h" |
| #import <AVFoundation/AVFoundation.h> |
| #import <CoreMedia/CoreMedia.h> |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <WebCore/LocalizedStrings.h> |
| #import <WebCore/SoftLinking.h> |
| #import <WebKit/WebNSFileManagerExtras.h> |
| #import <wtf/RetainPtr.h> |
| |
| using namespace WebKit; |
| |
| SOFT_LINK_FRAMEWORK(AVFoundation); |
| SOFT_LINK_CLASS(AVFoundation, AVAssetImageGenerator); |
| SOFT_LINK_CLASS(AVFoundation, AVURLAsset); |
| |
| SOFT_LINK_FRAMEWORK(CoreMedia); |
| SOFT_LINK_CONSTANT(CoreMedia, kCMTimeZero, CMTime); |
| |
| SOFT_LINK_FRAMEWORK(Photos); |
| SOFT_LINK_CLASS(Photos, PHAsset); |
| SOFT_LINK_CLASS(Photos, PHImageManager); |
| SOFT_LINK_CLASS(Photos, PHImageRequestOptions); |
| SOFT_LINK_CONSTANT(Photos, PHImageRequestOptionsResizeModeNone, NSString *); |
| SOFT_LINK_CONSTANT(Photos, PHImageRequestOptionsVersionCurrent, NSString *); |
| |
| #define kCMTimeZero getkCMTimeZero() |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| |
| static inline UIImagePickerControllerCameraDevice cameraDeviceForMediaCaptureType(WebCore::MediaCaptureType mediaCaptureType) |
| { |
| return mediaCaptureType == WebCore::MediaCaptureTypeUser ? UIImagePickerControllerCameraDeviceFront : UIImagePickerControllerCameraDeviceRear; |
| } |
| |
| #pragma mark - Document picker icons |
| |
| static inline UIImage *photoLibraryIcon() |
| { |
| return _UIImageGetWebKitPhotoLibraryIcon(); |
| } |
| |
| static inline UIImage *cameraIcon() |
| { |
| return _UIImageGetWebKitTakePhotoOrVideoIcon(); |
| } |
| |
| #pragma mark - Icon generation |
| |
| static const CGFloat iconSideLength = 100; |
| |
| static CGRect squareCropRectForSize(CGSize size) |
| { |
| CGFloat smallerSide = MIN(size.width, size.height); |
| CGRect cropRect = CGRectMake(0, 0, smallerSide, smallerSide); |
| |
| if (size.width < size.height) |
| cropRect.origin.y = std::round((size.height - smallerSide) / 2); |
| else |
| cropRect.origin.x = std::round((size.width - smallerSide) / 2); |
| |
| return cropRect; |
| } |
| |
| static UIImage *squareImage(CGImageRef image) |
| { |
| if (!image) |
| return nil; |
| |
| CGSize imageSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image)); |
| if (imageSize.width == imageSize.height) |
| return [UIImage imageWithCGImage:image]; |
| |
| CGRect squareCropRect = squareCropRectForSize(imageSize); |
| RetainPtr<CGImageRef> squareImage = adoptCF(CGImageCreateWithImageInRect(image, squareCropRect)); |
| return [UIImage imageWithCGImage:squareImage.get()]; |
| } |
| |
| static UIImage *thumbnailSizedImageForImage(CGImageRef image) |
| { |
| UIImage *squaredImage = squareImage(image); |
| if (!squaredImage) |
| return nil; |
| |
| CGRect destRect = CGRectMake(0, 0, iconSideLength, iconSideLength); |
| UIGraphicsBeginImageContext(destRect.size); |
| CGContextSetInterpolationQuality(UIGraphicsGetCurrentContext(), kCGInterpolationHigh); |
| [squaredImage drawInRect:destRect]; |
| UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); |
| UIGraphicsEndImageContext(); |
| return resultImage; |
| } |
| |
| static UIImage* fallbackIconForFile(NSURL *file) |
| { |
| ASSERT_ARG(file, [file isFileURL]); |
| |
| UIDocumentInteractionController *interactionController = [UIDocumentInteractionController interactionControllerWithURL:file]; |
| return thumbnailSizedImageForImage(interactionController.icons[0].CGImage); |
| } |
| |
| static UIImage* iconForImageFile(NSURL *file) |
| { |
| ASSERT_ARG(file, [file isFileURL]); |
| |
| NSDictionary *options = @{ |
| (id)kCGImageSourceCreateThumbnailFromImageIfAbsent: @YES, |
| (id)kCGImageSourceThumbnailMaxPixelSize: @(iconSideLength), |
| (id)kCGImageSourceCreateThumbnailWithTransform: @YES, |
| }; |
| RetainPtr<CGImageSource> imageSource = adoptCF(CGImageSourceCreateWithURL((CFURLRef)file, 0)); |
| RetainPtr<CGImageRef> thumbnail = adoptCF(CGImageSourceCreateThumbnailAtIndex(imageSource.get(), 0, (CFDictionaryRef)options)); |
| if (!thumbnail) { |
| LOG_ERROR("WKFileUploadPanel: Error creating thumbnail image for image: %@", file); |
| return fallbackIconForFile(file); |
| } |
| |
| return thumbnailSizedImageForImage(thumbnail.get()); |
| } |
| |
| static UIImage* iconForVideoFile(NSURL *file) |
| { |
| ASSERT_ARG(file, [file isFileURL]); |
| |
| RetainPtr<AVURLAsset> asset = adoptNS([allocAVURLAssetInstance() initWithURL:file options:nil]); |
| RetainPtr<AVAssetImageGenerator> generator = adoptNS([allocAVAssetImageGeneratorInstance() initWithAsset:asset.get()]); |
| [generator setAppliesPreferredTrackTransform:YES]; |
| |
| NSError *error = nil; |
| RetainPtr<CGImageRef> imageRef = adoptCF([generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:&error]); |
| if (!imageRef) { |
| LOG_ERROR("WKFileUploadPanel: Error creating image for video '%@': %@", file, error); |
| return fallbackIconForFile(file); |
| } |
| |
| return thumbnailSizedImageForImage(imageRef.get()); |
| } |
| |
| static UIImage* iconForFile(NSURL *file) |
| { |
| ASSERT_ARG(file, [file isFileURL]); |
| |
| NSString *fileExtension = file.pathExtension; |
| if (!fileExtension.length) |
| return nil; |
| |
| RetainPtr<CFStringRef> fileUTI = adoptCF(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)fileExtension, 0)); |
| |
| if (UTTypeConformsTo(fileUTI.get(), kUTTypeImage)) |
| return iconForImageFile(file); |
| |
| if (UTTypeConformsTo(fileUTI.get(), kUTTypeMovie)) |
| return iconForVideoFile(file); |
| |
| return fallbackIconForFile(file); |
| } |
| |
| |
| #pragma mark - _WKFileUploadItem |
| |
| @interface _WKFileUploadItem : NSObject |
| - (instancetype)initWithFileURL:(NSURL *)fileURL; |
| @property (nonatomic, readonly, getter=isVideo) BOOL video; |
| @property (nonatomic, readonly) NSURL *fileURL; |
| @property (nonatomic, readonly) UIImage *displayImage; |
| @end |
| |
| @implementation _WKFileUploadItem { |
| RetainPtr<NSURL> _fileURL; |
| } |
| |
| - (instancetype)initWithFileURL:(NSURL *)fileURL |
| { |
| if (!(self = [super init])) |
| return nil; |
| |
| ASSERT([fileURL isFileURL]); |
| ASSERT([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]); |
| _fileURL = fileURL; |
| return self; |
| } |
| |
| - (BOOL)isVideo |
| { |
| ASSERT_NOT_REACHED(); |
| return NO; |
| } |
| |
| - (NSURL *)fileURL |
| { |
| return _fileURL.get(); |
| } |
| |
| - (UIImage *)displayImage |
| { |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| @end |
| |
| |
| @interface _WKImageFileUploadItem : _WKFileUploadItem |
| @end |
| |
| @implementation _WKImageFileUploadItem |
| |
| - (BOOL)isVideo |
| { |
| return NO; |
| } |
| |
| - (UIImage *)displayImage |
| { |
| return iconForImageFile(self.fileURL); |
| } |
| |
| @end |
| |
| |
| @interface _WKVideoFileUploadItem : _WKFileUploadItem |
| @end |
| |
| @implementation _WKVideoFileUploadItem |
| |
| - (BOOL)isVideo |
| { |
| return YES; |
| } |
| |
| - (UIImage *)displayImage |
| { |
| return iconForVideoFile(self.fileURL); |
| } |
| |
| @end |
| |
| |
| #pragma mark - WKFileUploadPanel |
| |
| @interface WKFileUploadPanel () <UIPopoverControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, UIDocumentMenuDelegate> |
| @end |
| |
| @implementation WKFileUploadPanel { |
| WKContentView *_view; |
| RefPtr<WebKit::WebOpenPanelResultListenerProxy> _listener; |
| RetainPtr<NSArray> _mimeTypes; |
| CGPoint _interactionPoint; |
| BOOL _allowMultipleFiles; |
| BOOL _usingCamera; |
| RetainPtr<UIImagePickerController> _imagePicker; |
| RetainPtr<UIViewController> _presentationViewController; // iPhone always. iPad for Fullscreen Camera. |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| RetainPtr<UIPopoverController> _presentationPopover; // iPad for action sheet and Photo Library. |
| #pragma clang diagnostic pop |
| RetainPtr<UIDocumentMenuViewController> _documentMenuController; |
| RetainPtr<UIAlertController> _actionSheetController; |
| WebCore::MediaCaptureType _mediaCaptureType; |
| } |
| |
| - (instancetype)initWithView:(WKContentView *)view |
| { |
| if (!(self = [super init])) |
| return nil; |
| _view = view; |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| [_imagePicker setDelegate:nil]; |
| [_presentationPopover setDelegate:nil]; |
| [_documentMenuController setDelegate:nil]; |
| |
| [super dealloc]; |
| } |
| |
| - (void)_dispatchDidDismiss |
| { |
| if ([_delegate respondsToSelector:@selector(fileUploadPanelDidDismiss:)]) |
| [_delegate fileUploadPanelDidDismiss:self]; |
| } |
| |
| #pragma mark - Panel Completion (one of these must be called) |
| |
| - (void)_cancel |
| { |
| _listener->cancel(); |
| [self _dispatchDidDismiss]; |
| } |
| |
| - (void)_chooseFiles:(NSArray *)fileURLs displayString:(NSString *)displayString iconImage:(UIImage *)iconImage |
| { |
| NSUInteger count = [fileURLs count]; |
| if (!count) { |
| [self _cancel]; |
| return; |
| } |
| |
| Vector<String> filenames; |
| filenames.reserveInitialCapacity(count); |
| for (NSURL *fileURL in fileURLs) |
| filenames.uncheckedAppend(fileURL.fileSystemRepresentation); |
| |
| NSData *jpeg = UIImageJPEGRepresentation(iconImage, 1.0); |
| RefPtr<API::Data> iconImageDataRef = adoptRef(toImpl(WKDataCreate(reinterpret_cast<const unsigned char*>([jpeg bytes]), [jpeg length]))); |
| |
| _listener->chooseFiles(filenames, displayString, iconImageDataRef.get()); |
| [self _dispatchDidDismiss]; |
| } |
| |
| #pragma mark - Present / Dismiss API |
| |
| - (void)presentWithParameters:(API::OpenPanelParameters*)parameters resultListener:(WebKit::WebOpenPanelResultListenerProxy*)listener |
| { |
| ASSERT(!_listener); |
| |
| _listener = listener; |
| _allowMultipleFiles = parameters->allowMultipleFiles(); |
| _interactionPoint = [_view lastInteractionLocation]; |
| |
| Ref<API::Array> acceptMimeTypes = parameters->acceptMIMETypes(); |
| NSMutableArray *mimeTypes = [NSMutableArray arrayWithCapacity:acceptMimeTypes->size()]; |
| for (const auto& mimeType : acceptMimeTypes->elementsOfType<API::String>()) |
| [mimeTypes addObject:mimeType->string()]; |
| _mimeTypes = adoptNS([mimeTypes copy]); |
| |
| _mediaCaptureType = WebCore::MediaCaptureTypeNone; |
| #if ENABLE(MEDIA_CAPTURE) |
| _mediaCaptureType = parameters->mediaCaptureType(); |
| #endif |
| |
| if ([self _shouldMediaCaptureOpenMediaDevice]) { |
| [self _adjustMediaCaptureType]; |
| |
| _usingCamera = YES; |
| [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypeCamera]; |
| |
| return; |
| } |
| |
| [self _showDocumentPickerMenu]; |
| } |
| |
| - (void)dismiss |
| { |
| [self _dismissDisplayAnimated:NO]; |
| [self _cancel]; |
| } |
| |
| - (void)_dismissDisplayAnimated:(BOOL)animated |
| { |
| if (_presentationPopover) { |
| [_presentationPopover dismissPopoverAnimated:animated]; |
| [_presentationPopover setDelegate:nil]; |
| _presentationPopover = nil; |
| } |
| |
| if (_presentationViewController) { |
| [_presentationViewController dismissViewControllerAnimated:animated completion:^{ |
| _presentationViewController = nil; |
| }]; |
| } |
| } |
| |
| #pragma mark - Media Types |
| |
| static bool stringHasPrefixCaseInsensitive(NSString *str, NSString *prefix) |
| { |
| NSRange range = [str rangeOfString:prefix options:(NSCaseInsensitiveSearch | NSAnchoredSearch)]; |
| return range.location != NSNotFound; |
| } |
| |
| static NSArray *UTIsForMIMETypes(NSArray *mimeTypes) |
| { |
| // The HTML5 spec mentions the literal "image/*" and "video/*" strings. |
| // We support these and go a step further, if the MIME type starts with |
| // "image/" or "video/" we adjust the picker's image or video filters. |
| // So, "image/jpeg" would make the picker display all images types. |
| NSMutableSet *mediaTypes = [NSMutableSet set]; |
| for (NSString *mimeType in mimeTypes) { |
| // FIXME: We should support more MIME type -> UTI mappings. <http://webkit.org/b/142614> |
| if (stringHasPrefixCaseInsensitive(mimeType, @"image/")) |
| [mediaTypes addObject:(NSString *)kUTTypeImage]; |
| else if (stringHasPrefixCaseInsensitive(mimeType, @"video/")) |
| [mediaTypes addObject:(NSString *)kUTTypeMovie]; |
| } |
| |
| return mediaTypes.allObjects; |
| } |
| |
| - (NSArray *)_mediaTypesForPickerSourceType:(UIImagePickerControllerSourceType)sourceType |
| { |
| NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get()); |
| if (mediaTypes.count) |
| return mediaTypes; |
| |
| // Fallback to every supported media type if there is no filter. |
| return [UIImagePickerController availableMediaTypesForSourceType:sourceType]; |
| } |
| |
| - (NSArray *)_documentPickerMenuMediaTypes |
| { |
| NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get()); |
| if (mediaTypes.count) |
| return mediaTypes; |
| |
| // Fallback to every supported media type if there is no filter. |
| return @[@"public.item"]; |
| } |
| |
| #pragma mark - Source selection menu |
| |
| - (NSString *)_photoLibraryButtonLabel |
| { |
| return WEB_UI_STRING_KEY("Photo Library", "Photo Library (file upload action sheet)", "File Upload alert sheet button string for choosing an existing media item from the Photo Library"); |
| } |
| |
| - (NSString *)_cameraButtonLabel |
| { |
| if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) |
| return nil; |
| |
| // Choose the appropriate string for the camera button. |
| NSArray *filteredMediaTypes = [self _mediaTypesForPickerSourceType:UIImagePickerControllerSourceTypeCamera]; |
| BOOL containsImageMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeImage]; |
| BOOL containsVideoMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeMovie]; |
| ASSERT(containsImageMediaType || containsVideoMediaType); |
| if (containsImageMediaType && containsVideoMediaType) |
| return WEB_UI_STRING_KEY("Take Photo or Video", "Take Photo or Video (file upload action sheet)", "File Upload alert sheet camera button string for taking photos or videos"); |
| |
| if (containsVideoMediaType) |
| return WEB_UI_STRING_KEY("Take Video", "Take Video (file upload action sheet)", "File Upload alert sheet camera button string for taking only videos"); |
| |
| return WEB_UI_STRING_KEY("Take Photo", "Take Photo (file upload action sheet)", "File Upload alert sheet camera button string for taking only photos"); |
| } |
| |
| - (void)_showDocumentPickerMenu |
| { |
| // FIXME: Support multiple file selection when implemented. <rdar://17177981> |
| _documentMenuController = adoptNS([[UIDocumentMenuViewController alloc] _initIgnoringApplicationEntitlementForImportOfTypes:[self _documentPickerMenuMediaTypes]]); |
| [_documentMenuController setDelegate:self]; |
| |
| [_documentMenuController addOptionWithTitle:[self _photoLibraryButtonLabel] image:photoLibraryIcon() order:UIDocumentMenuOrderFirst handler:^{ |
| [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary]; |
| }]; |
| |
| if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { |
| if (NSString *cameraString = [self _cameraButtonLabel]) { |
| [_documentMenuController addOptionWithTitle:cameraString image:cameraIcon() order:UIDocumentMenuOrderFirst handler:^{ |
| _usingCamera = YES; |
| [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypeCamera]; |
| }]; |
| } |
| } |
| |
| [self _presentForCurrentInterfaceIdiom:_documentMenuController.get()]; |
| } |
| |
| #pragma mark - Image Picker |
| |
| - (void)_adjustMediaCaptureType |
| { |
| if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront] || [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) { |
| if (![UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) |
| _mediaCaptureType = WebCore::MediaCaptureTypeEnvironment; |
| |
| if (![UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) |
| _mediaCaptureType = WebCore::MediaCaptureTypeUser; |
| |
| return; |
| } |
| |
| _mediaCaptureType = WebCore::MediaCaptureTypeNone; |
| } |
| |
| - (BOOL)_shouldMediaCaptureOpenMediaDevice |
| { |
| if (_mediaCaptureType == WebCore::MediaCaptureTypeNone || ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) |
| return NO; |
| |
| return YES; |
| } |
| |
| - (void)_showPhotoPickerWithSourceType:(UIImagePickerControllerSourceType)sourceType |
| { |
| ASSERT([UIImagePickerController isSourceTypeAvailable:sourceType]); |
| |
| _imagePicker = adoptNS([[UIImagePickerController alloc] init]); |
| [_imagePicker setDelegate:self]; |
| [_imagePicker setSourceType:sourceType]; |
| [_imagePicker setAllowsEditing:NO]; |
| [_imagePicker setModalPresentationStyle:UIModalPresentationFullScreen]; |
| [_imagePicker _setAllowsMultipleSelection:_allowMultipleFiles]; |
| [_imagePicker setMediaTypes:[self _mediaTypesForPickerSourceType:sourceType]]; |
| |
| if (_mediaCaptureType != WebCore::MediaCaptureTypeNone) |
| [_imagePicker setCameraDevice:cameraDeviceForMediaCaptureType(_mediaCaptureType)]; |
| |
| // Use a popover on the iPad if the source type is not the camera. |
| // The camera will use a fullscreen, modal view controller. |
| BOOL usePopover = UICurrentUserInterfaceIdiomIsPad() && sourceType != UIImagePickerControllerSourceTypeCamera; |
| if (usePopover) |
| [self _presentPopoverWithContentViewController:_imagePicker.get() animated:YES]; |
| else |
| [self _presentFullscreenViewController:_imagePicker.get() animated:YES]; |
| } |
| |
| #pragma mark - Presenting View Controllers |
| |
| - (void)_presentForCurrentInterfaceIdiom:(UIViewController *)viewController |
| { |
| if (UICurrentUserInterfaceIdiomIsPad()) |
| [self _presentPopoverWithContentViewController:viewController animated:YES]; |
| else |
| [self _presentFullscreenViewController:viewController animated:YES]; |
| } |
| |
| - (void)_presentPopoverWithContentViewController:(UIViewController *)contentViewController animated:(BOOL)animated |
| { |
| [self _dismissDisplayAnimated:animated]; |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| _presentationPopover = adoptNS([[UIPopoverController alloc] initWithContentViewController:contentViewController]); |
| #pragma clang diagnostic pop |
| [_presentationPopover setDelegate:self]; |
| [_presentationPopover presentPopoverFromRect:CGRectIntegral(CGRectMake(_interactionPoint.x, _interactionPoint.y, 1, 1)) inView:_view permittedArrowDirections:UIPopoverArrowDirectionAny animated:animated]; |
| } |
| |
| - (void)_presentFullscreenViewController:(UIViewController *)viewController animated:(BOOL)animated |
| { |
| [self _dismissDisplayAnimated:animated]; |
| |
| _presentationViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:_view]; |
| [_presentationViewController presentViewController:viewController animated:animated completion:nil]; |
| } |
| |
| #pragma mark - UIPopoverControllerDelegate |
| |
| - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController |
| { |
| [self _cancel]; |
| } |
| |
| #pragma mark - UIDocumentMenuDelegate implementation |
| |
| - (void)documentMenu:(UIDocumentMenuViewController *)documentMenu didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker |
| { |
| documentPicker.delegate = self; |
| [self _presentFullscreenViewController:documentPicker animated:YES]; |
| } |
| |
| - (void)documentMenuWasCancelled:(UIDocumentMenuViewController *)documentMenu |
| { |
| [self _dismissDisplayAnimated:YES]; |
| [self _cancel]; |
| } |
| |
| #pragma mark - UIDocumentPickerControllerDelegate implementation |
| |
| - (void)documentPicker:(UIDocumentPickerViewController *)documentPicker didPickDocumentAtURL:(NSURL *)url |
| { |
| [self _dismissDisplayAnimated:YES]; |
| [self _chooseFiles:@[url] displayString:url.lastPathComponent iconImage:iconForFile(url)]; |
| } |
| |
| - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)documentPicker |
| { |
| [self _dismissDisplayAnimated:YES]; |
| [self _cancel]; |
| } |
| |
| #pragma mark - UIImagePickerControllerDelegate implementation |
| |
| - (BOOL)_willMultipleSelectionDelegateBeCalled |
| { |
| // The multiple selection delegate will not be called when the UIImagePicker was not multiple selection. |
| if (!_allowMultipleFiles) |
| return NO; |
| |
| // The multiple selection delegate will not be called when we used the camera in the UIImagePicker. |
| if (_usingCamera) |
| return NO; |
| |
| return YES; |
| } |
| |
| - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMediaWithInfo:(NSDictionary *)info |
| { |
| // Sometimes both delegates get called, sometimes just one. Always let the |
| // multiple selection delegate handle everything if it will get called. |
| if ([self _willMultipleSelectionDelegateBeCalled]) |
| return; |
| |
| [self _dismissDisplayAnimated:YES]; |
| |
| [self _processMediaInfoDictionaries:[NSArray arrayWithObject:info] |
| successBlock:^(NSArray *processedResults, NSString *displayString) { |
| ASSERT([processedResults count] == 1); |
| _WKFileUploadItem *result = [processedResults objectAtIndex:0]; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self _chooseFiles:[NSArray arrayWithObject:result.fileURL] displayString:displayString iconImage:result.displayImage]; |
| }); |
| } |
| failureBlock:^{ |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self _cancel]; |
| }); |
| } |
| ]; |
| } |
| |
| - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMultipleMediaWithInfo:(NSArray *)infos |
| { |
| [self _dismissDisplayAnimated:YES]; |
| |
| [self _processMediaInfoDictionaries:infos |
| successBlock:^(NSArray *processedResults, NSString *displayString) { |
| UIImage *iconImage = nil; |
| NSMutableArray *fileURLs = [NSMutableArray array]; |
| for (_WKFileUploadItem *result in processedResults) { |
| NSURL *fileURL = result.fileURL; |
| if (!fileURL) |
| continue; |
| [fileURLs addObject:result.fileURL]; |
| if (!iconImage) |
| iconImage = result.displayImage; |
| } |
| |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self _chooseFiles:fileURLs displayString:displayString iconImage:iconImage]; |
| }); |
| } |
| failureBlock:^{ |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self _cancel]; |
| }); |
| } |
| ]; |
| } |
| |
| - (void)imagePickerControllerDidCancel:(UIImagePickerController *)imagePicker |
| { |
| [self _dismissDisplayAnimated:YES]; |
| [self _cancel]; |
| } |
| |
| #pragma mark - Process UIImagePicker results |
| |
| - (void)_processMediaInfoDictionaries:(NSArray *)infos successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| [self _processMediaInfoDictionaries:infos atIndex:0 processedResults:[NSMutableArray array] processedImageCount:0 processedVideoCount:0 successBlock:successBlock failureBlock:failureBlock]; |
| } |
| |
| - (void)_processMediaInfoDictionaries:(NSArray *)infos atIndex:(NSUInteger)index processedResults:(NSMutableArray *)processedResults processedImageCount:(NSUInteger)processedImageCount processedVideoCount:(NSUInteger)processedVideoCount successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| NSUInteger count = [infos count]; |
| if (index == count) { |
| NSString *displayString = [self _displayStringForPhotos:processedImageCount videos:processedVideoCount]; |
| successBlock(processedResults, displayString); |
| return; |
| } |
| |
| NSDictionary *info = [infos objectAtIndex:index]; |
| ASSERT(index < count); |
| index++; |
| |
| auto uploadItemSuccessBlock = ^(_WKFileUploadItem *uploadItem) { |
| NSUInteger newProcessedVideoCount = processedVideoCount + (uploadItem.isVideo ? 1 : 0); |
| NSUInteger newProcessedImageCount = processedImageCount + (uploadItem.isVideo ? 0 : 1); |
| [processedResults addObject:uploadItem]; |
| [self _processMediaInfoDictionaries:infos atIndex:index processedResults:processedResults processedImageCount:newProcessedImageCount processedVideoCount:newProcessedVideoCount successBlock:successBlock failureBlock:failureBlock]; |
| }; |
| |
| [self _uploadItemFromMediaInfo:info successBlock:uploadItemSuccessBlock failureBlock:failureBlock]; |
| } |
| |
| - (void)_uploadItemForImageData:(NSData *)imageData imageName:(NSString *)imageName successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| ASSERT_ARG(imageData, imageData); |
| ASSERT(!isMainThread()); |
| |
| NSString * const kTemporaryDirectoryName = @"WKWebFileUpload"; |
| |
| // Build temporary file path. |
| NSFileManager *fileManager = [NSFileManager defaultManager]; |
| NSString *temporaryDirectory = [fileManager _webkit_createTemporaryDirectoryWithTemplatePrefix:kTemporaryDirectoryName]; |
| NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:imageName]; |
| if (!filePath) { |
| LOG_ERROR("WKFileUploadPanel: Failed to create temporary directory to save image"); |
| failureBlock(); |
| return; |
| } |
| |
| // Save the image to the temporary file. |
| NSError *error = nil; |
| [imageData writeToFile:filePath options:NSDataWritingAtomic error:&error]; |
| if (error) { |
| LOG_ERROR("WKFileUploadPanel: Error writing image data to temporary file: %@", error); |
| failureBlock(); |
| return; |
| } |
| |
| successBlock(adoptNS([[_WKImageFileUploadItem alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]).get()); |
| } |
| |
| - (void)_uploadItemForJPEGRepresentationOfImage:(UIImage *)image successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| ASSERT_ARG(image, image); |
| |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| // FIXME: Different compression for different devices? |
| // FIXME: Different compression for different UIImage sizes? |
| // FIXME: Should EXIF data be maintained? |
| const CGFloat compression = 0.8; |
| NSData *jpeg = UIImageJPEGRepresentation(image, compression); |
| if (!jpeg) { |
| LOG_ERROR("WKFileUploadPanel: Failed to create JPEG representation for image"); |
| failureBlock(); |
| return; |
| } |
| |
| // FIXME: Should we get the photo asset and get the actual filename for the photo instead of |
| // naming each of the individual uploads image.jpg? This won't work for photos taken with |
| // the camera, but would work for photos picked from the library. |
| NSString * const kUploadImageName = @"image.jpg"; |
| [self _uploadItemForImageData:jpeg imageName:kUploadImageName successBlock:successBlock failureBlock:failureBlock]; |
| }); |
| } |
| |
| - (void)_uploadItemForImage:(UIImage *)image withAssetURL:(NSURL *)assetURL successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| ASSERT_ARG(image, image); |
| ASSERT_ARG(assetURL, assetURL); |
| |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| PHFetchResult *result = [getPHAssetClass() fetchAssetsWithALAssetURLs:@[assetURL] options:nil]; |
| if (!result.count) { |
| LOG_ERROR("WKFileUploadPanel: Failed to fetch asset with URL %@", assetURL); |
| [self _uploadItemForJPEGRepresentationOfImage:image successBlock:successBlock failureBlock:failureBlock]; |
| return; |
| } |
| |
| PHAsset *firstAsset = result[0]; |
| [firstAsset fetchPropertySetsIfNeeded]; |
| NSString *originalFilename = [[firstAsset originalMetadataProperties] originalFilename]; |
| ASSERT(originalFilename); |
| |
| RetainPtr<PHImageRequestOptions> options = adoptNS([allocPHImageRequestOptionsInstance() init]); |
| [options setVersion:PHImageRequestOptionsVersionCurrent]; |
| [options setSynchronous:YES]; |
| [options setResizeMode:PHImageRequestOptionsResizeModeNone]; |
| |
| PHImageManager *manager = (PHImageManager *)[getPHImageManagerClass() defaultManager]; |
| [manager requestImageDataForAsset:firstAsset options:options.get() resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation, NSDictionary *info) |
| { |
| if (!imageData) { |
| LOG_ERROR("WKFileUploadPanel: Failed to request image data for asset with URL %@", assetURL); |
| [self _uploadItemForJPEGRepresentationOfImage:image successBlock:successBlock failureBlock:failureBlock]; |
| return; |
| } |
| |
| [self _uploadItemForImageData:imageData imageName:originalFilename successBlock:successBlock failureBlock:failureBlock]; |
| }]; |
| }); |
| } |
| |
| - (void)_uploadItemFromMediaInfo:(NSDictionary *)info successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock |
| { |
| NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType]; |
| |
| // For videos from the existing library or camera, the media URL will give us a file path. |
| if (UTTypeConformsTo((CFStringRef)mediaType, kUTTypeMovie)) { |
| NSURL *mediaURL = [info objectForKey:UIImagePickerControllerMediaURL]; |
| if (![mediaURL isFileURL]) { |
| LOG_ERROR("WKFileUploadPanel: Expected media URL to be a file path, it was not"); |
| ASSERT_NOT_REACHED(); |
| failureBlock(); |
| return; |
| } |
| |
| successBlock(adoptNS([[_WKVideoFileUploadItem alloc] initWithFileURL:mediaURL]).get()); |
| return; |
| } |
| |
| if (!UTTypeConformsTo((CFStringRef)mediaType, kUTTypeImage)) { |
| LOG_ERROR("WKFileUploadPanel: Unexpected media type. Expected image or video, got: %@", mediaType); |
| ASSERT_NOT_REACHED(); |
| failureBlock(); |
| return; |
| } |
| |
| UIImage *originalImage = [info objectForKey:UIImagePickerControllerOriginalImage]; |
| if (!originalImage) { |
| LOG_ERROR("WKFileUploadPanel: Expected image data but there was none"); |
| ASSERT_NOT_REACHED(); |
| failureBlock(); |
| return; |
| } |
| |
| // If we have an asset URL, try to upload the native image. |
| NSURL *referenceURL = [info objectForKey:UIImagePickerControllerReferenceURL]; |
| if (referenceURL) { |
| [self _uploadItemForImage:originalImage withAssetURL:referenceURL successBlock:successBlock failureBlock:failureBlock]; |
| return; |
| } |
| |
| // Photos taken with the camera will not have an asset URL. Fall back to a JPEG representation. |
| [self _uploadItemForJPEGRepresentationOfImage:originalImage successBlock:successBlock failureBlock:failureBlock]; |
| } |
| |
| - (NSString *)_displayStringForPhotos:(NSUInteger)imageCount videos:(NSUInteger)videoCount |
| { |
| if (!imageCount && !videoCount) |
| return nil; |
| |
| NSString *title; |
| NSString *countString; |
| NSString *imageString; |
| NSString *videoString; |
| NSUInteger numberOfTypes = 2; |
| |
| RetainPtr<NSNumberFormatter> countFormatter = adoptNS([[NSNumberFormatter alloc] init]); |
| [countFormatter setLocale:[NSLocale currentLocale]]; |
| [countFormatter setGeneratesDecimalNumbers:YES]; |
| [countFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; |
| |
| // Generate the individual counts for each type. |
| switch (imageCount) { |
| case 0: |
| imageString = nil; |
| --numberOfTypes; |
| break; |
| case 1: |
| imageString = WEB_UI_STRING_KEY("1 Photo", "1 Photo (file upload on page label for one photo)", "File Upload single photo label"); |
| break; |
| default: |
| countString = [countFormatter stringFromNumber:@(imageCount)]; |
| imageString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Photos", "# Photos (file upload on page label for multiple photos)", "File Upload multiple photos label"), countString]; |
| break; |
| } |
| |
| switch (videoCount) { |
| case 0: |
| videoString = nil; |
| --numberOfTypes; |
| break; |
| case 1: |
| videoString = WEB_UI_STRING_KEY("1 Video", "1 Video (file upload on page label for one video)", "File Upload single video label"); |
| break; |
| default: |
| countString = [countFormatter stringFromNumber:@(videoCount)]; |
| videoString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Videos", "# Videos (file upload on page label for multiple videos)", "File Upload multiple videos label"), countString]; |
| break; |
| } |
| |
| // Combine into a single result string if needed. |
| switch (numberOfTypes) { |
| case 2: |
| // FIXME: For localization we should build a complete string. We should have a localized string for each different combination. |
| title = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ and %@", "# Photos and # Videos (file upload on page label for image and videos)", "File Upload images and videos label"), imageString, videoString]; |
| break; |
| case 1: |
| title = imageString ? imageString : videoString; |
| break; |
| default: |
| ASSERT_NOT_REACHED(); |
| title = nil; |
| break; |
| } |
| |
| return [title lowercaseString]; |
| } |
| |
| @end |
| |
| #pragma clang diagnostic pop |
| |
| #endif // PLATFORM(IOS) |