Monday, June 28, 2010


# -*- coding: utf-8 -*-
# Copyright (c) 2008 Sebastian Wiesner <>

# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See


    Cmdline script, which uploads to different image hosters.

    :author: Sebastian Wiesner
    :copyright: 2008 by Sebastian Wiesner
    :license: WTFPL

from __future__ import print_function

import os
import sys
import imghdr
import urllib2
import webbrowser
import subprocess
from cStringIO import StringIO

import argparse
import ClientForm
import lxml.html
from lxml.cssselect import CSSSelector

def detect_mimetype(img):
    """Detects mime type of `img`.  Returns None, if mime type couldn't be

    :type img: str or file-like object supporting ``read``, ``tell`` and
    format = imghdr.what(img)
    return ('image/' + format if format is not None else None)

class Service(object):
    """Abstract class for services.  Each deriving class must re-define the
    class variables.

    - `url`: Website of the service
    - `name`:  A descriptive name of the upload service

    def _fill_missing_arguments(self, image, mime_type, filename):
        """Fills missing arguments to upload_file.  If `mimetype` is
        ``None``, the mime type of `image` is guessed with

        If `filename` is ``None``, this method attempts to get the filename
        from the `name` attribute of the stream denoted by `image`.  If this
        fails, it sets the name to \"foo\".

        All three arguments are returned properly initialized.  Note, that
        the caller is responsible for proper closing of the returned
        file-like object.

        :returns: ``(image, mime_type, filename)``
        :returntype: ``(file-like object, str, str)``
        if mime_type is None:
            mime_type = detect_mimetype(image)
        if mime_type is None:
            raise IOError('Couldn\'t detect mimetype of {0}'.format(image))

        stream = (open(image, 'rb') if isinstance(image, basestring)
                  else image)

        if filename is None:
            filename = getattr(stream, 'name', None)
        if filename is None:
            raise ValueError('No filename given')

        return stream, mime_type, os.path.basename(filename)

    def upload_image(self, image, mime_type=None, filename=None):
        """Uploads `image` with `mime_type` as `filename`.

        Returns ``(image_link, bbcode, links)``, where ``image_link`` is
        direct link to the image, ``bbcode`` a bbcode line useful for
        webforums and ``links`` the complete list of all links."""
        raise NotImplementedError()

    def __repr__(self):
        if hasattr(self, 'name'):
            return '<Service "{}" at {1}>'.format(self, id(self))
            return object.__repr__(self)

class WebFormService(Service):
    """Service, which parses web forms, fills them and returns a list of
    forms extracted from the web site.

    Subclasses of this class should have an attribute ``webform_url``,
    denoting the webform to parse, and must reimplement ``_get_links``.

    def upload_image(self, image, mime_type=None, filename=None):
        """Uploads `image` with `mime_type` as `filename`. Missing
        values are autodetected.
        html = self._get_webform_html()
        forms = ClientForm.ParseResponse(html, backwards_compat=False)
        upload_form = self._find_upload_webform(forms)
        stream, mime_type, filename = self._fill_missing_arguments(
            image, mime_type, filename)
            upload_form.add_file(stream, mime_type, filename)
            request =
            response = urllib2.urlopen(request)
            return self._get_links(response)

    def _fill_additional_form_fields(self, form):
        """Called to fill additional fields in `form`, if necessary.  The
        default implementation does nothing.

        The return value of this method is ignored.

    def _get_links(self, response):
        """Extracts all links from `response`, which is a urllib2 response
        object as returned by ``urllib2.urlopen``.

        It must return ``(image_link, bbcode, links)``, where ``image_link``
        is the direct link to the image, ``bbcode`` is a bbcode line usable
        for webforums and ``links`` is the complete list of links."""
        raise NotImplementedError()

    def _get_webform_html(self):
        """Returns html code of the upload webform.  The default
        implementation uses ``urllib2.urlopen`` with ``self.webform_url`` to
        retrieve the html code.

        Subclasses can reimplement this, to customize loading."""
        return urllib2.urlopen(self.webform_url)

    def _find_upload_webform(self, forms):
        """Finds the form which handles the upload.  The default
        implementation blindly iterates over all forms and returns the one,
        which has a file upload field.

        Raises `ClientForm.ControlNotFoundError`, if no upload webform
        was found."""
        for form in forms:
                return form
            except ClientForm.ControlNotFoundError, err:
                # throw away forms, that don't have an upload control
            raise err

