0x00 為什么要這么做
需要把一個(gè)網(wǎng)站嵌入到APP里木蹬,那個(gè)網(wǎng)站的一個(gè)頁面包含了一個(gè)上傳文件的按鈕礼预,他們只能接受后綴為jpg的圖片,但是iOS相冊(cè)里png圖片是常見的。
然而他們對(duì)客戶端要求必須有這種限制赊抖。所以客戶端需要想辦法,把用戶挑選出來的圖片轉(zhuǎn)為jpg格式寨典,后綴改為jpg再上傳氛雪。
初步考慮后大致想出了三個(gè)路子:
- 能不能在UIImageViewCongtroller里做method-swizzling.
- 能不能注入JS,通過JS攔截來轉(zhuǎn)換圖片格式耸成。
- 在WebKit里做method-swizzling,畢竟總得通過一個(gè)回調(diào)把片傳給WebView.
方法1無法走通报亩,因?yàn)椴恢勒也坏娇尚械姆椒ǎ强梢岳肬IImagePickerControllerDelegate井氢,這個(gè)由方法3描述弦追。
方法2是可以的,針對(duì)性的對(duì)input標(biāo)簽增加堅(jiān)挺花竞,獲取圖片資源的base64劲件,通過js轉(zhuǎn)換成jpg.
然后就是方法3了。這是一個(gè)完全可行的方法约急。
0x01 尋找攔截的入口
在xcode里的文檔并沒有指出UIImagePickerController的delegate是誰零远。
這里可以從WebKit2文檔里找到的,就是WKFileUploadPanel.
不過我用了一個(gè)自下而上的方法來尋找厌蔽,swizzle了viewDidAppear方法牵辣。
- (void)swizzled_viewDidAppear {
[self swizzled_viewDidAppear];
if ([self isKindOfClass:[UIImagePickerController class]]) {
UIImagePickerController *that = self;
NSLog(@"%@",that.delegate);
}
打印出來的便是<WKFileUploadPanel: 0x10bdc3710>,
google一下即可發(fā)現(xiàn)WKFileUploadPanel.mm源碼,后面需要用到奴饮。
0x02 置換回調(diào)方法
根據(jù)文檔纬向,和對(duì)mediaInfo的打印結(jié)果確認(rèn),只有didFinishPickingMediaWithInfo包含了圖片和后綴信息戴卜,是適用于重寫圖片信息的罢猪。
__TVOS_PROHIBITED @protocol UIImagePickerControllerDelegate<NSObject>
@optional
// The picker does not dismiss itself; the client dismisses it in these callbacks.
// The delegate will receive one or the other, but not both, depending whether the user
// confirms or cancels.
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0);
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
//MediaInfo
[0] (null) @"UIImagePickerControllerMediaType" : @"public.image"
[1] (null) @"UIImagePickerControllerOriginalImage" : (no summary)
[2] (null) @"UIImagePickerControllerReferenceURL" : @"assets-library://asset/asset.JPG?id=7C993AB5-2881-4261-BAB4-BB0559E8C65C&ext=JPG"
[3] (null) @"UIImagePickerControllerImageURL" : @"file:///private/var/mobile/Containers/Data/Application/F024EE26-344E-4A83-AD0F-8E4BCEFA18AA/tmp/E1167ADD-779C-4496-B4E3-5AD45A0B2478.jpeg"
然后參照WKFileUploadPanel.mm源碼進(jìn)行swizzling.
先貼出swizzle后的方法實(shí)現(xiàn)
- (void)swizzled_imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{
NSMutableDictionary *dict = [[NSMutableDictionary alloc]initWithDictionary:info];
//UIImagePickerControllerReferenceURL設(shè)為空是關(guān)鍵一步。
[dict setValue:nil forKey:@"UIImagePickerControllerReferenceURL"];
NSURL *oriPath = [dict valueForKey:@"UIImagePickerControllerImageURL"];
NSString *imgfolder =[NSString stringWithFormat:@"file://%@",fast_PathInDocumentDirectory(@"tmpImgs")];
NSString *imgsPath = fast_PathInDocumentDirectory(@"tmpImgs");
if ([oriPath.absoluteString hasSuffix:@"png"]){
UIImage *oriImg = [dict valueForKey:@"UIImagePickerControllerOriginalImage"];
NSData* data = UIImageJPEGRepresentation(oriImg,0);
NSString * shortUUID = [NSUUID shortUUIDString];
NSString *finalPath = [imgsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.jpg",shortUUID]];
[data writeToFile:finalPath atomically:YES];
UIImage *jpgImg = [UIImage imageWithData:data];
NSURL *jpgPath = [@"file://"stringByAppendingString:finalPath].mj_url;
[dict setObject:jpgImg forKey:@"UIImagePickerControllerOriginalImage"];
[dict setObject:jpgPath forKey:@"UIImagePickerControllerImageURL"];
}else if([oriPath.absoluteString hasSuffix:@"jpeg"]){
UIImage *oriImg = [dict valueForKey:@"UIImagePickerControllerOriginalImage"];
NSData* data = UIImageJPEGRepresentation(oriImg,0);
NSString * shortUUID = [NSUUID shortUUIDString];
NSString *finalPath = [imgsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.jpg",shortUUID]];
[data writeToFile:finalPath atomically:YES];
UIImage *jpgImg = [UIImage imageWithData:data];
NSURL *jpgPath = [@"file://"stringByAppendingString:finalPath].mj_url;
[dict setObject:jpgImg forKey:@"UIImagePickerControllerOriginalImage"];
[dict setObject:jpgPath forKey:@"UIImagePickerControllerImageURL"];
}
[self swizzled_imagePickerController:picker didFinishPickingMediaWithInfo:dict];
}
fast_PathInDocumentDirectory的實(shí)現(xiàn)
NSString *fast_PathInDocumentDirectory(NSString *fileName)
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
return [documentDirectory stringByAppendingPathComponent:fileName];
}
[NSUUID shortUUIDString] 是來源于UUIDShortener的擴(kuò)展方法
對(duì)置換方法的解釋
在結(jié)合mediaInfo從源碼尋找的過程中發(fā)現(xiàn)了突破口叉瘩,實(shí)際上在發(fā)現(xiàn)這個(gè)方法之前我都不確定是否可以實(shí)現(xiàn)對(duì)圖片的置換。
因?yàn)檫@個(gè)方法之前粘捎,我單純嘗試性地修改了mediaInfo的里相關(guān)的后綴和圖片格式薇缅。但這樣都無法把圖片上傳給webview危彩。
所以必須再深入一點(diǎn)去了解源碼,確認(rèn)是否可行泳桦。
下面是WKFileUploadPanel中上傳圖片的方法:
- (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];
}
上面這些代碼汤徽,主要關(guān)注一下幾個(gè)部分
- [info objectForKey:UIImagePickerControllerMediaURL]是無需修改的
- [info objectForKey:UIImagePickerControllerOriginalImage]不能為空,而且需要修改為轉(zhuǎn)換后的圖片灸撰。
- 然后就是最后幾行涉及到referenceURL的代碼:
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];
這里有個(gè)比較巧妙的過程谒府,按注釋的意思是,相機(jī)是不會(huì)有asset URL,即referenceURL會(huì)為空浮毯,所以這里不需要傳asset URL完疫,直接傳圖片對(duì)象即可。
對(duì)于web頁面來說债蓝,只需要我的圖片就行了壳鹤。
回到自己寫的置換的方法中去:
[dict setValue:nil forKey:@"UIImagePickerControllerReferenceURL"];為什么是關(guān)鍵一步呢。
就是因?yàn)槲蚁M鸚KFileUploadPanel以為從相冊(cè)來的圖片也是從相機(jī)來的饰迹。
最后一步執(zhí)行了
[self swizzled_imagePickerController:picker didFinishPickingMediaWithInfo:dict];
這里就是調(diào)用原始的WKFileUploadPanel對(duì)UIImagePickerDelegate的實(shí)現(xiàn)了芳誓,被修改后的mediaInfo被傳給webview,實(shí)現(xiàn)了置換啊鸭。
其實(shí)還有個(gè)方法就是考慮下能不能攔截input標(biāo)簽 跳轉(zhuǎn)到自定義的頁面上锹淌。