# # The Python Imaging Library. # $Id: Image.py 2933 2006-12-03 12:08:22Z fredrik $ # # the Image class wrapper # # partial release history: # 1995-09-09 fl Created # 1996-03-11 fl PIL release 0.0 (proof of concept) # 1996-04-30 fl PIL release 0.1b1 # 1999-07-28 fl PIL release 1.0 final # 2000-06-07 fl PIL release 1.1 # 2000-10-20 fl PIL release 1.1.1 # 2001-05-07 fl PIL release 1.1.2 # 2002-03-15 fl PIL release 1.1.3 # 2003-05-10 fl PIL release 1.1.4 # 2005-03-28 fl PIL release 1.1.5 # 2006-12-02 fl PIL release 1.1.6 # # Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. # Copyright (c) 1995-2006 by Fredrik Lundh. # # See the README file for information on usage and redistribution. # VERSION = "1.1.6" try: import warnings except ImportError: warnings = None class _imaging_not_installed: # module placeholder def __getattr__(self, id): raise ImportError("The _imaging C module is not installed") try: # give Tk a chance to set up the environment, in case we're # using an _imaging module linked against libtcl/libtk (use # __import__ to hide this from naive packagers; we don't really # depend on Tk unless ImageTk is used, and that module already # imports Tkinter) __import__("FixTk") except ImportError: pass try: # If the _imaging C module is not present, you can still use # the "open" function to identify files, but you cannot load # them. Note that other modules should not refer to _imaging # directly; import Image and use the Image.core variable instead. import _imaging core = _imaging del _imaging except ImportError, v: core = _imaging_not_installed() if str(v)[:20] == "Module use of python" and warnings: # The _imaging C module is present, but not compiled for # the right version (windows only). Print a warning, if # possible. warnings.warn( "The _imaging extension was built for another version " "of Python; most PIL functions will be disabled", RuntimeWarning ) import ImageMode import ImagePalette import os, stat, string, sys # type stuff from types import IntType, StringType, TupleType try: UnicodeStringType = type(unicode("")) ## # (Internal) Checks if an object is a string. If the current # Python version supports Unicode, this checks for both 8-bit # and Unicode strings. def isStringType(t): return isinstance(t, StringType) or isinstance(t, UnicodeStringType) except NameError: def isStringType(t): return isinstance(t, StringType) ## # (Internal) Checks if an object is a tuple. def isTupleType(t): return isinstance(t, TupleType) ## # (Internal) Checks if an object is an image object. def isImageType(t): return hasattr(t, "im") ## # (Internal) Checks if an object is a string, and that it points to a # directory. def isDirectory(f): return isStringType(f) and os.path.isdir(f) from operator import isNumberType, isSequenceType # # Debug level DEBUG = 0 # # Constants (also defined in _imagingmodule.c!) NONE = 0 # transpose FLIP_LEFT_RIGHT = 0 FLIP_TOP_BOTTOM = 1 ROTATE_90 = 2 ROTATE_180 = 3 ROTATE_270 = 4 # transforms AFFINE = 0 EXTENT = 1 PERSPECTIVE = 2 QUAD = 3 MESH = 4 # resampling filters NONE = 0 NEAREST = 0 ANTIALIAS = 1 # 3-lobed lanczos LINEAR = BILINEAR = 2 CUBIC = BICUBIC = 3 # dithers NONE = 0 NEAREST = 0 ORDERED = 1 # Not yet implemented RASTERIZE = 2 # Not yet implemented FLOYDSTEINBERG = 3 # default # palettes/quantizers WEB = 0 ADAPTIVE = 1 # categories NORMAL = 0 SEQUENCE = 1 CONTAINER = 2 # -------------------------------------------------------------------- # Registries ID = [] OPEN = {} MIME = {} SAVE = {} EXTENSION = {} # -------------------------------------------------------------------- # Modes supported by this version _MODEINFO = { # NOTE: this table will be removed in future versions. use # getmode* functions or ImageMode descriptors instead. # official modes "1": ("L", "L", ("1",)), "L": ("L", "L", ("L",)), "I": ("L", "I", ("I",)), "F": ("L", "F", ("F",)), "P": ("RGB", "L", ("P",)), "RGB": ("RGB", "L", ("R", "G", "B")), "RGBX": ("RGB", "L", ("R", "G", "B", "X")), "RGBA": ("RGB", "L", ("R", "G", "B", "A")), "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), # Experimental modes include I;16, I;16B, RGBa, BGR;15, # and BGR;24. Use these modes only if you know exactly # what you're doing... } if sys.byteorder == 'little': _ENDIAN = '<' else: _ENDIAN = '>' _MODE_CONV = { # official modes "1": ('|b1', None), "L": ('|u1', None), "I": ('%si4' % _ENDIAN, None), # FIXME: is this correct? "I;16": ('%si2' % _ENDIAN, None), "F": ('%sf4' % _ENDIAN, None), # FIXME: is this correct? "P": ('|u1', None), "RGB": ('|u1', 3), "RGBX": ('|u1', 4), "RGBA": ('|u1', 4), "CMYK": ('|u1', 4), "YCbCr": ('|u1', 4), } def _conv_type_shape(im): shape = im.size[::-1] typ, extra = _MODE_CONV[im.mode] if extra is None: return shape, typ else: return shape+(extra,), typ MODES = _MODEINFO.keys() MODES.sort() # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16B") ## # Gets the "base" mode for given mode. This function returns "L" for # images that contain grayscale data, and "RGB" for images that # contain color data. # # @param mode Input mode. # @return "L" or "RGB". # @exception KeyError If the input mode was not a standard mode. def getmodebase(mode): return ImageMode.getmode(mode).basemode ## # Gets the storage type mode. Given a mode, this function returns a # single-layer mode suitable for storing individual bands. # # @param mode Input mode. # @return "L", "I", or "F". # @exception KeyError If the input mode was not a standard mode. def getmodetype(mode): return ImageMode.getmode(mode).basetype ## # Gets a list of individual band names. Given a mode, this function # returns a tuple containing the names of individual bands (use # {@link #getmodetype} to get the mode used to store each individual # band. # # @param mode Input mode. # @return A tuple containing band names. The length of the tuple # gives the number of bands in an image of the given mode. # @exception KeyError If the input mode was not a standard mode. def getmodebandnames(mode): return ImageMode.getmode(mode).bands ## # Gets the number of individual bands for this mode. # # @param mode Input mode. # @return The number of bands in this mode. # @exception KeyError If the input mode was not a standard mode. def getmodebands(mode): return len(ImageMode.getmode(mode).bands) # -------------------------------------------------------------------- # Helpers _initialized = 0 ## # Explicitly loads standard file format drivers. def preinit(): "Load standard file format drivers." global _initialized if _initialized >= 1: return try: import BmpImagePlugin except ImportError: pass try: import GifImagePlugin except ImportError: pass try: import JpegImagePlugin except ImportError: pass try: import PpmImagePlugin except ImportError: pass try: import PngImagePlugin except ImportError: pass # try: # import TiffImagePlugin # except ImportError: # pass _initialized = 1 ## # Explicitly initializes the Python Imaging Library. This function # loads all available file format drivers. def init(): "Load all file format drivers." global _initialized if _initialized >= 2: return visited = {} directories = sys.path try: directories = directories + [os.path.dirname(__file__)] except NameError: pass # only check directories (including current, if present in the path) for directory in filter(isDirectory, directories): fullpath = os.path.abspath(directory) if visited.has_key(fullpath): continue for file in os.listdir(directory): if file[-14:] == "ImagePlugin.py": f, e = os.path.splitext(file) try: sys.path.insert(0, directory) try: __import__(f, globals(), locals(), []) finally: del sys.path[0] except ImportError: if DEBUG: print "Image: failed to import", print f, ":", sys.exc_value visited[fullpath] = None if OPEN or SAVE: _initialized = 2 # -------------------------------------------------------------------- # Codec factories (used by tostring/fromstring and ImageFile.load) def _getdecoder(mode, decoder_name, args, extra=()): # tweak arguments if args is None: args = () elif not isTupleType(args): args = (args,) try: # get decoder decoder = getattr(core, decoder_name + "_decoder") # print decoder, (mode,) + args + extra return apply(decoder, (mode,) + args + extra) except AttributeError: raise IOError("decoder %s not available" % decoder_name) def _getencoder(mode, encoder_name, args, extra=()): # tweak arguments if args is None: args = () elif not isTupleType(args): args = (args,) try: # get encoder encoder = getattr(core, encoder_name + "_encoder") # print encoder, (mode,) + args + extra return apply(encoder, (mode,) + args + extra) except AttributeError: raise IOError("encoder %s not available" % encoder_name) # -------------------------------------------------------------------- # Simple expression analyzer class _E: def __init__(self, data): self.data = data def __coerce__(self, other): return self, _E(other) def __add__(self, other): return _E((self.data, "__add__", other.data)) def __mul__(self, other): return _E((self.data, "__mul__", other.data)) def _getscaleoffset(expr): stub = ["stub"] data = expr(_E(stub)).data try: (a, b, c) = data # simplified syntax if (a is stub and b == "__mul__" and isNumberType(c)): return c, 0.0 if (a is stub and b == "__add__" and isNumberType(c)): return 1.0, c except TypeError: pass try: ((a, b, c), d, e) = data # full syntax if (a is stub and b == "__mul__" and isNumberType(c) and d == "__add__" and isNumberType(e)): return c, e except TypeError: pass raise ValueError("illegal expression") # -------------------------------------------------------------------- # Implementation wrapper ## # This class represents an image object. To create Image objects, use # the appropriate factory functions. There's hardly ever any reason # to call the Image constructor directly. # # @see #open # @see #new # @see #fromstring class Image: format = None format_description = None def __init__(self): self.im = None self.mode = "" self.size = (0, 0) self.palette = None self.info = {} self.category = NORMAL self.readonly = 0 def _new(self, im): new = Image() new.im = im new.mode = im.mode new.size = im.size new.palette = self.palette if im.mode == "P": new.palette = ImagePalette.ImagePalette() try: new.info = self.info.copy() except AttributeError: # fallback (pre-1.5.2) new.info = {} for k, v in self.info: new.info[k] = v return new _makeself = _new # compatibility def _copy(self): self.load() self.im = self.im.copy() self.readonly = 0 def _dump(self, file=None, format=None): import tempfile if not file: file = tempfile.mktemp() self.load() if not format or format == "PPM": self.im.save_ppm(file) else: file = file + "." + format self.save(file, format) return file def __getattr__(self, name): if name == "__array_interface__": # numpy array interface support new = {} shape, typestr = _conv_type_shape(self) new['shape'] = shape new['typestr'] = typestr new['data'] = self.tostring() return new raise AttributeError(name) ## # Returns a string containing pixel data. # # @param encoder_name What encoder to use. The default is to # use the standard "raw" encoder. # @param *args Extra arguments to the encoder. # @return An 8-bit string. def tostring(self, encoder_name="raw", *args): "Return image as a binary string" # may pass tuple instead of argument list if len(args) == 1 and isTupleType(args[0]): args = args[0] if encoder_name == "raw" and args == (): args = self.mode self.load() # unpack data e = _getencoder(self.mode, encoder_name, args) e.setimage(self.im) bufsize = max(65536, self.size[0] * 4) # see RawEncode.c data = [] while 1: l, s, d = e.encode(bufsize) data.append(d) if s: break if s < 0: raise RuntimeError("encoder error %d in tostring" % s) return string.join(data, "") ## # Returns the image converted to an X11 bitmap. This method # only works for mode "1" images. # # @param name The name prefix to use for the bitmap variables. # @return A string containing an X11 bitmap. # @exception ValueError If the mode is not "1" def tobitmap(self, name="image"): "Return image as an XBM bitmap" self.load() if self.mode != "1": raise ValueError("not a bitmap") data = self.tostring("xbm") return string.join(["#define %s_width %d\n" % (name, self.size[0]), "#define %s_height %d\n"% (name, self.size[1]), "static char %s_bits[] = {\n" % name, data, "};"], "") ## # Loads this image with pixel data from a string. #

