Skip to content

feat: allow custom image scales on iOS #10212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions packages/core/image-source/index.d.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -46,63 +62,63 @@ 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<ImageSource>;
static fromResource(name: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;

/**
* 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<ImageSource>;
static fromFile(path: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;

/**
* 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<ImageSource>;
static fromData(data: any, options?: ImageSourceLoadOptions): Promise<ImageSource>;

/**
* 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<ImageSource>;
static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;

/**
* Creates a new ImageSource instance and loads it from the specified font icon code.
* @param source The hex font icon code string
* @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).
Expand Down
123 changes: 83 additions & 40 deletions packages/core/image-source/index.ios.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -73,51 +73,77 @@ export class ImageSource implements ImageSourceDefinition {
return http.getImage(url);
}

static fromResourceSync(name: string): ImageSource {
const nativeSource = (<any>UIImage).tns_safeImageNamed(name) || (<any>UIImage).tns_safeImageNamed(`${name}.jpg`);
static fromResourceSync(name: string, options?: ImageSourceLoadOptions): ImageSource {
const scale = options?.ios?.scale;
const nativeSource = typeof scale === 'number' ? (<any>UIImage).tns_safeImageNamedScale(name, scale) || (<any>UIImage).tns_safeImageNamedScale(`${name}.jpg`, scale) : (<any>UIImage).tns_safeImageNamed(name) || (<any>UIImage).tns_safeImageNamed(`${name}.jpg`);

return nativeSource ? new ImageSource(nativeSource) : null;
}
static fromResource(name: string): Promise<ImageSource> {
static fromResource(name: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
const scale = options?.ios?.scale;
return new Promise<ImageSource>((resolve, reject) => {
try {
(<any>UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => {
if (image) {
resolve(new ImageSource(image));
} else {
(<any>UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, (img) => {
if (img) {
resolve(new ImageSource(img));
}
});
}
});
if (typeof scale === 'number') {
(<any>UIImage).tns_safeDecodeImageNamedScaleCompletion(name, (image) => {
if (image) {
resolve(new ImageSource(image));
} else {
(<any>UIImage).tns_safeDecodeImageNamedScaleCompletion(`${name}.jpg`, (img) => {
if (img) {
resolve(new ImageSource(img));
}
});
}
});
} else {
(<any>UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => {
if (image) {
resolve(new ImageSource(image));
} else {
(<any>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<ImageSource> {
static fromFile(path: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
const scale = options?.ios?.scale;
return new Promise<ImageSource>((resolve, reject) => {
try {
(<any>UIImage).tns_decodeImageWidthContentsOfFileCompletion(getFileName(path), (uiImage) => {
if (uiImage) {
resolve(new ImageSource(uiImage));
}
});
if (typeof scale === 'number') {
(<any>UIImage).tns_decodeImageWidthContentsOfFileScaleCompletion(getFileName(path), scale, (uiImage) => {
if (uiImage) {
resolve(new ImageSource(uiImage));
}
});
} else {
(<any>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);
Expand All @@ -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<ImageSource> {
static fromData(data: any, options?: ImageSourceLoadOptions): Promise<ImageSource> {
const scale = options?.ios?.scale;
return new Promise<ImageSource>((resolve, reject) => {
try {
(<any>UIImage).tns_decodeImageWithDataCompletion(data, (uiImage) => {
if (uiImage) {
resolve(new ImageSource(uiImage));
}
});
if (typeof scale === 'number') {
(<any>UIImage).tns_decodeImageWithDataScaleCompletion(data, scale, (uiImage) => {
if (uiImage) {
resolve(new ImageSource(uiImage));
}
});
} else {
(<any>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<ImageSource> {
static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
const scale = options?.ios?.scale;
return new Promise<ImageSource>((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));
});
Expand All @@ -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 = {
Expand All @@ -192,7 +235,7 @@ export class ImageSource implements ImageSourceDefinition {

const attributedString = NSAttributedString.alloc().initWithStringAttributes(source, <NSDictionary<string, any>>attributes);

UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, 0.0);
UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, scale);
attributedString.drawAtPoint(CGPointMake(0, 0));

const iconImage = UIGraphicsGetImageFromCurrentImageContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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