Remove dependency on Exif parsing library. (#2947)

* Remove dependency on Exif parsing library.

* Fix test.
This commit is contained in:
Dmitry Brant 2019-10-05 07:49:48 -04:00 committed by Josephine Lim
parent 6842bd80b1
commit 17a79800ae
6 changed files with 56 additions and 119 deletions

View file

@ -91,8 +91,9 @@ dependencies {
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.exifinterface:exifinterface:1.0.0" implementation "androidx.exifinterface:exifinterface:1.0.0"
//metadata extractor
implementation 'com.drewnoakes:metadata-extractor:2.11.0' //swipe_layout
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
} }
android { android {

View file

@ -1,18 +1,15 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.exifinterface.media.ExifInterface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
@ -125,16 +122,13 @@ public class UploadableFile implements Parcelable {
* @return * @return
*/ */
private DateTimeWithSource getDateTimeFromExif() { private DateTimeWithSource getDateTimeFromExif() {
Metadata metadata;
try { try {
metadata = ImageMetadataReader.readMetadata(file); ExifInterface exif = new ExifInterface(file.getAbsolutePath());
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); @SuppressLint("RestrictedApi") long dateTime = exif.getDateTime();
if (directory!=null && directory.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) { if (dateTime != -1) {
Date date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL); Date date = new Date(dateTime);
return new DateTimeWithSource(date, DateTimeWithSource.EXIF_SOURCE); return new DateTimeWithSource(date, DateTimeWithSource.EXIF_SOURCE);
} }
} catch (ImageProcessingException e) {
e.printStackTrace();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }

View file

@ -1,60 +1,36 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import com.drew.imaging.ImageMetadataReader; import androidx.exifinterface.media.ExifInterface;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import java.io.File;
import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Single; import io.reactivex.Single;
import timber.log.Timber;
/** /**
* We try to avoid copyright violations in commons app. * We try to minimize uploads from the Commons app that might be copyright violations.
* For doing that we read EXIF data using the library metadata-reader * If an image does not have any Exif metadata, then it was likely downloaded from the internet,
* If an image doesn't have any EXIF Directoris in it's metadata then the image is an * and is probably not an original work by the user. We detect these kinds of images by looking
* internet download image(and not the one taken using phone's camera) */ * for the presence of some basic Exif metadata.
*/
@Singleton @Singleton
public class EXIFReader { public class EXIFReader {
@Inject @Inject
public EXIFReader() { public EXIFReader() {
//Empty
} }
/**
* The method takes in path of the image and reads metadata using the library metadata-extractor
* And the checks for the presence of EXIF Directories in metadata object
* */
public Single<Integer> processMetadata(String path) { public Single<Integer> processMetadata(String path) {
Metadata readMetadata = null;
try { try {
readMetadata = ImageMetadataReader.readMetadata(new File(path)); ExifInterface exif = new ExifInterface(path);
} catch (ImageProcessingException e) { if (exif.getAttribute(ExifInterface.TAG_MAKE) != null
Timber.d(e.toString()); || exif.getAttribute(ExifInterface.TAG_DATETIME) != null) {
} catch (IOException e) {
Timber.d(e.toString());
}
if (readMetadata != null) {
for (Directory directory : readMetadata.getDirectories()) {
// In case of internet downloaded image these three fields are not present
if (directory.getName().equals("Exif IFD0") //Contains information about the device capturing the photo
|| directory.getName().equals("Exif SubIFD") //contains information like date, time and pixels of the image
|| directory.getName().equals("Exif Thumbnail")) //contains information about image thumbnail like compression and reolution
{
Timber.d(directory.getName() + " Contains metadata");
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
} }
} } catch (Exception e) {
return Single.just(ImageUtils.FILE_NO_EXIF);
} }
return Single.just(ImageUtils.FILE_NO_EXIF); return Single.just(ImageUtils.FILE_NO_EXIF);
} }
} }

View file

@ -1,12 +1,9 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -23,10 +20,7 @@ import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
/** /**
* Methods for pre-processing images to be uploaded * Methods for pre-processing images to be uploaded
*//*if (dataInBytes[0] == 70 && dataInBytes[1] == 66 && dataInBytes[2] == 77 && dataInBytes[3] == 68) { */
Timber.d("Contains FBMD");
return Single.just(ImageUtils.FILE_FBMD);
}*/
@Singleton @Singleton
public class ImageProcessingService { public class ImageProcessingService {
private final FileUtilsWrapper fileUtilsWrapper; private final FileUtilsWrapper fileUtilsWrapper;
@ -64,12 +58,11 @@ public class ImageProcessingService {
} }
Timber.d("Checking the validity of image"); Timber.d("Checking the validity of image");
String filePath = uploadItem.getMediaUri().getPath(); String filePath = uploadItem.getMediaUri().getPath();
Uri contentUri=uploadItem.getContentUri();
Single<Integer> duplicateImage = checkDuplicateImage(filePath); Single<Integer> duplicateImage = checkDuplicateImage(filePath);
Single<Integer> wrongGeoLocation = checkImageGeoLocation(uploadItem.getPlace(), filePath); Single<Integer> wrongGeoLocation = checkImageGeoLocation(uploadItem.getPlace(), filePath);
Single<Integer> darkImage = checkDarkImage(filePath); Single<Integer> darkImage = checkDarkImage(filePath);
Single<Integer> itemTitle = checkTitle ? validateItemTitle(uploadItem) : Single.just(ImageUtils.IMAGE_OK); Single<Integer> itemTitle = checkTitle ? validateItemTitle(uploadItem) : Single.just(ImageUtils.IMAGE_OK);
Single<Integer> checkFBMD = checkFBMD(context,contentUri); Single<Integer> checkFBMD = checkFBMD(filePath);
Single<Integer> checkEXIF = checkEXIF(filePath); Single<Integer> checkEXIF = checkEXIF(filePath);
Single<Integer> zipResult = Single.zip(duplicateImage, wrongGeoLocation, darkImage, itemTitle, Single<Integer> zipResult = Single.zip(duplicateImage, wrongGeoLocation, darkImage, itemTitle,
@ -84,31 +77,21 @@ public class ImageProcessingService {
} }
/** /**
* Other than the Image quality we need to check that using this Image doesn't violate's facebook's copyright's. * We want to discourage users from uploading images to Commons that were taken from Facebook.
* Whenever a user tries to upload an image that was downloaded from Facebook then we warn the user with a message to stop the upload * This attempts to detect whether an image was downloaded from Facebook by heuristically
* To know whether the Image is downloaded from facebook: * searching for metadata that is specific to images that come from Facebook.
* -We read the metadata of any Image and check for FBMD
* -Facebook downloaded image's contains metadata of the type IPTC
* - From this IPTC metadata we extract a byte array that contains FBMD as it's initials. If the image was downloaded from facebook
* Thus we successfully protect common's from Facebook's copyright violation
*/ */
private Single<Integer> checkFBMD(String filepath) {
public Single<Integer> checkFBMD(Context context,Uri contentUri) { return readFBMD.processMetadata(filepath);
try {
return readFBMD.processMetadata(context,contentUri);
} catch (IOException e) {
return Single.just(ImageUtils.FILE_FBMD);
}
} }
/** /**
* To avoid copyright we check for EXIF data in any image. * We try to minimize uploads from the Commons app that might be copyright violations.
* Images that are downloaded from internet generally don't have any EXIF data in them * If an image does not have any Exif metadata, then it was likely downloaded from the internet,
* while images taken via camera or screenshots in phone have EXIF data with them. * and is probably not an original work by the user. We detect these kinds of images by looking
* So we check if the image has no EXIF data then we display a warning to the user * for the presence of some basic Exif metadata.
* * */ */
private Single<Integer> checkEXIF(String filepath) {
public Single<Integer> checkEXIF(String filepath){
return EXIFReader.processMetadata(filepath); return EXIFReader.processMetadata(filepath);
} }

View file

@ -1,14 +1,6 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.content.Context; import java.io.FileInputStream;
import android.net.Uri;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.drew.metadata.iptc.IptcDirectory;
import java.io.IOException; import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
@ -17,37 +9,38 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Single; import io.reactivex.Single;
/**
* We want to discourage users from uploading images to Commons that were taken from Facebook.
* This attempts to detect whether an image was downloaded from Facebook by heuristically
* searching for metadata that is specific to images that come from Facebook.
*/
@Singleton @Singleton
public class ReadFBMD { public class ReadFBMD {
@Inject @Inject
public ReadFBMD() { public ReadFBMD() {
} }
public Single<Integer> processMetadata(Context context, Uri contentUri) throws IOException { public Single<Integer> processMetadata(String path) {
Metadata readMetadata = null;
try { try {
readMetadata = ImageMetadataReader.readMetadata(context.getContentResolver().openInputStream(contentUri)); int psBlockOffset;
} catch (ImageProcessingException e) { int fbmdOffset;
e.printStackTrace();
} catch (IOException e) { try (FileInputStream fs = new FileInputStream(path)) {
e.printStackTrace(); byte[] bytes = new byte[4096];
fs.read(bytes);
fs.close();
String fileStr = new String(bytes);
psBlockOffset = fileStr.indexOf("8BIM");
fbmdOffset = fileStr.indexOf("FBMD");
} }
IptcDirectory iptcDirectory = readMetadata != null ? readMetadata.getFirstDirectoryOfType(IptcDirectory.class) : null; if (psBlockOffset > 0 && fbmdOffset > 0
if (iptcDirectory == null) { && fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
return Single.just(ImageUtils.IMAGE_OK);
}
/**
* We parse through all the tags in the IPTC directory if the tagname equals "Special Instructions".
* And the description string starts with FBMD.
* Then the source of image is facebook
* */
for (Tag tag : iptcDirectory.getTags()) {
if (tag.getTagName().equals("Special Instructions") && tag.getDescription().substring(0, 4).equals("FBMD")) {
return Single.just(ImageUtils.FILE_FBMD); return Single.just(ImageUtils.FILE_FBMD);
} }
} catch (IOException e) {
e.printStackTrace();
} }
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
} }

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Cblair91
-->
<resources>
<string name="crash_dialog_title">Commons has crashed</string>
<string name="crash_dialog_text">Oops. Something went wrong!</string>
<string name="crash_dialog_comment_prompt">Tell us what you were doing, then share it via email to us. Will help us fix it!</string>
<string name="crash_dialog_ok_toast">Thank you!</string>
</resources>