# This method is similar to the {@link #fromstring} function, but # loads data into this image instead of creating a new image # object. def fromstring(self, data, decoder_name="raw", *args): "Load data to image from binary string" # may pass tuple instead of argument list if len(args) == 1 and isTupleType(args[0]): args = args[0] # default format if decoder_name == "raw" and args == (): args = self.mode # unpack data d = _getdecoder(self.mode, decoder_name, args) d.setimage(self.im) s = d.decode(data) if s[0] >= 0: raise ValueError("not enough image data") if s[1] != 0: raise ValueError("cannot decode image data") ## # Allocates storage for the image and loads the pixel data. In # normal cases, you don't need to call this method, since the # Image class automatically loads an opened image when it is # accessed for the first time. # # @return An image access object. def load(self): "Explicitly load pixel data." if self.im and self.palette and self.palette.dirty: # realize palette apply(self.im.putpalette, self.palette.getdata()) self.palette.dirty = 0 self.palette.mode = "RGB" self.palette.rawmode = None if self.info.has_key("transparency"): self.im.putpalettealpha(self.info["transparency"], 0) self.palette.mode = "RGBA" if self.im: return self.im.pixel_access(self.readonly) ## # Verifies the contents of a file. For data read from a file, this # method attempts to determine if the file is broken, without # actually decoding the image data. If this method finds any # problems, it raises suitable exceptions. If you need to load # the image after using this method, you must reopen the image # file. def verify(self): "Verify file contents." pass ## # Returns a converted copy of this image. For the "P" mode, this # method translates pixels through the palette. If mode is # omitted, a mode is chosen so that all information in the image # and the palette can be represented without a palette. #

