Repository: makkoncept/colorpalette
Branch: master
Commit: 0defd21205c0
Files: 19
Total size: 21.4 KB
Directory structure:
gitextract_53ta_i2o/
├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── app.json
├── colorpalette/
│ ├── __init__.py
│ ├── color.py
│ ├── forms.py
│ ├── routes.py
│ ├── static/
│ │ ├── css/
│ │ │ └── style.css
│ │ ├── images/
│ │ │ └── favicon/
│ │ │ └── site.webmanifest
│ │ └── js/
│ │ └── script.js
│ └── templates/
│ ├── 413.html
│ ├── base.html
│ ├── index.html
│ └── picture.html
├── config.py
├── requirements.txt
└── run.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
venv/
venv
__pycache__/
colorpalette/__pycache__
.idea
*.pyc
.vscode/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Mayank Nader
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Procfile
================================================
web: gunicorn colorpalette:app
================================================
FILE: README.md
================================================
ColorPalette
A simple web app to extract dominant colors from an image.
# Demo
# Live
View it live at : https://colorpalettedemo.herokuapp.com/
You can deploy your own instance on heroku by clicking the following button
[](https://heroku.com/deploy)
## What it do
Extracts 10 dominating colors from the image and adds the palette to the bottom of the image (inspired by [colorpalette.cinema](https://www.instagram.com/colorpalette.cinema/?hl=en)). You can adjust the palette height, outline color, and width to match your image dimension. Another independent image of the palette with the hex codes of colors is also generated.
The app does not depend on any APIs for extracting colors. `color.py` contains the code for it with [Pillow](https://pillow.readthedocs.io/en/stable/) as the only dependency.
## Some samples
1. Photo by Luka Odak on Unsplash
2. Photo by Alexandre Chambon on Unsplash
3. Photo by Federica Diliberto on Unsplash
4. Photo by Ashim D’Silva on Unsplash
## Directory Structure
|-- Procfile
|-- README.md
|-- app.json
|-- colorpalette
| |-- __init__.py
| |-- color.py
| |-- forms.py
| |-- routes.py
| |-- static
| | |-- css
| | |-- font
| | |-- images
| | | |-- favicon
| | `-- js
| `-- templates
| |-- 413.html
| |-- base.html
| |-- index.html
| `-- picture.html
|-- config.py
|-- requirements.txt
|-- run.py
`-- LICENSE
## Run locally
1. Clone the repostory:
```
git clone https://github.com/makkoncept/colorpalette.git
```
2. Navigate to the project root and create a virtual environment:
```
python3 -m venv venv
```
3. Activate the virtual environment:
Linux:
```
source venv/bin/activate
```
Windows cmd:
```
venv\Scripts\activate.bat
```
4. Install requirements:
```
pip install -r requirements.txt
```
5. Run the development server:
```
python run.py
```
6. View on [localhost:5000](http://127.0.0.1:5000)
## Attribution
Icon made by Freepik from www.flaticon.com
================================================
FILE: app.json
================================================
{
"name": "ColorPalette",
"description": "Extracts dominating colors from an image and present them as a palette.",
"repository": "https://github.com/makkoncept/colorpalette",
"logo": "https://raw.githubusercontent.com/makkoncept/colorpalette/master/.readme_assets/color-palette.png",
"keywords": ["Flask", "Python", "Colors"]
}
================================================
FILE: colorpalette/__init__.py
================================================
from flask import Flask
from dotenv import load_dotenv, find_dotenv
from config import Config
load_dotenv(find_dotenv())
app = Flask(__name__)
app.config.from_object(Config)
from colorpalette import routes
================================================
FILE: colorpalette/color.py
================================================
from PIL import Image, ImageDraw, ImageFont
from webcolors import rgb_to_hex
import os
def get_dominant_colors(infile):
image = Image.open(infile)
small_image = image.resize((80, 80))
result = small_image.convert(
"P", palette=Image.ADAPTIVE, colors=10
) # image with only 10 dominating colors
# Find dominant colors
palette = result.getpalette()
color_counts = sorted(result.getcolors(), reverse=True)
colors = list()
for i in range(10):
palette_index = color_counts[i][1]
dominant_color = palette[palette_index * 3 : palette_index * 3 + 3]
colors.append(tuple(dominant_color))
# print(colors)
return colors
def process_uploaded_image(
infile, outline_width, pallete_division_factor, outline_color, numcolors=10
):
# width of the pallete that will be an independent image itself
independent_pallete_width = 100
original_image = Image.open(infile)
width, height = original_image.size
# height for the pallete that will be pasted under the image
img_palette_height = int(height / pallete_division_factor)
img_palette_width = width / 10
processed_image = Image.new(
"RGB", (width, height + img_palette_height)
) # blank canvas(original image + palette)
pallete_under_image = Image.new("RGB", (width, img_palette_height))
# blank canvas for pallete.
independent_pallete = Image.new(
"RGB",
(numcolors * independent_pallete_width, independent_pallete_width + 20),
color="rgb(255, 255, 255)",
)
draw = ImageDraw.Draw(pallete_under_image)
draw2 = ImageDraw.Draw(independent_pallete)
posx = 0
posx2 = 0
fonts_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"colorpalette/static/font/Roboto-Bold.ttf",
)
# create font object with the font file
font = ImageFont.truetype(fonts_path, size=16)
colors = get_dominant_colors(infile)
# making the palettes
for color in colors:
draw.rectangle(
[posx, 0, posx + img_palette_width, img_palette_height],
fill=color,
width=outline_width,
outline=outline_color,
)
# drawing one pallete at a time on independent pallete canvas
draw2.rectangle(
[posx2, 0, posx2 + independent_pallete_width, independent_pallete_width],
fill=color,
)
# write the hex code under the pallete
draw2.text(
(posx2 + 20, independent_pallete_width),
rgb_to_hex(color[:3]),
fill="rgb(0, 0, 0)",
font=font,
)
# move the pointer to the beginning of next pallete color
posx = posx + img_palette_width
posx2 = posx2 + independent_pallete_width
del draw
del draw2
box = (0, height, width, height + img_palette_height)
# pasting image and palette on the canvas
processed_image.paste(original_image)
processed_image.paste(pallete_under_image, box)
return processed_image, independent_pallete
================================================
FILE: colorpalette/forms.py
================================================
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf.html5 import NumberInput
from wtforms import IntegerField
from wtforms.validators import NumberRange
class PhotoForm(FlaskForm):
photo = FileField(
"Upload Image", validators=[FileRequired(), FileAllowed(["jpg", "png", "jpeg"])]
)
palette_height = IntegerField(
"Palette Height", validators=[NumberRange(1, 8)], widget=NumberInput()
)
palette_outline_width = IntegerField(
"Palette Outline Width", validators=[NumberRange(1, 40)], widget=NumberInput()
)
================================================
FILE: colorpalette/routes.py
================================================
from colorpalette import app
from colorpalette.color import process_uploaded_image
from colorpalette.forms import PhotoForm
from flask import render_template, redirect, url_for, request, session
from werkzeug.utils import secure_filename
from webcolors import hex_to_rgb
import os
import uuid
@app.route("/", methods=["GET", "POST"])
def index():
form = PhotoForm()
if form.validate_on_submit():
image = form.photo.data
filename = secure_filename(image.filename) # sanitize image name
_, ext = os.path.splitext(filename)
new_filename = uuid.uuid4().hex + ext # creating a random name
image_with_palette, pallete = process_uploaded_image(
image,
pallete_division_factor=11 - form.palette_height.data,
outline_width=form.palette_outline_width.data,
outline_color=hex_to_rgb(request.form.get("palette_outline_color")),
)
image_with_pallete_path = os.path.join(
app.root_path, "static/images", new_filename
)
pallete_path = os.path.join(
app.root_path, "static/images", "pal" + new_filename
)
# saving image and pallete
image_with_palette.save(image_with_pallete_path)
pallete.save(pallete_path)
return redirect(
url_for(
"picture",
name=new_filename,
)
)
return render_template("index.html", form=form, src="default")
@app.route("/picture/")
def picture(name):
processed_img_relative_path = url_for("static", filename="images/" + name)
pallete_relative_path = url_for("static", filename="images/" + "pal" + name)
return render_template(
"picture.html",
src=processed_img_relative_path,
src2=pallete_relative_path,
)
@app.errorhandler(413)
def error413(e):
return render_template("413.html"), 413
================================================
FILE: colorpalette/static/css/style.css
================================================
*,
*::before,
*::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--primary-color: #ea538c;
--primary-color-light: #ffcde0;
}
body {
background-color: rgb(245, 245, 245);
color: #0c0c07;
font-weight: 500;
font-family: "Raleway", sans-serif;
}
h1 {
font-size: 3.3rem;
font-weight: 400;
}
h2 {
font-size: 2.5rem;
font-weight: 400;
}
.container {
padding: 2rem 5rem;
}
.image-showcase {
display: flex;
padding: 2rem 0 4rem 0;
justify-content: space-evenly;
align-items: center;
}
form {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
width: 90%;
margin: 0 auto;
}
.image-upload-wrapper {
margin: 1.5rem 0 3rem 0;
height: 11rem;
position: relative;
width: 100%;
}
.image-upload {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
border: 4px dashed var(--primary-color);
border-radius: 0.5rem;
transition: background-color 0.25s ease-out;
}
.image-upload:hover {
background-color: var(--primary-color-light);
}
.image-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
outline: none;
cursor: pointer;
}
.image-upload-wrapper .undertext {
font-size: 0.9rem;
color: #991c19;
float: right;
}
.number-input {
width: 100%;
display: flex;
align-items: center;
padding-bottom: 1rem;
}
.number-input label {
display: inline-block;
width: 50%;
padding-right: 2rem;
font-size: 1.3rem;
text-align: right;
}
.number-input input[type="number"] {
width: 30%;
font-size: 2rem;
font-weight: 400;
padding-left: 0.6rem;
border-radius: 2px;
border: 2px solid rgb(192, 192, 192);
}
.submit-button {
color: #fff;
background-color: var(--primary-color);
border-color: var(--primary-color);
display: inline-block;
height: 2.5rem;
padding: 0 30px;
text-align: center;
font-size: 1rem;
font-weight: 600;
line-height: 38px;
letter-spacing: 0.1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
}
/* pictures.html */
.image-wrapper {
width: 70%;
margin: auto;
position: relative;
}
.image-wrapper img {
display: block;
margin: auto;
max-width: 100%;
max-height: 100%;
}
.download-image-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
opacity: 0;
color: #fff;
background-color: var(--primary-color);
font-size: 1.1rem;
font-weight: 500;
line-height: 1.1;
text-transform: capitalize;
text-decoration: none !important;
text-align: center;
border: 0 none;
border-radius: 4px;
padding: 12px 12px;
cursor: pointer;
user-select: none;
}
.download-image-button:hover {
color: #fff;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
transition: background 0.5s ease;
}
.image-wrapper:hover .overlay {
display: block;
background: rgba(0, 0, 0, 0.3);
}
.image-wrapper:hover .download-image-button {
opacity: 1;
}
/* github fork ribbon : https://github.com/simonwhitaker/github-fork-ribbon-css */
.github-fork-ribbon::before {
background-color: #333;
}
/* utils */
.text-align-center {
text-align: center;
}
.margin-bottom-large {
margin-bottom: 4rem;
}
.margin-bottom-normal {
margin-bottom: 2rem;
}
.margin-top-large {
margin-top: 4rem;
}
================================================
FILE: colorpalette/static/images/favicon/site.webmanifest
================================================
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
================================================
FILE: colorpalette/static/js/script.js
================================================
const imageUpload = document.querySelector(".image-upload");
const imageInput = document.querySelector(".image-input");
const imageNameDiv = document.querySelector(".image-name");
const legalFileTypes = ["image/png", "image/jpg", "image/jpeg"];
imageUpload.addEventListener("change", handleImageUpload);
function handleImageUpload(event) {
console.log(event.target.files[0]);
const FileSize = event.target.files[0].size / 1024 / 1024; // in MB
const fileType = event.target.files[0].type;
if (FileSize > 2) {
alert("File size exceeds 2 MB");
} else if (!legalFileTypes.includes(fileType)) {
alert("Only file of type 'jpeg', 'jpg', or 'png' allowed");
} else {
const imageName = event.target.files[0].name;
imageNameDiv.innerText = imageName;
}
}
================================================
FILE: colorpalette/templates/413.html
================================================
{% extends "base.html" %} {% block content %}
Your image is too large. Please upload a smaller image.
{% endblock %}
================================================
FILE: colorpalette/templates/base.html
================================================
ColorPalette
Fork me on GitHub
{% block content %}{% endblock %}
================================================
FILE: colorpalette/templates/index.html
================================================
{% extends "base.html" %} {% block content %}
Color Palette
Extracts dominating colors from your image
Try It Now
{% endblock %}
================================================
FILE: colorpalette/templates/picture.html
================================================
{% extends "base.html" %} {% block content %}
{% endblock %}
================================================
FILE: config.py
================================================
import os
class Config(object):
SECRET_KEY = os.environ.get("SECRET_KEY") or "N6q067SQG7894Yw28d85BqWu4"
MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # image size upload limit (2 MB)
================================================
FILE: requirements.txt
================================================
Click==7.0
Flask==1.0.2
Flask-WTF==0.14.2
gunicorn==19.9.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
Pillow==7.2.0
webcolors==1.8.1
Werkzeug==0.15.5
WTForms==2.2.1
python-dotenv==0.14.0
================================================
FILE: run.py
================================================
from colorpalette import app
if __name__ == "__main__":
app.run(debug=True)