Skip to main content

Merge pull request #13 from alexwlchan/webapp

ID
1e6a8be
date
2022-05-04 21:41:20+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
977c9a8, 09c01c7
message
Merge pull request #13 from alexwlchan/webapp

Add the web app
changed files
9 files, 327 additions

Changed files

webapp/requirements.in (0) → webapp/requirements.in (35)

diff --git a/webapp/requirements.in b/webapp/requirements.in
new file mode 100644
index 0000000..f4b778f
--- /dev/null
+++ b/webapp/requirements.in
@@ -0,0 +1,3 @@
+flask
+gunicorn
+wcag_contrast_ratio

webapp/requirements.txt (0) → webapp/requirements.txt (580)

diff --git a/webapp/requirements.txt b/webapp/requirements.txt
new file mode 100644
index 0000000..8b580ee
--- /dev/null
+++ b/webapp/requirements.txt
@@ -0,0 +1,29 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+#    pip-compile
+#
+click==8.1.3
+    # via flask
+flask==2.1.2
+    # via -r requirements.in
+gunicorn==20.1.0
+    # via -r requirements.in
+importlib-metadata==4.11.3
+    # via flask
+itsdangerous==2.1.2
+    # via flask
+jinja2==3.1.2
+    # via flask
+markupsafe==2.1.1
+    # via jinja2
+wcag-contrast-ratio==0.9
+    # via -r requirements.in
+werkzeug==2.1.2
+    # via flask
+zipp==3.8.0
+    # via importlib-metadata
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools

webapp/server.py (0) → webapp/server.py (2403)

diff --git a/webapp/server.py b/webapp/server.py
new file mode 100755
index 0000000..98fda22
--- /dev/null
+++ b/webapp/server.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+
+import base64
+import colorsys
+import os
+import subprocess
+import tempfile
+
+from flask import Flask, render_template, request
+import wcag_contrast_ratio as contrast
+
+
+app = Flask(__name__)
+
+
+VERSION = subprocess.check_output(["dominant_colours", "--version"]).decode("utf8")
+
+
+@app.route("/")
+def index():
+    return render_template("index.html", version=VERSION)
+
+
+@app.template_filter("foreground_colour")
+def foreground_colour(hex_string):
+    red = int(hex_string[1:3], 16)
+    green = int(hex_string[3:5], 16)
+    blue = int(hex_string[5:7], 16)
+
+    ratio = contrast.rgb((red / 255, green / 255, blue / 255), (0, 0, 0))
+
+    if contrast.passes_AA(ratio):
+        return "#000000"
+    else:
+        return "#FFFFFF"
+
+
+@app.route("/palette", methods=["POST"])
+def get_palette():
+    if request.method == "POST":
+        uploaded_file = request.files["file"]
+        _, extension = os.path.splitext(uploaded_file.filename)
+
+        with tempfile.NamedTemporaryFile(suffix=extension) as tmp_file:
+            uploaded_file.save(tmp_file)
+
+            # If we don't flush here, the file may be incomplete.  This can
+            # lead to errors like:
+            #
+            #     failed to fill whole buffer
+            #
+            # when running dominant_colours.
+            tmp_file.flush()
+
+            result = subprocess.check_output(
+                ["dominant_colours", tmp_file.name, "--no-palette", "--max-colours=5"]
+            )
+            colours = result.decode("utf8").strip().split("\n")
+
+            with tempfile.NamedTemporaryFile(suffix="jpg") as thumbnail_file:
+                subprocess.check_call(
+                    [
+                        "convert",
+                        tmp_file.name,
+                        "-resize",
+                        "600x600",
+                        thumbnail_file.name,
+                    ]
+                )
+                thumbnail_file.seek(0)
+                thumbnail = thumbnail_file.read()
+
+            thumbnail_data_uri = (
+                b"data:image/jpg;base64," + base64.b64encode(thumbnail)
+            ).decode("ascii")
+
+            return render_template(
+                "palette.html",
+                colours=colours,
+                thumbnail_data_uri=thumbnail_data_uri,
+                version=VERSION,
+            )
+
+
+if __name__ == "__main__":
+    app.run(debug=True, host="0.0.0.0")

webapp/start.sh (0) → webapp/start.sh (668)

diff --git a/webapp/start.sh b/webapp/start.sh
new file mode 100755
index 0000000..e2d57ba
--- /dev/null
+++ b/webapp/start.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o nounset
+
+pip3 install -r requirements.txt
+
+DOWNLOAD_URL=$(curl --silent 'https://api.github.com/repos/alexwlchan/dominant_colours/releases/latest' \
+  | jq -r ' .assets | map(.browser_download_url) | map(select(test(".*linux.*")))[0]'
+)
+
+# The --location flag means we follow redirects
+curl --location "$DOWNLOAD_URL" > ~/.cargo/bin/dominant_colours.tar.gz
+tar -xzf ~/.cargo/bin/dominant_colours.tar.gz
+
+mv dominant_colours /usr/local/bin/dominant_colours
+chmod +x /usr/local/bin/dominant_colours
+dominant_colours --version
+
+if [[ "$DEBUG" == "yes" ]]
+then
+  python3 server.py
+else
+  gunicorn server:app -w 4 --log-file -
+fi

webapp/static/45degreee_fabric.png (0) → webapp/static/45degreee_fabric.png (153950)

diff --git a/webapp/static/45degreee_fabric.png b/webapp/static/45degreee_fabric.png
new file mode 100644
index 0000000..3c7fa3f
Binary files /dev/null and b/webapp/static/45degreee_fabric.png differ

