• Trg Slobode 16, Tuzla, 75000, Bosnia and Herzegovina
  • info@gradient.ba
Schedule informative meeting Contact Us
Gradient Software Development Logo
  • Home
  • Services
    • Software Development Partnership
    • Business Solution Development
    • Software Architecture Design and Consulting
  • Products
    • Gradient Image Processing
  • Team
    • Azra Kunić
    • Emir Aličić
  • About us
  • Pricing Models
  • Contact us

Usage Manual

Usage Manual

Summary

  1. Local Development
    1. Install Dependencies
    2. Configure Database Connection
    3. Run Migrations
    4. Start Server
  2. Deployment (with docker)
    1. Install Dependencies
    2. Build and start docker
  3. Insert Configurations
    1. Login as the Admin and create Client
    2. Login as the Client and create ClientApp
    3. Insert Configurations
  4. Integration with Gradient Image Processing
    1. Javascript (Node.js)
    2. PHP
    3. Python
    4. Java

1. Local Development

Local development implies setting up your local environment with all dependencies and configurations needed for the service to work.

Which dependencies and configurations are necessary, are explained in the sections below.

1.1. Install Dependencies

Before you start the service in your local environment, you must install the necessary dependencies.

The first one is Node.js. It is used for running the backend side of Gradient Image Processing.

Instructions for installing Node.js

After installing node.js, the next one is npm. It's a node package manager.

How to install npm

Once you have npm installed you can run the following instruction, both to install and upgrade Yarn:

 npm install --global yarn 

To install all necessary dependencies needed for the Gradient Image Processing project, you need to identify the directory "image-processing-service-backend" and execute the following command in that directory:

 yarn install

1.2. Configure Database Connection

We continue setting up our local environment by creating the empty ".env" file (or copying .env.template content) at the root of the project. We use the .env file to store environment-specific configurations or settings for a project. In this section, it is explained how to set variables with values needed for database connection.

For this example, "PostgresSQL" database is used. Inside the ".env" file, you should have content like in the example below:


      TYPEORM_CONNECTION = "postgres"
      TYPEORM_HOST = "localhost"
      TYPEORM_USERNAME = "test"
      TYPEORM_PASSWORD = "test"
      TYPEORM_DATABASE = "test"
      TYPEORM_PORT = 5432
      TYPEORM_SYNCHRONIZE = false
      TYPEORM_LOGGING = true
      TYPEORM_ENTITIES = "entity/*.js"
      TYPEORM_MIGRATIONS = "migration/*.js"

For instructions on how to fill the .env file for the different databases, please check Connection Options.

1.3. Run Migrations

To bring the initially empty database (without any tables structure) to the state required by the Gradient Image Processing service, you need to run the command:

 yarn run migration 

1.4. Start Server

The last step required to start the server is to run the command:

 yarn run start:dev 

2. Deployment with docker

When there is a need to deploy the solution to production, it is good to have a clear path what exactly is needed for the service to work. In this section, docker deployment is described. It includes Dockerfile with installation steps for Ubuntu 20.04. and bash script for running Gradient Image Processing.

2.1. Install Dependencies

Only dependency needed for this system to work is docker. Installation steps can be read here.

2.2. Build and start docker

In the root of the project, Dockerfile with the following content is provided:


    FROM ubuntu:20.04

    ARG DEBIAN_FRONTEND=noninteractive

    RUN apt-get update

    RUN apt-get install -y vim curl nodejs npm

    RUN npm install n -g

    RUN n 16.13

    RUN npm install yarn -g

    CMD ["/bin/bash", "/var/www/configure-project-on-dev.sh"]

Using the preferred cli interface, you should navigate to project root and execute:

docker build -t image-processing .

As the result of this operation, a docker image with the name "image-processing" is built.

Finally, to run the service (and the built docker image), the following command is suggested:


    docker run --detach \
      --hostname image-processing-hostname \
      --publish 1000:3000 \
      --name image-processing \
      --restart always \
      --volume /project-root-path/:/var/www \
      --shm-size 256m \
      image-processing
  

All parameters should be configured as needed. In this example, Gradient Image Processing is running on port 1000, as long as /project-root-path/ is properly configured to point to the Gradient Image Processing root project.

3. Insert Configurations

Before jumping to the sections below, you need to go to a login page of Gradient Image Processing by opening the url http(s)://<host>:<port>. If everything is configured properly, you should see a login screen similar to the one in the following screenshot.

View Picture in Full Size

3.1. Login as the Admin and create Client

One of the migrations that were executed from previous steps included the insertion of Admin. So, the first step on the login page is to log in as Admin using the credentials:


    email: admin@gradient.ba;
    password: admin
  

