EGO (Edit Graphic Object) specification

EGO (Edit Graphic Object) is a Mac protocol that employs Apple Events to implement editing of graphical objects (e.g. equations) embedded in an application's documents. It is similar to the object embedding features of Microsoft’s OLE but EGO is much simpler to implement and more broadly supported among Mac applications. This document gives a Mac programmer the information needed to add EGO support to their application. It also contains details regarding the EGO support in MathType.

An EGO object is typically a graphic, e.g. a picture, equation or chart and is created by an application with EGO server support. When the user double-clicks on an embedded object, a window opens containing the object, ready for editing. When the user has finished editing the object and closes its window, the new version of the object replaces the original in the document. This is a much easier process than using Cut and Paste, and gives the user the feeling that the two applications are integrated.

The EGO protocol describes the process of transferring the object from the document's application (the client) to the object’s application (the server), and also returning the edited object back to the client. For EGO to work, both the client and the server must adhere to the conventions outlined in this document.

EGO relies on Apple Events for inter-application communication. The EGO protocol consists of three Apple Events, two of which are defined in Apple’s Apple Event Registry. These two events (EditGraphic and SetData) are extended beyond their original definitions by the use of additional optional parameters. The third event, ComeHome, is a completely new event.

Since OS X 10.4, PICT has been a deprecated image format. Version 2 of the EGO specification introduced support for PDF, and version 3 introduces support for MathML and LaTeX. This takes the form of new parameters for sending and requesting data in a specific format, and for requesting a graphic in a new format without opening the old object in a window for editing to facilitate batch conversions of EGO objects in the deprecated PICT format.

Since PICT is deprecated, EGO clients are encouraged to prefer the PDF format whenever possible, subject to backward compatibility considerations, and to keep in mind that future versions of EGO servers will likely drop support for PICT entirely at some point.

EGO, like OLE, is really an automated alternative to cut and paste between separate applications. With EGO, once an object has been embedded (using a Paste command) all further editing can be accomplished without using the Cut and Paste commands. The smoothness of the operations creates the impression of using an integrated application, and allows users to focus on what they're trying to accomplish, rather than the mechanics of the process.

EGO is different from Publish and Subscribe in that the EGO object is embedded directly into a document, rather than residing in a separate file. For documents that contain many embedded objects (e.g. a technical paper may contain hundreds of equations), this is a huge advantage. In addition, editing an EGO object requires fewer steps for the user.

OLE's embedded objects feature is Microsoft’s solution to the same problem that EGO addresses. It is a standard in the Windows environment, but so far has only been implemented on the Mac by Microsoft’s Word, Excel and PowerPoint products, as well as MathType. OLE also uses Apple Events, but it is a more complicated protocol, and is harder for developers to implement than EGO. However, it does support a couple of features that EGO does not, namely the ability to use an Insert Object command to select an object-type from a list and open its editing window. By contrast, all EGO objects must be initially inserted into a document using a Paste command (see the section entitled "MathType's extension to EGO" below, for an extension that enables an Insert Object command to be supported). EGO and OLE support are not mutually exclusive (MathType is an OLE and an EGO server), but most client applications will choose to support only one.

An EGO object is a PDF or PICT graphic that contains the EGO picture data (stored a comment in PICT files and as a private dictionary in PDF files). This data contains information that allows EGO clients to know which application created the object, as well as its size and other positioning information. When the user pastes a graphic into an EGO client, the client searches for the EGO data. If it finds EGO data, the client has to remember that this graphic is an EGO object. When the user double-clicks on the graphic, the client will create an EditGraphic Apple Event that contains a copy of the graphic, and send it to the graphic’s creating application (as indicated in the graphic’s EGO data). If the application isn’t running, the client will launch it and then send it the EditGraphic event.

Version 3 of the EGO specification introduces the capability for a client to send MathML or LaTeX equation data not embedded in a graphic object directly to the EGO server to obtain the corresponding graphic in return.

When the server receives the EditGraphic event, it opens a new window containing the object to be edited, and sets the title to something like “Equation from DocMaster” (the client’s name is one of the event parameters). After editing, the user closes the window and the server sends a SetData event back to the client, containing the new version of the graphic. For situations where a client wishes to convert between equation formats, the client can suppress the opening of the MathType UI and immediately obtain a graphic in response, via the use of the optional Rfsh keyword in the EditGraphic Apple event.

