implemented upload file image for new event

This commit is contained in:
arnaucode
2017-06-23 16:45:13 +02:00
parent baa74e1921
commit 82cceaff53
247 changed files with 51054 additions and 49 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.camera;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.v4.content.FileProvider;
import java.io.File;
/*
* This class exists because Andorid FilesProvider doesn't work on Android 4.4.4 and below and throws
* weird errors. I'm not sure why writing to shared cache directories is somehow verboten, but it is
* and this error is irritating for a Compatibility library to have.
*
*/
public class CordovaUri {
private Uri androidUri;
private String fileName;
private Uri fileUri;
/*
* We always expect a FileProvider string to be passed in for the file that we create
*
*/
CordovaUri (Uri inputUri)
{
//Determine whether the file is a content or file URI
if(inputUri.getScheme().equals("content"))
{
androidUri = inputUri;
fileName = getFileNameFromUri(androidUri);
fileUri = Uri.parse("file://" + fileName);
}
else
{
fileUri = inputUri;
fileName = FileHelper.stripFileProtocol(inputUri.toString());
}
}
public Uri getFileUri()
{
return fileUri;
}
public String getFilePath()
{
return fileName;
}
/*
* This only gets called by takePicture
*/
public Uri getCorrectUri()
{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return androidUri;
else
return fileUri;
}
/*
* This is dirty, but it does the job.
*
* Since the FilesProvider doesn't really provide you a way of getting a URL from the file,
* and since we actually need the Camera to create the file for us most of the time, we don't
* actually write the file, just generate the location based on a timestamp, we need to get it
* back from the Intent.
*
* However, the FilesProvider preserves the path, so we can at least write to it from here, since
* we own the context in this case.
*/
private String getFileNameFromUri(Uri uri) {
String fullUri = uri.toString();
String partial_path = fullUri.split("external_files")[1];
File external_storage = Environment.getExternalStorageDirectory();
String path = external_storage.getAbsolutePath() + partial_path;
return path;
}
}

View File

@@ -0,0 +1,185 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.camera;
import java.io.IOException;
import android.media.ExifInterface;
public class ExifHelper {
private String aperture = null;
private String datetime = null;
private String exposureTime = null;
private String flash = null;
private String focalLength = null;
private String gpsAltitude = null;
private String gpsAltitudeRef = null;
private String gpsDateStamp = null;
private String gpsLatitude = null;
private String gpsLatitudeRef = null;
private String gpsLongitude = null;
private String gpsLongitudeRef = null;
private String gpsProcessingMethod = null;
private String gpsTimestamp = null;
private String iso = null;
private String make = null;
private String model = null;
private String orientation = null;
private String whiteBalance = null;
private ExifInterface inFile = null;
private ExifInterface outFile = null;
/**
* The file before it is compressed
*
* @param filePath
* @throws IOException
*/
public void createInFile(String filePath) throws IOException {
this.inFile = new ExifInterface(filePath);
}
/**
* The file after it has been compressed
*
* @param filePath
* @throws IOException
*/
public void createOutFile(String filePath) throws IOException {
this.outFile = new ExifInterface(filePath);
}
/**
* Reads all the EXIF data from the input file.
*/
public void readExifData() {
this.aperture = inFile.getAttribute(ExifInterface.TAG_APERTURE);
this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME);
this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH);
this.focalLength = inFile.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
this.gpsAltitude = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
this.gpsAltitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
this.gpsDateStamp = inFile.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
this.gpsLatitude = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
this.gpsLatitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
this.gpsLongitude = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
this.gpsLongitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
this.gpsProcessingMethod = inFile.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
this.gpsTimestamp = inFile.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
this.iso = inFile.getAttribute(ExifInterface.TAG_ISO);
this.make = inFile.getAttribute(ExifInterface.TAG_MAKE);
this.model = inFile.getAttribute(ExifInterface.TAG_MODEL);
this.orientation = inFile.getAttribute(ExifInterface.TAG_ORIENTATION);
this.whiteBalance = inFile.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
}
/**
* Writes the previously stored EXIF data to the output file.
*
* @throws IOException
*/
public void writeExifData() throws IOException {
// Don't try to write to a null file
if (this.outFile == null) {
return;
}
if (this.aperture != null) {
this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperture);
}
if (this.datetime != null) {
this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime);
}
if (this.exposureTime != null) {
this.outFile.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, this.exposureTime);
}
if (this.flash != null) {
this.outFile.setAttribute(ExifInterface.TAG_FLASH, this.flash);
}
if (this.focalLength != null) {
this.outFile.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, this.focalLength);
}
if (this.gpsAltitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, this.gpsAltitude);
}
if (this.gpsAltitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, this.gpsAltitudeRef);
}
if (this.gpsDateStamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, this.gpsDateStamp);
}
if (this.gpsLatitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE, this.gpsLatitude);
}
if (this.gpsLatitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, this.gpsLatitudeRef);
}
if (this.gpsLongitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, this.gpsLongitude);
}
if (this.gpsLongitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, this.gpsLongitudeRef);
}
if (this.gpsProcessingMethod != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, this.gpsProcessingMethod);
}
if (this.gpsTimestamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, this.gpsTimestamp);
}
if (this.iso != null) {
this.outFile.setAttribute(ExifInterface.TAG_ISO, this.iso);
}
if (this.make != null) {
this.outFile.setAttribute(ExifInterface.TAG_MAKE, this.make);
}
if (this.model != null) {
this.outFile.setAttribute(ExifInterface.TAG_MODEL, this.model);
}
if (this.orientation != null) {
this.outFile.setAttribute(ExifInterface.TAG_ORIENTATION, this.orientation);
}
if (this.whiteBalance != null) {
this.outFile.setAttribute(ExifInterface.TAG_WHITE_BALANCE, this.whiteBalance);
}
this.outFile.saveAttributes();
}
public int getOrientation() {
int o = Integer.parseInt(this.orientation);
if (o == ExifInterface.ORIENTATION_NORMAL) {
return 0;
} else if (o == ExifInterface.ORIENTATION_ROTATE_90) {
return 90;
} else if (o == ExifInterface.ORIENTATION_ROTATE_180) {
return 180;
} else if (o == ExifInterface.ORIENTATION_ROTATE_270) {
return 270;
} else {
return 0;
}
}
public void resetOrientation() {
this.orientation = "" + ExifInterface.ORIENTATION_NORMAL;
}
}

View File

@@ -0,0 +1,319 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.camera;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.LOG;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
public class FileHelper {
private static final String LOG_TAG = "FileUtils";
private static final String _DATA = "_data";
/**
* Returns the real path of the given URI string.
* If the given URI string represents a content:// URI, the real path is retrieved from the media store.
*
* @param uriString the URI string of the audio/image/video
* @param cordova the current application context
* @return the full path to the file
*/
@SuppressWarnings("deprecation")
public static String getRealPath(Uri uri, CordovaInterface cordova) {
String realPath = null;
if (Build.VERSION.SDK_INT < 11)
realPath = FileHelper.getRealPathFromURI_BelowAPI11(cordova.getActivity(), uri);
// SDK >= 11
else
realPath = FileHelper.getRealPathFromURI_API11_And_Above(cordova.getActivity(), uri);
return realPath;
}
/**
* Returns the real path of the given URI.
* If the given URI is a content:// URI, the real path is retrieved from the media store.
*
* @param uri the URI of the audio/image/video
* @param cordova the current application context
* @return the full path to the file
*/
public static String getRealPath(String uriString, CordovaInterface cordova) {
return FileHelper.getRealPath(Uri.parse(uriString), cordova);
}
@SuppressLint("NewApi")
public static String getRealPathFromURI_API11_And_Above(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) {
String[] proj = { MediaStore.Images.Media.DATA };
String result = null;
try {
Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
result = cursor.getString(column_index);
} catch (Exception e) {
result = null;
}
return result;
}
/**
* Returns an input stream based on given URI string.
*
* @param uriString the URI string from which to obtain the input stream
* @param cordova the current application context
* @return an input stream into the data at the given URI or null if given an invalid URI string
* @throws IOException
*/
public static InputStream getInputStreamFromUriString(String uriString, CordovaInterface cordova)
throws IOException {
InputStream returnValue = null;
if (uriString.startsWith("content")) {
Uri uri = Uri.parse(uriString);
returnValue = cordova.getActivity().getContentResolver().openInputStream(uri);
} else if (uriString.startsWith("file://")) {
int question = uriString.indexOf("?");
if (question > -1) {
uriString = uriString.substring(0, question);
}
if (uriString.startsWith("file:///android_asset/")) {
Uri uri = Uri.parse(uriString);
String relativePath = uri.getPath().substring(15);
returnValue = cordova.getActivity().getAssets().open(relativePath);
} else {
// might still be content so try that first
try {
returnValue = cordova.getActivity().getContentResolver().openInputStream(Uri.parse(uriString));
} catch (Exception e) {
returnValue = null;
}
if (returnValue == null) {
returnValue = new FileInputStream(getRealPath(uriString, cordova));
}
}
} else {
returnValue = new FileInputStream(uriString);
}
return returnValue;
}
/**
* Removes the "file://" prefix from the given URI string, if applicable.
* If the given URI string doesn't have a "file://" prefix, it is returned unchanged.
*
* @param uriString the URI string to operate on
* @return a path without the "file://" prefix
*/
public static String stripFileProtocol(String uriString) {
if (uriString.startsWith("file://")) {
uriString = uriString.substring(7);
}
return uriString;
}
public static String getMimeTypeForExtension(String path) {
String extension = path;
int lastDot = extension.lastIndexOf('.');
if (lastDot != -1) {
extension = extension.substring(lastDot + 1);
}
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
extension = extension.toLowerCase(Locale.getDefault());
if (extension.equals("3ga")) {
return "audio/3gpp";
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/**
* Returns the mime type of the data specified by the given URI string.
*
* @param uriString the URI string of the data
* @return the mime type of the specified data
*/
public static String getMimeType(String uriString, CordovaInterface cordova) {
String mimeType = null;
Uri uri = Uri.parse(uriString);
if (uriString.startsWith("content://")) {
mimeType = cordova.getActivity().getContentResolver().getType(uri);
} else {
mimeType = getMimeTypeForExtension(uri.getPath());
}
return mimeType;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
* @author paulburke
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} catch (Exception e) {
return null;
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
* @author paulburke
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
* @author paulburke
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
* @author paulburke
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>

View File

@@ -0,0 +1,227 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* globals qnx, FileError, PluginResult */
var PictureSourceType = {
PHOTOLIBRARY : 0, // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
CAMERA : 1, // Take picture from camera
SAVEDPHOTOALBUM : 2 // Choose image from picture library (same as PHOTOLIBRARY for Android)
},
DestinationType = {
DATA_URL: 0, // Return base64 encoded string
FILE_URI: 1, // Return file uri (content://media/external/images/media/2 for Android)
NATIVE_URI: 2 // Return native uri (eg. asset-library://... for iOS)
},
savePath = window.qnx.webplatform.getApplication().getEnv("HOME").replace('/data', '') + '/shared/camera/',
invokeAvailable = true;
//check for camera card - it isn't currently availble in work perimeter
window.qnx.webplatform.getApplication().invocation.queryTargets(
{
type: 'image/jpeg',
action: 'bb.action.CAPTURE',
target_type: 'CARD'
},
function (error, targets) {
invokeAvailable = !error && targets && targets instanceof Array &&
targets.filter(function (t) { return t.default === 'sys.camera.card'; }).length > 0;
}
);
//open a webview with getUserMedia camera card implementation when camera card not available
function showCameraDialog (done, cancel, fail) {
var wv = qnx.webplatform.createWebView(function () {
wv.url = 'local:///chrome/camera.html';
wv.allowQnxObject = true;
wv.allowRpc = true;
wv.zOrder = 1;
wv.setGeometry(0, 0, screen.width, screen.height);
wv.backgroundColor = 0x00000000;
wv.active = true;
wv.visible = true;
wv.on('UserMediaRequest', function (evt, args) {
wv.allowUserMedia(JSON.parse(args).id, 'CAMERA_UNIT_REAR');
});
wv.on('JavaScriptCallback', function (evt, data) {
var args = JSON.parse(data).args;
if (args[0] === 'cordova-plugin-camera') {
if (args[1] === 'cancel') {
cancel('User canceled');
} else if (args[1] === 'error') {
fail(args[2]);
} else {
saveImage(args[1], done, fail);
}
wv.un('JavaScriptCallback', arguments.callee);
wv.visible = false;
wv.destroy();
qnx.webplatform.getApplication().unlockRotation();
}
});
wv.on('Destroyed', function () {
wv.delete();
});
qnx.webplatform.getApplication().lockRotation();
qnx.webplatform.getController().dispatchEvent('webview.initialized', [wv]);
});
}
//create unique name for saved file (same pattern as BB10 camera app)
function imgName() {
var date = new Date(),
pad = function (n) { return n < 10 ? '0' + n : n; };
return 'IMG_' + date.getFullYear() + pad(date.getMonth() + 1) + pad(date.getDate()) + '_' +
pad(date.getHours()) + pad(date.getMinutes()) + pad(date.getSeconds()) + '.png';
}
//convert dataURI to Blob
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]),
mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0],
arrayBuffer = new ArrayBuffer(byteString.length),
ia = new Uint8Array(arrayBuffer),
i;
for (i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([new DataView(arrayBuffer)], { type: mimeString });
}
//save dataURI to file system and call success with path
function saveImage(data, success, fail) {
var name = savePath + imgName();
require('lib/webview').setSandbox(false);
window.webkitRequestFileSystem(window.PERSISTENT, 0, function (fs) {
fs.root.getFile(name, { create: true }, function (entry) {
entry.createWriter(function (writer) {
writer.onwriteend = function () {
success(name);
};
writer.onerror = fail;
writer.write(dataURItoBlob(data));
});
}, fail);
}, fail);
}
function encodeBase64(filePath, callback) {
var sandbox = window.qnx.webplatform.getController().setFileSystemSandbox, // save original sandbox value
errorHandler = function (err) {
var msg = "An error occured: ";
switch (err.code) {
case FileError.NOT_FOUND_ERR:
msg += "File or directory not found";
break;
case FileError.NOT_READABLE_ERR:
msg += "File or directory not readable";
break;
case FileError.PATH_EXISTS_ERR:
msg += "File or directory already exists";
break;
case FileError.TYPE_MISMATCH_ERR:
msg += "Invalid file type";
break;
default:
msg += "Unknown Error";
break;
}
// set it back to original value
window.qnx.webplatform.getController().setFileSystemSandbox = sandbox;
callback(msg);
},
gotFile = function (fileEntry) {
fileEntry.file(function (file) {
var reader = new FileReader();
reader.onloadend = function (e) {
// set it back to original value
window.qnx.webplatform.getController().setFileSystemSandbox = sandbox;
callback(this.result);
};
reader.readAsDataURL(file);
}, errorHandler);
},
onInitFs = function (fs) {
window.qnx.webplatform.getController().setFileSystemSandbox = false;
fs.root.getFile(filePath, {create: false}, gotFile, errorHandler);
};
window.webkitRequestFileSystem(window.TEMPORARY, 10 * 1024 * 1024, onInitFs, errorHandler); // set size to 10MB max
}
module.exports = {
takePicture: function (success, fail, args, env) {
var destinationType = JSON.parse(decodeURIComponent(args[1])),
sourceType = JSON.parse(decodeURIComponent(args[2])),
result = new PluginResult(args, env),
done = function (data) {
if (destinationType === DestinationType.FILE_URI) {
data = "file://" + data;
result.callbackOk(data, false);
} else {
encodeBase64(data, function (data) {
if (/^data:/.test(data)) {
data = data.slice(data.indexOf(",") + 1);
result.callbackOk(data, false);
} else {
result.callbackError(data, false);
}
});
}
},
cancel = function (reason) {
result.callbackError(reason, false);
},
invoked = function (error) {
if (error) {
result.callbackError(error, false);
}
};
switch(sourceType) {
case PictureSourceType.CAMERA:
if (invokeAvailable) {
window.qnx.webplatform.getApplication().cards.camera.open("photo", done, cancel, invoked);
} else {
showCameraDialog(done, cancel, fail);
}
break;
case PictureSourceType.PHOTOLIBRARY:
case PictureSourceType.SAVEDPHOTOALBUM:
window.qnx.webplatform.getApplication().cards.filePicker.open({
mode: "Picker",
type: ["picture"]
}, done, cancel, invoked);
break;
}
result.noResult(true);
}
};

View File

@@ -0,0 +1,123 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
var HIGHEST_POSSIBLE_Z_INDEX = 2147483647;
function takePicture(success, error, opts) {
if (opts && opts[2] === 1) {
capture(success, error, opts);
} else {
var input = document.createElement('input');
input.style.position = 'relative';
input.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
input.className = 'cordova-camera-select';
input.type = 'file';
input.name = 'files[]';
input.onchange = function(inputEvent) {
var reader = new FileReader();
reader.onload = function(readerEvent) {
input.parentNode.removeChild(input);
var imageData = readerEvent.target.result;
return success(imageData.substr(imageData.indexOf(',') + 1));
};
reader.readAsDataURL(inputEvent.target.files[0]);
};
document.body.appendChild(input);
}
}
function capture(success, errorCallback, opts) {
var localMediaStream;
var targetWidth = opts[3];
var targetHeight = opts[4];
targetWidth = targetWidth == -1?320:targetWidth;
targetHeight = targetHeight == -1?240:targetHeight;
var video = document.createElement('video');
var button = document.createElement('button');
var parent = document.createElement('div');
parent.style.position = 'relative';
parent.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
parent.className = 'cordova-camera-capture';
parent.appendChild(video);
parent.appendChild(button);
video.width = targetWidth;
video.height = targetHeight;
button.innerHTML = 'Capture!';
button.onclick = function() {
// create a canvas and capture a frame from video stream
var canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
canvas.getContext('2d').drawImage(video, 0, 0, targetWidth, targetHeight);
// convert image stored in canvas to base64 encoded image
var imageData = canvas.toDataURL('image/png');
imageData = imageData.replace('data:image/png;base64,', '');
// stop video stream, remove video and button.
// Note that MediaStream.stop() is deprecated as of Chrome 47.
if (localMediaStream.stop) {
localMediaStream.stop();
} else {
localMediaStream.getTracks().forEach(function (track) {
track.stop();
});
}
parent.parentNode.removeChild(parent);
return success(imageData);
};
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
var successCallback = function(stream) {
localMediaStream = stream;
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
document.body.appendChild(parent);
};
if (navigator.getUserMedia) {
navigator.getUserMedia({video: true, audio: true}, successCallback, errorCallback);
} else {
alert('Browser does not support camera :(');
}
}
module.exports = {
takePicture: takePicture,
cleanup: function(){}
};
require("cordova/exec/proxy").add("Camera",module.exports);

View File

@@ -0,0 +1,53 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* globals MozActivity */
function takePicture(success, error, opts) {
var pick = new MozActivity({
name: "pick",
data: {
type: ["image/*"]
}
});
pick.onerror = error || function() {};
pick.onsuccess = function() {
// image is returned as Blob in this.result.blob
// we need to call success with url or base64 encoded image
if (opts && opts.destinationType === 0) {
// TODO: base64
return;
}
if (!opts || !opts.destinationType || opts.destinationType > 0) {
// url
return success(window.URL.createObjectURL(this.result.blob));
}
};
}
module.exports = {
takePicture: takePicture,
cleanup: function(){}
};
require("cordova/exec/proxy").add("Camera", module.exports);

View File

@@ -0,0 +1,116 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import <CoreLocation/CLLocationManager.h>
#import <Cordova/CDVPlugin.h>
enum CDVDestinationType {
DestinationTypeDataUrl = 0,
DestinationTypeFileUri,
DestinationTypeNativeUri
};
typedef NSUInteger CDVDestinationType;
enum CDVEncodingType {
EncodingTypeJPEG = 0,
EncodingTypePNG
};
typedef NSUInteger CDVEncodingType;
enum CDVMediaType {
MediaTypePicture = 0,
MediaTypeVideo,
MediaTypeAll
};
typedef NSUInteger CDVMediaType;
@interface CDVPictureOptions : NSObject
@property (strong) NSNumber* quality;
@property (assign) CDVDestinationType destinationType;
@property (assign) UIImagePickerControllerSourceType sourceType;
@property (assign) CGSize targetSize;
@property (assign) CDVEncodingType encodingType;
@property (assign) CDVMediaType mediaType;
@property (assign) BOOL allowsEditing;
@property (assign) BOOL correctOrientation;
@property (assign) BOOL saveToPhotoAlbum;
@property (strong) NSDictionary* popoverOptions;
@property (assign) UIImagePickerControllerCameraDevice cameraDirection;
@property (assign) BOOL popoverSupported;
@property (assign) BOOL usesGeolocation;
@property (assign) BOOL cropToSize;
+ (instancetype) createFromTakePictureArguments:(CDVInvokedUrlCommand*)command;
@end
@interface CDVCameraPicker : UIImagePickerController
@property (strong) CDVPictureOptions* pictureOptions;
@property (copy) NSString* callbackId;
@property (copy) NSString* postUrl;
@property (strong) UIPopoverController* pickerPopoverController;
@property (assign) BOOL cropToSize;
@property (strong) UIView* webView;
+ (instancetype) createFromPictureOptions:(CDVPictureOptions*)options;
@end
// ======================================================================= //
@interface CDVCamera : CDVPlugin <UIImagePickerControllerDelegate,
UINavigationControllerDelegate,
UIPopoverControllerDelegate,
CLLocationManagerDelegate>
{}
@property (strong) CDVCameraPicker* pickerController;
@property (strong) NSMutableDictionary *metadata;
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong) NSData* data;
/*
* getPicture
*
* arguments:
* 1: this is the javascript function that will be called with the results, the first parameter passed to the
* javascript function is the picture as a Base64 encoded string
* 2: this is the javascript function to be called if there was an error
* options:
* quality: integer between 1 and 100
*/
- (void)takePicture:(CDVInvokedUrlCommand*)command;
- (void)cleanup:(CDVInvokedUrlCommand*)command;
- (void)repositionPopover:(CDVInvokedUrlCommand*)command;
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info;
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo;
- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker;
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (void)locationManager:(CLLocationManager*)manager didUpdateToLocation:(CLLocation*)newLocation fromLocation:(CLLocation*)oldLocation;
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;
@end

