CGRotation

A PyObjC Example without documentation

Sources

CGImageUtils.py

import objc
import Quartz
import Cocoa
import LaunchServices
import math

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 = 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,
                floorf((bounds.size.width - imageRect.size.height) / 2.0),
                floorf((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

CGImageView.py

import objc
import Cocoa
import Quartz
import CGImageUtils

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)

Controller.py

import objc
import Quartz
import Cocoa
import CGImageUtils
import LaunchServices
import math

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(), u"demo", u"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)

    # Returns an array with the extensions that match the given Uniform Type Identifier (UTI).
    def extensionsForUTI_(self, 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 = Cocoa.CFGetTypeID(filenameExtensions)
                        if type == Cocoa.CFStringGetTypeID():
                            # If a string was exported, then wrap it up in an array.
                            extensions = Cocoa.NSArray.arrayWithObject_(filenameExtensions)
                        elif type == 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_(
                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_(
                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

main.py

from PyObjCTools import AppHelper
import objc; objc.setVerbose(True)

import CGImageView
import Controller
import CGImageUtils

AppHelper.runEventLoop()

setup.py

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