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.

MIT License prs welcome

# Demo

# Live View it live at : https://colorpalettedemo.herokuapp.com/ You can deploy your own instance on heroku by clicking the following button [![Deploy](https://www.herokucdn.com/deploy/button.svg)](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

image before processing image after processing

Try It Now

{{form.hidden_tag()}}
{{form.photo(class="image-input")}} image icon
Upload Image
{% for error in form.photo.errors %} {{ error}} {% endfor %}

*Image size must be less than 2Mb

{{form.palette_height(value=6, min=1, max=8)}} {% for error in form.palette_height.errors %} {{ error}} {% endfor %}
{{form.palette_outline_width(class="u-full-width", value=10, min=1, max=40)}} {% for error in form.palette_outline_width.errors %} {{ error}} {% endfor %}
{% endblock %} ================================================ FILE: colorpalette/templates/picture.html ================================================ {% extends "base.html" %} {% block content %}

Try Again?

Palette with hex codes

{% 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)