Example #1
0
    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)
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
    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])
Example #5
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
Example #6
0
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
Example #7
0
    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)
Example #8
0
    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])
Example #9
0
    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)
Example #10
0
    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)
Example #11
0
    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)
Example #12
0
    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)
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
    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)
Example #16
0
    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)
Example #17
0
    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)
Example #18
0
    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)
Example #19
0
    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)
Example #20
0
    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)
Example #21
0
    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)
Example #22
0
    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)
Example #23
0
    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)
Example #24
0
    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
Example #25
0
    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)
Example #26
0
    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)
Example #27
0
    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
Example #28
0
    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)
Example #29
0
    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)
Example #30
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
Example #31
0
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)
Example #32
0
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)
Example #33
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])
Example #34
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])
Example #35
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)
Example #36
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])