# The current version supports all possible conversions between # "L", "RGB" and "CMYK." #

# When translating a colour image to black and white (mode "L"), # the library uses the ITU-R 601-2 luma transform: #

# L = R * 299/1000 + G * 587/1000 + B * 114/1000 #

# When translating a greyscale image into a bilevel image (mode # "1"), all non-zero values are set to 255 (white). To use other # thresholds, use the {@link #Image.point} method. # # @def convert(mode, matrix=None) # @param mode The requested mode. # @param matrix An optional conversion matrix. If given, this # should be 4- or 16-tuple containing floating point values. # @return An Image object. def convert(self, mode=None, data=None, dither=None, palette=WEB, colors=256): "Convert to other pixel format" if not mode: # determine default mode if self.mode == "P": self.load() if self.palette: mode = self.palette.mode else: mode = "RGB" else: return self.copy() self.load() if data: # matrix conversion if mode not in ("L", "RGB"): raise ValueError("illegal conversion") im = self.im.convert_matrix(mode, data) return self._new(im) if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) return self._new(im) # colourspace conversion if dither is None: dither = FLOYDSTEINBERG try: im = self.im.convert(mode, dither) except ValueError: try: # normalize source image and try again im = self.im.convert(getmodebase(self.mode)) im = im.convert(mode, dither) except KeyError: raise ValueError("illegal conversion") return self._new(im) def quantize(self, colors=256, method=0, kmeans=0, palette=None): # methods: # 0 = median cut # 1 = maximum coverage # NOTE: this functionality will be moved to the extended # quantizer interface in a later version of PIL. self.load() if palette: # use palette from reference image palette.load() if palette.mode != "P": raise ValueError("bad mode for palette image") if self.mode != "RGB" and self.mode != "L": raise ValueError( "only RGB or L mode images can be quantized to a palette" ) im = self.im.convert("P", 1, palette.im) return self._makeself(im) im = self.im.quantize(colors, method, kmeans) return self._new(im) ## # Copies this image. Use this method if you wish to paste things # into an image, but still retain the original. # # @return An Image object. def copy(self): "Copy raster data" self.load() im = self.im.copy() return self._new(im) ## # Returns a rectangular region from this image. The box is a # 4-tuple defining the left, upper, right, and lower pixel # coordinate. #

# This is a lazy operation. Changes to the source image may or # may not be reflected in the cropped image. To break the # connection, call the {@link #Image.load} method on the cropped # copy. # # @param The crop rectangle, as a (left, upper, right, lower)-tuple. # @return An Image object. def crop(self, box=None): "Crop region from image" self.load() if box is None: return self.copy() # lazy operation return _ImageCrop(self, box) ## # Configures the image file loader so it returns a version of the # image that as closely as possible matches the given mode and # size. For example, you can use this method to convert a colour # JPEG to greyscale while loading it, or to extract a 128x192 # version from a PCD file. #

# Note that this method modifies the Image object in place. If # the image has already been loaded, this method has no effect. # # @param mode The requested mode. # @param size The requested size. def draft(self, mode, size): "Configure image decoder" pass def _expand(self, xmargin, ymargin=None): if ymargin is None: ymargin = xmargin self.load() return self._new(self.im.expand(xmargin, ymargin, 0)) ## # Filters this image using the given filter. For a list of # available filters, see the ImageFilter module. # # @param filter Filter kernel. # @return An Image object. # @see ImageFilter def filter(self, filter): "Apply environment filter to image" self.load() from ImageFilter import Filter if not isinstance(filter, Filter): filter = filter() if self.im.bands == 1: return self._new(filter.filter(self.im)) # fix to handle multiband images since _imaging doesn't ims = [] for c in range(self.im.bands): ims.append(self._new(filter.filter(self.im.getband(c)))) return merge(self.mode, ims) ## # Returns a tuple containing the name of each band in this image. # For example, getbands on an RGB image returns ("R", "G", "B"). # # @return A tuple containing band names. def getbands(self): "Get band names" return ImageMode.getmode(self.mode).bands ## # Calculates the bounding box of the non-zero regions in the # image. # # @return The bounding box is returned as a 4-tuple defining the # left, upper, right, and lower pixel coordinate. If the image # is completely empty, this method returns None. def getbbox(self): "Get bounding box of actual data (non-zero pixels) in image" self.load() return self.im.getbbox() ## # Returns a list of colors used in this image. # # @param maxcolors Maximum number of colors. If this number is # exceeded, this method returns None. The default limit is # 256 colors. # @return An unsorted list of (count, pixel) values. def getcolors(self, maxcolors=256): "Get colors from image, up to given limit" self.load() if self.mode in ("1", "L", "P"): h = self.im.histogram() out = [] for i in range(256): if h[i]: out.append((h[i], i)) if len(out) > maxcolors: return None return out return self.im.getcolors(maxcolors) ## # Returns the contents of this image as a sequence object # containing pixel values. The sequence object is flattened, so # that values for line one follow directly after the values of # line zero, and so on. #

