From 85c6cda3d8cf14fd1442c04cc515a1032a50c81f Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 10 Dec 2024 21:21:10 -0600 Subject: [PATCH] Convert ImageProcessingService to kotlin --- .../upload/ImageProcessingService.java | 190 ------------------ .../commons/upload/ImageProcessingService.kt | 183 +++++++++++++++++ 2 files changed, 183 insertions(+), 190 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt 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 deleted file mode 100644 index 8065fde56..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java +++ /dev/null @@ -1,190 +0,0 @@ -package fr.free.nrw.commons.upload; - -import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION; -import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_DUPLICATE; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; - -import android.content.Context; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.media.MediaClient; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.ImageUtilsWrapper; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -/** - * Methods for pre-processing images to be uploaded - */ -@Singleton -public class ImageProcessingService { - - private final FileUtilsWrapper fileUtilsWrapper; - private final ImageUtilsWrapper imageUtilsWrapper; - private final ReadFBMD readFBMD; - private final EXIFReader EXIFReader; - private final MediaClient mediaClient; - - @Inject - public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper, - ImageUtilsWrapper imageUtilsWrapper, - ReadFBMD readFBMD, EXIFReader EXIFReader, - MediaClient mediaClient, Context context) { - this.fileUtilsWrapper = fileUtilsWrapper; - this.imageUtilsWrapper = imageUtilsWrapper; - this.readFBMD = readFBMD; - this.EXIFReader = EXIFReader; - this.mediaClient = mediaClient; - } - - - /** - * Check image quality before upload - checks duplicate image - checks dark image - checks - * geolocation for image - * - * @param uploadItem UploadItem whose quality is to be checked - * @param inAppPictureLocation In app picture location (if any) - * @return Quality of UploadItem - */ - Single validateImage(UploadItem uploadItem, LatLng inAppPictureLocation) { - int currentImageQuality = uploadItem.getImageQuality(); - Timber.d("Current image quality is %d", currentImageQuality); - if (currentImageQuality == IMAGE_KEEP || currentImageQuality == IMAGE_OK) { - return Single.just(IMAGE_OK); - } - Timber.d("Checking the validity of image"); - String filePath = uploadItem.getMediaUri().getPath(); - - return Single.zip( - checkDuplicateImage(filePath), - checkImageGeoLocation(uploadItem.getPlace(), filePath, inAppPictureLocation), - checkDarkImage(filePath), - checkFBMD(filePath), - checkEXIF(filePath), - (duplicateImage, wrongGeoLocation, darkImage, fbmd, exif) -> { - Timber.d("duplicate: %d, geo: %d, dark: %d" + "fbmd:" + fbmd + "exif:" - + exif, - duplicateImage, wrongGeoLocation, darkImage); - return duplicateImage | wrongGeoLocation | darkImage | fbmd | exif; - } - ); - } - - /** - * Checks caption of the given UploadItem - * - * @param uploadItem UploadItem whose caption is to be verified - * @return Quality of caption of the UploadItem - */ - Single validateCaption(UploadItem uploadItem) { - int currentImageQuality = uploadItem.getImageQuality(); - Timber.d("Current image quality is %d", currentImageQuality); - if (currentImageQuality == IMAGE_KEEP) { - return Single.just(IMAGE_OK); - } - Timber.d("Checking the validity of caption"); - - return validateItemTitle(uploadItem); - } - - /** - * 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. - */ - private Single checkFBMD(String filepath) { - return readFBMD.processMetadata(filepath); - } - - /** - * We try to minimize uploads from the Commons app that might be copyright violations. If an - * image does not have any Exif metadata, then it was likely downloaded from the internet, and - * is probably not an original work by the user. We detect these kinds of images by looking for - * the presence of some basic Exif metadata. - */ - private Single checkEXIF(String filepath) { - return EXIFReader.processMetadata(filepath); - } - - - /** - * Checks item caption - empty caption - existing caption - * - * @param uploadItem - * @return - */ - private Single validateItemTitle(UploadItem uploadItem) { - Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails()); - List captions = uploadItem.getUploadMediaDetails(); - if (captions.isEmpty()) { - return Single.just(EMPTY_CAPTION); - } - - return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName()) - .map(doesFileExist -> { - Timber.d("Result for valid title is %s", doesFileExist); - return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Checks for duplicate image - * - * @param filePath file to be checked - * @return IMAGE_DUPLICATE or IMAGE_OK - */ - Single checkDuplicateImage(String filePath) { - Timber.d("Checking for duplicate image %s", filePath); - return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) - .map(fileUtilsWrapper::getSHA1) - .flatMap(mediaClient::checkFileExistsUsingSha) - .map(b -> { - Timber.d("Result for duplicate image %s", b); - return b ? IMAGE_DUPLICATE : IMAGE_OK; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Checks for dark image - * - * @param filePath file to be checked - * @return IMAGE_DARK or IMAGE_OK - */ - private Single checkDarkImage(String filePath) { - Timber.d("Checking for dark image %s", filePath); - return imageUtilsWrapper.checkIfImageIsTooDark(filePath); - } - - /** - * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't - * contain a geolocation - * - * @param filePath file to be checked - * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK - */ - private Single checkImageGeoLocation(Place place, String filePath, LatLng inAppPictureLocation) { - Timber.d("Checking for image geolocation %s", filePath); - if (place == null || StringUtils.isBlank(place.getWikiDataEntityId())) { - return Single.just(IMAGE_OK); - } - return Single.fromCallable(() -> filePath) - .flatMap(path -> Single.just(fileUtilsWrapper.getGeolocationOfFile(path, inAppPictureLocation))) - .flatMap(geoLocation -> { - if (StringUtils.isBlank(geoLocation)) { - return Single.just(IMAGE_OK); - } - return imageUtilsWrapper - .checkImageGeolocationIsDifferent(geoLocation, place.getLocation()); - }) - .subscribeOn(Schedulers.io()); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt new file mode 100644 index 000000000..9fbb1f1e4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt @@ -0,0 +1,183 @@ +package fr.free.nrw.commons.upload + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION +import fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_DUPLICATE +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK +import fr.free.nrw.commons.utils.ImageUtilsWrapper +import io.reactivex.Single +import io.reactivex.functions.Function +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.io.FileInputStream +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Methods for pre-processing images to be uploaded + */ +@Singleton +class ImageProcessingService @Inject constructor( + private val fileUtilsWrapper: FileUtilsWrapper, + private val imageUtilsWrapper: ImageUtilsWrapper, + private val readFBMD: ReadFBMD, + private val EXIFReader: EXIFReader, + private val mediaClient: MediaClient +) { + /** + * Check image quality before upload - checks duplicate image - checks dark image - checks + * geolocation for image + * + * @param uploadItem UploadItem whose quality is to be checked + * @param inAppPictureLocation In app picture location (if any) + * @return Quality of UploadItem + */ + fun validateImage(uploadItem: UploadItem, inAppPictureLocation: LatLng?): Single { + val currentImageQuality = uploadItem.imageQuality + Timber.d("Current image quality is %d", currentImageQuality) + if (currentImageQuality == IMAGE_KEEP || currentImageQuality == IMAGE_OK) { + return Single.just(IMAGE_OK) + } + + Timber.d("Checking the validity of image") + val filePath = uploadItem.mediaUri.path + + return Single.zip( + checkDuplicateImage(filePath), + checkImageGeoLocation(uploadItem.place, filePath, inAppPictureLocation), + checkDarkImage(filePath!!), + checkFBMD(filePath), + checkEXIF(filePath) + ) { duplicateImage: Int, wrongGeoLocation: Int, darkImage: Int, fbmd: Int, exif: Int -> + Timber.d( + "duplicate: %d, geo: %d, dark: %d, fbmd: %d, exif: %d", + duplicateImage, wrongGeoLocation, darkImage, fbmd, exif + ) + return@zip duplicateImage or wrongGeoLocation or darkImage or fbmd or exif + } + } + + /** + * Checks caption of the given UploadItem + * + * @param uploadItem UploadItem whose caption is to be verified + * @return Quality of caption of the UploadItem + */ + fun validateCaption(uploadItem: UploadItem): Single { + val currentImageQuality = uploadItem.imageQuality + Timber.d("Current image quality is %d", currentImageQuality) + if (currentImageQuality == IMAGE_KEEP) { + return Single.just(IMAGE_OK) + } + Timber.d("Checking the validity of caption") + + return validateItemTitle(uploadItem) + } + + /** + * 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. + */ + private fun checkFBMD(filepath: String?): Single = + readFBMD.processMetadata(filepath) + + /** + * We try to minimize uploads from the Commons app that might be copyright violations. If an + * image does not have any Exif metadata, then it was likely downloaded from the internet, and + * is probably not an original work by the user. We detect these kinds of images by looking for + * the presence of some basic Exif metadata. + */ + private fun checkEXIF(filepath: String): Single = + EXIFReader.processMetadata(filepath) + + + /** + * Checks item caption - empty caption - existing caption + */ + private fun validateItemTitle(uploadItem: UploadItem): Single { + Timber.d("Checking for image title %s", uploadItem.uploadMediaDetails) + val captions = uploadItem.uploadMediaDetails + if (captions.isEmpty()) { + return Single.just(EMPTY_CAPTION) + } + + return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName) + .map { doesFileExist: Boolean -> + Timber.d("Result for valid title is %s", doesFileExist) + if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK + } + .subscribeOn(Schedulers.io()) + } + + /** + * Checks for duplicate image + * + * @param filePath file to be checked + * @return IMAGE_DUPLICATE or IMAGE_OK + */ + fun checkDuplicateImage(filePath: String?): Single { + Timber.d("Checking for duplicate image %s", filePath) + return Single.fromCallable { fileUtilsWrapper.getFileInputStream(filePath) } + .map { stream: FileInputStream? -> + fileUtilsWrapper.getSHA1(stream) + } + .flatMap { fileSha: String? -> + mediaClient.checkFileExistsUsingSha(fileSha) + } + .map { + Timber.d("Result for duplicate image %s", it) + if (it) IMAGE_DUPLICATE else IMAGE_OK + } + .subscribeOn(Schedulers.io()) + } + + /** + * Checks for dark image + * + * @param filePath file to be checked + * @return IMAGE_DARK or IMAGE_OK + */ + private fun checkDarkImage(filePath: String): Single { + Timber.d("Checking for dark image %s", filePath) + return imageUtilsWrapper.checkIfImageIsTooDark(filePath) + } + + /** + * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't + * contain a geolocation + * + * @param filePath file to be checked + * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK + */ + private fun checkImageGeoLocation( + place: Place?, + filePath: String?, + inAppPictureLocation: LatLng? + ): Single { + Timber.d("Checking for image geolocation %s", filePath) + if (place == null || StringUtils.isBlank(place.wikiDataEntityId)) { + return Single.just(IMAGE_OK) + } + + return Single.fromCallable { filePath } + .flatMap { path: String? -> + Single.just( + fileUtilsWrapper.getGeolocationOfFile(path!!, inAppPictureLocation) + ) + } + .flatMap { geoLocation: String? -> + if (geoLocation.isNullOrBlank()) { + return@flatMap Single.just(IMAGE_OK) + } + imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation()) + } + .subscribeOn(Schedulers.io()) + } +} +