Exemple #1
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)
Exemple #2
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)
Exemple #3
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)
Exemple #4
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)
Exemple #5
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)
Exemple #6
0
 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)
Exemple #7
0
 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)
Exemple #8
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
Exemple #9
0
    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)
Exemple #10
0
 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
Exemple #11
0
 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
Exemple #12
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
Exemple #13
0
    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)
Exemple #14
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])
Exemple #15
0
 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)
Exemple #16
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])
Exemple #17
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.,
    )
Exemple #18
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
Exemple #19
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
Exemple #20
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)
Exemple #21
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
Exemple #22
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
Exemple #23
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)
Exemple #24
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
Exemple #25
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
Exemple #26
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)
Exemple #27
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
Exemple #28
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)
Exemple #29
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()
Exemple #30
0
def blank_magick_pixel():
    magick_pixel = ffi.new("MagickPixelPacket *")
    lib.GetMagickPixelPacket(ffi.NULL, magick_pixel)
    return magick_pixel
Exemple #31
0
 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)
Exemple #32
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])
Exemple #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])
Exemple #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 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])
Exemple #35
0
def blank_magick_pixel():
    magick_pixel = ffi.new("MagickPixelPacket *")
    lib.GetMagickPixelPacket(ffi.NULL, magick_pixel)
    return magick_pixel
Exemple #36
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()