Package: imagehelper
OverviewTheimagehelperpackage offers a simple interface for image resizing,
optimizing and uploading image assets. Core image resizing operations are handled
by thePillow(PIL) package; S3 uploading is handled byboto3, and there are
hooks for optimizing the images with the commandline tools:advpng,gifsicle,jpegtran,jpegoptim,optipngandpngcrush.This library does not actually resize the images, it is used to define "recipes"
for resizing images with Pillow and uploading them to S3 with boto3.Aboutimagehelperis a fork of internal image helping routines that were built for
FindMeOn.com around 2005. It has been actively maintained as an open source
project since at least 2012.imagehelperallows you to define a schema for resizing images as a simpledict, and will then easily resize them.imagehelperalso supports uploading the resized images - and an archival
version - onto Amazon's S3 service.imagehelperrequiresPillow. Earlier versions relied onPILor supported
both. This really is an old package!This package will try to importboto3for communicating with AmazonS3.
If you don't want to use S3, no worries - that is only optional and you can save
to a local file.The package was originally aimed at thumbnails, but it works for all resizing
needs that are aimed at downsampling images.If you have optimization applications likegifsicle,pngcrushandjpegtraninstalled in your environment, you can 'optimize' the output (and archive) files.This is a barebones package that has NO FRAMEWORK DEPENDENCIES - which is a good
thing. You define image transformation recipes using a simple dict, the
package does the rest.This package also tries to avoid writing to disk whenever possible, tempfiles
(spooled) are avoided unless an external program is called. This package tries to
pipe everything through file-like in-memory objects.I could only find a single tool for resizing thumbnails on PyPi that did not
require a framework, and that's really annoying.The package is a bit awkard to use for a single task, but it was designed for
repetitive tasks - as in a web application.A typical usage is illustrated in the sections below. Also check thedemo.pyfile to see how flexible this can be.This package has been used in production for over a decade.It supports Python2.7 and Python3. A lot of things could be done better and
should be done better, but this works and is relatively fast.Why ?Imagine that you have a site that allows for user generated uploads, or you
want to make video stills...You can create a schema of image sizes...IMAGE_SIZES = {
    'thumb': {
        'width': 32,
        'height': 32,
        'save_quality': 50,
        'suffix': 't1',
        'format':'JPEG',
        'constraint-method': 'fit-within',
        'filename_template': '%(guid)s-120x120.%(format)s',
    },
    'og:image': {
        'width': 200,
        'height': 200,
        'save_quality': 50,
        'suffix': 'og',
        'format':'JPEG',
        'constraint-method': 'ensure-minimum',
        'filename_template': '%(guid)s-og.%(format)s',
    },
}And easily upload them:# create some configs in your app

# config object for IMAGE_SIZES
resizerConfig = imagehelper.resizer.ResizerConfig(
    resizesSchema=IMAGE_SIZES,
    optimize_original=True,
    optimize_resized=True,
)

# config object for S3
saverConfig= imagehelper.saver.s3.SaverConfig(
    key_public = AWS_KEY_PUBLIC,
    key_private = AWS_KEY_SECRET,
    bucket_public_name = AWS_BUCKET_PUBLIC,
    bucket_archive_name = AWS_BUCKET_ARCHIVE,
)

# create some factories.
# factories are unnecessary. they just generate the workhorse objects for you
# they're very useful for cutting down code
# build one, then stash in your app

USE_FACTORY = True
if USE_FACTORY:
    rFactory = imagehelper.resizer.ResizerFactory(resizerConfig=resizerConfig)
    s3Factory = imagehelper.saver.s3.s3ManagerFactory(saverConfig=saverConfig, resizerConfig=rConfig, saverLogger=saverLogger)

    resizer = rFactory.resizer()
    s3Manager = s3Factory.manager()

else:
    resizer = imagehelper.resizer.Resizer(resizerConfig=resizerConfig)
    s3Manager = imagehelper.saver.s3.s3Manager(saverConfig=saverConfig, resizerConfig=resizerConfig, saverLogger=saverLogger)