# Note that the sequence object returned by this method is an # internal PIL data type, which only supports certain sequence # operations. To convert it to an ordinary sequence (e.g. for # printing), use list(im.getdata()). # # @param band What band to return. The default is to return # all bands. To return a single band, pass in the index # value (e.g. 0 to get the "R" band from an "RGB" image). # @return A sequence-like object. def getdata(self, band = None): "Get image data as sequence object." self.load() if band is not None: return self.im.getband(band) return self.im # could be abused ## # Gets the the minimum and maximum pixel values for each band in # the image. # # @return For a single-band image, a 2-tuple containing the # minimum and maximum pixel value. For a multi-band image, # a tuple containing one 2-tuple for each band. def getextrema(self): "Get min/max value" self.load() if self.im.bands > 1: extrema = [] for i in range(self.im.bands): extrema.append(self.im.getband(i).getextrema()) return tuple(extrema) return self.im.getextrema() ## # Returns a PyCObject that points to the internal image memory. # # @return A PyCObject object. def getim(self): "Get PyCObject pointer to internal image memory" self.load() return self.im.ptr ## # Returns the image palette as a list. # # @return A list of color values [r, g, b, ...], or None if the # image has no palette. def getpalette(self): "Get palette contents." self.load() try: return map(ord, self.im.getpalette()) except ValueError: return None # no palette ## # Returns the pixel value at a given position. # # @param xy The coordinate, given as (x, y). # @return The pixel value. If the image is a multi-layer image, # this method returns a tuple. def getpixel(self, xy): "Get pixel value" self.load() return self.im.getpixel(xy) ## # Returns the horizontal and vertical projection. # # @return Two sequences, indicating where there are non-zero # pixels along the X-axis and the Y-axis, respectively. def getprojection(self): "Get projection to x and y axes" self.load() x, y = self.im.getprojection() return map(ord, x), map(ord, y) ## # Returns a histogram for the image. The histogram is returned as # a list of pixel counts, one for each pixel value in the source # image. If the image has more than one band, the histograms for # all bands are concatenated (for example, the histogram for an # "RGB" image contains 768 values). #

# A bilevel image (mode "1") is treated as a greyscale ("L") image # by this method. #

# If a mask is provided, the method returns a histogram for those # parts of the image where the mask image is non-zero. The mask # image must have the same size as the image, and be either a # bi-level image (mode "1") or a greyscale image ("L"). # # @def histogram(mask=None) # @param mask An optional mask. # @return A list containing pixel counts. def histogram(self, mask=None, extrema=None): "Take histogram of image" self.load() if mask: mask.load() return self.im.histogram((0, 0), mask.im) if self.mode in ("I", "F"): if extrema is None: extrema = self.getextrema() return self.im.histogram(extrema) return self.im.histogram() ## # (Deprecated) Returns a copy of the image where the data has been # offset by the given distances. Data wraps around the edges. If # yoffset is omitted, it is assumed to be equal to xoffset. #

# This method is deprecated. New code should use the offset # function in the ImageChops module. # # @param xoffset The horizontal distance. # @param yoffset The vertical distance. If omitted, both # distances are set to the same value. # @return An Image object. def offset(self, xoffset, yoffset=None): "(deprecated) Offset image in horizontal and/or vertical direction" if warnings: warnings.warn( "'offset' is deprecated; use 'ImageChops.offset' instead", DeprecationWarning, stacklevel=2 ) import ImageChops return ImageChops.offset(self, xoffset, yoffset) ## # Pastes another image into this image. The box argument is either # a 2-tuple giving the upper left corner, a 4-tuple defining the # left, upper, right, and lower pixel coordinate, or None (same as # (0, 0)). If a 4-tuple is given, the size of the pasted image # must match the size of the region. #

# If the modes don't match, the pasted image is converted to the # mode of this image (see the {@link #Image.convert} method for # details). #

# Instead of an image, the source can be a integer or tuple # containing pixel values. The method then fills the region # with the given colour. When creating RGB images, you can # also use colour strings as supported by the ImageColor module. #

# If a mask is given, this method updates only the regions # indicated by the mask. You can use either "1", "L" or "RGBA" # images (in the latter case, the alpha band is used as mask). # Where the mask is 255, the given image is copied as is. Where # the mask is 0, the current value is preserved. Intermediate # values can be used for transparency effects. #

# Note that if you paste an "RGBA" image, the alpha band is # ignored. You can work around this by using the same image as # both source image and mask. # # @param im Source image or pixel value (integer or tuple). # @param box An optional 4-tuple giving the region to paste into. # If a 2-tuple is used instead, it's treated as the upper left # corner. If omitted or None, the source is pasted into the # upper left corner. #

