feat(image-source): add saveToFileAsync, toBase64StringAsync & resizeAsync (#9404)

This commit is contained in:
Osei Fortune
2021-08-11 10:28:06 -07:00
committed by Nathan Walker
parent 3aff057b99
commit b2f792324d
9 changed files with 371 additions and 3 deletions

View File

@ -12,6 +12,7 @@
<Button text="a11y" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo"/>
<Button text="css-playground" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo"/>
<Button text="visibility-vs-hidden" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo"/>
<Button text="image-async" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo"/>
</StackLayout>
</ScrollView>
</StackLayout>

View File

@ -0,0 +1,35 @@
import { Page, ImageSource, Observable, EventData, knownFolders, path } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
page = <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');
}
}
}

View File

@ -0,0 +1,13 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<GridLayout padding="20">
<ScrollView>
<StackLayout>
<Button text="Image Async?" tap="{{save}}" />
<Image width="200" height="200" src="{{ src }}"/>
<Image marginTop="10" width="200" height="200" src="{{ savedData }}" />
<Image marginTop="10" width="50" height="50" imageSource="{{ resizedImage }}" />
</StackLayout>
</ScrollView>
</GridLayout>
</Page>

View File

@ -316,6 +316,29 @@ export class ImageSource implements ImageSourceDefinition {
return res;
}
public saveToFileAsync(path: string, format: 'png' | 'jpeg' | 'jpg', quality = 100): Promise<boolean> {
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<string> {
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<ImageSource> {
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 {

View File

@ -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<boolean>;
/**
* 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<string>;
/**
* 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<ImageSource>;
}
/**

View File

@ -317,6 +317,33 @@ export class ImageSource implements ImageSourceDefinition {
return false;
}
public saveToFileAsync(path: string, format: 'png' | 'jpeg' | 'jpg', quality?: number): Promise<boolean> {
return new Promise<boolean>((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<string> {
return new Promise<string>((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<ImageSource> {
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 {

View File

@ -635,6 +635,9 @@ declare module org {
public static class: java.lang.Class<org.nativescript.widgets.Utils>;
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 {

View File

@ -12,6 +12,7 @@ import android.net.Uri;
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 androidx.exifinterface.media.ExifInterface;
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 class Utils {
}
public interface AsyncImageCallback {
void onSuccess(Bitmap bitmap);
void onSuccess(Object bitmap);
void onError(Exception exception);
}
@ -287,6 +290,145 @@ public class Utils {
});
}
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<Integer, Integer> 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<Integer, Integer> 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;