ASImageNode.mm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. //
  2. // ASImageNode.mm
  3. // AsyncDisplayKit
  4. //
  5. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  6. // This source code is licensed under the BSD-style license found in the
  7. // LICENSE file in the root directory of this source tree. An additional grant
  8. // of patent rights can be found in the PATENTS file in the same directory.
  9. //
  10. #import "ASImageNode.h"
  11. #import <tgmath.h>
  12. #import "_ASDisplayLayer.h"
  13. #import "ASAssert.h"
  14. #import "ASDimension.h"
  15. #import "ASDisplayNode+FrameworkSubclasses.h"
  16. #import "ASDisplayNodeExtras.h"
  17. #import "ASDisplayNode+Beta.h"
  18. #import "ASLayout.h"
  19. #import "ASTextNode.h"
  20. #import "ASImageNode+AnimatedImagePrivate.h"
  21. #import "ASImageNode+CGExtras.h"
  22. #import "AsyncDisplayKit+Debug.h"
  23. #import "ASInternalHelpers.h"
  24. #import "ASEqualityHelpers.h"
  25. #import "ASEqualityHashHelpers.h"
  26. #import "ASWeakMap.h"
  27. #import "CoreGraphics+ASConvenience.h"
  28. // TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h
  29. #import "ASDisplayNodeInternal.h"
  30. #include <functional>
  31. struct ASImageNodeDrawParameters {
  32. BOOL opaque;
  33. CGRect bounds;
  34. CGFloat contentsScale;
  35. UIColor *backgroundColor;
  36. UIViewContentMode contentMode;
  37. BOOL cropEnabled;
  38. BOOL forceUpscaling;
  39. CGSize forcedSize;
  40. CGRect cropRect;
  41. CGRect cropDisplayBounds;
  42. asimagenode_modification_block_t imageModificationBlock;
  43. };
  44. /**
  45. * Contains all data that is needed to generate the content bitmap.
  46. */
  47. @interface ASImageNodeContentsKey : NSObject {}
  48. @property (nonatomic, strong) UIImage *image;
  49. @property CGSize backingSize;
  50. @property CGRect imageDrawRect;
  51. @property BOOL isOpaque;
  52. @property (nonatomic, strong) UIColor *backgroundColor;
  53. @property (nonatomic, copy) ASDisplayNodeContextModifier preContextBlock;
  54. @property (nonatomic, copy) ASDisplayNodeContextModifier postContextBlock;
  55. @property (nonatomic, copy) asimagenode_modification_block_t imageModificationBlock;
  56. @end
  57. @implementation ASImageNodeContentsKey
  58. - (BOOL)isEqual:(id)object
  59. {
  60. if (self == object) {
  61. return YES;
  62. }
  63. // Optimization opportunity: The `isKindOfClass` call here could be avoided by not using the NSObject `isEqual:`
  64. // convention and instead using a custom comparison function that assumes all items are heterogeneous.
  65. // However, profiling shows that our entire `isKindOfClass` expression is only ~1/40th of the total
  66. // overheard of our caching, so it's likely not high-impact.
  67. if ([object isKindOfClass:[ASImageNodeContentsKey class]]) {
  68. ASImageNodeContentsKey *other = (ASImageNodeContentsKey *)object;
  69. return [_image isEqual:other.image]
  70. && CGSizeEqualToSize(_backingSize, other.backingSize)
  71. && CGRectEqualToRect(_imageDrawRect, other.imageDrawRect)
  72. && _isOpaque == other.isOpaque
  73. && [_backgroundColor isEqual:other.backgroundColor]
  74. && _preContextBlock == other.preContextBlock
  75. && _postContextBlock == other.postContextBlock
  76. && _imageModificationBlock == other.imageModificationBlock;
  77. } else {
  78. return NO;
  79. }
  80. }
  81. - (NSUInteger)hash
  82. {
  83. NSUInteger subhashes[] = {
  84. // Profiling shows that the work done in UIImage's `hash` is on the order of 0.005ms on an A5 processor
  85. // and isn't proportional to the size of the image.
  86. [_image hash],
  87. // TODO: Hashing the floats in a CGRect or CGSize is tricky. Equality of floats is
  88. // fuzzy, but it's a 100% requirement that two equal values must produce an identical hash value.
  89. // Until there's a robust solution for hashing floats, leave all float values out of the hash.
  90. // This may lead to a greater number of isEqual comparisons but does not comprimise correctness.
  91. //AS::hash<CGRect>()(_backingSize),
  92. //AS::hash<CGRect>()(_imageDrawRect),
  93. AS::hash<BOOL>()(_isOpaque),
  94. [_backgroundColor hash],
  95. AS::hash<void *>()((void*)_preContextBlock),
  96. AS::hash<void *>()((void*)_postContextBlock),
  97. AS::hash<void *>()((void*)_imageModificationBlock),
  98. };
  99. return ASIntegerArrayHash(subhashes, sizeof(subhashes) / sizeof(subhashes[0]));
  100. }
  101. @end
  102. @implementation ASImageNode
  103. {
  104. @private
  105. UIImage *_image;
  106. ASWeakMapEntry *_weakCacheEntry; // Holds a reference that keeps our contents in cache.
  107. void (^_displayCompletionBlock)(BOOL canceled);
  108. // Drawing
  109. ASImageNodeDrawParameters _drawParameter;
  110. ASTextNode *_debugLabelNode;
  111. // Cropping.
  112. BOOL _cropEnabled; // Defaults to YES.
  113. BOOL _forceUpscaling; //Defaults to NO.
  114. CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size.
  115. CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0)
  116. CGRect _cropDisplayBounds; // Defaults to CGRectNull
  117. }
  118. @synthesize image = _image;
  119. @synthesize imageModificationBlock = _imageModificationBlock;
  120. #pragma mark - NSObject
  121. + (void)initialize
  122. {
  123. [super initialize];
  124. if (self != [ASImageNode class]) {
  125. // Prevent custom drawing in subclasses
  126. ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASImageNode class], self, @selector(displayWithParameters:isCancelled:)), @"Subclass %@ must not override displayWithParameters:isCancelled: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASImageNode class]));
  127. }
  128. }
  129. - (instancetype)init
  130. {
  131. if (!(self = [super init]))
  132. return nil;
  133. // TODO can this be removed?
  134. self.contentsScale = ASScreenScale();
  135. self.contentMode = UIViewContentModeScaleAspectFill;
  136. self.opaque = NO;
  137. // If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting
  138. // the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the
  139. // initial value. With setting a explicit backgroundColor we can prevent that change.
  140. self.backgroundColor = [UIColor clearColor];
  141. _cropEnabled = YES;
  142. _forceUpscaling = NO;
  143. _cropRect = CGRectMake(0.5, 0.5, 0, 0);
  144. _cropDisplayBounds = CGRectNull;
  145. _placeholderColor = ASDisplayNodeDefaultPlaceholderColor();
  146. _animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode;
  147. return self;
  148. }
  149. - (void)dealloc
  150. {
  151. // Invalidate all components around animated images
  152. [self invalidateAnimatedImage];
  153. }
  154. - (UIImage *)placeholderImage
  155. {
  156. // FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set.
  157. // This would completely eliminate the memory and performance cost of the backing store.
  158. CGSize size = self.calculatedSize;
  159. if ((size.width * size.height) < CGFLOAT_EPSILON) {
  160. return nil;
  161. }
  162. ASDN::MutexLocker l(__instanceLock__);
  163. UIGraphicsBeginImageContext(size);
  164. [self.placeholderColor setFill];
  165. UIRectFill(CGRectMake(0, 0, size.width, size.height));
  166. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  167. UIGraphicsEndImageContext();
  168. return image;
  169. }
  170. #pragma mark - Layout and Sizing
  171. - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
  172. {
  173. ASDN::MutexLocker l(__instanceLock__);
  174. if (_image == nil) {
  175. return [super calculateSizeThatFits:constrainedSize];
  176. }
  177. return _image.size;
  178. }
  179. #pragma mark - Setter / Getter
  180. - (void)setImage:(UIImage *)image
  181. {
  182. ASDN::MutexLocker l(__instanceLock__);
  183. if (!ASObjectIsEqual(_image, image)) {
  184. _image = image;
  185. [self setNeedsLayout];
  186. if (image) {
  187. [self setNeedsDisplay];
  188. if ([ASImageNode shouldShowImageScalingOverlay] && _debugLabelNode == nil) {
  189. ASPerformBlockOnMainThread(^{
  190. _debugLabelNode = [[ASTextNode alloc] init];
  191. _debugLabelNode.layerBacked = YES;
  192. [self addSubnode:_debugLabelNode];
  193. });
  194. }
  195. } else {
  196. self.contents = nil;
  197. }
  198. }
  199. }
  200. - (UIImage *)image
  201. {
  202. ASDN::MutexLocker l(__instanceLock__);
  203. return _image;
  204. }
  205. - (void)setPlaceholderColor:(UIColor *)placeholderColor
  206. {
  207. _placeholderColor = placeholderColor;
  208. // prevent placeholders if we don't have a color
  209. self.placeholderEnabled = placeholderColor != nil;
  210. }
  211. #pragma mark - Drawing
  212. - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
  213. {
  214. ASDN::MutexLocker l(__instanceLock__);
  215. _drawParameter = {
  216. .bounds = self.bounds,
  217. .opaque = self.opaque,
  218. .contentsScale = self.contentsScaleForDisplay,
  219. .backgroundColor = self.backgroundColor,
  220. .contentMode = self.contentMode,
  221. .cropEnabled = _cropEnabled,
  222. .forceUpscaling = _forceUpscaling,
  223. .forcedSize = _forcedSize,
  224. .cropRect = _cropRect,
  225. .cropDisplayBounds = _cropDisplayBounds,
  226. .imageModificationBlock = _imageModificationBlock
  227. };
  228. return nil;
  229. }
  230. - (NSDictionary *)debugLabelAttributes
  231. {
  232. return @{
  233. NSFontAttributeName: [UIFont systemFontOfSize:15.0],
  234. NSForegroundColorAttributeName: [UIColor redColor]
  235. };
  236. }
  237. - (UIImage *)displayWithParameters:(id<NSObject> *)parameter isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
  238. {
  239. UIImage *image = self.image;
  240. if (image == nil) {
  241. return nil;
  242. }
  243. __instanceLock__.lock();
  244. ASImageNodeDrawParameters drawParameter = _drawParameter;
  245. __instanceLock__.unlock();
  246. CGRect drawParameterBounds = drawParameter.bounds;
  247. BOOL forceUpscaling = drawParameter.forceUpscaling;
  248. CGSize forcedSize = drawParameter.forcedSize;
  249. BOOL cropEnabled = drawParameter.cropEnabled;
  250. BOOL isOpaque = drawParameter.opaque;
  251. UIColor *backgroundColor = drawParameter.backgroundColor;
  252. UIViewContentMode contentMode = drawParameter.contentMode;
  253. CGFloat contentsScale = drawParameter.contentsScale;
  254. CGRect cropDisplayBounds = drawParameter.cropDisplayBounds;
  255. CGRect cropRect = drawParameter.cropRect;
  256. asimagenode_modification_block_t imageModificationBlock = drawParameter.imageModificationBlock;
  257. BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds);
  258. CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds);
  259. ASDisplayNodeContextModifier preContextBlock = self.willDisplayNodeContentWithRenderingContext;
  260. ASDisplayNodeContextModifier postContextBlock = self.didDisplayNodeContentWithRenderingContext;
  261. ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time");
  262. // if the image is resizable, bail early since the image has likely already been configured
  263. BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
  264. if (stretchable) {
  265. if (imageModificationBlock != NULL) {
  266. image = imageModificationBlock(image);
  267. }
  268. return image;
  269. }
  270. CGSize imageSize = image.size;
  271. CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale);
  272. CGSize boundsSizeInPixels = CGSizeMake(std::floor(bounds.size.width * contentsScale), std::floor(bounds.size.height * contentsScale));
  273. if (_debugLabelNode) {
  274. CGFloat pixelCountRatio = (imageSizeInPixels.width * imageSizeInPixels.height) / (boundsSizeInPixels.width * boundsSizeInPixels.height);
  275. if (pixelCountRatio != 1.0) {
  276. NSString *scaleString = [NSString stringWithFormat:@"%.2fx", pixelCountRatio];
  277. _debugLabelNode.attributedText = [[NSAttributedString alloc] initWithString:scaleString attributes:[self debugLabelAttributes]];
  278. _debugLabelNode.hidden = NO;
  279. [self setNeedsLayout];
  280. } else {
  281. _debugLabelNode.hidden = YES;
  282. _debugLabelNode.attributedText = nil;
  283. }
  284. }
  285. BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill ||
  286. contentMode == UIViewContentModeScaleAspectFit ||
  287. contentMode == UIViewContentModeCenter;
  288. CGSize backingSize = CGSizeZero;
  289. CGRect imageDrawRect = CGRectZero;
  290. if (boundsSizeInPixels.width * contentsScale < 1.0f || boundsSizeInPixels.height * contentsScale < 1.0f ||
  291. imageSizeInPixels.width < 1.0f || imageSizeInPixels.height < 1.0f) {
  292. return nil;
  293. }
  294. // If we're not supposed to do any cropping, just decode image at original size
  295. if (!cropEnabled || !contentModeSupported || stretchable) {
  296. backingSize = imageSizeInPixels;
  297. imageDrawRect = (CGRect){.size = backingSize};
  298. } else {
  299. if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) {
  300. //scale forced size
  301. forcedSize.width *= contentsScale;
  302. forcedSize.height *= contentsScale;
  303. }
  304. ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels,
  305. boundsSizeInPixels,
  306. contentMode,
  307. cropRect,
  308. forceUpscaling,
  309. forcedSize,
  310. &backingSize,
  311. &imageDrawRect);
  312. }
  313. if (backingSize.width <= 0.0f || backingSize.height <= 0.0f ||
  314. imageDrawRect.size.width <= 0.0f || imageDrawRect.size.height <= 0.0f) {
  315. return nil;
  316. }
  317. ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init];
  318. contentsKey.image = image;
  319. contentsKey.backingSize = backingSize;
  320. contentsKey.imageDrawRect = imageDrawRect;
  321. contentsKey.isOpaque = isOpaque;
  322. contentsKey.backgroundColor = backgroundColor;
  323. contentsKey.preContextBlock = preContextBlock;
  324. contentsKey.postContextBlock = postContextBlock;
  325. contentsKey.imageModificationBlock = imageModificationBlock;
  326. if (isCancelled()) {
  327. return nil;
  328. }
  329. ASWeakMapEntry<UIImage *> *entry = [self.class contentsForkey:contentsKey isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled];
  330. if (entry == nil) { // If nil, we were cancelled.
  331. return nil;
  332. }
  333. __instanceLock__.lock();
  334. _weakCacheEntry = entry; // Retain so that the entry remains in the weak cache
  335. __instanceLock__.unlock();
  336. return entry.value;
  337. }
  338. static ASWeakMap<ASImageNodeContentsKey *, UIImage *> *cache = nil;
  339. static ASDN::Mutex cacheLock;
  340. + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
  341. {
  342. {
  343. ASDN::MutexLocker l(cacheLock);
  344. if (!cache) {
  345. cache = [[ASWeakMap alloc] init];
  346. }
  347. ASWeakMapEntry *entry = [cache entryForKey:key];
  348. if (entry != nil) {
  349. // cache hit
  350. return entry;
  351. }
  352. }
  353. // cache miss
  354. UIImage *contents = [self createContentsForkey:key isCancelled:isCancelled];
  355. if (contents == nil) { // If nil, we were cancelled
  356. return nil;
  357. }
  358. {
  359. ASDN::MutexLocker l(cacheLock);
  360. return [cache setObject:contents forKey:key];
  361. }
  362. }
  363. + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
  364. {
  365. // The following `UIGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an
  366. // A5 processor for a 400x800 backingSize.
  367. // Check for cancellation before we call it.
  368. if (isCancelled()) {
  369. return nil;
  370. }
  371. // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds
  372. // will do its rounding on pixel instead of point boundaries
  373. UIGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0);
  374. BOOL contextIsClean = YES;
  375. CGContextRef context = UIGraphicsGetCurrentContext();
  376. if (context && key.preContextBlock) {
  377. key.preContextBlock(context);
  378. contextIsClean = NO;
  379. }
  380. // if view is opaque, fill the context with background color
  381. if (key.isOpaque && key.backgroundColor) {
  382. [key.backgroundColor setFill];
  383. UIRectFill({ .size = key.backingSize });
  384. contextIsClean = NO;
  385. }
  386. // iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on
  387. // multiple threads concurrently. In fact, instead of crashing, it appears to deadlock.
  388. // The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premiere,
  389. // as well as iOS games, and a small number of ASDK apps that provide the same image reference
  390. // to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes
  391. // that may get the same pointer for a given UI asset image, etc.
  392. // FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and
  393. // only if the object already exists in the set we should create a semaphore to signal waiting threads
  394. // upon removal of the object from the set when the operation completes.
  395. // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer.
  396. // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068
  397. UIImage *image = key.image;
  398. BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)));
  399. CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal;
  400. @synchronized(image) {
  401. [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1];
  402. }
  403. if (context && key.postContextBlock) {
  404. key.postContextBlock(context);
  405. }
  406. // The following `UIGraphicsGetImageFromCurrentImageContext` call will commonly take more than 20ms on an
  407. // A5 processor. Check for cancellation before we call it.
  408. if (isCancelled()) {
  409. UIGraphicsEndImageContext();
  410. return nil;
  411. }
  412. UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
  413. UIGraphicsEndImageContext();
  414. if (key.imageModificationBlock != NULL) {
  415. result = key.imageModificationBlock(result);
  416. }
  417. return result;
  418. }
  419. - (void)displayDidFinish
  420. {
  421. [super displayDidFinish];
  422. __instanceLock__.lock();
  423. void (^displayCompletionBlock)(BOOL canceled) = _displayCompletionBlock;
  424. UIImage *image = _image;
  425. __instanceLock__.unlock();
  426. // If we've got a block to perform after displaying, do it.
  427. if (image && displayCompletionBlock) {
  428. displayCompletionBlock(NO);
  429. __instanceLock__.lock();
  430. _displayCompletionBlock = nil;
  431. __instanceLock__.unlock();
  432. }
  433. }
  434. - (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock
  435. {
  436. if (self.displaySuspended) {
  437. if (displayCompletionBlock)
  438. displayCompletionBlock(YES);
  439. return;
  440. }
  441. // Stash the block and call-site queue. We'll invoke it in -displayDidFinish.
  442. ASDN::MutexLocker l(__instanceLock__);
  443. if (_displayCompletionBlock != displayCompletionBlock) {
  444. _displayCompletionBlock = displayCompletionBlock;
  445. }
  446. [self setNeedsDisplay];
  447. }
  448. #pragma mark Interface State
  449. - (void)clearContents
  450. {
  451. [super clearContents];
  452. __instanceLock__.lock();
  453. _weakCacheEntry = nil; // release contents from the cache.
  454. __instanceLock__.unlock();
  455. }
  456. #pragma mark - Cropping
  457. - (BOOL)isCropEnabled
  458. {
  459. ASDN::MutexLocker l(__instanceLock__);
  460. return _cropEnabled;
  461. }
  462. - (void)setCropEnabled:(BOOL)cropEnabled
  463. {
  464. [self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds];
  465. }
  466. - (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds
  467. {
  468. ASDN::MutexLocker l(__instanceLock__);
  469. if (_cropEnabled == cropEnabled)
  470. return;
  471. _cropEnabled = cropEnabled;
  472. _cropDisplayBounds = cropBounds;
  473. // If we have an image to display, display it, respecting our recrop flag.
  474. if (self.image)
  475. {
  476. ASPerformBlockOnMainThread(^{
  477. if (recropImmediately)
  478. [self displayImmediately];
  479. else
  480. [self setNeedsDisplay];
  481. });
  482. }
  483. }
  484. - (CGRect)cropRect
  485. {
  486. ASDN::MutexLocker l(__instanceLock__);
  487. return _cropRect;
  488. }
  489. - (void)setCropRect:(CGRect)cropRect
  490. {
  491. ASDN::MutexLocker l(__instanceLock__);
  492. if (CGRectEqualToRect(_cropRect, cropRect))
  493. return;
  494. _cropRect = cropRect;
  495. // TODO: this logic needs to be updated to respect cropRect.
  496. CGSize boundsSize = self.bounds.size;
  497. CGSize imageSize = self.image.size;
  498. BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height));
  499. // Re-display if we need to.
  500. ASPerformBlockOnMainThread(^{
  501. if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage)
  502. [self setNeedsDisplay];
  503. });
  504. }
  505. - (BOOL)forceUpscaling
  506. {
  507. ASDN::MutexLocker l(__instanceLock__);
  508. return _forceUpscaling;
  509. }
  510. - (void)setForceUpscaling:(BOOL)forceUpscaling
  511. {
  512. ASDN::MutexLocker l(__instanceLock__);
  513. _forceUpscaling = forceUpscaling;
  514. }
  515. - (CGSize)forcedSize
  516. {
  517. ASDN::MutexLocker l(__instanceLock__);
  518. return _forcedSize;
  519. }
  520. - (void)setForcedSize:(CGSize)forcedSize
  521. {
  522. ASDN::MutexLocker l(__instanceLock__);
  523. _forcedSize = forcedSize;
  524. }
  525. - (asimagenode_modification_block_t)imageModificationBlock
  526. {
  527. ASDN::MutexLocker l(__instanceLock__);
  528. return _imageModificationBlock;
  529. }
  530. - (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock
  531. {
  532. ASDN::MutexLocker l(__instanceLock__);
  533. _imageModificationBlock = imageModificationBlock;
  534. }
  535. #pragma mark - Debug
  536. - (void)layout
  537. {
  538. [super layout];
  539. if (_debugLabelNode) {
  540. CGSize boundsSize = self.bounds.size;
  541. CGSize debugLabelSize = [_debugLabelNode layoutThatFits:ASSizeRangeMake(CGSizeZero, boundsSize)].size;
  542. CGPoint debugLabelOrigin = CGPointMake(boundsSize.width - debugLabelSize.width,
  543. boundsSize.height - debugLabelSize.height);
  544. _debugLabelNode.frame = (CGRect) {debugLabelOrigin, debugLabelSize};
  545. }
  546. }
  547. @end
  548. #pragma mark - Extras
  549. extern asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor)
  550. {
  551. return ^(UIImage *originalImage) {
  552. UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale);
  553. UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}];
  554. // Make the image round
  555. [roundOutline addClip];
  556. // Draw the original image
  557. [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1];
  558. // Draw a border on top.
  559. if (borderWidth > 0.0) {
  560. [borderColor setStroke];
  561. [roundOutline setLineWidth:borderWidth];
  562. [roundOutline stroke];
  563. }
  564. UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext();
  565. UIGraphicsEndImageContext();
  566. return modifiedImage;
  567. };
  568. }
  569. extern asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color)
  570. {
  571. return ^(UIImage *originalImage) {
  572. UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale);
  573. // Set color and render template
  574. [color setFill];
  575. UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  576. [templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1];
  577. UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext();
  578. UIGraphicsEndImageContext();
  579. // if the original image was stretchy, keep it stretchy
  580. if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) {
  581. modifiedImage = [modifiedImage resizableImageWithCapInsets:originalImage.capInsets resizingMode:originalImage.resizingMode];
  582. }
  583. return modifiedImage;
  584. };
  585. }