mirror of
https://github.com/arnaucube/openEventsPlatformApp.git
synced 2026-02-08 12:16:39 +01:00
implemented upload file image for new event
This commit is contained in:
1399
plugins/cordova-plugin-camera/src/android/CameraLauncher.java
Normal file
1399
plugins/cordova-plugin-camera/src/android/CameraLauncher.java
Normal file
File diff suppressed because it is too large
Load Diff
104
plugins/cordova-plugin-camera/src/android/CordovaUri.java
Normal file
104
plugins/cordova-plugin-camera/src/android/CordovaUri.java
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
185
plugins/cordova-plugin-camera/src/android/ExifHelper.java
Normal file
185
plugins/cordova-plugin-camera/src/android/ExifHelper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
319
plugins/cordova-plugin-camera/src/android/FileHelper.java
Normal file
319
plugins/cordova-plugin-camera/src/android/FileHelper.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
227
plugins/cordova-plugin-camera/src/blackberry10/index.js
vendored
Normal file
227
plugins/cordova-plugin-camera/src/blackberry10/index.js
vendored
Normal 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);
|
||||
}
|
||||
};
|
||||
123
plugins/cordova-plugin-camera/src/browser/CameraProxy.js
vendored
Normal file
123
plugins/cordova-plugin-camera/src/browser/CameraProxy.js
vendored
Normal 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);
|
||||
53
plugins/cordova-plugin-camera/src/firefoxos/CameraProxy.js
vendored
Normal file
53
plugins/cordova-plugin-camera/src/firefoxos/CameraProxy.js
vendored
Normal 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);
|
||||
116
plugins/cordova-plugin-camera/src/ios/CDVCamera.h
Normal file
116
plugins/cordova-plugin-camera/src/ios/CDVCamera.h
Normal 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
|
||||
771
plugins/cordova-plugin-camera/src/ios/CDVCamera.m
Normal file
771
plugins/cordova-plugin-camera/src/ios/CDVCamera.m
Normal 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
|
||||
43
plugins/cordova-plugin-camera/src/ios/CDVExif.h
Normal file
43
plugins/cordova-plugin-camera/src/ios/CDVExif.h
Normal 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
|
||||
62
plugins/cordova-plugin-camera/src/ios/CDVJpegHeaderWriter.h
Normal file
62
plugins/cordova-plugin-camera/src/ios/CDVJpegHeaderWriter.h
Normal 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
|
||||
547
plugins/cordova-plugin-camera/src/ios/CDVJpegHeaderWriter.m
Normal file
547
plugins/cordova-plugin-camera/src/ios/CDVJpegHeaderWriter.m
Normal 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:¤tDataOffset];
|
||||
|
||||
//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:¤tDataOffset];
|
||||
/*
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
118
plugins/cordova-plugin-camera/src/ubuntu/CaptureWidget.qml
Normal file
118
plugins/cordova-plugin-camera/src/ubuntu/CaptureWidget.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
plugins/cordova-plugin-camera/src/ubuntu/back.png
Normal file
BIN
plugins/cordova-plugin-camera/src/ubuntu/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
140
plugins/cordova-plugin-camera/src/ubuntu/camera.cpp
Normal file
140
plugins/cordova-plugin-camera/src/ubuntu/camera.cpp
Normal 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;
|
||||
}
|
||||
86
plugins/cordova-plugin-camera/src/ubuntu/camera.h
Normal file
86
plugins/cordova-plugin-camera/src/ubuntu/camera.h
Normal 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
|
||||
BIN
plugins/cordova-plugin-camera/src/ubuntu/shoot.png
Normal file
BIN
plugins/cordova-plugin-camera/src/ubuntu/shoot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-left.png
Normal file
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-middle.png
Normal file
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-middle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-right.png
Normal file
BIN
plugins/cordova-plugin-camera/src/ubuntu/toolbar-right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
882
plugins/cordova-plugin-camera/src/windows/CameraProxy.js
vendored
Normal file
882
plugins/cordova-plugin-camera/src/windows/CameraProxy.js
vendored
Normal 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);
|
||||
534
plugins/cordova-plugin-camera/src/wp/Camera.cs
Normal file
534
plugins/cordova-plugin-camera/src/wp/Camera.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user