Skip to content

Commit

Permalink
Merge pull request #14 from spoonconsulting/replace-android-compressor
Browse files Browse the repository at this point in the history
bitmap compression
  • Loading branch information
dinitri authored Apr 30, 2024
2 parents 821b14a + 17132bc commit 20804f8
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 137 deletions.
22 changes: 8 additions & 14 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@

<!-- android -->
<platform name="android">
<framework src="com.facebook.spectrum:spectrum-default:1.3.0" />
<framework src="com.facebook.spectrum:spectrum-core:1.3.0" />
<framework src="com.facebook.spectrum:spectrum-jpeg:1.3.0" />
<framework src="com.facebook.spectrum:spectrum-png:1.3.0" />
<config-file target="res/xml/config.xml" parent="/*">
<feature name="SpectrumManager">
<param name="android-package" value="com.spoon.spectrum.SpectrumManager" />
</feature>
<feature name="ImageSize">
<param name="android-package" value="com.spoon.spectrum.utils.ImageSize" />
</feature>
</config-file>
<source-file src="src/android/SpectrumManager.java" target-dir="src/com/spoon/spectrum" />
<source-file src="src/android/SpoonCameraExif.java" target-dir="src/com/spoon/spectrum" />
<source-file src="src/android/utils/ImageSize.java" target-dir="src/com/spoon/spectrum/utils" />
<source-file src="src/android/utils/DoNotStrip.java" target-dir="src/com/spoon/spectrum/utils" />
<source-file src="src/android/utils/Preconditions.java" target-dir="src/com/spoon/spectrum/utils" />
<framework src="com.google.code.findbugs:jsr305:3.0.2"></framework>
</platform>

<!-- ios -->
Expand All @@ -36,16 +40,6 @@
<param name="ios-package" value="SpectrumManager" />
</feature>
</config-file>
<podspec>
<config>
<source url="https://cdn.cocoapods.org/"/>
</config>
<pods>
<pod name="SpectrumKit/Base" spec="~> 1.2.0" />
<pod name="SpectrumKit/Plugins/Jpeg" spec="~> 1.2.0" />
<pod name="SpectrumKit/Plugins/Png" spec="~> 1.2.0" />
</pods>
</podspec>
<header-file src="src/ios/SpectrumManager.h" />
<source-file src="src/ios/SpectrumManager.m" />
</platform>
Expand Down
138 changes: 80 additions & 58 deletions src/android/SpectrumManager.java
Original file line number Diff line number Diff line change
@@ -1,44 +1,25 @@
package com.spoon.spectrum;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.MimeTypeMap;


import com.facebook.spectrum.DefaultPlugins;
import com.facebook.spectrum.EncodedImageSink;
import com.facebook.spectrum.EncodedImageSource;
import com.facebook.spectrum.Spectrum;
import com.facebook.spectrum.SpectrumException;
import com.facebook.spectrum.SpectrumResult;
import com.facebook.spectrum.SpectrumSoLoader;
import com.facebook.spectrum.image.ImageSize;
import com.facebook.spectrum.logging.SpectrumLogcatLogger;
import com.facebook.spectrum.options.TranscodeOptions;
import com.facebook.spectrum.requirements.EncodeRequirement;
import com.facebook.spectrum.requirements.ResizeRequirement;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.UUID;

import static com.facebook.spectrum.image.EncodedImageFormat.JPEG;
import androidx.exifinterface.media.ExifInterface;
import com.spoon.spectrum.utils.ImageSize;

