Skip to content

Commit 86e0f9f

Browse files
committed
Implement RCTUIImage
1 parent 7437065 commit 86e0f9f

File tree

3 files changed

+102
-71
lines changed

3 files changed

+102
-71
lines changed

packages/react-native/React/Base/RCTUIKit.h

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ extern "C" {
264264

265265
// UIGraphics.h
266266
CGContextRef UIGraphicsGetCurrentContext(void);
267-
CGImageRef UIImageGetCGImageRef(NSImage *image);
268267

269268
#ifdef __cplusplus
270269
}
@@ -330,28 +329,39 @@ NS_INLINE NSEdgeInsets UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat botto
330329
#define UIApplication NSApplication
331330

332331
// UIImage
333-
@compatibility_alias UIImage NSImage;
332+
// RCTUIImage is a subclass of NSImage that caches its CGImage representation.
333+
// This is needed because NSImage's CGImageForProposedRect: returns a new autoreleased
334+
// CGImage each time, which causes issues when used with CALayer.contents.
335+
@interface RCTUIImage : NSImage
336+
@property (nonatomic, readonly, nullable) CGImageRef CGImage;
337+
@end
338+
339+
@compatibility_alias UIImage RCTUIImage;
334340

335341
typedef NS_ENUM(NSInteger, UIImageRenderingMode) {
336342
UIImageRenderingModeAlwaysOriginal,
337343
UIImageRenderingModeAlwaysTemplate,
338344
};
339345

340346
#ifdef __cplusplus
341-
extern "C"
347+
extern "C" {
342348
#endif
343-
CGFloat UIImageGetScale(NSImage *image);
344349

345-
CGImageRef UIImageGetCGImageRef(NSImage *image);
350+
CGFloat UIImageGetScale(RCTUIImage *image);
351+
CGImageRef UIImageGetCGImageRef(RCTUIImage *image);
352+
353+
#ifdef __cplusplus
354+
}
355+
#endif
346356

347-
NS_INLINE UIImage *UIImageWithContentsOfFile(NSString *filePath)
357+
NS_INLINE RCTUIImage *UIImageWithContentsOfFile(NSString *filePath)
348358
{
349-
return [[NSImage alloc] initWithContentsOfFile:filePath];
359+
return [[RCTUIImage alloc] initWithContentsOfFile:filePath];
350360
}
351361

352-
NS_INLINE UIImage *UIImageWithData(NSData *imageData)
362+
NS_INLINE RCTUIImage *UIImageWithData(NSData *imageData)
353363
{
354-
return [[NSImage alloc] initWithData:imageData];
364+
return [[RCTUIImage alloc] initWithData:imageData];
355365
}
356366

