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, *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 color(self, value): if self._pixel == ffi.NULL: raise ValueError("Pixel has expired") array = ffi.new( "double[]", [value._red, value._green, value._blue, value._opacity]) lib.sanpera_pixel_from_doubles(self._pixel, array)
def color(self): # XXX this needs to do something special to handle non-rgba images if self._pixel == ffi.NULL: raise ValueError("Pixel has expired") array = ffi.new("double[]", 4) lib.sanpera_pixel_to_doubles(self._pixel, array) return RGBColor(*array)
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 _from_pixel(cls, pixel): """Create a color from a PixelPacket.""" array = ffi.new("double[]", 4) lib.sanpera_pixel_to_doubles(pixel, array) # Okay, yes, this isn't much of a classmethod. TODO? return RGBColor._from_c_array(array)
def to_rect_info(self): rectinfo = ffi.new("RectangleInfo *") rectinfo.x = self.left rectinfo.y = self.top rectinfo.width = self.width rectinfo.height = self.height return rectinfo
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 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 op_color(color): _color = ffi.new("PixelPacket *") color._populate_pixel(_color) return dict( op=lib.SANPERA_OP_LOAD_COLOR, color=_color, number=0., )
def __init__(self, red, green, blue, opacity=1.0, _array=None): self._red = red self._green = green self._blue = blue self._opacity = opacity self._extra_channels = () if _array is None: self._array = ffi.new("double[]", [red, green, blue, opacity]) else: self._array = _array
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 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 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 gradient(self, from_color, to_color): gradient_info = self.draw_info.gradient gradient_info.type = lib.LinearGradient width = self.frame._frame.columns height = self.frame._frame.rows # Fit bounding box to the size of the image itself gradient_info.bounding_box.x = 0 gradient_info.bounding_box.y = 0 gradient_info.bounding_box.width = width gradient_info.bounding_box.height = height # Draw it vertically for now gradient_info.gradient_vector.x1 = 0. gradient_info.gradient_vector.y1 = 0. gradient_info.gradient_vector.x2 = 0. gradient_info.gradient_vector.y2 = height - 1. gradient_info.spread = lib.PadSpread gradient_info.center.x = width / 2 gradient_info.center.y = height / 2 gradient_info.radius = lib.sanpera_to_magick_real_type( max(width, height) / 2) # Construct some stops stops = ffi.new("StopInfo[]", 2) from_color._populate_magick_pixel(ffi.addressof(stops[0], "color")) stops[0].offset = lib.sanpera_to_magick_real_type(0.) to_color._populate_magick_pixel(ffi.addressof(stops[1], "color")) stops[1].offset = lib.sanpera_to_magick_real_type(1.) try: gradient_info.stops = stops gradient_info.number_stops = 2 lib.DrawGradientImage(self.frame._frame, self.draw_info) finally: gradient_info.stops = ffi.NULL gradient_info.number_stops = 0
def gradient(self, from_color, to_color): gradient_info = self.draw_info.gradient gradient_info.type = lib.LinearGradient width = self.frame._frame.columns height = self.frame._frame.rows # Fit bounding box to the size of the image itself gradient_info.bounding_box.x = 0 gradient_info.bounding_box.y = 0 gradient_info.bounding_box.width = width gradient_info.bounding_box.height = height # Draw it vertically for now gradient_info.gradient_vector.x1 = 0. gradient_info.gradient_vector.y1 = 0. gradient_info.gradient_vector.x2 = 0. gradient_info.gradient_vector.y2 = height - 1. gradient_info.spread = lib.PadSpread gradient_info.center.x = width / 2 gradient_info.center.y = height / 2 gradient_info.radius = lib.sanpera_to_magick_real_type(max(width, height) / 2) # Construct some stops stops = ffi.new("StopInfo[]", 2) from_color._populate_magick_pixel(ffi.addressof(stops[0], "color")) stops[0].offset = lib.sanpera_to_magick_real_type(0.) to_color._populate_magick_pixel(ffi.addressof(stops[1], "color")) stops[1].offset = lib.sanpera_to_magick_real_type(1.) try: gradient_info.stops = stops gradient_info.number_stops = 2 lib.DrawGradientImage(self.frame._frame, self.draw_info) finally: gradient_info.stops = ffi.NULL gradient_info.number_stops = 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) 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 __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)
from __future__ import division from __future__ import print_function from collections import namedtuple from sanpera._api import ffi, lib from sanpera.exception import magick_try FEATURES = frozenset( ffi.string(lib.GetMagickFeatures()).decode('ascii').split(' ')) HAS_OPENMP = 'OpenMP' in FEATURES HAS_OPENCL = 'OpenCL' in FEATURES HAS_HDRI = 'HDRI' in FEATURES # Version number is given as hex; version A.B.C is 0xABC _out = ffi.new("size_t *") lib.GetMagickVersion(_out) VERSION = ( (_out[0] & 0xf00) >> 8, (_out[0] & 0x0f0) >> 4, (_out[0] & 0x00f) >> 0, ) del _out ImageFormat = namedtuple( 'ImageFormat', ['name', 'description', 'can_read', 'can_write', 'supports_frames']) def _get_formats(): formats = dict()
def blank_magick_pixel(): magick_pixel = ffi.new("MagickPixelPacket *") lib.GetMagickPixelPacket(ffi.NULL, magick_pixel) return magick_pixel
def color(self, value): if self._pixel == ffi.NULL: raise ValueError("Pixel has expired") array = ffi.new("double[]", [value._red, value._green, value._blue, value._opacity]) lib.sanpera_pixel_from_doubles(self._pixel, array)
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 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])
from __future__ import division from __future__ import print_function from collections import namedtuple from sanpera._api import ffi, lib from sanpera.exception import magick_try FEATURES = frozenset(ffi.string(lib.GetMagickFeatures()).decode('ascii').split(' ')) HAS_OPENMP = 'OpenMP' in FEATURES HAS_OPENCL = 'OpenCL' in FEATURES HAS_HDRI = 'HDRI' in FEATURES # Version number is given as hex; version A.B.C is 0xABC _out = ffi.new("size_t *") lib.GetMagickVersion(_out) VERSION = ( (_out[0] & 0xf00) >> 8, (_out[0] & 0x0f0) >> 4, (_out[0] & 0x00f) >> 0, ) del _out ImageFormat = namedtuple( 'ImageFormat', ['name', 'description', 'can_read', 'can_write', 'supports_frames']) def _get_formats(): formats = dict()