After success, it is advised to immediately change the Admin password by using the "Change Password" functionality. You can find the "Change Password" link in the navigation bar on the right side. Click on it and change the password using provided interface.

On the navigation bar, there is the 'Clients' link. Clicking on it, you will be presented with the Clients List Page. So, the next action you need to perform is to create a Client. In order to do that, you need to click on the "Add new" button. If everything is performed correctly, you should be presented with an interface similar to the one in the following screenshot:

View Picture in Full Size

Fill in the wanted client's data similar to the example below.


    fullName: 'Test Client',
    email: 'test@test.ba',
    password: 'test',
    status: 'ACTIVE' or 'INACTIVE'.

    NOTE: Only 'ACTIVE' Clients are previewed on the list of clients.
  

After the successful creation of the Client, you can log out, by clicking on the "Logout" link on the navbar.

3.2. Login as the Client and create ClientApp

The next step is to log in as the Client. So, on the login page, you need to fill in the credentials of the previously created Client. After successful login, and clicking on the navbar "Client Applications" link, the Client App List Page is presented. The next action you need to perform is to create a Client App, by clicking on the "Add new" button.

View Picture in Full Size

Fill in the wanted Client App's data similar to the example below:


    name: 'Test Client App',
    status: 'ACTIVE' or 'INACTIVE'.
  

3.3. Insert Configurations

Insertion of configurations is the final step in configuring Gradient Image Processing for a single Client App.

NOTE: Frontend interface for managing configurations is not in its final implementation state. To achieve configuration insert, it is currently necessary to insert configurations directly into the database.

PostgreSQL instructions needed to be executed are presented below. The use case presented here assumes the requirement for our Client App that for every uploaded image the following versions of it need to be created:

  • WEBP and JPEG (quality 90%, width: 1900, ratio preserved),
  • WEBP and JPEG (quality 90%, width: 450, ratio preserved),
  • WEBP and JPEG (quality 90%, width: 960, ratio preserved),
  • WEBP and JPEG (quality 90%, width: 100, ratio preserved).

    
      -- Execute every set of queries separately, since results of previous ones are needed in the next ones

      -- First set! Insert information about wanted image types (WEBP and JPEG, 90% quality).
      insert into image_type_configuration_entity ("imageType", "qualityPercentage") VALUES ('WEBP', 90);
      insert into image_type_configuration_entity ("imageType", "qualityPercentage") VALUES ('JPEG', 90);

      -- Second set! Insert information about configurations (to connect clientAppId and image types).
      -- NOTE: ClientAppId is the "id" attribute of the Client App in the database, not the "AppID".
      insert into configuration_entity ("name", "clientAppId", "imageTypeConfigurationId") VALUES ('Webp configuration', 3, '7a9ac778-1336-4d02-b2f0-aba6f3533f67');
      insert into configuration_entity ("name", "clientAppId", "imageTypeConfigurationId") VALUES ('Jpeg configuration', 3, 'b53a9d16-77ff-436a-aeaf-6d41df84eb49');

      -- Third set! Insert information about wanted configuration sizes, for every available configuration from the second set.
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (1900, null, null, 6);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (450, null, null, 6);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (960, null, null, 6);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (100, null, null, 6);

      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (1900, null, null, 7);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (450, null, null, 7);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (960, null, null, 7);
      insert into image_size_configuration_entity (width, height, "cropMethod", "configurationId") VALUES (100, null, null, 7);

For more information on which configurations are supported and how database can be filled, one can check ER Diagram of Gradient Image Processing database.

4. Integration with Gradient Image Processing

After Local Development or Docker Deployment are performed, and Client App with its credentials and configurations is stored into the database (Insert Configurations) system is ready to be used by Consumer Service.

Consumer service performs the following operations against Gradient Image Processing service:

  1. authorizes Client Application and retrieves token,
  2. uploads an original image that needs to be processed,
  3. processes response data about result images,
  4. optionally connects using WebSocket to get information about image processing progress,
  5. removes image and all its result images once these are not needed anymore.

Code samples are added in the following sections in different programming languages, separated by upper operations.

4.1. Javascript (Node.js)

Installed npm dependencies and predefined constants for this example are:


  import fs from 'fs';
  import axios from "axios";
  import FormData from 'form-data';
  import WebSocket from "ws";

  // Constants that are needed for Gradient Image Processing usage
  const API_HOST = 'localhost';
  const API_PATH = ['http://', API_HOST, ':3000/api'].join('');
  const WS_PATH = ['ws://', API_HOST, ':8080'].join('');
  const appID = 'u61v5mrq5wSOaAXxbYKE';
  const secretID = 'GS50RJ1PI2HeZNEd0usf';
  

