CGRotation
==========

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

A PyObjC Example without documentation

.. 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"],
    )