# resize !
resizedImages = resizer.resize(imagefile=get_imagefile())

# upload the resized items
uploaded_files = s3Manager.files_save(resizedImages, guid="123")

# want to delete them?
deleted = s3Manager.files_delete(uploaded_files)Behind the scenes, imagehelper does all the math and uploading.Resizing Optionsfit-withinResizes item to fit within the bounding box, on both height and width.
The resulting image will be the size of the bounding box or smaller.fit-within:crop-toResizes the item along whichever axis ensures the bounding box is 100% full, then crops.
The resulting image will be the size of the bounding box.fit-within:ensure-widthResizes item to fit within the bounding box, scaling height to ensure 100% width.
The resulting image will be the size of the bounding box.fit-within:ensure-heightResizes item to fit within the bounding box, scaling width to ensure 100% height.
The resulting image will be the size of the bounding box.smallest:ensure-minimumResizes the item to cover the bounding box on both axis.
One dimension may be larger than the bounding box.exact:no-resizeDo not scale! Raises an exception if a scale must be made.
This is a convenience for just saving/re-encoding files.
For example, 100x100 must receive an image that is 100x100.exact:proportionAttempt to scale the image to an exact size. Raise an exception if it can't.
Usually this is used to resample a 1:1 image, however this might be used to drop
an image to a specific proportion. i.e. 300x400 can scale to 30x40, 300x400
but not 30x50.Usage...Check out the demo.py module - it offers a narrative demo of how to use the
package. Be sure to include some Amazon S3 credentials in your environment.imagehelper is NOT designed for one-off resizing needs.
it's designed for a use in applications where you're repeatedly doing the same resizing.The general program flow is this:CreateConfigurationobjects to hold instructionsCreateFactoryobjects to hold theConfigurationobjects.Obtain aWorkerobject from theFactoryto do the actual work (resizing or uploading)You should typically create "Configuration" and "Factory" objects during
application startup, and create/destroy a work for each request or event.Here's a more in depth descriptionCreate a dict of "photo resizes" describing your schema.keys prepended withsave_are passed on to PIL during the call tosave(the prefix is removed)you can decide what type of resizing you want.  sometimes you want to crop,
other times you want to fit within a box, other times you want to ensure a
height or width. this makes your designers happy.create an array ofimage_resizes_selected-- the keys in the above schema
you want to resize.you can pass these arguments into the routines themselves, or generate aimagehelper.resizer.ResizerConfigobject or aimagehelper.resizer.ResizerFactorythat you stash into your application.If you're saving to AmazonS3, create animagehelper.saver.s3.SaverConfigconfig object to store your info. note that you can specify a public and
private bucket.resized thumbnails are saved to the public bucketthe original item is optionally saved to the archive, which is not viewable to the public.
this is so you can do different sizing schemes in the future.You can define your own Amazon S3 logger, a class that provides two methods:class SaverLogger(object):
def log_save(self, bucket_name=None, key=None, file_size=None, file_md5=None):
pass
def log_delete(self, bucket_name=None, key=None):
passThis will allow you to log what is uploaded into Amazon AWS on your side.
This is hugely helpful, because Amazon uploads are not transaction safe to your
application logic. There are some built-in precautions for this... but it's best
to play things safely.Items are currented saved to Amazon S3 as such:Public:Template:%(guid)s-%(suffix)s.%(format)sTokens:guid: you must supply a guid for the filesuffix: this is set in the resize schemaformat: this is dictated by the PIL format typeArchive:Template:%(guid)s.%(format)sTokens:guid: you must supply a guid for the fileformat: this is dictated by the original format type PIL foundHere is an example photo_resize schema:'jpeg_thumbnail-120': {
    'width': 120,
    'height': 120,
    'save_quality': 50,
    'suffix': 't120',
    'format':'JPEG',
    'constraint-method': 'fit-within',
    's3_bucket_public': 'my-test',
    'filename_template': '%(guid)s-%(suffix)s.%(format)s',
},This would create a file on Amazon S3 with aguidyou supply, such as123123123:/my-test/123123123-t120.jpg
_bucket_/_guid_-_suffix_._format_string templates may be used to affect how this is saved. read the source for more info.Transactional SupportIf you upload something viaimagehelper.saver.s3.S3Uploader().s3_upload(), the
task is considered to be "all or nothing".The actual uploading occurs within a try/except block, and a failure will "rollback"
and delete everything that has been successfully uploaded.If you want to integrate with something like the Zopetransactionpackage,imagehelper.saver.s3.S3Uploader().files_delete()is a public function that
expects as input the output of thes3_uploadfunction -- adictoftupleswhere thekeysare resize names (from the schema) and thevaluesare the(filename, bucket).You can also define a custom subclass ofimagehelper.saver.s3.SaverLoggerthat
supports the following methods:log_save(self,bucket_name=None,key=None,file_size=None,file_md5=None)log_delete(self,bucket_name=None,key=None)Every successful 'action' is sent to the logger. A valid transaction to upload 5
sizes will have 5 calls tolog_save, an invalid transaction will have alog_deletecall for every successful upload.This was designed for a variety of use cases:log activity to a file or non-transactional database connection, you get some
efficient bookkeeping of s3 activity and can audit those files to ensure there
is no orphan data in your s3 buckts.log activity to StatsD or another metrics app to show how much activity goes onFAQ - package componentserrors- custom exceptionsimage_wrapper- actual image reading/writing, resize operationsresizer- manage resizing operationss3- manage s3 communicationutils- miscellaneous utility fucntionsFAQ - deleting existing files ?If you don't have a current mapping of the files to delete in s3 but you do have
the archive filename and a guid, you can easily generate what they would be based
off a resizerConfig/schema and the archived filename.## fake the sizes that would be generated off a resize
resizer = imagehelper.resizer.Resizer(
    resizerConfig=resizerConfig,
    optimize_original=True,
    optimize_resized=True,
)
fakedResizedImages = resizer.fake_resultset(
    original_filename=archive_filename
)

