def __setitem__(self, point, color): """Set a single pixel to a given color. This is "slow", in the sense that you probably don't want to do this to edit every pixel in an entire image. """ point = Vector.coerce(point) rgb = color.rgb() # TODO retval is t/f with magick_try() as exc: # Surprise! GetOneCacheViewAuthenticPixel doesn't actually respect # writes, even though the docs explicitly says it does. # So get a view of this single pixel instead. px = lib.GetCacheViewAuthenticPixels(self._ptr, point.x, point.y, 1, 1, exc.ptr) exc.check(px == ffi.NULL) array = ffi.new("double[]", [rgb._red, rgb._green, rgb._blue, rgb._opacity]) lib.sanpera_pixel_from_doubles(px, array) #print(repr(ffi.buffer(ffi.cast("char*", ffi.cast("void*", px)), 16)[:])) with magick_try() as exc: assert lib.SyncCacheViewAuthenticPixels(self._ptr, exc.ptr)
def optimized_for_animated_gif(self): """Returns an image with frames optimized for animated GIFs. Each frame will be compared with previous frames to shrink each frame as much as possible while preserving the results of the animation. """ with magick_try() as exc: new_image = lib.OptimizeImageLayers(self._stack, exc.ptr) with magick_try() as exc: lib.OptimizeImageTransparency(new_image, exc.ptr) return type(self)(new_image)
def to_buffer(self, format=None): if not self._frames: raise EmptyImageError image_info = blank_image_info() length = ffi.new("size_t *") # Force writing to a single file image_info.adjoin = lib.MagickTrue # Stupid hack to fix a bug in the rgb codec if format == 'rgba': for frame in self._frames: frame._fix_for_rgba_codec() if format: # If the caller provided an explicit format, pass it along # Make sure not to overflow the char[] # TODO maybe just error out when this happens image_info.magick = format.encode('ascii')[:lib.MaxTextExtent] elif self._stack.magick[0] == b'\0': # Uhoh; no format provided and nothing given by caller raise MissingFormatError with magick_try() as exc: with self._link_frames(self._frames) as ptr: cbuf = ffi.gc( lib.ImagesToBlob(image_info, ptr, length, exc.ptr), lib.RelinquishMagickMemory) return ffi.buffer(cbuf, length[0])
def _get_formats(): formats = dict() num_formats = ffi.new("size_t *") # Snag the list of known supported image formats with magick_try() as exc: magick_infos = ffi.gc(lib.GetMagickInfoList(b"*", num_formats, exc.ptr), lib.RelinquishMagickMemory) # Sometimes this call can generate an exception (such as a module not # being loadable) but then succeed anyway and return a useful value, in # which case we want to ignore the exception if magick_infos != ffi.NULL: exc.clear() for i in range(num_formats[0]): name = ffi.string(magick_infos[i].name).decode("latin-1") formats[name.lower()] = ImageFormat( name=name, description=ffi.string(magick_infos[i].description).decode("latin-1"), can_read=magick_infos[i].decoder != ffi.NULL, can_write=magick_infos[i].encoder != ffi.NULL, supports_frames=magick_infos[i].adjoin != 0, ) return formats
def _get_formats(): formats = dict() formats_by_mime = dict() num_formats = ffi.new("size_t *") # Snag the list of known supported image formats with magick_try() as exc: magick_infos = ffi.gc( lib.GetMagickInfoList(b"*", num_formats, exc.ptr), lib.RelinquishMagickMemory) for i in range(num_formats[0]): imageformat = ImageFormat( name=ffi.string(magick_infos[i].name).decode('latin-1'), description=ffi.string(magick_infos[i].description).decode('latin-1'), can_read=magick_infos[i].decoder != ffi.NULL, can_write=magick_infos[i].encoder != ffi.NULL, supports_frames=magick_infos[i].adjoin != 0, mime_type=ffi.string(magick_infos[i].mime_type).decode('ascii') if magick_infos[i].mime_type else None, ) formats[imageformat.name.lower()] = imageformat formats_by_mime[imageformat.mime_type] = imageformat return formats, formats_by_mime
def coalesced(self): """Returns an image with each frame composited over previous frames.""" with magick_try() as exc: new_image = ffi.gc(lib.CoalesceImages(self._stack, exc.ptr), lib.DestroyImageList) return type(self)(new_image)
def coalesced(self): """Returns an image with each frame composited over previous frames.""" with magick_try() as exc: new_image = ffi.gc( lib.CoalesceImages(self._stack, exc.ptr), lib.DestroyImageList) return type(self)(new_image)
def __iter__(self): rect = self._frame.canvas pixel = PixelViewPixel.__new__(PixelViewPixel) # This is needed so that the pixel cannot exist after the view is # destroyed -- it's a wrapper around a bare pointer! pixel.owner = self for y in range(rect.top, rect.bottom): with magick_try() as exc: q = lib.GetCacheViewAuthenticPixels(self._ptr, rect.left, y, rect.width, 1, exc.ptr) # TODO check q for NULL # TODO is this useful who knows #fx_indexes=GetCacheViewAuthenticIndexQueue(fx_view); try: for x in range(rect.left, rect.right): # TODO this probably needs to do something else for indexed try: # TODO rather than /always/ reusing the same pixel # object, only reuse it if it's detected as only having # one refcnt left :) pixel._pixel = q pixel._x = x pixel._y = y yield pixel finally: pixel._pixel = ffi.NULL #ret = RoundToQuantum((MagickRealType) QuantumRange * ret) # TODO opacity... # XXX this is black for CMYK # if (((channel & IndexChannel) != 0) && (fx_image->colorspace == CMYKColorspace)) { # SetPixelIndex(fx_indexes+x,RoundToQuantum((MagickRealType) QuantumRange*alpha)); # } # Pointer increment q += 1 finally: # TODO check return value with magick_try() as exc: assert lib.SyncCacheViewAuthenticPixels(self._ptr, exc.ptr)
def from_buffer(cls, buf): assert isinstance(buf, bytes) image_info = blank_image_info() with magick_try() as exc: ptr = lib.BlobToImage(image_info, buf, len(buf), exc.ptr) exc.check(ptr == ffi.NULL) return cls(ptr)
def read(cls, filename): with open(filename, "rb") as fh: image_info = blank_image_info() image_info.file = ffi.cast("FILE *", fh) with magick_try() as exc: ptr = lib.ReadImage(image_info, exc.ptr) exc.check(ptr == ffi.NULL) return cls(ptr)
def copy(self): """Returns a lazy copy of this frame. Pixel data is not copied until either this or the new frame are modified. """ # 0, 0 => size; 0x0 means to reuse the same pixel cache # 1 => orphan; clear the previous/next pointers, detach i/o stream with magick_try() as exc: cloned_frame = lib.CloneImage(self._frame, 0, 0, 1, exc.ptr) exc.check(cloned_frame == ffi.NULL) return type(self)(cloned_frame)
def __getitem__(self, point): point = Vector.coerce(point) px = ffi.new("PixelPacket *") # TODO retval is t/f with magick_try() as exc: lib.GetOneCacheViewAuthenticPixel(self._ptr, point.x, point.y, px, exc.ptr) array = ffi.new("double[]", 4) lib.sanpera_pixel_to_doubles(px, array) return RGBColor(*array)
def __call__(self, *frames, **kwargs): channel = kwargs.get('channel', lib.DefaultChannels) c_channel = ffi.cast('ChannelType', channel) steps = ffi.new("sanpera_evaluate_step[]", self.compiled_steps) c_frames = ffi.new("Image *[]", [f._frame for f in frames] + [ffi.NULL]) with magick_try() as exc: new_frame = ffi.gc( lib.sanpera_evaluate_filter(c_frames, steps, c_channel, exc.ptr), lib.DestroyImageList) return Image(new_frame)
def __call__(self, frame, **kwargs): channel = kwargs.get('channel', lib.DefaultChannels) # This is incredibly stupid, but yes, ColorizeImage only accepts a # string for the opacity. opacity = str(self._amount * 100.).encode('ascii') + b"%" color = ffi.new("PixelPacket *") self._color._populate_pixel(color) with magick_try() as exc: # TODO what if this raises but doesn't return NULL # TODO in general i need to figure out when i use gc and do it # consistently return lib.ColorizeImage(frame._frame, opacity, color[0], exc.ptr)
def __call__(self, *frames, **kwargs): channel = kwargs.get('channel', lib.DefaultChannels) c_channel = ffi.cast('ChannelType', channel) steps = ffi.new("sanpera_evaluate_step[]", self.compiled_steps) c_frames = ffi.new("Image *[]", [f._frame for f in frames] + [ffi.NULL]) with magick_try() as exc: # TODO can this raise an exception /but also/ return a new value? # is that a thing i should be handling better new_frame = lib.sanpera_evaluate_filter( c_frames, steps, c_channel, exc.ptr) return Image(new_frame)
def __setitem__(self, point, color): """Set a single pixel to a given color. This is "slow", in the sense that you probably don't want to do this to edit every pixel in an entire image. """ point = Vector.coerce(point) rgb = color.rgb() # TODO retval is t/f with magick_try() as exc: # Surprise! GetOneCacheViewAuthenticPixel doesn't actually respect # writes, even though the docs explicitly says it does. # So get a view of this single pixel instead. px = lib.GetCacheViewAuthenticPixels( self._ptr, point.x, point.y, 1, 1, exc.ptr) exc.check(px == ffi.NULL) array = ffi.new("double[]", [rgb._red, rgb._green, rgb._blue, rgb._opacity]) lib.sanpera_pixel_from_doubles(px, array) #print(repr(ffi.buffer(ffi.cast("char*", ffi.cast("void*", px)), 16)[:])) with magick_try() as exc: assert lib.SyncCacheViewAuthenticPixels(self._ptr, exc.ptr)
def cropped(self, rect, preserve_canvas=False): rectinfo = rect.to_rect_info() p = self._stack new_stack_ptr = ffi.new("Image **", ffi.NULL) while p: with magick_try() as exc: new_frame = lib.CropImage(p, rectinfo, exc.ptr) # Only GC the first frame in the stack, since the others will be # in the same list and thus nuked automatically if new_stack_ptr == ffi.NULL: new_frame = ffi.gc(new_frame, lib.DestroyImageList) lib.AppendImageToList(new_stack_ptr, new_frame) p = lib.GetNextImageInList(p) new = type(self)(new_stack_ptr[0]) # Repage by default after a crop; not doing this is unexpected and # frankly insane. Plain old `+repage` behavior would involve nuking # the page entirely, but that would screw up multiple frames; instead, # shift the canvas for every frame so the crop region's upper left # corner is the new origin, and fix the dimensions so every frame fits # (up to the size of the crop area, though ImageMagick should never # return an image bigger than the crop area... right?) if not preserve_canvas: # ImageMagick actually behaves when the crop area extends out # beyond the origin, so don't fix the edges in that case # TODO this is complex enough that i should perhaps just do it # myself left_delta = max(rect.left, 0) top_delta = max(rect.top, 0) # New canvas should be the size of the overlap between the current # canvas and the crop area new_canvas = rect.intersection(self.size.at(origin)) new_height = new_canvas.height new_width = new_canvas.width for frame in new: frame._frame.page.x -= left_delta frame._frame.page.y -= top_delta frame._frame.page.height = new_height frame._frame.page.width = new_width return new
def from_magick(cls, name): """Passes a filename specifier directly to ImageMagick. This allows reading from any of the magic pseudo-formats, like `clipboard` and `null`. Use with care with user input! """ image_info = blank_image_info() # Make sure not to overflow the char[] # TODO maybe just error out when this happens image_info.filename = name.encode('ascii')[:lib.MaxTextExtent] with magick_try() as exc: ptr = lib.ReadImage(image_info, exc.ptr) exc.check(ptr == ffi.NULL) return cls(ptr)
def parse(cls, name): """Parse a color specification. Supports a whole buncha formats. """ # TODO i don't like that this is tied to imagemagick's implementation. # would rather do most of the parsing myself, well-define what those # formats *are*, and use some other mechanism to expose the list of # builtin color names. (maybe several lists, even.) # TODO also this always returns RGB anyway. pixel = ffi.new("PixelPacket *") with magick_try() as exc: success = lib.QueryColorDatabase(name.encode('ascii'), pixel, exc.ptr) if not success: raise ValueError("Can't find a color named {0!r}".format(name)) return cls._from_pixel(pixel)
def _get_formats(): formats = dict() num_formats = ffi.new("size_t *") # Snag the list of known supported image formats with magick_try() as exc: magick_infos = ffi.gc( lib.GetMagickInfoList(b"*", num_formats, exc.ptr), lib.RelinquishMagickMemory) for i in range(num_formats[0]): name = ffi.string(magick_infos[i].name).decode('latin-1') formats[name.lower()] = ImageFormat( name=name, description=ffi.string( magick_infos[i].description).decode('latin-1'), can_read=magick_infos[i].decoder != ffi.NULL, can_write=magick_infos[i].encoder != ffi.NULL, supports_frames=magick_infos[i].adjoin != 0, ) return formats
def _cache_view_destructor(cache_view): with magick_try() as exc: # TODO bool return value as well lib.SyncCacheViewAuthenticPixels(cache_view, exc.ptr) lib.DestroyCacheView(cache_view)
def resized(self, size, filter=None): size = Size.coerce(size) # TODO allow picking a filter # TODO allow messing with blur? p = self._stack new_stack_ptr = ffi.new("Image **", ffi.NULL) if filter == 'box': c_filter = lib.BoxFilter else: c_filter = lib.UndefinedFilter target_width = size.width target_height = size.height ratio_width = target_width / (self._stack.page.width or self._stack.columns) ratio_height = target_height / (self._stack.page.height or self._stack.rows) while p: # Alrighty, so. ResizeImage takes the given size as the new size # of the FRAME, rather than the CANVAS, which is almost certainly # not what anyone expects. So do the math to fix this manually, # converting from canvas size to frame size. frame_width = int(p.columns * ratio_width + 0.5) frame_height = int(p.rows * ratio_height + 0.5) # ImageMagick, brilliant as it is, does not scale non-linear colorspaces # correctly. This definitely affects sRGB (see Issue-23) and may affect # other colorspaces. The "helpful" solution is to preconvert to linear RGB, # and postconvert to the original value. See also: # http://www.imagemagick.org/script/color-management.php inputImage = p with magick_try() as exc: # Assume ImageMagick will do the dumbest possible thing to non-RGB spaces. # TODO: But maybe we can trust it a bit more? if p.colorspace != lib.RGBColorspace: inputImage = lib.CloneImage(p, 0, 0, 0, exc.ptr) lib.TransformImageColorspace(inputImage, lib.RGBColorspace) if c_filter == lib.BoxFilter: # Use the faster ScaleImage in this special case new_frame = lib.ScaleImage(inputImage, frame_width, frame_height, exc.ptr) else: new_frame = lib.ResizeImage(inputImage, frame_width, frame_height, c_filter, 1.0, exc.ptr) if new_frame.colorspace != p.colorspace: lib.TransformImageColorspace(new_frame, p.colorspace) if inputImage != p: lib.DestroyImage(inputImage) # TODO how do i do this correctly etc? will it ever be non-null?? #except Exception: # lib.DestroyImage(new_frame) # ImageMagick uses new_size/old_size to compute the resized frame's # position. But new_size has already been rounded, so for small # frames in a large image, the double rounding error can place the # new frame a noticable distance from where one might expect. Fix # the canvas manually, too. new_frame.page.width = target_width new_frame.page.height = target_height new_frame.page.x = int(p.page.x * ratio_width + 0.5) new_frame.page.y = int(p.page.y * ratio_height + 0.5) lib.AppendImageToList(new_stack_ptr, new_frame) p = lib.GetNextImageInList(p) return type(self)(new_stack_ptr[0])
def resized(self, size, filter=None): size = Size.coerce(size) # TODO allow picking a filter # TODO allow messing with blur? p = self._stack new_stack_ptr = ffi.new("Image **", ffi.NULL) if filter == "box": c_filter = lib.BoxFilter else: c_filter = lib.UndefinedFilter target_width = size.width target_height = size.height ratio_width = target_width / (self._stack.page.width or self._stack.columns) ratio_height = target_height / (self._stack.page.height or self._stack.rows) while p: # Alrighty, so. ResizeImage takes the given size as the new size # of the FRAME, rather than the CANVAS, which is almost certainly # not what anyone expects. So do the math to fix this manually, # converting from canvas size to frame size. frame_width = int(p.columns * ratio_width + 0.5) frame_height = int(p.rows * ratio_height + 0.5) # ImageMagick, brilliant as it is, does not scale non-linear colorspaces # correctly. This definitely affects sRGB (see Issue-23) and may affect # other colorspaces. The "helpful" solution is to preconvert to linear RGB, # and postconvert to the original value. See also: # http://www.imagemagick.org/script/color-management.php inputImage = p with magick_try() as exc: # Assume ImageMagick will do the dumbest possible thing to non-RGB spaces. # TODO: But maybe we can trust it a bit more? if p.colorspace != lib.RGBColorspace: inputImage = lib.CloneImage(p, 0, 0, 0, exc.ptr) lib.TransformImageColorspace(inputImage, lib.RGBColorspace) if c_filter == lib.BoxFilter: # Use the faster ScaleImage in this special case new_frame = lib.ScaleImage(inputImage, frame_width, frame_height, exc.ptr) else: new_frame = lib.ResizeImage(inputImage, frame_width, frame_height, c_filter, 1.0, exc.ptr) if new_frame.colorspace != p.colorspace: lib.TransformImageColorspace(new_frame, p.colorspace) if inputImage != p: lib.DestroyImage(inputImage) # TODO how do i do this correctly etc? will it ever be non-null?? # except Exception: # lib.DestroyImage(new_frame) # ImageMagick uses new_size/old_size to compute the resized frame's # position. But new_size has already been rounded, so for small # frames in a large image, the double rounding error can place the # new frame a noticable distance from where one might expect. Fix # the canvas manually, too. new_frame.page.width = target_width new_frame.page.height = target_height new_frame.page.x = int(p.page.x * ratio_width + 0.5) new_frame.page.y = int(p.page.y * ratio_height + 0.5) lib.AppendImageToList(new_stack_ptr, new_frame) p = lib.GetNextImageInList(p) return type(self)(new_stack_ptr[0])
def __call__(self, *frames, **kwargs): channel = kwargs.get('channel', lib.DefaultChannels) # TODO force_python should go away and this should become a wrapper for # evaluate_python # We're gonna be using this a lot, so let's cast it to a C int just # once (and get the error early if it's a bogus type) c_channel = ffi.cast('ChannelType', channel) # TODO any gc concerns in this? for f in frames: assert isinstance(f, ImageFrame) # XXX how to handle frames of different sizes? gravity? scaling? first # frame as the master? hm frame = frames[0] # TODO does this create a blank image or actually duplicate the pixels?? docs say it actually copies with (0, 0) but the code just refs the same pixel cache? # TODO could use an inplace version for, e.g. the SVG-style compose operators # TODO also might want a different sized clone! with magick_try() as exc: new_stack = lib.CloneImage(frame._frame, 0, 0, lib.MagickTrue, exc.ptr) exc.check(new_stack == ffi.NULL) # TODO: set image to full-color. # TODO: work out how this works, how different colorspaces work, and waht the ImageType has to do with anything # QUESTION: this doesn't actually do anything. how does it work? does it leave indexes populated? what happens if this isn't done? # if (SetImageStorageClass(fx_image,DirectClass) == MagickFalse) { # InheritException(exception,&fx_image->exception); # fx_image=DestroyImage(fx_image); # return((Image *) NULL); # } out_view = lib.AcquireCacheView(new_stack) # TODO i need to be a list in_view = lib.AcquireCacheView(frame._frame) state = FilterState() for y in range(frame._frame.rows): with magick_try() as exc: q = lib.GetCacheViewAuthenticPixels(out_view, 0, y, frame._frame.columns, 1, exc.ptr) exc.check(q == ffi.NULL) # TODO is this useful who knows #fx_indexes=GetCacheViewAuthenticIndexQueue(fx_view); for x in range(frame._frame.columns): # TODO per-channel things # TODO for usage: see line 1453 #GetMagickPixelPacket(image,&pixel); #(void) InterpolateMagickPixelPacket(image,fx_info->view[i],image->interpolate, point.x,point.y,&pixel,exception); # Set up state object # TODO document that this is reused, or somethin state._color = BaseColor._from_pixel(q) ret = self.impl(state) #q.red = c_api.RoundToQuantum(<c_api.MagickRealType> ret.c_struct.red * c_api.QuantumRange) #q.green = c_api.RoundToQuantum(<c_api.MagickRealType> ret.c_struct.green * c_api.QuantumRange) #q.blue = c_api.RoundToQuantum(<c_api.MagickRealType> ret.c_struct.blue * c_api.QuantumRange) # TODO black, opacity? # TODO seems like this should apply to any set of channels, but # IM's -fx only understands RGB # TODO this is a little invasive, but given that this inner # loop runs for every f*****g pixel, i'd like to avoid method # calls as much as possible. even that rgb() can add up rgb = ret.rgb() lib.sanpera_pixel_from_doubles_channel(q, rgb._array, c_channel) # XXX this is actually black # if (((channel & IndexChannel) != 0) && (fx_image->colorspace == CMYKColorspace)) { # (void) FxEvaluateChannelExpression(fx_info[id],IndexChannel,x,y, &alpha,exception); # SetPixelIndex(fx_indexes+x,RoundToQuantum((MagickRealType) QuantumRange*alpha)); # } q += 1 # q++ with magick_try() as exc: lib.SyncCacheViewAuthenticPixels(in_view, exc.ptr) # TODO check exception, return value # XXX destroy in_view # XXX destroy out_view s return Image(new_stack)
def resized(self, size, filter=None): size = Size.coerce(size) # TODO allow picking a filter # TODO allow messing with blur? p = self._stack new_stack_ptr = ffi.new("Image **", ffi.NULL) if filter == 'box': c_filter = lib.BoxFilter else: c_filter = lib.UndefinedFilter target_width = size.width target_height = size.height ratio_width = target_width / (self._stack.page.width or self._stack.columns) ratio_height = target_height / (self._stack.page.height or self._stack.rows) while p: # Alrighty, so. ResizeImage takes the given size as the new size # of the FRAME, rather than the CANVAS, which is almost certainly # not what anyone expects. So do the math to fix this manually, # converting from canvas size to frame size. frame_width = int(p.columns * ratio_width + 0.5) frame_height = int(p.rows * ratio_height + 0.5) # Imagemagick seems to consider sRGB (nonlinear) to be the default colorspace # for images without colorspace information. In doing so, it might mangle colors # during some operations which expect a linear colorspace. It also seems to prefer # sRGB as output, rather than using the input colorspace. # See also: # http://www.imagemagick.org/script/color-management.php inputImage = p with magick_try() as exc: if c_filter == lib.BoxFilter: # Use the faster ScaleImage in this special case new_frame = lib.ScaleImage( inputImage, frame_width, frame_height, exc.ptr) else: new_frame = lib.ResizeImage( inputImage, frame_width, frame_height, c_filter, 1.0, exc.ptr) if new_frame.colorspace != p.colorspace and p.colorspace != lib.UndefinedColorspace: lib.TransformImageColorspace(new_frame, p.colorspace) if inputImage != p: lib.DestroyImage(inputImage) # TODO how do i do this correctly etc? will it ever be non-null?? #except Exception: # lib.DestroyImage(new_frame) # ImageMagick uses new_size/old_size to compute the resized frame's # position. But new_size has already been rounded, so for small # frames in a large image, the double rounding error can place the # new frame a noticable distance from where one might expect. Fix # the canvas manually, too. new_frame.page.width = target_width new_frame.page.height = target_height new_frame.page.x = int(p.page.x * ratio_width + 0.5) new_frame.page.y = int(p.page.y * ratio_height + 0.5) lib.AppendImageToList(new_stack_ptr, new_frame) p = lib.GetNextImageInList(p) return type(self)(new_stack_ptr[0])