Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
################### | ||
Imagecast changelog | ||
################### | ||
|
||
|
||
in progress | ||
=========== | ||
|
||
|
||
2020-06-06 0.1.0 | ||
================ | ||
- First version, with command line interface |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
.. image:: https://img.shields.io/badge/Python-3-green.svg | ||
:target: https://github.com/panodata/imagecast | ||
|
||
.. image:: https://img.shields.io/pypi/v/imagecast.svg | ||
:target: https://pypi.org/project/imagecast/ | ||
|
||
.. image:: https://img.shields.io/github/tag/panodata/imagecast.svg | ||
:target: https://github.com/panodata/imagecast | ||
|
||
| | ||
.. imagecast-readme: | ||
######### | ||
Imagecast | ||
######### | ||
|
||
|
||
***** | ||
About | ||
***** | ||
Imagecast modifies images, optionally serving them via HTTP API. | ||
|
||
There might still be dragons. | ||
|
||
|
||
******* | ||
Install | ||
******* | ||
|
||
Prerequisites | ||
============= | ||
:: | ||
|
||
pip install imagecast | ||
|
||
With service API:: | ||
|
||
pip install imagecast[service] | ||
|
||
|
||
******** | ||
Features | ||
******** | ||
- Colorspace conversion: monochrome, grayscale | ||
- Cropping with negative right/bottom padding | ||
- Resizing while keeping aspect ratio | ||
- Output format: Any image formats or bytes | ||
- HTTP API | ||
|
||
|
||
******** | ||
Synopsis | ||
******** | ||
:: | ||
|
||
# Display on screen | ||
imagecast --uri="$IMGURL" --display | ||
|
||
# Colorspace reduction to bi-level with threshold, output as bytes | ||
imagecast --uri="$IMGURL" --monochrome=200 --format=bytes | ||
|
||
# Colorspace reduction, cropping, resizing and format conversion | ||
imagecast --uri="$IMGURL" --grayscale --crop=40,50,-50,-40 --width=200 --save=test.png | ||
|
||
|
||
Example:: | ||
|
||
imagecast --uri="https://unsplash.com/photos/WvdKljW55rM/download?force=true" --monochrome=80 --crop=850,1925,-950,-900 --width=640 --display | ||
|
||
|
||
HTTP API | ||
======== | ||
Start the Imagecast service as daemon:: | ||
|
||
imagecast service | ||
|
||
Example:: | ||
|
||
/?uri=https%3A%2F%2Funsplash.com%2Fphotos%2FWvdKljW55rM%2Fdownload%3Fforce%3Dtrue&monochrome=80&crop=850,1925,-950,-900&width=640 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Imagecast modifies images, optionally serving them via HTTP API""" | ||
__appname__ = 'imagecast' | ||
__version__ = '0.0.0' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# -*- coding: utf-8 -*- | ||
# (c) 2020 Andreas Motl <andreas@hiveeyes.org> | ||
# License: GNU Affero General Public License, Version 3 | ||
import sys | ||
import json | ||
import logging | ||
|
||
from docopt import docopt, DocoptExit | ||
|
||
from imagecast import __appname__, __version__ | ||
from imagecast.core import ImageEngine | ||
from imagecast.util import normalize_options, setup_logging | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def run(): | ||
""" | ||
Imagecast modifies images, optionally serving them via HTTP API. | ||
Usage: | ||
imagecast --uri=<uri> [--monochrome=<threshold>] [--grayscale] [--width=<width>] [--height=<height>] [--crop=<cropbox>] [--display] [--format=<format>] [--dpi=<dpi>] [--save=<save>] | ||
imagecast service [--listen=<listen>] | ||
imagecast --version | ||
imagecast (-h | --help) | ||
Options: | ||
--uri=<uri> URI to image | ||
--monochrome=<threshold> Make image monochrome (bi-level) | ||
--grayscale Make image grayscale | ||
--width=<width> Resize image to given width | ||
--height=<height> Resize image to given height | ||
--crop=<cropbox> Crop image to (left,top,right,bottom) | ||
--display Display image | ||
--format=<format> Output format | ||
--dpi=<dpi> Output DPI. [Default: 72] | ||
--save=<save> Path to output file | ||
--listen=<listen> HTTP server listen address. [Default: localhost:9999] | ||
--version Show version information | ||
--debug Enable debug messages | ||
-h --help Show this screen | ||
Examples:: | ||
""" | ||
|
||
name = f'{__appname__} {__version__}' | ||
|
||
# Parse command line arguments | ||
options = normalize_options(docopt(run.__doc__, version=name)) | ||
|
||
# Setup logging | ||
debug = options.get('debug') | ||
log_level = logging.INFO | ||
if debug: | ||
log_level = logging.DEBUG | ||
setup_logging(log_level) | ||
|
||
# Debugging | ||
log.debug('Options: {}'.format(json.dumps(options, indent=4))) | ||
|
||
# Run service. | ||
if options.service: | ||
listen_address = options.listen | ||
log.info(f'Starting {name}') | ||
log.info(f'Starting web service on {listen_address}') | ||
from imagecast.api import start_service | ||
start_service(listen_address) | ||
return | ||
|
||
# Run command. | ||
|
||
if not (options.display or options.save or options.format): | ||
raise KeyError('Please specify one of "--display", "--save" or "--format"') | ||
|
||
ie = ImageEngine() | ||
ie.download(options.uri) | ||
ie.read() | ||
|
||
if options.monochrome: | ||
ie.monochrome(int(options.monochrome)) | ||
|
||
if options.grayscale: | ||
ie.grayscale() | ||
|
||
# (left, top, right, bottom) | ||
if options.crop: | ||
cropbox = map(int, options.crop.split(',')) | ||
ie.crop(cropbox) | ||
|
||
if options.width: | ||
ie.resize_width(int(options.width)) | ||
if options.height: | ||
ie.resize_height(int(options.height)) | ||
|
||
dpi = int(options.dpi) | ||
|
||
if options.display: | ||
ie.display() | ||
elif options.save: | ||
ie.image.save(options.save, dpi=(dpi, dpi)) | ||
elif options.format: | ||
if options.format == 'bytes': | ||
buffer = ie.to_bytes() | ||
else: | ||
buffer = ie.to_buffer(options.format, dpi) | ||
sys.stdout.buffer.write(buffer) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# -*- coding: utf-8 -*- | ||
# (c) 2020 Andreas Motl <andreas@hiveeyes.org> | ||
# License: GNU Affero General Public License, Version 3 | ||
import io | ||
import requests | ||
from PIL import Image | ||
|
||
|
||
class ImageEngine: | ||
|
||
def __init__(self): | ||
self.data = None | ||
self.image = None | ||
|
||
def download(self, url): | ||
response = requests.get(url) | ||
self.data = response.content | ||
|
||
def read(self): | ||
self.image = Image.open(io.BytesIO(self.data)) | ||
|
||
def monochrome(self, threshold): | ||
|
||
#self.image = self.image.convert('1') | ||
#self.image = self.image.convert('1', dither=False) | ||
|
||
fn = lambda x: 255 if x > threshold else 0 | ||
self.image = self.image.convert('L').point(fn, mode='1') | ||
|
||
def grayscale(self): | ||
self.image = self.image.convert('L') | ||
|
||
def resize_width(self, width): | ||
size = self.image.size | ||
wpercent = (width / float(size[0])) | ||
height = int((float(size[1]) * float(wpercent))) | ||
self.image = self.image.resize((width, height), resample=Image.ANTIALIAS) | ||
|
||
def resize_height(self, height): | ||
size = self.image.size | ||
hpercent = (height / float(size[1])) | ||
width = int((float(size[0]) * float(hpercent))) | ||
self.image = self.image.resize((width, height), resample=Image.ANTIALIAS) | ||
|
||
def crop(self, box): | ||
size = self.image.size | ||
(left, top, right, bottom) = box | ||
if right < 0: | ||
right = size[0] + right | ||
if bottom < 0: | ||
bottom = size[1] + bottom | ||
box = (left, top, right, bottom) | ||
self.image = self.image.crop(box) | ||
|
||
def display(self): | ||
self.image.show() | ||
|
||
def to_bytes(self): | ||
return self.image.tobytes() | ||
|
||
def to_buffer(self, format, dpi): | ||
buffer = io.BytesIO() | ||
self.image.save(buffer, format=format, dpi=(dpi, dpi)) | ||
return buffer.getvalue() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# -*- coding: utf-8 -*- | ||
# (c) 2019-2020 Andreas Motl <andreas@terkin.org> | ||
# License: GNU Affero General Public License, Version 3 | ||
import sys | ||
import logging | ||
from munch import munchify | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def setup_logging(level=logging.INFO): | ||
log_format = '%(asctime)-15s [%(name)-22s] %(levelname)-7s: %(message)s' | ||
logging.basicConfig( | ||
format=log_format, | ||
stream=sys.stderr, | ||
level=level) | ||
|
||
|
||
def configure_http_logging(options): | ||
# Control debug logging of HTTP requests. | ||
|
||
if options.http_logging: | ||
log_level = log.getEffectiveLevel() | ||
else: | ||
log_level = logging.WARNING | ||
|
||
requests_log = logging.getLogger('requests') | ||
requests_log.setLevel(log_level) | ||
|
||
requests_log = logging.getLogger('urllib3.connectionpool') | ||
requests_log.setLevel(log_level) | ||
|
||
|
||
def normalize_options(options): | ||
normalized = {} | ||
for key, value in options.items(): | ||
|
||
# Add primary variant. | ||
key = key.strip('--<>') | ||
normalized[key] = value | ||
|
||
# Add secondary variant. | ||
key = key.replace('-', '_') | ||
normalized[key] = value | ||
|
||
return munchify(normalized) |