CGRotation
==========

* :download:`Download example <PyObjCExample-CGRotation.zip>`

CGRotation is a Cocoa application that demonstrates how to use the Current Transformation Matrix (CTM) to apply rotation, scaling and transformation to an image for display. It also demonstrates simple usage of ImageIO to load and save images and how to get the pixel data from a CGImageRef into an arbitrary buffer.


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

CGImageUtils.py
...............

.. sourcecode:: python

    import math
    
    import Cocoa
    import LaunchServices
    import objc
    import Quartz
    
    
    class ImageInfo(object):
        __slots__ = (
            "fRotation",
            "fScaleX",
            "fScaleY",
            "fTranslateX",
            "fTranslateY",
            "fImageRef",
            "fProperties",
            "fOrientation",
        )
    
        def __init__(self):
            self.fRotation = 0.0  # The rotation about the center of the image (degrees)
            self.fScaleX = 0.0  # The scaling of the image along it's X-axis
            self.fScaleY = 0.0  # The scaling of the image along it's Y-axis
            self.fTranslateX = 0.0  # Move the image along the X-axis
            self.fTranslateY = 0.0  # Move the image along the Y-axis
            self.fImageRef = None  # The image itself
            self.fProperties = None  # Image properties
            self.fOrientation = (
                None  # Affine transform that ensures the image displays correctly
            )
    
    
    # Create a new image from a file at the given url
    # Returns None if unsuccessful.
    def IICreateImage(url):
        ii = None
        # Try to create an image source to the image passed to us
        imageSrc = Quartz.CGImageSourceCreateWithURL(url, None)
        if imageSrc is not None:
            # And if we can, try to obtain the first image available
            image = Quartz.CGImageSourceCreateImageAtIndex(imageSrc, 0, None)
            if image is not None:
                # and if we could, create the ImageInfo struct with default values
                ii = ImageInfo()
                ii.fRotation = 0.0
                ii.fScaleX = 1.0
                ii.fScaleY = 1.0
                ii.fTranslateX = 0.0
                ii.fTranslateY = 0.0
                # the ImageInfo struct owns this CGImageRef now, so no need for a retain.
                ii.fImageRef = image
                # the ImageInfo struct owns this CFDictionaryRef, so no need for a retain.
                ii.fProperties = Quartz.CGImageSourceCopyPropertiesAtIndex(
                    imageSrc, 0, None
                )
                # Setup the orientation transformation matrix so that the image will
                # display with the proper orientation
                IIGetOrientationTransform(ii)
    
        return ii
    
    
    # Transforms the context based on the orientation of the image.
    # This ensures the image always appears correctly when drawn.
    def IIGetOrientationTransform(image):
        w = Quartz.CGImageGetWidth(image.fImageRef)
        h = Quartz.CGImageGetHeight(image.fImageRef)
        if image.fProperties is not None:
            # The Orientations listed here are mirroed from CGImageProperties.h,
            # listed under the kCGImagePropertyOrientation key.
            orientation = IIGetImageOrientation(image)
            if orientation == 1:
                # 1 = 0th row is at the top, and 0th column is on the left.
                # Orientation Normal
                image.fOrientation = Quartz.CGAffineTransformMake(
                    1.0, 0.0, 0.0, 1.0, 0.0, 0.0
                )
    
            elif orientation == 2:
                # 2 = 0th row is at the top, and 0th column is on the right.
                # Flip Horizontal
                image.fOrientation = Quartz.CGAffineTransformMake(
                    -1.0, 0.0, 0.0, 1.0, w, 0.0
                )
    
            elif orientation == 3:
                # 3 = 0th row is at the bottom, and 0th column is on the right.
                # Rotate 180 degrees
                image.fOrientation = Quartz.CGAffineTransformMake(
                    -1.0, 0.0, 0.0, -1.0, w, h
                )
    
            elif orientation == 4:
                # 4 = 0th row is at the bottom, and 0th column is on the left.
                # Flip Vertical
                image.fOrientation = Quartz.CGAffineTransformMake(1.0, 0.0, 0, -1.0, 0.0, h)
    
            elif orientation == 5:
                # 5 = 0th row is on the left, and 0th column is the top.
                # Rotate -90 degrees and Flip Vertical
                image.fOrientation = Quartz.CGAffineTransformMake(
                    0.0, -1.0, -1.0, 0.0, h, w
                )
    
            elif orientation == 6:
                # 6 = 0th row is on the right, and 0th column is the top.
                # Rotate 90 degrees
                image.fOrientation = Quartz.CGAffineTransformMake(
                    0.0, -1.0, 1.0, 0.0, 0.0, w
                )
    
            elif orientation == 7:
                # 7 = 0th row is on the right, and 0th column is the bottom.
                # Rotate 90 degrees and Flip Vertical
                image.fOrientation = Quartz.CGAffineTransformMake(
                    0.0, 1.0, 1.0, 0.0, 0.0, 0.0
                )
    
            elif orientation == 8:
                # 8 = 0th row is on the left, and 0th column is the bottom.
                # Rotate -90 degrees
                image.fOrientation = Quartz.CGAffineTransformMake(
                    0.0, 1.0, -1.0, 0.0, h, 0.0
                )
    
    
    # Gets the orientation of the image from the properties dictionary if available
    # If the kCGImagePropertyOrientation is not available or invalid,
    # then 1, the default orientation, is returned.
    def IIGetImageOrientation(image):
        result = 1
        if image.fProperties is not None:
            orientation = image.fProperties.get(Quartz.kCGImagePropertyOrientation)
            if orientation is not None:
                result = orientation
    
        return result
    
    
    # Save the given image to a file at the given url.
    # Returns true if successful, false otherwise.
    def IISaveImage(image, url, width, height):
        result = False
    
        # If there is no image, no destination, or the width/height is 0, then fail early.
        assert (
            (image is not None) and (url is not None) and (width != 0.0) and (height != 0.0)
        )
    
        # Try to create a jpeg image destination at the url given to us
        imageDest = Quartz.CGImageDestinationCreateWithURL(
            url, LaunchServices.kUTTypeJPEG, 1, None
        )
        if imageDest is not None:
            # And if we can, then we can start building our final image.
            # We begin by creating a CGBitmapContext to host our desintation image.
    
            # Allocate enough space to hold our pixels
            imageData = objc.allocateBuffer(int(4 * width * height))
    
            # Create the bitmap context
            bitmapContext = Quartz.CGBitmapContextCreate(
                imageData,  # image data we just allocated...
                width,  # width
                height,  # height
                8,  # 8 bits per component
                4 * width,  # bytes per pixel times number of pixels wide
                Quartz.CGImageGetColorSpace(
                    image.fImageRef
                ),  # use the same colorspace as the original image
                Quartz.kCGImageAlphaPremultipliedFirst,
            )  # use premultiplied alpha
    
            # Check that all that went well
            if bitmapContext is not None:
                # Now, we draw the image to the bitmap context
                IIDrawImageTransformed(
                    image, bitmapContext, Quartz.CGRectMake(0.0, 0.0, width, height)
                )
    
                # We have now gotten our image data to the bitmap context, and correspondingly
                # into imageData. If we wanted to, we could look at any of the pixels of the image
                # and manipulate them in any way that we desire, but for this case, we're just
                # going to ask ImageIO to write this out to disk.
    
                # Obtain a CGImageRef from the bitmap context for ImageIO
                imageIOImage = Quartz.CGBitmapContextCreateImage(bitmapContext)
    
                # Check if we have additional properties from the original image
                if image.fProperties is not None:
                    # If we do, then we want to inspect the orientation property.
                    # If it exists and is not the default orientation, then we
                    # want to replace that orientation in the destination file
                    orientation = IIGetImageOrientation(image)
                    if orientation != 1:
                        # If the orientation in the original image was not the default,
                        # then we need to replace that key in a duplicate of that dictionary
                        # and then pass that dictionary to ImageIO when adding the image.
                        prop = Cocoa.CFDictionaryCreateMutableCopy(
                            None, 0, image.fProperties
                        )
                        orientation = 1
                        prop[Quartz.kCGImagePropertyOrientation] = orientation
    
                        # And add the image with the new properties
                        Quartz.CGImageDestinationAddImage(imageDest, imageIOImage, prop)
    
                    else:
                        # Otherwise, the image was already in the default orientation and we can
                        # just save it with the original properties.
                        Quartz.CGImageDestinationAddImage(
                            imageDest, imageIOImage, image.fProperties
                        )
    
                else:
                    # If we don't, then just add the image without properties
                    Quartz.CGImageDestinationAddImage(imageDest, imageIOImage, None)
    
                del bitmapContext
    
            # Finalize the image destination
            result = Quartz.CGImageDestinationFinalize(imageDest)
            del imageDest
    
        return result
    
    
    # Applies the transformations specified in the ImageInfo struct without drawing the actual image
    def IIApplyTransformation(image, context, bounds):
        if image is not None:
            # Whenever you do multiple CTM changes, you have to be very careful with
            # order.  Changing the order of your CTM changes changes the outcome of
            # the drawing operation. For example, if you scale a context by 2.0 along
            # the x-axis, and then translate the context by 10.0 along the x-axis,
            # then you will see your drawing will be in a different position than if
            # you had done the operations in the opposite order.
            #
            # Our intent with this operation is that we want to change the location
            # from which we start drawing (translation), then rotate our axies so
            # that our image appears at an angle (rotation), and finally
            # scale our axies so that our image has a different size (scale).
            # Changing the order of operations will markedly change the results.
            IITranslateContext(image, context)
            IIRotateContext(image, context, bounds)
            IIScaleContext(image, context, bounds)
    
    
    # Draw the image to the given context centered inside the given bounds
    def IIDrawImage(image, context, bounds):
        imageRect = Cocoa.NSRect()
        if image is not None and context is not None:
            # Setup the image rect so that the image fills it's natural boudaries
            # in the base coordinate system.
            imageRect.origin.x = 0.0
            imageRect.origin.y = 0.0
            imageRect.size.width = Quartz.CGImageGetWidth(image.fImageRef)
            imageRect.size.height = Quartz.CGImageGetHeight(image.fImageRef)
    
            # Obtain the orientation matrix for this image
            ctm = image.fOrientation
    
            # Before we can apply the orientation matrix, we need to translate the
            # coordinate system so the center of the rectangle matces the center of
            # the image.
            if image.fProperties is None or IIGetImageOrientation(image) < 5:
                # For orientations 1-4, the images are unrotated, so the width and
                # height of the base image can be used as the width and height of
                # the coordinate translation calculation.
                Quartz.CGContextTranslateCTM(
                    context,
                    math.floor((bounds.size.width - imageRect.size.width) / 2.0),
                    math.floor((bounds.size.height - imageRect.size.height) / 2.0),
                )
    
            else:
                # For orientations 5-8, the images are rotated 90 or -90 degrees,
                # so we need to use the image width in place of the height and
                # vice versa.
                Quartz.CGContextTranslateCTM(
                    context,
                    math.floor((bounds.size.width - imageRect.size.height) / 2.0),
                    math.floor((bounds.size.height - imageRect.size.width) / 2.0),
                )
    
            # Finally, orient the context so that the image draws naturally.
            Quartz.CGContextConcatCTM(context, ctm)
    
            # And draw the image.
            Quartz.CGContextDrawImage(context, imageRect, image.fImageRef)
    
    
    # Rotates the context around the center point of the given bounds
    def IIRotateContext(image, context, bounds):
        # First we translate the context such that the 0,0 location is at the center of the bounds
        Quartz.CGContextTranslateCTM(
            context, bounds.size.width / 2.0, bounds.size.height / 2.0
        )
    
        # Then we rotate the context, converting our angle from degrees to radians
        Quartz.CGContextRotateCTM(context, image.fRotation * math.pi / 180.0)
    
        # Finally we have to restore the center position
        Quartz.CGContextTranslateCTM(
            context, -bounds.size.width / 2.0, -bounds.size.height / 2.0
        )
    
    
    # Scale the context around the center point of the given bounds
    def IIScaleContext(image, context, bounds):
        # First we translate the context such that the 0,0 location is at the center of the bounds
        Quartz.CGContextTranslateCTM(
            context, bounds.size.width / 2.0, bounds.size.height / 2.0
        )
    
        # Next we scale the context to the size that we want
        Quartz.CGContextScaleCTM(context, image.fScaleX, image.fScaleY)
    
        # Finally we have to restore the center position
        Quartz.CGContextTranslateCTM(
            context, -bounds.size.width / 2.0, -bounds.size.height / 2.0
        )
    
    
    # Translate the context
    def IITranslateContext(image, context):
        # Translation is easy, just translate.
        Quartz.CGContextTranslateCTM(context, image.fTranslateX, image.fTranslateY)
    
    
    # Draw the image to the given context centered inside the given bounds with
    # the transformation info. The CTM of the context is unchanged after this call
    def IIDrawImageTransformed(image, context, bounds):
        # We save the current graphics state so as to not disrupt it for the caller.
        Quartz.CGContextSaveGState(context)
    
        # Apply the transformation
        IIApplyTransformation(image, context, bounds)
    
        # Draw the image centered in the context
        IIDrawImage(image, context, bounds)
    
        # Restore our original graphics state.
        Quartz.CGContextRestoreGState(context)
    
    
    # Release the ImageInfo struct and other associated data
    # you should not refer to the reference after this call
    # This function is None safe.
    def IIRelease(image):
        pass

