Annotate Any Canvas with HTML5

Screenshot of Annotations on a custom canvas
We have seen a lot of interest in our HTML5 SDK, and I recently had a customer ask about using our annotation framework on their own viewer or canvas. If you read my recent post about using LEADTOOLS HTML5 annotations in Windows Metro, you will notice that one of the arguments to the constructor for the main AnnAutomation object is an instance of an object which implements IAnnAutomationControl. In that particular demo, we used the ImageViewerAutomationControl which was attached to a LEADTOOLS ImageViewer, but since the viewer is based on the HTML5 canvas it is possible to use other LEADTOOLS features such as annotations on ANY canvas.

The IAnnAutomationControl interface is basically the glue between the drawing surface and the internal annotation framework. In order to accurately draw annotations, the framework needs to know about things like mouse and touch events, surface resolution, surface transforms, etc. The ImageViewerAutomationControl object is nothing more than a class which implements the IAnnAutomationControl interface, but it is specifically designed to be used with our image viewer control.

Back to the customer’s question, “If I have my own canvas that I am using to display image data but I still want to use the LEADTOOLS automated annotation framework, how do I attach the automation object to my canvas?” Even though you don’t use ImageViewerAutomationControl, it’s still pretty simple: (1) create your own class which implements IAnnAutomationControl and (2) pass that to the AnnAutomation constructor. I have attached the complete source code for this example at the end of this post but let’s go over a few key items in the interface first.

The most important pieces of information information for the annotation framework are the user initiated events occurring on the drawing surface. The easiest way to provide this information is to use the LEADTOOLS InteractiveService, which handles the task of interpreting mouse, touch, drag, pinch, and other types of events regardless of the type of browser or device we are running on. For example, the IAnnAutomationControl interface requires we implement PointerDown, PointerUp, and PointerMove events. To make things easy, I used the IntractiveService to notify me of all the various events that could be interpreted as PointerDown, PointerUp, and PointerMove so that I could pass them on to the underlying framework.

_service_DoubleTap: function Leadtools_Annotations_Automation_MyAutomationControl$_service_DoubleTap(sender, e) {
   if (this.__automationDoubleClick != null) {
      this.__automationDoubleClick(this, 
         Leadtools.Annotations.Core.AnnPointerEventArgs.create(
            Leadtools.Annotations.Core.AnnMouseButton.left, 
            Leadtools.LeadPointD.get_empty())
      );
   }
},

_service_DragCompleted: function Leadtools_Annotations_Automation_MyAutomationControl$_service_DragCompleted(sender, e) {
   if (this.__automationPointerUp != null) {
      this.__automationPointerUp(
         this, Leadtools.Annotations.Core.AnnPointerEventArgs.create(
            Leadtools.Annotations.Core.AnnMouseButton.left, 
            this.get_automationContainer().get_mapper().pointToContainerCoordinates(
               Leadtools.LeadPointD.create(
                  e.get_position().get_x() - e.get_change().get_x(), 
                  e.get_position().get_y() - e.get_change().get_y()
               )
            )
         )
      );
   }
},

_service_DragDelta: function Leadtools_Annotations_Automation_MyAutomationControl$_service_DragDelta(sender, e) {
   if (this.__automationPointerMove != null) {
      this.__automationPointerMove(this, 
         Leadtools.Annotations.Core.AnnPointerEventArgs.create(
            Leadtools.Annotations.Core.AnnMouseButton.left, 
            this.get_automationContainer().get_mapper().pointToContainerCoordinates(
               Leadtools.LeadPointD.create(
                  e.get_position().get_x() - e.get_change().get_x(), 
                  e.get_position().get_y() - e.get_change().get_y())
            )
         )
      );
   }
},

_service_DragStarted: function Leadtools_Annotations_Automation_MyAutomationControl$_service_DragStarted(sender, e) {
   if (!this._hasFocus) {
      this.handleGotFocus(null);
   }
   if (this.__automationPointerDown != null) {
      this.__automationPointerDown(this, 
         Leadtools.Annotations.Core.AnnPointerEventArgs.create(
            Leadtools.Annotations.Core.AnnMouseButton.left, 
            this.get_automationContainer().get_mapper().pointToContainerCoordinates(
               Leadtools.LeadPointD.create(
               e.get_position().get_x() - e.get_change().get_x(), 
               e.get_position().get_y() - e.get_change().get_y())
            )
         )
      );
   }
},

Since the framework will also handle rendering the annotations on your drawing surface, it must also be informed about any transforms which have been applied to the underlying drawing surface (scale, rotate, translate). The same holds true about the resolution; for the purpose of simplicity, we use 96 DPI and an Identity matrix for this demo.

get_automationXResolution: function Leadtools_Annotations_Automation_MyAutomationControl$get_automationXResolution() {
   return 96;
},

get_automationYResolution: function Leadtools_Annotations_Automation_MyAutomationControl$get_automationYResolution() {
   return 96;
}, 
get_automationTransform: function Leadtools_Annotations_Automation_MyAutomationControl$get_automationTransform() {
   //For this example, we will use the identity matrix. 
   //If you make any changes to the matrix of your background canvas, 
   //you should apply that same matrix here.
   return Leadtools.LeadMatrix.get_identity();
},

The next step is to create a canvas for the annotations, which is done internally. It is important that the canvas on which the annotations are rendered and the background canvas are maintained as separate canvases so that manipulating one does not affect the other. For example, if we need to clear all of the annotations, we do not want to affect any objects rendered on the background surface. Rendering in the annotation framework is handled by the AnnHtml5RenderingEngine object. So in our class, we create an instance of the renderer and utilize it within the AutomationInvalidate function to render the annotations to our canvas.