View File

@@ -0,0 +1,771 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVCamera.h"
#import "CDVJpegHeaderWriter.h"
#import "UIImage+CropScaleOrientation.h"
#import <ImageIO/CGImageProperties.h>
#import <AssetsLibrary/ALAssetRepresentation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <AVFoundation/AVFoundation.h>
#import <ImageIO/CGImageSource.h>
#import <ImageIO/CGImageProperties.h>
#import <ImageIO/CGImageDestination.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <objc/message.h>
#ifndef __CORDOVA_4_0_0
#import <Cordova/NSData+Base64.h>
#endif
#define CDV_PHOTO_PREFIX @"cdv_photo_"
static NSSet* org_apache_cordova_validArrowDirections;
static NSString* toBase64(NSData* data) {
SEL s1 = NSSelectorFromString(@"cdv_base64EncodedString");
SEL s2 = NSSelectorFromString(@"base64EncodedString");
SEL s3 = NSSelectorFromString(@"base64EncodedStringWithOptions:");
if ([data respondsToSelector:s1]) {
NSString* (*func)(id, SEL) = (void *)[data methodForSelector:s1];
return func(data, s1);
} else if ([data respondsToSelector:s2]) {
NSString* (*func)(id, SEL) = (void *)[data methodForSelector:s2];
return func(data, s2);
} else if ([data respondsToSelector:s3]) {
NSString* (*func)(id, SEL, NSUInteger) = (void *)[data methodForSelector:s3];
return func(data, s3, 0);
} else {
return nil;
}
}
@implementation CDVPictureOptions
+ (instancetype) createFromTakePictureArguments:(CDVInvokedUrlCommand*)command
{
CDVPictureOptions* pictureOptions = [[CDVPictureOptions alloc] init];
pictureOptions.quality = [command argumentAtIndex:0 withDefault:@(50)];
pictureOptions.destinationType = [[command argumentAtIndex:1 withDefault:@(DestinationTypeFileUri)] unsignedIntegerValue];
pictureOptions.sourceType = [[command argumentAtIndex:2 withDefault:@(UIImagePickerControllerSourceTypeCamera)] unsignedIntegerValue];
NSNumber* targetWidth = [command argumentAtIndex:3 withDefault:nil];
NSNumber* targetHeight = [command argumentAtIndex:4 withDefault:nil];
pictureOptions.targetSize = CGSizeMake(0, 0);
if ((targetWidth != nil) && (targetHeight != nil)) {
pictureOptions.targetSize = CGSizeMake([targetWidth floatValue], [targetHeight floatValue]);
}
pictureOptions.encodingType = [[command argumentAtIndex:5 withDefault:@(EncodingTypeJPEG)] unsignedIntegerValue];
pictureOptions.mediaType = [[command argumentAtIndex:6 withDefault:@(MediaTypePicture)] unsignedIntegerValue];
pictureOptions.allowsEditing = [[command argumentAtIndex:7 withDefault:@(NO)] boolValue];
pictureOptions.correctOrientation = [[command argumentAtIndex:8 withDefault:@(NO)] boolValue];
pictureOptions.saveToPhotoAlbum = [[command argumentAtIndex:9 withDefault:@(NO)] boolValue];
pictureOptions.popoverOptions = [command argumentAtIndex:10 withDefault:nil];
pictureOptions.cameraDirection = [[command argumentAtIndex:11 withDefault:@(UIImagePickerControllerCameraDeviceRear)] unsignedIntegerValue];
pictureOptions.popoverSupported = NO;
pictureOptions.usesGeolocation = NO;
return pictureOptions;
}
@end
@interface CDVCamera ()
@property (readwrite, assign) BOOL hasPendingOperation;
@end
@implementation CDVCamera
+ (void)initialize
{
org_apache_cordova_validArrowDirections = [[NSSet alloc] initWithObjects:[NSNumber numberWithInt:UIPopoverArrowDirectionUp], [NSNumber numberWithInt:UIPopoverArrowDirectionDown], [NSNumber numberWithInt:UIPopoverArrowDirectionLeft], [NSNumber numberWithInt:UIPopoverArrowDirectionRight], [NSNumber numberWithInt:UIPopoverArrowDirectionAny], nil];
}
@synthesize hasPendingOperation, pickerController, locationManager;
- (NSURL*) urlTransformer:(NSURL*)url
{
NSURL* urlToTransform = url;
// for backwards compatibility - we check if this property is there
SEL sel = NSSelectorFromString(@"urlTransformer");
if ([self.commandDelegate respondsToSelector:sel]) {
// grab the block from the commandDelegate
NSURL* (^urlTransformer)(NSURL*) = ((id(*)(id, SEL))objc_msgSend)(self.commandDelegate, sel);
// if block is not null, we call it
if (urlTransformer) {
urlToTransform = urlTransformer(url);
}
}
return urlToTransform;
}
- (BOOL)usesGeolocation
{
id useGeo = [self.commandDelegate.settings objectForKey:[@"CameraUsesGeolocation" lowercaseString]];
return [(NSNumber*)useGeo boolValue];
}
- (BOOL)popoverSupported
{
return (NSClassFromString(@"UIPopoverController") != nil) &&
(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
}
- (void)takePicture:(CDVInvokedUrlCommand*)command
{
self.hasPendingOperation = YES;
__weak CDVCamera* weakSelf = self;
[self.commandDelegate runInBackground:^{
CDVPictureOptions* pictureOptions = [CDVPictureOptions createFromTakePictureArguments:command];
pictureOptions.popoverSupported = [weakSelf popoverSupported];
pictureOptions.usesGeolocation = [weakSelf usesGeolocation];
pictureOptions.cropToSize = NO;
BOOL hasCamera = [UIImagePickerController isSourceTypeAvailable:pictureOptions.sourceType];
if (!hasCamera) {
NSLog(@"Camera.getPicture: source type %lu not available.", (unsigned long)pictureOptions.sourceType);
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No camera available"];
[weakSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId];
return;
}
// Validate the app has permission to access the camera
if (pictureOptions.sourceType == UIImagePickerControllerSourceTypeCamera && [AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (authStatus == AVAuthorizationStatusDenied ||
authStatus == AVAuthorizationStatusRestricted) {
// If iOS 8+, offer a link to the Settings app
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
NSString* settingsButton = (&UIApplicationOpenSettingsURLString != NULL)
? NSLocalizedString(@"Settings", nil)
: nil;
#pragma clang diagnostic pop
// Denied; show an alert
dispatch_async(dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleDisplayName"]
message:NSLocalizedString(@"Access to the camera has been prohibited; please enable it in the Settings app to continue.", nil)
delegate:weakSelf
cancelButtonTitle:NSLocalizedString(@"OK", nil)
otherButtonTitles:settingsButton, nil] show];
});
}
}
CDVCameraPicker* cameraPicker = [CDVCameraPicker createFromPictureOptions:pictureOptions];
weakSelf.pickerController = cameraPicker;
cameraPicker.delegate = weakSelf;
cameraPicker.callbackId = command.callbackId;
// we need to capture this state for memory warnings that dealloc this object
cameraPicker.webView = weakSelf.webView;
// Perform UI operations on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// If a popover is already open, close it; we only want one at a time.
if (([[weakSelf pickerController] pickerPopoverController] != nil) && [[[weakSelf pickerController] pickerPopoverController] isPopoverVisible]) {
[[[weakSelf pickerController] pickerPopoverController] dismissPopoverAnimated:YES];
[[[weakSelf pickerController] pickerPopoverController] setDelegate:nil];
[[weakSelf pickerController] setPickerPopoverController:nil];
}
if ([weakSelf popoverSupported] && (pictureOptions.sourceType != UIImagePickerControllerSourceTypeCamera)) {
if (cameraPicker.pickerPopoverController == nil) {
cameraPicker.pickerPopoverController = [[NSClassFromString(@"UIPopoverController") alloc] initWithContentViewController:cameraPicker];
}
[weakSelf displayPopover:pictureOptions.popoverOptions];
weakSelf.hasPendingOperation = NO;
} else {
[weakSelf.viewController presentViewController:cameraPicker animated:YES completion:^{
weakSelf.hasPendingOperation = NO;
}];
}
});
}];
}
// Delegate for camera permission UIAlertView
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// If Settings button (on iOS 8), open the settings app
if (buttonIndex == 1) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
if (&UIApplicationOpenSettingsURLString != NULL) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}
#pragma clang diagnostic pop
}
// Dismiss the view
[[self.pickerController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to camera"]; // error callback expects string ATM
[self.commandDelegate sendPluginResult:result callbackId:self.pickerController.callbackId];
self.hasPendingOperation = NO;
self.pickerController = nil;
}
- (void)repositionPopover:(CDVInvokedUrlCommand*)command
{
if (([[self pickerController] pickerPopoverController] != nil) && [[[self pickerController] pickerPopoverController] isPopoverVisible]) {
[[[self pickerController] pickerPopoverController] dismissPopoverAnimated:NO];
NSDictionary* options = [command argumentAtIndex:0 withDefault:nil];
[self displayPopover:options];
}
}
- (NSInteger)integerValueForKey:(NSDictionary*)dict key:(NSString*)key defaultValue:(NSInteger)defaultValue
{
NSInteger value = defaultValue;
NSNumber* val = [dict valueForKey:key]; // value is an NSNumber
if (val != nil) {
value = [val integerValue];
}
return value;
}
- (void)displayPopover:(NSDictionary*)options
{
NSInteger x = 0;
NSInteger y = 32;
NSInteger width = 320;
NSInteger height = 480;
UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny;
if (options) {
x = [self integerValueForKey:options key:@"x" defaultValue:0];
y = [self integerValueForKey:options key:@"y" defaultValue:32];
width = [self integerValueForKey:options key:@"width" defaultValue:320];
height = [self integerValueForKey:options key:@"height" defaultValue:480];
arrowDirection = [self integerValueForKey:options key:@"arrowDir" defaultValue:UIPopoverArrowDirectionAny];
if (![org_apache_cordova_validArrowDirections containsObject:[NSNumber numberWithUnsignedInteger:arrowDirection]]) {
arrowDirection = UIPopoverArrowDirectionAny;
}
}
[[[self pickerController] pickerPopoverController] setDelegate:self];
[[[self pickerController] pickerPopoverController] presentPopoverFromRect:CGRectMake(x, y, width, height)
inView:[self.webView superview]
permittedArrowDirections:arrowDirection
animated:YES];
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if([navigationController isKindOfClass:[UIImagePickerController class]]){
UIImagePickerController* cameraPicker = (UIImagePickerController*)navigationController;
if(![cameraPicker.mediaTypes containsObject:(NSString*)kUTTypeImage]){
[viewController.navigationItem setTitle:NSLocalizedString(@"Videos", nil)];
}
}
}
- (void)cleanup:(CDVInvokedUrlCommand*)command
{
// empty the tmp directory
NSFileManager* fileMgr = [[NSFileManager alloc] init];
NSError* err = nil;
BOOL hasErrors = NO;
// clear contents of NSTemporaryDirectory
NSString* tempDirectoryPath = NSTemporaryDirectory();
NSDirectoryEnumerator* directoryEnumerator = [fileMgr enumeratorAtPath:tempDirectoryPath];
NSString* fileName = nil;
BOOL result;
while ((fileName = [directoryEnumerator nextObject])) {
// only delete the files we created
if (![fileName hasPrefix:CDV_PHOTO_PREFIX]) {
continue;
}
NSString* filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName];
result = [fileMgr removeItemAtPath:filePath error:&err];
if (!result && err) {
NSLog(@"Failed to delete: %@ (error: %@)", filePath, err);
hasErrors = YES;
}
}
CDVPluginResult* pluginResult;
if (hasErrors) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:@"One or more files failed to be deleted."];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
}
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (void)popoverControllerDidDismissPopover:(id)popoverController
{
UIPopoverController* pc = (UIPopoverController*)popoverController;
[pc dismissPopoverAnimated:YES];
pc.delegate = nil;
if (self.pickerController && self.pickerController.callbackId && self.pickerController.pickerPopoverController) {
self.pickerController.pickerPopoverController = nil;
NSString* callbackId = self.pickerController.callbackId;
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"]; // error callback expects string ATM
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
self.hasPendingOperation = NO;
}
- (NSData*)processImage:(UIImage*)image info:(NSDictionary*)info options:(CDVPictureOptions*)options
{
NSData* data = nil;
switch (options.encodingType) {
case EncodingTypePNG:
data = UIImagePNGRepresentation(image);
break;
case EncodingTypeJPEG:
{
if ((options.allowsEditing == NO) && (options.targetSize.width <= 0) && (options.targetSize.height <= 0) && (options.correctOrientation == NO) && (([options.quality integerValue] == 100) || (options.sourceType != UIImagePickerControllerSourceTypeCamera))){
// use image unedited as requested , don't resize
data = UIImageJPEGRepresentation(image, 1.0);
} else {
data = UIImageJPEGRepresentation(image, [options.quality floatValue] / 100.0f);
}
if (options.usesGeolocation) {
NSDictionary* controllerMetadata = [info objectForKey:@"UIImagePickerControllerMediaMetadata"];
if (controllerMetadata) {
self.data = data;
self.metadata = [[NSMutableDictionary alloc] init];
NSMutableDictionary* EXIFDictionary = [[controllerMetadata objectForKey:(NSString*)kCGImagePropertyExifDictionary]mutableCopy];
if (EXIFDictionary) {
[self.metadata setObject:EXIFDictionary forKey:(NSString*)kCGImagePropertyExifDictionary];
}
if (IsAtLeastiOSVersion(@"8.0")) {
[[self locationManager] performSelector:NSSelectorFromString(@"requestWhenInUseAuthorization") withObject:nil afterDelay:0];
}
[[self locationManager] startUpdatingLocation];
}
}
}
break;
default:
break;
};
return data;
}
- (NSString*)tempFilePath:(NSString*)extension
{
NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath];
NSFileManager* fileMgr = [[NSFileManager alloc] init]; // recommended by Apple (vs [NSFileManager defaultManager]) to be threadsafe
NSString* filePath;
// generate unique file name
int i = 1;
do {
filePath = [NSString stringWithFormat:@"%@/%@%03d.%@", docsPath, CDV_PHOTO_PREFIX, i++, extension];
} while ([fileMgr fileExistsAtPath:filePath]);
return filePath;
}
- (UIImage*)retrieveImage:(NSDictionary*)info options:(CDVPictureOptions*)options
{
// get the image
UIImage* image = nil;
if (options.allowsEditing && [info objectForKey:UIImagePickerControllerEditedImage]) {
image = [info objectForKey:UIImagePickerControllerEditedImage];
} else {
image = [info objectForKey:UIImagePickerControllerOriginalImage];
}
if (options.correctOrientation) {
image = [image imageCorrectedForCaptureOrientation];
}
UIImage* scaledImage = nil;
if ((options.targetSize.width > 0) && (options.targetSize.height > 0)) {
// if cropToSize, resize image and crop to target size, otherwise resize to fit target without cropping
if (options.cropToSize) {
scaledImage = [image imageByScalingAndCroppingForSize:options.targetSize];
} else {
scaledImage = [image imageByScalingNotCroppingForSize:options.targetSize];
}
}
return (scaledImage == nil ? image : scaledImage);
}
- (void)resultForImage:(CDVPictureOptions*)options info:(NSDictionary*)info completion:(void (^)(CDVPluginResult* res))completion
{
CDVPluginResult* result = nil;
BOOL saveToPhotoAlbum = options.saveToPhotoAlbum;
UIImage* image = nil;
switch (options.destinationType) {
case DestinationTypeNativeUri:
{
NSURL* url = [info objectForKey:UIImagePickerControllerReferenceURL];
saveToPhotoAlbum = NO;
// If, for example, we use sourceType = Camera, URL might be nil because image is stored in memory.
// In this case we must save image to device before obtaining an URI.
if (url == nil) {
image = [self retrieveImage:info options:options];
ALAssetsLibrary* library = [ALAssetsLibrary new];
[library writeImageToSavedPhotosAlbum:image.CGImage orientation:(ALAssetOrientation)(image.imageOrientation) completionBlock:^(NSURL *assetURL, NSError *error) {
CDVPluginResult* resultToReturn = nil;
if (error) {
resultToReturn = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]];
} else {
NSString* nativeUri = [[self urlTransformer:assetURL] absoluteString];
resultToReturn = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:nativeUri];
}
completion(resultToReturn);
}];
return;
} else {
NSString* nativeUri = [[self urlTransformer:url] absoluteString];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:nativeUri];
}
}
break;
case DestinationTypeFileUri:
{
image = [self retrieveImage:info options:options];
NSData* data = [self processImage:image info:info options:options];
if (data) {
NSString* extension = options.encodingType == EncodingTypePNG? @"png" : @"jpg";
NSString* filePath = [self tempFilePath:extension];
NSError* err = nil;
// save file
if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
}
}
}
break;
case DestinationTypeDataUrl:
{
image = [self retrieveImage:info options:options];
NSData* data = [self processImage:image info:info options:options];
if (data) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:toBase64(data)];
}
}
break;
default:
break;
};
if (saveToPhotoAlbum && image) {
ALAssetsLibrary* library = [ALAssetsLibrary new];
[library writeImageToSavedPhotosAlbum:image.CGImage orientation:(ALAssetOrientation)(image.imageOrientation) completionBlock:nil];
}
completion(result);
}
- (CDVPluginResult*)resultForVideo:(NSDictionary*)info
{
NSString* moviePath = [[info objectForKey:UIImagePickerControllerMediaURL] absoluteString];
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:moviePath];
}
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info
{
__weak CDVCameraPicker* cameraPicker = (CDVCameraPicker*)picker;
__weak CDVCamera* weakSelf = self;
dispatch_block_t invoke = ^(void) {
__block CDVPluginResult* result = nil;
NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString*)kUTTypeImage]) {
[weakSelf resultForImage:cameraPicker.pictureOptions info:info completion:^(CDVPluginResult* res) {
if (![self usesGeolocation] || picker.sourceType != UIImagePickerControllerSourceTypeCamera) {
[weakSelf.commandDelegate sendPluginResult:res callbackId:cameraPicker.callbackId];
weakSelf.hasPendingOperation = NO;
weakSelf.pickerController = nil;
}
}];
}
else {
result = [weakSelf resultForVideo:info];
[weakSelf.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId];
weakSelf.hasPendingOperation = NO;
weakSelf.pickerController = nil;
}
};
if (cameraPicker.pictureOptions.popoverSupported && (cameraPicker.pickerPopoverController != nil)) {
[cameraPicker.pickerPopoverController dismissPopoverAnimated:YES];
cameraPicker.pickerPopoverController.delegate = nil;
cameraPicker.pickerPopoverController = nil;
invoke();
} else {
[[cameraPicker presentingViewController] dismissViewControllerAnimated:YES completion:invoke];
}
}
// older api calls newer didFinishPickingMediaWithInfo
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo
{
NSDictionary* imageInfo = [NSDictionary dictionaryWithObject:image forKey:UIImagePickerControllerOriginalImage];
[self imagePickerController:picker didFinishPickingMediaWithInfo:imageInfo];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker
{
__weak CDVCameraPicker* cameraPicker = (CDVCameraPicker*)picker;
__weak CDVCamera* weakSelf = self;
dispatch_block_t invoke = ^ (void) {
CDVPluginResult* result;
if (picker.sourceType == UIImagePickerControllerSourceTypeCamera && [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] != ALAuthorizationStatusAuthorized) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to camera"];
} else if (picker.sourceType != UIImagePickerControllerSourceTypeCamera && [ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusAuthorized) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to assets"];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"];
}
[weakSelf.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId];
weakSelf.hasPendingOperation = NO;
weakSelf.pickerController = nil;
};
[[cameraPicker presentingViewController] dismissViewControllerAnimated:YES completion:invoke];
}
- (CLLocationManager*)locationManager
{
if (locationManager != nil) {
return locationManager;
}
locationManager = [[CLLocationManager alloc] init];
[locationManager setDesiredAccuracy:kCLLocationAccuracyNearestTenMeters];
[locationManager setDelegate:self];
return locationManager;
}
- (void)locationManager:(CLLocationManager*)manager didUpdateToLocation:(CLLocation*)newLocation fromLocation:(CLLocation*)oldLocation
{
if (locationManager == nil) {
return;
}
[self.locationManager stopUpdatingLocation];
self.locationManager = nil;
NSMutableDictionary *GPSDictionary = [[NSMutableDictionary dictionary] init];
CLLocationDegrees latitude = newLocation.coordinate.latitude;
CLLocationDegrees longitude = newLocation.coordinate.longitude;
// latitude
if (latitude < 0.0) {
latitude = latitude * -1.0f;
[GPSDictionary setObject:@"S" forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
} else {
[GPSDictionary setObject:@"N" forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
}
[GPSDictionary setObject:[NSNumber numberWithFloat:latitude] forKey:(NSString*)kCGImagePropertyGPSLatitude];
// longitude
if (longitude < 0.0) {
longitude = longitude * -1.0f;
[GPSDictionary setObject:@"W" forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
}
else {
[GPSDictionary setObject:@"E" forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
}
[GPSDictionary setObject:[NSNumber numberWithFloat:longitude] forKey:(NSString*)kCGImagePropertyGPSLongitude];
// altitude
CGFloat altitude = newLocation.altitude;
if (!isnan(altitude)){
if (altitude < 0) {
altitude = -altitude;
[GPSDictionary setObject:@"1" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
} else {
[GPSDictionary setObject:@"0" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
}
[GPSDictionary setObject:[NSNumber numberWithFloat:altitude] forKey:(NSString *)kCGImagePropertyGPSAltitude];
}
// Time and date
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss.SSSSSS"];
[formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
[GPSDictionary setObject:[formatter stringFromDate:newLocation.timestamp] forKey:(NSString *)kCGImagePropertyGPSTimeStamp];
[formatter setDateFormat:@"yyyy:MM:dd"];
[GPSDictionary setObject:[formatter stringFromDate:newLocation.timestamp] forKey:(NSString *)kCGImagePropertyGPSDateStamp];
[self.metadata setObject:GPSDictionary forKey:(NSString *)kCGImagePropertyGPSDictionary];
[self imagePickerControllerReturnImageResult];
}
- (void)locationManager:(CLLocationManager*)manager didFailWithError:(NSError*)error
{
if (locationManager == nil) {
return;
}
[self.locationManager stopUpdatingLocation];
self.locationManager = nil;
[self imagePickerControllerReturnImageResult];
}
- (void)imagePickerControllerReturnImageResult
{
CDVPictureOptions* options = self.pickerController.pictureOptions;
CDVPluginResult* result = nil;
if (self.metadata) {
CGImageSourceRef sourceImage = CGImageSourceCreateWithData((__bridge CFDataRef)self.data, NULL);
CFStringRef sourceType = CGImageSourceGetType(sourceImage);
CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)self.data, sourceType, 1, NULL);
CGImageDestinationAddImageFromSource(destinationImage, sourceImage, 0, (__bridge CFDictionaryRef)self.metadata);
CGImageDestinationFinalize(destinationImage);
CFRelease(sourceImage);
CFRelease(destinationImage);
}
switch (options.destinationType) {
case DestinationTypeFileUri:
{
NSError* err = nil;
NSString* extension = self.pickerController.pictureOptions.encodingType == EncodingTypePNG ? @"png":@"jpg";
NSString* filePath = [self tempFilePath:extension];
// save file
if (![self.data writeToFile:filePath options:NSAtomicWrite error:&err]) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]];
}
else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
}
}
break;
case DestinationTypeDataUrl:
{
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:toBase64(self.data)];
}
break;
case DestinationTypeNativeUri:
default:
break;
};
if (result) {
[self.commandDelegate sendPluginResult:result callbackId:self.pickerController.callbackId];
}
self.hasPendingOperation = NO;
self.pickerController = nil;
self.data = nil;
self.metadata = nil;
if (options.saveToPhotoAlbum) {
ALAssetsLibrary *library = [ALAssetsLibrary new];
[library writeImageDataToSavedPhotosAlbum:self.data metadata:self.metadata completionBlock:nil];
}
}
@end
@implementation CDVCameraPicker
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (UIViewController*)childViewControllerForStatusBarHidden
{
return nil;
}
- (void)viewWillAppear:(BOOL)animated
{
SEL sel = NSSelectorFromString(@"setNeedsStatusBarAppearanceUpdate");
if ([self respondsToSelector:sel]) {
[self performSelector:sel withObject:nil afterDelay:0];
}
[super viewWillAppear:animated];
}
+ (instancetype) createFromPictureOptions:(CDVPictureOptions*)pictureOptions;
{
CDVCameraPicker* cameraPicker = [[CDVCameraPicker alloc] init];
cameraPicker.pictureOptions = pictureOptions;
cameraPicker.sourceType = pictureOptions.sourceType;
cameraPicker.allowsEditing = pictureOptions.allowsEditing;
if (cameraPicker.sourceType == UIImagePickerControllerSourceTypeCamera) {
// We only allow taking pictures (no video) in this API.
cameraPicker.mediaTypes = @[(NSString*)kUTTypeImage];
// We can only set the camera device if we're actually using the camera.
cameraPicker.cameraDevice = pictureOptions.cameraDirection;
} else if (pictureOptions.mediaType == MediaTypeAll) {
cameraPicker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:cameraPicker.sourceType];
} else {
NSArray* mediaArray = @[(NSString*)(pictureOptions.mediaType == MediaTypeVideo ? kUTTypeMovie : kUTTypeImage)];
cameraPicker.mediaTypes = mediaArray;
}
return cameraPicker;
}
@end

