TWAIN Data Sources for Mac OS X

This technote describes how to implement a TWAIN Data Source (DS) for Mac OS X





TWAIN for Mac OS X

TWAIN defines a standard software protocol and application programming interface (API) for communication between software applications and image acquisition devices (the source of the data). This technote assumes the reader is already familiar with the TWAIN specification. To get the full TWAIN specification, and to learn more about TWAIN, visit the TWAIN Working Group website.

The Mac OS X TWAIN implementation includes a TWAIN Data Source Manager (DSM). This software manages the interactions between the application and the TWAIN Data Source. This DSM is implemented as a Mach-O framework, and is located in:

/System/Library/Frameworks/TWAIN.framework

All Mach-O based Cocoa and Carbon client applications should link to this TWAIN.framework.

CFM-based applications require a CFM glue library that is located in:

/System/Library/CFMSupport/TWAIN Source Manager.Shlb

TWAIN Data Sources can be CFM or Mach-O based. They must be implemented as a bundle (see the section TWAIN DS packaging below).

Developers should place their TWAIN Data Sources in:

/Library/Image Capture/TWAIN Data Sources/

though Image Capture will also search:

/System/Library/Image Capture/TWAIN Data Sources/

Also, on Mac OS X 10.2 and later, Apple Image Capture uses a TWAIN software "bridge" TWAINBridge.app (found in: /System/Library/Image Capture/Devices/TWAINBridge.app) to access TWAIN Data Sources. This TWAINBridge software allows all Image Capture clients to access TWAIN devices transparently as if they were being handled by Image Capture native scanner drivers.

Back to Top

TWAIN DS packaging

In order to be recognized by the TWAIN DSM, TWAIN Data Sources have to be laid-out as a new-style bundle as shown below:

Figure 1: DS bundle layout.

Figure 1, DS bundle layout.

For more information about bundles see Mac OS X Bundles.

The bundle must contain a Contents directory and, inside it, an Info.plist file. It must also contain a Resources directory and, inside it, a DeviceInfo.plist file. For more information about the role of these property list files, see the section TWAIN DS Property List Files below.

Finally, the folder extension for the bundle must be .ds.

Back to Top

TWAIN DS UI-less mode

A TWAIN DS for Mac OS X has to support the otherwise optional mode of operation in which the user interface (UI) is suppressed (check the TWAIN specification for the more information). This allows applications like Apple Image Capture to use the TWAIN DS via the Image Capture TWAINBridge software.

Back to Top

TWAIN DS Property List Files

As with all other new-style bundles, a TWAIN DS carries an information property list file Info.plist in its Contents folder. The Info.plist has standard entries for keys like CFBundleIdentifier , CFBundleName and so on.

In order to use a TWAIN DS, Image Capture needs a way to associate the TWAIN DS with a connected device. Adding device related information to the Info.plist for the Data Source allows this.

For FireWire devices, the Info.plist contains the product and vendor identification and the device type which should be set to "scanner". For USB devices, the Info.plist contains the product and vendor ID values and the device type which should also be set to "scanner". The product and vendor values are taken from the IORegistry.

You can connect your device and examine these IORegistry values using the IORegistryExplorer utility (this utility is part of the Apple Mac OS X Developer Tools, see Apple Mac OS X Developer Tools ) or from the Terminal using the ioreg tool (type "ioreg -lw 0 dump" for example).

More specifically, for USB devices, the properties can be found in the node representing the IOUSBDevice and are named "idVendor" and "idProduct". For FireWire devices, the properties are named "Vendor Identification" and "Product Identification" and can be found in the IOSCSIPeripheralDeviceNub node.

Here's Terminal output from the ioreg tool for a USB device showing these properties:

