Quartz2DBasics

A PyObjC Example without documentation

Sources

AppDrawing.py

import array
import math

import Cocoa
import Quartz
import UIHandling

kOurImageFile = "ptlobos.tif"

# For best performance make bytesPerRow a multiple of 16 bytes.
BEST_BYTE_ALIGNMENT = 16


def COMPUTE_BEST_BYTES_PER_ROW(bpr):
    return (bpr + (BEST_BYTE_ALIGNMENT - 1)) & ~(BEST_BYTE_ALIGNMENT - 1)


def DEGREES_TO_RADIANS(degrees):
    return degrees * math.pi / 180


_colorSpace = None


def myGetGenericRGBSpace():
    global _colorSpace

    if _colorSpace is None:
        _colorSpace = Quartz.CGColorSpaceCreateWithName(Quartz.kCGColorSpaceGenericRGB)

    return _colorSpace


_blue = None


def myGetBlueColor():
    global _blue

    if _blue is None:
        _blue = Quartz.CGColorCreate(myGetGenericRGBSpace(), (0, 0, 1, 1))

    return _blue


_green = None


def myGetGreenColor():
    global _green

    if _green is None:
        _green = Quartz.CGColorCreate(myGetGenericRGBSpace(), (0, 1, 0, 1))

    return _green


_red = None


def myGetRedColor():
    global _red

    if _red is None:
        _red = Quartz.CGColorCreate(myGetGenericRGBSpace(), (1, 0, 0, 1))

    return _red


_ourImageURL = None


def doDrawImageFile(context, doclip):
    global _ourImageURL

    if _ourImageURL is None:
        mainBundle = Cocoa.CFBundleGetMainBundle()
        if mainBundle:
            _ourImageURL = Cocoa.CFBundleCopyResourceURL(
                mainBundle, kOurImageFile, None, None
            )

        else:
            print("Can't get the app bundle!")

    if _ourImageURL:
        if doclip:
            clipImageToEllipse(context, _ourImageURL)
        else:
            drawCGImage(context, _ourImageURL)

    else:
        print("Couldn't create the URL for our Image file!")


def myDispatchDrawing(context, drawingType):
    if drawingType == UIHandling.kCommandStrokedAndFilledRects:
        drawStrokedAndFilledRects(context)

    elif drawingType == UIHandling.kCommandAlphaRects:
        drawAlphaRects(context)

    elif drawingType == UIHandling.kCommandSimpleClip:
        doDrawImageFile(context, True)

    elif drawingType == UIHandling.kCommandDrawImageFile:
        doDrawImageFile(context, False)

    elif drawingType == UIHandling.kCommandDoUncachedDrawing:
        drawUncachedForLayer(context)

    elif drawingType == UIHandling.kCommandDoCGLayer:
        drawSimpleCGLayer(context)


def drawStrokedAndFilledRects(context):
    ourRect = Quartz.CGRectMake(40, 40, 130, 100)

    # Set the fill color to an opaque blue.
    Quartz.CGContextSetFillColorWithColor(context, myGetBlueColor())
    # Fill the rect.
    Quartz.CGContextFillRect(context, ourRect)

    # Set the stroke color to an opaque green.
    Quartz.CGContextSetStrokeColorWithColor(context, myGetGreenColor())
    # Stroke the rect with a line width of 10 units.
    Quartz.CGContextStrokeRectWithWidth(context, ourRect, 10)

    # Save the current graphics state.
    Quartz.CGContextSaveGState(context)
    # Translate the coordinate system origin to the right
    # by 200 units.
    Quartz.CGContextTranslateCTM(context, 200, 0)
    # Stroke the rect with a line width of 10 units.
    Quartz.CGContextStrokeRectWithWidth(context, ourRect, 10)
    # Fill the rect.
    Quartz.CGContextFillRect(context, ourRect)
    # Restore the graphics state to the previously saved
    # graphics state. This restores all graphics state
    # parameters to those in effect during the last call
    # to CGContextSaveGState. In this example that restores
    # the coordinate system to that in effect prior to the
    # call to CGContextTranslateCTM.
    Quartz.CGContextRestoreGState(context)


