Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copy exif info of source image into compressed image #229

Merged
merged 1 commit into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.net.Uri
import android.util.Base64
import com.facebook.react.bridge.ReactApplicationContext
import com.reactnativecompressor.Utils.MediaCache
import com.reactnativecompressor.Utils.Utils.exifAttributes
import com.reactnativecompressor.Utils.Utils.generateCacheFilePath
import com.reactnativecompressor.Utils.Utils.slashifyFilePath
import java.io.ByteArrayOutputStream
Expand Down Expand Up @@ -55,7 +56,28 @@ object ImageCompressor {
return BitmapFactory.decodeFile(filePath)
}

fun encodeImage(imageDataByteArrayOutputStream: ByteArrayOutputStream, isBase64: Boolean, outputExtension: String?, reactContext: ReactApplicationContext?): String? {
fun copyExifInfo(imagePath:String, outputUri:String){
try {
// for copy exif info
val sourceExif = ExifInterface(imagePath)
val compressedExif = ExifInterface(outputUri)
for (tag in exifAttributes) {
val compressedValue = compressedExif.getAttribute(tag)
if(compressedValue==null)
{
val sourceValue = sourceExif.getAttribute(tag)
if (sourceValue != null) {
compressedExif.setAttribute(tag, sourceValue)
}
}
}
compressedExif.saveAttributes()
} catch (e: Exception) {
e.printStackTrace()
}
}

fun encodeImage(imageDataByteArrayOutputStream: ByteArrayOutputStream, isBase64: Boolean, outputExtension: String?,imagePath: String?, reactContext: ReactApplicationContext?): String? {
if (isBase64) {
val imageData = imageDataByteArrayOutputStream.toByteArray()
return Base64.encodeToString(imageData, Base64.DEFAULT)
Expand All @@ -64,6 +86,9 @@ object ImageCompressor {
try {
val fos = FileOutputStream(outputUri)
imageDataByteArrayOutputStream.writeTo(fos)

copyExifInfo(imagePath!!, outputUri)

return getRNFileUrl(outputUri)
} catch (e: Exception) {
e.printStackTrace()
Expand Down Expand Up @@ -112,7 +137,7 @@ object ImageCompressor {
val resizedImage = resize(image, options.maxWidth, options.maxHeight)
val imageDataByteArrayOutputStream = compress(resizedImage, options.output, options.quality,options.disablePngTransparency)
val isBase64 = options.returnableOutputType === ImageCompressorOptions.ReturnableOutputType.base64
return encodeImage(imageDataByteArrayOutputStream, isBase64, options.output.toString(), reactContext)
return encodeImage(imageDataByteArrayOutputStream, isBase64, options.output.toString(),imagePath, reactContext)
}

fun isCompressedSizeLessThanActualFile(sourceFileUrl: String,compressedFileUrl: String?): Boolean {
Expand Down Expand Up @@ -197,7 +222,7 @@ object ImageCompressor {
}
scaledBitmap = correctImageOrientation(scaledBitmap, imagePath)
val imageDataByteArrayOutputStream = compress(scaledBitmap, compressorOptions.output, compressorOptions.quality,compressorOptions.disablePngTransparency)
val compressedImagePath=encodeImage(imageDataByteArrayOutputStream, isBase64, compressorOptions.output.toString(), reactContext)
val compressedImagePath=encodeImage(imageDataByteArrayOutputStream, isBase64, compressorOptions.output.toString(),imagePath, reactContext)
if(isCompressedSizeLessThanActualFile(imagePath!!,compressedImagePath))
{
return compressedImagePath
Expand Down
181 changes: 161 additions & 20 deletions android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,31 @@ object Utils {
val currentVideoCompression = intArrayOf(0)
val videoCompressorClass: VideoCompressorClass? = VideoCompressorClass(reactContext);
compressorExports[uuid] = videoCompressorClass
videoCompressorClass?.start(srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(),
listener = object : CompressionListener {
override fun onProgress(index: Int, percent: Float) {
if (percent <= 100)
{
val roundProgress = Math.round(percent)
if (progressDivider==0||(roundProgress % progressDivider == 0 && roundProgress > currentVideoCompression[0])) {
EventEmitterHandler.emitVideoCompressProgress((percent / 100).toDouble(),uuid)
currentVideoCompression[0] = roundProgress
}
videoCompressorClass?.start(
srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(),
listener = object : CompressionListener {
override fun onProgress(index: Int, percent: Float) {
if (percent <= 100) {
val roundProgress = Math.round(percent)
if (progressDivider == 0 || (roundProgress % progressDivider == 0 && roundProgress > currentVideoCompression[0])) {
EventEmitterHandler.emitVideoCompressProgress((percent / 100).toDouble(), uuid)
currentVideoCompression[0] = roundProgress
}
}
}

override fun onStart(index: Int) {
override fun onStart(index: Int) {

}
}

override fun onSuccess(index: Int, size: Long, path: String?) {
val fileUrl = "file://$destinationPath"
//convert finish,result(true is success,false is fail)
promise.resolve(fileUrl)
MediaCache.removeCompletedImagePath(fileUrl)
currentVideoCompression[0] = 0
compressorExports[uuid]=null
}
override fun onSuccess(index: Int, size: Long, path: String?) {
val fileUrl = "file://$destinationPath"
//convert finish,result(true is success,false is fail)
promise.resolve(fileUrl)
MediaCache.removeCompletedImagePath(fileUrl)
currentVideoCompression[0] = 0
compressorExports[uuid] = null
}

override fun onFailure(index: Int, failureMessage: String) {
Log.wtf("failureMessage", failureMessage)
Expand Down Expand Up @@ -155,6 +155,147 @@ object Utils {
Log.d(AudioCompressor.TAG, log)
}

val exifAttributes = arrayOf(
"FNumber",
"ApertureValue",
"Artist",
"BitsPerSample",
"BrightnessValue",
"CFAPattern",
"ColorSpace",
"ComponentsConfiguration",
"CompressedBitsPerPixel",
"Compression",
"Contrast",
"Copyright",
"CustomRendered",
"DateTime",
"DateTimeDigitized",
"DateTimeOriginal",
"DefaultCropSize",
"DeviceSettingDescription",
"DigitalZoomRatio",
"DNGVersion",
"ExifVersion",
"ExposureBiasValue",
"ExposureIndex",
"ExposureMode",
"ExposureProgram",
"ExposureTime",
"FileSource",
"Flash",
"FlashpixVersion",
"FlashEnergy",
"FocalLength",
"FocalLengthIn35mmFilm",
"FocalPlaneResolutionUnit",
"FocalPlaneXResolution",
"FocalPlaneYResolution",
"FNumber",
"GainControl",
"GPSAltitude",
"GPSAltitudeRef",
"GPSAreaInformation",
"GPSDateStamp",
"GPSDestBearing",
"GPSDestBearingRef",
"GPSDestDistance",
"GPSDestDistanceRef",
"GPSDestLatitude",
"GPSDestLatitudeRef",
"GPSDestLongitude",
"GPSDestLongitudeRef",
"GPSDifferential",
"GPSDOP",
"GPSImgDirection",
"GPSImgDirectionRef",
"GPSLatitude",
"GPSLatitudeRef",
"GPSLongitude",
"GPSLongitudeRef",
"GPSMapDatum",
"GPSMeasureMode",
"GPSProcessingMethod",
"GPSSatellites",
"GPSSpeed",
"GPSSpeedRef",
"GPSStatus",
"GPSTimeStamp",
"GPSTrack",
"GPSTrackRef",
"GPSVersionID",
"ImageDescription",
"ImageLength",
"ImageUniqueID",
"ImageWidth",
"InteroperabilityIndex",
"ISOSpeedRatings",
"ISOSpeedRatings",
"JPEGInterchangeFormat",
"JPEGInterchangeFormatLength",
"LightSource",
"Make",
"MakerNote",
"MaxApertureValue",
"MeteringMode",
"Model",
"NewSubfileType",
"OECF",
"AspectFrame",
"PreviewImageLength",
"PreviewImageStart",
"ThumbnailImage",
"Orientation",
"PhotometricInterpretation",
"PixelXDimension",
"PixelYDimension",
"PlanarConfiguration",
"PrimaryChromaticities",
"ReferenceBlackWhite",
"RelatedSoundFile",
"ResolutionUnit",
"RowsPerStrip",
"ISO",
"JpgFromRaw",
"SensorBottomBorder",
"SensorLeftBorder",
"SensorRightBorder",
"SensorTopBorder",
"SamplesPerPixel",
"Saturation",
"SceneCaptureType",
"SceneType",
"SensingMethod",
"Sharpness",
"ShutterSpeedValue",
"Software",
"SpatialFrequencyResponse",
"SpectralSensitivity",
"StripByteCounts",
"StripOffsets",
"SubfileType",
"SubjectArea",
"SubjectDistance",
"SubjectDistanceRange",
"SubjectLocation",
"SubSecTime",
"SubSecTimeDigitized",
"SubSecTimeDigitized",
"SubSecTimeOriginal",
"SubSecTimeOriginal",
"ThumbnailImageLength",
"ThumbnailImageWidth",
"TransferFunction",
"UserComment",
"WhiteBalance",
"WhitePoint",
"XResolution",
"YCbCrCoefficients",
"YCbCrPositioning",
"YCbCrSubSampling",
"YResolution"
)

fun getLength(uri: Uri, contentResolver: ContentResolver): Long {
var assetFileDescriptor: AssetFileDescriptor? = null
try {
Expand Down
38 changes: 36 additions & 2 deletions ios/Image/ImageCompressor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,38 @@ class ImageCompressor {
}
return false
}


static func isPNG(_ data: Data) -> Bool {
return data.starts(with: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
}

static func copyExifInfo(actualImagePath:String, image: UIImage, data: Data) -> Data {
let fileURL = URL(string: actualImagePath)!
let filePath = fileURL.path

let url = URL(fileURLWithPath: filePath)
let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]

let dataProvider = CGDataProvider(data: data as CFData)
let dataImageSource = CGImageSourceCreateWithDataProvider(dataProvider!, nil)!
let dataMetadata = CGImageSourceCopyPropertiesAtIndex(dataImageSource, 0, nil) as? [CFString: Any]

// Copy all keys from source metadata to destination metadata if they don't exist
for (key, value) in dataMetadata ?? [:] {
if metadata?[key] == nil {
metadata?[key] = value
}
}

let outputFormat = isPNG(data) ? kUTTypePNG : kUTTypeJPEG

let destinationData = NSMutableData()
let destination = CGImageDestinationCreateWithData(destinationData, outputFormat, 1, nil)!
CGImageDestinationAddImage(destination, image.cgImage!, metadata as CFDictionary?)
CGImageDestinationFinalize(destination)
return destinationData as Data
}

static func writeImage(_ image: UIImage, output: Int, quality: Float, outputExtension: String, isBase64: Bool,disablePngTransparency:Bool,isEnableAutoCompress:Bool,actualImagePath:String)-> String{
var data: Data
Expand All @@ -135,7 +166,10 @@ class ImageCompressor {
}

}


data=copyExifInfo(actualImagePath: actualImagePath, image: image, data: data)


if isBase64 {
return data.base64EncodedString(options: [])
} else {
Expand Down
Loading