blob: 4cbe4da3f6c759b474d551fe3874556626e49965 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#import "WXImageComponent.h"
#import "WXHandlerFactory.h"
#import "WXComponent_internal.h"
#import "WXImgLoaderProtocol.h"
#import "WXLayer.h"
#import "WXType.h"
#import "WXConvert.h"
#import "WXURLRewriteProtocol.h"
#import "WXRoundedRect.h"
#import "UIBezierPath+Weex.h"
#import "WXSDKEngine.h"
#import "WXUtility.h"
#import "WXAssert.h"
#import <pthread/pthread.h>
@interface WXImageView : UIImageView
@end
@implementation WXImageView
+ (Class)layerClass
{
return [WXLayer class];
}
@end
static dispatch_queue_t WXImageUpdateQueue;
@interface WXImageComponent ()
{
NSString * _imageSrc;
pthread_mutex_t _imageSrcMutex;
pthread_mutexattr_t _propertMutexAttr;
}
@property (nonatomic, strong) NSString *placeholdSrc;
@property (nonatomic, assign) CGFloat blurRadius;
@property (nonatomic, assign) UIViewContentMode resizeMode;
@property (nonatomic, assign) WXImageQuality imageQuality;
@property (nonatomic, assign) WXImageSharp imageSharp;
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) id<WXImageOperationProtocol> imageOperation;
@property (nonatomic, strong) id<WXImageOperationProtocol> placeholderOperation;
@property (nonatomic) BOOL imageLoadEvent;
@property (nonatomic) BOOL imageDownloadFinish;
@end
@implementation WXImageComponent
WX_EXPORT_METHOD(@selector(save:))
- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
_async = YES;
if (!WXImageUpdateQueue) {
WXImageUpdateQueue = dispatch_queue_create("com.taobao.weex.ImageUpdateQueue", DISPATCH_QUEUE_SERIAL);
}
pthread_mutexattr_init(&(_propertMutexAttr));
pthread_mutexattr_settype(&(_propertMutexAttr), PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&(_imageSrcMutex), &(_propertMutexAttr));
if (attributes[@"src"]) {
pthread_mutex_lock(&(_imageSrcMutex));
_imageSrc = [[WXConvert NSString:attributes[@"src"]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
pthread_mutex_unlock(&(_imageSrcMutex));
} else {
WXLogWarning(@"image src is nil");
}
[self configPlaceHolder:attributes];
_resizeMode = [WXConvert UIViewContentMode:attributes[@"resize"]];
[self configFilter:styles];
_imageQuality = WXImageQualityNone;
if (styles[@"quality"]) {
_imageQuality = [WXConvert WXImageQuality:styles[@"quality"]];
}
if (attributes[@"quality"]) {
_imageQuality = [WXConvert WXImageQuality:attributes[@"quality"]];
}
_imageSharp = [WXConvert WXImageSharp:styles[@"sharpen"]];
_imageLoadEvent = NO;
_imageDownloadFinish = NO;
}
return self;
}
- (void)configPlaceHolder:(NSDictionary*)attributes
{
if (attributes[@"placeHolder"] || attributes[@"placeholder"]) {
_placeholdSrc = [[WXConvert NSString:attributes[@"placeHolder"]?:attributes[@"placeholder"]]stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
}
- (void)configFilter:(NSDictionary *)styles
{
if (styles[@"filter"]) {
NSString *filter = styles[@"filter"];
NSString *pattern = @"blur\\((\\d+)(px)?\\)";
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:NSRegularExpressionCaseInsensitive
error:&error];
NSArray *matches = [regex matchesInString:filter options:0 range:NSMakeRange(0, filter.length)];
if (matches && matches.count > 0) {
NSTextCheckingResult *match = matches[matches.count - 1];
NSRange matchRange = [match rangeAtIndex:1];
NSString *matchString = [filter substringWithRange:matchRange];
if (matchString && matchString.length > 0) {
_blurRadius = [matchString doubleValue];
[self updateImage];
}
}
}
}
- (void)save:(WXCallback)resultCallback
{
NSDictionary *info = [NSBundle mainBundle].infoDictionary;
if(!info[@"NSPhotoLibraryUsageDescription"]) {
if (resultCallback) {
resultCallback(@{
@"success" : @(false),
@"errorDesc": @"This maybe crash above iOS 10 because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data."
});
}
return ;
}
// iOS 11 needs a NSPhotoLibraryUsageDescription key for permission
if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) {
if (!info[@"NSPhotoLibraryUsageDescription"]) {
if (resultCallback) {
resultCallback(@{
@"success" : @(false),
@"errorDesc": @"This maybe crash above iOS 10 because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data."
});
}
return;
}
}
if (![self isViewLoaded]) {
if (resultCallback) {
resultCallback(@{@"success": @(false),
@"errorDesc":@"the image is not ready"});
}
return;
}
UIImageView * imageView = (UIImageView*)self.view;
if (!resultCallback) {
// there is no need to callback any result;
UIImageWriteToSavedPhotosAlbum(imageView.image, nil, nil, NULL);
}else {
UIImageWriteToSavedPhotosAlbum(imageView.image, self, @selector(image:didFinishSavingWithError:contextInfo:), (void*)CFBridgingRetain(resultCallback));
}
}
// the callback for PhotoAlbum.
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
if (!contextInfo) {
return;
}
NSMutableDictionary * callbackResult = [NSMutableDictionary new];
BOOL success = false;
if (!error) {
success = true;
} else {
[callbackResult setObject:[error description] forKey:@"errorDesc"];
}
if (contextInfo) {
[callbackResult setObject:@(success) forKey:@"success"];
((__bridge WXCallback)contextInfo)(callbackResult);
CFRelease(contextInfo);
}
}
- (UIView *)loadView
{
return [[WXImageView alloc] init];
}
- (void)addEvent:(NSString *)eventName {
if ([eventName isEqualToString:@"load"]) {
_imageLoadEvent = YES;
}
}
- (void)removeEvent:(NSString *)eventName {
if ([eventName isEqualToString:@"load"]) {
_imageLoadEvent = NO;
}
}
- (void)updateStyles:(NSDictionary *)styles
{
if (styles[@"quality"]) {
_imageQuality = [WXConvert WXImageQuality:styles[@"quality"]];
[self updateImage];
}
if (styles[@"sharpen"]) {
_imageSharp = [WXConvert WXImageSharp:styles[@"sharpen"]];
[self updateImage];
}
[self configFilter:styles];
}
- (void)dealloc
{
[self cancelImage];
pthread_mutex_destroy(&(_imageSrcMutex));
pthread_mutexattr_destroy(&_propertMutexAttr);
}
- (void)updateAttributes:(NSDictionary *)attributes
{
if (attributes[@"src"]) {
[self setImageSrc:[[WXConvert NSString:attributes[@"src"]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}
if (attributes[@"quality"]) {
_imageQuality = [WXConvert WXImageQuality:attributes[@"quality"]];
[self updateImage];
}
[self configPlaceHolder:attributes];
if (attributes[@"resize"]) {
_resizeMode = [WXConvert UIViewContentMode:attributes[@"resize"]];
self.view.contentMode = _resizeMode;
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
UIImageView *imageView = (UIImageView *)self.view;
imageView.contentMode = _resizeMode;
imageView.userInteractionEnabled = YES;
imageView.clipsToBounds = YES;
imageView.exclusiveTouch = YES;
imageView.isAccessibilityElement = YES;
[self _clipsToBounds];
[self updateImage];
}
- (BOOL)_needsDrawBorder
{
return NO;
}
- (BOOL)needsDrawRect
{
if (_isCompositingChild) {
return YES;
} else {
return NO;
}
}
- (UIImage *)drawRect:(CGRect)rect;
{
if (!self.image) {
[self updateImage];
return nil;
}
WXRoundedRect *borderRect = [[WXRoundedRect alloc] initWithRect:rect topLeft:_borderTopLeftRadius topRight:_borderTopRightRadius bottomLeft:_borderBottomLeftRadius bottomRight:_borderBottomRightRadius];
WXRadii *radii = borderRect.radii;
if ([radii hasBorderRadius]) {
CGFloat topLeft = radii.topLeft, topRight = radii.topRight, bottomLeft = radii.bottomLeft, bottomRight = radii.bottomRight;
UIBezierPath *bezierPath = [UIBezierPath wx_bezierPathWithRoundedRect:rect topLeft:topLeft topRight:topRight bottomLeft:bottomLeft bottomRight:bottomRight];
[bezierPath addClip];
}
return self.image;
}
- (void)viewWillUnload
{
[super viewWillUnload];
[self cancelImage];
_image = nil;
}
- (void)_frameDidCalculated:(BOOL)isChanged
{
[super _frameDidCalculated:isChanged];
if ([self isViewLoaded] && isChanged) {
__weak typeof(self) weakSelf = self;
WXPerformBlockOnMainThread(^{
[weakSelf _clipsToBounds];
});
}
}
- (NSString *)imageSrc
{
pthread_mutex_lock(&(_imageSrcMutex));
NSString * imageSrcCpy = [_imageSrc copy];
pthread_mutex_unlock(&(_imageSrcMutex));
return imageSrcCpy;
}
- (void)setImageSrc:(NSString*)src
{
if ([src isEqualToString:_imageSrc]) {
// if image src is equal to then ignore it.
return;
}
pthread_mutex_lock(&(_imageSrcMutex));
_imageSrc = src;
_imageDownloadFinish = NO;
((UIImageView*)self.view).image = nil;
pthread_mutex_unlock(&(_imageSrcMutex));
[self updateImage];
}
- (void)updateImage
{
__weak typeof(self) weakSelf = self;
dispatch_async(WXImageUpdateQueue, ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf cancelImage];
void(^downloadFailed)(NSString *, NSError *) = ^void(NSString *url, NSError *error) {
weakSelf.imageDownloadFinish = YES;
WXLogError(@"Error downloading image: %@, detail:%@", url, [error localizedDescription]);
};
[strongSelf updatePlaceHolderWithFailedBlock:downloadFailed];
[strongSelf updateContentImageWithFailedBlock:downloadFailed];
if (!strongSelf.imageSrc && !strongSelf.placeholdSrc) {
dispatch_async(dispatch_get_main_queue(), ^{
strongSelf.layer.contents = nil;
strongSelf.imageDownloadFinish = YES;
[strongSelf readyToRender];
});
}
});
}
- (void)updatePlaceHolderWithFailedBlock:(void(^)(NSString *, NSError *))downloadFailedBlock
{
NSString *placeholderSrc = self.placeholdSrc;
if ([WXUtility isBlankString:placeholderSrc]) {
return;
}
WXLogDebug(@"Updating image, component:%@, placeholder:%@ ", self.ref, placeholderSrc);
NSString *newURL = [_placeholdSrc copy];
WX_REWRITE_URL(_placeholdSrc, WXResourceTypeImage, self.weexInstance)
__weak typeof(self) weakSelf = self;
self.placeholderOperation = [[self imageLoader] downloadImageWithURL:newURL imageFrame:self.calculatedFrame userInfo:nil completed:^(UIImage *image, NSError *error, BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
UIImage *viewImage = ((UIImageView *)strongSelf.view).image;
if (error) {
downloadFailedBlock(placeholderSrc,error);
if ([strongSelf isViewLoaded] && !viewImage) {
((UIImageView *)(strongSelf.view)).image = nil;
[strongSelf readyToRender];
}
return;
}
if (![placeholderSrc isEqualToString:strongSelf.placeholdSrc]) {
return;
}
if ([strongSelf isViewLoaded] && !viewImage) {
((UIImageView *)strongSelf.view).image = image;
strongSelf.imageDownloadFinish = YES;
[strongSelf readyToRender];
} else if (strongSelf->_isCompositingChild) {
strongSelf->_image = image;
strongSelf.imageDownloadFinish = YES;
}
});
}];
}
- (void)updateContentImageWithFailedBlock:(void(^)(NSString *, NSError *))downloadFailedBlock
{
NSString *imageSrc = [NSString stringWithFormat:@"%@", self.imageSrc?:@""];
if ([WXUtility isBlankString:imageSrc]) {
WXLogError(@"image src is empty");
return;
}
WXLogDebug(@"Updating image:%@, component:%@", self.imageSrc, self.ref);
NSDictionary *userInfo = @{@"imageQuality":@(self.imageQuality), @"imageSharp":@(self.imageSharp), @"blurRadius":@(self.blurRadius)};
NSString * newURL = [imageSrc copy];
WX_REWRITE_URL(imageSrc, WXResourceTypeImage, self.weexInstance)
__weak typeof(self) weakSelf = self;
weakSelf.imageOperation = [[weakSelf imageLoader] downloadImageWithURL:newURL imageFrame:weakSelf.calculatedFrame userInfo:userInfo completed:^(UIImage *image, NSError *error, BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.imageLoadEvent) {
NSMutableDictionary *sizeDict = [NSMutableDictionary new];
sizeDict[@"naturalWidth"] = @0;
sizeDict[@"naturalHeight"] = @0;
if (!error) {
sizeDict[@"naturalWidth"] = @(image.size.width * image.scale);
sizeDict[@"naturalHeight"] = @(image.size.height * image.scale);
} else {
[sizeDict setObject:[error description]?:@"" forKey:@"errorDesc"];
}
[strongSelf fireEvent:@"load" params:@{ @"success": error? @false : @true,@"size":sizeDict}];
}
if (error) {
downloadFailedBlock(imageSrc, error);
[strongSelf readyToRender];
return ;
}
if (![imageSrc isEqualToString:strongSelf.imageSrc]) {
return ;
}
if ([strongSelf isViewLoaded]) {
strongSelf.imageDownloadFinish = YES;
((UIImageView *)strongSelf.view).image = image;
[strongSelf readyToRender];
} else if (strongSelf->_isCompositingChild) {
strongSelf.imageDownloadFinish = YES;
strongSelf->_image = image;
[strongSelf setNeedsDisplay];
}
});
}];
}
- (void)readyToRender
{
// when image download completely (success or failed)
if (self.weexInstance.trackComponent && _imageDownloadFinish) {
[super readyToRender];
}
}
- (void)cancelImage
{
[_imageOperation cancel];
_imageOperation = nil;
[_placeholderOperation cancel];
_placeholderOperation = nil;
}
- (id<WXImgLoaderProtocol>)imageLoader
{
static id<WXImgLoaderProtocol> imageLoader;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageLoader = [WXHandlerFactory handlerForProtocol:@protocol(WXImgLoaderProtocol)];
});
return imageLoader;
}
- (void)_clipsToBounds
{
WXAssertMainThread();
WXRoundedRect *borderRect = [[WXRoundedRect alloc] initWithRect:self.view.bounds topLeft:_borderTopLeftRadius topRight:_borderTopRightRadius bottomLeft:_borderBottomLeftRadius bottomRight:_borderBottomRightRadius];
// here is computed radii, do not use original style
WXRadii *radii = borderRect.radii;
if ([radii radiusesAreEqual]) {
return;
}
CGFloat topLeft = radii.topLeft, topRight = radii.topRight, bottomLeft = radii.bottomLeft, bottomRight = radii.bottomRight;
// clip to border radius
UIBezierPath *bezierPath = [UIBezierPath wx_bezierPathWithRoundedRect:self.view.bounds topLeft:topLeft topRight:topRight bottomLeft:bottomLeft bottomRight:bottomRight];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = bezierPath.CGPath;
self.layer.mask = shapeLayer;
self.layer.cornerRadius = 0;
}
#ifdef UITEST
- (NSString *)description
{
NSString *superDescription = super.description;
NSRange semicolonRange = [superDescription rangeOfString:@";"];
NSString *replacement = [NSString stringWithFormat:@"; imageSrc: %@; imageQuality: %@; imageSharp: %@; ",_imageSrc,_imageQuality,_imageSharp];
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
}
#endif
@end