.. rst-class:: tabbertab

CGImageView.py
..............

.. sourcecode:: python

    import CGImageUtils
    import Cocoa
    import objc
    import Quartz
    
    
    class CGImageView(Cocoa.NSView):
        _image = objc.ivar()
    
        def setImage_(self, img):
            if img is not None and self._image is not img:
                self._image = img
                # Mark this view as needing to be redisplayed.
                self.setNeedsDisplay_(True)
    
        def image(self):
            return self._image
    
        def drawRect_(self, rect):
            # Obtain the current context
            ctx = Cocoa.NSGraphicsContext.currentContext().graphicsPort()
    
            # Draw the image in the context
            CGImageUtils.IIDrawImageTransformed(
                self._image,
                ctx,
                Quartz.CGRectMake(
                    rect.origin.x, rect.origin.y, rect.size.width, rect.size.height
                ),
            )
    
            # Draw the view border, just a simple stroked rectangle
            Quartz.CGContextAddRect(
                ctx,
                Quartz.CGRectMake(
                    rect.origin.x, rect.origin.y, rect.size.width, rect.size.height
                ),
            )
            Quartz.CGContextSetRGBStrokeColor(ctx, 1.0, 0.0, 0.0, 1.0)
            Quartz.CGContextStrokePath(ctx)

