diff --git a/README.md b/README.md index 19ae17f..9663f39 100644 --- a/README.md +++ b/README.md @@ -21,29 +21,30 @@ const ImageFactory = require('ti.imagefactory'); Creates a new image by creating a copy of the given image that is rotated. -#### Arguments +##### Arguments -* Image blob[blob]: Image to transform -* Options[dictionary]: A dictionary specifying the options for the transformation - * degrees[int]: The degrees to rotate the image +* object with: + * Image blob[blob]: Image to transform + * degrees[int]: The degrees to rotate the image + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### imageWithAlpha(blob, options) Creates a new image by creating a copy of the given image, adding an alpha channel if it doesn't already have one. -#### Arguments +##### Arguments -* Image blob[blob]: Image to transform -* Options[dictionary]: A dictionary specifying the options for the transformation - * iOS: Currently there are no properties for this method -- specify an empty dictionary - * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) - * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) +* object with: + * Image blob[blob]: Image to transform + * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) + * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### imageWithTransparentBorder(blob, options) Creates a new image by creating a copy of the given image, adding a transparent border of the given size around its edges. The size of the image will be expanded by the specified border size. -#### Arguments +##### Arguments * Image blob[blob]: Image to transform * Options[dictionary]: A dictionary specifying the options for the transformation @@ -55,7 +56,7 @@ Creates a new image by creating a copy of the given image, adding a transparent Creates a new image by creating a copy of the given image with rounded corners. -#### Arguments +##### Arguments * Image blob[blob]: Image to transform * Options[dictionary]: A dictionary specifying the options for the transformation @@ -68,63 +69,66 @@ Creates a new image by creating a copy of the given image with rounded corners. Creates a new image by creating a copy of the given image that is squared to the thumbnail size. -#### Arguments - -* Image blob[blob]: Image to transform -* Options[dictionary]: A dictionary specifying the options for the transformation - * size[int]: The size of the thumbnail (default: 48) - * borderSize[int]: The size of the border (default: 1) - * cornerRadius[int]: The radius of the corner edges (default:0) - * iOS: quality[int]: The interpolation quality. One of the following constants (default: imagefactory.QUALITY\_HIGH) - * imagefactory.QUALITY\_DEFAULT - * imagefactory.QUALITY\_NONE - * imagefactory.QUALITY\_LOW - * imagefactory.QUALITY\_MEDIUM - * imagefactory.QUALITY\_HIGH - * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) - * Android: dither[boolean]: Indicates if dithering should be applied while scaling. (default: false) - * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) +##### Arguments + +* object with: + * Image blob[blob]: Image to transform + * size[int]: The size of the thumbnail (default: 48) + * borderSize[int]: The size of the border (default: 1) + * cornerRadius[int]: The radius of the corner edges (default:0) + * iOS: quality[int]: The interpolation quality. One of the following constants (default: imagefactory.QUALITY\_HIGH) + * imagefactory.QUALITY\_DEFAULT + * imagefactory.QUALITY\_NONE + * imagefactory.QUALITY\_LOW + * imagefactory.QUALITY\_MEDIUM + * imagefactory.QUALITY\_HIGH + * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) + * Android: dither[boolean]: Indicates if dithering should be applied while scaling. (default: false) + * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### imageAsResized(blob, options) Creates a new image by creating a copy of the given image that is rescaled to the specified size. -#### Arguments - -* Image blob[blob]: Image to transform -* Options[dictionary]: A dictionary specifying the options for the transformation - * width[int]: The width of the new image (default: image width) - * height[int]: The height of the new image (default: image height) - * iOS: hires[boolean]: Create a hires image (for Retina displays only) - * iOS: quality[int]: The interpolation quality. One of the following constants (default: imagefactory.QUALITY\_HIGH) - * imagefactory.QUALITY\_DEFAULT - * imagefactory.QUALITY\_NONE - * imagefactory.QUALITY\_LOW - * imagefactory.QUALITY\_MEDIUM - * imagefactory.QUALITY\_HIGH - * Android. format [int]: The output format of the image: either ImageFactory.PNG or ImageFactory.JPEG (default: ImageFactory.JPEG) - * quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) +##### Arguments + +* object with: + * Image blob[blob]: Image to transform + * width[int]: The width of the new image (default: image width) + * height[int]: The height of the new image (default: image height) + * iOS: hires[boolean]: Create a hires image (for Retina displays only) + * iOS: quality[int]: The interpolation quality. One of the following constants (default: imagefactory.QUALITY\_HIGH) + * imagefactory.QUALITY\_DEFAULT + * imagefactory.QUALITY\_NONE + * imagefactory.QUALITY\_LOW + * imagefactory.QUALITY\_MEDIUM + * imagefactory.QUALITY\_HIGH + * Android. format [int]: The output format of the image: either ImageFactory.PNG or ImageFactory.JPEG (default: ImageFactory.JPEG) + * quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### imageAsCropped(blob, options) Creates a new image by creating a copy of the given image that is cropped to the specified bounds. -#### Arguments +##### Arguments -* Image blob[blob]: Image to transform -* Options[dictionary]: A dictionary specifying the options for the transformation - * width[int]: The width of the new image (default: image width) - * height[int]: The height of the new image (default: image height) - * x[int]: The x-coordinate of the upper-left corner of the bounds (default: image center - width / 2) - * y[int]: The y-coordinate of the upper-left corner of the bounds (default: image center - height / 2) - * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) - * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) +* object with: + * Image blob[blob]: Image to transform + * width[int]: The width of the new image (default: image width) + * height[int]: The height of the new image (default: image height) + * x[int]: The x-coordinate of the upper-left corner of the bounds (default: image center - width / 2) + * y[int]: The y-coordinate of the upper-left corner of the bounds (default: image center - height / 2) + * Android: format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) + * Android: quality[float]: The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). (default: 0.7) + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### imageTransform(blob, options) Creates a new image by applying a sequence of transformations to the image. -#### Arguments +##### Arguments * Image blob[blob]: Image to transform * Transform[dictionary]: A sequence of transform specifications. Transforms are listed as additional parameters to the function and are applied in the order specified. Each transform is a dictionary with the options described above for each transform along with an additional 'type' property included with each dictionary of transform options. @@ -141,17 +145,19 @@ Creates a new image by applying a sequence of transformations to the image. Creates a new image by creating a copy of the given image and applying the specified compression quality. -#### Arguments +##### Arguments -* Image blob[blob]: Image to compress -* Compression Quality[float]; The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). -* Android (optional): format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) +* object with: + * blob [blob]: Image to compress + * quality [float]; The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). + * format [int]: The output format: ImageFactory.JPEG, ImageFactory.PNG, ImageFactory.WEBP (default: ImageFactory.JPEG) + * success [function]: Method that will be called when the compressin is done. It will return `image` [blob]. ### compressToFile(blob, quality, fileUrl) Compresses the given blob to an image file. -#### Arguments +##### Arguments * Image blob[blob]: Image to write to file * Compression Quality[float]; The quality of the resulting JPEG or WebP image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). diff --git a/android/manifest b/android/manifest index aa04036..9d641d9 100644 --- a/android/manifest +++ b/android/manifest @@ -2,7 +2,7 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 5.1.0 +version: 5.2.0 apiversion: 4 architectures: arm64-v8a armeabi-v7a x86 x86_64 description: Image Factory diff --git a/android/src/ti/imagefactory/ImageFactoryModule.java b/android/src/ti/imagefactory/ImageFactoryModule.java index 09f9d14..03e9268 100644 --- a/android/src/ti/imagefactory/ImageFactoryModule.java +++ b/android/src/ti/imagefactory/ImageFactoryModule.java @@ -7,11 +7,16 @@ package ti.imagefactory; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.lang.reflect.Field; import java.util.HashMap; import org.appcelerator.kroll.KrollDict; +import org.appcelerator.kroll.KrollFunction; import org.appcelerator.kroll.KrollModule; import org.appcelerator.kroll.annotations.Kroll; import org.appcelerator.kroll.common.Log; @@ -19,6 +24,7 @@ import org.appcelerator.titanium.io.TiBaseFile; import org.appcelerator.titanium.io.TiFileFactory; import org.appcelerator.titanium.util.TiConvert; +import org.appcelerator.titanium.view.TiDrawableReference; @Kroll.module(name = "ImageFactory", id = "ti.imagefactory") public class ImageFactoryModule extends KrollModule @@ -65,16 +71,64 @@ public class ImageFactoryModule extends KrollModule @Kroll.constant public static final int WEBP = ImageFormatType.WEBP.toTitaniumIntegerId(); - @Kroll.method - public TiBlob imageWithRotation(TiBlob blob, HashMap args) + public TiBlob localImageWithRotation(TiBlob blob, KrollDict args) { - return ImageFactory.imageRotate(blob, new KrollDict(args)); + return ImageFactory.imageRotate(blob, args); } @Kroll.method - public TiBlob imageWithAlpha(TiBlob blob, HashMap args) + public void imageWithRotation(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localImageWithRotation(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localImageWithRotation(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } + + public TiBlob localImageWithAlpha(TiBlob blob, KrollDict args) { - return ImageFactory.imageAlpha(blob, new KrollDict(args)); + return ImageFactory.imageAlpha(blob, args); + } + + @Kroll.method + public void imageWithAlpha(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localImageWithAlpha(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localImageWithAlpha(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } } @Kroll.method @@ -89,22 +143,95 @@ public TiBlob imageWithRoundedCorner(TiBlob blob, HashMap args) return ImageFactory.imageRoundedCorner(blob, new KrollDict(args)); } - @Kroll.method - public TiBlob imageAsThumbnail(TiBlob blob, HashMap args) + public TiBlob localImageAsThumbnail(TiBlob blob, KrollDict args) { - return ImageFactory.imageThumbnail(blob, new KrollDict(args)); + return ImageFactory.imageThumbnail(blob, args); } @Kroll.method - public TiBlob imageAsResized(TiBlob blob, HashMap args) + public void imageAsThumbnail(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localImageAsThumbnail(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localImageAsThumbnail(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } + + @Kroll.method + public TiBlob localImageAsResized(TiBlob blob, KrollDict args) { - return ImageFactory.imageResize(blob, new KrollDict(args)); + return ImageFactory.imageResize(blob, args); } @Kroll.method - public TiBlob imageAsCropped(TiBlob blob, HashMap args) + public void imageAsResized(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localImageAsResized(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localImageAsResized(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } + + public TiBlob localImageAsCropped(TiBlob blob, KrollDict args) { - return ImageFactory.imageCrop(blob, new KrollDict(args)); + return ImageFactory.imageCrop(blob, args); + } + + @Kroll.method + public void imageAsCropped(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localImageAsCropped(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localImageAsCropped(blob, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } } @Kroll.method @@ -209,14 +336,13 @@ public TiBlob imageTransform(Object[] args) return blob; } - @Kroll.method - public TiBlob compress(TiBlob blob, float quality, @Kroll.argument(optional = true) Object formatId) + + public TiBlob localCompress(TiBlob blob, float quality, @Kroll.argument(optional = true) Object formatId) { // Do not continue if not given a blob. if (blob == null) { return null; } - // Compress blob's image to JPEG with given quality. (Quality range: 0.0 - 1.0) // Calling imageRotate() with degrees 0 forces image to upright position in case it has EXIF orientation. KrollDict args = new KrollDict(); @@ -275,10 +401,151 @@ public boolean compressToFile(TiBlob blob, float quality, String fileUrl) return wasSuccessful; } + @Kroll.method + public void compress(KrollDict args) { + final ImageFactoryModule that = this; + + TiBlob blob = TiConvert.toBlob(args.get("blob")); + float quality = TiConvert.toFloat(args.get("quality")); + int formatId = TiConvert.toInt(args.get("format"), ImageFactoryModule.JPEG); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + KrollFunction callback = (KrollFunction) args.get("success"); + + if (sync) { + TiBlob result = that.localCompress(blob, quality, formatId); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localCompress(blob, quality, formatId); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } + @Kroll.method public KrollDict metadataFrom(TiBlob blob) { ImageMetadata metadata = ImageMetadata.from(blob); return (metadata != null) ? metadata.getEntries() : null; } + + public int[] localGetPixelArray(TiBlob blob) + { + TiDrawableReference ref = TiDrawableReference.fromBlob(getActivity(), blob); + Bitmap bitmap = ref.getBitmap(false,false); + + int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + + return pixels; + } + + @Kroll.method + public void getPixelArray(KrollDict args) { + final ImageFactoryModule that = this; + TiBlob blob = TiConvert.toBlob(args.get("blob")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + int[] result = that.localGetPixelArray(blob); + KrollDict map = new KrollDict(); + map.put("pixels", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + int[] result = that.localGetPixelArray(blob); + KrollDict map = new KrollDict(); + map.put("pixels", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } + + private void coerceDimensionsIntoBlob(Bitmap image, TiBlob blob) { + try { + Field field = TiBlob.class.getDeclaredField("width"); + field.setAccessible(true); + field.setInt(blob, image.getWidth()); + field = TiBlob.class.getDeclaredField("height"); + field.setAccessible(true); + field.setInt(blob, image.getHeight()); + } catch (Exception e) { + // ** cry ** + } + } + + private TiBlob convertImageToBlob(Bitmap image) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte data[] = new byte[0]; + // NOTE: While "70" is indeed ignored by the "compress" method, it is there so that in the future if we let the + // user decide the final output format and compression, we can remember what the default value is. For now, let + // us keep things simple and compress to a png so that we get transparency. Because it is loseless, users can + // then turn around and make a call to "compress" if they want it to be a smaller JPEG. + if (image.compress(Bitmap.CompressFormat.PNG, 70, bos)) { + data = bos.toByteArray(); + } + + TiBlob result = TiBlob.blobFromData(data, "image/png"); + coerceDimensionsIntoBlob(image, result); + + // [MOD-309] Free up memory to work around issue in Android + image.recycle(); + image = null; + + return result; + } + + public TiBlob localResampleImage(String fileName, KrollDict args) + { + final KrollDict argsDict = new KrollDict(args); + final int inSampleSize = argsDict.optInt("inSampleSize", 1); + final int inDensity = argsDict.optInt("inDensity", 1); + final int inTargetDensity = argsDict.optInt("inTargetDensity", 1); + + final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inSampleSize = inSampleSize; + bitmapOptions.inDensity = inDensity; + bitmapOptions.inTargetDensity = inTargetDensity; + + final Bitmap scaledBitmap = BitmapFactory.decodeFile(fileName, bitmapOptions); + scaledBitmap.setDensity(Bitmap.DENSITY_NONE); + + return convertImageToBlob(scaledBitmap); + } + + @Kroll.method + public void resampleImage(KrollDict args) { + final ImageFactoryModule that = this; + String fileName = TiConvert.toString(args.get("file")); + KrollFunction callback = (KrollFunction) args.get("success"); + boolean sync = TiConvert.toBoolean(args.get("sync"), false); + + if (sync) { + TiBlob result = that.localResampleImage(fileName, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } else { + new Thread() { + @Override + public void run() { + TiBlob result = that.localResampleImage(fileName, args); + KrollDict map = new KrollDict(); + map.put("image", result); + callback.call(getKrollObject(), map); + } + }.start(); + } + } }