Listing 1: ioreg tool output for a USB device.

 +-o IOUSBDevice@1100000 <class IOUSBDevice>
   | {
   |   "bDeviceSubClass" = 0
   |   "bcdDevice" = 256
   |   "USB Serial Number" = "AP94M1703PPE"
   |   "IOUserClientClass" = "IOUSBDeviceUserClient"
   |   "USB Vendor Name" = "ACME Peripherals"
   |   "IOGeneralInterest" = ("_IOServiceInterestNotifier is not serial...
   |   "idVendor" = 1452
   |   "Device Speed" = 1
   |   "sessionID" = 167599321426758
   |   "locationID" = 17825792
   |   "iManufacturer" = 1
   |   "iProduct" = 2
   |   "bDeviceProtocol" = 0
   |   "bDeviceClass" = 0
   |   "idProduct" = 291
   |   "Bus Power Available" = 250
   |   "bMaxPacketSize0" = 8
   |   "USB Address" = 2
   |   "PortNum" = 1
   |   "IOCFPlugInTypes" = {"9dc7b780-9ec0-11d4-a54f-000a27052861"="IOUSB...
   |   "bNumConfigurations" = 1
   |   "iSerialNumber" = 3
   | }

Here's Terminal output from the ioreg tool for a FireWire device showing these properties:

Listing 2: ioreg tool output for a FireWire device.

+-o IOSCSIPeripheralDeviceNub <class IOSCSIPeripheralDeviceNub<
   | {
   |   "Product Identification" = "ABC 2000"
   |   "Product Revision Level" = "2.03"
   |   "IOProviderClass" = "IOSCSIProtocolServices"
   |   "CFBundleIdentifier" = "com.apple.iokit.IOSCSIArchitectureModelFam...
   |   "Vendor Identification" = "ACME Peripherals"
   |   "Protocol Characteristics" = {"Physical Interconnect Location"="Ex...
   |   "IOMatchCategory" = "SCSITaskUserClientIniter"
   |   "IOUserClientClass" = "SCSITaskUserClient"
   |   "IOClass" = "IOSCSIPeripheralDeviceNub"
   |   "IOProbeScore" = 0
   |   "Peripheral Device Type" = 3
   |   "IOCFPlugInTypes" = {"7D66678E-08A2-11D5-A1B8-0030657D052A"="IOSCS...
   |   "SCSITaskUserClient GUID" = <02e41c00000023363114ca1d>
   |   "SCSITaskDeviceCategory" = "SCSITaskUserClientDevice"
   | }
   |

Here's sample output from the Property List Editor utility (also from the Mac OS X Developer Tools) for a typical scanner device property list file with the appropriate product and vendor values added:

Figure 2: A typical scanner device property list file.

Figure 2, A typical scanner device property list file.

The DeviceInfo.plist property list file (the second property list file in the bundle's Resource directory) for the device contains information that allows the Image Capture TWAINBridge to use the TWAIN DS. This file must contain an entry "ProductNames" that allows a mapping of the Data Source bundle to whatever the Data Source returns as the TWAIN identifier. Here's how it looks:

Figure 3: A DeviceInfo.plist property list file.

Figure 3, A DeviceInfo.plist property list file.

The DeviceInfo.plist may also contain a reference to a button listener plug-in code module (see TWAIN DS Button Listener below) in the "ButtonListener" entry. This plug-in module gets loaded by the TWAINBridge and does the "button listening" for the connected device. Once a device button is pressed, a user-selected application gets launched.

Back to Top

TWAIN DS Button Listener

As mentioned in the previous section TWAIN DS Property List Files, a TWAIN DS may specify a reference to a button listener plug-in code module in its DeviceInfo.plist property list file. This code module resides in the Resources folder of the DS, and is loaded by the TWAINBridge to query for button presses for the device.

Sample code for a button listener plug-in ("SampleButtonPlugin") can be found on the Apple Developer website here: SampleButtonPlugin sample code.

This plug-in code module is packaged as a bundle. The bundle file must end with .btn. Here's how the layout looks:

Figure 4: Button listener bundle layout.

Figure 4, Button listener bundle layout.

The plug-in code module must export the following symbols:

Listing 3: Module Entry-Points.

_TWAINButtonPluginStart
_TWAINButtonPluginStop

The TWAINButtonPluginStart entry point is called by the TWAINBridge to initiate button listening by the plug-in, and the TWAINButtonPluginStop entry point is called by the TWAINBridge to terminate button listening by the plug-in.

Prototypes for these entry points are as follows:

Listing 4: Module Entry-Point Prototypes.

OSErr TWAINButtonPluginStart(UInt32* locationID,
                             UInt64* guid,
                             io_string_t twainPath,
                             ButtonPressedCallback callback);

OSErr TWAINButtonPluginStop(UInt32* locationID,
                            UInt64* guid,
                            io_string_t twainPath);

For USB devices, the locationID parameter passed to the TWAINButtonPluginStart entry point will contain a valid value uniquely identifying the USB device. The guid parameter (corresponding to the GUID, the globally unique identifier) in this case is not used.

For FireWire devices, the guid parameter (corresponding to the GUID) passed to the TWAINButtonPluginStart entry point will contain a valid value uniquely identifying the FireWire device. The locationID parameter in this case is not used.

The twainPath specifies the file path to the plug-in module.

When the TWAINButtonPluginStart entry point is called, the plug-in will typically begin polling for key-presses (via a timer) to determine whether or not a button is pressed. When the plug-in determines a button has been pressed, it must call the button-press callback procedure (which was passed via the callback parameter to the TWAINButtonPluginStart entry point). Calling the button-press callback in this manner will cause the application specified in the Image Capture application Scanner Preferences to be launched.

Here's the prototype for the button-press callback procedure:

Listing 5: Button-press Callback Prototype.

typedef CALLBACK_API_C( void, ButtonPressedCallback )(OSType message,
                                                      UInt32*  locationID,
                                                      UInt64*  guid);

Back to Top

TWAIN DS & Carbon Events

Previous TWAIN implementations for Mac OS 9 were implemented around the WaitNextEvent loop. This meant the client application was actively polling the TWAIN DS by passing NULL events and all other events to the DS. The DS would then decide if the event should be handled or returned to the client application.

This type of event handling is not suitable for the Mac OS X environment, where Carbon or Cocoa applications do not even have a user exposed event loop. For old-style WaitNextEvent applications, the TWAIN DSM will currently block the MSG_PROCESSEVENT and not even pass it on to the TWAIN DS.

Passing events from the Client Application to the DS - The Old Way

Pages 3-28 through 3-31 of the TWAIN Specification Version 1.9 describe how to modify the application's event loop to pass events from the client application to the DS so the DS can properly respond to them, and how to properly check for messages sent from the DS. Page 3-30 illustrates typical modifications necessary for a Macintosh application to support TWAIN-connected sources by polling for events with an old-style WaitNextEvent loop.

For sending events to the DS, an application normally uses DG_CONTROL / DAT_EVENT / MSG_PROCESSEVENT and the pEvent field of the TW_EVENT data structure used for these events points to the Macintosh EventRecord . The DS then receives the event from the Source Manager and determines who the event belongs to.

While this technique for sending events to the DS is still supported by the Mac OS X TWAIN DSM, it is no longer the preferred technique.

Back to Top

Passing events from the Client Application to the DS - The New Way

Instead of getting called by the application with a MSG_PROCESSEVENT message as described above for each event on Mac OS X, the TWAIN DS should now setup CarbonEvent handlers to receive events. Note these event handlers are also used for the user-interface (UI) interactions.

For more detailed information about Carbon Events, see the Carbon Event Manager Reference.

This means instead of getting called with a MSG_PROCESSEVENT that has an old style EventRecord in its TW_EVENT structure pData field, the TWAIN DS must set up Carbon Event handlers using the Carbon APIs as follows:

Listing 6: Setting-Up Carbon Event Handlers.

InstallStandardEventHandler(GetWindowEventTarget(GetDialogWindow(gDialog)));

InstallWindowEventHandler(GetDialogWindow(gDialog),
          NewEventHandlerUPP((EventHandlerProcPtr) windowEventHandler),
          GetEventTypeCount(windowEvents), windowEvents,
          0,
          NULL);

The event handler will also get called for all UI interactions.

Here's a more detailed code example, taken from our Data Source sample code (which you can get from the Apple Developer website here: Data Source sample code) showing how to set up Carbon Event handlers:

Listing 7: Carbon Event Handlers Setup.

// ------------------------------------------------------------------
//    DisplayUserInterface
// ------------------------------------------------------------------
//

void DisplayUserInterface()
{
    SInt16       savedResRefNum;
    SInt16       resRefNum = 0;
    OSStatus     status = noErr;
    CFBundleRef  selfBundleRef;
    ControlRef   pictureControlRef;

EventTypeSpec windowEvents[] = {{kEventClassWindow,kEventWindowDrawContent},
    { kEventClassWindow, kEventWindowUpdate },
    { kEventClassWindow, kEventWindowClose },
    { kEventClassWindow, kEventWindowClickDragRgn },
    { kEventClassMouse, kEventMouseDown },
    { kEventClassMouse, kEventMouseMoved } };

    selfBundleRef = 
         CFBundleGetBundleWithIdentifier(CFSTR("com.apple.sampleds"));

    if (selfBundleRef)
        resRefNum = CFBundleOpenBundleResourceMap ( selfBundleRef );

    savedResRefNum = CurResFile();
    UseResFile ( resRefNum );

    gDialog = GetNewDialog(128, nil, (WindowRef)-1);

    UseResFile ( savedResRefNum );

    ChangeWindowAttributes(GetDialogWindow(gDialog),
                           kWindowStandardHandlerAttribute |
                           kWindowCloseBoxAttribute,
                           kWindowCollapseBoxAttribute);

    GetDialogItemAsControl(gDialog, kScanButton,   &gScanButton);
    GetDialogItemAsControl(gDialog, kCancelButton, &gCancelButton);

InstallStandardEventHandler(GetWindowEventTarget(GetDialogWindow(gDialog)));
    InstallWindowEventHandler(GetDialogWindow(gDialog),
          NewEventHandlerUPP((EventHandlerProcPtr) windowEventHandler),
          GetEventTypeCount(windowEvents), windowEvents,
          0,NULL);

    DS_LogText("InstallWindowEventHandler done\n");

    ShowWindow(GetDialogWindow(gDialog));

    status = GetDialogItemAsControl ( gDialog, 3, &pictureControlRef );
    require_noerr ( status, BAIL );

    gPicture = GetPicture(130);
    status = SetControlData ( pictureControlRef,
                              kControlPicturePart,
                              kControlPictureHandleTag,
                              sizeof ( PicHandle ),
                              &gPicture );
    require_noerr ( status, BAIL );

    DrawOneControl ( pictureControlRef );

BAIL:

    return;
}



// -------------------------------------------------------------------
//    windowEventHandler
// -------------------------------------------------------------------
//

OSStatus  windowEventHandler(EventHandlerCallRef eventHandlerCallRef,
                             EventRef            eventRef,
                             void*               userData)
{
#pragma unused (eventHandlerCallRef, userData)

    OSStatus    result = eventNotHandledErr;
    UInt32      eventClass;
    UInt32      eventKind;
    EventRecord eventRecord;

    eventClass = GetEventClass(eventRef);
    eventKind  = GetEventKind(eventRef);

    switch(eventClass)
    {
             // event class window
        case kEventClassWindow: 

            ConvertEventRefToEventRecord(eventRef,&eventRecord);

            switch(eventKind)
            {
                case kEventWindowUpdate:

                    doDrawContent(GetDialogWindow(gDialog));

                    break;

              /* etc. */
            }
    }
}

Back to Top

Communicating from the DS to the Client Application - The Old Way

The TWAIN Specification Version 1.9 pages 5-111 through 5-114 describe handling of events by the DS, and techniques for transmitting notices from the DS to the application. Normally, the DS uses a TW_EVENT structure to send its notice to the application. More specifically, the DS places one of MSG_XFERREADY , MSG_CLOSEDSREQ , MSG_CLOSEDSOK or MSG_DEVICEEVENT in the TW_EVENT.TWMessage field as described on page 5-113 of this specification.

Back to Top

Communicating from the DS to the Client Application - The New Way

To pass information back to the client application (such as with MSG_XFERREADY or MSG_CLOSEDSREQ ) the TWAIN DS now has to use a callback mechanism through the DSM_Entry as follows:

Listing 8: Client Application Callback Mechanism.

 TW_CALLBACK   callback = {};

     callback.Message = MSG_CLOSEDSREQ;
     result = DSM_Entry((pTW_IDENTITY)&Identity,
                         (pTW_IDENTITY)NULL,
                         (TW_UINT32)DG_CONTROL,
                         (TW_UINT16)DAT_CALLBACK,
                         (TW_UINT16)MSG_INVOKE_CALLBACK,
                         (TW_MEMREF) &callback);

The DSM actually will take care of the case where the client application is the older WaitNextEvent based type.

Once the DSM gets the callback.Message , it will pass the message back the next time it gets called with a MSG_PROCESSEVENT and an EventRecord . This allows applications like the WaitNextEvent based Photoshop 7 to use the new TWAIN.framework and the new style TWAIN Data Sources.

Back to Top

References

Document Revision History

Date Notes
2003-06-19 Describes how to implement a TWAIN Data Source (DS) for Mac OS X

Posted: 2003-06-19


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.