From 4ecc0cb7e8bcfd242b91c01b0a27a42e9193cec3 Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Sat, 15 May 2021 12:01:15 +0200
Subject: [PATCH] backend: add API endpoint for comparing images

---
 backend/Procfile      |  2 +-
 backend/src/image.ts  | 55 ++++++++++++++++++++++++++++++++++++++++
 backend/src/index.ts  | 10 +++++++-
 backend/src/ribbon.ts | 59 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 124 insertions(+), 2 deletions(-)
 create mode 100644 backend/src/image.ts

diff --git a/backend/Procfile b/backend/Procfile
index 225d088..f64d669 100644
--- a/backend/Procfile
+++ b/backend/Procfile
@@ -1 +1 @@
-web: deno run --allow-net src/index.ts --port=${PORT}
+web: deno run --allow-write --allow-read --allow-net src/index.ts --port=${PORT}
diff --git a/backend/src/image.ts b/backend/src/image.ts
new file mode 100644
index 0000000..04f36fd
--- /dev/null
+++ b/backend/src/image.ts
@@ -0,0 +1,55 @@
+import {
+  readAll,
+  readerFromStreamReader,
+} from "https://deno.land/std@0.95.0/io/mod.ts";
+import { decode, Image } from "https://deno.land/x/jpegts@1.1/mod.ts";
+import { resize } from "https://deno.land/x/deno_image@v0.0.3/mod.ts";
+
+export const compareMunicipalRibbon = async (
+  ribbonUrl: string,
+  imageToCompareTo: Image,
+): Promise<number> => {
+  const ribbon = (await fetch(ribbonUrl))?.body?.getReader();
+  if (ribbon) {
+    const reader = readerFromStreamReader(ribbon);
+    const rawImage = await readAll(reader);
+    const picture = decode(
+      await resize(rawImage, {
+        height: imageToCompareTo.height,
+        width: imageToCompareTo.width,
+        aspectRatio: false,
+      }),
+    );
+    return compareImages(picture, imageToCompareTo);
+  }
+
+  throw new Error("Couldn't fetch remote Image!");
+};
+
+const compareImages = (
+  firstImage: Image,
+  secondImage: Image,
+): number => {
+  if (
+    firstImage.width != secondImage.width ||
+    firstImage.height != secondImage.height
+  ) {
+    throw new Error(
+      "Can't compare images of different sizes. " + firstImage.width + "x" +
+        firstImage.height + " vs " + secondImage.width + "x" +
+        secondImage.height,
+    );
+  }
+
+  let diff = 0;
+  for (let i = 0; i < firstImage.data.length / 5; i++) {
+    diff += Math.abs(firstImage.data[4 * i + 0] - secondImage.data[4 * i + 0]) /
+      255;
+    diff += Math.abs(firstImage.data[4 * i + 1] - secondImage.data[4 * i + 1]) /
+      255;
+    diff += Math.abs(firstImage.data[4 * i + 2] - secondImage.data[4 * i + 2]) /
+      255;
+  }
+
+  return 100 - 100 * diff / (firstImage.width * firstImage.height * 3);
+};
diff --git a/backend/src/index.ts b/backend/src/index.ts
index cf0c115..4d73fde 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -1,7 +1,8 @@
 import { Application, Router } from "https://deno.land/x/oak@v7.3.0/mod.ts";
 import { parse } from "https://deno.land/std@0.95.0/flags/mod.ts";
+import logger from "https://deno.land/x/oak_logger@1.0.0/mod.ts";
 
-import { getAllRibbons, getRibbon, Ribbon } from "./ribbon.ts";
+import { compareRibbon, getAllRibbons, getRibbon, Ribbon } from "./ribbon.ts";
 
 const { args } = Deno;
 const DEFAULT_PORT = 8000;
@@ -14,10 +15,17 @@ const state: Ribbon[] = await (await fetch(
 
 const app = new Application({ state });
 
+app.use(logger.logger);
+app.use(logger.responseTime);
+
 const router = new Router();
 
 router.get("/ribbons", getAllRibbons);
 router.get("/ribbons/:municipality", getRibbon);
+router.post(
+  "/ribbons/compare/:municipality",
+  compareRibbon,
+);
 
 app.use(router.routes());
 
diff --git a/backend/src/ribbon.ts b/backend/src/ribbon.ts
index 33f2996..c0e175c 100644
--- a/backend/src/ribbon.ts
+++ b/backend/src/ribbon.ts
@@ -1,4 +1,6 @@
 import { helpers, RouterContext } from "https://deno.land/x/oak@v7.3.0/mod.ts";
+import { compareMunicipalRibbon } from "./image.ts";
+import { decode, Image } from "https://deno.land/x/jpegts@1.1/mod.ts";
 
 export interface Ribbon {
   acceptance: string;
@@ -42,6 +44,7 @@ export const getAllRibbons = (ctx: RouterContext) => {
 export const getRibbon = ({ params, response, state }: RouterContext) => {
   if (params?.municipality === undefined) {
     response.status = 400;
+    return;
   }
   const municipality = params?.municipality;
   const ribbon = state.find(
@@ -56,3 +59,59 @@ export const getRibbon = ({ params, response, state }: RouterContext) => {
   response.status = 200;
   response.body = ribbon;
 };
+
+export const compareRibbon = async (
+  { params, request, response, state }: RouterContext,
+) => {
+  if (params?.municipality === undefined) {
+    response.status = 400;
+    return;
+  }
+  const municipality = params?.municipality;
+  const ribbon: Ribbon = state.find(
+    (ribbon: Ribbon) => ribbon.municipality.endsWith(`/${municipality}`),
+  );
+
+  if (ribbon === undefined) {
+    response.status = 404;
+    response.body = "Couldn't find municipality " + municipality;
+    return;
+  }
+
+  const { value } = request.body({ type: "form-data" });
+  const { files } = await value.read();
+  if (!files) {
+    response.status = 400;
+    return;
+  }
+  const filename = files[0].filename;
+  if (!filename) {
+    response.status = 400;
+    return;
+  }
+
+  let img: Image | undefined = undefined;
+  try {
+    img = decode(
+      await Deno.readFile(filename),
+    );
+  } catch (e) {
+    response.status = 500;
+    response.body = "" + e;
+    return;
+  }
+
+  let matchPercentage = 0;
+  try {
+    matchPercentage = await compareMunicipalRibbon(
+      ribbon.img,
+      img,
+    );
+  } catch (e) {
+    response.status = 500;
+    response.body = "" + e;
+    return;
+  }
+
+  response.body = { matchPercentage: matchPercentage };
+};
-- 
GitLab