mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 10:01:08 +08:00
feat(image-source): add saveToFileAsync, toBase64StringAsync & resizeAsync (#9404)
This commit is contained in:

committed by
Nathan Walker

parent
3aff057b99
commit
b2f792324d
@ -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>
|
||||
|
35
apps/toolbox/src/pages/image-async.ts
Normal file
35
apps/toolbox/src/pages/image-async.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
13
apps/toolbox/src/pages/image-async.xml
Normal file
13
apps/toolbox/src/pages/image-async.xml
Normal 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>
|
@ -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 {
|
||||
|
28
packages/core/image-source/index.d.ts
vendored
28
packages/core/image-source/index.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
Binary file not shown.
@ -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 {
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user