CGRotation¶
A PyObjC Example without documentation
Sources¶
CGImageUtils.py¶
import math
import Cocoa
import LaunchServices
import objc
import Quartz
class ImageInfo:
__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 mirrored 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 destination 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 boundaries
# 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
CGImageView.py¶
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)
Controller.py¶
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 declaration for the UTI (if it exists)
declaration = LaunchServices.UTTypeCopyDeclaration(uti)
if declaration is not None:
# Grab the tags for this UTI, which includes extensions, OSTypes and MIME types.
tags = Cocoa.CFDictionaryGetValue(
declaration, 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 extensions 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
main.py¶
import CGImageUtils # noqa: F401
import CGImageView # noqa: F401
import Controller # noqa: F401
from PyObjCTools import AppHelper
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"],
)