class ImageBananaService(WebFormService):
    """Uploads images to"""

    name = 'ImageBanana'
    webform_url = url
    find_url_elements = CSSSelector('input .input_text')

    def _get_links(self, response):
        tree = lxml.html.parse(response)
        urls = [el.get('value') for el in self.find_url_elements(tree)]
        return urls[1], urls[3], urls

class UbuntuPicsService(WebFormService):
    """Uploads images to"""

    name = 'UbuntuPics'

    def _get_links(self, response):
        tree = lxml.html.parse(response)
        content = tree.getroot().get_element_by_id('content')
        direct = content.get_element_by_id('direct').get('value')
        bbcode = content.get_element_by_id('bbcode').get('value')
        urls = content.xpath('input/attribute::value')
        return direct, bbcode, urls

def copy_to_clipboard(text):
    """Copy `text` to clipboard."""
    p = subprocess.Popen(["xsel", "-i"], stdin=subprocess.PIPE)
    return p.communicate(text)

def get_all_services():
    objects = globals().itervalues()
    classes = (obj for obj in objects if isinstance(obj, type))
    services = (cls for cls in classes if issubclass(cls, Service))
    return dict(((, s) for s in services if hasattr(s, 'name')))

SERVICES = get_all_services()

class ListServices(argparse.Action):
    def __init__(self, *args, **kwargs):
        if not 'nargs' in kwargs:
            kwargs['nargs'] = 0
        super(ListServices, self).__init__(*args, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        table = []
        widths = [0, 0]
        for service in SERVICES.itervalues():
            row = [, service.url]
            widths = map(max, zip(widths, map(len, row)))
        line_tmpl = '{0[0]:<{1[0]}} - {0[1]:<{1[1]}}'
        for row in table:
            print(line_tmpl.format(row, widths))

def _parse_args():
    parser = argparse.ArgumentParser(epilog="""
(C) 2008  Sebastian Wiesner, licensed under the terms of WTFPL 2.""",
Uploads images. If you don't specify a file name, the image is read from
standard input. An image read from standard input will be called \"stdin\"
unless you specify an explicit filename using the --filename option.""")
    parser.add_argument('--list-services', action=ListServices,
                        help='List all upload services and exit.')
    parser.add_argument('image', nargs='?', help='Image to upload. '
                        '"-" means standard input, which is the default '
                        'if unspecified.',)
    upload_args = parser.add_argument_group('Upload settings',
                                            'How to upload a file?')
    upload_args.add_argument('-s', '--service', choices=SERVICES,
                             help='Load image up to SERVICE. Defaults to '
                             '"UbuntuPics". The default can be changed '
                             'through the UPLOAD_IMAGE_DEFAULT_SERVICE '
                             'environment variable.')
    upload_args.add_argument('-f', '--filename', metavar='NAME',
                             help='Transmits NAME as filename to the '
                             'server. Defaults to local filename, or '
                             '"stdin", if image data comes from standard '
    upload_args.add_argument('-m', '--mime-type', metavar='TYPE',
                             help='Force mimetype to be TYPE. If '
                             'unspecified, mime type is auto-detected from '
                             'image format.')
    result_args = parser.add_argument_group('Result handling',
                                            'What to do with the result of '
                                            'the upload?')
    result_args.add_argument('-c', '--copy',
                             choices=['no', 'bbcode', 'image'],
                             help='What to copy into clipboard?')
    result_args.add_argument('-b', '--browser', action='store_true',
                             help='If set, image url is opened in '
    misc_args = parser.add_argument_group('Misc settings')
    misc_args.add_argument('-d', '--delete', action='store_true',
                           help='Delete the file after uploading (use with '
                           'care!)', dest='delete')
    def_service = os.environ.get('UPLOAD_IMAGE_DEFAULT_SERVICE',
    parser.set_defaults(copy='bbcode', service=def_service, image='-')
    return parser.parse_args()

def main():
        # setup localized environment
        import locale
        locale.setlocale(locale.LC_ALL, '')
        args = _parse_args()
        print('Uploading to {0.service}...'.format(args), end='\n'*2)
        # finalize command line arguments
        if args.filename is None:
            args.filename = (args.image if args.image != '-' else 'stdin')
        if args.image == '-':
            args.image = StringIO(
            # upload image and print return values
            service = SERVICES[args.service]()
            image, bbcode, urls = service.upload_image(
                args.image, args.mime_type, args.filename)
            print('bbcode:', bbcode)
            print('image:', image, end='\n'*2)
            for url in urls:
        except ClientForm.ControlNotFoundError:
            sys.exit('Website at {0.url} doesn\'t support file '
        if args.copy != 'no':
            except OSError as err:
        if args.browser:
        if args.delete:
    except KeyboardInterrupt:

if __name__ == '__main__':

upload_image – Command line image uploading

upload_image automates some image hosting sites to allow uploading files from command line.



    You need Python (at least version 2.6) installed and working.

    Installation of the script itself is rather easy, just place it somewhere in your $PATH. More complex is the installation of its dependencies:

    argparse: Easy and convenient command line parsing

    ClientForm: Webform automation

    lxml: HTML parsing and scraping

    All these packages are avialable on the PyPI: and can therefore be installed with easy_install:

    easy_install argparse ClientForm lxml>=2

    If you want to have clipboard support, you will also need to manually install the xsel utility.


    upload_image is very simple. It uploads the specified input file, or reads the image data from standard input. A single dash (-) as file name will read from standard input, too:

    # upload_image foobar.png

    There are some options that fine-tune the behaviour of this script. The --help gives an overview over all supported options, the most important are presented here.

    -f name, --filename name

    Sends name as filename to the server. This option defaults to the filename of the local file or to stdin, if data is read from standard input. The following will read a image file from standard input and send it to the server as foobar.png:

    # upload_image -f foobar.png < my_image.png

    -m type, --mime-type type

    Allows to overwrite the mimetype send to the server. Normally the type is detected from the image header using python's standard imghdr module.

    -s {UbuntuPics,ImageBanana}, --service service {UbuntuPics,ImageBanana}

    This option allows to specify the image hosting service where to upload the image file. Currently two services are supported, Image Banana and Ubuntu Pics. The default is read from UPLOAD_IMAGE_DEFAULT_SERVICE. If this is unset, Ubuntu Pics is used.


    Use of this script with any of the above services is at your own risk! The existence of this script doesn't mean, that the admins of these services appreciate its use with their sites.


    This environment variable provides the default service for upload_image. Set this in your shell initialisation file (like ~/.bashrc or ~/.zshrc) to switch to a specific service permanently.

    Note The -s option overrules this setting!

    After a successful upload the script parses all links returned by the website and prints them on standard output. The link pointing to the image itself and the phpbb-compatible bbcode are printed first. Two options exist to work with the results from the web server:

    -c {no,bbcode,image}, --copy {no,bbcode,image}

    This option selects a link to be copied into clipboard. This needs the xsel utility. no copies nothing, bbcode copies a phpbb-compatible bbcode into clipboard, image copies to full url to the image.

    -b, --browser

    This options opens the image in your favourite webbrowser. You should set $BROWSER to something reasonable for this to work correctly.


    You often want to quickly take some screenshot to illustrate your problems or explanations in some webforum or in the usenet. I use the following command line to accomplish this conventiently:

    import png:- | upload_image -f shot.png -c bbcode

    This takes a screenshot using the import utility from the ImageMagick toolkit to take a screenshot and write as PNG file to standard output. This output is then piped to upload_image, which uploads the image as shot.png and copies the bbcode link into clipboard for use in your webbrowser. If creating a screenshot for the usenet, I use -c image instead to copy the full image url.

Additional services

    Extending the script to support additional services shouldn't be that difficult. Contact me or send me a patch, if you want support for another image hosting site.


    This programm is free software, you can redistribute and/or modify it under the terms of the WTFPL 2.

No comments: