├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── aws ├── ec2 │ ├── cloudInit │ ├── gtk.sh │ └── userData.sh └── iam │ ├── ec2Policy.json │ └── lambdaPolicy.json ├── grunt ├── configs │ ├── aws.js │ ├── aws_s3.js │ ├── env.js │ ├── lambda-prep.js │ └── mochaTest.js └── tasks │ └── lambda-prep.js ├── package.json ├── src ├── app.py └── svg.py ├── test ├── images │ ├── alphas │ │ ├── 3HzjEjX.png │ │ ├── BlAteqY.png │ │ ├── C3cabAb.png │ │ ├── FRC34Ws.png │ │ ├── JySTBFY.png │ │ ├── KuDllLA.png │ │ ├── LKBP6t6.png │ │ ├── TpO34bo.png │ │ ├── XPQA14I.png │ │ ├── ab9fA0c.png │ │ ├── blC2MOZ.png │ │ ├── cutoutman_by_zappian-d9s845f.png │ │ ├── qfRUpy5.png │ │ ├── rbTaSvu.png │ │ ├── sl9p8zp.png │ │ └── vBQc9nS.png │ ├── back │ │ ├── 16049563402_84fdd758a8_o.jpg │ │ ├── 16147578964_026de73776_o.jpg │ │ ├── 16972596478_97140d5403_o.jpg │ │ ├── 23759377091_a5749381fe_o.jpg │ │ ├── 8441274425_0bec0ff08a_o.jpg │ │ └── 9971067404_df8fbce4d7_o.jpg │ ├── debug │ │ └── grid.gif │ └── watermark │ │ ├── logo.png │ │ └── logo.svg ├── jsonTemplates │ ├── composite.json │ ├── s3event.json │ └── svg.json └── tasks │ └── composite.js └── virtualenv └── env.zip /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/grunt,node,python 3 | 4 | ### grunt ### 5 | # Grunt usually compiles files inside this directory 6 | dist/ 7 | 8 | # Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory 9 | .tmp/ 10 | 11 | # aws-sdk logs 12 | log/ 13 | 14 | # aws config file 15 | config.json 16 | 17 | ### Node ### 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (http://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules 45 | jspm_packages 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | 54 | ### Python ### 55 | # Byte-compiled / optimized / DLL files 56 | __pycache__/ 57 | *.py[cod] 58 | *$py.class 59 | 60 | # C extensions 61 | *.so 62 | 63 | # Distribution / packaging 64 | .Python 65 | env/ 66 | build/ 67 | develop-eggs/ 68 | dist/ 69 | downloads/ 70 | eggs/ 71 | .eggs/ 72 | lib/ 73 | lib64/ 74 | parts/ 75 | sdist/ 76 | var/ 77 | *.egg-info/ 78 | .installed.cfg 79 | *.egg 80 | 81 | # PyInstaller 82 | # Usually these files are written by a python script from a template 83 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 84 | *.manifest 85 | *.spec 86 | 87 | # Installer logs 88 | pip-log.txt 89 | pip-delete-this-directory.txt 90 | 91 | # Unit test / coverage reports 92 | htmlcov/ 93 | .tox/ 94 | .coverage 95 | .coverage.* 96 | .cache 97 | nosetests.xml 98 | coverage.xml 99 | *,cover 100 | .hypothesis/ 101 | 102 | # Translations 103 | *.mo 104 | *.pot 105 | 106 | # Django stuff: 107 | *.log 108 | local_settings.py 109 | 110 | # Flask instance folder 111 | instance/ 112 | 113 | # Sphinx documentation 114 | docs/_build/ 115 | 116 | # PyBuilder 117 | target/ 118 | 119 | # IPython Notebook 120 | .ipynb_checkpoints 121 | 122 | # pyenv 123 | .python-version -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install grunt-cli -g 6 | - npm install mocha -g -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-gruntfile": {} 3 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var _ = require('lodash'); 3 | var async = require('async'); 4 | var AWS = require('aws-sdk'); 5 | 6 | module.exports = function(grunt) { 7 | 'use strict'; 8 | //Load Task Configurations 9 | require('load-grunt-config')(grunt, { 10 | configPath: path.join(process.cwd(), 'grunt', 'configs'), 11 | data: { 12 | config: grunt.file.readJSON('config.json'), 13 | userData: grunt.file.read('aws/ec2/userData.sh', { 14 | encoding: 'base64' 15 | }) 16 | } 17 | }); 18 | //Load NPM Tasks 19 | require('load-grunt-tasks')(grunt); 20 | //Load Custom Tasks 21 | grunt.loadTasks(path.join(process.cwd(), 'grunt', 'tasks')); 22 | 23 | grunt.registerTask('default', [ 24 | 'lambda-prep:composite', 25 | 'lambda-prep:webpConverter', 26 | 'aws_s3:lambda', 27 | 'aws:updateLambda-composite', 28 | 'aws:updateLambda-webpConverter' 29 | ]); 30 | grunt.registerTask('lambda-composite', ['lambda-prep:composite', 'aws_s3:lambda', 'aws:updateLambda-composite']); 31 | grunt.registerTask('lambda-webpConverter', ['lambda-prep:webpConverter', 'aws_s3:lambda', 'aws:updateLambda-webpConverter']); 32 | grunt.registerTask('lambda-svgConverter', ['lambda-prep:svgConverter', 'aws_s3:lambda', 'aws:updateLambda-svgConverter']); 33 | grunt.registerTask('make-env', ['aws:launchEC2Instance']); 34 | grunt.registerTask('get-env', ['aws_s3:virtenv']); 35 | grunt.registerTask('test-local', ['env:config', 'mochaTest:test']); 36 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James MacDonald 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 | # aws-lambda-pillow 2 | 3 | ## Description 4 | 5 | A toolkit of Lambda functions, implemented using Pillow, to cover day-to-day use cases: 6 | 7 | * Resizing 8 | * Format Conversion 9 | * Watermarking 10 | * Composositing 11 | * Meta-Data Manipulation 12 | 13 | The Lambda functions are designed to integrate with S3, API Gateway and will eventually include a Cloudformation template. 14 | 15 | ## Image Return Modes 16 | 17 | At the moment Lambda, and API Gateway, have significant restrictions on what can be returned. As such the are several options: 18 | 19 | * 64-bit encoded string (up to 6mb) 20 | * Signed S3 URL (unlimited size) 21 | * JSON: `{"Bucket": "", "Key":""}` 22 | 23 | ## Supported Formats 24 | 25 | * jpeg 26 | * tiff 27 | * png 28 | * webp 29 | * bmp 30 | * gif 31 | 32 | ## Setup 33 | 34 | a `config.json` file is required in the root of the project with the following attributes 35 | 36 | ```json 37 | { 38 | "accessKeyId": "< REQUIRED: YOUR KEY ID >", 39 | "secretAccessKey": "< REQUIRED: YOUR ACCESS KEY >", 40 | "region": "< REQUIRED: YOUR AWS REGION >", 41 | "codeBucket":"< REQUIRED: YOUR CODE BUCKET >", 42 | "imageBucket":"< REQUIRED: YOUR IMAGE BUCKET >", 43 | "KeyName": "< OPTIONAL: YOUR EC2 PEM NAME >", 44 | "SecurityGroupIds": [ 45 | "< OPTIONAL: YOUR EC2 SECURITY GROUP ID >" 46 | ] 47 | } 48 | 49 | ``` 50 | 51 | ## Virtualenv Bundle 52 | 53 | To enable support for the broadest possible spectrum of image formats a virtualenv deployment package is required. 54 | To ensure compability, the virtualenv bundle (env.zip) has been built using the same [Linux AMI](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) that Lambda utilises. 55 | 56 | The virtualenv bundle (env.zip) in this project includes the following Python packages: 57 | 58 | * pillow 59 | * simplejson 60 | * eventlet 61 | * requests 62 | 63 | **Warning Boto3 is not included by default**, as it already installed on Lambda. 64 | 65 | If you want to enable Boto3 (to test locally for example), or add other pip installable packages and dependencies: 66 | 67 | 1. Uncomment `pip install --verbose --use-wheel boto3` in [userData.sh](https://github.com/jDmacD/aws-lambda-pillow/blob/master/aws/ec2/userData.sh). 68 | 2. Execute `grunt make-env`. This will perform the following actions: 69 | - Launch an ec2 70 | - Perform `yum install`s 71 | - Create a virutalenv 72 | - Perform `pip install`s 73 | - Zip `lib` and `lib64` to env.zip 74 | - Copy env.zip to the codeBucket defined in the `config.json` 75 | 3. This operation will take about 5 minutes. 76 | 4. Execute `grunt get-env`. This will download the new env.zip, overwriting the original. 77 | 78 | -------------------------------------------------------------------------------- /aws/ec2/cloudInit: -------------------------------------------------------------------------------- 1 | #http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html#user-data-cloud-init 2 | #cloud-config 3 | 4 | repo_update: true 5 | repo_upgrade: all 6 | 7 | packages: 8 | - libtiff-devel 9 | - libjpeg-devel 10 | - libzip-devel 11 | - freetype-devel 12 | - lcms2-devel 13 | - libwebp-devel 14 | - tcl-devel 15 | - tk-devel -------------------------------------------------------------------------------- /aws/ec2/gtk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig 4 | export LD_LIBRARY_PATH=/usr/lib 5 | 6 | sudo yum update -y 7 | sudo yum install -y gcc 8 | 9 | sudo yum-config-manager --add-repo https://raw.githubusercontent.com/jDmacD/cuddly-octo-meme/master/fedora.repo 10 | sudo yum-config-manager --enable fedora 11 | 12 | yum repolist all 13 | 14 | sudo yum install -y --disableplugin=priorities \ 15 | flex \ 16 | bison \ 17 | zlib-devel \ 18 | libffi-devel \ 19 | gettext \ 20 | pcre-devel \ 21 | cairo-devel \ 22 | pango-devel \ 23 | libtiff-devel \ 24 | libjpeg-devel \ 25 | libpng-devel \ 26 | pygobject2 27 | 28 | 29 | #GLIB 30 | wget -N http://ftp.gnome.org/pub/gnome/sources/glib/2.46/glib-2.46.2.tar.xz 31 | 32 | tar -xf glib-2.46.2.tar.xz 33 | cd glib-2.46.2 34 | 35 | ./configure --prefix=/usr && make 36 | sudo make install 37 | 38 | pkg-config --modversion glib-2.0 39 | 40 | cd ~/ 41 | 42 | 43 | #GI 44 | wget -N http://ftp.gnome.org/pub/gnome/sources/gobject-introspection/1.46/gobject-introspection-1.46.0.tar.xz 45 | tar -xf gobject-introspection-1.46.0.tar.xz 46 | cd gobject-introspection-1.46.0 47 | 48 | ./configure --prefix=/usr --disable-static 49 | sudo make install 50 | 51 | cd ~/ 52 | 53 | #ATK 54 | wget http://ftp.gnome.org/pub/gnome/sources/atk/2.18/atk-2.18.0.tar.xz 55 | tar -xf atk-2.18.0.tar.xz 56 | cd atk-2.18.0 57 | 58 | ./configure --prefix=/usr && make 59 | sudo make install 60 | 61 | cd ~/ 62 | 63 | #PIXBUF 64 | wget http://ftp.gnome.org/pub/gnome/sources/gdk-pixbuf/2.32/gdk-pixbuf-2.32.3.tar.xz 65 | tar -xf gdk-pixbuf-2.32.3.tar.xz 66 | cd gdk-pixbuf-2.32.3 67 | 68 | ./configure --prefix=/usr && make 69 | sudo make install 70 | 71 | pkg-config --modversion gdk-pixbuf-2.0 72 | 73 | cd ~/ 74 | 75 | #GTK 76 | wget http://ftp.gnome.org/pub/gnome/sources/gtk+/2.24/gtk+-2.24.30.tar.xz 77 | tar -xf gtk+-2.24.30.tar.xz 78 | cd gtk+-2.24.30 79 | 80 | ./configure --prefix=/usr && make 81 | sudo make install 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /aws/ec2/userData.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | set -o pipefail 5 | 6 | echo "do update" 7 | yum update -y 8 | 9 | echo "do dependcy install" 10 | yum install -y \ 11 | gcc \ 12 | libtiff-devel \ 13 | libzip-devel \ 14 | libjpeg-devel \ 15 | freetype-devel \ 16 | lcms2-devel \ 17 | libwebp-devel \ 18 | tcl-devel \ 19 | tk-devel 20 | 21 | echo "copy webp packages" 22 | cd /usr/lib64/ 23 | find . -name "*webp*" | cpio -pdm ~/env/lib64/python2.7/site-packages/ 24 | cd ~/ 25 | 26 | echo "make env" 27 | /usr/bin/virtualenv \ 28 | --python /usr/bin/python env \ 29 | --always-copy 30 | 31 | 32 | echo "activate env in `pwd`" 33 | source env/bin/activate 34 | 35 | echo "install pips" 36 | pip install --upgrade pip 37 | pip install --verbose --use-wheel pillow 38 | pip install --verbose --use-wheel simplejson 39 | pip install --verbose --use-wheel eventlet 40 | pip install --verbose --use-wheel requests 41 | #pip install --verbose --use-wheel boto3 42 | deactivate 43 | 44 | echo "zip lib and lib64" 45 | cd ~/env/lib/python2.7/site-packages 46 | zip -r9 ~/env.zip * 47 | cd ~/env/lib64/python2.7/site-packages 48 | zip -r9 ~/env.zip * 49 | cd ~/ 50 | 51 | echo "copy env to s3" 52 | aws s3 cp env.zip s3://code.jdmacd/env.zip --region eu-west-1 --content-type application/zip 53 | echo "copy log to s3" 54 | aws s3 cp /var/log/cloud-init-output.log s3://code.jdmacd/ec2.log --region eu-west-1 --content-type text/plain -------------------------------------------------------------------------------- /aws/iam/ec2Policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "s3:*", 7 | "Resource": "*" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /aws/iam/lambdaPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "s3:GetObject", 17 | "s3:PutObject", 18 | "s3:ListBucket" 19 | ], 20 | "Resource": [ 21 | "arn:aws:s3:::*" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /grunt/configs/aws.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | credentials: { 4 | accessKeyId: '<%= config.accessKeyId %>', 5 | secretAccessKey: '<%= config.secretAccessKey %>', 6 | region: '<%= config.region %>' 7 | }, 8 | dest: 'log/' 9 | }, 10 | launchEC2Instance: { 11 | service: 'ec2', 12 | method: 'runInstances', 13 | params: { 14 | ImageId: 'ami-bff32ccc', // http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html 15 | MinCount: 1, 16 | MaxCount: 1, 17 | KeyName: '<%= config.KeyName %>', 18 | SecurityGroupIds: '<%= config.SecurityGroupIds %>', 19 | InstanceType: 't2.micro', 20 | UserData: '<%= userData %>', 21 | IamInstanceProfile: { 22 | Arn: "arn:aws:iam::535915538966:instance-profile/ec2_s3" 23 | } 24 | } 25 | }, 26 | 'updateLambda-composite': { 27 | service: 'lambda', 28 | method: 'updateFunctionCode', 29 | params: { 30 | FunctionName: 'composite', 31 | S3Bucket: '<%= config.codeBucket %>', 32 | S3Key: 'lambda/composite.zip' 33 | } 34 | }, 35 | 'updateLambda-webpConverter': { 36 | service: 'lambda', 37 | method: 'updateFunctionCode', 38 | params: { 39 | FunctionName: 'webpConverter', 40 | S3Bucket: '<%= config.codeBucket %>', 41 | S3Key: 'lambda/webpConverter.zip' 42 | } 43 | }, 44 | 'updateLambda-svgConverter': { 45 | service: 'lambda', 46 | method: 'updateFunctionCode', 47 | params: { 48 | FunctionName: 'svgConverter', 49 | S3Bucket: '<%= config.codeBucket %>', 50 | S3Key: 'lambda/svgConverter.zip' 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /grunt/configs/aws_s3.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | accessKeyId: '<%= config.accessKeyId %>', 4 | secretAccessKey: '<%= config.secretAccessKey %>', 5 | region: '<%= config.region %>', 6 | uploadConcurrency: 5, // 5 simultaneous uploads 7 | downloadConcurrency: 5 // 5 simultaneous downloads 8 | }, 9 | virtenv: { 10 | options: { 11 | bucket: '<%= config.codeBucket %>', 12 | differential: false 13 | }, 14 | files: [{ 15 | cwd: './', 16 | dest: 'virtualenv/env.zip', 17 | action: 'download' 18 | }] 19 | }, 20 | lambda: { 21 | options: { 22 | bucket: '<%= config.codeBucket %>', 23 | differential: false 24 | }, 25 | files: [{ 26 | action: 'upload', 27 | expand: true, 28 | cwd: 'dist/lambda/', 29 | src: ['**'], 30 | dest: 'lambda/' 31 | }] 32 | } 33 | } -------------------------------------------------------------------------------- /grunt/configs/env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: {}, 3 | config: { 4 | AWS_KEY: '<%= config.accessKeyId %>', 5 | AWS_SECRET:'<%= config.secretAccessKey %>', 6 | AWS_REGION:'<%= config.region %>' 7 | } 8 | } -------------------------------------------------------------------------------- /grunt/configs/lambda-prep.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | composite: { 3 | src: 'src/app.py', 4 | dest: 'dist/lambda/', 5 | config: { 6 | bucket_in: '<%= config.imageBucket %>', 7 | bucket_out: '<%= config.imageBucket %>' 8 | } 9 | }, 10 | webpConverter: { 11 | src: 'src/app.py', 12 | dest: 'dist/lambda/', 13 | config: { 14 | keepOld: false, 15 | format: 'webp' 16 | } 17 | }, 18 | svgConverter: { 19 | src: 'src/svg.py', 20 | dest: 'dist/lambda/', 21 | config: { 22 | foo: false, 23 | bar: 'webp' 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /grunt/configs/mochaTest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: { 3 | options: { 4 | reporter: 'spec', 5 | quiet: false, // Optionally suppress output to standard out (defaults to false) 6 | clearRequireCache: true // Optionally clear the require cache before running tests (defaults to false) 7 | }, 8 | src: ['test/tasks/*.js'] 9 | } 10 | } -------------------------------------------------------------------------------- /grunt/tasks/lambda-prep.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs-extra'); 4 | var JSZip = require('jszip'); 5 | 6 | module.exports = function(grunt) { 7 | 8 | grunt.registerMultiTask('lambda-prep', 'prep code for lambda upload', function() { 9 | 10 | var done = this.async(); 11 | var src = this.data.src; 12 | var dest = this.data.dest; 13 | var config = this.data.config; 14 | 15 | var envZip = 'virtualenv/env.zip'; 16 | var envZipFinal = dest + this.target + '.zip'; 17 | 18 | fs.copySync(envZip, envZipFinal); 19 | 20 | fs.readFile(envZipFinal, function(err, data) { 21 | if (err) { 22 | 23 | throw err; 24 | done(); 25 | 26 | } else { 27 | var zip = new JSZip(data); 28 | fs.readFile(src, function(err, data) { 29 | if (err) { 30 | throw err; 31 | done(); 32 | } else { 33 | zip.file(src.split('/')[1], data); 34 | zip.file('config.json', JSON.stringify(config)) 35 | var buffer = zip.generate({ 36 | type: "nodebuffer", 37 | compression: 'DEFLATE' 38 | }); 39 | fs.writeFile(envZipFinal, buffer, function(err) { 40 | if (err) { 41 | throw err; 42 | done(); 43 | } else { 44 | grunt.log.ok([envZipFinal + ' zipped']); 45 | done(); 46 | }; 47 | 48 | }); 49 | } 50 | }); 51 | }; 52 | }); 53 | 54 | }); 55 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-lambda-pillow", 3 | "version": "0.0.1", 4 | "description": "A set of Python Pillow Lambda functions to cover common cases", 5 | "main": "Gruntfile.js", 6 | "scripts": { 7 | "test": "mocha test/tasks/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jDmacD/aws-lambda-pillow.git" 12 | }, 13 | "keywords": [ 14 | "AWS", 15 | "Lambda", 16 | "Pillow", 17 | "Python", 18 | "virtualenv" 19 | ], 20 | "author": "James D. MacDonald", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jDmacD/aws-lambda-pillow/issues" 24 | }, 25 | "homepage": "https://github.com/jDmacD/aws-lambda-pillow#readme", 26 | "devDependencies": { 27 | "async": "^1.5.2", 28 | "aws-sdk": "^2.2.39", 29 | "chai": "^3.5.0", 30 | "fs-extra": "^0.26.5", 31 | "grunt": "^0.4.5", 32 | "grunt-aws-s3": "^0.14.4", 33 | "grunt-aws-sdk": "jDmacD/grunt-aws-sdk", 34 | "grunt-cli": "^0.1.13", 35 | "grunt-env": "^0.4.4", 36 | "grunt-mocha-test": "^0.12.7", 37 | "grunt-replace": "^0.11.0", 38 | "jszip": "^2.5.0", 39 | "load-grunt-config": "^0.19.1", 40 | "load-grunt-tasks": "^3.4.1", 41 | "lodash": "^4.5.1", 42 | "mocha": "^2.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | #http://stackoverflow.com/questions/4720735/fastest-way-to-download-3-million-objects-from-a-s3-bucket 2 | #https://boto3.readthedocs.org/en/latest/reference/services/s3.html#S3.Client.generate_presigned_url 3 | #https://boto3.readthedocs.org/en/latest/reference/services/cloudfront.html#CloudFront.Client.generate_presigned_url 4 | 5 | import os 6 | import sys 7 | import hashlib 8 | import mimetypes 9 | # add webp to mime library, as it is a draft format 10 | mimetypes.add_type('image/webp', '.webp', strict=False) 11 | 12 | from eventlet import * 13 | patcher.monkey_patch(all=True) 14 | 15 | import boto3 16 | import botocore 17 | 18 | import requests 19 | import urllib 20 | 21 | import base64 22 | from io import BytesIO 23 | 24 | from PIL import Image 25 | from PIL import ImageOps 26 | 27 | import time 28 | import itertools 29 | import simplejson as json 30 | 31 | 32 | s3 = boto3.resource('s3') 33 | s3Client = boto3.client('s3') 34 | pool = GreenPool() 35 | pool.waitall() 36 | 37 | with open('config.json') as data_file: 38 | config = json.load(data_file) 39 | 40 | try: 41 | bucket_in = config['bucket_in'] 42 | bucket_out = config['bucket_out'] 43 | except: 44 | print('default buckets not loaded') 45 | 46 | def clean_format(format): 47 | 48 | if format.lower() in ['jpg', 'jpeg']: 49 | return 'jpeg' 50 | elif format.lower() in ['tiff', 'tif']: 51 | return 'tiff' 52 | elif format.lower() in ['webp']: 53 | return 'webp' 54 | elif format.lower() in ['png']: 55 | return 'png' 56 | elif format.lower() in ['bmp']: 57 | return 'bmp' 58 | elif format.lower() in ['gif']: 59 | return 'gif' 60 | else: 61 | return format.lower() 62 | 63 | def to_pixels(part, whole): 64 | 65 | part_type = type(part) 66 | if part_type is float or part_type is int: 67 | #part is in pixels 68 | return part 69 | elif part_type is unicode: 70 | #part is a percentage, convert to pixels 71 | pixels = float(whole) /100 * float(part) 72 | return pixels 73 | 74 | def unknown_side(known, side_1, side_2): 75 | 76 | return float(known) * float(side_1) / float(side_2) 77 | 78 | def composite_images(images): 79 | 80 | base_img = None 81 | 82 | for image in pool.imap(get_image, images): 83 | 84 | image = resize_image(image) 85 | 86 | img = image['img'] 87 | 88 | if 'r' in image: 89 | img = img.rotate(int(image['r']), expand=True) 90 | 91 | if base_img == None: 92 | base_img = Image.new('RGB', img.size) 93 | 94 | if 'x' in image: 95 | x = to_pixels(image['x'], base_img.size[0]) 96 | elif 'cx' in image: 97 | x = to_pixels(image['cx'], base_img.size[0]) - (img.size[0]/2) 98 | else: 99 | x = 0 100 | 101 | if 'y' in image: 102 | y = to_pixels(image['y'], base_img.size[1]) 103 | if 'cy' in image: 104 | y = to_pixels(image['cy'], base_img.size[1]) - (img.size[1]/2) 105 | else: 106 | y = 0 107 | 108 | if img.mode == 'RGBA': 109 | base_img.paste(img, ( int(x) , int(y) ), img) 110 | else: 111 | base_img.paste(img, ( int(x) , int(y) ) ) 112 | 113 | return base_img 114 | 115 | def resize_image(image): 116 | 117 | try: 118 | height = image['height'] 119 | except: 120 | height = None 121 | 122 | try: 123 | width = image['width'] 124 | except: 125 | width = None 126 | 127 | if height == None and width == None: 128 | # No height or width supplied, return as is 129 | return image 130 | else: 131 | 132 | i_width, i_height = image['img'].size 133 | 134 | if height != None: 135 | height = to_pixels(part=height, whole=i_height) 136 | else: 137 | # Calculate height from width 138 | height = unknown_side(known=to_pixels(width, i_width), side_1=i_height, side_2=i_width) 139 | 140 | if width != None: 141 | width = to_pixels(part=width, whole=i_width) 142 | else: 143 | # Calulate width from height 144 | width = unknown_side(known=to_pixels(height, i_height), side_1=i_width, side_2=i_height) 145 | 146 | image['img'] = image['img'].resize((int(width),int(height)), Image.ANTIALIAS) 147 | return image 148 | 149 | def data_url(img, quality, format, ContentType): 150 | 151 | start_time = time.time() 152 | 153 | img_buffer = BytesIO() 154 | img.save(img_buffer, quality=quality, format=format) 155 | imgStr = base64.b64encode(img_buffer.getvalue()) 156 | dataUrl = 'data:' + ContentType +';base64,' + imgStr.decode('utf-8') 157 | 158 | print('dataUrl conversion in ' + str(time.time() - start_time)) 159 | 160 | return dataUrl 161 | 162 | def S3_url(Key, Bucket): 163 | 164 | return s3Client.generate_presigned_url('get_object', Params = {'Bucket': Bucket, 'Key': Key}, ExpiresIn = 3600) 165 | 166 | def get_image(image): 167 | 168 | if 'Key' in image: 169 | Key = image['Key'] 170 | 171 | try: 172 | bucket = image['bucket'] 173 | except: 174 | bucket = bucket_in 175 | 176 | object = s3.Object(bucket, Key) 177 | 178 | try: 179 | res = object.get() 180 | except: 181 | print('get fail: ' + Key) 182 | 183 | image['img'] = Image.open(res['Body']) 184 | 185 | elif 'url' in image: 186 | # http://pillow.readthedocs.org/en/3.1.x/releasenotes/2.8.0.html 187 | image['img'] = Image.open(requests.get(image['url'], stream=True).raw) 188 | 189 | return image 190 | 191 | def save_image(img, quality, format, ContentType, Key, Bucket): 192 | 193 | Metadata={'width': str(img.size[0]), 'height', str(img.size[1]), 'format': img.format, 'mode': img.mode} 194 | 195 | object = s3.Object(Bucket, Key) 196 | image_buffer = BytesIO() 197 | img.save(image_buffer, quality=quality, format=format) 198 | res = object.put(Body=image_buffer.getvalue(), ContentType=ContentType, Metadata=Metadata) 199 | return res 200 | 201 | 202 | def composite_handler(event, context): 203 | 204 | quality = event['quality'] 205 | mode = event['mode'] 206 | 207 | try: 208 | Bucket = event['bucket'] 209 | except: 210 | Bucket = bucket_out 211 | 212 | try: 213 | Key = event['Key'] 214 | except: 215 | Key = hashlib.md5(json.dumps(event)).hexdigest() 216 | 217 | if '.' in Key: 218 | format = clean_format(Key.split('.')[1]) 219 | else: 220 | format = clean_format(event['format']) 221 | Key = Key + '.' + format 222 | 223 | 224 | ContentType = mimetypes.guess_type(Key, strict=False)[0] 225 | 226 | if mode == 'DATA': 227 | 228 | composite_image = composite_images(event['images']) 229 | return data_url(composite_image, quality=quality, format=format, ContentType=ContentType) 230 | 231 | elif mode == 'S3URL': 232 | 233 | try: 234 | s3Client.head_object(Bucket=Bucket, Key=Key) 235 | return S3_url(Key=Key, Bucket=Bucket) 236 | except: 237 | composite_image = composite_images(event['images']) 238 | save_image(composite_image, quality=quality, format=format, ContentType=ContentType, Key=Key ,Bucket=Bucket) 239 | return S3_url(Key=Key, Bucket=Bucket) 240 | 241 | elif mode == 'S3': 242 | 243 | try: 244 | res = s3Client.head_object(Bucket=Bucket, Key=Key) 245 | print(res) 246 | return {'Bucket': Bucket, 'Key':Key} 247 | except: 248 | composite_image = composite_images(event['images']) 249 | res = save_image(composite_image, quality=quality, format=format, ContentType=ContentType, Key=Key ,Bucket=Bucket) 250 | print(res) 251 | return {'Bucket': Bucket, 'Key':Key} 252 | 253 | def s3_converter(event, context): 254 | 255 | bucket = event['Records'][0]['s3']['bucket']['name'] 256 | key = urllib.unquote_plus(event['Records'][0]['s3']['object']['key']).decode('utf8') 257 | 258 | image = {'bucket':bucket,'Key': key} 259 | 260 | if 'width' in config: 261 | image['width'] = config['width'] 262 | 263 | if 'height' in config: 264 | image['height'] = config['height'] 265 | 266 | if 'keepOld' in config: 267 | keep_old = config['keepOld'] 268 | else: 269 | keep_old = True 270 | 271 | if 'format' in config: 272 | format = clean_format(config['format']) 273 | else: 274 | format = key.split('.')[1] 275 | 276 | if 'quality' in config: 277 | quality = config['quality'] 278 | else: 279 | quality = 100 280 | 281 | Key = key.split('.')[0] + '.' + format 282 | 283 | ContentType = mimetypes.guess_type(Key, strict=False)[0] 284 | 285 | img = get_image(image)['img'] 286 | 287 | res = save_image(img, quality, format, ContentType, Key, bucket) 288 | 289 | if not keep_old: 290 | object = s3.Object(bucket, key) 291 | res = object.delete() 292 | 293 | return res -------------------------------------------------------------------------------- /src/svg.py: -------------------------------------------------------------------------------- 1 | #http://stackoverflow.com/questions/4720735/fastest-way-to-download-3-million-objects-from-a-s3-bucket 2 | #https://boto3.readthedocs.org/en/latest/reference/services/s3.html#S3.Client.generate_presigned_url 3 | #https://boto3.readthedocs.org/en/latest/reference/services/cloudfront.html#CloudFront.Client.generate_presigned_url 4 | 5 | import os 6 | import sys 7 | import hashlib 8 | import mimetypes 9 | # add webp to mime library, as it is a draft format 10 | mimetypes.add_type('image/webp', '.webp', strict=False) 11 | 12 | from eventlet import * 13 | patcher.monkey_patch(all=True) 14 | 15 | import boto3 16 | import botocore 17 | 18 | import requests 19 | import urllib 20 | 21 | import base64 22 | from io import BytesIO 23 | 24 | from PIL import Image 25 | from PIL import ImageOps 26 | 27 | import cairosvg 28 | import cairocffi as cairo 29 | import rsvg 30 | 31 | import time 32 | import itertools 33 | import simplejson as json 34 | 35 | 36 | s3 = boto3.resource('s3') 37 | s3Client = boto3.client('s3') 38 | pool = GreenPool() 39 | pool.waitall() 40 | 41 | def svg_converter(event, context): 42 | #res = requests.get('https://s3-eu-west-1.amazonaws.com/components.jdmacd/watermark/logo.svg') 43 | #print res.content 44 | png = cairosvg.svg2png(url='https://s3-eu-west-1.amazonaws.com/components.jdmacd/watermark/logo.svg') 45 | return event -------------------------------------------------------------------------------- /test/images/alphas/3HzjEjX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/3HzjEjX.png -------------------------------------------------------------------------------- /test/images/alphas/BlAteqY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/BlAteqY.png -------------------------------------------------------------------------------- /test/images/alphas/C3cabAb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/C3cabAb.png -------------------------------------------------------------------------------- /test/images/alphas/FRC34Ws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/FRC34Ws.png -------------------------------------------------------------------------------- /test/images/alphas/JySTBFY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/JySTBFY.png -------------------------------------------------------------------------------- /test/images/alphas/KuDllLA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/KuDllLA.png -------------------------------------------------------------------------------- /test/images/alphas/LKBP6t6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/LKBP6t6.png -------------------------------------------------------------------------------- /test/images/alphas/TpO34bo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/TpO34bo.png -------------------------------------------------------------------------------- /test/images/alphas/XPQA14I.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/XPQA14I.png -------------------------------------------------------------------------------- /test/images/alphas/ab9fA0c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/ab9fA0c.png -------------------------------------------------------------------------------- /test/images/alphas/blC2MOZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/blC2MOZ.png -------------------------------------------------------------------------------- /test/images/alphas/cutoutman_by_zappian-d9s845f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/cutoutman_by_zappian-d9s845f.png -------------------------------------------------------------------------------- /test/images/alphas/qfRUpy5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/qfRUpy5.png -------------------------------------------------------------------------------- /test/images/alphas/rbTaSvu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/rbTaSvu.png -------------------------------------------------------------------------------- /test/images/alphas/sl9p8zp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/sl9p8zp.png -------------------------------------------------------------------------------- /test/images/alphas/vBQc9nS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/alphas/vBQc9nS.png -------------------------------------------------------------------------------- /test/images/back/16049563402_84fdd758a8_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/16049563402_84fdd758a8_o.jpg -------------------------------------------------------------------------------- /test/images/back/16147578964_026de73776_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/16147578964_026de73776_o.jpg -------------------------------------------------------------------------------- /test/images/back/16972596478_97140d5403_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/16972596478_97140d5403_o.jpg -------------------------------------------------------------------------------- /test/images/back/23759377091_a5749381fe_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/23759377091_a5749381fe_o.jpg -------------------------------------------------------------------------------- /test/images/back/8441274425_0bec0ff08a_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/8441274425_0bec0ff08a_o.jpg -------------------------------------------------------------------------------- /test/images/back/9971067404_df8fbce4d7_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/back/9971067404_df8fbce4d7_o.jpg -------------------------------------------------------------------------------- /test/images/debug/grid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/debug/grid.gif -------------------------------------------------------------------------------- /test/images/watermark/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/test/images/watermark/logo.png -------------------------------------------------------------------------------- /test/images/watermark/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 12 | 15 | 17 | 20 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/jsonTemplates/composite.json: -------------------------------------------------------------------------------- 1 | { 2 | "Key": "out/test", 3 | "format": "webp", 4 | "mode": "S3URL", 5 | "quality": 40, 6 | "images": [ 7 | { 8 | "Key": "back.webp", 9 | "x": 0, 10 | "y": 0, 11 | "height": 200, 12 | "width": 200 13 | }, 14 | { 15 | "Key": "mid.webp", 16 | "x": 100, 17 | "y": 100, 18 | "height": 200, 19 | "width": 200 20 | }, 21 | { 22 | "Key": "fore.webp", 23 | "x": 0, 24 | "y": 0, 25 | "height": 200, 26 | "width": 200 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /test/jsonTemplates/s3event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventVersion": "2.0", 5 | "eventTime": "1970-01-01T00:00:00.000Z", 6 | "requestParameters": { 7 | "sourceIPAddress": "127.0.0.1" 8 | }, 9 | "s3": { 10 | "configurationId": "testConfigRule", 11 | "object": { 12 | "eTag": "0123456789abcdef0123456789abcdef", 13 | "sequencer": "0A1B2C3D4E5F678901", 14 | "key": "alphas/3HzjEjX.png", 15 | "size": 1024 16 | }, 17 | "bucket": { 18 | "arn": "arn:aws:s3:::mybucket", 19 | "name": "components.jdmacd", 20 | "ownerIdentity": { 21 | "principalId": "EXAMPLE" 22 | } 23 | }, 24 | "s3SchemaVersion": "1.0" 25 | }, 26 | "responseElements": { 27 | "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", 28 | "x-amz-request-id": "EXAMPLE123456789" 29 | }, 30 | "awsRegion": "eu-west-1", 31 | "eventName": "ObjectCreated:Put", 32 | "userIdentity": { 33 | "principalId": "EXAMPLE" 34 | }, 35 | "eventSource": "aws:s3" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /test/jsonTemplates/svg.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /test/tasks/composite.js: -------------------------------------------------------------------------------- 1 | var should = require('chai').should(); 2 | var expect = require('chai').expect; 3 | 4 | var _ = require('lodash'); 5 | 6 | var creds = { 7 | accessKeyId: process.env.AWS_KEY, 8 | secretAccessKey: process.env.AWS_SECRET, 9 | region: process.env.AWS_REGION 10 | }; 11 | 12 | var AWS = require('aws-sdk'); 13 | 14 | var lambda = new AWS.Lambda(creds); 15 | var s3 = new AWS.S3(creds); 16 | 17 | 18 | function invokeLambda(params, callback) { 19 | lambda.invoke(params, function(err, res) { 20 | callback(err, res) 21 | }); 22 | }; 23 | 24 | function listS3(params, callback) { 25 | s3.listObjects(params, function(err, res) { 26 | callback(err, res) 27 | }); 28 | } 29 | 30 | describe('composite', function() { 31 | 32 | before(function (done) { 33 | listS3({ 34 | Bucket:'components.jdmacd', 35 | Delimiter: '/', 36 | Prefix: 'back/' 37 | },function(err, res) { 38 | if (err) { 39 | done(err) 40 | } else { 41 | console.log(res) 42 | done() 43 | } 44 | }) 45 | }); 46 | 47 | // describe('absolute pixels', function() { 48 | 49 | // var data = {}; 50 | 51 | // before(function (done) { 52 | 53 | // invokeLambda({},function(err, res) { 54 | // if (err) { 55 | // done(err); 56 | // } else { 57 | // data = res; 58 | // done(); 59 | // } 60 | // }) 61 | 62 | // }); 63 | 64 | // it('should do what...', function (done) { 65 | // done() 66 | // }); 67 | 68 | // }); 69 | 70 | }); -------------------------------------------------------------------------------- /virtualenv/env.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jDmacD/aws-lambda-pillow/68481ed7231648915485fa4694f71f38cf056fc6/virtualenv/env.zip --------------------------------------------------------------------------------