Annotate Any Canvas with HTML5

Posted on 2016-10-07 Greg

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

LEADTOOLS Blog

LEADTOOLS Powered by Apryse,the Market Leading PDF SDK,All Rights Reserved