357367
NSData *UIImagePNGRepresentation(NSImage *image);
@@ -624,7 +634,7 @@ typedef void (^RCTUIGraphicsImageDrawingActions)(RCTUIGraphicsImageRendererConte
624634

625635
- (instancetype)initWithSize:(CGSize)size;
626636
- (instancetype)initWithSize:(CGSize)size format:(RCTUIGraphicsImageRendererFormat *)format;
627-
- (NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions;
637+
- (RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions;
628638

629639
@end
630640
NS_ASSUME_NONNULL_END

packages/react-native/React/Base/macOS/RCTUIKit.m

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ CGContextRef UIGraphicsGetCurrentContext(void)
5757

5858
// UIImage
5959

60-
CGFloat UIImageGetScale(NSImage *image)
60+
CGFloat UIImageGetScale(RCTUIImage *image)
6161
{
6262
if (image == nil) {
6363
return 0.0;
@@ -76,9 +76,33 @@ CGFloat UIImageGetScale(NSImage *image)
7676
return 1.0;
7777
}
7878

79-
CGImageRef __nullable UIImageGetCGImageRef(NSImage *image)
79+
// RCTUIImage - NSImage subclass with cached CGImage
80+
81+
@implementation RCTUIImage {
82+
CGImageRef _cachedCGImage;
83+
}
84+
85+
- (void)dealloc {
86+
if (_cachedCGImage != NULL) {
87+
CGImageRelease(_cachedCGImage);
88+
}
89+
}
90+
91+
- (CGImageRef)CGImage {
92+
if (_cachedCGImage == NULL) {
93+
CGImageRef cgImage = [self CGImageForProposedRect:NULL context:NULL hints:NULL];
94+
if (cgImage != NULL) {
95+
_cachedCGImage = CGImageRetain(cgImage);
96+
}
97+
}
98+
return _cachedCGImage;
99+
}
100+
101+
@end
102+
103+
CGImageRef __nullable UIImageGetCGImageRef(RCTUIImage *image)
80104
{
81-
return [image CGImageForProposedRect:NULL context:NULL hints:NULL];
105+
return image.CGImage;
82106
}
83107

84108
static NSData *NSImageDataForFileType(NSImage *image, NSBitmapImageFileType fileType, NSDictionary<NSString *, id> *properties)
@@ -825,22 +849,54 @@ - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull RCTUIGraphicsI
825849
return self;
826850
}
827851

828-
- (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions {
829-
NSImage *image = [NSImage imageWithSize:_size
830-
flipped:YES
831-
drawingHandler:^BOOL(NSRect dstRect) {
832-
RCTUIGraphicsImageRendererContext *context = [NSGraphicsContext currentContext];
833-
if (self->_format.opaque) {
834-
CGContextSetAlpha([context CGContext], 1.0);
835-
}
836-
actions(context);
837-
return YES;
838-
}];
852+
- (nonnull RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions {
853+
// Create an RCTUIImage which caches its CGImage for efficient layer.contents usage.
854+
// We draw into a bitmap context and create the image from that.
855+
856+
CGFloat scale = _format.scale > 0 ? _format.scale : [[NSScreen mainScreen] backingScaleFactor];
857+
NSInteger pixelWidth = (NSInteger)(_size.width * scale);
858+
NSInteger pixelHeight = (NSInteger)(_size.height * scale);
859+
860+
if (pixelWidth <= 0 || pixelHeight <= 0) {
861+
return [[RCTUIImage alloc] initWithSize:_size];
862+
}
863+
864+
// Create a bitmap context
865+
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc]
866+
initWithBitmapDataPlanes:NULL
867+
pixelsWide:pixelWidth
868+
pixelsHigh:pixelHeight
869+
bitsPerSample:8
870+
samplesPerPixel:4
871+
hasAlpha:YES
872+
isPlanar:NO
873+
colorSpaceName:NSCalibratedRGBColorSpace
874+
bytesPerRow:0
875+
bitsPerPixel:0];
876+
877+
bitmapRep.size = _size;
878+
879+
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapRep];
880+
[NSGraphicsContext saveGraphicsState];
881+
[NSGraphicsContext setCurrentContext:context];
882+
883+
// Flip the context to match iOS coordinate system (origin at top-left)
884+
CGContextRef cgContext = [context CGContext];
885+
CGContextTranslateCTM(cgContext, 0, _size.height);
886+
CGContextScaleCTM(cgContext, 1.0, -1.0);
887+
888+
if (_format.opaque) {
889+
CGContextSetAlpha(cgContext, 1.0);
890+
}
891+
892+
// Execute the drawing actions
893+
actions(context);
894+
895+
[NSGraphicsContext restoreGraphicsState];
839896

840-
// Force the image to render immediately by locking focus.
841-
// This creates the backing store and makes CGImageForProposedRect work reliably.
842-
[image lockFocus];
843-
[image unlockFocus];
897+
// Create an RCTUIImage from the bitmap representation
898+
RCTUIImage *image = [[RCTUIImage alloc] initWithSize:_size];
899+
[image addRepresentation:bitmapRep];
844900

845901
return image;
846902
}

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ @implementation RCTViewComponentView {
4848
__weak CALayer *_borderLayer;
4949
CALayer *_outlineLayer;
5050
CALayer *_boxShadowLayer;
51-
#if TARGET_OS_OSX // [macOS
52-
UIImage *_boxShadowImage; // Strong reference to keep CGImage valid for layer.contents
53-
UIImage *_borderImage; // Strong reference to keep CGImage valid for layer.contents
54-
UIImage *_outlineImage; // Strong reference to keep CGImage valid for layer.contents
55-
#endif // macOS]
5651
CALayer *_filterLayer;
5752
NSMutableArray<CALayer *> *_backgroundImageLayers;
5853
BOOL _needsInvalidateLayer;
@@ -837,29 +832,16 @@ static RCTCornerRadii RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRad
837832
}
838833

839834
// To be used for CSS properties like `border` and `outline`.
840-
// On macOS, outImage is set to the generated UIImage so the caller can keep a strong reference.
841835
static void RCTAddContourEffectToLayer(
842836
CALayer *layer,
843837
const RCTCornerRadii &cornerRadii,
844838
const RCTBorderColors &contourColors,
845839
const UIEdgeInsets &contourInsets,
846-
const RCTBorderStyle &contourStyle
847-
#if TARGET_OS_OSX
848-
, UIImage * __strong *outImage // [macOS]
849-
#endif
850-
)
840+
const RCTBorderStyle &contourStyle)
851841
{
852842
UIImage *image = RCTGetBorderImage(
853843
contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [RCTUIColor clearColor], NO); // [macOS]
854844

855-
#if TARGET_OS_OSX // [macOS
856-
// Return the image to the caller so they can keep a strong reference.
857-
// This prevents the CGImage from being deallocated while the layer uses it.
858-
if (outImage) {
859-
*outImage = image;
860-
}
861-
#endif // macOS]
862-
863845
if (image == nil) {
864846
layer.contents = nil;
865847
} else {
@@ -872,7 +854,9 @@ static void RCTAddContourEffectToLayer(
872854
layer.contents = (id)image.CGImage;
873855
layer.contentsScale = image.scale;
874856
#else // [macOS
875-
layer.contents = (__bridge id)UIImageGetCGImageRef(image);
857+
// RCTUIImage caches its CGImage, so it stays valid as long as the image is alive.
858+
// The image is retained by the layer.contents assignment.
859+
layer.contents = (__bridge id)image.CGImage;
876860
layer.contentsScale = UIImageGetScale(image);
877861
#endif // macOS]
878862

@@ -1206,9 +1190,6 @@ - (void)invalidateLayer
12061190
if (useCoreAnimationBorderRendering) {
12071191
[_borderLayer removeFromSuperlayer];
12081192
_borderLayer = nil;
1209-
#if TARGET_OS_OSX
1210-
_borderImage = nil; // [macOS] Clear image reference when not using custom border layer
1211-
#endif
12121193

12131194
layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
12141195
RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS]
@@ -1243,19 +1224,12 @@ - (void)invalidateLayer
12431224
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
12441225
borderColors,
12451226
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
1246-
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left)
1247-
#if TARGET_OS_OSX
1248-
, &_borderImage // [macOS] Keep strong reference to prevent CGImage deallocation
1249-
#endif
1250-
);
1227+
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left));
12511228
}
12521229