View File

@@ -0,0 +1,43 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#ifndef CordovaLib_ExifData_h
#define CordovaLib_ExifData_h
// exif data types
typedef enum exifDataTypes {
EDT_UBYTE = 1, // 8 bit unsigned integer
EDT_ASCII_STRING, // 8 bits containing 7 bit ASCII code, null terminated
EDT_USHORT, // 16 bit unsigned integer
EDT_ULONG, // 32 bit unsigned integer
EDT_URATIONAL, // 2 longs, first is numerator and second is denominator
EDT_SBYTE,
EDT_UNDEFINED, // 8 bits
EDT_SSHORT,
EDT_SLONG, // 32bit signed integer (2's complement)
EDT_SRATIONAL, // 2 SLONGS, first long is numerator, second is denominator
EDT_SINGLEFLOAT,
EDT_DOUBLEFLOAT
} ExifDataTypes;
// maps integer code for exif data types to width in bytes
static const int DataTypeToWidth[] = {1,1,2,4,8,1,1,2,4,8,4,8};
static const int RECURSE_HORIZON = 8;
#endif

View File

@@ -0,0 +1,62 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
@interface CDVJpegHeaderWriter : NSObject {
NSDictionary * SubIFDTagFormatDict;
NSDictionary * IFD0TagFormatDict;
}
- (NSData*) spliceExifBlockIntoJpeg: (NSData*) jpegdata
withExifBlock: (NSString*) exifstr;
- (NSString*) createExifAPP1 : (NSDictionary*) datadict;
- (NSString*) formattedHexStringFromDecimalNumber: (NSNumber*) numb
withPlaces: (NSNumber*) width;
- (NSString*) formatNumberWithLeadingZeroes: (NSNumber*) numb
withPlaces: (NSNumber*) places;
- (NSString*) decimalToUnsignedRational: (NSNumber*) numb
withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator;
- (void) continuedFraction: (double) val
withFractionList: (NSMutableArray*) fractionlist
withHorizon: (int) horizon;
//- (void) expandContinuedFraction: (NSArray*) fractionlist;
- (void) splitDouble: (double) val
withIntComponent: (int*) rightside
withFloatRemainder: (double*) leftside;
- (NSString*) formatRationalWithNumerator: (NSNumber*) numerator
withDenominator: (NSNumber*) denominator
asSigned: (Boolean) signedFlag;
- (NSString*) hexStringFromData : (NSData*) data;
- (NSNumber*) numericFromHexString : (NSString *) hexstring;
/*
- (void) readExifMetaData : (NSData*) imgdata;
- (void) spliceImageData : (NSData*) imgdata withExifData: (NSDictionary*) exifdata;
- (void) locateExifMetaData : (NSData*) imgdata;
- (NSString*) createExifAPP1 : (NSDictionary*) datadict;
- (void) createExifDataString : (NSDictionary*) datadict;
- (NSString*) createDataElement : (NSString*) element
withElementData: (NSString*) data
withExternalDataBlock: (NSDictionary*) memblock;
- (NSString*) hexStringFromData : (NSData*) data;
- (NSNumber*) numericFromHexString : (NSString *) hexstring;
*/
@end

