CIMicroPaint

A PyObjC Example without documentation

Sources

CIMicroPaintView.py

import Cocoa
import objc
import Quartz
from objc import super  # noqa: A004
from SampleCIView import SampleCIView


class CIMicroPaintView(SampleCIView):
    imageAccumulator = objc.ivar()
    brushFilter = objc.ivar()
    compositeFilter = objc.ivar()
    color = objc.ivar()
    brushSize = objc.ivar(objc._C_FLT)

    def initWithFrame_(self, frame):
        self = super().initWithFrame_(frame)
        if self is None:
            return None

        self.brushSize = 25.0
        self.color = Cocoa.NSColor.colorWithDeviceRed_green_blue_alpha_(
            0.0, 0.0, 0.0, 1.0
        )

        self.brushFilter = Quartz.CIFilter.filterWithName_("CIRadialGradient")
        self.brushFilter.setDefaults()
        for k, v in (
            (
                "inputColor1",
                Quartz.CIColor.colorWithRed_green_blue_alpha_(0.0, 0.0, 0.0, 0.0),
            ),
            ("inputRadius0", 0.0),
        ):
            self.brushFilter.setValue_forKey_(v, k)

        self.compositeFilter = Quartz.CIFilter.filterWithName_(
            "CISourceOverCompositing"
        )
        self.compositeFilter.setDefaults()

        return self

    def viewBoundsDidChange_(self, bounds):
        if (
            self.imageAccumulator is not None
            and bounds == self.imageAccumulator.extent()
        ):
            print("Nothing changed")
            return

        # Create a new accumulator and composite the old one over the it.

        c = Quartz.CIImageAccumulator.alloc().initWithExtent_format_(
            bounds, Quartz.kCIFormatRGBA16
        )
        f = Quartz.CIFilter.filterWithName_("CIConstantColorGenerator")
        f.setDefaults()
        f.setValue_forKey_(
            Quartz.CIColor.colorWithRed_green_blue_alpha_(1.0, 1.0, 1.0, 1.0),
            "inputColor",
        )

        if self.imageAccumulator is not None:
            f = Quartz.CIFilter.filterWithName_("CISourceOverCompositing")
            f.setDefaults()
            f.setValue_forKey_(self.imageAccumulator.image(), "inputImage")
            f.setValue_forKey_(c.image(), "inputBackgroundImage")
            c.setImage_(f.valueForKey_("outputImage"))

        self.imageAccumulator = c
        self.setImage_(self.imageAccumulator.image())

    def mouseDragged_(self, event):
        loc = self.convertPoint_fromView_(event.locationInWindow(), None)

        rect = Quartz.CGRectMake(
            loc.x - self.brushSize,
            loc.y - self.brushSize,
            2.0 * self.brushSize,
            2.0 * self.brushSize,
        )
        self.brushFilter.setValue_forKey_(self.brushSize, "inputRadius1")

        cicolor = Quartz.CIColor.alloc().initWithColor_(self.color)
        self.brushFilter.setValue_forKey_(cicolor, "inputColor0")

        self.brushFilter.setValue_forKey_(
            Quartz.CIVector.vectorWithX_Y_(loc.x, loc.y), "inputCenter"
        )

        self.compositeFilter.setValue_forKey_(
            self.brushFilter.valueForKey_("outputImage"), "inputImage"
        )
        self.compositeFilter.setValue_forKey_(
            self.imageAccumulator.image(), "inputBackgroundImage"
        )

        self.imageAccumulator.setImage_dirtyRect_(
            self.compositeFilter.valueForKey_("outputImage"), rect
        )

        self.setImage_dirtyRect_(self.imageAccumulator.image(), rect)

    def mouseDown_(self, event):
        self.mouseDragged_(event)

SampleCIView.py

"""
SampleCIView - simple OpenGL based CoreImage view
"""

# XXX: FIXME
# flake8: noqa F403, F405

import CGL
import Cocoa
import objc
import Quartz
from OpenGL.GL import *
from OpenGL.GL.APPLE.transform_hint import *

# The default pixel format
_pf = None