Authorization of Client Application to get token:


  const loginPath = [API_PATH, 'login/client-app'].join('/');
  const loginResponse = await axios.post(loginPath, {
      appID,
      secretID
  });

  const token = loginResponse.data.key;

Upload image (loading using fs from local directory):


  const token = loginResponse.data.key;
  const imageUploadPath = [API_PATH, 'images/upload-image'].join('/');

  const formData = new FormData();
  formData.append('imageContent', fs.createReadStream('../images/original-1.jpg'));
  formData.append('imageAccessibility', 'PUBLIC');

  const imageUploadResponse = await axios.post(imageUploadPath, formData, {
      headers: {
          ...formData.getHeaders(),
          token,
      }
  });

  const {progressEventBusName, sourceImageId, resultImages} = imageUploadResponse.data;

Access response data about result images


  const {progressEventBusName, sourceImageId, resultImages} = imageUploadResponse.data;

  // Store id of source/original image to be able to delete it.
  console.log(sourceImageId);

  // Retrieve result images info
  resultImages.forEach((resultImage) => {
      console.log(resultImage);
  });

Receive data through web sockets about processing progress:


  const socketClient = new WebSocket(WS_PATH);
  socketClient.on('open', () => {
    socketClient.on('message', (data) => {
      console.log("Received upload information:");
      console.log(data.toString());
    });

    socketClient.send(JSON.stringify({
       type: 'LISTEN',
       data: {
           token,
           clientAppID: appID,
           progressEventBusNames: [progressEventBusName],
       }
    }));
});

Make request to delete all images (source and result images):


  const imageDeletePath = [API_PATH, 'images', sourceImageId].join('/');
  const deleteResponse = await axios.delete(imageDeletePath, {
      headers: {
          token,
      }
  });

  console.log("Delete response status:");
  console.log(deleteResponse.status);

4.2. PHP

Installed composer dependencies and predefined constants for this example are:


  use GuzzleHttp\Client;
  use WebSocket\ConnectionException;

  $API_HOST = 'localhost';
  $API_PATH = implode('', ['http://', $API_HOST, ':3000/api']);
  $WS_PATH = implode('', ['ws://', $API_HOST, ':8080']);
  $appID = 'u61v5mrq5wSOaAXxbYKE';
  $secretID = 'GS50RJ1PI2HeZNEd0usf';

Authorization of Client Application to get token:


  $client = new Client();
  $loginPath = implode('/', [$API_PATH, 'login/client-app']);
  $loginResponse = $client->post($loginPath, [
    'json' => [
      'appID' => $appID,
      'secretID' => $secretID,
    ]
  ]);

  $tokenData = json_decode($loginResponse->getBody(), true);
  $token = $tokenData['key'];

Upload image (loading from local directory):


  $imageData = file_get_contents(__DIR__ . '/../../images/original-1.jpg');
  $imageUploadPath = implode("/", [$API_PATH, 'images/upload-image']);

  $uploadResultResponse = $client->post($imageUploadPath, [
    'headers' => [
      'token' => $token
    ],
    'multipart' => [
      [
          'name' => 'imageContent',
          'contents' => $imageData,
          'filename' => 'image-filename.jpg',
      ],
      [
          'name' => 'imageAccessibility',
          'contents' => 'PUBLIC',
      ]
    ]
  ]);

  $uploadData = json_decode($uploadResultResponse->getBody(), true);

  $sourceImageId = $uploadData['sourceImageId'];
  $progressEventBusName = $uploadData['progressEventBusName'];
  $resultImages = $uploadData['resultImages'];

Access response data about result images


  $sourceImageId = $uploadData['sourceImageId'];
  $progressEventBusName = $uploadData['progressEventBusName'];
  $resultImages = $uploadData['resultImages'];

  // Store id of source/original image to be able to delete it.
  echo $sourceImageId;

  // Retrieve result images info
  foreach ($resultImages as $resultImage) {
    var_dump($resultImage);
  }

Receive data through web sockets about processing progress:


  $client = new WebSocket\Client($WS_PATH);
  $client->text(json_encode([
    'type' => 'LISTEN',
    'data' => [
       'token' => $token,
       'clientAppID' => $appID,
       'progressEventBusNames' => [$progressEventBusName],
    ]
  ]));

  while(1) {
    try {
      echo $client->receive();
    } catch (ConnectionException $e) {}
  }