The client then inserts the graphic in the correct location within the document and becomes the front-most window, ready for further editing.

If the client requires the current version of the object while its EGO window is still open (e.g. the user closes the client document),  the client sends a ComeHome event to the server, instructing it to return the latest version of the graphic. All returned EGO objects are then inserted into the document before it is closed.

The EGO picture data

All EGO objects (currently PICTs and PDFs are supported) must contain the EGO picture data.

In a PICT image, the EGO data is a comment which must occur within the first 1024 bytes of the PICT data. This restriction permits client applications to perform a fast binary search for the comment’s binary signature, rather than having to draw or parse the picture to find the comment. The comment has a kind=498, and its data handle contains the following data:

     creator          4 bytes
     descent          2 bytes
     tactile rect     8 bytes
     "GrâphØbj"       8 bytes

The creator field identifies the application that created the object.

The descent field is the number of pixels that the picture should be lowered in order to align it with the surrounding text’s baseline. For objects that are inserted in-line with text (such as equations), it is important that client applications support this feature. If an application doesn’t support this behavior users must resort to manually subscripting objects, a tedious process.

The tactile rectangle is the bounding box of the object. Normally this is the same as the PICT’s bounding box. Some objects actually draw outside their rectangle, for example when italics are used. In these cases, the bounding box is set to entirely contain the object, and the tactile rectangle is set to indicate the space the client should allow for the graphic.

The last field is the comment’s binary signature, and contains the 8-byte string “GrâphØbj”. The purpose of this field is described above.

In a PDF image, the EGO picture data takes the form of a private EGO dictionary. This data structure is located the PDF document's the PieceInfo dictionary (for storage of third-party information), and is exactly analogous to the identifying information embedded in comments in PICT EGO objects.

The individual data item keys and types are:

Data Item KeyData Type
creatorCGPDFStringRef
descentCGPDFReal (value is in points)
tactileRectCGPDFArrayRef<CGPDFReal> [llx, lly, urx, ury] (values are in points)

The PDF data hierarchy and the location of the EGO picture data within it is summarized here:

pdfdoc
   pdfpage
     PieceInfo (dictionary)
        EGO (dictionary)
         LastModified (required date string)
         Private (required data - dictionary)
             creator
             descent
             tactileRect

Editing an EGO object

When the user double-clicks on a graphic, or selects it and then selects the “Edit this graphic” command (perhaps a menu item), the client should inspect the graphic for the EGO picture comment (alternatively a client could detect and remember which graphics are EGO objects when it opens the document or when the user pastes a graphic into the document).

Once identified as an EGO object, its creator can be extracted from the picture comment. This is used as the address for the Edit Graphic Apple event that the client sends. This Apple Event contains the following information:

Event Class     'misc'

Event ID     'edit'

Parameters
keyword: 'data',     type:'PICT', 'PDF', 'utf8'     (required)

The input equation data. For 'PICT' and 'PDF' the equation data must be embedded in the graphic as described above. When the data is 'utf8', the data is expected to be equation markup, typically a flavor of MathML or LaTeX. In this case, the type of markup is specified using the 'Flvr' keyword.

keyword: 'Flvr',     type: 'utf8 '     (optional)

A Universal Type Identifier specifying the markup language used in the 'data' parameter if the type of the data is 'utf8'. The currently recognized markup languages are public.mathml.presentation, public.mathml, and com.dessci.texvc (a subset of LaTeX math markup including a superset of the Wikipedia Texvc language). The default value is public.mathml.

keyword: keyDirectObject,     type: 'obj'     (required)

Client data indicating location of object in client document. This data is ignored (but returned) by the server. The client can use any data it desires, but if it supports Apple's Object Model it will probably want to use an object specifier.

keyword: 'Uniq',     type: 'long'     (required)

Unique ID used by client to identify this transaction from all others, including any other EGO clients' (e.g. result from TickCount()).

keyword: 'Wher',     type: 'qdrt'     (optional)