# If an image is given as the second argument and there is no # third, the box defaults to (0, 0), and the second argument # is interpreted as a mask image. # @param mask An optional mask image. # @return An Image object. def paste(self, im, box=None, mask=None): "Paste other image into region" if isImageType(box) and mask is None: # abbreviated paste(im, mask) syntax mask = box; box = None if box is None: # cover all of self box = (0, 0) + self.size if len(box) == 2: # lower left corner given; get size from image or mask if isImageType(im): size = im.size elif isImageType(mask): size = mask.size else: # FIXME: use self.size here? raise ValueError( "cannot determine region size; use 4-item box" ) box = box + (box[0]+size[0], box[1]+size[1]) if isStringType(im): import ImageColor im = ImageColor.getcolor(im, self.mode) elif isImageType(im): im.load() if self.mode != im.mode: if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) im = im.im self.load() if self.readonly: self._copy() if mask: mask.load() self.im.paste(im, box, mask.im) else: self.im.paste(im, box) ## # Maps this image through a lookup table or function. # # @param lut A lookup table, containing 256 values per band in the # image. A function can be used instead, it should take a single # argument. The function is called once for each possible pixel # value, and the resulting table is applied to all bands of the # image. # @param mode Output mode (default is same as input). In the # current version, this can only be used if the source image # has mode "L" or "P", and the output has mode "1". # @return An Image object. def point(self, lut, mode=None): "Map image through lookup table" self.load() if not isSequenceType(lut): # if it isn't a list, it should be a function if self.mode in ("I", "I;16", "F"): # check if the function can be used with point_transform scale, offset = _getscaleoffset(lut) return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table lut = map(lut, range(256)) * self.im.bands if self.mode == "F": # FIXME: _imaging returns a confusing error message for this case raise ValueError("point operation not supported for this mode") return self._new(self.im.point(lut, mode)) ## # Adds or replaces the alpha layer in this image. If the image # does not have an alpha layer, it's converted to "LA" or "RGBA". # The new layer must be either "L" or "1". # # @param im The new alpha layer. This can either be an "L" or "1" # image having the same size as this image, or an integer or # other color value. def putalpha(self, alpha): "Set alpha layer" self.load() if self.readonly: self._copy() if self.mode not in ("LA", "RGBA"): # attempt to promote self to a matching alpha mode try: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) except (AttributeError, ValueError): # do things the hard way im = self.im.convert(mode) if im.mode not in ("LA", "RGBA"): raise ValueError # sanity check self.im = im self.mode = self.im.mode except (KeyError, ValueError): raise ValueError("illegal image mode") if self.mode == "LA": band = 1 else: band = 3 if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): raise ValueError("illegal image mode") alpha.load() if alpha.mode == "1": alpha = alpha.convert("L") else: # constant alpha try: self.im.fillband(band, alpha) except (AttributeError, ValueError): # do things the hard way alpha = new("L", self.size, alpha) else: return self.im.putband(alpha.im, band) ## # Copies pixel data to this image. This method copies data from a # sequence object into the image, starting at the upper left # corner (0, 0), and continuing until either the image or the # sequence ends. The scale and offset values are used to adjust # the sequence values: pixel = value*scale + offset. # # @param data A sequence object. # @param scale An optional scale value. The default is 1.0. # @param offset An optional offset value. The default is 0.0. def putdata(self, data, scale=1.0, offset=0.0): "Put data from a sequence object into an image." self.load() if self.readonly: self._copy() self.im.putdata(data, scale, offset) ## # Attaches a palette to this image. The image must be a "P" or # "L" image, and the palette sequence must contain 768 integer # values, where each group of three values represent the red, # green, and blue values for the corresponding pixel # index. Instead of an integer sequence, you can use an 8-bit # string. # # @def putpalette(data) # @param data A palette sequence (either a list or a string). def putpalette(self, data, rawmode="RGB"): "Put palette data into an image." self.load() if self.mode not in ("L", "P"): raise ValueError("illegal image mode") if not isStringType(data): data = string.join(map(chr, data), "") self.mode = "P" self.palette = ImagePalette.raw(rawmode, data) self.palette.mode = "RGB" self.load() # install new palette ## # Modifies the pixel at the given position. The colour is given as # a single numerical value for single-band images, and a tuple for # multi-band images. #

# Note that this method is relatively slow. For more extensive # changes, use {@link #Image.paste} or the ImageDraw module # instead. # # @param xy The pixel coordinate, given as (x, y). # @param value The pixel value. # @see #Image.paste # @see #Image.putdata # @see ImageDraw def putpixel(self, xy, value): "Set pixel value" self.load() if self.readonly: self._copy() return self.im.putpixel(xy, value) ## # Returns a resized copy of this image. # # @def resize(size, filter=NEAREST) # @param size The requested size in pixels, as a 2-tuple: # (width, height). # @param filter An optional resampling filter. This can be # one of NEAREST (use nearest neighbour), BILINEAR # (linear interpolation in a 2x2 environment), BICUBIC # (cubic spline interpolation in a 4x4 environment), or # ANTIALIAS (a high-quality downsampling filter). # If omitted, or if the image has mode "1" or "P", it is # set NEAREST. # @return An Image object. def resize(self, size, resample=NEAREST): "Resize image" if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): raise ValueError("unknown resampling filter") self.load() if self.mode in ("1", "P"): resample = NEAREST if resample == ANTIALIAS: # requires stretch support (imToolkit & PIL 1.1.3) try: im = self.im.stretch(size, resample) except AttributeError: raise ValueError("unsupported resampling filter") else: im = self.im.resize(size, resample) return self._new(im) ## # Returns a rotated copy of this image. This method returns a # copy of this image, rotated the given number of degrees counter # clockwise around its centre. # # @def rotate(angle, filter=NEAREST) # @param angle In degrees counter clockwise. # @param filter An optional resampling filter. This can be # one of NEAREST (use nearest neighbour), BILINEAR # (linear interpolation in a 2x2 environment), or BICUBIC # (cubic spline interpolation in a 4x4 environment). # If omitted, or if the image has mode "1" or "P", it is # set NEAREST. # @param expand Optional expansion flag. If true, expands the output # image to make it large enough to hold the entire rotated image. # If false or omitted, make the output image the same size as the # input image. # @return An Image object. def rotate(self, angle, resample=NEAREST, expand=0): "Rotate image. Angle given as degrees counter-clockwise." if expand: import math angle = -angle * math.pi / 180 matrix = [ math.cos(angle), math.sin(angle), 0.0, -math.sin(angle), math.cos(angle), 0.0 ] def transform(x, y, (a, b, c, d, e, f)=matrix): return a*x + b*y + c, d*x + e*y + f # calculate output size w, h = self.size xx = [] yy = [] for x, y in ((0, 0), (w, 0), (w, h), (0, h)): x, y = transform(x, y) xx.append(x) yy.append(y) w = int(math.ceil(max(xx)) - math.floor(min(xx))) h = int(math.ceil(max(yy)) - math.floor(min(yy))) # adjust center x, y = transform(w / 2.0, h / 2.0) matrix[2] = self.size[0] / 2.0 - x matrix[5] = self.size[1] / 2.0 - y return self.transform((w, h), AFFINE, matrix) if resample not in (NEAREST, BILINEAR, BICUBIC): raise ValueError("unknown resampling filter") self.load() if self.mode in ("1", "P"): resample = NEAREST return self._new(self.im.rotate(angle, resample)) ## # Saves this image under the given filename. If no format is # specified, the format to use is determined from the filename # extension, if possible. #

