diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java new file mode 100644 index 00000000..5fd65d16 --- /dev/null +++ b/java/compress_image/Index.java @@ -0,0 +1,298 @@ +import java.util.Map; +import java.util.HashMap; +import java.net.URL; +import java.util.stream.Collectors; +import java.util.stream.*; +import java.util.Base64; +import com.google.gson.Gson; +import com.tinify.Source; +import com.tinify.Tinify; +import java.io.*; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.MultipartBody; + +/** + * Enum for provider names + * @param name is provider name + * TINY_PNG is TinyPNG provider + * KRAKENIO is Kraken.io provider + * getName() is getter for provider name + */ + +private enum Provider { + TINY_PNG("tinypng"), KRAKENIO("krakenio"); + + private final String name; + + Provider(String name) { + this.name = name; + } + + String getName() { + return name; + } + + // check if provider is valid + static boolean validateProvider(String name) { + for (Provider providerName : Provider.values()) { + if (providerName.name.equals(name)) { + return true; + } + } + return false; + } +} + + final Gson gson = new Gson(); + + public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exception { + + // payload string + String payloadString = req.getPayload() == null || req.getPayload().isEmpty() ? "{}" : req.getPayload(); + + // convert payload to map + Map payload = gson.fromJson(payloadString, Map.class); + + // check if requested payload and variables are present + RuntimeResponse errorResponse = checkPayloadAndVariables(req, res); + if (errorResponse != null) { + return errorResponse; + } + + // check if payload contains provider and image + errorResponse = validatePayload(payload, res); + if (errorResponse != null) { + return errorResponse; + } + + // get provider from payload and image from payload + String provider = payload.get("provider").toString(); + String image = payload.get("image").toString(); + + // check API key is present in variables + String apiKeyVariable = Provider.TINY_PNG.getName().equals(provider) ? "TINYPNG_API_KEY" : "KRAKENIO_API_KEY"; + + errorResponse = checkEmptyApiKey(req, res, apiKeyVariable); + if (errorResponse != null) { + return errorResponse; + } + String apiKey = req.getVariables().get(apiKeyVariable); + String secretKey = req.getVariables().get("KRAKENIO_API_SECRET"); + + // compressed image in Base64 string + String compressedImage = ""; + + // response data to return + Map responseData = new HashMap<>(); + + // compress image using provider API and store the result in compressedImage variable + if (Provider.TINY_PNG.getName().equals(provider)) { + // Decode image from Base64 string + byte[] imageByte = convertToByte(image); + + // Compress image + byte[] compressedImageByte = tinifyCompress(imageByte, apiKey); + + // Encode image to Base64 string + compressedImage = convertToBase64(compressedImageByte); + } else { + // Decode input string + byte[] imageBytes = convertToByte(image); + + // Compress image + String urlResponse = krakenCompress(imageBytes, apiKey, secretKey); + + // Decode compressed image from URL + URL url = new URL(urlResponse); + InputStream inputStream = url.openStream(); + byte[] compressedImageBytes = inputStream.readAllBytes(); + compressedImage = convertToBase64(compressedImageBytes); + inputStream.close(); + } + + // Check if compressedImage is valid + errorResponse = checkCompressedImage(compressedImage, res); + if (errorResponse != null) { + return errorResponse; + } + + // If input valid then return success true and compressed image + responseData.put("success", true); + responseData.put("image", compressedImage); + + return res.json(responseData); + } + + /** + * Check if requested payload and variables are present + * @param req is request object from function call + * @param res is response object from function call + * @return null if payload and variables are present, otherwise return error response + */ + private RuntimeResponse checkPayloadAndVariables(RuntimeRequest req, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if requested payload and variables are present + if (req.getPayload() == null || req.getPayload().trim().isEmpty() || req.getPayload().trim().equals("{}")) { + responseData.put("success", false); + responseData.put("message", "Payload is empty, please provide provider and image"); + return res.json(responseData); + } + + if (req.getVariables() == null) { + responseData.put("success", false); + responseData.put("message", "Variables are empty, please provide an API key for provider"); + return res.json(responseData); + } + return null; + } + + /** + * Validate payload whether it contains provider and image + * @param payload is an object from request payload which contains provider and image + * @param res is response object from function call + * @return null if payload is valid, otherwise return error response + */ + + private RuntimeResponse validatePayload(Map payload, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if payload contains provider and image + if (!payload.containsKey("provider") || !payload.containsKey("image")) { + responseData.put("success", false); + responseData.put("message", "Payload is invalid, please provide provider and image"); + return res.json(responseData); + } + + // get provider from payload and image from payload + String provider = payload.get("provider").toString(); + String image = payload.get("image").toString(); + + // check if provider is valid + if (!Provider.validateProvider(provider)) { + responseData.put("success", false); + String providerNames = Stream.of(Provider.values()).map(Provider::getName).collect(Collectors.joining(", ")); // get all provider names + responseData.put("message", "Provider " + provider + "is invalid, please provide one of providers: " + providerNames); + return res.json(responseData); + } + + // check if image is valid in Base64 with regex pattern matching + if (!image.matches("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$") || image.trim().isEmpty()) { + responseData.put("success", false); + responseData.put("message", "Image is invalid, please provide a valid Base64 image"); + return res.json(responseData); + } + return null; + } + + /** + * Check if API key is present in variables for provider + * @param req is request object from function call + * @param res is response object from function call + * @param apiKeyVariable is API key variable name for provider + * @return null if API key is present, otherwise return error response + */ + + private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res, String apiKeyVariable) { + Map variables = req.getVariables(); + + if (!variables.containsKey(apiKeyVariable) || variables.get(apiKeyVariable) == null || variables.get(apiKeyVariable).trim().isEmpty()) { + Map responseData = new HashMap<>(); + responseData.put("success", false); + responseData.put("message", "API key is not present in variables, please provide " + apiKeyVariable + " for the provider"); + return res.json(responseData); + } + return null; + } + + /** + * Check if compressed image is valid + * @param compressedImage is compressed image in Base64 string + * @param res is response object from function call + * @return null if compressed image is valid, otherwise return error response + */ + + private RuntimeResponse checkCompressedImage(String compressedImage, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if compressed image is valid + if (!compressedImage.matches("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$") || compressedImage.trim().isEmpty()) { + responseData.put("success", false); + responseData.put("message", "Compressed image is invalid, please provide a valid Base64 image"); + return res.json(responseData); + } + return null; + } + + + /** + * Converts Base64 input of payload to byte array + * @param String baseInput is base64 string variable from payload + * @return byte [] baseInput decoded as byte array + */ + + private byte [] convertToByte(String baseInput) { + return Base64.getDecoder().decode(baseInput); + } + + /** + * Converts byte array input returned by compression method to Base64 + * @param byte [] byteInput is byte array variable returned from compression method + * @return String byteInput encoded as Base64 String + */ + + private String convertToBase64(byte [] byteInput) { + return Base64.getEncoder().encodeToString(byteInput); + } + + + /** + * Compresses image in byte array format using TinyPNG provider + * @param byte [] image is image to compress in byte array format + * @return byte [] compressed image + */ + + private byte [] tinifyCompress(byte [] image, String apiKey) throws Exception { + Tinify.setKey(apiKey); + Source source = Tinify.fromBuffer(image); + return source.toBuffer(); + } + + /** + * Compresses image in byte array format using Kraken.io provider + * @param byte [] image is image to compress in byte array format + * @return String url of compressed image in String format + */ + + public String krakenCompress(byte[] image, String apiKey, String apiSecret) throws IOException { + OkHttpClient client = new OkHttpClient(); + String krakedUrl = ""; + + String data = "{\"auth\":{\"api_key\":\"" + apiKey + "\",\"api_secret\":\"" + apiSecret + "\"}, \"wait\": true}"; + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("data", data) + .addFormDataPart("upload", "image.jpg", RequestBody.create(MediaType.parse("image/jpg"), image)) + .build(); + + Request request = new Request.Builder() + .url("https://api.kraken.io/v1/upload") + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = client.newCall(request).execute()) { + Gson responseGson = new Gson(); + Map responseData = responseGson.fromJson(response.body().string(), Map.class); + krakedUrl = responseData.get("kraked_url").toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return krakedUrl; + }; diff --git a/java/compress_image/README.md b/java/compress_image/README.md new file mode 100644 index 00000000..d30e88d8 --- /dev/null +++ b/java/compress_image/README.md @@ -0,0 +1,67 @@ +# Compress .png Images + +A Java Cloud Function that compresses png images. + + +`image` and `provider` are recieved from the payload, where `image` is a base64 encoded string and `provider` is either [`tinypng`](https://tinypng.com) or [`krakenio`](https://kraken.io) + +_Example input:_ + +```json +{ + "provider": "tinypng", + "image": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==", + +} +``` + +> `krakenio` is also a supported provider + +_Example output:_ + +```json +{ + "success": true, + "image": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAG1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUUeIgAAAACHRSTlMA8712Sxr5g97cFtUAAAA9SURBVCjPY6Aa6AADfAIcDSA8KoBTgLGVgSFCAEmAqZmBwUIBSYClzTQ4wwE52Cs6OtpR4oFFUciBerEKAP58HnyLtZsYAAAAAElFTkSuQmCC" +} +``` + +## 📝 Variables + +> only selected provider's api keys are neccessary, ie. kraken's api keys are not neccessary when choosing tinypng as the provider. + +- **TINYPNG_API_KEY** - API key for tinypng service +- **KRAKENIO_API_KEY** - API key for kraken-io service +- **KRAKENIO_API_SECRET** - API Secret for kraken-io service + +## 🚀 Deployment + +1. Clone this repository, and enter this function folder: + +``` +$ git clone https://github.com/open-runtimes/examples.git && cd examples +$ cd java/compress_image +``` + +2. Enter this function folder and build the code: +``` +docker run -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD:/usr/code openruntimes/java:v2-11.0 sh /usr/local/src/build.sh +``` +As a result, a `code.tar.gz` file will be generated. + +3. Start the Open Runtime: +``` +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/java:v2-11.0 sh /usr/local/src/start.sh +``` + +4. Execute function: + +```shell +curl http://localhost:3000/ -d '{"variables":{"TINYPNG_API_KEY":"[YOUR_API_KEY]"},"payload":"{\"provider\":\"tinypng\",\"image\":\"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" +``` + +Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/java-11.0). + +## 📝 Notes +- This function is designed for use with Appwrite Cloud Functions. You can learn more about it in [Appwrite docs](https://appwrite.io/docs/functions). +- This example is compatible with Java 11.0. Other versions may work but are not guaranteed to work as they haven't been tested. \ No newline at end of file diff --git a/java/compress_image/deps.gradle b/java/compress_image/deps.gradle new file mode 100644 index 00000000..c60effe1 --- /dev/null +++ b/java/compress_image/deps.gradle @@ -0,0 +1,5 @@ +dependencies { + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.tinify:tinify:1.8.3' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' +} \ No newline at end of file