#    Create a mutable path object that represents 'rect'.
#    Note that this is for demonstrating how to create a simple
#    CGPath object. The Quartz function CGPathAddRect would normally
#    be a better choice for adding a rect to a CGPath object.
def createRectPath(rect):
    path = Quartz.CGPathCreateMutable()

    # Start a new subpath.
    Quartz.CGPathMoveToPoint(path, None, rect.origin.x, rect.origin.y)

    # ***** Segment 1 *****
    Quartz.CGPathAddLineToPoint(
        path, None, rect.origin.x + rect.size.width, rect.origin.y
    )

    # ***** Segment 2 *****
    Quartz.CGPathAddLineToPoint(
        path, None, rect.origin.x + rect.size.width, rect.origin.y + rect.size.height
    )

    # ***** Segment 3 *****
    Quartz.CGPathAddLineToPoint(
        path, None, rect.origin.x, rect.origin.y + rect.size.height
    )

    # ***** Segment 4 is created by closing the path *****
    Quartz.CGPathCloseSubpath(path)

    return path


def drawAlphaRects(context):
    ourRect = Quartz.CGRectMake(0, 0, 130, 100)
    numRects = 6
    rotateAngle = 2 * math.pi / numRects
    tintAdjust = 1.0 / numRects

    # Create the path object representing our rectangle. This
    # example is for demonstrating the use of a CGPath object.
    # For a simple rectangular shape, you'd typically use
    # CGContextFillRect or CGContextStrokeRect instead of this
    # approach.
    path = createRectPath(ourRect)

    # Move the origin of coordinates to a location that allows
    # the drawing to be within the window.
    Quartz.CGContextTranslateCTM(
        context, 2 * ourRect.size.width, 2 * ourRect.size.height
    )

    # Set the fill color to a red color.
    Quartz.CGContextSetFillColorWithColor(context, myGetRedColor())

    tint = 1.0
    while 0 < tint:
        # Set the global alpha to the tint value.
        Quartz.CGContextSetAlpha(context, tint)

        # For a CGPath object that is a simple rect,
        # this is equivalent to CGContextFillRect.
        Quartz.CGContextBeginPath(context)
        Quartz.CGContextAddPath(context, path)
        Quartz.CGContextFillPath(context)

        # These transformations are cumulative.
        Quartz.CGContextRotateCTM(context, rotateAngle)

        tint -= tintAdjust


def drawCGImage(context, url):
    # Create a CGImageSource object from 'url'.
    imageSource = Quartz.CGImageSourceCreateWithURL(url, None)

    # Create a CGImage object from the first image in the file. Image
    # indexes are 0 based.
    image = Quartz.CGImageSourceCreateImageAtIndex(imageSource, 0, None)

    # Create a rectangle that has its origin at (100, 100) with the width
    # and height of the image itself.
    imageRect = Quartz.CGRectMake(
        100, 100, Quartz.CGImageGetWidth(image), Quartz.CGImageGetHeight(image)
    )

    # Draw the image into the rect.
    Quartz.CGContextDrawImage(context, imageRect, image)


def clipImageToEllipse(context, url):
    # Create a CGImageSource object from 'url'.
    imageSource = Quartz.CGImageSourceCreateWithURL(url, None)

    # Create a CGImage object from the first image in the file. Image
    # indexes are 0 based.
    image = Quartz.CGImageSourceCreateImageAtIndex(imageSource, 0, None)

    # Create a rectangle that has its origin at (100, 100) with the width
    # and height of the image itself.
    imageRect = Quartz.CGRectMake(
        100, 100, Quartz.CGImageGetWidth(image), Quartz.CGImageGetHeight(image)
    )

    Quartz.CGContextBeginPath(context)
    # Create an elliptical path corresponding to the image width and height.
    Quartz.CGContextAddEllipseInRect(context, imageRect)
    # Clip to the current path.
    Quartz.CGContextClip(context)

    # Draw the image into the rect, clipped by the ellipse.
    Quartz.CGContextDrawImage(context, imageRect, image)