# Keyword options can be used to provide additional instructions # to the writer. If a writer doesn't recognise an option, it is # silently ignored. The available options are described later in # this handbook. #

# You can use a file object instead of a filename. In this case, # you must always specify the format. The file object must # implement the seek, tell, and write # methods, and be opened in binary mode. # # @def save(file, format=None, **options) # @param file File name or file object. # @param format Optional format override. If omitted, the # format to use is determined from the filename extension. # If a file object was used instead of a filename, this # parameter should always be used. # @param **options Extra parameters to the image writer. # @return None # @exception KeyError If the output format could not be determined # from the file name. Use the format option to solve this. # @exception IOError If the file could not be written. The file # may have been created, and may contain partial data. def save(self, fp, format=None, **params): "Save image to file or stream" if isStringType(fp): filename = fp else: if hasattr(fp, "name") and isStringType(fp.name): filename = fp.name else: filename = "" # may mutate self! self.load() self.encoderinfo = params self.encoderconfig = () preinit() ext = string.lower(os.path.splitext(filename)[1]) if not format: try: format = EXTENSION[ext] except KeyError: init() try: format = EXTENSION[ext] except KeyError: raise KeyError(ext) # unknown extension try: save_handler = SAVE[string.upper(format)] except KeyError: init() save_handler = SAVE[string.upper(format)] # unknown format if isStringType(fp): import __builtin__ fp = __builtin__.open(fp, "wb") close = 1 else: close = 0 try: save_handler(self, fp, filename) finally: # do what we can to clean up if close: fp.close() ## # Seeks to the given frame in this sequence file. If you seek # beyond the end of the sequence, the method raises an # EOFError exception. When a sequence file is opened, the # library automatically seeks to frame 0. #

# Note that in the current version of the library, most sequence # formats only allows you to seek to the next frame. # # @param frame Frame number, starting at 0. # @exception EOFError If the call attempts to seek beyond the end # of the sequence. # @see #Image.tell def seek(self, frame): "Seek to given frame in sequence file" # overridden by file handlers if frame != 0: raise EOFError ## # Displays this image. This method is mainly intended for # debugging purposes. #

# On Unix platforms, this method saves the image to a temporary # PPM file, and calls the xv utility. #

# On Windows, it saves the image to a temporary BMP file, and uses # the standard BMP display utility to show it (usually Paint). # # @def show(title=None) # @param title Optional title to use for the image window, # where possible. def show(self, title=None, command=None): "Display image (for debug purposes only)" _showxv(self, title, command) ## # Split this image into individual bands. This method returns a # tuple of individual image bands from an image. For example, # splitting an "RGB" image creates three new images each # containing a copy of one of the original bands (red, green, # blue). # # @return A tuple containing bands. def split(self): "Split image into bands" ims = [] self.load() for i in range(self.im.bands): ims.append(self._new(self.im.getband(i))) return tuple(ims) ## # Returns the current frame number. # # @return Frame number, starting with 0. # @see #Image.seek def tell(self): "Return current frame number" return 0 ## # Make this image into a thumbnail. This method modifies the # image to contain a thumbnail version of itself, no larger than # the given size. This method calculates an appropriate thumbnail # size to preserve the aspect of the image, calls the {@link # #Image.draft} method to configure the file reader (where # applicable), and finally resizes the image. #

# Note that the bilinear and bicubic filters in the current # version of PIL are not well-suited for thumbnail generation. # You should use ANTIALIAS unless speed is much more # important than quality. #

# Also note that this function modifies the Image object in place. # If you need to use the full resolution image as well, apply this # method to a {@link #Image.copy} of the original image. # # @param size Requested size. # @param resample Optional resampling filter. This can be one # of NEAREST, BILINEAR, BICUBIC, or # ANTIALIAS (best quality). If omitted, it defaults # to NEAREST (this will be changed to ANTIALIAS in a # future version). # @return None def thumbnail(self, size, resample=NEAREST): "Create thumbnail representation (modifies image in place)" # FIXME: the default resampling filter will be changed # to ANTIALIAS in future versions # preserve aspect ratio x, y = self.size if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] size = x, y if size == self.size: return self.draft(None, size) self.load() try: im = self.resize(size, resample) except ValueError: if resample != ANTIALIAS: raise im = self.resize(size, NEAREST) # fallback self.im = im.im self.mode = im.mode self.size = size self.readonly = 0 # FIXME: the different tranform methods need further explanation # instead of bloating the method docs, add a separate chapter. ## # Transforms this image. This method creates a new image with the # given size, and the same mode as the original, and copies data # to the new image using the given transform. #