Rectangle in global coordinates indicating screen position of graphic in client's document. Can be used by server to position editing window so that graphic appears in same location on screen.

keyword: 'From',     type: 'TEXT'     (optional)

Client's name, can be used by server in object's window title to indicate source (e.g. <object> from <client name>).

keyword: 'Kick',     type: 'psn'     (optional)

ProcessSerialNumber of client. If present, indicates that client wishes to be made the front process when server returns the object. This behavior is recommended.

keyword: 'iFmt',     type: 'type'     (optional)

The iFmt parameter data must have the value 'PDF ' or 'PICT'. If this parameter is present, it indicates the desired format of the data parameter when the server returns the object.  If this parameter is not present, the server should default to 'PICT' for backward compatibility. If the request cannot be honored, it is recommended that the server not open the object for editing, and instead immediately return the unchanged object (e.g. in future a PDF-only server might not be able to honor a request for PICT).

keyword: 'Rfsh',     type: 'true'     (optional)

If present, indicates that the server should immediately return the object in the format requested by the 'iFmt' parameter, without opening the object in a window for editing.  This parameter is useful for implementing batch conversion operations on EGO objects in a document, e.g. to convert deprecated PICT objects to PDF.

If the optional parameters are included, an 'optk' attribute list must be included.

The event is sent using the kAENoReply reply mode. This means that the client application will not be explicitly "waiting" for a response (the reply is a actually a separate Apple event sent by the server when the user closes the editing window). In fact, the user can switch back to the client application and make further changes to the document before returning to the server. It is the client's responsibility to specify the location of the object in such a way that if editing changes are made to the document, the graphic will still be returned to the correct location.

If the user double-clicks on a graphic that is already being edited, the client should not start another editing session, but rather bring the server to the foreground (via the SetFrontProcess() call). The client could perhaps "gray-out" the graphic, and even allow option-double-clicking to force a new editing session in case the server application has crashed. Note that if the client issues an ‘edit’ event repeating the ‘Uniq’ id in an open equation, the EGO server does not bring the open equation window to the front, but creates a second window. Although this might seem counter-intuitive, we retain this behavior for historical reasons.

If the server application is not running, AESend() returns connectionInvalid (defined in EPPC.h). The client should then launch the server using the procedure described in the next section, and then re-send the Edit Graphic Apple event (the client may choose to ask the user if they want to launch the server).

The Apple Event contains enough information for the server to open a new window containing the graphic, and bring itself to the foreground.

Launching an EGO Session