webapp/static/style.css (0) → webapp/static/style.css (2097)

diff --git a/webapp/static/style.css b/webapp/static/style.css
new file mode 100644
index 0000000..5c7b01a
--- /dev/null
+++ b/webapp/static/style.css
@@ -0,0 +1,132 @@
+body {
+  margin:  0;
+  padding: 0;
+
+  /* Pattern from https://www.toptal.com/designers/subtlepatterns/awesome-pattern/ */
+  background-image: url('/static/45degreee_fabric.png');
+}
+
+main, footer {
+  max-width: 500px;
+  margin-left:  auto;
+  margin-right: auto;
+  text-align: center;
+  padding: 5px;
+
+  font: 14pt Avenir, Arial, sans-serif;
+  color: #333;
+}
+
+footer {
+  font-size: 9pt;
+  color: #aaa;
+}
+
+a, a:visited {
+  color: #555;
+}
+
+footer a, footer a:visited {
+  color: #888;
+}
+
+a:hover {
+  background: #ccc;
+}
+
+#results {
+  display: grid;
+  grid-template-columns: 325px 70px auto;
+  grid-template-rows: repeat(5, 56px);
+  grid-gap: 5px;
+  height: 300px;
+
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.thumbnail {
+  grid-row: 1 / span 5;
+  grid-column: 1 / 3;
+  width:  300px;
+  height: 300px;
+}
+
+img {
+  width:  100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.sample {
+  grid-column: 1 / 2;
+}
+
+.label {
+  font-family: monospace;
+  height:      56px;
+  line-height: 56px;
+  text-align: left;
+}
+
+.sample {
+  width:  56px;
+  height: 56px;
+  grid-column: 2 / 3;
+}
+
+.label {
+  grid-column: 3 / 3;
+}
+
+#label_1, #sample_1 { grid-row: 1 / 5; }
+#label_2, #sample_2 { grid-row: 2 / 5; }
+#label_3, #sample_3 { grid-row: 3 / 5; }
+#label_4, #sample_4 { grid-row: 4 / 5; }
+#label_5, #sample_5 { grid-row: 5 / 5; }
+
+@media screen and (max-width: 600px) {
+  .thumbnail {
+    width:  300px;
+    height: 230px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  #results {
+    grid-template-columns: 42px auto;
+    width: 300px;
+    grid-template-rows: 240px repeat(5, 42px);
+    grid-row-gap: 10px;
+    height: auto;
+  }
+
+  .thumbnail {
+    grid-column: 1 / span 2;
+    grid-row:1 / 6;
+  }
+
+  .sample {
+    grid-column: 1 / 2;
+  }
+
+  .label {
+    grid-column: 2 / 2;
+  }
+
+  #label_1, #sample_1 { grid-row: 2 / 6; }
+  #label_2, #sample_2 { grid-row: 3 / 6; }
+  #label_3, #sample_3 { grid-row: 4 / 6; }
+  #label_4, #sample_4 { grid-row: 5 / 6; }
+  #label_5, #sample_5 { grid-row: 6 / 6; }
+
+  .label {
+    height:      42px;
+    line-height: 42px;
+  }
+
+  .sample {
+    width:  42px;
+    height: 42px;
+  }
+}

webapp/templates/base.html (0) → webapp/templates/base.html (734)

diff --git a/webapp/templates/base.html b/webapp/templates/base.html
new file mode 100644
index 0000000..9015714
--- /dev/null
+++ b/webapp/templates/base.html
@@ -0,0 +1,26 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+
+    <link rel="stylesheet" href="/static/style.css">
+
+    <title>dominant colours</title>
+  </head>
+  <body>
+    <main>
+      <h2>find the dominant colours in an image</h2>
+      {% block content %}
+      {% endblock %}
+    </main>
+    <footer>
+        made with <span class="heart">&#x2764;</span> by <a href="https://alexwlchan.net">alexwlchan</a>
+        ·
+        {{ version }}
+        ·
+        code on <a href="https://github.com/alexwlchan/dominant_colours/tree/main/webapp">github</a>
+      </div>
+    </footer>
+  </body>
+</html>
\ No newline at end of file

webapp/templates/index.html (0) → webapp/templates/index.html (282)

diff --git a/webapp/templates/index.html b/webapp/templates/index.html
new file mode 100644
index 0000000..82c6be1
--- /dev/null
+++ b/webapp/templates/index.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block content %}
+  <p>upload an image to analyse:</p>
+
+  <form action="/palette" method="POST"
+     enctype="multipart/form-data">
+     <input type="file" name="file" accept=".gif,.jpg,.jpeg,.png"/>
+     <input type="submit"/>
+  </form>
+{% endblock %}

webapp/templates/palette.html (0) → webapp/templates/palette.html (416)

diff --git a/webapp/templates/palette.html b/webapp/templates/palette.html
new file mode 100644
index 0000000..85ec9dc
--- /dev/null
+++ b/webapp/templates/palette.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block content %}
+  <div id="results">
+    <div class="thumbnail">
+      <img src="{{ thumbnail_data_uri }}">
+    </div>
+    {% for c in colours %}
+    <div class="sample" id="sample_{{ loop.index }}" style="background: {{ c }};"></div>
+    <div class="label" id="label_{{ loop.index }}">{{ c }}</div>
+    {% endfor %}
+  </div>
+
+  <p><a href="/">try another image</a></p>
+{% endblock %}