From 3873c29448d8264660a4997ea49da2944b2b8e57 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 31 Oct 2012 23:33:59 -0700 Subject: [PATCH] artresizer (#64): helper functions, not classes The previous method was to change self.__class__ dynamically to make __init__ instantiate different classes. This new way, which uses bare functions instead of separate functor-like classes, instead just forwards the resize() call to a module-global implementation based on self.method. Additionally, the semantics of ArtResizer have changed. Clients now *always* call resize() and proxy_url(), regardless of method. The method makes *one* of these a no-op. This way, clients need not manually inspect which method is being used. --- beets/util/artresizer.py | 138 ++++++++++++++++++++++----------------- beetsplug/embedart.py | 7 +- beetsplug/fetchart.py | 6 +- 3 files changed, 83 insertions(+), 68 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 19a4a4ebc..b078a19b9 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -18,7 +18,6 @@ public resizing proxy if neither is available. import urllib import subprocess import os -import shutil from tempfile import NamedTemporaryFile import logging @@ -70,65 +69,97 @@ def temp_file_for(path): return f.name -class PilResizer(object): - def resize(self, maxwidth, path_in, path_out=None): - """Resize using Python Imaging Library (PIL). Return the output path - of resized image. - """ - from PIL import Image - if not path_out: - path_out = temp_file_for(path_in) - try: - im = Image.open(path_in) - size = maxwidth, maxwidth - im.thumbnail(size, Image.ANTIALIAS) - im.save(path_out) - return path_out - except IOError: - log.error("Cannot create thumbnail for '%s'" % path_in) - - -class ImageMagickResizer(object): - def resize(self, maxwidth, path_in, path_out=None): - """Resize using ImageMagick command-line - tool. Return the output path of resized image. - """ - if not path_out: - path_out = temp_file_for(path_in) - - # widthxheight> Shrinks images with dimension(s) larger than the - # corresponding width and/or height dimension(s). - # "only shrink flag" is prefixed by ^ escape char for Windows compat. - cmd = ['convert', path_in, '-resize', - '{0}x^>'.format(maxwidth), path_out] - call(cmd) +def pil_resize(self, maxwidth, path_in, path_out=None): + """Resize using Python Imaging Library (PIL). Return the output path + of resized image. + """ + from PIL import Image + if not path_out: + path_out = temp_file_for(path_in) + try: + im = Image.open(path_in) + size = maxwidth, maxwidth + im.thumbnail(size, Image.ANTIALIAS) + im.save(path_out) return path_out + except IOError: + log.error("Cannot create thumbnail for '%s'" % path_in) + + +def im_resize(self, maxwidth, path_in, path_out=None): + """Resize using ImageMagick's ``convert`` tool. + tool. Return the output path of resized image. + """ + if not path_out: + path_out = temp_file_for(path_in) + + # "-resize widthxheight>" shrinks images with dimension(s) larger + # than the corresponding width and/or height dimension(s). The > + # "only shrink" flag is prefixed by ^ escape char for Windows + # compatability. + cmd = ['convert', path_in, + '-resize', '{0}x^>'.format(maxwidth), path_out] + call(cmd) + return path_out + + +BACKEND_FUNCS = { + PIL: pil_resize, + IMAGEMAGICK: im_resize, +} class ArtResizer(object): """A singleton class that performs image resizes. """ - def __init__(self): - """ArtResizer factory method""" - self.method = self.set_method() + def __init__(self, method=None): + """Create a resizer object for the given method or, if none is + specified, with an inferred method. + """ + self.method = method or self._guess_method() + log.debug("ArtResizer method is {0}".format(self.method)) + + def resize(self, maxwidth, path_in, path_out=None): + """Manipulate an image file according to the method, returning a + new path. For PIL or IMAGEMAGIC methods, resizes the image to a + temporary file. For WEBPROXY, returns `path_in` unmodified. + """ + if self.local: + func = BACKEND_FUNCS[self.method] + return func(maxwidth, path_in, path_out) + else: + return path_in - if self.method == PIL: - self.__class__ = PilResizer - elif self.method == IMAGEMAGICK: - self.__class__ = ImageMagickResizer - log.debug("ArtResizer method is %s" % self.__class__) + def proxy_url(self, maxwidth, url): + """Modifies an image URL according the method, returning a new + URL. For WEBPROXY, a URL on the proxy server is returned. + Otherwise, the URL is returned unmodified. + """ + if self.local: + return url + else: + return resize_url(url, maxwidth) - def set_method(self): - """Set the most appropriate resize method. Use PIL if present, else - check if ImageMagick is installed. - If none is available, use a web proxy.""" + @property + def local(self): + """A boolean indicating whether the resizing method is performed + locally (i.e., PIL or IMAGEMAGICK). + """ + return self.method in BACKEND_FUNCS + @staticmethod + def _guess_method(): + """Determine which resizing method to use. Returns PIL, + IMAGEMAGICK, or WEBPROXY depending on available dependencies. + """ + # Try importing PIL. try: __import__('PIL', fromlist=['Image']) return PIL except ImportError: pass + # Try invoking ImageMagick's "convert". try: out = subprocess.check_output(['convert', '--version']).lower() if 'imagemagick' in out: @@ -136,24 +167,9 @@ class ArtResizer(object): except subprocess.CalledProcessError: pass # system32/convert.exe may be interfering + # Fall back to Web proxy method. return WEBPROXY - def resize(self, maxwidth, url, path_out=None): - """Resize using web proxy. Return the output path of resized image. - """ - - reqUrl = resize_url(url, maxwidth) - try: - fn, headers = urllib.urlretrieve(reqUrl) - except IOError: - log.debug('error fetching resized image') - return - - if not path_out: - path_out = get_temp_file_out(fn) - shutil.copy(fn, path_out) - return path_out - # Singleton instantiation. inst = ArtResizer() diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 8c9e00d68..0a7a48403 100755 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -29,7 +29,6 @@ def _embed(path, items): """ if options['maxwidth']: path = artresizer.inst.resize(options['maxwidth'], syspath(path)) - log.debug('Resize album art to %s before embedding' % path) data = open(syspath(path), 'rb').read() kindstr = imghdr.what(None, data) @@ -64,10 +63,10 @@ class EmbedCoverArtPlugin(BeetsPlugin): options['maxwidth'] = \ int(ui.config_val(config, 'embedart', 'maxwidth', '0')) - if options['maxwidth'] and artresizer.inst.method == artresizer.WEBPROXY: + if options['maxwidth'] and not artresizer.inst.local: options['maxwidth'] = 0 - log.error("embedart: 'maxwidth' option ignored, " - "please install ImageMagick first") + log.error("embedart: ImageMagick or PIL not found; " + "'maxwidth' option ignored") def commands(self): # Embed command. diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index de4149580..0f36bdbe0 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -36,8 +36,8 @@ log = logging.getLogger('beets') def do_resize_url(func): def wrapper(url, maxwidth=None): """Returns url pointing to resized image instead of original one""" - if maxwidth and artresizer.inst.method == artresizer.WEBPROXY : - url = artresizer.resize_url(url, maxwidth) + if maxwidth: + url = artresizer.inst.proxy_url(url, maxwidth) return func(url) return wrapper @@ -174,7 +174,7 @@ def art_for_album(album, path, maxwidth=None, local_only=False): out = _fetch_image(url, maxwidth) - if maxwidth and artresizer.inst.method != artresizer.WEBPROXY : + if maxwidth: artresizer.inst.resize(maxwidth, out, out) return out