It is important to understand the correct way to launch an EGO application, i.e., how to implement the code which is invoked when a user double-clicks on an EGO equation or invokes an Insert Equation command (assuming you implement one as we encourage. see MathType's Extension to EGO below) and the application is not currently running.

You need to use the FSSpec-based LaunchApplication API, as follows:

  1. Obtain an FSRef for Equation Editor/MathType using Launch Services (LSGetApplicationForInfo).
  2. Convert the FSRef to an FSSpec using FSGetCatalogInfo.
  3. Call LaunchApplication with a dummy event in the launchAppParameters field.

To understand why this method is required you need to recall what happens when an EGO application is activated by the OS.Under OS X, when the client application launches an EGO application like Equation Editor or MathType by creator, it generally involves Launch Services, which manages the affiliation of creator codes. However when launched in this manner (e.g.. via routines such as LSOpenFromRefSpec) the EGO application will in general create an 'untitled' (i.e. non-EGO) window (even when setting the numDocs in the LSLaunchFSRefSpec to zero) in keeping with Apple's Human Interface Guidelines. The EGO application cannot tell at that point if it is being started for the purpose of initiating an EGO session. That is why the start-up mechanism described here is required; it resolves the ambiguity and insures a smooth start to your EGO session.

We should note that, when launching Equation Editor or MathType by this method, the splash screen flashes by as the application starts up but no 'extra' untitled window results.

To make this process clear, the Appendix that follows contains some sample code fully illustrating the correct launch method. The SDK contains a sample Xcode, EGO Application Launcher project (see Launcher.zip) -- a project that produces a simple application with the sole function to launch an EGO application using the code given below.

Returning the object

When the user closes the editing window, the server assembles a SetData Apple event that contains the following data:

Event Class     'core'

Event ID     'setd'

Parameters
keyword: 'data',     type:'PICT' or 'PDF'     (required)

The edited PICT or PDF data for the object. If omitted, client should assume that the object was unchanged (an extension to the Core Apple Events specification for SetData).

keyword: keyDirectObject,     type: 'obj'     (required)

The object location data sent in the original EditGraphic event.

keyword:rd: 'Uniq',     type: 'long'     (required)

The unique ID sent in the original EditGraphic event.

keyword: 'Crea',     type: 'ostp'     (optional)

The server's creator type can be included.

                              type: 'shor'     (optional)

The object's descent can be included.

keyword: 'Tact',     type: 'qdrt'     (optional)

The object's tactile rectangle can be included.

As before, if the optional parameters are included, an 'optk' attribute list must be present, indicating the supplied optional parameters. The three optional parameters are all included in the PICT or PDF EGO picture comment, so the client can always find this data if the parameters are not in the Apple event.

Note that because version 2 and higher of EGO supports PICT or PDF, it is important that the client inspect the type of the 'data' parameter to handle the data appropriately.

This event is sent using the kAEWaitReply (or kAEQueueReply) reply mode, although the reply is only used to return an error.

When the client receives this event it should replace the original graphic with the new one and remove this transaction from its list of outstanding EGO editing sessions (perhaps indexed by the unique IDs). One enhancement is to retain the original graphic if the user holds down the option key while closing the server's editing window. This is the client's option to implement.

Resolving outstanding EGO editing sessions

The situation could arise where a user closes a client document (or even quits a client application) when one or more EGO editing sessions are still active. In this case the client sends a ComeHome message for each outstanding edit, containing the following information:

Event Class     'misc'

Event ID     'CmHm'

Parameters
keyword: 'Uniq',     type: 'long'     (required)

The unique ID sent in the original Edit Graphic event.

This event is sent using the kAEWaitReply (or kAEQueueReply) reply mode. The server will close the appropriate editing window (identified by the unique ID) and return in the event's reply parameter the same data as described for the SetData event above. The client will not bring itself to the front in this situation.

The client will have to incorporate handling of EGO edits into its document-saving approach. For example, when the user closes a document with outstanding EGO edits, the client should ask the user if they want to "Save Changes". If the user responds Save or Don't Save the ComeHome messages should be sent, but ignored for the latter case. If the user responded Cancel then no ComeHome messages should be sent.

Other situations also can cause the ComeHome message to be sent, basically whenever the client needs the current version of the object. These situations include if the object is cut, copied or cleared from the client document, or if the user performs a Save or Save As on the client document. The philosophy is that the object is still part of the document even when it is being edited in another window.

A fairly simple extension to the EGO specification allows a client application to support, for example, an Insert Equation command. This command is virtually essential when working with objects, as it allows users to edit and create objects without manually switching applications.

If MathType receives an EGO message without a 'data' parameter it will open an empty equation window, giving the user the idea that they’re creating a new equation. To support this functionality, the client application must know the application signature of the objects it wants to be able to insert (remember, when editing a graphic this signature is retrieved from the graphic itself).

Although this extension is not part of the official EGO specification, we suspect that most other EGO servers act in a similar way.

This section consists of two source file listings: a prototype and implementation of the function 'LaunchByCreator' which illustrates the launching requirements discussed in Launching an EGO Session above.

File 1: LaunchByCreator.h

// LaunchByCreator.h --- © 2018, WIRIS Europe (Maths for more S.L), All rights reserved.
// Purpose: Routine to launch app (typically MathType or Equation Editor) by creator code
 
// Note all types are standard Apple types.
 
// Launch app (typically MathType) by creator code.
// Uses Launch Services to find application, Process Manager to launch.
// Suppresses sending Open Application %%('%%oapp') Apple Event to app on launch by default.
// Skips launch if application is already running but bring to front.
 
OSStatus // toolbox error code if error, noErr otherwise
LaunchByCreator(
    OSType inCreator,               // Creator code for app to launch
    bool suppressOAPP = true,       // Suppress Open Application 'oapp' event
    AppParametersPtr firstEvent = 0 // First event delivered to app, 0: use null event
                                    // (ignored if suppressOAPP is false)
);> );