12531230
// outline
12541231
[_outlineLayer removeFromSuperlayer];
12551232
_outlineLayer = nil;
1256-
#if TARGET_OS_OSX
1257-
_outlineImage = nil; // [macOS] Clear image reference when outline is removed
1258-
#endif
12591233
if (_props->outlineWidth != 0) {
12601234
if (!_outlineLayer) {
12611235
CALayer *outlineLayer = [CALayer new];
@@ -1280,11 +1254,7 @@ - (void)invalidateLayer
12801254
RCTCreateOutlineCornerRadiiFromBorderRadii(borderMetrics.borderRadii, _props->outlineWidth),
12811255
RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor},
12821256
UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth},
1283-
RCTBorderStyleFromOutlineStyle(_props->outlineStyle)
1284-
#if TARGET_OS_OSX
1285-
, &_outlineImage // [macOS] Keep strong reference to prevent CGImage deallocation
1286-
#endif
1287-
);
1257+
RCTBorderStyleFromOutlineStyle(_props->outlineStyle));
12881258
}
12891259
}
12901260

@@ -1339,9 +1309,6 @@ - (void)invalidateLayer
13391309
// box shadow
13401310
[_boxShadowLayer removeFromSuperlayer];
13411311
_boxShadowLayer = nil;
1342-
#if TARGET_OS_OSX // [macOS
1343-
_boxShadowImage = nil; // Release previous image
1344-
#endif // macOS]
13451312
if (!_props->boxShadow.empty()) {
13461313
_boxShadowLayer = [CALayer layer];
13471314
[self.layer addSublayer:_boxShadowLayer];
@@ -1358,10 +1325,8 @@ - (void)invalidateLayer
13581325
#if !TARGET_OS_OSX // [macOS]
13591326
_boxShadowLayer.contents = (id)boxShadowImage.CGImage;
13601327
#else // [macOS
1361-
// Keep a strong reference to the NSImage so that the CGImage it provides
1362-
// (via UIImageGetCGImageRef) remains valid while the layer uses it.
1363-
_boxShadowImage = boxShadowImage;
1364-
_boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(_boxShadowImage);
1328+
// RCTUIImage caches its CGImage, so it stays valid as long as the image is alive.
1329+
_boxShadowLayer.contents = (__bridge id)boxShadowImage.CGImage;
13651330
#endif // macOS]
13661331
}
13671332

0 commit comments

Comments
 (0)