Make request to delete all images (source and result images):


  $imageDeletePath = implode("/", [$API_PATH, 'images', $sourceImageId]);
  $deleteResponse = $client->delete($imageDeletePath, [
    'headers' => [
      'token' => $token
    ]
  ]);

  echo "Delete response status:";
  echo $deleteResponse->getStatusCode() . PHP_EOL;

4.3. Python

Installed pip dependencies and predefined constants for this example are:


  import asyncio
  import json

  import requests
  import websockets

  API_HOST = 'localhost'
  API_PATH = ''.join(['http://', API_HOST, ':3000/api'])
  WS_PATH = ''.join(['ws://', API_HOST, ':8080'])

  app_id = 'u61v5mrq5wSOaAXxbYKE'
  secret_id = 'GS50RJ1PI2HeZNEd0usf'

Authorization of Client Application to get token:


  login_path = '/'.join([API_PATH, 'login/client-app'])
  login_response = requests.post(login_path, json={'appID': app_id, 'secretID': secret_id})
  token_data = login_response.json()
  token = token_data['key']

Upload image (loading from local directory):


  image_data = open('../images/original-1.jpg', 'rb')
  image_upload_path = '/'.join([API_PATH, 'images/upload-image'])
  image_upload_response = requests.post(image_upload_path,
                                        files={
                                            'imageContent': ('original-1.jpg', image_data),
                                        },
                                        data={
                                            'imageAccessibility': 'PUBLIC'
                                        },
                                        headers={
                                            'token': token
                                        })

  upload_data = image_upload_response.json()

  source_image_id = upload_data['sourceImageId']
  progress_event_bus_name = upload_data['progressEventBusName']
  result_images = upload_data['resultImages']

Access response data about result images:


  source_image_id = upload_data['sourceImageId']
  progress_event_bus_name = upload_data['progressEventBusName']
  result_images = upload_data['resultImages']

  print(source_image_id)

  for result_image in result_images:
    print(result_image)

Receive data through web sockets about processing progress:


  async def on_message(message):
    print(message)

  async def connect_web_socket():
      async with websockets.connect(WS_PATH) as websocket:
          listen_message = json.dumps({
            'type': 'LISTEN',
            'data': {
              'token': token,
              'clientAppID': app_id,
              'progressEventBusNames': [progress_event_bus_name],
            }
          })
          await websocket.send(listen_message)
          while True:
              asyncio.create_task(on_message(await websocket.recv()))

  asyncio.run(connect_web_socket())

Make request to delete all images (source and result images):


  image_delete_path = '/'.join([API_PATH, 'images', source_image_id])
  delete_response = requests.delete(image_delete_path,
                                    headers={
                                      'token': token
                                    })

  print('Delete response status:')
  print(delete_response.status_code)

4.4. Java

Installed gradle dependencies and predefined constants for this example are:


package com.gradient.imageprocessing.javashowcase;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;

public class App {
    private static final String API_HOST = "localhost";
    private static final String API_PATH = "http://" + API_HOST + ":3000/api";
    private static final String WS_PATH = "ws://" + API_HOST + ":8080";
    private static final String APP_ID = "u61v5mrq5wSOaAXxbYKE";
    private static final String SECRET_ID = "GS50RJ1PI2HeZNEd0usf";
  

Authorization of Client Application to get token:


 HttpClient client = HttpClient.newHttpClient();

 HttpRequest loginRequest = HttpRequest.newBuilder()
                .uri(new URI("/" + API_PATH + "/login/client-app"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(new JSONObject()
                        .append("appID", APP_ID)
                        .append("secretID", SECRET_ID)
                        .toString()))
                .build();

 HttpResponse loginResponse = client.send(loginRequest, HttpResponse.BodyHandlers.ofString());
 JSONObject loginResponseBody = new JSONObject(loginResponse.body());
 String token = loginResponseBody.getString("key");

Upload image (loading from local directory):


  String imageUploadPath = String.join("/", new String[]{API_PATH, "images/upload-image"});

  File file = new File("../../images/original-1.jpg");

  Map<Object, Object> imageUploadRequestBody = new HashMap<>();
  imageUploadRequestBody.put("imageContent", file.toPath());
  imageUploadRequestBody.put("imageAccessibility", "PUBLIC");

  String boundary = new BigInteger(256, new Random()).toString();

  HttpRequest imageUploadRequest = HttpRequest.newBuilder()
          .uri(new URI(imageUploadPath))
          .header("Content-Type", "multipart/form-data;boundary=" + boundary)
          .header("token", token)
          .POST(ofMimeMultipartData(imageUploadRequestBody, boundary))
          .build();

  HttpResponse<String> imageUploadResponse = client.send(imageUploadRequest, HttpResponse.BodyHandlers.ofString());

  JSONObject uploadImageResponseBody = new JSONObject(imageUploadResponse.body());

  String sourceImageId = uploadImageResponseBody.getString("sourceImageId");
  String progressEventBusName = uploadImageResponseBody.getString("progressEventBusName");
  JSONArray resultImages = uploadImageResponseBody.getJSONArray("resultImages");


// Implementation ofMimeMultipartData
// Reference: https://github.com/ralscha/blog2019/blob/master/java11httpclient/client/src/main/java/ch/rasc/httpclient/File.java#L69-L91
  public static HttpRequest.BodyPublisher ofMimeMultipartData(Map<Object, Object> data,
                                                              String boundary) throws IOException {
      var byteArrays = new ArrayList<byte[]>();
      byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=")
              .getBytes(StandardCharsets.UTF_8);
      for (Map.Entry<Object, Object> entry : data.entrySet()) {
          byteArrays.add(separator);

          if (entry.getValue() instanceof Path) {
              var path = (Path) entry.getValue();
              String mimeType = Files.probeContentType(path);
              byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName()
                      + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n")
                      .getBytes(StandardCharsets.UTF_8));
              byteArrays.add(Files.readAllBytes(path));
              byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
          }
          else {
              byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n")
                      .getBytes(StandardCharsets.UTF_8));
          }
      }
      byteArrays.add(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
      return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
  }
  

Access response data about result images:


