1+ #import " ENRMImageAttachment.h"
2+ #import " RuntimeKeys.h"
3+ #import " StyleConfig.h"
4+ #import < React/RCTLog.h>
5+ #import < objc/runtime.h>
6+
7+ @interface ENRMImageAttachment ()
8+
9+ @property (nonatomic , readwrite ) NSString *imageURL;
10+ @property (nonatomic , weak ) StyleConfig *styleConfiguration;
11+ @property (nonatomic , assign ) BOOL isInline;
12+ @property (nonatomic , assign ) CGFloat cachedHeight;
13+ @property (nonatomic , assign ) CGFloat cachedBorderRadius;
14+ @property (nonatomic , weak ) NSTextContainer *textContainer;
15+ @property (nonatomic , weak ) UITextView *textView;
16+ @property (nonatomic , strong ) UIImage *originalImage;
17+ @property (nonatomic , strong ) UIImage *loadedImage;
18+ @property (nonatomic , strong ) NSURLSessionDataTask *loadingTask;
19+
20+ @end
21+
22+ @implementation ENRMImageAttachment
23+
24+ - (instancetype )initWithImageURL : (NSString *)imageURL config : (StyleConfig *)config isInline : (BOOL )isInline
25+ {
26+ self = [super init ];
27+ if (self) {
28+ _imageURL = imageURL;
29+ _styleConfiguration = config;
30+ _isInline = isInline;
31+
32+ _cachedHeight = isInline ? [config inlineImageSize ] : [config imageHeight ];
33+ _cachedBorderRadius = [config imageBorderRadius ];
34+
35+ [self setupPlaceholder ];
36+ [self startDownloadingImage ];
37+ }
38+ return self;
39+ }
40+
41+ - (CGRect)attachmentBoundsForTextContainer : (NSTextContainer *)textContainer
42+ proposedLineFragment : (CGRect)lineFragment
43+ glyphPosition : (CGPoint)position
44+ characterIndex : (NSUInteger )characterIndex
45+ {
46+ CGFloat height = self.cachedHeight ;
47+ CGFloat width = self.isInline ? height : (lineFragment.size .width > 0 ? lineFragment.size .width : height);
48+
49+ if (self.isInline ) {
50+ UIFont *appliedFont = nil ;
51+ NSLayoutManager *layoutManager = textContainer.layoutManager ;
52+ NSTextStorage *textStorage = layoutManager.textStorage ;
53+
54+ if (textStorage && characterIndex < textStorage.length ) {
55+ appliedFont = [textStorage attribute: NSFontAttributeName atIndex: characterIndex effectiveRange: NULL ];
56+ }
57+
58+ // Determine the vertical alignment:
59+ // Center against the font's Capital Height if available,
60+ // otherwise center within the line fragment.
61+ CGFloat verticalOffset;
62+ if (appliedFont) {
63+ verticalOffset = (appliedFont.capHeight - height) / 2.0 ;
64+ } else {
65+ verticalOffset = (lineFragment.size .height - height) / 2.0 ;
66+ }
67+
68+ return CGRectMake (0 , verticalOffset, width, height);
69+ }
70+
71+ return CGRectMake (0 , 0 , width, height);
72+ }
73+
74+ - (UIImage *)imageForBounds : (CGRect)imageBounds
75+ textContainer : (NSTextContainer *)textContainer
76+ characterIndex : (NSUInteger )characterIndex
77+ {
78+ self.textContainer = textContainer;
79+
80+ if (self.originalImage && imageBounds.size .width > 0 ) {
81+ CGFloat currentWidth = imageBounds.size .width ;
82+
83+ BOOL isFirstLoad = (self.loadedImage == nil );
84+ BOOL hasWidthChanged = !self.isInline && self.loadedImage && fabs (self.loadedImage .size .width - currentWidth) > 1.0 ;
85+
86+ if (isFirstLoad || hasWidthChanged) {
87+ self.bounds = imageBounds;
88+ [self processAndApplyImage: self .originalImage withTargetWidth: currentWidth];
89+ }
90+ }
91+
92+ return self.loadedImage ?: self.image ;
93+ }
94+
95+ - (void )handleLoadedImage : (UIImage *)image
96+ {
97+ if (!image) {
98+ return ;
99+ }
100+
101+ self.originalImage = image;
102+ CGFloat targetWidth = self.isInline ? self.cachedHeight : self.bounds .size .width ;
103+
104+ // If bounds width isn't known yet (image loaded before layout), defer scaling
105+ // until imageForBounds: provides the real width via the text system.
106+ if (!self.isInline && targetWidth <= self.cachedHeight ) {
107+ return ;
108+ }
109+
110+ [self processAndApplyImage: image withTargetWidth: targetWidth];
111+ }
112+
113+ - (void )processAndApplyImage : (UIImage *)image withTargetWidth : (CGFloat)targetWidth
114+ {
115+ if (targetWidth <= 0 ) {
116+ return ;
117+ }
118+
119+ __weak typeof (self) weakSelf = self;
120+ dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
121+ __strong typeof (weakSelf) strongSelf = weakSelf;
122+ if (!strongSelf)
123+ return ;
124+
125+ UIImage *processedImage = [strongSelf createScaledImage: image
126+ toWidth: targetWidth
127+ height: strongSelf.cachedHeight
128+ borderRadius: strongSelf.cachedBorderRadius];
129+
130+ dispatch_async (dispatch_get_main_queue (), ^{
131+ strongSelf.loadedImage = processedImage;
132+
133+ if (strongSelf.isInline ) {
134+ strongSelf.image = processedImage;
135+ strongSelf.bounds = CGRectMake (0 , 0 , strongSelf.cachedHeight , strongSelf.cachedHeight );
136+ } else {
137+ strongSelf.image = image;
138+ }
139+
140+ [strongSelf refreshDisplay ];
141+ });
142+ });
143+ }
144+
145+ - (UIImage *)createScaledImage : (UIImage *)image
146+ toWidth : (CGFloat)targetWidth
147+ height : (CGFloat)targetHeight
148+ borderRadius : (CGFloat)radius
149+ {
150+ CGFloat sourceWidth = image.size .width ;
151+ CGFloat sourceHeight = image.size .height ;
152+
153+ CGFloat drawingWidth, drawingHeight;
154+
155+ if (!self.isInline && sourceWidth > 0 && sourceHeight > 0 ) {
156+ CGFloat aspectRatioScale = targetWidth / sourceWidth;
157+ drawingWidth = targetWidth;
158+ drawingHeight = sourceHeight * aspectRatioScale;
159+ } else {
160+ drawingWidth = targetWidth;
161+ drawingHeight = targetHeight;
162+ }
163+
164+ CGFloat xOffset = (targetWidth - drawingWidth) / 2.0 ;
165+ CGFloat yOffset = (targetHeight - drawingHeight) / 2.0 ;
166+ CGRect drawingRect = CGRectMake (xOffset, yOffset, drawingWidth, drawingHeight);
167+
168+ UIGraphicsImageRenderer *renderer =
169+ [[UIGraphicsImageRenderer alloc ] initWithSize: CGSizeMake (targetWidth, targetHeight)];
170+
171+ return [renderer imageWithActions: ^(UIGraphicsImageRendererContext *context) {
172+ if (radius > 0 ) {
173+ CGRect clippingRect = CGRectIntersection (CGRectMake (0 , 0 , targetWidth, targetHeight), drawingRect);
174+ UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect: clippingRect cornerRadius: radius];
175+ [path addClip ];
176+ }
177+ [image drawInRect: drawingRect];
178+ }];
179+ }
180+
181+ - (void )refreshDisplay
182+ {
183+ UITextView *textView = [self fetchAssociatedTextView ];
184+ if (!textView) {
185+ return ;
186+ }
187+
188+ NSRange attachmentRange = [self findAttachmentRangeInText: textView.attributedText];
189+ if (attachmentRange.location == NSNotFound ) {
190+ return ;
191+ }
192+
193+ [textView.layoutManager invalidateDisplayForCharacterRange: attachmentRange];
194+ if (!self.isInline ) {
195+ [textView.layoutManager invalidateLayoutForCharacterRange: attachmentRange actualCharacterRange: NULL ];
196+ }
197+ }
198+
199+ - (UITextView *)fetchAssociatedTextView
200+ {
201+ if (self.textView ) {
202+ return self.textView ;
203+ }
204+
205+ if (!self.textContainer ) {
206+ return nil ;
207+ }
208+
209+ // Look up the text view via the associated object key stored on the container
210+ self.textView = objc_getAssociatedObject (self.textContainer , kTextViewKey );
211+ return self.textView ;
212+ }
213+
214+ - (void )setupPlaceholder
215+ {
216+ CGFloat size = self.cachedHeight ;
217+ self.bounds = CGRectMake (0 , 0 , size, size);
218+
219+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc ] initWithSize: CGSizeMake (size, size)];
220+ self.image = [renderer imageWithActions: ^(UIGraphicsImageRendererContext *context){
221+ // Generates an empty transparent placeholder
222+ }];
223+ }
224+
225+ - (void )startDownloadingImage
226+ {
227+ if (self.imageURL .length == 0 ) {
228+ return ;
229+ }
230+
231+ NSURL *url = [NSURL URLWithString: self .imageURL];
232+ if (!url) {
233+ return ;
234+ }
235+
236+ __weak typeof (self) weakSelf = self;
237+ self.loadingTask = [[NSURLSession sharedSession ]
238+ dataTaskWithURL: url
239+ completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) {
240+ if (data && !error) {
241+ UIImage *downloadedImage = [UIImage imageWithData: data];
242+ dispatch_async (dispatch_get_main_queue (), ^{ [weakSelf handleLoadedImage: downloadedImage]; });
243+ }
244+ }];
245+
246+ [self .loadingTask resume ];
247+ }
248+
249+ - (NSRange )findAttachmentRangeInText : (NSAttributedString *)attributedString
250+ {
251+ __block NSRange foundRange = NSMakeRange (NSNotFound , 0 );
252+
253+ [attributedString enumerateAttribute: NSAttachmentAttributeName
254+ inRange: NSMakeRange (0 , attributedString.length)
255+ options: 0
256+ usingBlock: ^(id value, NSRange range, BOOL *stop) {
257+ if (value == self) {
258+ foundRange = range;
259+ *stop = YES ;
260+ }
261+ }];
262+
263+ return foundRange;
264+ }
265+
266+ - (void )dealloc
267+ {
268+ [_loadingTask cancel ];
269+ }
270+
271+ @end
0 commit comments