Skip to content

Commit

Permalink
First version of program
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Jun 6, 2020
1 parent 351f5a7 commit e47b5d1
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 0 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
@@ -0,0 +1,12 @@
###################
Imagecast changelog
###################


in progress
===========


2020-06-06 0.1.0
================
- First version, with command line interface
80 changes: 80 additions & 0 deletions README.rst
@@ -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
3 changes: 3 additions & 0 deletions imagecast/__init__.py
@@ -0,0 +1,3 @@
"""Imagecast modifies images, optionally serving them via HTTP API"""
__appname__ = 'imagecast'
__version__ = '0.0.0'
107 changes: 107 additions & 0 deletions imagecast/cli.py
@@ -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)
64 changes: 64 additions & 0 deletions imagecast/core.py
@@ -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()
46 changes: 46 additions & 0 deletions imagecast/util.py
@@ -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)

0 comments on commit e47b5d1

Please sign in to comment.