## generate the filenames
deleter = imagehelper.saver.s3.SaverManager(
    saverConfig=saverConfig, resizerConfig=resizerConfig
)
targetFilenames = build.generate_filenames(fakedResizedImages, guid)Theoriginal_filenameis needed infake_resultset, because a resultset tracks
the original file and it's type. As of the0.1.0branch, only the extension
of the filename is utilized.FAQ - validate uploaded image ?This is simple.Create a dumb resizer factorynullResizerFactory = imagehelper.resizer.ResizerFactory()Validate ittry:
resizer = nullResizerFactory.resizer(
imagefile = uploaded_image_file,
)
except imagehelper.errors.ImageError_Parsing as exc:
raise ValueError('Invalid Filetype')grab the original file for advanced opsresizerImage = resizer.get_original()
if resizerImage.file_size > MAX_FILESIZE_PHOTO_UPLOAD:
raise ValueError('Too Big!')Passing an imagefile toResizerFactory.resizerorResizer.__init__will
register the file with the resizer. This action creates animage_wrapper.ImageWrapperobject from the file, which contains the original
file and a PIL/Pillow object. If PIL/Pillow can not read the file, an error
will be raised.FAQ - what sort of file types are supported ?All the reading and resizing of image formats happens in PIL/Pillow.imagehelper tries to support most common file objectsimagehelper.image_wrapper.ImageWrapperour core class for reading files,
supports reading the following file typesfile (native python object, i.e.types.FileType`)cgi.FieldStorageStringIO.StringIO,cStringIO.InputType,cStringIO.OutputTypeWe try to "be kind and rewind" and callseek(0)on the underlying file when
appropriate - but sometimes we forget.The resize operations accepts the following file kwargs:imagefile-- one of the above file objectsimageWrapper-- an instance ofimagehelper.image_wrapper.ImageWrapperfile_b64-- a base64 encoded file datastream. this will decoded into acStringIOobject for operations.FAQ - using celery ?Celery message brokers require serialized data.In order to pass the task to celery, you will need to serialize/deserialize the
data. imagehelper provides convenience functionality for thisnullResizerFactory = imagehelper.resizer.ResizerFactory()
resizer = nullResizerFactory.resizer(
    imagefile = uploaded_file,
)

# grab the original file for advanced ops
resizerImage = resizer.get_original()

# serialize the image
instructions = {
    'image_md5': resizerImage.file_md5,
    'image_b64': resizerImage.file_b64,
    'image_format': resizerImage.format,
}

# send to celery
deferred_task = celery_tasks.do_something.apply_async((id, instructions,))


# in celery...
@task
def do_something(id, instructions):
    ## resize the images
    resizer = resizerFactory.resizer(
        file_b64 = instructions['image_b64'],
    )
    resizedImages = resizer.resize()How are optimizations handled?Image optimizations are handled by piping the image through external programs.
The idea (and settings) were borrowed from the mac app
ImageOptim (https://github.com/pornel/ImageOptimorhttps://imageoptim.com/)The default image Optimizations are LOSSLESSFine-grained control of image optimization strategies is achieved on a package
level basis. In the future this could be handled within configuration objects.
This strategy was chosen for 2 reasons:The config objects were getting complexChoosing an image optimization level is more of a "machine" concern than a
"program" concern.Not everyone has every program installed on their machines.imagehelperwill attempt to autodetect what is available on the first
invocation of.optimizeIf you are on a forking server, you can do this before the fork and save yourself
a tiny bit of cpu cycles. yay.import imagehelper
imagehelper.image_wrapper.autodetect_support()Theautodetect_supportrouting will setimagehelper.image_wrapper[ program ]['available']If you want to enable/disable them manuall, just editimagehelper.image_wrapper[ program ]['use']You can also set a custom binaryimagehelper.image_wrapper[ program ]['binary']Autodetection is handled by invoking each program's help command to see if it is
installed.JPEGjpegs are optimized in a two-stage process.jpegtran is used to do an initial optimization and ensure a progressive jpeg.
all jpeg markers are preserved.
jpegoptim is used on the output of the above, in this stage all jpeg markers
are removed.The exact arguments are:"""jpegtran -copy all -optimize -progressive -outfile %s %s""" % (fileOutput.name, fileInput.name)
"""jpegoptim --strip-all -q %s""" % (fileOutput.name, )GIFGifsicle is given the following params
-O3
--no-comments
--no-names
--same-delay
--same-loopcount
--no-warningsThe03level can be affected by changing the package level variable to a new integer (1-3)imagehelper.image_wrapper.OPTIMIZE_GIFSICLE_LEVEL = 3PNGThe package will try to use multiple png operators in sequence.You can disable any png operator by changing the package level variable toFalseOPTIMIZE_PNGCRUSH_USE = True
OPTIMIZE_OPTIPNG_USE = True
OPTIMIZE_ADVPNG_USE = Truepngcrushpngcrush -rem alla -nofilecheck -bail -blacken -reduce -ccoptipngoptipng -i0 -o3The optipng level can be set by setting the package level variable to a new integer (1-3)OPTIMIZE_OPTIPNG_LEVEL = 3  # 6 would be bestadvpngadvpng -4 -zThe advpng level can be set by setting the package level variable to a new integer (1-4)OPTIMIZE_ADVPNG_LEVEL = 4  # 4 is maxwhat external libraries are needed to be installedNone. These are all optional!  But here you goubuntuapt-get install advancecomp  # advpng
apt-get install gifsicle
apt-get install libjpeg-turbo-progs  # jpegtran
apt-get install jpegoptim
apt-get install optipng
apt-get install pngcrushToDoSeeTODO.txtLicenseThe code is licensed under the BSD license.The sample image is licensed under the Creative Commons
Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0)http://creativecommons.org/licenses/by-nc-nd/3.0/