    String sourceImageId = uploadImageResponseBody.getString("sourceImageId");
    String progressEventBusName = uploadImageResponseBody.getString("progressEventBusName");
    JSONArray resultImages = uploadImageResponseBody.getJSONArray("resultImages");

    System.out.println(sourceImageId);

    for (int i = 0; i < resultImages.length(); i++) {
        JSONObject resultImage = resultImages.getJSONObject(i);
        System.out.println(resultImage);
    }

  

Receive data through web sockets about processing progress:


    JSONArray progressEventBusNames = new JSONArray();
    progressEventBusNames.put(progressEventBusName);

    JSONObject listenData = new JSONObject();
    listenData.put("token", token);
    listenData.put("clientAppID", APP_ID);
    listenData.put("progressEventBusNames", progressEventBusNames);

    JSONObject listenRequest = new JSONObject();
    listenRequest.put("type", "LISTEN");
    listenRequest.put("data", listenData);

    CountDownLatch latch = new CountDownLatch(20); // Set how many messages to receive before closing connection.
    WebSocket ws = HttpClient
            .newHttpClient()
            .newWebSocketBuilder()
            .buildAsync(URI.create(WS_PATH), new WebSocketClient(latch))
            .join();
    ws.sendText(listenRequest.toString(), true);
    latch.await();


    // Reference: https://stackoverflow.com/questions/55380813/require-assistance-with-simple-pure-java-11-websocket-client-example
    private static class WebSocketClient implements WebSocket.Listener {
        private final CountDownLatch latch;

        public WebSocketClient(CountDownLatch latch) { this.latch = latch; }

        @Override
        public void onOpen(WebSocket webSocket) {
            System.out.println("onOpen using subprotocol " + webSocket.getSubprotocol());
            WebSocket.Listener.super.onOpen(webSocket);
        }

        @Override
        public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) {
            System.out.println(data);
            latch.countDown();
            return WebSocket.Listener.super.onText(webSocket, data, last);
        }

        @Override
        public void onError(WebSocket webSocket, Throwable error) {
            System.out.println("Error occurred: " + webSocket.toString());
            WebSocket.Listener.super.onError(webSocket, error);
        }
    }

  

Make request to delete all images (source and result images):


    String imageDeletePath = String.join("/", new String[]{API_PATH, "images", sourceImageId});
    HttpRequest imageDeleteRequest = HttpRequest.newBuilder()
            .uri(new URI(imageDeletePath))
            .header("Content-Type", "application/json")
            .header("token", token)
            .DELETE()
            .build();

    HttpResponse<String> imageDeleteResponse = client.send(imageDeleteRequest, HttpResponse.BodyHandlers.ofString());

    System.out.println("Delete response status:");
    System.out.println(imageDeleteResponse.statusCode());

  

footer-logo

Trg Slobode 16, Tuzla 75000

Bosnia and Herzegovina

info@gradient.ba

Services

  • Software Development Partnership
  • Business Solution Development
  • Software Architecture Design and Consulting

Get Information

  • About us
  • Make an Appointment
  • Contact us

Social

  • Facebook
  • LinkedIn
  • UpWork

Legal

  • Privacy Policy
  • Cookies Policy
  • Terms and Conditions
Copyright © 2022 Gradient.ba