.. rst-class:: tabbertab

Controller.py
.............

.. sourcecode:: python

    import math
    
    import CGImageUtils
    import Cocoa
    import LaunchServices
    import objc
    import Quartz
    
    
    class Controller(Cocoa.NSObject):
        imageView = objc.IBOutlet()
        scaleYView = objc.IBOutlet()
        textScaleYView = objc.IBOutlet()
    
        _rotation = objc.ivar.float()
        _scaleX = objc.ivar.float()
        _scaleY = objc.ivar.float()
        _translateX = objc.ivar.float()
        _translateY = objc.ivar.float()
        _preserveAspectRatio = objc.ivar.bool()
    
        openImageIOSupportedTypes = objc.ivar()
    
        def awakeFromNib(self):
            self.openImageIOSupportedTypes = None
            # Ask CFBundle for the location of our demo image
            url = Cocoa.CFBundleCopyResourceURL(
                Cocoa.CFBundleGetMainBundle(), "demo", "png", None
            )
            if url is not None:
                # And if available, load it
                self.imageView.setImage_(CGImageUtils.IICreateImage(url))
    
            self.imageView.window().center()
            self.setRotation_(0.0)
            self.setScaleX_(1.0)
            self.setScaleY_(1.0)
            self.setTranslateX_(0.0)
            self.setTranslateY_(0.0)
            self.setPreserveAspectRatio_(False)
    
        @objc.IBAction
        def changeScaleX_(self, sender):
            self.setScaleX_(self._scaleX + sender.floatValue())
            sender.setFloatValue_(0.0)
    
        @objc.IBAction
        def changeScaleY_(self, sender):
            self.setScaleY_(self._scaleY + sender.floatValue())
            sender.setFloatValue_(0.0)
    
        @objc.IBAction
        def changeTranslateX_(self, sender):
            self.setTranslateX_(self._translateX + sender.floatValue())
            sender.setFloatValue_(0.0)
    
        @objc.IBAction
        def changeTranslateY_(self, sender):
            self.setTranslateY_(self._translateY + sender.floatValue())
            sender.setFloatValue_(0.0)
    
        @objc.IBAction
        def reset_(self, sender):
            self.setRotation_(0.0)
            self.setScaleX_(1.0)
            self.setScaleY_(1.0)
            self.setTranslateX_(0.0)
            self.setTranslateY_(0.0)
    
            self.imageView.setNeedsDisplay_(True)
    
        def extensionsForUTI_(self, uti):
            """
            Returns an array with the extensions that match the given
            Uniform Type Identifier (UTI).
            """
            # If anything goes wrong, we'll return None, otherwise this will be the array
            # of extensions for this image type.
            extensions = None
            # Only get extensions for UTIs that are images (i.e. conforms to
            # public.image aka kUTTypeImage) This excludes PDF support that ImageIO
            # advertises, but won't actually use.
            if LaunchServices.UTTypeConformsTo(uti, LaunchServices.kUTTypeImage):
                # Copy the decleration for the UTI (if it exists)
                decleration = LaunchServices.UTTypeCopyDeclaration(uti)
                if decleration is not None:
                    # Grab the tags for this UTI, which includes extensions, OSTypes and MIME types.
                    tags = Cocoa.CFDictionaryGetValue(
                        decleration, LaunchServices.kUTTypeTagSpecificationKey
                    )
                    if tags is not None:
                        # We are interested specifically in the extensions that this UTI uses
                        filenameExtensions = tags.get(
                            LaunchServices.kUTTagClassFilenameExtension
                        )
                        if filenameExtensions is not None:
                            # It is valid for a UTI to export either an Array
                            # (of Strings) representing multiple tags, or a String
                            # representing a single tag.
                            type_id = Cocoa.CFGetTypeID(filenameExtensions)
                            if type_id == Cocoa.CFStringGetTypeID():
                                # If a string was exported, then wrap it up in an array.
                                extensions = Cocoa.NSArray.arrayWithObject_(
                                    filenameExtensions
                                )
                            elif type_id == Cocoa.CFArrayGetTypeID():
                                # If an array was exported, then just return that array.
                                extensions = filenameExtensions.copy()
    
            return extensions
    
        # On Tiger NSOpenPanel only understands extensions, not UTIs, so we have to
        # obtain a list of extentions from the UTIs that Image IO tells us it can
        # handle.
        def createOpenTypesArray(self):
            if self.openImageIOSupportedTypes is None:
                imageIOUTIs = Quartz.CGImageSourceCopyTypeIdentifiers()
                count = len(imageIOUTIs)
                self.openImageIOSupportedTypes = Cocoa.NSMutableArray.alloc().initWithCapacity_(
                    count
                )
                for i in range(count):
                    self.openImageIOSupportedTypes.addObjectsFromArray_(
                        self.extensionsForUTI_(imageIOUTIs[i])
                    )
    
        @objc.IBAction
        def openDocument_(self, sender):
            panel = Cocoa.NSOpenPanel.openPanel()
            panel.setAllowsMultipleSelection_(False)
            panel.setResolvesAliases_(True)
            panel.setTreatsFilePackagesAsDirectories_(True)
    
            self.createOpenTypesArray()
    
            panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(  # noqa: B950
                None,
                None,
                self.openImageIOSupportedTypes,
                self.imageView.window(),
                self,
                "openImageDidEnd:returnCode:contextInfo:",
                None,
            )
    
        @objc.signature(b"v@:@i^v")
        def openImageDidEnd_returnCode_contextInfo_(self, panel, returnCode, contextInfo_):
            if returnCode == Cocoa.NSOKButton:
                if len(panel.filenames()) > 0:
                    image = CGImageUtils.IICreateImage(
                        Cocoa.NSURL.fileURLWithPath_(panel.filenames()[0])
                    )
                    if image is not None:
                        # Ownership is transferred to the CGImageView.
                        self.imageView.setImage_(image)
    
        @objc.IBAction
        def saveDocumentAs_(self, sender):
            panel = Cocoa.NSSavePanel.savePanel()
            panel.setCanSelectHiddenExtension_(True)
            panel.setRequiredFileType_("jpeg")
            panel.setAllowsOtherFileTypes_(False)
            panel.setTreatsFilePackagesAsDirectories_(True)
    
            panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(  # noqa: B950
                None,
                "untitled image",
                self.imageView.window(),
                self,
                "saveImageDidEnd:returnCode:contextInfo:",
                None,
            )
    
        @objc.signature(b"v@:@i^v")
        def saveImageDidEnd_returnCode_contextInfo_(self, panel, returnCode, contextInfo):
            if returnCode == Cocoa.NSOKButton:
                frame = self.imageView.frame()
                CGImageUtils.IISaveImage(
                    self.imageView.image(),
                    panel.URL(),
                    math.ceil(frame.size.width),
                    math.ceil(frame.size.height),
                )
    
        def setRotation_(self, r):
            r = r % 360.0
            if r < 0:
                r += 360.0
    
            self._rotation = r
            self.imageView.image().fRotation = 360.0 - r  # XXX
            self.imageView.setNeedsDisplay_(True)
    
        def setScaleX_(self, x):
            self._scaleX = x
            self.imageView.image().fScaleX = self._scaleX
            if self._preserveAspectRatio:
                self.imageView.image().fScaleY = self._scaleX
    
            self.imageView.setNeedsDisplay_(True)
    
        def setScaleY_(self, y):
            self._scaleY = y
            if not self._preserveAspectRatio:
                self.imageView.image().fScaleY = self._scaleY
                self.imageView.setNeedsDisplay_(True)
    
        def setPreserveAspectRatio_(self, preserve):
            self._preserveAspectRatio = preserve
            self.imageView.image().fScaleX = self._scaleX
            if self._preserveAspectRatio:
                self.imageView.image().fScaleY = self._scaleX
    
            else:
                self.imageView.image().fScaleY = self._scaleY
    
            self.scaleYView.setEnabled_(not self._preserveAspectRatio)
            self.textScaleYView.setEnabled_(not self._preserveAspectRatio)
            self.imageView.setNeedsDisplay_(True)
    
        def setTranslateX_(self, x):
            self._translateX = x
            self.imageView.image().fTranslateX = self._translateX
            self.imageView.setNeedsDisplay_(True)
    
        def setTranslateY_(self, y):
            self._translateY = y
            self.imageView.image().fTranslateY = self._translateY
            self.imageView.setNeedsDisplay_(True)
    
        def rotation(self):
            return self._rotation
    
        def scaleX(self):
            return self._scaleX
    
        def scaleY(self):
            return self._scaleY
    
        def preserveAspectRatio(self):
            return self._preserveAspectRatio
    
        def translateX(self):
            return self._translateX
    
        def translateY(self):
            return self._translateY

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import CGImageUtils  # noqa: F401
    import CGImageView  # noqa: F401
    import Controller  # noqa: F401
    from PyObjCTools import AppHelper
    
    
    AppHelper.runEventLoop()

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

    """
    Script for building the example.
    
    Usage:
        python3 setup.py py2app
    """
    from setuptools import setup
    
    setup(
        name="CGRotation",
        app=["main.py"],
        data_files=["English.lproj", "demo.png"],
        setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
    )

