diff --git a/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java b/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java new file mode 100644 index 000000000..70e568956 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java @@ -0,0 +1,60 @@ +package fr.free.nrw.commons.upload; + +import com.drew.imaging.ImageMetadataReader; +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.Singleton; + +import fr.free.nrw.commons.utils.ImageUtils; +import io.reactivex.Single; +import timber.log.Timber; + +/** +* We try to avoid copyright violations in commons app. +* For doing that we read EXIF data using the library metadata-reader +* If an image doesn't have any EXIF Directoris in it's metadata then the image is an +* internet download image(and not the one taken using phone's camera) */ + +@Singleton +public class EXIFReader { + @Inject + 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 processMetadata(String path) { + Metadata readMetadata = null; + try { + readMetadata = ImageMetadataReader.readMetadata(new File(path)); + } catch (ImageProcessingException e) { + Timber.d(e.toString()); + } 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.FILE_NO_EXIF); + } + +} + diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java index 7ad7b150d..b1086eac9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java @@ -34,17 +34,19 @@ public class ImageProcessingService { private final ImageUtilsWrapper imageUtilsWrapper; private final MediaWikiApi mwApi; private final ReadFBMD readFBMD; + private final EXIFReader EXIFReader; @Inject public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper, BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper, ImageUtilsWrapper imageUtilsWrapper, - MediaWikiApi mwApi, ReadFBMD readFBMD) { + MediaWikiApi mwApi, ReadFBMD readFBMD, EXIFReader EXIFReader) { this.fileUtilsWrapper = fileUtilsWrapper; this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper; this.imageUtilsWrapper = imageUtilsWrapper; this.mwApi = mwApi; this.readFBMD = readFBMD; + this.EXIFReader = EXIFReader; } /** @@ -69,17 +71,16 @@ public class ImageProcessingService { Single darkImage = checkDarkImage(filePath); Single itemTitle = checkTitle ? validateItemTitle(uploadItem) : Single.just(ImageUtils.IMAGE_OK); Single checkFBMD = checkFBMD(context,contentUri); - + Single checkEXIF = checkEXIF(filePath); Single zipResult = Single.zip(duplicateImage, wrongGeoLocation, darkImage, itemTitle, (duplicate, wrongGeo, dark, title) -> { Timber.d("Result for duplicate: %d, geo: %d, dark: %d, title: %d", duplicate, wrongGeo, dark, title); return duplicate | wrongGeo | dark | title; }); - - return Single.zip(zipResult, checkFBMD, (zip, fbmd) -> { - Timber.d("zip:" + zip + "fbmd:" + fbmd); - return zip | fbmd; - }); + return Single.zip(zipResult, checkFBMD , checkEXIF , (zip , fbmd , exif)->{ + Timber.d("zip:" + zip + "fbmd:" + fbmd + "exif:" + exif); + return zip | fbmd | exif; + }); } /** @@ -100,6 +101,17 @@ public class ImageProcessingService { } } + /** + * To avoid copyright we check for EXIF data in any image. + * Images that are downloaded from internet generally don't have any EXIF data in them + * while images taken via camera or screenshots in phone have EXIF data with them. + * So we check if the image has no EXIF data then we display a warning to the user + * * */ + + public Single checkEXIF(String filepath){ + return EXIFReader.processMetadata(filepath); + } + /** * Checks item title diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 7db525eab..011fc17e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -55,7 +55,11 @@ public class ImageUtils { * ie. 10000 */ public static final int FILE_FBMD = 1 << 4; - + /** + * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does not contains EXIF data else returns IMAGE_OK + * ie. 100000 + */ + public static final int FILE_NO_EXIF = 1 << 5; public static final int IMAGE_OK = 0; public static final int IMAGE_KEEP = -1; public static final int IMAGE_WAIT = -2; diff --git a/app/src/main/res/layout/activity_notification.xml b/app/src/main/res/layout/activity_notification.xml index a5a759629..f2556be08 100644 --- a/app/src/main/res/layout/activity_notification.xml +++ b/app/src/main/res/layout/activity_notification.xml @@ -32,7 +32,7 @@ + android:layout_height="match_parent"/> diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt index 3045242e2..c9c961f3e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt @@ -30,6 +30,8 @@ class u { internal var mwApi: MediaWikiApi? = null @Mock internal var readFBMD: ReadFBMD?=null + @Mock + internal var readEXIF: EXIFReader?=null @InjectMocks var imageProcessingService: ImageProcessingService? = null @@ -84,6 +86,8 @@ class u { .thenReturn(false) `when`(readFBMD?.processMetadata(ArgumentMatchers.any(),ArgumentMatchers.any())) .thenReturn(Single.just(ImageUtils.IMAGE_OK)) + `when`(readEXIF?.processMetadata(ArgumentMatchers.anyString())) + .thenReturn(Single.just(ImageUtils.IMAGE_OK)) } @Test