//Create a canvas for the annotation container. We will render the annotations directly to this container
this._annotationCanvas = document.createElement('canvas');
this._annotationCanvas.width = backgroundCanvas.width;
this._annotationCanvas.height = backgroundCanvas.height;
this._annotationCanvas.id = backgroundCanvas.id + '_annotationCanvas';
this._annotationCanvas.style.pixelLeft = 0;
this._annotationCanvas.style.pixelTop = 0;
this._annotationCanvas.style.marginTop = '0px';
this._annotationCanvas.style.marginRight = 'auto';
this._annotationCanvas.style.marginBottom = '0px';
this._annotationCanvas.style.marginLeft = 'auto';
this._annotationCanvas.style.position = 'absolute';
this._annotationCanvas.style.zIndex = 100;

//Add our annotation container to the parent of the background canvas
backgroundCanvas.parentNode.appendChild(this._annotationCanvas);
//Get the context for our canvas
this._context = this._annotationCanvas.getContext('2d');

//The automation object is attaching. We will create our renderer here.
automationAttach: function Leadtools_Annotations_Automation_MyAutomationControl$automationAttach(container) {
   this._container = container;
   if (this._annotationCanvas != null) {
      if (this._context != null) {
         //Create a new rendererer that will render to our annotation canvas
         this._engine = new Leadtools.Annotations.Rendering.AnnHtml5RenderingEngine(
               this._container, this._context, false);
         if (this._engine != null) {
            this._engine.render(Leadtools.LeadRectD.create(0, 0, 
                  this._annotationCanvas.width, this._annotationCanvas.height), true);
         }
      }
   }
},

//Render the annotations to our canvas
automationInvalidate: function Leadtools_Annotations_Automation_MyAutomationControl$automationInvalidate(rc) {
   if (this._engine != null) {
      this._engine.render(Leadtools.LeadRectD.create(0, 0, 
         this._annotationCanvas.width, this._annotationCanvas.height), true);
   }
},

Even though you are using a non-LEADTOOLS object for the canvas, it is still possible to permanently realize (burn in) the annotation to the drawing surface by implementing getImageData and putImageData. This is very important if you plan on using the redaction object, which is often used to hide confidential information in a document. It is also possible to restore the data which was overwritten when the redaction was realized. In order to do this, your class must handle replacing the image data on your drawing surface for realizing, and returning the image data under the redact object for restoring.

getImageData: function Leadtools_Annotations_Automation_MyAutomationControl$getImageData(rc) {
   if (rc.get_isEmpty()) {
      return null;
   }
   
   var context = this._backgroundCanvas.getContext('2d');
   return context.getImageData(parseInt(rc.get_left()), parseInt(rc.get_top()), 
      parseInt(rc.get_width()), parseInt(rc.get_height()));
},

putImageData: function Leadtools_Annotations_Automation_MyAutomationControl$putImageData(data, position) {
   if (position.get_isEmpty() || data == null) {
      return;
   }
   
   var context = this._backgroundCanvas.getContext('2d');
   var pixelData = data.data;
   context.putImageData(data, parseInt(position.get_x()), parseInt(position.get_y()));
}

You can download the complete source for this demo here. The class we discussed in the article can be found in ‘Scripts\MyAutomationControl.js’. If you have any questions about this article, feel free to contact us at support@leadtools.com.

Thanks,
Otis Goodwin

This entry was posted in HTML5 and tagged , , , , , , . Bookmark the permalink.

4 Responses to Annotate Any Canvas with HTML5

  1. Dower Chin says:

    Otis, thanks for your help with this issue, this example is incredibly useful.

    I did have another question though. How do I tell this custom annotation layer that I”ve zoomed/scaled? My canvas that it”s attached to does change, and the offset and size attributes change, but it seems the custom annotation layer doesn”t understand this, so the objects drawn on its layer scale up, but are rasterized versions which don”t look good; the mouse events are also off because the scale isn”t correct. Any tips on how I can get the custom annotation layer to adjust to zoom/scale?

    Thanks!

    • Otis says:

      Hey Dower,

      We are actually working on a sample that shows how to do this. Once we have it complete, we will send it to you.

      Thanks,
      Otis Goodwin

  2. Otis, Example is very useful … i just modified your example as
    1 just drawed some objects
    2.tried to save the drawed objects as xml which i displayed the xml in alert()javascript code line in the example..
    3.then i loaded a sample xml which is hardcoded and loaded in the canvas..

    but the problem now i am facing is – got loaded the objects of the sample xml but again when i am editing the loaded objects and trying to save its not getting saved but throwing an error(some thing like enum member not found..

    can you send me your email so that i can send u the code files in a zip

    • Walter says:

      Hello Monish,

      Please send an email to Support@leadtools.com with the following:
      1. The company you work for
      2. The LEADTOOLS serial number/numbers you own (if you are evaluating, just say so)
      3. The name/names of the developer/developers using the serial number/numbers
      4. A link to this forum post

      Please pack your sample code in a ZIP/RAR file, and also note that our email server only allows attachments up to 5 MB. If for some reason you need to send something larger, we can send you FTP instructions to upload your files to us.

      Walter Bates
      Assistant Support Manager

Leave a Reply to Otis Cancel reply

Your email address will not be published. Required fields are marked *