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

kraken compression method added #173

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f5f50c1
Upload input validation
tinpham5614 Jul 26, 2023
c0ba97a
Merge pull request #1 from tinpham5614/feat-4125-compress-image-funct…
tinpham5614 Jul 26, 2023
1082a98
conversion method created
brinkbrink Aug 1, 2023
7afa743
fixed method header
brinkbrink Aug 1, 2023
ea43842
tinify compression method
brinkbrink Aug 2, 2023
d95f1e5
Merge pull request #2 from tinpham5614/brink-tinify-compression-method
brinkbrink Aug 2, 2023
dae50f8
API key and import statement added
brinkbrink Aug 2, 2023
645a431
Merge pull request #3 from tinpham5614/brink-tinify-compression-method
brinkbrink Aug 2, 2023
6472a0b
apikey parameter
brinkbrink Aug 2, 2023
3276dec
Merge pull request #4 from tinpham5614/brink-tinify-compression-method
brinkbrink Aug 2, 2023
0c9a0bd
update current code
tinpham5614 Aug 9, 2023
092b140
Merge pull request #5 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
6b93cb1
update current code
tinpham5614 Aug 9, 2023
95de047
Merge pull request #6 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
6343cd3
update current code
tinpham5614 Aug 9, 2023
39ecb1a
Merge pull request #7 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
02b628a
update current code
tinpham5614 Aug 9, 2023
2781060
Merge pull request #8 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
149adb6
update current code
tinpham5614 Aug 9, 2023
64b1b65
Merge pull request #9 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
91690fc
update current code
tinpham5614 Aug 9, 2023
aea4193
Merge pull request #10 from tinpham5614/tin-update-methods
tinpham5614 Aug 9, 2023
ada6f53
add krakenCompress method
Ali-7s Aug 11, 2023
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
298 changes: 298 additions & 0 deletions java/compress_image/Index.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> payload, RuntimeResponse res) {
Map<String, Object> 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<String, String> variables = req.getVariables();

if (!variables.containsKey(apiKeyVariable) || variables.get(apiKeyVariable) == null || variables.get(apiKeyVariable).trim().isEmpty()) {
Map<String, Object> 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<String, Object> 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<String, Object> responseData = responseGson.fromJson(response.body().string(), Map.class);
krakedUrl = responseData.get("kraked_url").toString();
} catch (Exception e) {
e.printStackTrace();
}
return krakedUrl;
};
67 changes: 67 additions & 0 deletions java/compress_image/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions java/compress_image/deps.gradle
Original file line number Diff line number Diff line change
@@ -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'
}