diff --git a/packages/core/image-source/index.d.ts b/packages/core/image-source/index.d.ts index 55037abc92..7a64536f52 100644 --- a/packages/core/image-source/index.d.ts +++ b/packages/core/image-source/index.d.ts @@ -1,6 +1,22 @@ import { ImageAsset } from '../image-asset'; import { Font } from '../ui/styling/font'; import { Color } from '../color'; + +export interface ImageSourceLoadOptions { + /** + * ios specific options + */ + ios?: { + /** + * The desired scale of the image. + * By default it is set by the system based on a few factors: + * if the image is loaded from a file, the scale is 1.0, except if you have a file@2x.png or the file itself ends in a @2x.png + * in which case it will set the scale to 2 (and so on, depending on the scale of the device). + * For everything else, it'll be 1.0. + */ + scale?: number; + }; +} /** * Encapsulates the common abstraction behind a platform specific object (typically a Bitmap) that is used as a source for images. */ @@ -46,55 +62,55 @@ export class ImageSource { * Loads this instance from the specified resource name. * @param name The name of the resource (without its extension). */ - static fromResourceSync(name: string): ImageSource; + static fromResourceSync(name: string, options?: ImageSourceLoadOptions): ImageSource; /** * Loads this instance from the specified resource name asynchronously. * @param name The name of the resource (without its extension). */ - static fromResource(name: string): Promise; + static fromResource(name: string, options?: ImageSourceLoadOptions): Promise; /** * Loads this instance from the specified file. * @param path The location of the file on the file system. */ - static fromFileSync(path: string): ImageSource; + static fromFileSync(path: string, options?: ImageSourceLoadOptions): ImageSource; /** * Loads this instance from the specified file asynchronously. * @param path The location of the file on the file system. */ - static fromFile(path: string): Promise; + static fromFile(path: string, options?: ImageSourceLoadOptions): Promise; /** * Creates a new ImageSource instance and loads it from the specified local file or resource (if specified with the "res://" prefix). * @param path The location of the file on the file system. */ - static fromFileOrResourceSync(path: string): ImageSource; + static fromFileOrResourceSync(path: string, options?: ImageSourceLoadOptions): ImageSource; /** * Loads this instance from the specified native image data. * @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS. */ - static fromDataSync(data: any): ImageSource; + static fromDataSync(data: any, options?: ImageSourceLoadOptions): ImageSource; /** * Loads this instance from the specified native image data asynchronously. * @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS. */ - static fromData(data: any): Promise; + static fromData(data: any, options?: ImageSourceLoadOptions): Promise; /** * Loads this instance from the specified base64 encoded string. * @param source The Base64 string to load the image from. */ - static fromBase64Sync(source: string): ImageSource; + static fromBase64Sync(source: string, options?: ImageSourceLoadOptions): ImageSource; /** * Loads this instance from the specified base64 encoded string asynchronously. * @param source The Base64 string to load the image from. */ - static fromBase64(source: string): Promise; + static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise; /** * Creates a new ImageSource instance and loads it from the specified font icon code. @@ -102,7 +118,7 @@ export class ImageSource { * @param font The font for the corresponding font icon code * @param color The color of the generated icon image */ - static fromFontIconCodeSync(source: string, font: Font, color: Color): ImageSource; + static fromFontIconCodeSync(source: string, font: Font, color: Color, options?: ImageSourceLoadOptions): ImageSource; /** * Creates a new ImageSource instance and sets the provided native source object (typically a Bitmap). diff --git a/packages/core/image-source/index.ios.ts b/packages/core/image-source/index.ios.ts index fd8fbf6df4..a0ec553d42 100644 --- a/packages/core/image-source/index.ios.ts +++ b/packages/core/image-source/index.ios.ts @@ -1,5 +1,5 @@ // Definitions. -import { ImageSource as ImageSourceDefinition } from '.'; +import type { ImageSource as ImageSourceDefinition, ImageSourceLoadOptions } from '.'; import { ImageAsset } from '../image-asset'; import * as httpModule from '../http'; import { Font } from '../ui/styling/font'; @@ -73,51 +73,77 @@ export class ImageSource implements ImageSourceDefinition { return http.getImage(url); } - static fromResourceSync(name: string): ImageSource { - const nativeSource = (UIImage).tns_safeImageNamed(name) || (UIImage).tns_safeImageNamed(`${name}.jpg`); + static fromResourceSync(name: string, options?: ImageSourceLoadOptions): ImageSource { + const scale = options?.ios?.scale; + const nativeSource = typeof scale === 'number' ? (UIImage).tns_safeImageNamedScale(name, scale) || (UIImage).tns_safeImageNamedScale(`${name}.jpg`, scale) : (UIImage).tns_safeImageNamed(name) || (UIImage).tns_safeImageNamed(`${name}.jpg`); return nativeSource ? new ImageSource(nativeSource) : null; } - static fromResource(name: string): Promise { + static fromResource(name: string, options?: ImageSourceLoadOptions): Promise { + const scale = options?.ios?.scale; return new Promise((resolve, reject) => { try { - (UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => { - if (image) { - resolve(new ImageSource(image)); - } else { - (UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, (img) => { - if (img) { - resolve(new ImageSource(img)); - } - }); - } - }); + if (typeof scale === 'number') { + (UIImage).tns_safeDecodeImageNamedScaleCompletion(name, (image) => { + if (image) { + resolve(new ImageSource(image)); + } else { + (UIImage).tns_safeDecodeImageNamedScaleCompletion(`${name}.jpg`, (img) => { + if (img) { + resolve(new ImageSource(img)); + } + }); + } + }); + } else { + (UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => { + if (image) { + resolve(new ImageSource(image)); + } else { + (UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, (img) => { + if (img) { + resolve(new ImageSource(img)); + } + }); + } + }); + } } catch (ex) { reject(ex); } }); } - static fromFileSync(path: string): ImageSource { - const uiImage = UIImage.imageWithContentsOfFile(getFileName(path)); + static fromFileSync(path: string, options?: ImageSourceLoadOptions): ImageSource { + const scale = options?.ios?.scale; + const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(NSData.dataWithContentsOfFile(getFileName(path)), scale) : UIImage.imageWithContentsOfFile(getFileName(path)); return uiImage ? new ImageSource(uiImage) : null; } - static fromFile(path: string): Promise { + static fromFile(path: string, options?: ImageSourceLoadOptions): Promise { + const scale = options?.ios?.scale; return new Promise((resolve, reject) => { try { - (UIImage).tns_decodeImageWidthContentsOfFileCompletion(getFileName(path), (uiImage) => { - if (uiImage) { - resolve(new ImageSource(uiImage)); - } - }); + if (typeof scale === 'number') { + (UIImage).tns_decodeImageWidthContentsOfFileScaleCompletion(getFileName(path), scale, (uiImage) => { + if (uiImage) { + resolve(new ImageSource(uiImage)); + } + }); + } else { + (UIImage).tns_decodeImageWidthContentsOfFileCompletion(getFileName(path), (uiImage) => { + if (uiImage) { + resolve(new ImageSource(uiImage)); + } + }); + } } catch (ex) { reject(ex); } }); } - static fromFileOrResourceSync(path: string): ImageSource { + static fromFileOrResourceSync(path: string, options?: ImageSourceLoadOptions): ImageSource { if (!isFileOrResourcePath(path)) { if (Trace.isEnabled()) { Trace.write('Path "' + path + '" is not a valid file or resource.', Trace.categories.Binding, Trace.messageType.error); @@ -126,48 +152,64 @@ export class ImageSource implements ImageSourceDefinition { } if (path.indexOf(RESOURCE_PREFIX) === 0) { - return ImageSource.fromResourceSync(path.substr(RESOURCE_PREFIX.length)); + return ImageSource.fromResourceSync(path.substr(RESOURCE_PREFIX.length), options); } - return ImageSource.fromFileSync(path); + return ImageSource.fromFileSync(path, options); } - static fromDataSync(data: any): ImageSource { - const uiImage = UIImage.imageWithData(data); + static fromDataSync(data: any, options?: ImageSourceLoadOptions): ImageSource { + const scale = options?.ios?.scale; + const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(data, scale) : UIImage.imageWithData(data); return uiImage ? new ImageSource(uiImage) : null; } - static fromData(data: any): Promise { + static fromData(data: any, options?: ImageSourceLoadOptions): Promise { + const scale = options?.ios?.scale; return new Promise((resolve, reject) => { try { - (UIImage).tns_decodeImageWithDataCompletion(data, (uiImage) => { - if (uiImage) { - resolve(new ImageSource(uiImage)); - } - }); + if (typeof scale === 'number') { + (UIImage).tns_decodeImageWithDataScaleCompletion(data, scale, (uiImage) => { + if (uiImage) { + resolve(new ImageSource(uiImage)); + } + }); + } else { + (UIImage).tns_decodeImageWithDataCompletion(data, (uiImage) => { + if (uiImage) { + resolve(new ImageSource(uiImage)); + } + }); + } } catch (ex) { reject(ex); } }); } - static fromBase64Sync(source: string): ImageSource { + static fromBase64Sync(source: string, options?: ImageSourceLoadOptions): ImageSource { + const scale = options?.ios?.scale; let uiImage: UIImage; if (typeof source === 'string') { const data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.IgnoreUnknownCharacters); - uiImage = UIImage.imageWithData(data); + if (typeof scale === 'number') { + uiImage = UIImage.imageWithDataScale(data, scale); + } else { + uiImage = UIImage.imageWithData(data); + } } return uiImage ? new ImageSource(uiImage) : null; } - static fromBase64(source: string): Promise { + static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise { + const scale = options?.ios?.scale; return new Promise((resolve, reject) => { try { const data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.IgnoreUnknownCharacters); const main_queue = dispatch_get_current_queue(); const background_queue = dispatch_get_global_queue(qos_class_t.QOS_CLASS_DEFAULT, 0); dispatch_async(background_queue, () => { - const uiImage = UIImage.imageWithData(data); + const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(data, scale) : UIImage.imageWithData(data); dispatch_async(main_queue, () => { resolve(new ImageSource(uiImage)); }); @@ -178,8 +220,9 @@ export class ImageSource implements ImageSourceDefinition { }); } - static fromFontIconCodeSync(source: string, font: Font, color: Color): ImageSource { + static fromFontIconCodeSync(source: string, font: Font, color: Color, options?: ImageSourceLoadOptions): ImageSource { font = font || Font.default; + const scale = typeof options?.ios?.scale === 'number' ? options.ios.scale : 0.0; // TODO: Consider making 36 font size as default for optimal look on TabView and ActionBar const attributes = { @@ -192,7 +235,7 @@ export class ImageSource implements ImageSourceDefinition { const attributedString = NSAttributedString.alloc().initWithStringAttributes(source, >attributes); - UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, 0.0); + UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, scale); attributedString.drawAtPoint(CGPointMake(0, 0)); const iconImage = UIGraphicsGetImageFromCurrentImageContext(); diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h index 9e67228be6..9b2ca15079 100644 --- a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h +++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h @@ -13,14 +13,18 @@ * It also draws the UIImage in a small thumb to force decoding potentially avoiding UI hicckups when displayed. */ + (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIImage*))callback; - ++ (void) tns_safeDecodeImageNamed: (NSString*) name scale: (CGFloat) scale completion: (void (^) (UIImage*))callback; /** * Same as imageNamed, however calls to this method are sinchronized to be thread safe in iOS8 along with calls to tns_safeImageNamed and tns_safeDecodeImageNamed:completion: * imageNamed is thread safe in iOS 9 and later so in later versions this methods simply fallbacks to imageNamed: */ + (UIImage*) tns_safeImageNamed: (NSString*) name; ++ (UIImage*) tns_safeImageNamed: (NSString*) name scale: (CGFloat) scale; + (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback; ++ (void) tns_decodeImageWithData: (NSData*) data scale: (CGFloat)scale completion: (void (^) (UIImage*))callback; + ++ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file scale: (CGFloat)scale completion: (void (^) (UIImage*))callback; + (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback; @end diff --git a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m index 135564cb93..932691c40b 100644 --- a/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m +++ b/packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m @@ -39,6 +39,19 @@ + (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIIma } }); } ++ (void) tns_safeDecodeImageNamed: (NSString*) name scale:(CGFloat)scale completion: (void (^) (UIImage*))callback { + dispatch_async(image_queue, ^(void){ + @autoreleasepool { + UIImage* image = [UIImage imageNamed: name]; + image = [UIImage imageWithCGImage:[image CGImage] scale:scale orientation:[image imageOrientation]]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + } + }); +} + (UIImage*) tns_safeImageNamed: (NSString*) name { UIImage* image; @@ -48,6 +61,15 @@ + (UIImage*) tns_safeImageNamed: (NSString*) name { return image; } ++ (UIImage*) tns_safeImageNamed: (NSString*) name scale:(CGFloat)scale { + UIImage* image; + @autoreleasepool { + image = [UIImage imageNamed: name]; + image = [UIImage imageWithCGImage:[image CGImage] scale:scale orientation:[image imageOrientation]]; + } + return image; +} + + (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback { dispatch_async(image_queue, ^(void) { @autoreleasepool { @@ -61,6 +83,19 @@ + (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage* }); } ++ (void) tns_decodeImageWithData:(NSData *)data scale:(CGFloat)scale completion:(void (^)(UIImage *))callback { + dispatch_async(image_queue, ^(void) { + @autoreleasepool { + UIImage* image = [UIImage imageWithData: data scale: scale]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + } + }); +} + + (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback { dispatch_async(image_queue, ^(void) { @autoreleasepool { @@ -74,4 +109,17 @@ + (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void }); } ++ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file scale: (CGFloat) scale completion: (void (^) (UIImage*))callback { + dispatch_async(image_queue, ^(void) { + @autoreleasepool { + UIImage* image = [UIImage imageWithData:[NSData dataWithContentsOfFile:file] scale:scale]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + } + }); +} + @end