├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── app.yaml ├── appengine_config.py ├── core.py ├── requirements.txt ├── static ├── images │ └── blend.jpg ├── jumbotron-narrow.css └── style.css ├── templates ├── error.html ├── index.html └── processing.html └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled python files 2 | *.pyc 3 | 4 | # text and pdf files 5 | *.pdf 6 | *.txt 7 | 8 | # images 9 | static/images/* 10 | 11 | # except stock photo 12 | !static/images/blend.jpg 13 | 14 | # Created by https://www.gitignore.io/api/pycharm 15 | 16 | ### PyCharm ### 17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 18 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 19 | 20 | # User-specific stuff: 21 | .idea/**/workspace.xml 22 | .idea/**/tasks.xml 23 | .idea/dictionaries 24 | 25 | # Sensitive or high-churn files: 26 | .idea/**/dataSources/ 27 | .idea/**/dataSources.ids 28 | .idea/**/dataSources.xml 29 | .idea/**/dataSources.local.xml 30 | .idea/**/sqlDataSources.xml 31 | .idea/**/dynamic.xml 32 | .idea/**/uiDesigner.xml 33 | 34 | # Gradle: 35 | .idea/**/gradle.xml 36 | .idea/**/libraries 37 | 38 | # CMake 39 | cmake-build-debug/ 40 | 41 | # Mongo Explorer plugin: 42 | .idea/**/mongoSettings.xml 43 | 44 | ## File-based project format: 45 | *.iws 46 | 47 | ## Plugin-specific files: 48 | 49 | # IntelliJ 50 | /out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | ### PyCharm Patch ### 68 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 69 | 70 | # *.iml 71 | # modules.xml 72 | # .idea/misc.xml 73 | # *.ipr 74 | 75 | # Sonarlint plugin 76 | .idea/sonarlint 77 | 78 | # End of https://www.gitignore.io/api/pycharm 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 gxercavins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image API 2 | Image Processing API written in Python, using the Pillow library for image manipulation and exposing the functions with the Flask framework. The API has been tested with jpg, png and bmp formats and is able to flip, rotate and crop an image, as well as blending two images, either RGB or gray scale. 3 | 4 | For a live demo, please visit this [link](https://image-demo-dot-gxt-proj1.appspot.com/). The App has been deployed to the Google Cloud Platform using App Engine. 5 | 6 | ## Getting started 7 | The API includes three Python files: 8 | * `core.py`: includes the basic calls of the API. Run the file and use `GET` requests on `localhost:5000`. For more details please refer to the documentation section in this file. 9 | * `app.py`: a web application to test the functionality that serves as a proof of concept. Run it, navigate to `localhost:5000` and follow the instructions. For more details please refer to the documentation section in this file. 10 | * `test.py`: a file to test API requests by checking the received http status codes. `core.py` needs to be running. 11 | 12 | Other files have been included for GCP deployment: `app.yaml`, `appengine_config.py` and `requirements.txt`. 13 | 14 | ## Dependencies 15 | Python installation needs the `PIL` library (image processing), `flask` with its dependencies (`werkzeug`, `jinja2`, `markupsafe`, `itsdangerous`), testing libraries (`unittest` and `requests`) and `gunicorn` to provide an entrypoint for the live deployment. It is recommended to use the provided `requirements.txt` file: 16 | ``` 17 | sudo pip install -r requirements.txt 18 | ``` 19 | 20 | ## Documentation 21 | The different calls can be interfaced with `GET` methods. All images must be located in the `static/images` folder, or otherwise specify the relative path, from that folder, in `filename` parameter. If the request is correct, the modified image will be returned. The syntaxes and an example for each function are described herein. 22 | 23 | ### Flip 24 | ``` http 25 | GET /flip// 26 | ``` 27 | where `mode` can either be `vertical` or `horizontal` and `filename` is the image file name, including extension and relative to the images folder. Browser input example: 28 | ``` 29 | http://127.0.0.1:5000/flip/vertical/minimalistic-coca-cola_00411260.jpg 30 | ``` 31 | ![flip](https://user-images.githubusercontent.com/29493411/27295171-3b04a502-551c-11e7-82b1-9283f49a050d.PNG) 32 | 33 | ### Rotate 34 | ``` http 35 | GET /rotate// 36 | ``` 37 | where `angle` can take any value between 0 and 359 degrees. A positive value indicates clockwise rotation, whereas a negative one indicates counter-clockwise rotation. `filename` is the image file name, including extension and relative to the images folder. Browser input example: 38 | ``` 39 | http://127.0.0.1:5000/rotate/30/Star-War-l.jpg 40 | ``` 41 | ![rotate](https://user-images.githubusercontent.com/29493411/27295173-3b07127e-551c-11e7-89e6-d76a4fee731e.PNG) 42 | 43 | ### Crop 44 | ``` http 45 | GET /crop///// 46 | ``` 47 | with the start and stop point coordinates, (`x1, y1`) and (`x2, y2`), respectively. `filename` is the image file name, including extension and relative to the images folder. Browser input example: 48 | ``` 49 | http://127.0.0.1:5000/crop/150/250/350/500/The_Scream.jpg 50 | ``` 51 | ![crop](https://user-images.githubusercontent.com/29493411/27295172-3b06dade-551c-11e7-9b92-0ae0c20d5981.PNG) 52 | 53 | ### Blend 54 | ``` http 55 | GET /blend/// 56 | ``` 57 | where `alpha`, in % (between 0 and 100), is the weight of the first image in the blend. `filename1` and `filename2` specify the images to blend. If one of them is in gray scale, the other one will be converted automatically. Antialias resizing is also done behind the curtains. Browser input example: 58 | ``` 59 | http://127.0.0.1:5000/blend/50/3x1gKAL.png/blend.jpg 60 | ``` 61 | ![blend](https://user-images.githubusercontent.com/29493411/27295174-3b09945e-551c-11e7-94d9-7eecd4fae415.PNG) 62 | 63 | ## Web application 64 | To test the app localy, run `app.py` and navigate to `localhost:5000`. Otherwise, navigate to the live demo. 65 | 66 | Use the `SELECT FILE` button to upload the desired file. 67 | 68 | ![web1](https://user-images.githubusercontent.com/29493411/27295175-3b0a1af0-551c-11e7-94fd-7b4106330537.PNG) 69 | 70 | If successful, the browser will redirect to the processing page. 71 | 72 | ![web2](https://user-images.githubusercontent.com/29493411/27295176-3b0d56de-551c-11e7-9cc8-0628eecd22d0.PNG) 73 | 74 | Input the desired parameters to apply the corresponding transformation. The modified image will be opened with your default image viewing program. The parameters are now sent through the form data with a post method, instead of being passed as arguments in the GET request. So, this app showcases a different approach to API functionality. The different transformations are: 75 | 76 | * Flip: simply click either the `Vertical` or the `Horizontal` buttons. 77 | * Rotate: input the degrees between 0 and 359 (html field validation). Use a positive number for clockwise rotation or a negative one for a counter-clockwise one. Click `GO` to proceed. 78 | * Crop: input the start and stop point coordinates, (`x1, y1`) and (`x2, y2`), respectively. Click `GO` to proceed. Will be validated by the API. 79 | * Blend: input alpha (%) between 0 and 100, html validated. The image will be blend with the stock photo `blend.jpg`. The higher the alpha parameter, the more weight will be assigned to the stock photo (i.e. for alpha equals 0 the image will remain unchanged). Click `GO` to proceed. 80 | 81 | ## License 82 | This API is provided under the MIT license. 83 | 84 | ## Issues 85 | Report any issue to the GitHub issue tracker. 86 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # web-app for API image manipulation 2 | 3 | from flask import Flask, request, render_template, send_from_directory 4 | import os 5 | from PIL import Image 6 | 7 | app = Flask(__name__) 8 | 9 | APP_ROOT = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | # default access page 13 | @app.route("/") 14 | def main(): 15 | return render_template('index.html') 16 | 17 | 18 | # upload selected image and forward to processing page 19 | @app.route("/upload", methods=["POST"]) 20 | def upload(): 21 | target = os.path.join(APP_ROOT, 'static/images/') 22 | 23 | # create image directory if not found 24 | if not os.path.isdir(target): 25 | os.mkdir(target) 26 | 27 | # retrieve file from html file-picker 28 | upload = request.files.getlist("file")[0] 29 | print("File name: {}".format(upload.filename)) 30 | filename = upload.filename 31 | 32 | # file support verification 33 | ext = os.path.splitext(filename)[1] 34 | if (ext == ".jpg") or (ext == ".png") or (ext == ".bmp"): 35 | print("File accepted") 36 | else: 37 | return render_template("error.html", message="The selected file is not supported"), 400 38 | 39 | # save file 40 | destination = "/".join([target, filename]) 41 | print("File saved to to:", destination) 42 | upload.save(destination) 43 | 44 | # forward to processing page 45 | return render_template("processing.html", image_name=filename) 46 | 47 | 48 | # rotate filename the specified degrees 49 | @app.route("/rotate", methods=["POST"]) 50 | def rotate(): 51 | # retrieve parameters from html form 52 | angle = request.form['angle'] 53 | filename = request.form['image'] 54 | 55 | # open and process image 56 | target = os.path.join(APP_ROOT, 'static/images') 57 | destination = "/".join([target, filename]) 58 | 59 | img = Image.open(destination) 60 | img = img.rotate(-1*int(angle)) 61 | 62 | # save and return image 63 | destination = "/".join([target, 'temp.png']) 64 | if os.path.isfile(destination): 65 | os.remove(destination) 66 | img.save(destination) 67 | 68 | return send_image('temp.png') 69 | 70 | 71 | # flip filename 'vertical' or 'horizontal' 72 | @app.route("/flip", methods=["POST"]) 73 | def flip(): 74 | 75 | # retrieve parameters from html form 76 | if 'horizontal' in request.form['mode']: 77 | mode = 'horizontal' 78 | elif 'vertical' in request.form['mode']: 79 | mode = 'vertical' 80 | else: 81 | return render_template("error.html", message="Mode not supported (vertical - horizontal)"), 400 82 | filename = request.form['image'] 83 | 84 | # open and process image 85 | target = os.path.join(APP_ROOT, 'static/images') 86 | destination = "/".join([target, filename]) 87 | 88 | img = Image.open(destination) 89 | 90 | if mode == 'horizontal': 91 | img = img.transpose(Image.FLIP_LEFT_RIGHT) 92 | else: 93 | img = img.transpose(Image.FLIP_TOP_BOTTOM) 94 | 95 | # save and return image 96 | destination = "/".join([target, 'temp.png']) 97 | if os.path.isfile(destination): 98 | os.remove(destination) 99 | img.save(destination) 100 | 101 | return send_image('temp.png') 102 | 103 | 104 | # crop filename from (x1,y1) to (x2,y2) 105 | @app.route("/crop", methods=["POST"]) 106 | def crop(): 107 | # retrieve parameters from html form 108 | x1 = int(request.form['x1']) 109 | y1 = int(request.form['y1']) 110 | x2 = int(request.form['x2']) 111 | y2 = int(request.form['y2']) 112 | filename = request.form['image'] 113 | 114 | # open image 115 | target = os.path.join(APP_ROOT, 'static/images') 116 | destination = "/".join([target, filename]) 117 | 118 | img = Image.open(destination) 119 | 120 | # check for valid crop parameters 121 | width = img.size[0] 122 | height = img.size[1] 123 | 124 | crop_possible = True 125 | if not 0 <= x1 < width: 126 | crop_possible = False 127 | if not 0 < x2 <= width: 128 | crop_possible = False 129 | if not 0 <= y1 < height: 130 | crop_possible = False 131 | if not 0 < y2 <= height: 132 | crop_possible = False 133 | if not x1 < x2: 134 | crop_possible = False 135 | if not y1 < y2: 136 | crop_possible = False 137 | 138 | # crop image and show 139 | if crop_possible: 140 | img = img.crop((x1, y1, x2, y2)) 141 | 142 | # save and return image 143 | destination = "/".join([target, 'temp.png']) 144 | if os.path.isfile(destination): 145 | os.remove(destination) 146 | img.save(destination) 147 | return send_image('temp.png') 148 | else: 149 | return render_template("error.html", message="Crop dimensions not valid"), 400 150 | return '', 204 151 | 152 | 153 | # blend filename with stock photo and alpha parameter 154 | @app.route("/blend", methods=["POST"]) 155 | def blend(): 156 | # retrieve parameters from html form 157 | alpha = request.form['alpha'] 158 | filename1 = request.form['image'] 159 | 160 | # open images 161 | target = os.path.join(APP_ROOT, 'static/images') 162 | filename2 = 'blend.jpg' 163 | destination1 = "/".join([target, filename1]) 164 | destination2 = "/".join([target, filename2]) 165 | 166 | img1 = Image.open(destination1) 167 | img2 = Image.open(destination2) 168 | 169 | # resize images to max dimensions 170 | width = max(img1.size[0], img2.size[0]) 171 | height = max(img1.size[1], img2.size[1]) 172 | 173 | img1 = img1.resize((width, height), Image.ANTIALIAS) 174 | img2 = img2.resize((width, height), Image.ANTIALIAS) 175 | 176 | # if image in gray scale, convert stock image to monochrome 177 | if len(img1.mode) < 3: 178 | img2 = img2.convert('L') 179 | 180 | # blend and show image 181 | img = Image.blend(img1, img2, float(alpha)/100) 182 | 183 | # save and return image 184 | destination = "/".join([target, 'temp.png']) 185 | if os.path.isfile(destination): 186 | os.remove(destination) 187 | img.save(destination) 188 | 189 | return send_image('temp.png') 190 | 191 | 192 | # retrieve file from 'static/images' directory 193 | @app.route('/static/images/') 194 | def send_image(filename): 195 | return send_from_directory("static/images", filename) 196 | 197 | 198 | if __name__ == "__main__": 199 | app.run() 200 | 201 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python 2 | env: flex 3 | api_version: 1 4 | service: image-demo 5 | threadsafe: true 6 | runtime_config: 7 | python_version: 2 8 | entrypoint: gunicorn -b :$PORT app:app 9 | automatic_scaling: 10 | min_num_instances: 1 11 | -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | `appengine_config.py` is automatically loaded when Google App Engine 3 | starts a new instance of your application. This runs before any 4 | WSGI applications specified in app.yaml are loaded. 5 | """ 6 | 7 | """`appengine_config` gets loaded when starting a new application instance.""" 8 | 9 | from google.appengine.ext import vendor 10 | 11 | vendor.add('lib') 12 | 13 | -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | # Image manipulation API 2 | 3 | from flask import Flask, render_template, send_from_directory, redirect 4 | import os 5 | from PIL import Image 6 | 7 | app = Flask(__name__) 8 | 9 | APP_ROOT = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | # default access redirects to API documentation 13 | @app.route("/") 14 | def main(): 15 | return redirect("https://github.com/gxercavins/image-api/blob/master/README.md", code=302) 16 | 17 | 18 | # rotate filename the specified degrees 19 | @app.route("/rotate//", methods=["GET"]) 20 | def rotate(angle, filename): 21 | 22 | # check for valid angle 23 | angle = int(angle) 24 | if not -360 < angle < 360: 25 | return render_template("error.html", message="Invalid angle parameter (-359 to 359)"), 400 26 | 27 | # open and process image 28 | target = os.path.join(APP_ROOT, 'static/images') 29 | destination = "/".join([target, filename]) 30 | 31 | img = Image.open(destination) 32 | img = img.rotate(-1*angle) 33 | 34 | # save and return image 35 | destination = "/".join([target, 'temp.png']) 36 | if os.path.isfile(destination): 37 | os.remove(destination) 38 | img.save(destination) 39 | 40 | return send_image('temp.png') 41 | 42 | 43 | # flip filename 'vertical' or 'horizontal' 44 | @app.route("/flip//", methods=["GET"]) 45 | def flip(mode, filename): 46 | 47 | # open and process image 48 | target = os.path.join(APP_ROOT, 'static/images') 49 | destination = "/".join([target, filename]) 50 | 51 | img = Image.open(destination) 52 | 53 | # check mode 54 | if mode == 'horizontal': 55 | img = img.transpose(Image.FLIP_LEFT_RIGHT) 56 | elif mode == 'vertical': 57 | img = img.transpose(Image.FLIP_TOP_BOTTOM) 58 | else: 59 | return render_template("error.html", message="Invalid mode (vertical or horizontal)"), 400 60 | 61 | # save and return image 62 | destination = "/".join([target, 'temp.png']) 63 | if os.path.isfile(destination): 64 | os.remove(destination) 65 | img.save(destination) 66 | 67 | return send_image('temp.png') 68 | 69 | 70 | # crop filename from (x1,y1) to (x2,y2) 71 | @app.route("/crop/////", methods=["GET"]) 72 | def crop(x1, y1, x2, y2, filename): 73 | 74 | # open image 75 | target = os.path.join(APP_ROOT, 'static/images') 76 | destination = "/".join([target, filename]) 77 | 78 | img = Image.open(destination) 79 | width = img.size[0] 80 | height = img.size[1] 81 | 82 | # check for valid crop parameters 83 | [x1, y1, x2, y2] = [int(x1), int(y1), int(x2), int(y2)] 84 | 85 | crop_possible = True 86 | 87 | while True: 88 | if not 0 <= x1 < width: 89 | crop_possible = False 90 | break 91 | if not 0 < x2 <= width: 92 | crop_possible = False 93 | break 94 | if not 0 <= y1 < height: 95 | crop_possible = False 96 | break 97 | if not 0 < y2 <= height: 98 | crop_possible = False 99 | break 100 | if not x1 < x2: 101 | crop_possible = False 102 | break 103 | if not y1 < y2: 104 | crop_possible = False 105 | break 106 | break 107 | 108 | # process image 109 | if crop_possible: 110 | img = img.crop((x1, y1, x2, y2)) 111 | else: 112 | return render_template("error.html", message="Crop dimensions not valid"), 400 113 | 114 | # save and return image 115 | destination = "/".join([target, 'temp.png']) 116 | if os.path.isfile(destination): 117 | os.remove(destination) 118 | img.save(destination) 119 | 120 | return send_image('temp.png') 121 | 122 | 123 | # blend filename1 and filename2 with alpha parameter 124 | @app.route("/blend///", methods=["GET"]) 125 | def blend(alpha, filename1, filename2): 126 | 127 | # check for valid alpha 128 | alpha = float(alpha) 129 | if not 0 <= alpha <= 100: 130 | return render_template("error.html", message="Invalid alpha value (0-100)"), 400 131 | 132 | #open images 133 | target = os.path.join(APP_ROOT, 'static/images') 134 | destination1 = "/".join([target, filename1]) 135 | destination2 = "/".join([target, filename2]) 136 | 137 | img1 = Image.open(destination1) 138 | img2 = Image.open(destination2) 139 | 140 | # check for dimensions and resize to larger ones 141 | width = max(img1.size[0], img2.size[0]) 142 | height = max(img1.size[1], img2.size[1]) 143 | 144 | img1 = img1.resize((width, height), Image.ANTIALIAS) 145 | img2 = img2.resize((width, height), Image.ANTIALIAS) 146 | 147 | # if one image in gray scale, convert the other to monochrome 148 | if len(img1.mode) < 3: 149 | img2 = img2.convert('L') 150 | elif len(img2.mode) < 3: 151 | img1 = img1.convert('L') 152 | 153 | # blend images 154 | img = Image.blend(img1, img2, float(alpha)/100) 155 | 156 | # save and return 157 | destination = "/".join([target, 'temp.png']) 158 | if os.path.isfile(destination): 159 | os.remove(destination) 160 | img.save(destination) 161 | 162 | return send_image('temp.png') 163 | 164 | 165 | # retrieve file from 'static/images' directory 166 | @app.route('/static/images/') 167 | def send_image(filename): 168 | return send_from_directory("static/images", filename) 169 | 170 | 171 | if __name__ == "__main__": 172 | app.run() 173 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | Flask==1.0 3 | gunicorn 4 | requests[security] 5 | -------------------------------------------------------------------------------- /static/images/blend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gxercavins/image-api/55f6b4dd7794109accbceae076dd7a23213b7635/static/images/blend.jpg -------------------------------------------------------------------------------- /static/jumbotron-narrow.css: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | body { 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | } 6 | 7 | /* Everything but the jumbotron gets side spacing for mobile first views */ 8 | .header, 9 | .marketing, 10 | .footer { 11 | padding-right: 15px; 12 | padding-left: 15px; 13 | } 14 | 15 | /* Custom page header */ 16 | .header { 17 | padding-bottom: 20px; 18 | border-bottom: 1px solid #e5e5e5; 19 | } 20 | /* Make the masthead heading the same height as the navigation */ 21 | .header h3 { 22 | margin-top: 0; 23 | margin-bottom: 0; 24 | line-height: 40px; 25 | } 26 | 27 | /* Custom page footer */ 28 | .footer { 29 | padding-top: 19px; 30 | color: #777; 31 | border-top: 1px solid #e5e5e5; 32 | } 33 | 34 | /* Customize container */ 35 | @media (min-width: 768px) { 36 | .container { 37 | /*max-width: 730px;*/ 38 | } 39 | } 40 | .container-narrow > hr { 41 | margin: 30px 0; 42 | } 43 | 44 | /* Main marketing message and sign up button */ 45 | .jumbotron { 46 | text-align: center; 47 | border-bottom: 1px solid #e5e5e5; 48 | } 49 | .jumbotron .btn { 50 | padding: 14px 24px; 51 | font-size: 21px; 52 | } 53 | 54 | /* Supporting marketing content */ 55 | .marketing { 56 | margin: 40px 0; 57 | } 58 | .marketing p + h4 { 59 | margin-top: 28px; 60 | } 61 | 62 | /* Responsive: Portrait tablets and up */ 63 | @media screen and (min-width: 768px) { 64 | /* Remove the padding we set earlier */ 65 | .header, 66 | .marketing, 67 | .footer { 68 | padding-right: 0; 69 | padding-left: 0; 70 | } 71 | /* Space out the masthead */ 72 | .header { 73 | margin-bottom: 30px; 74 | } 75 | /* Remove the bottom border on the jumbotron for visual effect */ 76 | .jumbotron { 77 | border-bottom: 0; 78 | } 79 | } 80 | 81 | /* RD: making vertical align great again */ 82 | .table > tbody > tr > td { 83 | vertical-align: middle; 84 | } 85 | 86 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | .btn-lg, 2 | .nav-pills > li > a, 3 | .container .jumbotron { 4 | border: 0px; 5 | border-radius: 0px; 6 | } 7 | 8 | #upload-button, 9 | #file-picker { 10 | margin-top: 30px; 11 | } 12 | 13 | div#image-container { 14 | max-width: 500px; 15 | } 16 | 17 | div#image-container > img { 18 | max-width: 100%; 19 | } 20 | 21 | #form-crop > input[type=number] { 22 | width: 50px; 23 | } 24 | 25 | #form-blend > input[type=number] { 26 | width: 77px; 27 | } 28 | 29 | .hidden { 30 | display: none !important; 31 | } 32 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error Page Image Manipulation App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |

An error occurred

22 |
23 | 24 |
25 |

{{ message }}

26 | Documentation 27 |
28 | 29 | 30 |
31 |

© gxercavins 2017. Using Python, Flask and PIL.

32 |
33 | 34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Python Flask Image Manipulation App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 28 |

Python Flask Image Manipulation App

29 |
30 | 31 |
32 |

Upload your image

33 |

34 |
35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 |

Flip

43 |

Flip your uploaded image either horizontally or vertically.

44 | 45 |

Crop

46 |

Select your start and stop points and crop your image accordingly.

47 | 48 |
49 | 50 |
51 |

Rotate

52 |

Enter the angle (in degrees) to rotate your image clockwise.

53 | 54 |

Blend

55 |

Blend with a stock photo, either in color or in black & white.

56 | 57 |
58 |
59 | 60 |
61 |

© gxercavins 2017. Using Python, Flask and PIL.

62 |
63 | 64 |
65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /templates/processing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Process your image 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 28 |

Process your image

29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |

Flip

40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 |

Crop

48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 |
59 |

Rotate

60 |
61 | 62 | 63 | 64 |
65 | 66 |

Blend

67 |
68 | 69 | 70 | 71 |
72 | 73 |
74 |
75 | 76 |
77 |

© gxercavins 2017. Using Python, Flask and PIL.