class SampleCIView(Cocoa.NSOpenGLView):
    _context = objc.ivar()
    _image = objc.ivar()
    _lastBounds = objc.ivar(type=Cocoa.NSRect.__typestr__)

    @classmethod
    def defaultPixelFormat(self):
        global _pf

        if _pf is None:
            # Making sure the context's pixel format doesn't have a recovery
            # renderer is important - otherwise CoreImage may not be able to
            # create deeper context's that share textures with this one.

            attr = (
                Cocoa.NSOpenGLPFAAccelerated,
                Cocoa.NSOpenGLPFANoRecovery,
                Cocoa.NSOpenGLPFAColorSize,
                32,
            )
            _pf = Cocoa.NSOpenGLPixelFormat.alloc().initWithAttributes_(attr)

        return _pf

    def image(self):
        return self._image

    def setImage_dirtyRect_(self, image, r):
        if self._image is not image:
            self._image = image

            if Quartz.CGRectIsInfinite(r):
                self.setNeedsDisplay_(True)
            else:
                self.setNeedsDisplayInRect_(r)

    def setImage_(self, image):
        self.setImage_dirtyRect_(image, Quartz.CGRectInfinite)

    def prepareOpenGL(self):
        param = 1

        # Enable beam-synced updates.

        self.openGLContext().setValues_forParameter_(
            (param,), Cocoa.NSOpenGLCPSwapInterval
        )

        # Make sure that everything we don't need is disabled. Some of these
        # are enabled by default and can slow down rendering.

        glDisable(GL_ALPHA_TEST)
        glDisable(GL_DEPTH_TEST)
        glDisable(GL_SCISSOR_TEST)
        glDisable(GL_BLEND)
        glDisable(GL_DITHER)
        glDisable(GL_CULL_FACE)
        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)
        glDepthMask(GL_FALSE)
        glStencilMask(0)
        glClearColor(0.0, 0.0, 0.0, 0.0)
        glHint(GL_TRANSFORM_HINT_APPLE, GL_FASTEST)

    def viewBoundsDidChange_(self, bounds):
        # For subclasses.
        pass

    def updateMatrices(self):
        r = self.bounds()

        if r != self._lastBounds:
            self.openGLContext().update()

            # Install an orthographic projection matrix (no perspective)
            # with the origin in the bottom left and one unit equal to one
            # device pixel.

            glViewport(0, 0, r.size.width, r.size.height)

            glMatrixMode(GL_PROJECTION)
            glLoadIdentity()
            glOrtho(0, r.size.width, 0, r.size.height, -1, 1)

            glMatrixMode(GL_MODELVIEW)
            glLoadIdentity()

            self._lastBounds = r

            self.viewBoundsDidChange_(r)

    def drawRect_(self, r):
        self.openGLContext().makeCurrentContext()

        # Allocate a CoreImage rendering context using the view's OpenGL
        # context as its destination if none already exists.

        if self._context is None:
            pf = self.pixelFormat()
            if pf is None:
                pf = type(self).defaultPixelFormat()

            self._context = Quartz.CIContext.contextWithCGLContext_pixelFormat_options_(
                CGL.CGLGetCurrentContext(), pf.CGLPixelFormatObj(), None
            )

        ir = Quartz.CGRectIntegral(r)

        if Cocoa.NSGraphicsContext.currentContextDrawingToScreen():
            self.updateMatrices()

            # Clear the specified subrect of the OpenGL surface then
            # render the image into the view. Use the GL scissor test to
            # clip to * the subrect. Ask CoreImage to generate an extra
            # pixel in case * it has to interpolate (allow for hardware
            # inaccuracies)

            rr = Quartz.CGRectIntersection(
                Quartz.CGRectInset(ir, -1.0, -1.0), self._lastBounds
            )

            glScissor(ir.origin.x, ir.origin.y, ir.size.width, ir.size.height)
            glEnable(GL_SCISSOR_TEST)

            glClear(GL_COLOR_BUFFER_BIT)

            if self.respondsToSelector_("drawRect:inCIContext:"):
                self.drawRect_inCIContext_(rr, self._context)

            elif self._image is not None:
                self._context.drawImage_atPoint_fromRect_(self._image, rr.origin, rr)

            glDisable(GL_SCISSOR_TEST)

            # Flush the OpenGL command stream. If the view is double
            # buffered this should be replaced by [[self openGLContext]
            # flushBuffer].

            glFlush()

        else:
            # Printing the view contents. Render using CG, not OpenGL.

            if self.respondsToSelector_("drawRect:inCIContext:"):
                self.drawRect_inCIContext_(ir, self._context)

            elif self._image is not None:
                cgImage = self._context.createCGImage_fromRect_(self._image, ir)

                if cgImage is not None:
                    Quartz.CGContextDrawImage(
                        Cocoa.NSGraphicsContext.currentContext().graphicsPort(),
                        ir,
                        cgImage,
                    )

main.py

import CIMicroPaintView  # noqa: F401
import SampleCIView  # 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="CIMicroPaint",
    app=["main.py"],
    data_files=["English.lproj"],
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
        "pyobjc-framework-Quartz",
        "PyOpenGL",
    ],
)