def createRGBAImageFromQuartzDrawing(dpi, drawingCommand):
    # For generating RGBA data from drawing. Use a Letter size page as the
    # image dimensions. Typically this size would be the minimum necessary to
    # capture the drawing of interest. We want 8 bits per component and for
    # RGBA data there are 4 components.
    width = 8.5 * dpi
    height = 11 * dpi
    bitsPerComponent = 8
    numComps = 4
    # Compute the minimum number of bytes in a given scanline.
    bytesPerRow = width * bitsPerComponent / 8 * numComps

    # This bitmapInfo value specifies that we want the format where alpha is
    # premultiplied and is the last of the components. We use this to produce
    # RGBA data.
    bitmapInfo = Quartz.kCGImageAlphaPremultipliedLast

    # Round to nearest multiple of BEST_BYTE_ALIGNMENT for optimal performance.
    bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow)

    # Allocate the data for the bitmap.
    data = array.array("c", "\0" * bytesPerRow * height)

    # Create the bitmap context. Characterize the bitmap data with the
    # Generic RGB color space.
    bitmapContext = Quartz.CGBitmapContextCreate(
        data,
        width,
        height,
        bitsPerComponent,
        bytesPerRow,
        myGetGenericRGBSpace(),
        bitmapInfo,
    )

    # Clear the destination bitmap so that it is completely transparent before
    # performing any drawing. This is appropriate for exporting PNG data or
    # other data formats that capture alpha data. If the destination output
    # format doesn't support alpha then a better choice would be to paint
    # to white.
    Quartz.CGContextClearRect(bitmapContext, Quartz.CGRectMake(0, 0, width, height))

    # Scale the coordinate system so that 72 units are dpi pixels.
    Quartz.CGContextScaleCTM(bitmapContext, dpi / 72, dpi / 72)

    # Perform the requested drawing.
    myDispatchDrawing(bitmapContext, drawingCommand)

    # Create a CGImage object from the drawing performed to the bitmapContext.
    image = Quartz.CGBitmapContextCreateImage(bitmapContext)

    # Return the CGImage object this code created from the drawing.
    return image


def myExportCGDrawingAsPNG(url, drawingCommand):
    dpi = 300
    # Create an RGBA image from the Quartz drawing that corresponds to drawingCommand.
    image = createRGBAImageFromQuartzDrawing(dpi, drawingCommand)

    # Create a CGImageDestination object will write PNG data to URL.
    # We specify that this object will hold 1 image.
    imageDestination = Quartz.CGImageDestinationCreateWithURL(
        url, Quartz.kUTTypePNG, 1, None
    )

    properties = {
        Quartz.kCGImagePropertyDPIWidth: dpi,
        Quartz.kCGImagePropertyDPIHeight: dpi,
    }

    # Add the image to the destination, characterizing the image with
    # the properties dictionary.
    Quartz.CGImageDestinationAddImage(imageDestination, image, properties)

    # When all the images (only 1 in this example) are added to the destination,
    # finalize the CGImageDestination object.
    Quartz.CGImageDestinationFinalize(imageDestination)


def createCachedContent(c):
    # The cached content will be 50x50 units.
    width = height = 50

    # Create the layer to draw into.
    layer = Quartz.CGLayerCreateWithContext(c, Quartz.CGSizeMake(width, height), None)

    # Get the CG context corresponding to the layer.
    layerContext = Quartz.CGLayerGetContext(layer)

    # Cache some very simple drawing just as an example.
    Quartz.CGContextFillRect(layerContext, Quartz.CGRectMake(0, 0, width, height))

    # The layer now contains cached drawing so return it.
    return layer


def drawSimpleCGLayer(context):
    # Create a CGLayer object that represents some drawing.
    layer = createCachedContent(context)

    # Get the size of the layer created.
    s = Quartz.CGLayerGetSize(layer)

    # Position the drawing to an appropriate location.
    Quartz.CGContextTranslateCTM(context, 40, 100)

    # Paint 4 columns of layer objects.
    for i in range(4):
        # Draw the layer at the point that varies as the code loops.
        Quartz.CGContextDrawLayerAtPoint(
            context, Quartz.CGPointMake(2 * (i + 1) * s.width, 0), layer
        )


# The equivalent drawing as doSimpleCGLayer but without creating
# a CGLayer object and caching that drawing to a layer.
def drawUncachedForLayer(context):
    r = Quartz.CGRectMake(0, 0, 50, 50)

    Quartz.CGContextTranslateCTM(context, 40, 100)

    for _ in range(4):
        # Adjust the origin as the code loops. Recall that
        # transformations are cumulative.
        Quartz.CGContextTranslateCTM(context, 2 * Quartz.CGRectGetWidth(r), 0)
        Quartz.CGContextFillRect(context, r)  # Do the uncached drawing.