78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # IMPORTANT: core.py must be up and running (locally) for this test to work 2 | # Test each function for correct http status codes (200=ok, 400=bad request, 405=incorrect method) 3 | 4 | from core import * 5 | import unittest 6 | import requests 7 | 8 | 9 | class TestStringMethods(unittest.TestCase): 10 | 11 | # test flip function 12 | def test_flip(self): 13 | print 'Test Flip function' 14 | print '------------------------' 15 | 16 | r = [0, 0, 0] 17 | 18 | r[0] = requests.get('http://127.0.0.1:5000/flip/vertical/blend.jpg') 19 | r[1] = requests.get('http://127.0.0.1:5000/flip/invalid/blend.jpg') 20 | r[2] = requests.put('http://127.0.0.1:5000/flip/vertical/blend.jpg') 21 | 22 | self.assertEqual(str(r), '[, , ]') 23 | 24 | # test rotate function 25 | def test_rotate(self): 26 | print 'Test Rotate function' 27 | print '------------------------' 28 | 29 | r = [0, 0, 0] 30 | 31 | r[0] = requests.get('http://127.0.0.1:5000/rotate/-34/blend.jpg') 32 | r[1] = requests.get('http://127.0.0.1:5000/flip/370/blend.jpg') 33 | r[2] = requests.put('http://127.0.0.1:5000/flip/60/blend.jpg') 34 | 35 | self.assertEqual(str(r), '[, , ]') 36 | 37 | # test crop function 38 | def test_crop(self): 39 | print 'Test Crop function' 40 | print '------------------------' 41 | 42 | r = [0, 0, 0] 43 | 44 | r[0] = requests.get('http://127.0.0.1:5000/crop/10/10/20/20/blend.jpg') 45 | r[1] = requests.get('http://127.0.0.1:5000/crop/30/10/20/20/blend.jpg') 46 | r[2] = requests.put('http://127.0.0.1:5000/crop/10/10/20/20/blend.jpg') 47 | 48 | self.assertEqual(str(r), '[, , ]') 49 | 50 | # test blend function 51 | def test_blend(self): 52 | print 'Test Blend function' 53 | print '------------------------' 54 | 55 | r = [0, 0, 0] 56 | 57 | r[0] = requests.get('http://127.0.0.1:5000/blend/50/blend.jpg/blend.jpg') 58 | r[1] = requests.get('http://127.0.0.1:5000/blend/120/blend.jpg/blend.jpg') 59 | r[2] = requests.put('http://127.0.0.1:5000/blend/30/blend.jpg/blend.jpg') 60 | 61 | self.assertEqual(str(r), '[, , ]') 62 | 63 | if __name__ == '__main__': 64 | unittest.main() --------------------------------------------------------------------------------