# @def transform(size, method, data, resample=NEAREST) # @param size The output size. # @param method The transformation method. This is one of # EXTENT (cut out a rectangular subregion), AFFINE # (affine transform), PERSPECTIVE (perspective # transform), QUAD (map a quadrilateral to a # rectangle), or MESH (map a number of source quadrilaterals # in one operation). # @param data Extra data to the transformation method. # @param resample Optional resampling filter. It can be one of # NEAREST (use nearest neighbour), BILINEAR # (linear interpolation in a 2x2 environment), or # BICUBIC (cubic spline interpolation in a 4x4 # environment). If omitted, or if the image has mode # "1" or "P", it is set to NEAREST. # @return An Image object. def transform(self, size, method, data=None, resample=NEAREST, fill=1): "Transform image" import ImageTransform if isinstance(method, ImageTransform.Transform): method, data = method.getdata() if data is None: raise ValueError("missing method data") im = new(self.mode, size, None) if method == MESH: # list of quads for box, quad in data: im.__transformer(box, self, QUAD, quad, resample, fill) else: im.__transformer((0, 0)+size, self, method, data, resample, fill) return im def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): # FIXME: this should be turned into a lazy operation (?) w = box[2]-box[0] h = box[3]-box[1] if method == AFFINE: # change argument order to match implementation data = (data[2], data[0], data[1], data[5], data[3], data[4]) elif method == EXTENT: # convert extent to an affine transform x0, y0, x1, y1 = data xs = float(x1 - x0) / w ys = float(y1 - y0) / h method = AFFINE data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) elif method == PERSPECTIVE: # change argument order to match implementation data = (data[2], data[0], data[1], data[5], data[3], data[4], data[6], data[7]) elif method == QUAD: # quadrilateral warp. data specifies the four corners # given as NW, SW, SE, and NE. nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] x0, y0 = nw; As = 1.0 / w; At = 1.0 / h data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, (se[0]-sw[0]-ne[0]+x0)*As*At, y0, (ne[1]-y0)*As, (sw[1]-y0)*At, (se[1]-sw[1]-ne[1]+y0)*As*At) else: raise ValueError("unknown transformation method") if resample not in (NEAREST, BILINEAR, BICUBIC): raise ValueError("unknown resampling filter") image.load() self.load() if image.mode in ("1", "P"): resample = NEAREST self.im.transform2(box, image.im, method, data, resample, fill) ## # Returns a flipped or rotated copy of this image. # # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, # ROTATE_90, ROTATE_180, or ROTATE_270. def transpose(self, method): "Transpose image (flip or rotate in 90 degree steps)" self.load() im = self.im.transpose(method) return self._new(im) # -------------------------------------------------------------------- # Lazy operations class _ImageCrop(Image): def __init__(self, im, box): Image.__init__(self) x0, y0, x1, y1 = box if x1 < x0: x1 = x0 if y1 < y0: y1 = y0 self.mode = im.mode self.size = x1-x0, y1-y0 self.__crop = x0, y0, x1, y1 self.im = im.im def load(self): # lazy evaluation! if self.__crop: self.im = self.im.crop(self.__crop) self.__crop = None # FIXME: future versions should optimize crop/paste # sequences! # -------------------------------------------------------------------- # Factories # # Debugging def _wedge(): "Create greyscale wedge (for debugging only)" return Image()._new(core.wedge("L")) ## # Creates a new image with the given mode and size. # # @param mode The mode to use for the new image. # @param size A 2-tuple, containing (width, height) in pixels. # @param color What colour to use for the image. Default is black. # If given, this should be a single integer or floating point value # for single-band modes, and a tuple for multi-band modes (one value # per band). When creating RGB images, you can also use colour # strings as supported by the ImageColor module. If the colour is # None, the image is not initialised. # @return An Image object. def new(mode, size, color=0): "Create a new image" if color is None: # don't initialize return Image()._new(core.new(mode, size)) if isStringType(color): # css3-style specifier import ImageColor color = ImageColor.getcolor(color, mode) return Image()._new(core.fill(mode, size, color)) ## # Creates an image memory from pixel data in a string. #

# In its simplest form, this function takes three arguments # (mode, size, and unpacked pixel data). #

# You can also use any pixel decoder supported by PIL. For more # information on available decoders, see the section Writing Your Own File Decoder. #

# Note that this function decodes pixel data only, not entire images. # If you have an entire image in a string, wrap it in a # StringIO object, and use {@link #open} to load it. # # @param mode The image mode. # @param size The image size. # @param data An 8-bit string containing raw data for the given mode. # @param decoder_name What decoder to use. # @param *args Additional parameters for the given decoder. # @return An Image object. def fromstring(mode, size, data, decoder_name="raw", *args): "Load image from string" # may pass tuple instead of argument list if len(args) == 1 and isTupleType(args[0]): args = args[0] if decoder_name == "raw" and args == (): args = mode im = new(mode, size) im.fromstring(data, decoder_name, args) return im ## # (New in 1.1.4) Creates an image memory from pixel data in a string # or byte buffer. #

# This function is similar to {@link #fromstring}, but uses data in # the byte buffer, where possible. This means that changes to the # original buffer object are reflected in this image). Not all modes # can share memory; supported modes include "L", "RGBX", "RGBA", and # "CMYK". #

# Note that this function decodes pixel data only, not entire images. # If you have an entire image file in a string, wrap it in a # StringIO object, and use {@link #open} to load it. #

# In the current version, the default parameters used for the "raw" # decoder differs from that used for {@link fromstring}. This is a # bug, and will probably be fixed in a future release. The current # release issues a warning if you do this; to disable the warning, # you should provide the full set of parameters. See below for # details. # # @param mode The image mode. # @param size The image size. # @param data An 8-bit string or other buffer object containing raw # data for the given mode. # @param decoder_name What decoder to use. # @param *args Additional parameters for the given decoder. For the # default encoder ("raw"), it's recommended that you provide the # full set of parameters: # frombuffer(mode, size, data, "raw", mode, 0, 1). # @return An Image object. # @since 1.1.4 def frombuffer(mode, size, data, decoder_name="raw", *args): "Load image from string or buffer" # may pass tuple instead of argument list if len(args) == 1 and isTupleType(args[0]): args = args[0] if decoder_name == "raw": if args == (): if warnings: warnings.warn( "the frombuffer defaults may change in a future release; " "for portability, change the call to read:\n" " frombuffer(mode, size, data, 'raw', mode, 0, 1)", RuntimeWarning, stacklevel=2 ) args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 if args[0] in _MAPMODES: im = new(mode, (1,1)) im = im._new( core.map_buffer(data, size, decoder_name, None, 0, args) ) im.readonly = 1 return im return apply(fromstring, (mode, size, data, decoder_name, args)) ## # (New in 1.1.6) Create an image memory from an object exporting # the array interface (using the buffer protocol). # # If obj is not contiguous, then the tostring method is called # and {@link frombuffer} is used. # # @param obj Object with array interface # @param mode Mode to use (will be determined from type if None) # @return An image memory. def fromarray(obj, mode=None): arr = obj.__array_interface__ shape = arr['shape'] ndim = len(shape) try: strides = arr['strides'] except KeyError: strides = None if mode is None: typestr = arr['typestr'] if not (typestr[0] == '|' or typestr[0] == _ENDIAN or typestr[1:] not in ['u1', 'b1', 'i4', 'f4']): raise TypeError("cannot handle data-type") typestr = typestr[:2] if typestr == 'i4': mode = 'I' elif typestr == 'f4': mode = 'F' elif typestr == 'b1': mode = '1' elif ndim == 2: mode = 'L' elif ndim == 3: mode = 'RGB' elif ndim == 4: mode = 'RGBA' else: raise TypeError("Do not understand data.") ndmax = 4 bad_dims=0 if mode in ['1','L','I','P','F']: ndmax = 2 elif mode == 'RGB': ndmax = 3 if ndim > ndmax: raise ValueError("Too many dimensions.") size = shape[:2][::-1] if strides is not None: obj = obj.tostring() return frombuffer(mode, size, obj, "raw", mode, 0, 1) ## # Opens and identifies the given image file. #