# Create a PDF document at 'url' from the drawing represented by drawingCommand.
def myCreatePDFDocument(url, drawingCommand):
    # mediaRect represents the media box for the PDF document the code is
    # creating. The size here is that of a US Letter size sheet.
    mediaRect = Quartz.CGRectMake(0, 0, 8.5 * 72, 11 * 72)

    # Create a CGContext object to capture the drawing as a PDF document located
    # at 'url'.
    pdfContext, mediaRect = Quartz.CGPDFContextCreateWithURL(url, mediaRect, None)

    # Start capturing drawing on a page.
    mediaRect = Quartz.CGContextBeginPage(pdfContext, mediaRect)

    # Perform drawing for the first page.
    myDispatchDrawing(pdfContext, drawingCommand)

    # Tell the PDF context that drawing for the current page is finished.
    Quartz.CGContextEndPage(pdfContext)

    # If there were more pages they would be captured as:
    #
    #    mediaRect = CGContextBeginPage(pdfContext, None)
    #
    #   DrawingForPage2(pdfContext)
    #
    #   CGContextEndPage(pdfContext)
    #
    #   mediaRect = CGContextBeginPage(pdfContext, None)
    #

MyAppController.py

import AppDrawing
import Cocoa
import objc


def getURLToExport(suffix):
    savePanel = Cocoa.NSSavePanel.savePanel()

    initialFileName = f"Quartz2DBasics.{suffix}"

    if (
        savePanel.runModalForDirectory_file_(None, initialFileName)
        == Cocoa.NSFileHandlingPanelOKButton
    ):
        return savePanel.URL()

    return None


class MyAppController(Cocoa.NSObject):
    theView = objc.IBOutlet()

    @objc.IBAction
    def print_(self, sender):
        self.theView.print_(sender)

    @objc.IBAction
    def exportAsPNG_(self, sender):
        url = getURLToExport("png")
        if url:
            AppDrawing.myExportCGDrawingAsPNG(
                url, self.theView.currentPrintableCommand()
            )

    @objc.IBAction
    def exportAsPDF_(self, sender):
        url = getURLToExport("pdf")
        if url:
            AppDrawing.myCreatePDFDocument(url, self.theView.currentPrintableCommand())

MyView.py

import AppDrawing
import Cocoa
import objc
import UIHandling

_drawingCommand = UIHandling.kCommandStrokedAndFilledRects
_pdfDocument = None


class MyView(Cocoa.NSView):
    currentMenuItem = objc.IBOutlet()

    def drawRect_(self, rect):
        context = Cocoa.NSGraphicsContext.currentContext().graphicsPort()
        AppDrawing.myDispatchDrawing(context, _drawingCommand)

    @objc.IBAction
    def setDrawCommand_(self, sender):
        global _drawingCommand

        newCommand = sender.tag()

        if newCommand != _drawingCommand:
            _drawingCommand = newCommand
            self.setNeedsDisplay_(True)

            self.currentMenuItem.setState_(Cocoa.NSOffState)
            self.currentMenuItem = sender
            self.currentMenuItem.setState_(Cocoa.NSOnState)

    def currentPrintableCommand(self):
        # The best representation for printing or exporting
        # when the current command caches using a bitmap context
        # or a layer is to not do any caching.
        if _drawingCommand == UIHandling.kCommandDoCGLayer:
            return UIHandling.kCommandDoUncachedDrawing

        return _drawingCommand

    def print_(self, sender):
        global _drawingCommand

        savedDrawingCommand = _drawingCommand
        _drawingCommand = self.currentPrintableCommand()
        Cocoa.NSPrintOperation.printOperationWithView_(self).runOperation()
        _drawingCommand = savedDrawingCommand

    def knowsPageRange_(self, page_range):
        return True, Cocoa.NSRange(1, 1)

    def rectForPage_(self, page):
        pi = Cocoa.NSPrintOperation.currentOperation().printInfo()
        paperSize = pi.paperSize()
        return Cocoa.NSMakeRect(0, 0, paperSize.width, paperSize.height)

    def validateMenuItem_(self, menuItem):
        if menuItem.tag() == _drawingCommand:
            self.currentMenuItem = menuItem
            menuItem.setState_(True)
        else:
            menuItem.setState_(False)

        return True

UIHandling.py

kCommandStrokedAndFilledRects = 1000
kCommandAlphaRects = 1001
kCommandSimpleClip = 1002
kCommandDrawImageFile = 1003
kCommandDoUncachedDrawing = 1004
kCommandDoCGLayer = 1005

main.py

import MyAppController  # noqa: F401
import MyView  # 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="Quartz2DBasics.Python",
    app=["main.py"],
    data_files=["English.lproj", "GraphicsFiles/ptlobos.tif"],
    setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
)