Images processor with cli and json flask server.
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# MacOS files
|
||||||
|
.DS_Store
|
||||||
|
images
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
BIN
output/5d1f390486bf42548051ea74353277d5_lg.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
output/5d1f390486bf42548051ea74353277d5_md.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
output/5d1f390486bf42548051ea74353277d5_sm.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
output/5d1f390486bf42548051ea74353277d5_thumb.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
output/629220597dfb462ab5cf18fa9ea6a592_lg.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
output/629220597dfb462ab5cf18fa9ea6a592_md.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
output/629220597dfb462ab5cf18fa9ea6a592_sm.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
output/629220597dfb462ab5cf18fa9ea6a592_thumb.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
output/755071b1433e4bfb97e46266db2cd723_lg.jpg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
output/755071b1433e4bfb97e46266db2cd723_md.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
output/755071b1433e4bfb97e46266db2cd723_sm.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
output/755071b1433e4bfb97e46266db2cd723_thumb.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
output/7b8a991a427a4f0c8a446c0afa13ee91_lg.jpg
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
output/7b8a991a427a4f0c8a446c0afa13ee91_md.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
output/7b8a991a427a4f0c8a446c0afa13ee91_sm.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
output/7b8a991a427a4f0c8a446c0afa13ee91_thumb.jpg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
82
processor.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import glob
|
||||||
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
import concurrent.futures
|
||||||
|
import argparse
|
||||||
|
import fileinput
|
||||||
|
import uuid
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
IMAGE_TYPES = ['.png', '.jpg', '.jpeg', '.JPG', '.PNG']
|
||||||
|
OUTPUT_EXTENSION = 'jpg'
|
||||||
|
OUTPUT_FALLBACK = os.path.dirname(__file__)
|
||||||
|
OUTPUT_SIZES = [
|
||||||
|
{ 'dimensions': (250, 250), 'name': 'thumb', 'crop': True },
|
||||||
|
{ 'dimensions': (650, 650), 'name': 'sm', 'crop': False },
|
||||||
|
{ 'dimensions': (1200, 1200), 'name': 'md', 'crop': False },
|
||||||
|
{ 'dimensions': (1800, 1800), 'name': 'lg', 'crop': False }]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def processImage(file, outputPath=None):
|
||||||
|
outputPath = args.output if 'args.output' in globals() else os.path.join(OUTPUT_FALLBACK, 'output')
|
||||||
|
print('outputpath', outputPath)
|
||||||
|
image = Image.open(file)
|
||||||
|
fileID = uuid.uuid4().hex
|
||||||
|
|
||||||
|
for size in OUTPUT_SIZES:
|
||||||
|
temp = image.copy()
|
||||||
|
|
||||||
|
if size['crop']:
|
||||||
|
temp = temp.crop(squareDimensions(temp.size))
|
||||||
|
|
||||||
|
temp.thumbnail(size['dimensions'], Image.LANCZOS)
|
||||||
|
|
||||||
|
filename = generateFilename(fileID, size['name'], outputPath)
|
||||||
|
temp.save(filename)
|
||||||
|
|
||||||
|
return '.'.join([fileID, OUTPUT_EXTENSION])
|
||||||
|
|
||||||
|
def generateFilename(fileID, modifier, outputPath):
|
||||||
|
filename = "{}_{}.{}".format(fileID, modifier, OUTPUT_EXTENSION)
|
||||||
|
return os.path.join(outputPath, filename)
|
||||||
|
|
||||||
|
def squareDimensions(dimensions):
|
||||||
|
(width, height) = dimensions
|
||||||
|
|
||||||
|
if width > height:
|
||||||
|
delta = width - height
|
||||||
|
left = int(delta/2)
|
||||||
|
upper = 0
|
||||||
|
right = height + left
|
||||||
|
lower = height
|
||||||
|
else:
|
||||||
|
delta = height - width
|
||||||
|
left = 0
|
||||||
|
upper = int(delta/2)
|
||||||
|
right = width
|
||||||
|
lower = width + upper
|
||||||
|
|
||||||
|
return (left, upper, right, lower)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description='Process some images')
|
||||||
|
parser.add_argument('files', metavar="files", type=str, help='Directory of images to process')
|
||||||
|
parser.add_argument('--output', metavar="DIR", help="Output directory")
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
pass
|
||||||
|
|
||||||
|
args = Args()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Create a pool of processes. By default, one is created for each CPU in your machine.
|
||||||
|
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||||
|
# Get a list of files to process
|
||||||
|
image_files = glob.glob('{}/*'.format(args.files))
|
||||||
|
|
||||||
|
print('Processing and generating images in following sizes: {}'.format(OUTPUT_SIZES))
|
||||||
|
# Process the list of files, but split the work across the process pool to use all CPUs!
|
||||||
|
for image_file, output_file in zip(image_files, executor.map(processImage, image_files)):
|
||||||
|
print(f"Processed image {image_file} and save as {output_file}")
|
||||||
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Click==7.0
|
||||||
|
Flask==1.0.2
|
||||||
|
itsdangerous==1.1.0
|
||||||
|
Jinja2==2.10
|
||||||
|
MarkupSafe==1.1.0
|
||||||
|
Pillow==5.4.1
|
||||||
|
Werkzeug==0.14.1
|
||||||
57
server.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from io import BytesIO
|
||||||
|
import os
|
||||||
|
|
||||||
|
from processor import processImage
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
OUTPUT_PATH = 'thumbnails/'
|
||||||
|
|
||||||
|
class InvalidFiletype(Exception):
|
||||||
|
status_code = 400
|
||||||
|
|
||||||
|
def __init__(self, message, status_code=None, payload=None):
|
||||||
|
Exception.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
if status_code is not None:
|
||||||
|
self.status_code = status_code
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
rv = dict(self.payload or ())
|
||||||
|
rv['message'] = self.message
|
||||||
|
return rv
|
||||||
|
|
||||||
|
@app.errorhandler(InvalidFiletype)
|
||||||
|
def handle_invalid_filetype(error):
|
||||||
|
response = jsonify(error.to_dict())
|
||||||
|
response.status_code = error.status_code
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route("/upload", methods=["POST"])
|
||||||
|
def upload():
|
||||||
|
print('Received uploads')
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
for upload in request.files.getlist('images'):
|
||||||
|
filename = upload.filename
|
||||||
|
print('processing file: ', filename)
|
||||||
|
|
||||||
|
ext = os.path.splitext(filename)[1][1:].strip().lower()
|
||||||
|
if ext in set(['jpg', 'jpfg', 'png']):
|
||||||
|
print('File supported moving on.')
|
||||||
|
else:
|
||||||
|
raise InvalidFiletype('Unsupported file type {}'.format(ext), status_code=415)
|
||||||
|
|
||||||
|
imageInBytes = BytesIO(upload.read())
|
||||||
|
outputFilename = processImage(imageInBytes, OUTPUT_PATH)
|
||||||
|
outputs.append(outputFilename)
|
||||||
|
|
||||||
|
response = jsonify({ 'filenames': outputs })
|
||||||
|
response.status_code = 200
|
||||||
|
|
||||||
|
# print(uploaded_files)
|
||||||
|
return response
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(port=5001)
|
||||||