# This is a lazy operation; this function identifies the file, but the # actual image data is not read from the file until you try to process # the data (or call the {@link #Image.load} method). # # @def open(file, mode="r") # @param file A filename (string) or a file object. The file object # must implement read, seek, and tell methods, # and be opened in binary mode. # @param mode The mode. If given, this argument must be "r". # @return An Image object. # @exception IOError If the file cannot be found, or the image cannot be # opened and identified. # @see #new def open(fp, mode="r"): "Open an image file, without loading the raster data" if mode != "r": raise ValueError("bad mode") if isStringType(fp): import __builtin__ filename = fp fp = __builtin__.open(fp, "rb") else: filename = "" prefix = fp.read(16) preinit() for i in ID: try: factory, accept = OPEN[i] if not accept or accept(prefix): fp.seek(0) return factory(fp, filename) except (SyntaxError, IndexError, TypeError): pass init() for i in ID: try: factory, accept = OPEN[i] if not accept or accept(prefix): fp.seek(0) return factory(fp, filename) except (SyntaxError, IndexError, TypeError): pass raise IOError("cannot identify image file") # # Image processing. ## # Creates a new image by interpolating between two input images, using # a constant alpha. # #

#    out = image1 * (1.0 - alpha) + image2 * alpha
# 
# # @param im1 The first image. # @param im2 The second image. Must have the same mode and size as # the first image. # @param alpha The interpolation alpha factor. If alpha is 0.0, a # copy of the first image is returned. If alpha is 1.0, a copy of # the second image is returned. There are no restrictions on the # alpha value. If necessary, the result is clipped to fit into # the allowed output range. # @return An Image object. def blend(im1, im2, alpha): "Interpolate between images." im1.load() im2.load() return im1._new(core.blend(im1.im, im2.im, alpha)) ## # Creates a new image by interpolating between two input images, # using the mask as alpha. # # @param image1 The first image. # @param image2 The second image. Must have the same mode and # size as the first image. # @param mask A mask image. This image can can have mode # "1", "L", or "RGBA", and must have the same size as the # other two images. def composite(image1, image2, mask): "Create composite image by blending images using a transparency mask" image = image2.copy() image.paste(image1, None, mask) return image ## # Applies the function (which should take one argument) to each pixel # in the given image. If the image has more than one band, the same # function is applied to each band. Note that the function is # evaluated once for each possible pixel value, so you cannot use # random components or other generators. # # @def eval(image, function) # @param image The input image. # @param function A function object, taking one integer argument. # @return An Image object. def eval(image, *args): "Evaluate image expression" return image.point(args[0]) ## # Creates a new image from a number of single-band images. # # @param mode The mode to use for the output image. # @param bands A sequence containing one single-band image for # each band in the output image. All bands must have the # same size. # @return An Image object. def merge(mode, bands): "Merge a set of single band images into a new multiband image." if getmodebands(mode) != len(bands) or "*" in mode: raise ValueError("wrong number of bands") for im in bands[1:]: if im.mode != getmodetype(mode): raise ValueError("mode mismatch") if im.size != bands[0].size: raise ValueError("size mismatch") im = core.new(mode, bands[0].size) for i in range(getmodebands(mode)): bands[i].load() im.putband(bands[i].im, i) return bands[0]._new(im) # -------------------------------------------------------------------- # Plugin registry ## # Register an image file plugin. This function should not be used # in application code. # # @param id An image format identifier. # @param factory An image file factory method. # @param accept An optional function that can be used to quickly # reject images having another format. def register_open(id, factory, accept=None): id = string.upper(id) ID.append(id) OPEN[id] = factory, accept ## # Registers an image MIME type. This function should not be used # in application code. # # @param id An image format identifier. # @param mimetype The image MIME type for this format. def register_mime(id, mimetype): MIME[string.upper(id)] = mimetype ## # Registers an image save function. This function should not be # used in application code. # # @param id An image format identifier. # @param driver A function to save images in this format. def register_save(id, driver): SAVE[string.upper(id)] = driver ## # Registers an image extension. This function should not be # used in application code. # # @param id An image format identifier. # @param extension An extension used for this format. def register_extension(id, extension): EXTENSION[string.lower(extension)] = string.upper(id) # -------------------------------------------------------------------- # Simple display support def _showxv(image, title=None, command=None): if os.name == "nt": format = "BMP" elif sys.platform == "darwin": format = "JPEG" if not command: command = "open -a /Applications/Preview.app" else: format = None if not command: for cmd in ["eog", "gqview", "gwenview", "xv"]: if _iscommand(cmd): command = cmd break if not command: # raise OSError, "no image viewer found" print "no image viewer found" if command in ("xv") and title: command = command + " -name \"%s\"" % title if image.mode == "I;16": # @PIL88 @PIL101 # "I;16" isn't an 'official' mode, but we still want to # provide a simple way to show 16-bit images. base = "L" else: base = getmodebase(image.mode) if base != image.mode and image.mode != "1": file = image.convert(base)._dump(format=format) else: file = image._dump(format=format) if os.name == "nt": command = "start /wait %s && del /f %s" % (file, file) elif sys.platform == "darwin": # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) else: command = "(%s %s; rm -f %s)&" % (command, file, file) os.system(command) def _isexecutable(cmd): if os.path.isfile(cmd): mode = os.stat(cmd)[stat.ST_MODE] if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH: return True return False def _iscommand(cmd): """Return True if cmd is executable or can be found on the executable search path.""" if _isexecutable(cmd): return True path = os.environ.get("PATH") if not path: return False for d in path.split(os.pathsep): exe = os.path.join(d, cmd) if _isexecutable(exe): return True return False