ImageBrowser

This sample demonstrates the ImageKit ImageBrowser in a basic Cocoa application. It uses IB to create a window an ImageBrowser and a zoom slider.

This sample should present a reasonably complete correctly formed Cocoa application which can be used as a starting point for using the ImageBrowser in a Cocoa applications.

Usual steps to use the ImageKit image browser in your application:

  1. setup your nib file

    Add a custom view and set its class to IKImageBrowserView. Connect an IBOutlet from your controller to your image browser view and connect the IKImageBrowserView‘s _datasource IBOutlet to your controller (if you want your controller to be the data source)

  2. Don’t forget to import the Quartz package:

    from Quartz import *
    
  3. Create your own data source representation (here using a NSMutableArray):

    class MyController (NSWindowController):
        _myImages = objc.ivar()
        _myImageView = objc.IBOutlet()
    
  4. implement the required methods of the informal data source protocol (IKImageBrowserDataSource):

    def numberOfItemsInImageBrowser_(self, browser):
            return len(self._images)
    
    def imageBrowser_itemAtIndex_(self, aBrowser, index):
            return self._images[index]
    
  5. The returned data source object must implement the 3 required methods from the IKImageBrowserItemProtocol informal protocol:

    def imageUID(self): pass def mageRepresentationType(self): pass def imageRepresentation(self): pass

    • the id returned by imageUID MUST be different for each item displayed in the image-view. Moreover, the image browser build it’s own internal cache according to this UID. the imageUID can be for exemple the absolute path of an image existing on the filesystem or another UID based on your own data structures.

    • imageRepresentationType return one of the following string constant depending of the client’s choice of representation:

      IKImageBrowserPathRepresentationType
      IKImageBrowserNSImageRepresentationType
      IKImageBrowserCGImageRepresentationType
      IKImageBrowserNSDataRepresentationType
      IKImageBrowserNSBitmapImageRepresentationType
      IKImageBrowserQTMovieRepresentationType
      

      (see IKImageBrowserView.h for complete list)

    • imageRepresentation return an object depending of the representation type:

      • a NSString for IKImageBrowserPathRepresentationType
      • a NSImage for IKImageBrowserNSImageRepresentationType
      • a CGImageRef for IKImageBrowserCGImageRepresentationType
      • ...

    Here is a sample code of a simple implementation of a data source item:

    class myItemObject (NSObject, IKImageBrowserItem):
      _path = objc.ivar()
    
      def mageRepresentationType(self):
          return IKImageBrowserPathRepresentationType;
    
      def mageRepresentation(self):
          return self._path
    
      def imageUID(self):
          return self._path
    
  6. Now to see your data displayed in your instance of the image browser view, you have to tell the browser to read your data using your IBOutlet connected to the browser and invoke reloadData on it:

    self._myImageView.reloadData()
    

    Call reloadData each time you want the image browser to reflect changes of your data source.

That’s all for a very basic use. Then you may need to add a scroller or a scrollview and a slider to your interface to let the user to scroll and zoom to browser his images.

Sources

ImageBrowserController.py

import objc
import Cocoa
import Quartz
import LaunchServices

import os

#       openFiles
#
#       A simple C function that opens NSOpenPanel and returns an array of file paths.
#       It uses uniform type identifiers (UTIs) for proper filtering of image files.
# -------------------------------------------------------------------------
def openFiles():
    # Get a list of extensions to filter in our NSOpenPanel.
    panel = Cocoa.NSOpenPanel.openPanel()

    # The user can choose a folder; images in the folder are added recursively.
    panel.setCanChooseDirectories_(True)
    panel.setCanChooseFiles_(True)
    panel.setAllowsMultipleSelection_(True)

    if panel.runModalForTypes_(Cocoa.NSImage.imageUnfilteredTypes()) == Cocoa.NSOKButton:
        return panel.filenames()

    return []

#==============================================================================
# This is the data source object.
class myImageObject (Cocoa.NSObject):
    _path = objc.ivar()

    # -------------------------------------------------------------------------
    #   setPath:path
    #
    #   The data source object is just a file path representation
    # -------------------------------------------------------------------------
    def setPath_(self, inPath):
        self._path = inPath

    # The required methods of the IKImageBrowserItem protocol.

    # -------------------------------------------------------------------------
    #   imageRepresentationType:
    #
    #   Set up the image browser to use a path representation.
    # -------------------------------------------------------------------------
    def imageRepresentationType(self):
        return Quartz.IKImageBrowserPathRepresentationType

    # -------------------------------------------------------------------------
    #   imageRepresentation:
    #
    #   Give the path representation to the image browser.
    # -------------------------------------------------------------------------
    def imageRepresentation(self):
        return self._path

    # -------------------------------------------------------------------------
    #   imageUID:
    #
    #   Use the absolute file path as the identifier.
    # -------------------------------------------------------------------------
    def imageUID(self):
        return self._path

class ImageBrowserController (Cocoa.NSWindowController):
    imageBrowser = objc.IBOutlet()

    images = objc.ivar()
    importedImages = objc.ivar()


    # -------------------------------------------------------------------------
    #   awakeFromNib:
    # -------------------------------------------------------------------------
    def awakeFromNib(self):
        # Create two arrays : The first is for the data source representation.
        # The second one contains temporary imported images  for thread safeness.
        self.images = Cocoa.NSMutableArray.alloc().init()
        self.importedImages = Cocoa.NSMutableArray.alloc().init()

        # Allow reordering, animations and set the dragging destination
        # delegate.
        self.imageBrowser.setAllowsReordering_(True)
        self.imageBrowser.setAnimates_(True)
        self.imageBrowser.setDraggingDestinationDelegate_(self)

    # -------------------------------------------------------------------------
    #   updateDatasource:
    #
    #   This is the entry point for reloading image browser data and
    #   triggering setNeedsDisplay.
    # -------------------------------------------------------------------------
    def updateDatasource(self):
        # Update the datasource, add recently imported items.
        self.images.extend(self.importedImages)

        # Empty the temporary array.
        del self.importedImages[:]

        # Reload the image browser, which triggers setNeedsDisplay.
        self.imageBrowser.reloadData()


    # -------------------------------------------------------------------------
    #   isImageFile:filePath
    #
    #   This utility method indicates if the file located at 'filePath' is
    #   an image file based on the UTI. It relies on the ImageIO framework for
    #   the supported type identifiers.
    #
    # -------------------------------------------------------------------------
    def isImageFile_(self, filePath):
        isImageFile = False
        uti = None

        url = Cocoa.CFURLCreateWithFileSystemPath(None, filePath, Cocoa.kCFURLPOSIXPathStyle, False)

        res, info =  LaunchServices.LSCopyItemInfoForURL(url, LaunchServices.kLSRequestExtension | LaunchServices.kLSRequestTypeCreator, None)
        if res == 0:
            # Obtain the UTI using the file information.

            # If there is a file extension, get the UTI.
            if info[3] != None:
                uti = LaunchServices.UTTypeCreatePreferredIdentifierForTag(LaunchServices.kUTTagClassFilenameExtension, info[3], LaunchServices.kUTTypeData)

            # No UTI yet
            if uti is None:
                # If there is an OSType, get the UTI.
                typeString = LaunchServices.UTCreateStringForOSType(info.filetype)
                if typeString != None:
                    uti = LaunchServices.UTTypeCreatePreferredIdentifierForTag(LaunchServices.kUTTagClassOSType, typeString, LaunchServices.kUTTypeData)

            # Verify that this is a file that the ImageIO framework supports.
            if uti is not None:
                supportedTypes = Quartz.CGImageSourceCopyTypeIdentifiers()

                for item in supportedTypes:
                    if LaunchServices.UTTypeConformsTo(uti, item):
                        isImageFile = True
                        break

        return isImageFile

    # -------------------------------------------------------------------------
    #   addAnImageWithPath:path
    # -------------------------------------------------------------------------
    def addAnImageWithPath_(self, path):
        addObject = False

        fileAttribs = Cocoa.NSFileManager.defaultManager().fileAttributesAtPath_traverseLink_(path, True)
        if fileAttribs is not None:
            # Check for packages.
            if Cocoa.NSFileTypeDirectory == fileAttribs[Cocoa.NSFileType]:
                if not Cocoa.NSWorkspace.sharedWorkspace().isFilePackageAtPath_(path):
                    addObject = True    # If it is a file, it's OK to add.

            else:
                addObject = True    # It is a file, so it's OK to add.

        if addObject and self.isImageFile_(path):
            # Add a path to the temporary images array.
            p = myImageObject.alloc().init()
            p.setPath_(path)
            self.importedImages.append(p)

    # -------------------------------------------------------------------------
    #   addImagesWithPath:path:recursive
    # -------------------------------------------------------------------------
    def addImagesWithPath_recursive_(self, path, recursive):
        dir = os.path.isdir(path)
        if dir:
            content = os.listdir(path)
            # Parse the directory content.
            for fn in content:
                if recursive:
                    self.addImagesWithPath_recursive_(
                            os.path.join(path, fn), True)
                else:
                    self.addAnImageWithPath_(os.path.join(path, fn))

        else:
            self.addAnImageWithPath_(path)

    # -------------------------------------------------------------------------
    #   addImagesWithPaths:paths
    #
    #   Performed in an independent thread, parse all paths in "paths" and
    #   add these paths in the temporary images array.
    # -------------------------------------------------------------------------
    def addImagesWithPaths_(self, paths):
        pool = Cocoa.NSAutoreleasePool.alloc().init()

        for path in paths:
            isdir = os.path.isdir(path)
            self.addImagesWithPath_recursive_(path, isdir)

        # Update the data source in the main thread.
        self.performSelectorOnMainThread_withObject_waitUntilDone_(
                b'updateDatasource', None, True)

        del pool


    #pragma mark -
    #pragma mark actions

    # -------------------------------------------------------------------------
    #   addImageButtonClicked:sender
    #
    #   The user clicked the Add button.d
    # -------------------------------------------------------------------------
    @objc.IBAction
    def addImageButtonClicked_(self, sender):
        path = openFiles()
        if path:
            # launch import in an independent thread
            Cocoa.NSThread.detachNewThreadSelector_toTarget_withObject_(
                    b'addImagesWithPaths:', self, path)

    # -------------------------------------------------------------------------
    #   addImageButtonClicked:sender
    #
    #   Action called when the zoom slider changes.
    # -------------------------------------------------------------------------
    @objc.IBAction
    def zoomSliderDidChange_(self, sender):
        # update the zoom value to scale images
        self.imageBrowser.setZoomValue_(sender.floatValue())

        # redisplay
        self.imageBrowser.setNeedsDisplay_(True)

    # Implement the image browser  data source protocol .
    # The data source representation is a simple mutable array.

    # -------------------------------------------------------------------------
    #   numberOfItemsInImageBrowser:view
    # -------------------------------------------------------------------------
    def numberOfItemsInImageBrowser_(self, view):
        # The item count to display is the datadsource item count.
        return len(self.images)

    # -------------------------------------------------------------------------
    #   imageBrowser:view:index:
    # -------------------------------------------------------------------------
    def imageBrowser_itemAtIndex_(self, view, index):
        return self.images[index]


    # Implement some optional methods of the image browser  datasource protocol to allow for removing and reodering items.

    # -------------------------------------------------------------------------
    #   removeItemsAtIndexes:
    #
    #   The user wants to delete images, so remove these entries from the data source.
    # -------------------------------------------------------------------------
    def imageBrowser_removeItemsAtIndexes_(self, view, indexes):
        self.images.removeObjectsAtIndexes_(indexes)

    # -------------------------------------------------------------------------
    #   moveItemsAtIndexes:
    #
    #   The user wants to reorder images, update the datadsource and the browser
    #   will reflect our changes.
    # -------------------------------------------------------------------------
    def imageBrowser_moveItemsAtIndexes_toIndex_(self, browser, indexes, destinationIndex):
        temporaryArray = []

        # First remove items from the data source and keep them in a
        # temporary array.
        for index in reversed(sorted(list(indexes))):
            if index < destinationIndex:
                destinationIndex -= 1

            obj = self.images[index]
            temporaryArray.append(obj)
            del images[index]

        # Then insert the removed items at the appropriate location.
        for item in temporaryArray:
            self.images.insertObject_atIndex_(item, destinationIndex)

        return True


    # -------------------------------------------------------------------------
    #   draggingEntered:sender
    # -------------------------------------------------------------------------
    def draggingEntered_(self, sender):
        return Cocoa.NSDragOperationCopy

    # -------------------------------------------------------------------------
    #   draggingUpdated:sender
    # -------------------------------------------------------------------------
    def draggingUpdated_(self, sender):
        return Cocoa.NSDragOperationCopy

    # -------------------------------------------------------------------------
    #   performDragOperation:sender
    # -------------------------------------------------------------------------
    def performDragOperation_(self, sender):
        pasteboard = sender.draggingPasteboard()

        # Look for paths on the pasteboard.
        data = None
        if Cocoa.NSFilenamesPboardType in pasteboard.types():
            data = pasteboard.dataForType_(Cocoa.NSFilenamesPboardType)

        if data is not None:
            # Retrieve  paths.
            filenames, format, errorDescription = Cocoa.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
                    data , Cocoa.kCFPropertyListImmutable, None, None)

            # Add paths to the data source.
            for fn in filenames:
                self.addAnImageWithPath_(fn)

            # Make the image browser reload the data source.
            self.updateDatasource()

        # Accept the drag operation.
        return True

main.py

from PyObjCTools import NibClassBuilder, AppHelper

import ImageBrowserController

import objc; objc.setVerbose(True)

AppHelper.runEventLoop()

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

setup(
    name="ImageBrowser",
    app=["main.py"],
    data_files=["English.lproj"],
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
        "pyobjc-framework-Quartz",
    ]
)

Resources