View File

@@ -0,0 +1,547 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVJpegHeaderWriter.h"
#include "CDVExif.h"
/* macros for tag info shorthand:
tagno : tag number
typecode : data type
components : number of components
appendString (TAGINF_W_APPEND only) : string to append to data
Exif date data format include an extra 0x00 to the end of the data
*/
#define TAGINF(tagno, typecode, components) [NSArray arrayWithObjects: tagno, typecode, components, nil]
#define TAGINF_W_APPEND(tagno, typecode, components, appendString) [NSArray arrayWithObjects: tagno, typecode, components, appendString, nil]
const uint mJpegId = 0xffd8; // JPEG format marker
const uint mExifMarker = 0xffe1; // APP1 jpeg header marker
const uint mExif = 0x45786966; // ASCII 'Exif', first characters of valid exif header after size
const uint mMotorallaByteAlign = 0x4d4d; // 'MM', motorola byte align, msb first or 'sane'
const uint mIntelByteAlgin = 0x4949; // 'II', Intel byte align, lsb first or 'batshit crazy reverso world'
const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a(MM) or 0x2a00(II), tiff version number
@implementation CDVJpegHeaderWriter
- (id) init {
self = [super init];
// supported tags for exif IFD
IFD0TagFormatDict = [[NSDictionary alloc] initWithObjectsAndKeys:
// TAGINF(@"010e", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"ImageDescription",
TAGINF_W_APPEND(@"0132", [NSNumber numberWithInt:EDT_ASCII_STRING], @20, @"00"), @"DateTime",
TAGINF(@"010f", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Make",
TAGINF(@"0110", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Model",
TAGINF(@"0131", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Software",
TAGINF(@"011a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"XResolution",
TAGINF(@"011b", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"YResolution",
// currently supplied outside of Exif data block by UIImagePickerControllerMediaMetadata, this is set manually in CDVCamera.m
/* TAGINF(@"0112", [NSNumber numberWithInt:EDT_USHORT], @1), @"Orientation",
// rest of the tags are supported by exif spec, but are not specified by UIImagePickerControllerMediaMedadata
// should camera hardware supply these values in future versions, or if they can be derived, ImageHeaderWriter will include them gracefully
TAGINF(@"0128", [NSNumber numberWithInt:EDT_USHORT], @1), @"ResolutionUnit",
TAGINF(@"013e", [NSNumber numberWithInt:EDT_URATIONAL], @2), @"WhitePoint",
TAGINF(@"013f", [NSNumber numberWithInt:EDT_URATIONAL], @6), @"PrimaryChromaticities",
TAGINF(@"0211", [NSNumber numberWithInt:EDT_URATIONAL], @3), @"YCbCrCoefficients",
TAGINF(@"0213", [NSNumber numberWithInt:EDT_USHORT], @1), @"YCbCrPositioning",
TAGINF(@"0214", [NSNumber numberWithInt:EDT_URATIONAL], @6), @"ReferenceBlackWhite",
TAGINF(@"8298", [NSNumber numberWithInt:EDT_URATIONAL], @0), @"Copyright",
// offset to exif subifd, we determine this dynamically based on the size of the main exif IFD
TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1), @"ExifOffset",*/
nil];
// supported tages for exif subIFD
SubIFDTagFormatDict = [[NSDictionary alloc] initWithObjectsAndKeys:
//TAGINF(@"9000", [NSNumber numberWithInt:], @), @"ExifVersion",
//TAGINF(@"9202",[NSNumber numberWithInt:EDT_URATIONAL],@1), @"ApertureValue",
//TAGINF(@"9203",[NSNumber numberWithInt:EDT_SRATIONAL],@1), @"BrightnessValue",
TAGINF(@"a001",[NSNumber numberWithInt:EDT_USHORT],@1), @"ColorSpace",
TAGINF_W_APPEND(@"9004",[NSNumber numberWithInt:EDT_ASCII_STRING],@20,@"00"), @"DateTimeDigitized",
TAGINF_W_APPEND(@"9003",[NSNumber numberWithInt:EDT_ASCII_STRING],@20,@"00"), @"DateTimeOriginal",
TAGINF(@"a402", [NSNumber numberWithInt:EDT_USHORT], @1), @"ExposureMode",
TAGINF(@"8822", [NSNumber numberWithInt:EDT_USHORT], @1), @"ExposureProgram",
//TAGINF(@"829a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"ExposureTime",
//TAGINF(@"829d", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"FNumber",
TAGINF(@"9209", [NSNumber numberWithInt:EDT_USHORT], @1), @"Flash",
// FocalLengthIn35mmFilm
TAGINF(@"a405", [NSNumber numberWithInt:EDT_USHORT], @1), @"FocalLenIn35mmFilm",
//TAGINF(@"920a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"FocalLength",
//TAGINF(@"8827", [NSNumber numberWithInt:EDT_USHORT], @2), @"ISOSpeedRatings",
TAGINF(@"9207", [NSNumber numberWithInt:EDT_USHORT],@1), @"MeteringMode",
// specific to compressed data
TAGINF(@"a002", [NSNumber numberWithInt:EDT_ULONG],@1), @"PixelXDimension",
TAGINF(@"a003", [NSNumber numberWithInt:EDT_ULONG],@1), @"PixelYDimension",
// data type undefined, but this is a DSC camera, so value is always 1, treat as ushort
TAGINF(@"a301", [NSNumber numberWithInt:EDT_USHORT],@1), @"SceneType",
TAGINF(@"a217",[NSNumber numberWithInt:EDT_USHORT],@1), @"SensingMethod",
//TAGINF(@"9201", [NSNumber numberWithInt:EDT_SRATIONAL], @1), @"ShutterSpeedValue",
// specifies location of main subject in scene (x,y,wdith,height) expressed before rotation processing
//TAGINF(@"9214", [NSNumber numberWithInt:EDT_USHORT], @4), @"SubjectArea",
TAGINF(@"a403", [NSNumber numberWithInt:EDT_USHORT], @1), @"WhiteBalance",
nil];
return self;
}
- (NSData*) spliceExifBlockIntoJpeg: (NSData*) jpegdata withExifBlock: (NSString*) exifstr {
CDVJpegHeaderWriter * exifWriter = [[CDVJpegHeaderWriter alloc] init];
NSMutableData * exifdata = [NSMutableData dataWithCapacity: [exifstr length]/2];
int idx;
for (idx = 0; idx+1 < [exifstr length]; idx+=2) {
NSRange range = NSMakeRange(idx, 2);
NSString* hexStr = [exifstr substringWithRange:range];
NSScanner* scanner = [NSScanner scannerWithString:hexStr];
unsigned int intValue;
[scanner scanHexInt:&intValue];
[exifdata appendBytes:&intValue length:1];
}
NSMutableData * ddata = [NSMutableData dataWithCapacity: [jpegdata length]];
NSMakeRange(0,4);
int loc = 0;
bool done = false;
// read the jpeg data until we encounter the app1==0xFFE1 marker
while (loc+1 < [jpegdata length]) {
NSData * blag = [jpegdata subdataWithRange: NSMakeRange(loc,2)];
if( [[blag description] isEqualToString : @"<ffe1>"]) {
// read the APP1 block size bits
NSString * the = [exifWriter hexStringFromData:[jpegdata subdataWithRange: NSMakeRange(loc+2,2)]];
NSNumber * app1width = [exifWriter numericFromHexString:the];
//consume the original app1 block
[ddata appendData:exifdata];
// advance our loc marker past app1
loc += [app1width intValue] + 2;
done = true;
} else {
if(!done) {
[ddata appendData:blag];
loc += 2;
} else {
break;
}
}
}
// copy the remaining data
[ddata appendData:[jpegdata subdataWithRange: NSMakeRange(loc,[jpegdata length]-loc)]];
return ddata;
}
/**
* Create the Exif data block as a hex string
* jpeg uses Application Markers (APP's) as markers for application data
* APP1 is the application marker reserved for exif data
*
* (NSDictionary*) datadict - with subdictionaries marked '{TIFF}' and '{EXIF}' as returned by imagePickerController with a valid
* didFinishPickingMediaWithInfo data dict, under key @"UIImagePickerControllerMediaMetadata"
*
* the following constructs a hex string to Exif specifications, and is therefore brittle
* altering the order of arguments to the string constructors, modifying field sizes or formats,
* and any other minor change will likely prevent the exif data from being read
*/
- (NSString*) createExifAPP1 : (NSDictionary*) datadict {
NSMutableString * app1; // holds finalized product
NSString * exifIFD; // exif information file directory
NSString * subExifIFD; // subexif information file directory
// FFE1 is the hex APP1 marker code, and will allow client apps to read the data
NSString * app1marker = @"ffe1";
// SSSS size, to be determined
// EXIF ascii characters followed by 2bytes of zeros
NSString * exifmarker = @"457869660000";
// Tiff header: 4d4d is motorolla byte align (big endian), 002a is hex for 42
NSString * tiffheader = @"4d4d002a";
//first IFD offset from the Tiff header to IFD0. Since we are writing it, we know it's address 0x08
NSString * ifd0offset = @"00000008";
// current offset to next data area
int currentDataOffset = 0;
//data labeled as TIFF in UIImagePickerControllerMediaMetaData is part of the EXIF IFD0 portion of APP1
exifIFD = [self createExifIFDFromDict: [datadict objectForKey:@"{TIFF}"] withFormatDict: IFD0TagFormatDict isIFD0:YES currentDataOffset:&currentDataOffset];
//data labeled as EXIF in UIImagePickerControllerMediaMetaData is part of the EXIF Sub IFD portion of APP1
subExifIFD = [self createExifIFDFromDict: [datadict objectForKey:@"{Exif}"] withFormatDict: SubIFDTagFormatDict isIFD0:NO currentDataOffset:&currentDataOffset];
/*
NSLog(@"SUB EXIF IFD %@ WITH SIZE: %d",exifIFD,[exifIFD length]);
NSLog(@"SUB EXIF IFD %@ WITH SIZE: %d",subExifIFD,[subExifIFD length]);
*/
// construct the complete app1 data block
app1 = [[NSMutableString alloc] initWithFormat: @"%@%04x%@%@%@%@%@",
app1marker,
(unsigned int)(16 + ([exifIFD length]/2) + ([subExifIFD length]/2)) /*16+[exifIFD length]/2*/,
exifmarker,
tiffheader,
ifd0offset,
exifIFD,
subExifIFD];
return app1;
}
// returns hex string representing a valid exif information file directory constructed from the datadict and formatdict
- (NSString*) createExifIFDFromDict : (NSDictionary*) datadict
withFormatDict : (NSDictionary*) formatdict
isIFD0 : (BOOL) ifd0flag
currentDataOffset : (int*) dataoffset {
NSArray * datakeys = [datadict allKeys]; // all known data keys
NSArray * knownkeys = [formatdict allKeys]; // only keys in knowkeys are considered for entry in this IFD
NSMutableArray * ifdblock = [[NSMutableArray alloc] initWithCapacity: [datadict count]]; // all ifd entries
NSMutableArray * ifddatablock = [[NSMutableArray alloc] initWithCapacity: [datadict count]]; // data block entries
// ifd0flag = NO; // ifd0 requires a special flag and has offset to next ifd appended to end
// iterate through known provided data keys
for (int i = 0; i < [datakeys count]; i++) {
NSString * key = [datakeys objectAtIndex:i];
// don't muck about with unknown keys
if ([knownkeys indexOfObject: key] != NSNotFound) {
// create new IFD entry
NSString * entry = [self createIFDElement: key
withFormat: [formatdict objectForKey:key]
withElementData: [datadict objectForKey:key]];
// create the IFD entry's data block
NSString * data = [self createIFDElementDataWithFormat: [formatdict objectForKey:key]
withData: [datadict objectForKey:key]];
if (entry) {
[ifdblock addObject:entry];
if(!data) {
[ifdblock addObject:@""];
} else {
[ifddatablock addObject:data];
}
}
}
}
NSMutableString * exifstr = [[NSMutableString alloc] initWithCapacity: [ifdblock count] * 24];
NSMutableString * dbstr = [[NSMutableString alloc] initWithCapacity: 100];
int addr=*dataoffset; // current offset/address in datablock
if (ifd0flag) {
// calculate offset to datablock based on ifd file entry count
addr += 14+(12*([ifddatablock count]+1)); // +1 for tag 0x8769, exifsubifd offset
} else {
// current offset + numSubIFDs (2-bytes) + 12*numSubIFDs + endMarker (4-bytes)
addr += 2+(12*[ifddatablock count])+4;
}
for (int i = 0; i < [ifdblock count]; i++) {
NSString * entry = [ifdblock objectAtIndex:i];
NSString * data = [ifddatablock objectAtIndex:i];
// check if the data fits into 4 bytes
if( [data length] <= 8) {
// concatenate the entry and the (4byte) data entry into the final IFD entry and append to exif ifd string
[exifstr appendFormat : @"%@%@", entry, data];
} else {
[exifstr appendFormat : @"%@%08x", entry, addr];
[dbstr appendFormat: @"%@", data];
addr+= [data length] / 2;
/*
NSLog(@"=====data-length[%i]=======",[data length]);
NSLog(@"addr-offset[%i]",addr);
NSLog(@"entry[%@]",entry);
NSLog(@"data[%@]",data);
*/
}
}
// calculate IFD0 terminal offset tags, currently ExifSubIFD
unsigned int entrycount = (unsigned int)[ifdblock count];
if (ifd0flag) {
// 18 accounts for 8769's width + offset to next ifd, 8 accounts for start of header
NSNumber * offset = [NSNumber numberWithUnsignedInteger:[exifstr length] / 2 + [dbstr length] / 2 + 18+8];
[self appendExifOffsetTagTo: exifstr
withOffset : offset];
entrycount++;
}
*dataoffset = addr;
return [[NSString alloc] initWithFormat: @"%04x%@%@%@",
entrycount,
exifstr,
@"00000000", // offset to next IFD, 0 since there is none
dbstr]; // lastly, the datablock
}
// Creates an exif formatted exif information file directory entry
- (NSString*) createIFDElement: (NSString*) elementName withFormat: (NSArray*) formtemplate withElementData: (NSString*) data {
//NSArray * fielddata = [formatdict objectForKey: elementName];// format data of desired field
if (formtemplate) {
// format string @"%@%@%@%@", tag number, data format, components, value
NSNumber * dataformat = [formtemplate objectAtIndex:1];
NSNumber * components = [formtemplate objectAtIndex:2];
if([components intValue] == 0) {
components = [NSNumber numberWithUnsignedInteger:[data length] * DataTypeToWidth[[dataformat intValue]-1]];
}
return [[NSString alloc] initWithFormat: @"%@%@%08x",
[formtemplate objectAtIndex:0], // the field code
[self formatNumberWithLeadingZeroes: dataformat withPlaces: @4], // the data type code
[components intValue]]; // number of components
}
return NULL;
}
/**
* appends exif IFD0 tag 8769 "ExifOffset" to the string provided
* (NSMutableString*) str - string you wish to append the 8769 tag to: APP1 or IFD0 hex data string
* // TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1), @"ExifOffset",
*/
- (void) appendExifOffsetTagTo: (NSMutableString*) str withOffset : (NSNumber*) offset {
NSArray * format = TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1);
NSString * entry = [self createIFDElement: @"ExifOffset"
withFormat: format
withElementData: [offset stringValue]];
NSString * data = [self createIFDElementDataWithFormat: format
withData: [offset stringValue]];
[str appendFormat:@"%@%@", entry, data];
}
// formats the Information File Directory Data to exif format
- (NSString*) createIFDElementDataWithFormat: (NSArray*) dataformat withData: (NSString*) data {
NSMutableString * datastr = nil;
NSNumber * tmp = nil;
NSNumber * formatcode = [dataformat objectAtIndex:1];
NSUInteger formatItemsCount = [dataformat count];
NSNumber * num = @0;
NSNumber * denom = @0;
switch ([formatcode intValue]) {
case EDT_UBYTE:
break;
case EDT_ASCII_STRING:
datastr = [[NSMutableString alloc] init];
for (int i = 0; i < [data length]; i++) {
[datastr appendFormat:@"%02x",[data characterAtIndex:i]];
}
if (formatItemsCount > 3) {
// We have additional data to append.
// currently used by Date format to append final 0x00 but can be used by other data types as well in the future
[datastr appendString:[dataformat objectAtIndex:3]];
}
if ([datastr length] < 8) {
NSString * format = [NSString stringWithFormat:@"%%0%dd", (int)(8 - [datastr length])];
[datastr appendFormat:format,0];
}
return datastr;
case EDT_USHORT:
return [[NSString alloc] initWithFormat : @"%@%@",
[self formattedHexStringFromDecimalNumber: [NSNumber numberWithInt: [data intValue]] withPlaces: @4],
@"0000"];
case EDT_ULONG:
tmp = [NSNumber numberWithUnsignedLong:[data intValue]];
return [NSString stringWithFormat : @"%@",
[self formattedHexStringFromDecimalNumber: tmp withPlaces: @8]];
case EDT_URATIONAL:
return [self decimalToUnsignedRational: [NSNumber numberWithDouble:[data doubleValue]]
withResultNumerator: &num
withResultDenominator: &denom];
case EDT_SBYTE:
break;
case EDT_UNDEFINED:
break; // 8 bits
case EDT_SSHORT:
break;
case EDT_SLONG:
break; // 32bit signed integer (2's complement)
case EDT_SRATIONAL:
break; // 2 SLONGS, first long is numerator, second is denominator
case EDT_SINGLEFLOAT:
break;
case EDT_DOUBLEFLOAT:
break;
}
return datastr;
}
//======================================================================================================================
// Utility Methods
//======================================================================================================================
// creates a formatted little endian hex string from a number and width specifier
- (NSString*) formattedHexStringFromDecimalNumber: (NSNumber*) numb withPlaces: (NSNumber*) width {
NSMutableString * str = [[NSMutableString alloc] initWithCapacity:[width intValue]];
NSString * formatstr = [[NSString alloc] initWithFormat: @"%%%@%dx", @"0", [width intValue]];
[str appendFormat:formatstr, [numb intValue]];
return str;
}
// format number as string with leading 0's
- (NSString*) formatNumberWithLeadingZeroes: (NSNumber *) numb withPlaces: (NSNumber *) places {
NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init];
NSString *formatstr = [@"" stringByPaddingToLength:[places unsignedIntegerValue] withString:@"0" startingAtIndex:0];
[formatter setPositiveFormat:formatstr];
return [formatter stringFromNumber:numb];
}
// approximate a decimal with a rational by method of continued fraction
// can be collasped into decimalToUnsignedRational after testing
- (void) decimalToRational: (NSNumber *) numb
withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator {
NSMutableArray * fractionlist = [[NSMutableArray alloc] initWithCapacity:8];
[self continuedFraction: [numb doubleValue]
withFractionList: fractionlist
withHorizon: 8];
// simplify complex fraction represented by partial fraction list
[self expandContinuedFraction: fractionlist
withResultNumerator: numerator
withResultDenominator: denominator];
}
// approximate a decimal with an unsigned rational by method of continued fraction
- (NSString*) decimalToUnsignedRational: (NSNumber *) numb
withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator {
NSMutableArray * fractionlist = [[NSMutableArray alloc] initWithCapacity:8];
// generate partial fraction list
[self continuedFraction: [numb doubleValue]
withFractionList: fractionlist
withHorizon: 8];
// simplify complex fraction represented by partial fraction list
[self expandContinuedFraction: fractionlist
withResultNumerator: numerator
withResultDenominator: denominator];
return [self formatFractionList: fractionlist];
}
// recursive implementation of decimal approximation by continued fraction
- (void) continuedFraction: (double) val
withFractionList: (NSMutableArray*) fractionlist
withHorizon: (int) horizon {
int whole;
double remainder;
// 1. split term
[self splitDouble: val withIntComponent: &whole withFloatRemainder: &remainder];
[fractionlist addObject: [NSNumber numberWithInt:whole]];
// 2. calculate reciprocal of remainder
if (!remainder) return; // early exit, exact fraction found, avoids recip/0
double recip = 1 / remainder;
// 3. exit condition
if ([fractionlist count] > horizon) {
return;
}
// 4. recurse
[self continuedFraction:recip withFractionList: fractionlist withHorizon: horizon];
}
// expand continued fraction list, creating a single level rational approximation
-(void) expandContinuedFraction: (NSArray*) fractionlist
withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator {
NSUInteger i = 0;
int den = 0;
int num = 0;
if ([fractionlist count] == 1) {
*numerator = [NSNumber numberWithInt:[[fractionlist objectAtIndex:0] intValue]];
*denominator = @1;
return;
}
//begin at the end of the list
i = [fractionlist count] - 1;
num = 1;
den = [[fractionlist objectAtIndex:i] intValue];
while (i > 0) {
int t = [[fractionlist objectAtIndex: i-1] intValue];
num = t * den + num;
if (i==1) {
break;
} else {
t = num;
num = den;
den = t;
}
i--;
}
// set result parameters values
*numerator = [NSNumber numberWithInt: num];
*denominator = [NSNumber numberWithInt: den];
}
// formats expanded fraction list to string matching exif specification
- (NSString*) formatFractionList: (NSArray *) fractionlist {
NSMutableString * str = [[NSMutableString alloc] initWithCapacity:16];
if ([fractionlist count] == 1){
[str appendFormat: @"%08x00000001", [[fractionlist objectAtIndex:0] intValue]];
}
return str;
}
// format rational as
- (NSString*) formatRationalWithNumerator: (NSNumber*) numerator withDenominator: (NSNumber*) denominator asSigned: (Boolean) signedFlag {
NSMutableString * str = [[NSMutableString alloc] initWithCapacity:16];
if (signedFlag) {
long num = [numerator longValue];
long den = [denominator longValue];
[str appendFormat: @"%08lx%08lx", num >= 0 ? num : ~ABS(num) + 1, num >= 0 ? den : ~ABS(den) + 1];
} else {
[str appendFormat: @"%08lx%08lx", [numerator unsignedLongValue], [denominator unsignedLongValue]];
}
return str;
}
// split a floating point number into two integer values representing the left and right side of the decimal
- (void) splitDouble: (double) val withIntComponent: (int*) rightside withFloatRemainder: (double*) leftside {
*rightside = val; // convert numb to int representation, which truncates the decimal portion
*leftside = val - *rightside;
}
//
- (NSString*) hexStringFromData : (NSData*) data {
//overflow detection
const unsigned char *dataBuffer = [data bytes];
return [[NSString alloc] initWithFormat: @"%02x%02x",
(unsigned char)dataBuffer[0],
(unsigned char)dataBuffer[1]];
}
// convert a hex string to a number
- (NSNumber*) numericFromHexString : (NSString *) hexstring {
NSScanner * scan = NULL;
unsigned int numbuf= 0;
scan = [NSScanner scannerWithString:hexstring];
[scan scanHexInt:&numbuf];
return [NSNumber numberWithInt:numbuf];
}
@end

View File

@@ -0,0 +1,29 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <UIKit/UIKit.h>
@interface UIImage (CropScaleOrientation)
- (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize;
- (UIImage*)imageCorrectedForCaptureOrientation;
- (UIImage*)imageCorrectedForCaptureOrientation:(UIImageOrientation)imageOrientation;
- (UIImage*)imageByScalingNotCroppingForSize:(CGSize)targetSize;
@end

View File

@@ -0,0 +1,175 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "UIImage+CropScaleOrientation.h"
@implementation UIImage (CropScaleOrientation)
- (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize
{
UIImage* sourceImage = self;
UIImage* newImage = nil;
CGSize imageSize = sourceImage.size;
CGFloat width = imageSize.width;
CGFloat height = imageSize.height;
CGFloat targetWidth = targetSize.width;
CGFloat targetHeight = targetSize.height;
CGFloat scaleFactor = 0.0;
CGFloat scaledWidth = targetWidth;
CGFloat scaledHeight = targetHeight;
CGPoint thumbnailPoint = CGPointMake(0.0, 0.0);
if (CGSizeEqualToSize(imageSize, targetSize) == NO) {
CGFloat widthFactor = targetWidth / width;
CGFloat heightFactor = targetHeight / height;
if (widthFactor > heightFactor) {
scaleFactor = widthFactor; // scale to fit height
} else {
scaleFactor = heightFactor; // scale to fit width
}
scaledWidth = width * scaleFactor;
scaledHeight = height * scaleFactor;
// center the image
if (widthFactor > heightFactor) {
thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5;
} else if (widthFactor < heightFactor) {
thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
}
}
UIGraphicsBeginImageContext(targetSize); // this will crop
CGRect thumbnailRect = CGRectZero;
thumbnailRect.origin = thumbnailPoint;
thumbnailRect.size.width = scaledWidth;
thumbnailRect.size.height = scaledHeight;
[sourceImage drawInRect:thumbnailRect];
newImage = UIGraphicsGetImageFromCurrentImageContext();
if (newImage == nil) {
NSLog(@"could not scale image");
}
// pop the context to get back to the default
UIGraphicsEndImageContext();
return newImage;
}
- (UIImage*)imageCorrectedForCaptureOrientation:(UIImageOrientation)imageOrientation
{
float rotation_radians = 0;
bool perpendicular = false;
switch (imageOrientation) {
case UIImageOrientationUp :
rotation_radians = 0.0;
break;
case UIImageOrientationDown:
rotation_radians = M_PI; // don't be scared of radians, if you're reading this, you're good at math
break;
case UIImageOrientationRight:
rotation_radians = M_PI_2;
perpendicular = true;
break;
case UIImageOrientationLeft:
rotation_radians = -M_PI_2;
perpendicular = true;
break;
default:
break;
}
UIGraphicsBeginImageContext(CGSizeMake(self.size.width, self.size.height));
CGContextRef context = UIGraphicsGetCurrentContext();
// Rotate around the center point
CGContextTranslateCTM(context, self.size.width / 2, self.size.height / 2);
CGContextRotateCTM(context, rotation_radians);
CGContextScaleCTM(context, 1.0, -1.0);
float width = perpendicular ? self.size.height : self.size.width;
float height = perpendicular ? self.size.width : self.size.height;
CGContextDrawImage(context, CGRectMake(-width / 2, -height / 2, width, height), [self CGImage]);
// Move the origin back since the rotation might've change it (if its 90 degrees)
if (perpendicular) {
CGContextTranslateCTM(context, -self.size.height / 2, -self.size.width / 2);
}
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
- (UIImage*)imageCorrectedForCaptureOrientation
{
return [self imageCorrectedForCaptureOrientation:[self imageOrientation]];
}
- (UIImage*)imageByScalingNotCroppingForSize:(CGSize)targetSize
{
UIImage* sourceImage = self;
UIImage* newImage = nil;
CGSize imageSize = sourceImage.size;
CGFloat width = imageSize.width;
CGFloat height = imageSize.height;
CGFloat targetWidth = targetSize.width;
CGFloat targetHeight = targetSize.height;
CGFloat scaleFactor = 0.0;
CGSize scaledSize = targetSize;
if (CGSizeEqualToSize(imageSize, targetSize) == NO) {
CGFloat widthFactor = targetWidth / width;
CGFloat heightFactor = targetHeight / height;
// opposite comparison to imageByScalingAndCroppingForSize in order to contain the image within the given bounds
if (widthFactor > heightFactor) {
scaleFactor = heightFactor; // scale to fit height
} else {
scaleFactor = widthFactor; // scale to fit width
}
scaledSize = CGSizeMake(MIN(width * scaleFactor, targetWidth), MIN(height * scaleFactor, targetHeight));
}
// If the pixels are floats, it causes a white line in iOS8 and probably other versions too
scaledSize.width = (int)scaledSize.width;
scaledSize.height = (int)scaledSize.height;
UIGraphicsBeginImageContext(scaledSize); // this will resize
[sourceImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
if (newImage == nil) {
NSLog(@"could not scale image");
}
// pop the context to get back to the default
UIGraphicsEndImageContext();
return newImage;
}
@end

View File

@@ -0,0 +1,118 @@
/*
*
* Copyright 2013 Canonical Ltd.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
import QtQuick 2.0
import QtMultimedia 5.0
Rectangle {
property string shootImagePath: "shoot.png"
function isSuffix(str, suffix) {
return String(str).substr(String(str).length - suffix.length) == suffix
}
id: ui
color: "#252423"
anchors.fill: parent
Camera {
objectName: "camera"
id: camera
onError: {
console.log(errorString);
}
videoRecorder.audioBitRate: 128000
imageCapture {
onImageSaved: {
root.exec("Camera", "onImageSaved", [path]);
ui.destroy();
}
}
}
VideoOutput {
id: output
source: camera
width: parent.width
height: parent.height
}
Item {
anchors.bottom: parent.bottom
width: parent.width
height: shootButton.height
BorderImage {
id: leftBackground
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: middle.left
anchors.topMargin: units.dp(2)
anchors.bottomMargin: units.dp(2)
source: "toolbar-left.png"
Image {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: parent.iconSpacing
source: "back.png"
width: units.gu(6)
height: units.gu(5)
MouseArea {
anchors.fill: parent
onClicked: {
root.exec("Camera", "cancel");
}
}
}
}
BorderImage {
id: middle
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: shootButton.height + units.gu(1)
width: shootButton.width
source: "toolbar-middle.png"
Image {
id: shootButton
width: units.gu(8)
height: width
anchors.horizontalCenter: parent.horizontalCenter
source: shootImagePath
MouseArea {
anchors.fill: parent
onClicked: {
camera.imageCapture.captureToLocation(ui.parent.plugin('Camera').generateLocation("jpg"));
}
}
}
}
BorderImage {
id: rightBackground
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: middle.right
anchors.topMargin: units.dp(2)
anchors.bottomMargin: units.dp(2)
source: "toolbar-right.png"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,140 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#include "camera.h"
#include <cordova.h>
#include <QCameraViewfinder>
#include <QCameraImageCapture>
#include <QGraphicsObject>
#include <QCloseEvent>
#include <QQuickItem>
const char code[] = "\
var component, object; \
function createObject() { \
component = Qt.createComponent(%1); \
if (component.status == Component.Ready) \
finishCreation(); \
else \
component.statusChanged.connect(finishCreation); \
} \
function finishCreation() { \
CordovaWrapper.global.cameraPluginWidget = component.createObject(root, \
{root: root, cordova: cordova}); \
} \
createObject()";
Camera::Camera(Cordova *cordova):
CPlugin(cordova),
_lastScId(0),
_lastEcId(0) {
}
bool Camera::preprocessImage(QString &path) {
bool convertToPNG = (*_options.find("encodingType")).toInt() == Camera::PNG;
int quality = (*_options.find("quality")).toInt();
int width = (*_options.find("targetWidth")).toInt();
int height = (*_options.find("targetHeight")).toInt();
QImage image(path);
if (width <= 0)
width = image.width();
if (height <= 0)
height = image.height();
image = image.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QFile oldImage(path);
QTemporaryFile newImage;
const char *type;
if (convertToPNG) {
path = generateLocation("png");
type = "png";
} else {
path = generateLocation("jpg");
type = "jpg";
}
image.save(path, type, quality);
oldImage.remove();
return true;
}
void Camera::onImageSaved(QString path) {
bool dataURL = _options.find("destinationType")->toInt() == Camera::DATA_URL;
QString cbParams;
if (preprocessImage(path)) {
QString absolutePath = QFileInfo(path).absoluteFilePath();
if (dataURL) {
QFile image(absolutePath);
image.open(QIODevice::ReadOnly);
QByteArray content = image.readAll().toBase64();
cbParams = QString("\"%1\"").arg(content.data());
image.remove();
} else {
cbParams = CordovaInternal::format(QString("file://localhost") + absolutePath);
}
}
this->callback(_lastScId, cbParams);
_lastEcId = _lastScId = 0;
}
void Camera::takePicture(int scId, int ecId, int quality, int destinationType, int/*sourceType*/, int targetWidth, int targetHeight, int encodingType,
int/*mediaType*/, bool/*allowEdit*/, bool/*correctOrientation*/, bool/*saveToPhotoAlbum*/, const QVariantMap &/*popoverOptions*/, int/*cameraDirection*/) {
if (_camera.isNull()) {
_camera = QSharedPointer<QCamera>(new QCamera());
}
if (((_lastScId || _lastEcId) && (_lastScId != scId && _lastEcId != ecId)) || !_camera->isAvailable() || _camera->lockStatus() != QCamera::Unlocked) {
this->cb(_lastEcId, "Device is busy");
return;
}
_options.clear();
_options.insert("quality", quality);
_options.insert("destinationType", destinationType);
_options.insert("targetWidth", targetWidth);
_options.insert("targetHeight", targetHeight);
_options.insert("encodingType", encodingType);
_lastScId = scId;
_lastEcId = ecId;
QString path = m_cordova->get_app_dir() + "/../qml/CaptureWidget.qml";
// TODO: relative url
QString qml = QString(code).arg(CordovaInternal::format(path));
m_cordova->execQML(qml);
}
void Camera::cancel() {
m_cordova->execQML("CordovaWrapper.global.cameraPluginWidget.destroy()");
this->cb(_lastEcId, "canceled");
_lastEcId = _lastScId = 0;
}

View File

@@ -0,0 +1,86 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#ifndef CAMERA_H
#define CAMERA_H
#include <cplugin.h>
#include <QtCore>
#include <QQuickView>
#include <QCamera>
#include <QtMultimediaWidgets/QCameraViewfinder>
#include <QCameraImageCapture>
class Camera: public CPlugin {
Q_OBJECT
public:
explicit Camera(Cordova *cordova);
virtual const QString fullName() override {
return Camera::fullID();
}
virtual const QString shortName() override {
return "Camera";
}
static const QString fullID() {
return "Camera";
}
public slots:
void takePicture(int scId, int ecId, int quality, int destinationType, int/*sourceType*/, int targetWidth, int targetHeight, int encodingType,
int/*mediaType*/, bool/*allowEdit*/, bool/*correctOrientation*/, bool/*saveToPhotoAlbum*/, const QVariantMap &popoverOptions, int cameraDirection);
void cancel();
void onImageSaved(QString path);
QString generateLocation(const QString &extension) {
int i = 1;
for (;;++i) {
QString path = QString("%1/.local/share/%2/persistent/%3.%4").arg(QDir::homePath())
.arg(QCoreApplication::applicationName()).arg(i).arg(extension);
if (!QFileInfo(path).exists())
return path;
}
}
private:
bool preprocessImage(QString &path);
int _lastScId;
int _lastEcId;
QSharedPointer<QCamera> _camera;
QVariantMap _options;
protected:
enum DestinationType {
DATA_URL = 0,
FILE_URI = 1
};
enum EncodingType {
JPEG = 0,
PNG = 1
};
};
#endif // CAMERA_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,882 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/*jshint unused:true, undef:true, browser:true */
/*global Windows:true, URL:true, module:true, require:true, WinJS:true */
var Camera = require('./Camera');
var getAppData = function () {
return Windows.Storage.ApplicationData.current;
};
var encodeToBase64String = function (buffer) {
return Windows.Security.Cryptography.CryptographicBuffer.encodeToBase64String(buffer);
};
var OptUnique = Windows.Storage.CreationCollisionOption.generateUniqueName;
var CapMSType = Windows.Media.Capture.MediaStreamType;
var webUIApp = Windows.UI.WebUI.WebUIApplication;
var fileIO = Windows.Storage.FileIO;
var pickerLocId = Windows.Storage.Pickers.PickerLocationId;
module.exports = {
// args will contain :
// ... it is an array, so be careful
// 0 quality:50,
// 1 destinationType:Camera.DestinationType.FILE_URI,
// 2 sourceType:Camera.PictureSourceType.CAMERA,
// 3 targetWidth:-1,
// 4 targetHeight:-1,
// 5 encodingType:Camera.EncodingType.JPEG,
// 6 mediaType:Camera.MediaType.PICTURE,
// 7 allowEdit:false,
// 8 correctOrientation:false,
// 9 saveToPhotoAlbum:false,
// 10 popoverOptions:null
// 11 cameraDirection:0
takePicture: function (successCallback, errorCallback, args) {
var sourceType = args[2];
if (sourceType != Camera.PictureSourceType.CAMERA) {
takePictureFromFile(successCallback, errorCallback, args);
} else {
takePictureFromCamera(successCallback, errorCallback, args);
}
}
};
// https://msdn.microsoft.com/en-us/library/windows/apps/ff462087(v=vs.105).aspx
var windowsVideoContainers = [".avi", ".flv", ".asx", ".asf", ".mov", ".mp4", ".mpg", ".rm", ".srt", ".swf", ".wmv", ".vob"];
var windowsPhoneVideoContainers = [".avi", ".3gp", ".3g2", ".wmv", ".3gp", ".3g2", ".mp4", ".m4v"];
// Default aspect ratio 1.78 (16:9 hd video standard)
var DEFAULT_ASPECT_RATIO = '1.8';
// Highest possible z-index supported across browsers. Anything used above is converted to this value.
var HIGHEST_POSSIBLE_Z_INDEX = 2147483647;
// Resize method
function resizeImage(successCallback, errorCallback, file, targetWidth, targetHeight, encodingType) {
var tempPhotoFileName = "";
var targetContentType = "";
if (encodingType == Camera.EncodingType.PNG) {
tempPhotoFileName = "camera_cordova_temp_return.png";
targetContentType = "image/png";
} else {
tempPhotoFileName = "camera_cordova_temp_return.jpg";
targetContentType = "image/jpeg";
}
var storageFolder = getAppData().localFolder;
file.copyAsync(storageFolder, file.name, Windows.Storage.NameCollisionOption.replaceExisting)
.then(function (storageFile) {
return fileIO.readBufferAsync(storageFile);
})
.then(function(buffer) {
var strBase64 = encodeToBase64String(buffer);
var imageData = "data:" + file.contentType + ";base64," + strBase64;
var image = new Image();
image.src = imageData;
image.onload = function() {
var ratio = Math.min(targetWidth / this.width, targetHeight / this.height);
var imageWidth = ratio * this.width;
var imageHeight = ratio * this.height;
var canvas = document.createElement('canvas');
var storageFileName;
canvas.width = imageWidth;
canvas.height = imageHeight;
canvas.getContext("2d").drawImage(this, 0, 0, imageWidth, imageHeight);
var fileContent = canvas.toDataURL(targetContentType).split(',')[1];
var storageFolder = getAppData().localFolder;
storageFolder.createFileAsync(tempPhotoFileName, OptUnique)
.then(function (storagefile) {
var content = Windows.Security.Cryptography.CryptographicBuffer.decodeFromBase64String(fileContent);
storageFileName = storagefile.name;
return fileIO.writeBufferAsync(storagefile, content);
})
.done(function () {
successCallback("ms-appdata:///local/" + storageFileName);
}, errorCallback);
};
})
.done(null, function(err) {
errorCallback(err);
}
);
}
// Because of asynchronous method, so let the successCallback be called in it.
function resizeImageBase64(successCallback, errorCallback, file, targetWidth, targetHeight) {
fileIO.readBufferAsync(file).done( function(buffer) {
var strBase64 = encodeToBase64String(buffer);
var imageData = "data:" + file.contentType + ";base64," + strBase64;
var image = new Image();
image.src = imageData;
image.onload = function() {
var ratio = Math.min(targetWidth / this.width, targetHeight / this.height);
var imageWidth = ratio * this.width;
var imageHeight = ratio * this.height;
var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;
var ctx = canvas.getContext("2d");
ctx.drawImage(this, 0, 0, imageWidth, imageHeight);
// The resized file ready for upload
var finalFile = canvas.toDataURL(file.contentType);
// Remove the prefix such as "data:" + contentType + ";base64," , in order to meet the Cordova API.
var arr = finalFile.split(",");
var newStr = finalFile.substr(arr[0].length + 1);
successCallback(newStr);
};
}, function(err) { errorCallback(err); });
}
function takePictureFromFile(successCallback, errorCallback, args) {
// Detect Windows Phone
if (navigator.appVersion.indexOf('Windows Phone 8.1') >= 0) {
takePictureFromFileWP(successCallback, errorCallback, args);
} else {
takePictureFromFileWindows(successCallback, errorCallback, args);
}
}
function takePictureFromFileWP(successCallback, errorCallback, args) {
var mediaType = args[6],
destinationType = args[1],
targetWidth = args[3],
targetHeight = args[4],
encodingType = args[5];
/*
Need to add and remove an event listener to catch activation state
Using FileOpenPicker will suspend the app and it's required to catch the PickSingleFileAndContinue
https://msdn.microsoft.com/en-us/library/windows/apps/xaml/dn631755.aspx
*/
var filePickerActivationHandler = function(eventArgs) {
if (eventArgs.kind === Windows.ApplicationModel.Activation.ActivationKind.pickFileContinuation) {
var file = eventArgs.files[0];
if (!file) {
errorCallback("User didn't choose a file.");
webUIApp.removeEventListener("activated", filePickerActivationHandler);
return;
}
if (destinationType == Camera.DestinationType.FILE_URI || destinationType == Camera.DestinationType.NATIVE_URI) {
if (targetHeight > 0 && targetWidth > 0) {
resizeImage(successCallback, errorCallback, file, targetWidth, targetHeight, encodingType);
}
else {
var storageFolder = getAppData().localFolder;
file.copyAsync(storageFolder, file.name, Windows.Storage.NameCollisionOption.replaceExisting).done(function (storageFile) {
if(destinationType == Camera.DestinationType.NATIVE_URI) {
successCallback("ms-appdata:///local/" + storageFile.name);
}
else {
successCallback(URL.createObjectURL(storageFile));
}
}, function () {
errorCallback("Can't access localStorage folder.");
});
}
}
else {
if (targetHeight > 0 && targetWidth > 0) {
resizeImageBase64(successCallback, errorCallback, file, targetWidth, targetHeight);
} else {
fileIO.readBufferAsync(file).done(function (buffer) {
var strBase64 =encodeToBase64String(buffer);
successCallback(strBase64);
}, errorCallback);
}
}
webUIApp.removeEventListener("activated", filePickerActivationHandler);
}
};
var fileOpenPicker = new Windows.Storage.Pickers.FileOpenPicker();
if (mediaType == Camera.MediaType.PICTURE) {
fileOpenPicker.fileTypeFilter.replaceAll([".png", ".jpg", ".jpeg"]);
fileOpenPicker.suggestedStartLocation = pickerLocId.picturesLibrary;
}
else if (mediaType == Camera.MediaType.VIDEO) {
fileOpenPicker.fileTypeFilter.replaceAll(windowsPhoneVideoContainers);
fileOpenPicker.suggestedStartLocation = pickerLocId.videosLibrary;
}
else {
fileOpenPicker.fileTypeFilter.replaceAll(["*"]);
fileOpenPicker.suggestedStartLocation = pickerLocId.documentsLibrary;
}
webUIApp.addEventListener("activated", filePickerActivationHandler);
fileOpenPicker.pickSingleFileAndContinue();
}
function takePictureFromFileWindows(successCallback, errorCallback, args) {
var mediaType = args[6],
destinationType = args[1],
targetWidth = args[3],
targetHeight = args[4],
encodingType = args[5];
var fileOpenPicker = new Windows.Storage.Pickers.FileOpenPicker();
if (mediaType == Camera.MediaType.PICTURE) {
fileOpenPicker.fileTypeFilter.replaceAll([".png", ".jpg", ".jpeg"]);
fileOpenPicker.suggestedStartLocation = pickerLocId.picturesLibrary;
}
else if (mediaType == Camera.MediaType.VIDEO) {
fileOpenPicker.fileTypeFilter.replaceAll(windowsVideoContainers);
fileOpenPicker.suggestedStartLocation = pickerLocId.videosLibrary;
}
else {
fileOpenPicker.fileTypeFilter.replaceAll(["*"]);
fileOpenPicker.suggestedStartLocation = pickerLocId.documentsLibrary;
}
fileOpenPicker.pickSingleFileAsync().done(function (file) {
if (!file) {
errorCallback("User didn't choose a file.");
return;
}
if (destinationType == Camera.DestinationType.FILE_URI || destinationType == Camera.DestinationType.NATIVE_URI) {
if (targetHeight > 0 && targetWidth > 0) {
resizeImage(successCallback, errorCallback, file, targetWidth, targetHeight, encodingType);
}
else {
var storageFolder = getAppData().localFolder;
file.copyAsync(storageFolder, file.name, Windows.Storage.NameCollisionOption.replaceExisting).done(function (storageFile) {
if(destinationType == Camera.DestinationType.NATIVE_URI) {
successCallback("ms-appdata:///local/" + storageFile.name);
}
else {
successCallback(URL.createObjectURL(storageFile));
}
}, function () {
errorCallback("Can't access localStorage folder.");
});
}
}
else {
if (targetHeight > 0 && targetWidth > 0) {
resizeImageBase64(successCallback, errorCallback, file, targetWidth, targetHeight);
} else {
fileIO.readBufferAsync(file).done(function (buffer) {
var strBase64 =encodeToBase64String(buffer);
successCallback(strBase64);
}, errorCallback);
}
}
}, function () {
errorCallback("User didn't choose a file.");
});
}
function takePictureFromCamera(successCallback, errorCallback, args) {
// Check if necessary API available
if (!Windows.Media.Capture.CameraCaptureUI) {
takePictureFromCameraWP(successCallback, errorCallback, args);
} else {
takePictureFromCameraWindows(successCallback, errorCallback, args);
}
}
function takePictureFromCameraWP(successCallback, errorCallback, args) {
// We are running on WP8.1 which lacks CameraCaptureUI class
// so we need to use MediaCapture class instead and implement custom UI for camera
var destinationType = args[1],
targetWidth = args[3],
targetHeight = args[4],
encodingType = args[5],
saveToPhotoAlbum = args[9],
cameraDirection = args[11],
capturePreview = null,
cameraCaptureButton = null,
cameraCancelButton = null,
capture = null,
captureSettings = null,
CaptureNS = Windows.Media.Capture,
sensor = null;
function createCameraUI() {
// create style for take and cancel buttons
var buttonStyle = "width:45%;padding: 10px 16px;font-size: 18px;line-height: 1.3333333;color: #333;background-color: #fff;border-color: #ccc; border: 1px solid transparent;border-radius: 6px; display: block; margin: 20px; z-index: 1000;border-color: #adadad;";
// Create fullscreen preview
// z-order style element for capturePreview and cameraCancelButton elts
// is necessary to avoid overriding by another page elements, -1 sometimes is not enough
capturePreview = document.createElement("video");
capturePreview.style.cssText = "position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: " + (HIGHEST_POSSIBLE_Z_INDEX - 1) + ";";
// Create capture button
cameraCaptureButton = document.createElement("button");
cameraCaptureButton.innerText = "Take";
cameraCaptureButton.style.cssText = buttonStyle + "position: fixed; left: 0; bottom: 0; margin: 20px; z-index: " + HIGHEST_POSSIBLE_Z_INDEX + ";";
// Create cancel button
cameraCancelButton = document.createElement("button");
cameraCancelButton.innerText = "Cancel";
cameraCancelButton.style.cssText = buttonStyle + "position: fixed; right: 0; bottom: 0; margin: 20px; z-index: " + HIGHEST_POSSIBLE_Z_INDEX + ";";
capture = new CaptureNS.MediaCapture();
captureSettings = new CaptureNS.MediaCaptureInitializationSettings();
captureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.video;
}
function continueVideoOnFocus() {
// if preview is defined it would be stuck, play it
if (capturePreview) {
capturePreview.play();
}
}
function startCameraPreview() {
// Search for available camera devices
// This is necessary to detect which camera (front or back) we should use
var DeviceEnum = Windows.Devices.Enumeration;
var expectedPanel = cameraDirection === 1 ? DeviceEnum.Panel.front : DeviceEnum.Panel.back;
// Add focus event handler to capture the event when user suspends the app and comes back while the preview is on
window.addEventListener("focus", continueVideoOnFocus);
DeviceEnum.DeviceInformation.findAllAsync(DeviceEnum.DeviceClass.videoCapture).then(function (devices) {
if (devices.length <= 0) {
destroyCameraPreview();
errorCallback('Camera not found');
return;
}
devices.forEach(function(currDev) {
if (currDev.enclosureLocation.panel && currDev.enclosureLocation.panel == expectedPanel) {
captureSettings.videoDeviceId = currDev.id;
}
});
captureSettings.photoCaptureSource = Windows.Media.Capture.PhotoCaptureSource.photo;
return capture.initializeAsync(captureSettings);
}).then(function () {
// create focus control if available
var VideoDeviceController = capture.videoDeviceController;
var FocusControl = VideoDeviceController.focusControl;
if (FocusControl.supported === true) {
capturePreview.addEventListener('click', function () {
// Make sure function isn't called again before previous focus is completed
if (this.getAttribute('clicked') === '1') {
return false;
} else {
this.setAttribute('clicked', '1');
}
var preset = Windows.Media.Devices.FocusPreset.autoNormal;
var parent = this;
FocusControl.setPresetAsync(preset).done(function () {
// set the clicked attribute back to '0' to allow focus again
parent.setAttribute('clicked', '0');
});
});
}
// msdn.microsoft.com/en-us/library/windows/apps/hh452807.aspx
capturePreview.msZoom = true;
capturePreview.src = URL.createObjectURL(capture);
capturePreview.play();
// Bind events to controls
sensor = Windows.Devices.Sensors.SimpleOrientationSensor.getDefault();
if (sensor !== null) {
sensor.addEventListener("orientationchanged", onOrientationChange);
}
// add click events to capture and cancel buttons
cameraCaptureButton.addEventListener('click', onCameraCaptureButtonClick);
cameraCancelButton.addEventListener('click', onCameraCancelButtonClick);
// Change default orientation
if (sensor) {
setPreviewRotation(sensor.getCurrentOrientation());
} else {
setPreviewRotation(Windows.Graphics.Display.DisplayInformation.getForCurrentView().currentOrientation);
}
// Get available aspect ratios
var aspectRatios = getAspectRatios(capture);
// Couldn't find a good ratio
if (aspectRatios.length === 0) {
destroyCameraPreview();
errorCallback('There\'s not a good aspect ratio available');
return;
}
// add elements to body
document.body.appendChild(capturePreview);
document.body.appendChild(cameraCaptureButton);
document.body.appendChild(cameraCancelButton);
if (aspectRatios.indexOf(DEFAULT_ASPECT_RATIO) > -1) {
return setAspectRatio(capture, DEFAULT_ASPECT_RATIO);
} else {
// Doesn't support 16:9 - pick next best
return setAspectRatio(capture, aspectRatios[0]);
}
}).done(null, function (err) {
destroyCameraPreview();
errorCallback('Camera intitialization error ' + err);
});
}
function destroyCameraPreview() {
// If sensor is available, remove event listener
if (sensor !== null) {
sensor.removeEventListener('orientationchanged', onOrientationChange);
}
// Pause and dispose preview element
capturePreview.pause();
capturePreview.src = null;
// Remove event listeners from buttons
cameraCaptureButton.removeEventListener('click', onCameraCaptureButtonClick);
cameraCancelButton.removeEventListener('click', onCameraCancelButtonClick);
// Remove the focus event handler
window.removeEventListener("focus", continueVideoOnFocus);
// Remove elements
[capturePreview, cameraCaptureButton, cameraCancelButton].forEach(function (elem) {
if (elem /* && elem in document.body.childNodes */) {
document.body.removeChild(elem);
}
});
// Stop and dispose media capture manager
if (capture) {
capture.stopRecordAsync();
capture = null;
}
}
function captureAction() {
var encodingProperties,
fileName,
tempFolder = getAppData().temporaryFolder;
if (encodingType == Camera.EncodingType.PNG) {
fileName = 'photo.png';
encodingProperties = Windows.Media.MediaProperties.ImageEncodingProperties.createPng();
} else {
fileName = 'photo.jpg';
encodingProperties = Windows.Media.MediaProperties.ImageEncodingProperties.createJpeg();
}
tempFolder.createFileAsync(fileName, OptUnique)
.then(function(tempCapturedFile) {
return new WinJS.Promise(function (complete) {
var photoStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
var finalStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
capture.capturePhotoToStreamAsync(encodingProperties, photoStream)
.then(function() {
return Windows.Graphics.Imaging.BitmapDecoder.createAsync(photoStream);
})
.then(function(dec) {
finalStream.size = 0; // BitmapEncoder requires the output stream to be empty
return Windows.Graphics.Imaging.BitmapEncoder.createForTranscodingAsync(finalStream, dec);
})
.then(function(enc) {
// We need to rotate the photo wrt sensor orientation
enc.bitmapTransform.rotation = orientationToRotation(sensor.getCurrentOrientation());
return enc.flushAsync();
})
.then(function() {
return tempCapturedFile.openAsync(Windows.Storage.FileAccessMode.readWrite);
})
.then(function(fileStream) {
return Windows.Storage.Streams.RandomAccessStream.copyAndCloseAsync(finalStream, fileStream);
})
.done(function() {
photoStream.close();
finalStream.close();
complete(tempCapturedFile);
}, function() {
photoStream.close();
finalStream.close();
throw new Error("An error has occured while capturing the photo.");
});
});
})
.done(function(capturedFile) {
destroyCameraPreview();
savePhoto(capturedFile, {
destinationType: destinationType,
targetHeight: targetHeight,
targetWidth: targetWidth,
encodingType: encodingType,
saveToPhotoAlbum: saveToPhotoAlbum
}, successCallback, errorCallback);
}, function(err) {
destroyCameraPreview();
errorCallback(err);
});
}
function getAspectRatios(capture) {
var videoDeviceController = capture.videoDeviceController;
var photoAspectRatios = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.photo).map(function (element) {
return (element.width / element.height).toFixed(1);
}).filter(function (element, index, array) { return (index === array.indexOf(element)); });
var videoAspectRatios = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.videoRecord).map(function (element) {
return (element.width / element.height).toFixed(1);
}).filter(function (element, index, array) { return (index === array.indexOf(element)); });
var videoPreviewAspectRatios = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.videoPreview).map(function (element) {
return (element.width / element.height).toFixed(1);
}).filter(function (element, index, array) { return (index === array.indexOf(element)); });
var allAspectRatios = [].concat(photoAspectRatios, videoAspectRatios, videoPreviewAspectRatios);
var aspectObj = allAspectRatios.reduce(function (map, item) {
if (!map[item]) {
map[item] = 0;
}
map[item]++;
return map;
}, {});
return Object.keys(aspectObj).filter(function (k) {
return aspectObj[k] === 3;
});
}
function setAspectRatio(capture, aspect) {
// Max photo resolution with desired aspect ratio
var videoDeviceController = capture.videoDeviceController;
var photoResolution = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.photo)
.filter(function (elem) {
return ((elem.width / elem.height).toFixed(1) === aspect);
})
.reduce(function (prop1, prop2) {
return (prop1.width * prop1.height) > (prop2.width * prop2.height) ? prop1 : prop2;
});
// Max video resolution with desired aspect ratio
var videoRecordResolution = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.videoRecord)
.filter(function (elem) {
return ((elem.width / elem.height).toFixed(1) === aspect);
})
.reduce(function (prop1, prop2) {
return (prop1.width * prop1.height) > (prop2.width * prop2.height) ? prop1 : prop2;
});
// Max video preview resolution with desired aspect ratio
var videoPreviewResolution = videoDeviceController.getAvailableMediaStreamProperties(CapMSType.videoPreview)
.filter(function (elem) {
return ((elem.width / elem.height).toFixed(1) === aspect);
})
.reduce(function (prop1, prop2) {
return (prop1.width * prop1.height) > (prop2.width * prop2.height) ? prop1 : prop2;
});
return videoDeviceController.setMediaStreamPropertiesAsync(CapMSType.photo, photoResolution)
.then(function () {
return videoDeviceController.setMediaStreamPropertiesAsync(CapMSType.videoPreview, videoPreviewResolution);
})
.then(function () {
return videoDeviceController.setMediaStreamPropertiesAsync(CapMSType.videoRecord, videoRecordResolution);
});
}
/**
* When Capture button is clicked, try to capture a picture and return
*/
function onCameraCaptureButtonClick() {
// Make sure user can't click more than once
if (this.getAttribute('clicked') === '1') {
return false;
} else {
this.setAttribute('clicked', '1');
}
captureAction();
}
/**
* When Cancel button is clicked, destroy camera preview and return with error callback
*/
function onCameraCancelButtonClick() {
// Make sure user can't click more than once
if (this.getAttribute('clicked') === '1') {
return false;
} else {
this.setAttribute('clicked', '1');
}
destroyCameraPreview();
errorCallback('no image selected');
}
/**
* When the phone orientation change, get the event and change camera preview rotation
* @param {Object} e - SimpleOrientationSensorOrientationChangedEventArgs
*/
function onOrientationChange(e) {
setPreviewRotation(e.orientation);
}
/**
* Converts SimpleOrientation to a VideoRotation to remove difference between camera sensor orientation
* and video orientation
* @param {number} orientation - Windows.Devices.Sensors.SimpleOrientation
* @return {number} - Windows.Media.Capture.VideoRotation
*/
function orientationToRotation(orientation) {
// VideoRotation enumerable and BitmapRotation enumerable have the same values
// https://msdn.microsoft.com/en-us/library/windows/apps/windows.media.capture.videorotation.aspx
// https://msdn.microsoft.com/en-us/library/windows/apps/windows.graphics.imaging.bitmaprotation.aspx
switch (orientation) {
// portrait
case Windows.Devices.Sensors.SimpleOrientation.notRotated:
return Windows.Media.Capture.VideoRotation.clockwise90Degrees;
// landscape
case Windows.Devices.Sensors.SimpleOrientation.rotated90DegreesCounterclockwise:
return Windows.Media.Capture.VideoRotation.none;
// portrait-flipped (not supported by WinPhone Apps)
case Windows.Devices.Sensors.SimpleOrientation.rotated180DegreesCounterclockwise:
// Falling back to portrait default
return Windows.Media.Capture.VideoRotation.clockwise90Degrees;
// landscape-flipped
case Windows.Devices.Sensors.SimpleOrientation.rotated270DegreesCounterclockwise:
return Windows.Media.Capture.VideoRotation.clockwise180Degrees;
// faceup & facedown
default:
// Falling back to portrait default
return Windows.Media.Capture.VideoRotation.clockwise90Degrees;
}
}
/**
* Rotates the current MediaCapture's video
* @param {number} orientation - Windows.Devices.Sensors.SimpleOrientation
*/
function setPreviewRotation(orientation) {
capture.setPreviewRotation(orientationToRotation(orientation));
}
try {
createCameraUI();
startCameraPreview();
} catch (ex) {
errorCallback(ex);
}
}
function takePictureFromCameraWindows(successCallback, errorCallback, args) {
var destinationType = args[1],
targetWidth = args[3],
targetHeight = args[4],
encodingType = args[5],
allowCrop = !!args[7],
saveToPhotoAlbum = args[9],
WMCapture = Windows.Media.Capture,
cameraCaptureUI = new WMCapture.CameraCaptureUI();
cameraCaptureUI.photoSettings.allowCropping = allowCrop;
if (encodingType == Camera.EncodingType.PNG) {
cameraCaptureUI.photoSettings.format = WMCapture.CameraCaptureUIPhotoFormat.png;
} else {
cameraCaptureUI.photoSettings.format = WMCapture.CameraCaptureUIPhotoFormat.jpeg;
}
// decide which max pixels should be supported by targetWidth or targetHeight.
var maxRes = null;
var UIMaxRes = WMCapture.CameraCaptureUIMaxPhotoResolution;
var totalPixels = targetWidth * targetHeight;
if (targetWidth == -1 && targetHeight == -1) {
maxRes = UIMaxRes.highestAvailable;
}
// Temp fix for CB-10539
/*else if (totalPixels <= 320 * 240) {
maxRes = UIMaxRes.verySmallQvga;
}*/
else if (totalPixels <= 640 * 480) {
maxRes = UIMaxRes.smallVga;
} else if (totalPixels <= 1024 * 768) {
maxRes = UIMaxRes.mediumXga;
} else if (totalPixels <= 3 * 1000 * 1000) {
maxRes = UIMaxRes.large3M;
} else if (totalPixels <= 5 * 1000 * 1000) {
maxRes = UIMaxRes.veryLarge5M;
} else {
maxRes = UIMaxRes.highestAvailable;
}
cameraCaptureUI.photoSettings.maxResolution = maxRes;
var cameraPicture;
// define focus handler for windows phone 10.0
var savePhotoOnFocus = function () {
window.removeEventListener("focus", savePhotoOnFocus);
// call only when the app is in focus again
savePhoto(cameraPicture, {
destinationType: destinationType,
targetHeight: targetHeight,
targetWidth: targetWidth,
encodingType: encodingType,
saveToPhotoAlbum: saveToPhotoAlbum
}, successCallback, errorCallback);
};
// if windows phone 10, add and delete focus eventHandler to capture the focus back from cameraUI to app
if (navigator.appVersion.indexOf('Windows Phone 10.0') >= 0) {
window.addEventListener("focus", savePhotoOnFocus);
}
cameraCaptureUI.captureFileAsync(WMCapture.CameraCaptureUIMode.photo).done(function (picture) {
if (!picture) {
errorCallback("User didn't capture a photo.");
// Remove the focus handler if present
window.removeEventListener("focus", savePhotoOnFocus);
return;
}
cameraPicture = picture;
// If not windows 10, call savePhoto() now. If windows 10, wait for the app to be in focus again
if (navigator.appVersion.indexOf('Windows Phone 10.0') < 0) {
savePhoto(cameraPicture, {
destinationType: destinationType,
targetHeight: targetHeight,
targetWidth: targetWidth,
encodingType: encodingType,
saveToPhotoAlbum: saveToPhotoAlbum
}, successCallback, errorCallback);
}
}, function () {
errorCallback("Fail to capture a photo.");
window.removeEventListener("focus", savePhotoOnFocus);
});
}
function savePhoto(picture, options, successCallback, errorCallback) {
// success callback for capture operation
var success = function(picture) {
if (options.destinationType == Camera.DestinationType.FILE_URI || options.destinationType == Camera.DestinationType.NATIVE_URI) {
if (options.targetHeight > 0 && options.targetWidth > 0) {
resizeImage(successCallback, errorCallback, picture, options.targetWidth, options.targetHeight, options.encodingType);
} else {
picture.copyAsync(getAppData().localFolder, picture.name, OptUnique).done(function (copiedFile) {
successCallback("ms-appdata:///local/" + copiedFile.name);
},errorCallback);
}
} else {
if (options.targetHeight > 0 && options.targetWidth > 0) {
resizeImageBase64(successCallback, errorCallback, picture, options.targetWidth, options.targetHeight);
} else {
fileIO.readBufferAsync(picture).done(function(buffer) {
var strBase64 = encodeToBase64String(buffer);
picture.deleteAsync().done(function() {
successCallback(strBase64);
}, function(err) {
errorCallback(err);
});
}, errorCallback);
}
}
};
if (!options.saveToPhotoAlbum) {
success(picture);
return;
} else {
var savePicker = new Windows.Storage.Pickers.FileSavePicker();
var saveFile = function(file) {
if (file) {
// Prevent updates to the remote version of the file until we're done
Windows.Storage.CachedFileManager.deferUpdates(file);
picture.moveAndReplaceAsync(file)
.then(function() {
// Let Windows know that we're finished changing the file so
// the other app can update the remote version of the file.
return Windows.Storage.CachedFileManager.completeUpdatesAsync(file);
})
.done(function(updateStatus) {
if (updateStatus === Windows.Storage.Provider.FileUpdateStatus.complete) {
success(picture);
} else {
errorCallback("File update status is not complete.");
}
}, errorCallback);
} else {
errorCallback("Failed to select a file.");
}
};
savePicker.suggestedStartLocation = pickerLocId.picturesLibrary;
if (options.encodingType === Camera.EncodingType.PNG) {
savePicker.fileTypeChoices.insert("PNG", [".png"]);
savePicker.suggestedFileName = "photo.png";
} else {
savePicker.fileTypeChoices.insert("JPEG", [".jpg"]);
savePicker.suggestedFileName = "photo.jpg";
}
// If Windows Phone 8.1 use pickSaveFileAndContinue()
if (navigator.appVersion.indexOf('Windows Phone 8.1') >= 0) {
/*
Need to add and remove an event listener to catch activation state
Using FileSavePicker will suspend the app and it's required to catch the pickSaveFileContinuation
https://msdn.microsoft.com/en-us/library/windows/apps/xaml/dn631755.aspx
*/
var fileSaveHandler = function(eventArgs) {
if (eventArgs.kind === Windows.ApplicationModel.Activation.ActivationKind.pickSaveFileContinuation) {
var file = eventArgs.file;
saveFile(file);
webUIApp.removeEventListener("activated", fileSaveHandler);
}
};
webUIApp.addEventListener("activated", fileSaveHandler);
savePicker.pickSaveFileAndContinue();
} else {
savePicker.pickSaveFileAsync()
.done(saveFile, errorCallback);
}
}
}
require("cordova/exec/proxy").add("Camera",module.exports);

View File

@@ -0,0 +1,534 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Collections.Generic;
using Microsoft.Phone.Tasks;
using System.Runtime.Serialization;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows.Media.Imaging;
using Microsoft.Phone;
using Microsoft.Xna.Framework.Media;
using System.Diagnostics;
namespace WPCordovaClassLib.Cordova.Commands
{
public class Camera : BaseCommand
{
/// <summary>
/// Return base64 encoded string
/// </summary>
private const int DATA_URL = 0;
/// <summary>
/// Return file uri
/// </summary>
private const int FILE_URI = 1;
/// <summary>
/// Return native uri
/// </summary>
private const int NATIVE_URI = 2;
/// <summary>
/// Choose image from picture library
/// </summary>
private const int PHOTOLIBRARY = 0;
/// <summary>
/// Take picture from camera
/// </summary>
private const int CAMERA = 1;
/// <summary>
/// Choose image from picture library
/// </summary>
private const int SAVEDPHOTOALBUM = 2;
/// <summary>
/// Take a picture of type JPEG
/// </summary>
private const int JPEG = 0;
/// <summary>
/// Take a picture of type PNG
/// </summary>
private const int PNG = 1;
/// <summary>
/// Folder to store captured images
/// </summary>
private const string isoFolder = "CapturedImagesCache";
/// <summary>
/// Represents captureImage action options.
/// </summary>
[DataContract]
public class CameraOptions
{
/// <summary>
/// Source to getPicture from.
/// </summary>
[DataMember(IsRequired = false, Name = "sourceType")]
public int PictureSourceType { get; set; }
/// <summary>
/// Format of image that returned from getPicture.
/// </summary>
[DataMember(IsRequired = false, Name = "destinationType")]
public int DestinationType { get; set; }
/// <summary>
/// Quality of saved image
/// </summary>
[DataMember(IsRequired = false, Name = "quality")]
public int Quality { get; set; }
/// <summary>
/// Controls whether or not the image is also added to the device photo album.
/// </summary>
[DataMember(IsRequired = false, Name = "saveToPhotoAlbum")]
public bool SaveToPhotoAlbum { get; set; }
/// <summary>
/// Ignored
/// </summary>
[DataMember(IsRequired = false, Name = "correctOrientation")]
public bool CorrectOrientation { get; set; }
/// <summary>
/// Ignored
/// </summary>
[DataMember(IsRequired = false, Name = "allowEdit")]
public bool AllowEdit { get; set; }
/// <summary>
/// Height in pixels to scale image
/// </summary>
[DataMember(IsRequired = false, Name = "encodingType")]
public int EncodingType { get; set; }
/// <summary>
/// Height in pixels to scale image
/// </summary>
[DataMember(IsRequired = false, Name = "mediaType")]
public int MediaType { get; set; }
/// <summary>
/// Height in pixels to scale image
/// </summary>
[DataMember(IsRequired = false, Name = "targetHeight")]
public int TargetHeight { get; set; }
/// <summary>
/// Width in pixels to scale image
/// </summary>
[DataMember(IsRequired = false, Name = "targetWidth")]
public int TargetWidth { get; set; }
/// <summary>
/// Creates options object with default parameters
/// </summary>
public CameraOptions()
{
this.SetDefaultValues(new StreamingContext());
}
/// <summary>
/// Initializes default values for class fields.
/// Implemented in separate method because default constructor is not invoked during deserialization.
/// </summary>
/// <param name="context"></param>
[OnDeserializing()]
public void SetDefaultValues(StreamingContext context)
{
PictureSourceType = CAMERA;
DestinationType = FILE_URI;
Quality = 80;
TargetHeight = -1;
TargetWidth = -1;
SaveToPhotoAlbum = false;
CorrectOrientation = true;
AllowEdit = false;
MediaType = -1;
EncodingType = -1;
}
}
/// <summary>
/// Camera options
/// </summary>
CameraOptions cameraOptions;
public void takePicture(string options)
{
try
{
string[] args = JSON.JsonHelper.Deserialize<string[]>(options);
// ["quality", "destinationType", "sourceType", "targetWidth", "targetHeight", "encodingType",
// "mediaType", "allowEdit", "correctOrientation", "saveToPhotoAlbum" ]
cameraOptions = new CameraOptions();
cameraOptions.Quality = int.Parse(args[0]);
cameraOptions.DestinationType = int.Parse(args[1]);
cameraOptions.PictureSourceType = int.Parse(args[2]);
cameraOptions.TargetWidth = int.Parse(args[3]);
cameraOptions.TargetHeight = int.Parse(args[4]);
cameraOptions.EncodingType = int.Parse(args[5]);
cameraOptions.MediaType = int.Parse(args[6]);
cameraOptions.AllowEdit = bool.Parse(args[7]);
cameraOptions.CorrectOrientation = bool.Parse(args[8]);
cameraOptions.SaveToPhotoAlbum = bool.Parse(args[9]);
// a very large number will force the other value to be the bound
if (cameraOptions.TargetWidth > -1 && cameraOptions.TargetHeight == -1)
{
cameraOptions.TargetHeight = 100000;
}
else if (cameraOptions.TargetHeight > -1 && cameraOptions.TargetWidth == -1)
{
cameraOptions.TargetWidth = 100000;
}
}
catch (Exception ex)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message));
return;
}
// Api supports FILE_URI, DATA_URL, NATIVE_URI destination types.
// Treat all other destination types as an error.
switch (cameraOptions.DestinationType)
{
case Camera.FILE_URI:
case Camera.DATA_URL:
case Camera.NATIVE_URI:
break;
default:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Incorrect option: destinationType"));
return;
}
ChooserBase<PhotoResult> chooserTask = null;
if (cameraOptions.PictureSourceType == CAMERA)
{
chooserTask = new CameraCaptureTask();
}
else if ((cameraOptions.PictureSourceType == PHOTOLIBRARY) || (cameraOptions.PictureSourceType == SAVEDPHOTOALBUM))
{
chooserTask = new PhotoChooserTask();
}
// if chooserTask is still null, then PictureSourceType was invalid
if (chooserTask != null)
{
chooserTask.Completed += onTaskCompleted;
chooserTask.Show();
}
else
{
Debug.WriteLine("Unrecognized PictureSourceType :: " + cameraOptions.PictureSourceType.ToString());
DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
}
}
public void onTaskCompleted(object sender, PhotoResult e)
{
var task = sender as ChooserBase<PhotoResult>;
if (task != null)
{
task.Completed -= onTaskCompleted;
}
if (e.Error != null)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR));
return;
}
switch (e.TaskResult)
{
case TaskResult.OK:
try
{
string imagePathOrContent = string.Empty;
// Save image back to media library
// only save to photoalbum if it didn't come from there ...
if (cameraOptions.PictureSourceType == CAMERA && cameraOptions.SaveToPhotoAlbum)
{
MediaLibrary library = new MediaLibrary();
Picture pict = library.SavePicture(e.OriginalFileName, e.ChosenPhoto); // to save to photo-roll ...
}
int newAngle = 0;
// There's bug in Windows Phone 8.1 causing Seek on a DssPhotoStream not working properly.
// https://connect.microsoft.com/VisualStudio/feedback/details/783252
// But a mis-oriented file is better than nothing, so try and catch.
try {
int orient = ImageExifHelper.getImageOrientationFromStream(e.ChosenPhoto);
switch (orient) {
case ImageExifOrientation.LandscapeLeft:
newAngle = 90;
break;
case ImageExifOrientation.PortraitUpsideDown:
newAngle = 180;
break;
case ImageExifOrientation.LandscapeRight:
newAngle = 270;
break;
case ImageExifOrientation.Portrait:
default: break; // 0 default already set
}
} catch {
Debug.WriteLine("Error fetching orientation from Exif");
}
if (newAngle != 0)
{
using (Stream rotImageStream = ImageExifHelper.RotateStream(e.ChosenPhoto, newAngle))
{
// we should reset stream position after saving stream to media library
rotImageStream.Seek(0, SeekOrigin.Begin);
if (cameraOptions.DestinationType == DATA_URL)
{
imagePathOrContent = GetImageContent(rotImageStream);
}
else // FILE_URL or NATIVE_URI (both use the same resultant uri format)
{
imagePathOrContent = SaveImageToLocalStorage(rotImageStream, Path.GetFileName(e.OriginalFileName));
}
}
}
else // no need to reorient
{
if (cameraOptions.DestinationType == DATA_URL)
{
imagePathOrContent = GetImageContent(e.ChosenPhoto);
}
else // FILE_URL or NATIVE_URI (both use the same resultant uri format)
{
imagePathOrContent = SaveImageToLocalStorage(e.ChosenPhoto, Path.GetFileName(e.OriginalFileName));
}
}
DispatchCommandResult(new PluginResult(PluginResult.Status.OK, imagePathOrContent));
}
catch (Exception)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error retrieving image."));
}
break;
case TaskResult.Cancel:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection cancelled."));
break;
default:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection did not complete!"));
break;
}
}
/// <summary>
/// Returns image content in a form of base64 string
/// </summary>
/// <param name="stream">Image stream</param>
/// <returns>Base64 representation of the image</returns>
private string GetImageContent(Stream stream)
{
byte[] imageContent = null;
try
{
// Resize photo and convert to JPEG
imageContent = ResizePhoto(stream);
}
finally
{
stream.Dispose();
}
return Convert.ToBase64String(imageContent);
}
/// <summary>
/// Resize image
/// </summary>
/// <param name="stream">Image stream</param>
/// <returns>resized image</returns>
private byte[] ResizePhoto(Stream stream)
{
//output
byte[] resizedFile;
BitmapImage objBitmap = new BitmapImage();
objBitmap.SetSource(stream);
objBitmap.CreateOptions = BitmapCreateOptions.None;
WriteableBitmap objWB = new WriteableBitmap(objBitmap);
objBitmap.UriSource = null;
// Calculate resultant image size
int width, height;
if (cameraOptions.TargetWidth >= 0 && cameraOptions.TargetHeight >= 0)
{
// Keep proportionally
double ratio = Math.Min(
(double)cameraOptions.TargetWidth / objWB.PixelWidth,
(double)cameraOptions.TargetHeight / objWB.PixelHeight);
width = Convert.ToInt32(ratio * objWB.PixelWidth);
height = Convert.ToInt32(ratio * objWB.PixelHeight);
}
else
{
width = objWB.PixelWidth;
height = objWB.PixelHeight;
}
//Hold the result stream
using (MemoryStream objBitmapStreamResized = new MemoryStream())
{
try
{
// resize the photo with user defined TargetWidth & TargetHeight
Extensions.SaveJpeg(objWB, objBitmapStreamResized, width, height, 0, cameraOptions.Quality);
}
finally
{
//Dispose bitmaps immediately, they are memory expensive
DisposeImage(objBitmap);
DisposeImage(objWB);
GC.Collect();
}
//Convert the resized stream to a byte array.
int streamLength = (int)objBitmapStreamResized.Length;
resizedFile = new Byte[streamLength]; //-1
objBitmapStreamResized.Position = 0;
//for some reason we have to set Position to zero, but we don't have to earlier when we get the bytes from the chosen photo...
objBitmapStreamResized.Read(resizedFile, 0, streamLength);
}
return resizedFile;
}
/// <summary>
/// Util: Dispose a bitmap resource
/// </summary>
/// <param name="image">BitmapSource subclass to dispose</param>
private void DisposeImage(BitmapSource image)
{
if (image != null)
{
try
{
using (var ms = new MemoryStream(new byte[] { 0x0 }))
{
image.SetSource(ms);
}
}
catch (Exception)
{
}
}
}
/// <summary>
/// Saves captured image in isolated storage
/// </summary>
/// <param name="imageFileName">image file name</param>
/// <returns>Image path</returns>
private string SaveImageToLocalStorage(Stream stream, string imageFileName)
{
if (stream == null)
{
throw new ArgumentNullException("imageBytes");
}
try
{
var isoFile = IsolatedStorageFile.GetUserStoreForApplication();
if (!isoFile.DirectoryExists(isoFolder))
{
isoFile.CreateDirectory(isoFolder);
}
string filePath = System.IO.Path.Combine("///" + isoFolder + "/", imageFileName);
using (IsolatedStorageFileStream outputStream = isoFile.CreateFile(filePath))
{
BitmapImage objBitmap = new BitmapImage();
objBitmap.SetSource(stream);
objBitmap.CreateOptions = BitmapCreateOptions.None;
WriteableBitmap objWB = new WriteableBitmap(objBitmap);
objBitmap.UriSource = null;
try
{
//use photo's actual width & height if user doesn't provide width & height
if (cameraOptions.TargetWidth < 0 && cameraOptions.TargetHeight < 0)
{
objWB.SaveJpeg(outputStream, objWB.PixelWidth, objWB.PixelHeight, 0, cameraOptions.Quality);
}
else
{
//Resize
//Keep proportionally
double ratio = Math.Min((double)cameraOptions.TargetWidth / objWB.PixelWidth, (double)cameraOptions.TargetHeight / objWB.PixelHeight);
int width = Convert.ToInt32(ratio * objWB.PixelWidth);
int height = Convert.ToInt32(ratio * objWB.PixelHeight);
// resize the photo with user defined TargetWidth & TargetHeight
objWB.SaveJpeg(outputStream, width, height, 0, cameraOptions.Quality);
}
}
finally
{
//Dispose bitmaps immediately, they are memory expensive
DisposeImage(objBitmap);
DisposeImage(objWB);
GC.Collect();
}
}
return new Uri(filePath, UriKind.Relative).ToString();
}
catch (Exception)
{
//TODO: log or do something else
throw;
}
finally
{
stream.Dispose();
}
}
}
}