diff --git a/apps/app/splashscreen.png b/apps/app/splashscreen.png new file mode 100644 index 000000000..61fbb7ed0 Binary files /dev/null and b/apps/app/splashscreen.png differ diff --git a/apps/app/ui-tests-app/image-view/image-asset/image-asset.ts b/apps/app/ui-tests-app/image-view/image-asset/image-asset.ts new file mode 100644 index 000000000..3d417f208 --- /dev/null +++ b/apps/app/ui-tests-app/image-view/image-asset/image-asset.ts @@ -0,0 +1,8 @@ +import * as vmModule from "./view-model"; + +var viewModel = vmModule.imageViewModel; + +export function pageLoaded(args) { + let page = args.object; + page.bindingContext = viewModel; +} \ No newline at end of file diff --git a/apps/app/ui-tests-app/image-view/image-asset/image-asset.xml b/apps/app/ui-tests-app/image-view/image-asset/image-asset.xml new file mode 100644 index 000000000..ad4053ae2 --- /dev/null +++ b/apps/app/ui-tests-app/image-view/image-asset/image-asset.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/app/ui-tests-app/image-view/image-asset/view-model.ts b/apps/app/ui-tests-app/image-view/image-asset/view-model.ts new file mode 100644 index 000000000..053c73d33 --- /dev/null +++ b/apps/app/ui-tests-app/image-view/image-asset/view-model.ts @@ -0,0 +1,44 @@ +import * as dialogs from "tns-core-modules/ui/dialogs"; +import * as observable from "tns-core-modules/data/observable"; +import * as imageAssetModule from "tns-core-modules/image-asset"; +import { ImageSource } from 'tns-core-modules/image-source'; + +let _cameraImageAsset = null; +let _cameraImageSrc = null; + +export class ImageViewModel extends observable.Observable { + + constructor() { + super(); + let asset = new imageAssetModule.ImageAsset('~/splashscreen.png'); + asset.options = { + width: 300, + height: 300, + keepAspectRatio: true + }; + let source = new ImageSource(); + source.fromAsset(asset).then((source) => { + this.set("cameraImageAsset", asset); + this.set("cameraImageSrc", source); + }, (error) => { + console.log(error); + }); + } + + get cameraImageAsset(): string { + return _cameraImageAsset; + } + + set cameraImageAsset(value: string) { + _cameraImageAsset = value; + } + + get cameraImageSrc(): string { + return _cameraImageSrc; + } + + set cameraImageSrc(value: string) { + _cameraImageSrc = value; + } +} +export var imageViewModel = new ImageViewModel(); diff --git a/apps/app/ui-tests-app/image-view/main-page.ts b/apps/app/ui-tests-app/image-view/main-page.ts index 2e714d423..40fcf5a3e 100644 --- a/apps/app/ui-tests-app/image-view/main-page.ts +++ b/apps/app/ui-tests-app/image-view/main-page.ts @@ -16,6 +16,7 @@ export function loadExamples() { examples.set("mode-matrix", "image-view/mode-matrix"); examples.set("stretch-modes", "image-view/stretch-modes"); examples.set("missing-image", "image-view/missing-image"); + examples.set("image-asset", "image-view/image-asset/image-asset"); return examples; } \ No newline at end of file diff --git a/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png b/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png deleted file mode 100644 index bd53be2ec..000000000 Binary files a/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png and /dev/null differ diff --git a/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.png b/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.png new file mode 100644 index 000000000..a157c4dda Binary files /dev/null and b/tests/app/App_Resources/Android/drawable-nodpi/splashscreen.png differ diff --git a/tests/app/App_Resources/iOS/splashscreen.png b/tests/app/App_Resources/iOS/splashscreen.png new file mode 100644 index 000000000..a157c4dda Binary files /dev/null and b/tests/app/App_Resources/iOS/splashscreen.png differ diff --git a/tests/app/image-source/image-source-tests.ts b/tests/app/image-source/image-source-tests.ts index 614bf3a6e..94a52ee2c 100644 --- a/tests/app/image-source/image-source-tests.ts +++ b/tests/app/image-source/image-source-tests.ts @@ -1,10 +1,14 @@ import * as imageSource from "tns-core-modules/image-source"; +import * as imageAssetModule from "tns-core-modules/image-asset"; import * as fs from "tns-core-modules/file-system"; import * as app from "tns-core-modules/application"; import * as TKUnit from "../TKUnit"; import * as platform from "tns-core-modules/platform"; const imagePath = "~/logo.png"; +const splashscreenPath = "~/splashscreen.png"; +const splashscreenWidth = 372; +const splashscreenHeight = 218; const smallImagePath = "~/small-image.png"; export function testFromResource() { @@ -65,6 +69,96 @@ export function testFromFile() { TKUnit.assert(!fs.File.exists(path), "test.png not removed"); } +export function testFromAssetFileNotFound(done) { + let asset = new imageAssetModule.ImageAsset('invalidFile.png'); + asset.options = { + width: 0, + height: 0, + keepAspectRatio: true + }; + + let img = imageSource.fromAsset(asset).then((source) => { + done('Should not resolve with invalid file name.'); + }, (error) => { + TKUnit.assertNotNull(error); + done(); + }); +} + +export function testFromAssetSimple(done) { + let asset = new imageAssetModule.ImageAsset(splashscreenPath); + asset.options = { + width: 0, + height: 0, + keepAspectRatio: true + }; + + let img = imageSource.fromAsset(asset).then((source) => { + TKUnit.assertEqual(source.width, splashscreenWidth); + TKUnit.assertEqual(source.height, splashscreenHeight); + done(); + }, (error) => { + done(error); + }); +} + +export function testFromAssetWithScaling(done) { + let asset = new imageAssetModule.ImageAsset(splashscreenPath); + let scaleWidth = 10; + let scaleHeight = 11; + asset.options = { + width: scaleWidth, + height: scaleHeight, + keepAspectRatio: false + }; + + let img = imageSource.fromAsset(asset).then((source) => { + TKUnit.assertEqual(source.width, scaleWidth); + TKUnit.assertEqual(source.height, scaleHeight); + done(); + }, (error) => { + done(error); + }); +} + +export function testFromAssetWithScalingAndAspectRatio(done) { + let asset = new imageAssetModule.ImageAsset(splashscreenPath); + let scaleWidth = 10; + let scaleHeight = 11; + asset.options = { + width: scaleWidth, + height: scaleHeight, + keepAspectRatio: true + }; + + let img = imageSource.fromAsset(asset).then((source) => { + TKUnit.assertEqual(source.width, scaleWidth); + TKUnit.assertEqual(source.height, 5); + done(); + }, (error) => { + done(error); + }); +} + +export function testFromAssetWithBiggerScaling(done) { + let asset = new imageAssetModule.ImageAsset(splashscreenPath); + let scaleWidth = 600; + let scaleHeight = 600; + asset.options = { + width: scaleWidth, + height: scaleHeight, + keepAspectRatio: false + }; + + let img = imageSource.fromAsset(asset).then((source) => { + TKUnit.assertEqual(source.width, scaleWidth); + TKUnit.assertEqual(source.height, scaleHeight); + done(); + }, (error) => { + done(error); + }); +} + export function testNativeFields() { const img = imageSource.fromFile(imagePath); if (app.android) { diff --git a/tests/app/splashscreen.png b/tests/app/splashscreen.png new file mode 100644 index 000000000..a157c4dda Binary files /dev/null and b/tests/app/splashscreen.png differ diff --git a/tests/app/ui/image/image-tests.ts b/tests/app/ui/image/image-tests.ts index e0ce57e4f..693ae5670 100644 --- a/tests/app/ui/image/image-tests.ts +++ b/tests/app/ui/image/image-tests.ts @@ -138,7 +138,8 @@ export const test_SettingImageSrcToDataURI_async = function (done) { export function test_imageSourceNotResetAfterCreateUI() { let image = new ImageModule.Image(); - let imageSource = ImageSourceModule.fromResource("splashscreen.9"); + let imageSource = ImageSourceModule.fromResource("splashscreen"); + TKUnit.assertNotEqual(null, imageSource); image.imageSource = imageSource; helper.buildUIAndRunTest(image, () => { TKUnit.waitUntilReady(() => image.isLoaded); diff --git a/tns-core-modules/image-asset/image-asset-common.ts b/tns-core-modules/image-asset/image-asset-common.ts index c6e35641d..b50f6594a 100644 --- a/tns-core-modules/image-asset/image-asset-common.ts +++ b/tns-core-modules/image-asset/image-asset-common.ts @@ -43,14 +43,10 @@ export function getAspectSafeDimensions(sourceWidth, sourceHeight, reqWidth, req } export function getRequestedImageSize(src: { width: number, height: number }, options: definition.ImageAssetOptions): { width: number, height: number } { - let reqWidth = platform.screen.mainScreen.widthDIPs; - let reqHeight = platform.screen.mainScreen.heightDIPs; - if (options && options.width) { - reqWidth = (options.width > 0 && options.width < reqWidth) ? options.width : reqWidth; - } - if (options && options.height) { - reqHeight = (options.height > 0 && options.height < reqHeight) ? options.height : reqHeight; - } + var screen = platform.screen.mainScreen; + + var reqWidth = options.width || Math.min(src.width, screen.widthPixels); + var reqHeight = options.height || Math.min(src.height, screen.heightPixels); if (options && options.keepAspectRatio) { let safeAspectSize = getAspectSafeDimensions(src.width, src.height, reqWidth, reqHeight); diff --git a/tns-core-modules/image-asset/image-asset.android.ts b/tns-core-modules/image-asset/image-asset.android.ts index 282bafea8..eaba56d88 100644 --- a/tns-core-modules/image-asset/image-asset.android.ts +++ b/tns-core-modules/image-asset/image-asset.android.ts @@ -1,5 +1,6 @@ import * as platform from "../platform"; import * as common from "./image-asset-common"; +import { path as fsPath, knownFolders } from "../file-system"; global.moduleMerge(common, exports); @@ -8,7 +9,11 @@ export class ImageAsset extends common.ImageAsset { constructor(asset: string) { super(); - this.android = asset; + let fileName = typeof asset === "string" ? asset.trim() : ""; + if (fileName.indexOf("~/") === 0) { + fileName = fsPath.join(knownFolders.currentApp().path, fileName.replace("~/", "")); + } + this.android = fileName; } get android(): string { @@ -22,6 +27,7 @@ export class ImageAsset extends common.ImageAsset { public getImageAsync(callback: (image, error) => void) { let bitmapOptions = new android.graphics.BitmapFactory.Options(); bitmapOptions.inJustDecodeBounds = true; + // read only the file size let bitmap = android.graphics.BitmapFactory.decodeFile(this.android, bitmapOptions); let sourceSize = { width: bitmapOptions.outWidth, @@ -29,13 +35,34 @@ export class ImageAsset extends common.ImageAsset { }; let requestedSize = common.getRequestedImageSize(sourceSize, this.options); - let sampleSize = calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height); + let sampleSize = org.nativescript.widgets.image.Fetcher.calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height); let finalBitmapOptions = new android.graphics.BitmapFactory.Options(); finalBitmapOptions.inSampleSize = sampleSize; try { + let error = null; + // read as minimum bitmap as possible (slightly bigger than the requested size) bitmap = android.graphics.BitmapFactory.decodeFile(this.android, finalBitmapOptions); - callback(bitmap, null); + + if (bitmap) { + if (requestedSize.width !== bitmap.getWidth() || requestedSize.height !== bitmap.getHeight()) { + // scale to exact size + bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, requestedSize.width, requestedSize.height, true); + } + + const rotationAngle = calculateAngleFromFile(this.android); + if (rotationAngle !== 0) { + const matrix = new android.graphics.Matrix(); + matrix.postRotate(rotationAngle); + bitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + } + + if (!bitmap) { + error = "Asset '" + this.android + "' cannot be found."; + } + + callback(bitmap, error); } catch (ex) { callback(null, ex); @@ -43,6 +70,26 @@ export class ImageAsset extends common.ImageAsset { } } +var calculateAngleFromFile = function (filename: string) { + let rotationAngle = 0; + const ei = new android.media.ExifInterface(filename); + const orientation = ei.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL); + + switch (orientation) { + case android.media.ExifInterface.ORIENTATION_ROTATE_90: + rotationAngle = 90; + break; + case android.media.ExifInterface.ORIENTATION_ROTATE_180: + rotationAngle = 180; + break; + case android.media.ExifInterface.ORIENTATION_ROTATE_270: + rotationAngle = 270; + break; + } + + return rotationAngle; +} + var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeight) { let sampleSize = 1; let displayWidth = platform.screen.mainScreen.widthDIPs; @@ -56,5 +103,16 @@ var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeig sampleSize *= 2; } } + + var totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize); + + // Anything more than 2x the requested pixels we'll sample down further + var totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels > totalReqPixelsCap) { + sampleSize *= 2; + totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize); + } + return sampleSize; } diff --git a/tns-core-modules/image-asset/image-asset.ios.ts b/tns-core-modules/image-asset/image-asset.ios.ts index 48290c1a0..b0f338014 100644 --- a/tns-core-modules/image-asset/image-asset.ios.ts +++ b/tns-core-modules/image-asset/image-asset.ios.ts @@ -1,13 +1,21 @@ import * as common from "./image-asset-common"; +import { path as fsPath, knownFolders } from "../file-system"; global.moduleMerge(common, exports); export class ImageAsset extends common.ImageAsset { private _ios: PHAsset; - constructor(asset: PHAsset | UIImage) { + constructor(asset: string | PHAsset | UIImage) { super(); - if (asset instanceof UIImage) { + if (typeof asset === "string") { + if (asset.indexOf("~/") === 0) { + asset = fsPath.join(knownFolders.currentApp().path, asset.replace("~/", "")); + } + + this.nativeImage = UIImage.imageWithContentsOfFile(asset); + } + else if (asset instanceof UIImage) { this.nativeImage = asset } else { @@ -24,6 +32,10 @@ export class ImageAsset extends common.ImageAsset { } public getImageAsync(callback: (image, error) => void) { + if (!this.ios && !this.nativeImage) { + callback(null, "Asset cannot be found."); + } + let srcWidth = this.nativeImage ? this.nativeImage.size.width : this.ios.pixelWidth; let srcHeight = this.nativeImage ? this.nativeImage.size.height : this.ios.pixelHeight; let requestedSize = common.getRequestedImageSize({ width: srcWidth, height: srcHeight }, this.options); diff --git a/tns-core-modules/image-source/image-source.android.ts b/tns-core-modules/image-source/image-source.android.ts index f8847d5b3..0f14b2e4d 100644 --- a/tns-core-modules/image-source/image-source.android.ts +++ b/tns-core-modules/image-source/image-source.android.ts @@ -44,7 +44,6 @@ export class ImageSource implements ImageSourceDefinition { return new Promise((resolve, reject) => { asset.getImageAsync((image, err) => { if (image) { - this.setRotationAngleFromFile(asset.android); this.setNativeSource(image); resolve(this); } diff --git a/tns-core-modules/ui/image/image.android.ts b/tns-core-modules/ui/image/image.android.ts index fb03c7825..1e425dd57 100644 --- a/tns-core-modules/ui/image/image.android.ts +++ b/tns-core-modules/ui/image/image.android.ts @@ -4,6 +4,7 @@ } from "./image-common"; import { knownFolders } from "../../file-system"; +import * as platform from "../../platform"; export * from "./image-common"; const FILE_PREFIX = "file:///"; @@ -82,12 +83,27 @@ export class Image extends ImageBase { } if (!value) { - imageView.setUri(null, 0, 0, false, true); + imageView.setUri(null, 0, 0, false, false, true); return; } - const async = this.loadMode === ASYNC; + let screen = platform.screen.mainScreen; + let decodeWidth = Math.min(this.decodeWidth, screen.widthPixels); + let decodeHeight = Math.min(this.decodeHeight, screen.heightPixels); + let keepAspectRatio = this._calculateKeepAspectRatio(); + if (value instanceof ImageAsset) { + if (value.options) { + decodeWidth = value.options.width || decodeWidth; + decodeHeight = value.options.height || decodeHeight; + keepAspectRatio = !!value.options.keepAspectRatio; + } + + // handle assets as file paths natively + value = value.android; + } + + const async = this.loadMode === ASYNC; if (typeof value === "string" || value instanceof String) { value = value.trim(); this.isLoading = true; @@ -97,24 +113,28 @@ export class Image extends ImageBase { super._createImageSourceFromSrc(value); } else if (isFileOrResourcePath(value)) { if (value.indexOf(RESOURCE_PREFIX) === 0) { - imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, async); + imageView.setUri(value, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, async); } else { let fileName = value; if (fileName.indexOf("~/") === 0) { fileName = knownFolders.currentApp().path + "/" + fileName.replace("~/", ""); } - imageView.setUri(FILE_PREFIX + fileName, this.decodeWidth, this.decodeHeight, this.useCache, async); + imageView.setUri(FILE_PREFIX + fileName, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, async); } } else { // For backwards compatibility http always use async loading. - imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, true); + imageView.setUri(value, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, true); } } else { super._createImageSourceFromSrc(value); } } + private _calculateKeepAspectRatio(): boolean { + return this.stretch === "fill" ? false : true; + } + [stretchProperty.getDefault](): "aspectFit" { return "aspectFit"; } diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index 85701f6d7..1d336d827 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -347,7 +347,7 @@ getRotationAngle(): number; setRotationAngle(angle: number): void; - setUri(uri: string, decodeWidth: number, decodeHeight: number, useCache: boolean, async: boolean): void; + setUri(uri: string, decodeWidth: number, decodeHeight: number, keepAspectRatio: boolean, useCache: boolean, async: boolean): void; setImageLoadedListener(listener: image.Worker.OnImageLoadedListener): void; } @@ -409,6 +409,8 @@ export class Fetcher extends Worker { private constructor(); public static getInstance(context: android.content.Context): Fetcher; + public static calculateInSampleSize(imageWidth: number, imageHeight: number, + reqWidth: number, reqHeight: number): number; public addImageCache(cache: Cache): void; public initCache(): void; public clearCache(): void;