diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index 37a46ca60a..0342597aa1 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -12,6 +12,7 @@
+
diff --git a/apps/toolbox/src/pages/image-async.ts b/apps/toolbox/src/pages/image-async.ts
new file mode 100644
index 0000000000..893cd4d23f
--- /dev/null
+++ b/apps/toolbox/src/pages/image-async.ts
@@ -0,0 +1,35 @@
+import { Page, ImageSource, Observable, EventData, knownFolders, path } from '@nativescript/core';
+
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new SampleData();
+}
+
+export class SampleData extends Observable {
+ src: string = 'https://source.unsplash.com/random';
+ savedData: string = '';
+ resizedImage: ImageSource;
+ async save() {
+ try {
+ const imageSource = await ImageSource.fromUrl(this.src);
+ const tempFile = path.join(knownFolders.temp().path, `${Date.now()}.jpg`);
+ const base64 = imageSource.toBase64StringAsync('jpg');
+ const image = imageSource.saveToFileAsync(tempFile, 'jpg');
+ const resizedImage = imageSource.resizeAsync(50);
+ const results = await Promise.all([image, base64, resizedImage]);
+ const saved = results[0];
+ const base64Result = results[1];
+ if (saved) {
+ this.set('savedData', tempFile);
+ console.log('ImageAsset saved', saved, tempFile);
+ }
+ console.log('base64', base64Result);
+ console.log(results[2].width, results[2].height);
+ this.set('resizedImage', results[2]);
+ } catch (e) {
+ console.log('Failed to save ImageAsset');
+ }
+ }
+}
diff --git a/apps/toolbox/src/pages/image-async.xml b/apps/toolbox/src/pages/image-async.xml
new file mode 100644
index 0000000000..5e815c46ca
--- /dev/null
+++ b/apps/toolbox/src/pages/image-async.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/image-source/index.android.ts b/packages/core/image-source/index.android.ts
index 6bdee43a79..45542524e0 100644
--- a/packages/core/image-source/index.android.ts
+++ b/packages/core/image-source/index.android.ts
@@ -316,6 +316,29 @@ export class ImageSource implements ImageSourceDefinition {
return res;
}
+ public saveToFileAsync(path: string, format: 'png' | 'jpeg' | 'jpg', quality = 100): Promise {
+ return new Promise((resolve, reject) => {
+ org.nativescript.widgets.Utils.saveToFileAsync(
+ this.android,
+ path,
+ format,
+ quality,
+ new org.nativescript.widgets.Utils.AsyncImageCallback({
+ onSuccess(param0: boolean) {
+ resolve(param0);
+ },
+ onError(param0: java.lang.Exception) {
+ if (param0) {
+ reject(param0.getMessage());
+ } else {
+ reject();
+ }
+ },
+ })
+ );
+ });
+ }
+
public toBase64String(format: 'png' | 'jpeg' | 'jpg', quality = 100): string {
if (!this.android) {
return null;
@@ -334,12 +357,56 @@ export class ImageSource implements ImageSourceDefinition {
return outputStream.toString();
}
+ public toBase64StringAsync(format: 'png' | 'jpeg' | 'jpg', quality = 100): Promise {
+ return new Promise((resolve, reject) => {
+ org.nativescript.widgets.Utils.toBase64StringAsync(
+ this.android,
+ format,
+ quality,
+ new org.nativescript.widgets.Utils.AsyncImageCallback({
+ onSuccess(param0: string) {
+ resolve(param0);
+ },
+ onError(param0: java.lang.Exception) {
+ if (param0) {
+ reject(param0.getMessage());
+ } else {
+ reject();
+ }
+ },
+ })
+ );
+ });
+ }
+
public resize(maxSize: number, options?: any): ImageSource {
const dim = getScaledDimensions(this.android.getWidth(), this.android.getHeight(), maxSize);
const bm: android.graphics.Bitmap = android.graphics.Bitmap.createScaledBitmap(this.android, dim.width, dim.height, options && options.filter);
return new ImageSource(bm);
}
+
+ public resizeAsync(maxSize: number, options?: any): Promise {
+ return new Promise((resolve, reject) => {
+ org.nativescript.widgets.Utils.resizeAsync(
+ this.android,
+ maxSize,
+ JSON.stringify(options || {}),
+ new org.nativescript.widgets.Utils.AsyncImageCallback({
+ onSuccess(param0: any) {
+ resolve(new ImageSource(param0));
+ },
+ onError(param0: java.lang.Exception) {
+ if (param0) {
+ reject(param0.getMessage());
+ } else {
+ reject();
+ }
+ },
+ })
+ );
+ });
+ }
}
function getTargetFormat(format: 'png' | 'jpeg' | 'jpg'): android.graphics.Bitmap.CompressFormat {
diff --git a/packages/core/image-source/index.d.ts b/packages/core/image-source/index.d.ts
index a22f823800..55037abc92 100644
--- a/packages/core/image-source/index.d.ts
+++ b/packages/core/image-source/index.d.ts
@@ -198,6 +198,14 @@ export class ImageSource {
*/
saveToFile(path: string, format: 'png' | 'jpeg' | 'jpg', quality?: number): boolean;
+ /**
+ * Saves this instance to the specified file, using the provided image format and quality asynchronously.
+ * @param path The path of the file on the file system to save to.
+ * @param format The format (encoding) of the image.
+ * @param quality Optional parameter, specifying the quality of the encoding. Defaults to the maximum available quality. Quality varies on a scale of 0 to 100.
+ */
+ saveToFileAsync(path: string, format: 'png' | 'jpeg' | 'jpg', quality?: number): Promise;
+
/**
* Converts the image to base64 encoded string, using the provided image format and quality.
* @param format The format (encoding) of the image.
@@ -205,6 +213,13 @@ export class ImageSource {
*/
toBase64String(format: 'png' | 'jpeg' | 'jpg', quality?: number): string;
+ /**
+ * Converts the image to base64 encoded string, using the provided image format and quality asynchronously.
+ * @param format The format (encoding) of the image.
+ * @param quality Optional parameter, specifying the quality of the encoding. Defaults to the maximum available quality. Quality varies on a scale of 0 to 100.
+ */
+ toBase64StringAsync(format: 'png' | 'jpeg' | 'jpg', quality?: number): Promise;
+
/**
* Returns a new ImageSource that is a resized version of this image with the same aspect ratio, but the max dimension set to the provided maxSize.
* @param maxSize The maximum pixel dimension of the resulting image.
@@ -217,6 +232,19 @@ export class ImageSource {
* bilinear filtering is typically minimal and the improved image quality is significant.
*/
resize(maxSize: number, options?: any): ImageSource;
+
+ /**
+ * Returns a new ImageSource that is a resized version of this image with the same aspect ratio, but the max dimension set to the provided maxSize asynchronously.
+ * @param maxSize The maximum pixel dimension of the resulting image.
+ * @param options Optional parameter, Only used for android, options.filter is a boolean which
+ * determines whether or not bilinear filtering should be used when scaling the bitmap.
+ * If this is true then bilinear filtering will be used when scaling which has
+ * better image quality at the cost of worse performance. If this is false then
+ * nearest-neighbor scaling is used instead which will have worse image quality
+ * but is faster. Recommended default is to set filter to 'true' as the cost of
+ * bilinear filtering is typically minimal and the improved image quality is significant.
+ */
+ resizeAsync(maxSize: number, options?: any): Promise;
}
/**
diff --git a/packages/core/image-source/index.ios.ts b/packages/core/image-source/index.ios.ts
index bb3269b920..41ab10eb19 100644
--- a/packages/core/image-source/index.ios.ts
+++ b/packages/core/image-source/index.ios.ts
@@ -317,6 +317,33 @@ export class ImageSource implements ImageSourceDefinition {
return false;
}
+ public saveToFileAsync(path: string, format: 'png' | 'jpeg' | 'jpg', quality?: number): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.ios) {
+ reject(false);
+ }
+ let isSuccess = false;
+ try {
+ if (quality) {
+ quality = (quality - 0) / (100 - 0); // Normalize quality on a scale of 0 to 1
+ }
+ 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 data = getImageData(this.ios, format, quality);
+ if (data) {
+ isSuccess = NSFileManager.defaultManager.createFileAtPathContentsAttributes(path, data, null);
+ }
+ dispatch_async(main_queue, () => {
+ resolve(isSuccess);
+ });
+ });
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
public toBase64String(format: 'png' | 'jpeg' | 'jpg', quality?: number): string {
let res = null;
if (!this.ios) {
@@ -335,6 +362,33 @@ export class ImageSource implements ImageSourceDefinition {
return res;
}
+ public toBase64StringAsync(format: 'png' | 'jpeg' | 'jpg', quality?: number): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.ios) {
+ reject(null);
+ }
+ let result = null;
+ try {
+ if (quality) {
+ quality = (quality - 0) / (100 - 0); // Normalize quality on a scale of 0 to 1
+ }
+ 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 data = getImageData(this.ios, format, quality);
+ if (data) {
+ result = data.base64Encoding();
+ }
+ dispatch_async(main_queue, () => {
+ resolve(result);
+ });
+ });
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
public resize(maxSize: number, options?: any): ImageSource {
const size: CGSize = this.ios.size;
const dim = getScaledDimensions(size.width, size.height, maxSize);
@@ -349,6 +403,31 @@ export class ImageSource implements ImageSourceDefinition {
return new ImageSource(resizedImage);
}
+
+ public resizeAsync(maxSize: number, options?: any): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.ios) {
+ reject(null);
+ }
+ 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 size: CGSize = this.ios.size;
+ const dim = getScaledDimensions(size.width, size.height, maxSize);
+
+ const newSize: CGSize = CGSizeMake(dim.width, dim.height);
+
+ UIGraphicsBeginImageContextWithOptions(newSize, options?.opaque ?? false, this.ios.scale);
+ this.ios.drawInRect(CGRectMake(0, 0, newSize.width, newSize.height));
+
+ const resizedImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ dispatch_async(main_queue, () => {
+ resolve(new ImageSource(resizedImage));
+ });
+ });
+ });
+ }
}
function getFileName(path: string): string {
diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar
index 8d51acb358..c7c7e005da 100644
Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ
diff --git a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts
index fa14244999..cfe985457a 100644
--- a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts
+++ b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts
@@ -635,6 +635,9 @@ declare module org {
public static class: java.lang.Class;
public static loadImageAsync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: number, param4: number, param5: org.nativescript.widgets.Utils.AsyncImageCallback): void;
public static drawBoxShadow(param0: globalAndroid.view.View, param1: string): void;
+ public static saveToFileAsync(param0: globalAndroid.graphics.Bitmap, param1: string, param2: string, param3: number, param4: org.nativescript.widgets.Utils.AsyncImageCallback): void;
+ public static toBase64StringAsync(param0: globalAndroid.graphics.Bitmap, param1: string, param2: number, param3: org.nativescript.widgets.Utils.AsyncImageCallback): void;
+ public static resizeAsync(param0: globalAndroid.graphics.Bitmap, param1: number, param2: string, param3: org.nativescript.widgets.Utils.AsyncImageCallback): void;
public constructor();
}
export module Utils {
@@ -644,11 +647,11 @@ declare module org {
* Constructs a new instance of the org.nativescript.widgets.Utils$AsyncImageCallback interface with the provided implementation. An empty constructor exists calling super() when extending the interface class.
*/
public constructor(implementation: {
- onSuccess(param0: globalAndroid.graphics.Bitmap): void;
+ onSuccess(param0: any): void;
onError(param0: java.lang.Exception): void;
});
public constructor();
- public onSuccess(param0: globalAndroid.graphics.Bitmap): void;
+ public onSuccess(param0: any): void;
public onError(param0: java.lang.Exception): void;
}
export class ImageAssetOptions {
diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java
index 43187bd566..63ff20ed3a 100644
--- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java
+++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java
@@ -12,6 +12,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
+import android.util.Base64OutputStream;
import android.util.Log;
import android.util.Pair;
import android.view.View;
@@ -22,6 +23,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -72,7 +75,7 @@ public static void drawBoxShadow(View view, String value) {
}
public interface AsyncImageCallback {
- void onSuccess(Bitmap bitmap);
+ void onSuccess(Object bitmap);
void onError(Exception exception);
}
@@ -287,6 +290,145 @@ public void run() {
});
}
+ static Bitmap.CompressFormat getTargetFormat(String format) {
+ switch (format) {
+ case "jpeg":
+ case "jpg":
+ return Bitmap.CompressFormat.JPEG;
+ default:
+ return Bitmap.CompressFormat.PNG;
+ }
+ }
+
+
+ public static void saveToFileAsync(final Bitmap bitmap, final String path, final String format, final int quality, final AsyncImageCallback callback) {
+ executors.execute(new Runnable() {
+ @Override
+ public void run() {
+ boolean isSuccess = false;
+ Exception exception = null;
+ if (bitmap != null) {
+ Bitmap.CompressFormat targetFormat = getTargetFormat(format);
+ try (BufferedOutputStream outputStream = new BufferedOutputStream(new java.io.FileOutputStream(path))) {
+ isSuccess = bitmap.compress(targetFormat, quality, outputStream);
+ } catch (Exception e) {
+ exception = e;
+ }
+ }
+
+ final Exception finalException = exception;
+ final boolean finalIsSuccess = isSuccess;
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (finalException != null) {
+ callback.onError(finalException);
+ } else {
+ callback.onSuccess(finalIsSuccess);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ public static void toBase64StringAsync(final Bitmap bitmap, final String format, final int quality, final AsyncImageCallback callback) {
+ executors.execute(new Runnable() {
+ @Override
+ public void run() {
+ String result = null;
+ Exception exception = null;
+ if (bitmap != null) {
+
+ Bitmap.CompressFormat targetFormat = getTargetFormat(format);
+
+ try (
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Base64OutputStream base64Stream = new Base64OutputStream(outputStream, android.util.Base64.NO_WRAP)
+ ) {
+ bitmap.compress(targetFormat, quality, base64Stream);
+ result = outputStream.toString();
+ } catch (Exception e) {
+ exception = e;
+ }
+ }
+
+ final Exception finalException = exception;
+ final String finalResult = result;
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (finalException != null) {
+ callback.onError(finalException);
+ } else {
+ callback.onSuccess(finalResult);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ static Pair getScaledDimensions(float width, float height, float maxSize) {
+ if (height >= width) {
+ if (height <= maxSize) {
+ // if image already smaller than the required height
+ return new Pair<>((int) width, (int) height);
+ }
+
+ return new Pair<>(
+ Math.round((maxSize * width) / height)
+ , (int) height);
+ }
+
+ if (width <= maxSize) {
+ // if image already smaller than the required width
+ return new Pair<>((int) width, (int) height);
+ }
+
+ return new Pair<>((int) maxSize, Math.round((maxSize * height) / width));
+
+ }
+
+ public static void resizeAsync(final Bitmap bitmap, final float maxSize, final String options, final AsyncImageCallback callback) {
+ executors.execute(new Runnable() {
+ @Override
+ public void run() {
+ Bitmap result = null;
+ Exception exception = null;
+ if (bitmap != null) {
+ Pair dim = getScaledDimensions(bitmap.getWidth(), bitmap.getHeight(), maxSize);
+ boolean filter = false;
+ if (options != null) {
+ try {
+ JSONObject json = new JSONObject(options);
+ filter = json.optBoolean("filter", false);
+ } catch (JSONException ignored) {
+ }
+ }
+ try {
+ result = android.graphics.Bitmap.createScaledBitmap(bitmap, dim.first, dim.second, filter);
+ } catch (Exception e) {
+ exception = e;
+ }
+ }
+
+ final Exception finalException = exception;
+ final Bitmap finalResult = result;
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (finalException != null) {
+ callback.onError(finalException);
+ } else {
+ callback.onSuccess(finalResult);
+ }
+ }
+ });
+ }
+ });
+ }
+
// public static void clearBoxShadow(View view) {
// if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
// return;