public class SpectrumManager extends CordovaPlugin {

private static Spectrum mSpectrum;

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
cordova.getThreadPool().execute(new Runnable() {
Expand All @@ -62,57 +43,98 @@ private void sendErrorResultForException(CallbackContext callbackContext, Except
}

private void transcodeImage(String path, int size, CallbackContext callbackContext) {
if (mSpectrum == null) {
SpectrumSoLoader.init(cordova.getActivity());
mSpectrum = Spectrum.make(new SpectrumLogcatLogger(Log.INFO), DefaultPlugins.get());
}
Uri tmpSrc = Uri.parse(path);
final Uri sourceUri = tmpSrc.getScheme() != null ? webView.getResourceApi().remapUri(tmpSrc) : tmpSrc;
final String sourcePath = sourceUri.toString();
File file = new File(sourcePath);
if (!file.exists()) {
callbackContext.error("source file does not exists");
callbackContext.error("source file does not exist");
return;
}
InputStream inputStream;

Bitmap bitmap;
try {
inputStream = new FileInputStream(sourcePath);
} catch (FileNotFoundException e) {
sendErrorResultForException(callbackContext, e);
bitmap = BitmapFactory.decodeFile(sourcePath);
if (bitmap == null) {
callbackContext.error("Could not decode the image");
return;
}
} catch (Exception e) {
callbackContext.error("Failed to load image: " + e.getMessage());
return;
}
final TranscodeOptions transcodeOptions;

// Resize the bitmap if necessary
ImageSize targetSize = getImageSize(path, size);
transcodeOptions = TranscodeOptions.Builder(new EncodeRequirement(JPEG, 80)).resize(ResizeRequirement.Mode.EXACT_OR_SMALLER, targetSize).build();
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString());
String destinationFileName = UUID.randomUUID().toString() + "_compressed." + fileExtension;
if (bitmap.getWidth() != targetSize.width || bitmap.getHeight() != targetSize.height) {
bitmap = Bitmap.createScaledBitmap(bitmap, targetSize.width, targetSize.height, true);
}

String destinationFileName = UUID.randomUUID().toString() + "_compressed.jpg";
String destinationPath = sourcePath.replace(file.getName(), destinationFileName);
SpectrumResult result;
try {
result = mSpectrum.transcode(
EncodedImageSource.from(inputStream),
EncodedImageSink.from(destinationPath),
transcodeOptions,
"com.spectrum-plugin");
} catch (SpectrumException | FileNotFoundException e) {
sendErrorResultForException(callbackContext, e);
File outputFile = new File(destinationPath);

try (FileOutputStream out = new FileOutputStream(outputFile)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)) {
callbackContext.error("Failed to compress image");
return;
}
}
} catch (Exception e) {
callbackContext.error("Failed to save compressed image: " + e.getMessage());
return;
} finally {
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
}
if (result.isSuccessful()) {
if (!file.delete()) {
callbackContext.error("could not delete source image");
return;

// Initialize ExifInterface for the original and compressed image
ExifInterface originalExif = null;
try {
originalExif = new ExifInterface(sourcePath);
} catch (IOException e) {
Log.d("Can't extract origExifs", e.toString());
}

// Iterate over all EXIF tags in the original file
if (originalExif != null) {
ExifInterface compressedExif = null;
try {
compressedExif = new ExifInterface(destinationPath);
} catch (IOException e) {
Log.d("Can't extract compExifs", e.toString());
}
if (!new File(destinationPath).renameTo(file)) {
callbackContext.error("could not rename image");
return;

for (String attribute : SpoonCameraExif.COMMON_TAGS) {
String value = originalExif.getAttribute(attribute);
if (value != null) {
compressedExif.setAttribute(attribute, value);
}
}
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
if (compressedExif != null) {
try {
compressedExif.saveAttributes();
} catch (IOException e) {
Log.d("Error saving exifs ", e.toString());
}
}
}

// Replace the original file with the compressed one
if (!file.delete()) {
callbackContext.error("could not delete source image");
return;
}
callbackContext.error("could not compress image");
if (!outputFile.renameTo(file)) {
callbackContext.error("could not rename image");
return;
}

PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
}

private ImageSize getImageSize(String sourcePath, int defaultSize) {
Expand Down
72 changes: 72 additions & 0 deletions src/android/SpoonCameraExif.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.spoon.spectrum;

import androidx.camera.core.impl.utils.Exif;
import androidx.exifinterface.media.ExifInterface;

public class SpoonCameraExif {
public static final String[] COMMON_TAGS = {
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_SOFTWARE,
ExifInterface.TAG_Y_CB_CR_POSITIONING,
ExifInterface.TAG_X_RESOLUTION,
ExifInterface.TAG_Y_RESOLUTION,
ExifInterface.TAG_RESOLUTION_UNIT,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_EXPOSURE_PROGRAM,
ExifInterface.TAG_RW2_ISO,
ExifInterface.TAG_EXIF_VERSION,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_OFFSET_TIME,
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
ExifInterface.TAG_BRIGHTNESS_VALUE,
ExifInterface.TAG_MAX_APERTURE_VALUE,
ExifInterface.TAG_METERING_MODE,
ExifInterface.TAG_FLASHPIX_VERSION,
ExifInterface.TAG_COMPONENTS_CONFIGURATION,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_COLOR_SPACE,
ExifInterface.TAG_SCENE_TYPE,
ExifInterface.TAG_CUSTOM_RENDERED,
ExifInterface.TAG_EXPOSURE_MODE,
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
"FocalLengthIn35mmFormat",
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
ExifInterface.TAG_CONTRAST,
ExifInterface.TAG_SATURATION,
ExifInterface.TAG_SHARPNESS,
ExifInterface.TAG_IMAGE_UNIQUE_ID,
ExifInterface.TAG_GPS_VERSION_ID,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
"latitude",
"longitude",
"ModifyDate",
"CreateDate",
"ExposureCompensation",
"ExifImageWidth",
"ExifImageHeight"
};

}
23 changes: 23 additions & 0 deletions src/android/utils/DoNotStrip.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.spoon.spectrum.utils;
/*
* Copyright (c) 2004-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/

import static java.lang.annotation.RetentionPolicy.CLASS;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Add this annotation to a class, method, or field to instruct Proguard to not strip it out.
*
* <p>This is useful for methods called via reflection that could appear as unused to Proguard.
*/
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(CLASS)
public @interface DoNotStrip {}
63 changes: 63 additions & 0 deletions src/android/utils/ImageSize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.spoon.spectrum.utils;

import javax.annotation.concurrent.Immutable;

/** Represents a rectangular area defined by its width and height. */
@DoNotStrip
@Immutable
public class ImageSize {

/**
* Setting to 2**16 as this is the smallest common denominator for all common image libraries that
* we are interested in.
*/
@DoNotStrip static final int MAX_IMAGE_SIDE_DIMENSION = 65536;

/** The size's width. */
@DoNotStrip public final int width;

/** The size's height. */
@DoNotStrip public final int height;

/**
* Creates a new {@link ImageSize}
*
* @param width Must be within [0, {@link #MAX_IMAGE_SIDE_DIMENSION}]
* @param height Must be within [0, {@link #MAX_IMAGE_SIDE_DIMENSION}]
*/
@DoNotStrip
public ImageSize(final int width, final int height) {
Preconditions.checkArgument(width >= 0 && width <= MAX_IMAGE_SIDE_DIMENSION);
Preconditions.checkArgument(height >= 0 && height <= MAX_IMAGE_SIDE_DIMENSION);
this.width = width;
this.height = height;
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

final ImageSize that = (ImageSize) o;
return width == that.width && height == that.height;
}

@Override
public int hashCode() {
int result = width;
result = 31 * result + height;
return result;
}

@Override
public String toString() {
return "ImageSize{" + "width=" + width + ", height=" + height + '}';
}
}
Loading

0 comments on commit 20804f8

Please sign in to comment.