Skip to content

Commit a94ec99

Browse files
Dimitar TachevADjenkov
Dimitar Tachev
authored andcommitted
Improve ImageAsset scaling (NativeScript#5110)
* Do not depend on current device screen while calculating Image Asset size. * Scale the image asset to the exact requested size. * Process image assets natively, pass keepAspectRatio based on the stretch property and Asset options. * Fixed the splashscreen resource name as it cannot be read when containing a dot. * Updated the Image Asset scale and rotate logic based on the Native one. * Make the ImageAsset size more important than the Image decode size as its more specific. * Fixed tslint errors. * Added filePath support in the ImageAsset constructor for iOS in order to unify it with the Android implementation, support for relative files and file not found support errors. * Added unit tests for ImageAssets. * Added a sample app for UI testing of image-view with ImageAsset src. * chore: apply PR comments
1 parent 27622d8 commit a94ec99

File tree

17 files changed

+262
-21
lines changed

17 files changed

+262
-21
lines changed

apps/app/splashscreen.png

35.4 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as vmModule from "./view-model";
2+
3+
var viewModel = vmModule.imageViewModel;
4+
5+
export function pageLoaded(args) {
6+
let page = args.object;
7+
page.bindingContext = viewModel;
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Page loaded="pageLoaded">
2+
<GridLayout rows="*, *">
3+
<Image row="0" src="{{ cameraImageAsset }}" stretch="aspectFill" margin="10"/>
4+
<Image row="1" src="{{ cameraImageSrc }}" stretch="aspectFill" margin="10"></Image>
5+
</GridLayout>
6+
</Page>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as dialogs from "tns-core-modules/ui/dialogs";
2+
import * as observable from "tns-core-modules/data/observable";
3+
import * as imageAssetModule from "tns-core-modules/image-asset";
4+
import { ImageSource } from 'tns-core-modules/image-source';
5+
6+
let _cameraImageAsset = null;
7+
let _cameraImageSrc = null;
8+
9+
export class ImageViewModel extends observable.Observable {
10+
11+
constructor() {
12+
super();
13+
let asset = new imageAssetModule.ImageAsset('~/splashscreen.png');
14+
asset.options = {
15+
width: 300,
16+
height: 300,
17+
keepAspectRatio: true
18+
};
19+
let source = new ImageSource();
20+
source.fromAsset(asset).then((source) => {
21+
this.set("cameraImageAsset", asset);
22+
this.set("cameraImageSrc", source);
23+
}, (error) => {
24+
console.log(error);
25+
});
26+
}
27+
28+
get cameraImageAsset(): string {
29+
return _cameraImageAsset;
30+
}
31+
32+
set cameraImageAsset(value: string) {
33+
_cameraImageAsset = value;
34+
}
35+
36+
get cameraImageSrc(): string {
37+
return _cameraImageSrc;
38+
}
39+
40+
set cameraImageSrc(value: string) {
41+
_cameraImageSrc = value;
42+
}
43+
}
44+
export var imageViewModel = new ImageViewModel();

apps/app/ui-tests-app/image-view/main-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function loadExamples() {
1616
examples.set("mode-matrix", "image-view/mode-matrix");
1717
examples.set("stretch-modes", "image-view/stretch-modes");
1818
examples.set("missing-image", "image-view/missing-image");
19+
examples.set("image-asset", "image-view/image-asset/image-asset");
1920

2021
return examples;
2122
}
Binary file not shown.
34.6 KB
Loading

tests/app/image-source/image-source-tests.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as imageSource from "tns-core-modules/image-source";
2+
import * as imageAssetModule from "tns-core-modules/image-asset";
23
import * as fs from "tns-core-modules/file-system";
34
import * as app from "tns-core-modules/application";
45
import * as TKUnit from "../TKUnit";
56
import * as platform from "tns-core-modules/platform";
67

78
const imagePath = "~/logo.png";
9+
const splashscreenPath = "~/splashscreen.png";
10+
const splashscreenWidth = 372;
11+
const splashscreenHeight = 218;
812
const smallImagePath = "~/small-image.png";
913

1014
export function testFromResource() {
@@ -65,6 +69,96 @@ export function testFromFile() {
6569
TKUnit.assert(!fs.File.exists(path), "test.png not removed");
6670
}
6771

72+
export function testFromAssetFileNotFound(done) {
73+
let asset = new imageAssetModule.ImageAsset('invalidFile.png');
74+
asset.options = {
75+
width: 0,
76+
height: 0,
77+
keepAspectRatio: true
78+
};
79+
80+
let img = imageSource.fromAsset(asset).then((source) => {
81+
done('Should not resolve with invalid file name.');
82+
}, (error) => {
83+
TKUnit.assertNotNull(error);
84+
done();
85+
});
86+
}
87+
88+
export function testFromAssetSimple(done) {
89+
let asset = new imageAssetModule.ImageAsset(splashscreenPath);
90+
asset.options = {
91+
width: 0,
92+
height: 0,
93+
keepAspectRatio: true
94+
};
95+
96+
let img = imageSource.fromAsset(asset).then((source) => {
97+
TKUnit.assertEqual(source.width, splashscreenWidth);
98+
TKUnit.assertEqual(source.height, splashscreenHeight);
99+
done();
100+
}, (error) => {
101+
done(error);
102+
});
103+
}
104+
105+
export function testFromAssetWithScaling(done) {
106+
let asset = new imageAssetModule.ImageAsset(splashscreenPath);
107+
let scaleWidth = 10;
108+
let scaleHeight = 11;
109+
asset.options = {
110+
width: scaleWidth,
111+
height: scaleHeight,
112+
keepAspectRatio: false
113+
};
114+
115+
let img = imageSource.fromAsset(asset).then((source) => {
116+
TKUnit.assertEqual(source.width, scaleWidth);
117+
TKUnit.assertEqual(source.height, scaleHeight);
118+
done();
119+
}, (error) => {
120+
done(error);
121+
});
122+
}
123+
124+
export function testFromAssetWithScalingAndAspectRatio(done) {
125+
let asset = new imageAssetModule.ImageAsset(splashscreenPath);
126+
let scaleWidth = 10;
127+
let scaleHeight = 11;
128+
asset.options = {
129+
width: scaleWidth,
130+
height: scaleHeight,
131+
keepAspectRatio: true
132+
};
133+
134+
let img = imageSource.fromAsset(asset).then((source) => {
135+
TKUnit.assertEqual(source.width, scaleWidth);
136+
TKUnit.assertEqual(source.height, 5);
137+
done();
138+
}, (error) => {
139+
done(error);
140+
});
141+
}
142+
143+
export function testFromAssetWithBiggerScaling(done) {
144+
let asset = new imageAssetModule.ImageAsset(splashscreenPath);
145+
let scaleWidth = 600;
146+
let scaleHeight = 600;
147+
asset.options = {
148+
width: scaleWidth,
149+
height: scaleHeight,
150+
keepAspectRatio: false
151+
};
152+
153+
let img = imageSource.fromAsset(asset).then((source) => {
154+
TKUnit.assertEqual(source.width, scaleWidth);
155+
TKUnit.assertEqual(source.height, scaleHeight);
156+
done();
157+
}, (error) => {
158+
done(error);
159+
});
160+
}
161+
68162
export function testNativeFields() {
69163
const img = imageSource.fromFile(imagePath);
70164
if (app.android) {

tests/app/splashscreen.png

34.6 KB
Loading

tests/app/ui/image/image-tests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ export const test_SettingImageSrcToDataURI_async = function (done) {
138138

139139
export function test_imageSourceNotResetAfterCreateUI() {
140140
let image = new ImageModule.Image();
141-
let imageSource = ImageSourceModule.fromResource("splashscreen.9");
141+
let imageSource = ImageSourceModule.fromResource("splashscreen");
142+
TKUnit.assertNotEqual(null, imageSource);
142143
image.imageSource = imageSource;
143144
helper.buildUIAndRunTest(image, () => {
144145
TKUnit.waitUntilReady(() => image.isLoaded);

tns-core-modules/image-asset/image-asset-common.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,10 @@ export function getAspectSafeDimensions(sourceWidth, sourceHeight, reqWidth, req
4343
}
4444

4545
export function getRequestedImageSize(src: { width: number, height: number }, options: definition.ImageAssetOptions): { width: number, height: number } {
46-
let reqWidth = platform.screen.mainScreen.widthDIPs;
47-
let reqHeight = platform.screen.mainScreen.heightDIPs;
48-
if (options && options.width) {
49-
reqWidth = (options.width > 0 && options.width < reqWidth) ? options.width : reqWidth;
50-
}
51-
if (options && options.height) {
52-
reqHeight = (options.height > 0 && options.height < reqHeight) ? options.height : reqHeight;
53-
}
46+
var screen = platform.screen.mainScreen;
47+
48+
var reqWidth = options.width || Math.min(src.width, screen.widthPixels);
49+
var reqHeight = options.height || Math.min(src.height, screen.heightPixels);
5450

5551
if (options && options.keepAspectRatio) {
5652
let safeAspectSize = getAspectSafeDimensions(src.width, src.height, reqWidth, reqHeight);

tns-core-modules/image-asset/image-asset.android.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as platform from "../platform";
22
import * as common from "./image-asset-common";
3+
import { path as fsPath, knownFolders } from "../file-system";
34

45
global.moduleMerge(common, exports);
56

@@ -8,7 +9,11 @@ export class ImageAsset extends common.ImageAsset {
89

910
constructor(asset: string) {
1011
super();
11-
this.android = asset;
12+
let fileName = typeof asset === "string" ? asset.trim() : "";
13+
if (fileName.indexOf("~/") === 0) {
14+
fileName = fsPath.join(knownFolders.currentApp().path, fileName.replace("~/", ""));
15+
}
16+
this.android = fileName;
1217
}
1318

1419
get android(): string {
@@ -22,27 +27,69 @@ export class ImageAsset extends common.ImageAsset {
2227
public getImageAsync(callback: (image, error) => void) {
2328
let bitmapOptions = new android.graphics.BitmapFactory.Options();
2429
bitmapOptions.inJustDecodeBounds = true;
30+
// read only the file size
2531
let bitmap = android.graphics.BitmapFactory.decodeFile(this.android, bitmapOptions);
2632
let sourceSize = {
2733
width: bitmapOptions.outWidth,
2834
height: bitmapOptions.outHeight
2935
};
3036
let requestedSize = common.getRequestedImageSize(sourceSize, this.options);
3137

32-
let sampleSize = calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height);
38+
let sampleSize = org.nativescript.widgets.image.Fetcher.calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height);
3339

3440
let finalBitmapOptions = new android.graphics.BitmapFactory.Options();
3541
finalBitmapOptions.inSampleSize = sampleSize;
3642
try {
43+
let error = null;
44+
// read as minimum bitmap as possible (slightly bigger than the requested size)
3745
bitmap = android.graphics.BitmapFactory.decodeFile(this.android, finalBitmapOptions);
38-
callback(bitmap, null);
46+
47+
if (bitmap) {
48+
if (requestedSize.width !== bitmap.getWidth() || requestedSize.height !== bitmap.getHeight()) {
49+
// scale to exact size
50+
bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, requestedSize.width, requestedSize.height, true);
51+
}
52+
53+
const rotationAngle = calculateAngleFromFile(this.android);
54+
if (rotationAngle !== 0) {
55+
const matrix = new android.graphics.Matrix();
56+
matrix.postRotate(rotationAngle);
57+
bitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
58+
}
59+
}
60+
61+
if (!bitmap) {
62+
error = "Asset '" + this.android + "' cannot be found.";
63+
}
64+
65+
callback(bitmap, error);
3966
}
4067
catch (ex) {
4168
callback(null, ex);
4269
}
4370
}
4471
}
4572

73+
var calculateAngleFromFile = function (filename: string) {
74+
let rotationAngle = 0;
75+
const ei = new android.media.ExifInterface(filename);
76+
const orientation = ei.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL);
77+
78+
switch (orientation) {
79+
case android.media.ExifInterface.ORIENTATION_ROTATE_90:
80+
rotationAngle = 90;
81+
break;
82+
case android.media.ExifInterface.ORIENTATION_ROTATE_180:
83+
rotationAngle = 180;
84+
break;
85+
case android.media.ExifInterface.ORIENTATION_ROTATE_270:
86+
rotationAngle = 270;
87+
break;
88+
}
89+
90+
return rotationAngle;
91+
}
92+
4693
var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeight) {
4794
let sampleSize = 1;
4895
let displayWidth = platform.screen.mainScreen.widthDIPs;
@@ -56,5 +103,16 @@ var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeig
56103
sampleSize *= 2;
57104
}
58105
}
106+
107+
var totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize);
108+
109+
// Anything more than 2x the requested pixels we'll sample down further
110+
var totalReqPixelsCap = reqWidth * reqHeight * 2;
111+
112+
while (totalPixels > totalReqPixelsCap) {
113+
sampleSize *= 2;
114+
totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize);
115+
}
116+
59117
return sampleSize;
60118
}

tns-core-modules/image-asset/image-asset.ios.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import * as common from "./image-asset-common";
2+
import { path as fsPath, knownFolders } from "../file-system";
23

34
global.moduleMerge(common, exports);
45

56
export class ImageAsset extends common.ImageAsset {
67
private _ios: PHAsset;
78

8-
constructor(asset: PHAsset | UIImage) {
9+
constructor(asset: string | PHAsset | UIImage) {
910
super();
10-
if (asset instanceof UIImage) {
11+
if (typeof asset === "string") {
12+
if (asset.indexOf("~/") === 0) {
13+
asset = fsPath.join(knownFolders.currentApp().path, asset.replace("~/", ""));
14+
}
15+
16+
this.nativeImage = UIImage.imageWithContentsOfFile(asset);
17+
}
18+
else if (asset instanceof UIImage) {
1119
this.nativeImage = asset
1220
}
1321
else {
@@ -24,6 +32,10 @@ export class ImageAsset extends common.ImageAsset {
2432
}
2533

2634
public getImageAsync(callback: (image, error) => void) {
35+
if (!this.ios && !this.nativeImage) {
36+
callback(null, "Asset cannot be found.");
37+
}
38+
2739
let srcWidth = this.nativeImage ? this.nativeImage.size.width : this.ios.pixelWidth;
2840
let srcHeight = this.nativeImage ? this.nativeImage.size.height : this.ios.pixelHeight;
2941
let requestedSize = common.getRequestedImageSize({ width: srcWidth, height: srcHeight }, this.options);

tns-core-modules/image-source/image-source.android.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export class ImageSource implements ImageSourceDefinition {
4444
return new Promise<ImageSource>((resolve, reject) => {
4545
asset.getImageAsync((image, err) => {
4646
if (image) {
47-
this.setRotationAngleFromFile(asset.android);
4847
this.setNativeSource(image);
4948
resolve(this);
5049
}

0 commit comments

Comments
 (0)