File 2: LaunchByCreator.cpp

// LaunchByCreator.cpp --- © 2018, WIRIS Europe (Maths for more S.L), All rights reserved.
// Purpose: Routine to launch app (typically MathType) by creator code
 
// Note all types are standard Apple types.
 
#include <Carbon.h>
#include "LaunchByCreator.h"
 
// Find running app by signature and return PSN
static bool
FindRunningAppBySignature(
    OSType signature,
    ProcessSerialNumber &psn
){
    ProcessInfoRec info;  // Process Information Block
    info.processInfoLength = sizeof(info);
    info.processName       = 0;
    info.processAppSpec    = 0;
   
    psn.highLongOfPSN = 0;
    psn.lowLongOfPSN  = kNoProcess;
 
    // search running processes for an app with our signature...
    while( (::GetNextProcess(&psn) == noErr) && (::GetProcessInformation(&psn, &info) == noErr) ) {
        if( (info.processType == 'APPL') && (info.processSignature == signature) ) {
            psn.highLongOfPSN = info.processNumber.highLongOfPSN;
            psn.lowLongOfPSN = info.processNumber.lowLongOfPSN;
            return true;
        }
    }
    return false;
}
 
// Launch app (typically MathType) by creator code.
// Uses Launch Services to find application, Process Manager to launch.
// Suppresses sending Open Application ('oapp') Apple Event to app on launch by default.
// Skips launch if application is already running but bring to front.
 
OSStatus // toolbox error code if error, noErr otherwise
LaunchByCreator(
    OSType inCreator,                         // creator code for app to launch
    bool suppressOAPP           /* = true */, // suppress Open Application 'oapp' event
    AppParametersPtr firstEvent /* = 0 */     // first event delivered to app, 0: use null event
                                              // (ignored if suppressOAPP is false)
){
    ProcessSerialNumber psn;
    if( FindRunningAppBySignature(inCreator, psn) ) {
        return ::SetFrontProcess(&psn);
    }
 
    OSType inType = 'APPL';
    CFStringRef inExtension = CFSTR(".app"); // can be NULL
    LSRolesMask inRoleMask = kLSRolesAll;
    FSRef outAppRef; // can be NULL
    CFURLRef * outAppURL = 0; // can be NULL
 
    // Obtain file system reference for application with given creator
    OSStatus status = ::LSGetApplicationForInfo( inType, inCreator, inExtension,
                                                    inRoleMask, &outAppRef, outAppURL);
    if( status != noErr )
        return status;
 
    // Convert FSRef returned by Launch Services into FSSpec required by LaunchApplication
    FSSpec spec;
    OSErr err = ::FSGetCatalogInfo(&outAppRef, kFSCatInfoNone, NULL, NULL, &spec, NULL);
 
    if( err != noErr )
        return err;
 
    // Null event to suppress Open Application Apple Event if not supplied as argument
    AppParameters appParams;
    appParams.theMsgEvent.what = nullEvent;
    appParams.theMsgEvent.message = 0;
    appParams.theMsgEvent.when = 0;
    appParams.theMsgEvent.where.v = 0;
    appParams.theMsgEvent.where.h = 0;
    appParams.theMsgEvent.modifiers = 0;
    appParams.eventRefCon = 0;
    appParams.messageLength = 0;
 
    // Choose first event pointer
    AppParametersPtr appParamsPtr = 0;
    if( suppressOAPP ) {
        if( firstEvent != 0 )
            appParamsPtr = firstEvent; // use supplied event, if any
        else
            appParamsPtr = &appParams; // otherwise use null event
    }
 
    // Configure parameter block
    LaunchParamBlockRec launchParams;
    launchParams.launchBlockID = extendedBlock;
    launchParams.launchEPBLength = extendedBlockLen;
    launchParams.launchFileFlags = launchNoFileFlags;
    launchParams.launchAppSpec = &spec;
    launchParams.launchControlFlags = 0;
    launchParams.launchAppParameters = appParamsPtr;
 
    err = ::LaunchApplication(&launchParams);
 
    return err;
}
  |