How can I use a hatch fill for annotations in WebViewer?

Q:

How can I create a hatch fill for an annotation?

A:

You can use the Annotations.setCustomDrawHandler API. Below is an example implementation for ellipse annotations:

Annotations.setCustomDrawHandler(Annotations.EllipseAnnotation, (ctx, pageMatrix, rotation, options) => {
  const { annotation, originalDraw } = options;

  if (!annotation.Measure) {
    // you can use this block if you want non-measurement ellipses to not have a hatch fill
    originalDraw(ctx, pageMatrix);
    return;
  }

  ctx.beginPath();
  annotation.setStyles(ctx, pageMatrix);

  const x = annotation.X + annotation.fringe.x1;
  const y = annotation.Y + annotation.fringe.y1;
  let width = annotation.getWidth() - annotation.fringe.x1 - annotation.fringe.x2;
  let height = annotation.getHeight() - annotation.fringe.y1 - annotation.fringe.y2;

  width = Math.max(width, 1);
  height = Math.max(height, 1);

  // cap the linewidth if it is higher than the width or height
  const minDimension = Math.min(width, height);
  if (ctx.lineWidth >= minDimension) {
    ctx.lineWidth = Math.max(minDimension - 1, 1);
  }

  ctx.lineWidth *= 2;

  const whRatio = width / height;
  if (isNaN(whRatio) || whRatio === Infinity) {
    return;
  }
  ctx.save();
  // move annotation to (width / 2, 0), the center top point to start drawing the circle
  ctx.translate(x + (1 - whRatio) * width / 2, y);
  // scale the cirle to become an ellipse
  ctx.scale(whRatio, 1);

  ctx.beginPath();
  // center-x, center-y, radius, start-angle, end-angle, ccw
  ctx.arc(width * 0.5, height * 0.5, Math.max(height * 0.5, 0), 0, Math.PI * 2, false);
  ctx.closePath();
  ctx.restore();
  ctx.clip();

  ctx.stroke();

  // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation
  // ctx.strokeStyle = ctx.fillStyle;

  const hatchSize = 10;
  const hatchLineWidth = 1;
  ctx.lineWidth = hatchLineWidth;

  // horizontal lines
  for (let i = annotation.Y; i < annotation.Y + annotation.Height; i += hatchSize) {
    ctx.beginPath();
    ctx.moveTo(annotation.X, i);
    ctx.lineTo(annotation.X + annotation.Width, i);
    ctx.stroke();
  }

  // vertical lines
  for (let i = annotation.X; i < annotation.X + annotation.Width; i += hatchSize) {
    ctx.beginPath();
    ctx.moveTo(i, annotation.Y);
    ctx.lineTo(i, annotation.Y + annotation.Height);
    ctx.stroke();
  }
});

Hi @Matt_Parizeau
with this given method(setCustomDrawHandler), all the annotations gets hatch filled, and it affects to new annotation as well. and this method executes continuously.

  • can you please suggest how to hatch fill a specific annotation? that should not affect others and new ones.

Instead of using the check at the top for !annotation.Measure you could add custom data to your annotation and then check for that instead. So only annotations with that particular custom data will draw the hatch fill.

For example when you have your annotation that you want to draw with a hatch fill:
myAnnotation.setCustomData('customFillStyle', 'hatch');

And then inside the custom draw handler check
if (annotation.getCustomData('customFillStyle') !== 'hatch') {
Instead of the check for annotation.Measure

Hi @Matt_Parizeau
thanks for the reply.

With this mechanism I am able to add hatching to specific annotations.
but now the problems are :

  1. If we have doing hatch fill, we are not getting any annotation change event, so Undo/ Redo not working.

  2. after select hatch fill for selected annotation, we have to click somewhere else( focus blur for annotation), then we are getting hatch fill to that annotation.

  3. after hatch fill, if someone changes its style with Stroke and Fill, then how can we get to know that, they have changed in Fill and Stroke ? because in that case we have to remove that ‘customFillStyle’ value to blank for stop hatching to that annotation.

  1. Since you are calling setCustomData on the annotation yourself you will need to trigger the annotationChanged event yourself as well. You can do it like this:
myAnnotation.setCustomData(...);
annotManager.trigger('annotationChanged', [[myAnnotation], 'modify', {}]);
  1. After changing an annotation property directly you’ll want to call the redrawAnnotation function. This should update it for you right away.
annotManager.redrawAnnotation(myAnnotation);
  1. For this, one way I can think of doing it would be to also store the stroke and fill color at the time the hatch is set. For example:
const originalColors = {
  stroke: myAnnotation.StrokeColor.toString(),
  fill: myAnnotation.FillColor.toString()
};
myAnnotation.setCustomData('originalColors', JSON.stringify(originalColors));

Then you would also listen to the annotationChanged event and if the action is modify check if the hatch fill is set and if it is check if the stroke or fill has changed from the original stroke and fill. If one of them has changed then call deleteCustomData on the hatch fill (and redrawAnnotation).

Hi @Matt_Parizeau
thanks, it is very useful. Now I am able to hatch fill now.

I am using PDFtron’s version 7.3.3.

as you suggest in point #1, I have used to trigger event, but getting error :

TypeError: e.forEach is not a function
    at ba.onEditBoxAnnotationChanged (CoreControls.js:1110)
    at CoreControls.js:485
    at Array.forEach (<anonymous>)
    at ba.f.trigger (CoreControls.js:485)
  • so I’ve look forward in your document and referred this : https://www.pdftron.com/documentation/web/get-started/migrating-to-v6/#event-trigger-for-annotationchanged
  • but still I am getting same error.

I have few points :

  • our requirement is like this, we need hatching in cross lines.
    Annotation 2021-07-23 170248
  • I have somehow manage to draw :smile: , but lines goes out side of circle. (this is just example) providing html for this.
<canvas id="mycanvas" width="400" height="300"></canvas>
<script>
    var c=document.querySelector('#mycanvas');
    var ctx=c.getContext("2d");
    ctx.arc(400.15 * 0.5, 300.36 * 0.5, Math.max(122.36 * 1, 0), 0, Math.PI * 2, false);
                    ctx.stroke();

    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(150, 38);
    ctx.lineTo(85, 190);
    ctx.stroke(); 

    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(180, 32);
    ctx.lineTo(100, 220);
    ctx.stroke(); 

    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(210, 26);
    ctx.lineTo(115, 250);
    ctx.stroke(); 
    
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(240, 20);
    ctx.lineTo(130, 280);
    ctx.stroke(); 
    
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(270, 14);
    ctx.lineTo(145, 310);
    ctx.stroke();
    
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(300, 8);
    ctx.lineTo(160, 340);
    ctx.stroke();
    
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(330, 2);
    ctx.lineTo(175, 370);
    ctx.stroke();
    
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(360, -4);
    ctx.lineTo(190, 400);
    ctx.stroke();
    
</script>
  • as you have provided this only for Ellipse which is working fine in straight lines, but we need in other shapes as well ( Rectangle, Cloud, Polygon).

  • can you please check and update.

Thanks in advance.

It looks like I made a mistake in the trigger code for 1, I’ve just edited the post, but it should be annotManager.trigger('annotationChanged', [[myAnnotation], 'modify', {}]); note that myAnnotation should be part of an array.

For drawing the hatch fill you can see in the example code that there is a call to ctx.clip which makes sure that the lines are clipped away when drawn outside the shape.

I would expect you would be able to do a similar approach for all types of annotations, clip the shape and then do something similar to those two loops at the bottom of the example code which create the horizontal and vertical lines.

Rectangle will just use the X/Y/Width/Height. Polygon and cloud polygon will draw lines between the path points from annotation.getPath().

ok @Matt_Parizeau

we will try to do for other shapes.

  • It would very helpful to us, if you can provide fill lines : ‘for loop’ logic of the example which Sunil have created, to implement in our annotation code.

Hi @Matt_Parizeau
replying to this : How can I use a hatch fill for annotations in WebViewer? - #4 by Matt_Parizeau

  • I have done same as adding custom Data key and managed, but the problem is if there are multiple annotations in a doc (with our custom data key), and we are changing one annotation, then it changes to all other annotations.
  • As I’ve noticed that, earlier Annotations.setCustomDrawHandler() this method was executing on page load, but now it is not executed on page load. Is there any change with update or what ? because already hatched annotations are not filled with hatch on page load, even they have custom data.

Hey @Matt_Parizeau
I got stuck with these issues :

  1. Annotations.setCustomDrawHandler() is not executing on page load, so already hatched annotations are not hatching after page load / page refresh.
  2. Changing in one annotation, gets change in all other annotations. ( I’ve tried with comparing selected annotation id with setCustomDrawHandler()'s annotation id, but its also not working, with this all other types of annotation gets affect).
  3. I’ve tried with annotation.getPath(), for creating Polygon, but there will become single line after this code :
let polygon = annotation.getPath();
ctx.beginPath();
ctx.moveTo(polygon[0].X, polygon[1].Y);
                                
for(let item = 2; item < polygon.length-1; item += 2 ){
     ctx.lineTo( polygon[item].X , polygon[item+1].Y )
}
ctx.closePath();
ctx.stroke(); 
  • can you provide code for Polygon, that would be great for me.

I have spent much in this functionality, need to close this soon.

Thanks in advance.

The setCustomDrawHandler function will execute whenever you call it, so I would recommend that you call it immediately after the WebViewer constructor function resolves. For example:

WebViewer({
   ...
}, viewerElement).then(instance => {
  instance.Core.Annotations.setCustomDrawHandler(...);
});

For changing the custom data on a single annotation it should not affect any other annotations. For example if you called myAnnotation.setCustomData('customFillStyle', 'hatch'); this will only apply to this particular annotation.

From my first reply, see how I suggest that you check the custom data which looks only at the data on that annotation if (annotation.getCustomData('customFillStyle') !== 'hatch') {

For your code snippet it looks like you’re mixing the X and Y values at different indexes. You should be using the x and y value for the same path point. Here is what works for me:

Annotations.setCustomDrawHandler(Annotations.PolygonAnnotation, (ctx, pageMatrix, rotation, options) => {
  const { annotation } = options;

  let polygon = annotation.getPath();
  ctx.beginPath();
  ctx.moveTo(polygon[0].X, polygon[0].Y);

  for(let item = 1; item < polygon.length; item++ ){
       ctx.lineTo( polygon[item].X , polygon[item].Y )
  }
  ctx.closePath();
  ctx.stroke();
});

@Matt_Parizeau
yes, Matt. It applies to that particular annotation, but what if I have multiple annotations with this customFillStyle key?

The function that you pass into setCustomDrawHandler will be called once for each annotation. Each annotation has their own custom data associated with only that annotation.

So the approach is as shown in one of the first messages:

const { annotation, originalDraw } = options;

if (annotation.getCustomData('customFillStyle') !== 'hatch') {
    // don't do custom drawing if they don't have custom data set
    originalDraw(ctx, pageMatrix);
    return;
}

// do the custom drawing

Hi @Matt_Parizeau ,
I implemented as you have suggested, we are facing 3 issues.

1 ) Page unresponsive / page become slow :

  • as you mentioned, pre-filled hatch annotation(already hatch) can be load on the time of Webviewer instance, see my below code, It’s causing page unresponsiveness / page loads much (because of constantly executing of setCustomHandler()).
  • this loadHatchAnnotation() method, I’ve called after instance loaded.
    image

result :
image

2 ) updating one type of annotation, affects same type of another annotation :
here I am sharing code snippet, for hatching. If I am changing one Ellipse, then the changes affects to another Ellipse which I’ve already hatched with different styles.

const { Core } = instance;	
const annotList = Core.annotationManager.getSelectedAnnotations();	
annotList.forEach(annotation => {	
         annotation.setCustomData('customFillStyle', 'hatch');	
         this.fillHatching(instance,annotation,optionValue); //optionValue will be : Solid Fill / Hatch - Fine / Hatch - Coarse
});	

=====================================
//this method we are
fillHatching(instance, selectedAnnotation:any, value:any){	
        const { Core } = instance;	
        let originalColors:any = JSON.parse(localStorage.getItem('originalColors'));	
        if(originalColors)	
        {	
            const dataAvailable = originalColors.filter(element => element.annotationID == selectedAnnotation.id);	
            const indexOfObject = this.allVersions.indexOf(dataAvailable);	
            originalColors.splice(indexOfObject,1);	
        }	
        else{	
            originalColors = [];	
        }	
        let formValue = {	
            strokeInString:selectedAnnotation.StrokeColor.toString(),	
            fillInString:selectedAnnotation.FillColor.toString(),	
            StrokeThickness:selectedAnnotation.StrokeThickness.toString()	
        };	
        let newObj={	
            annotationID : selectedAnnotation.Id,	
            annotationValues : formValue	
        }	
                	
        originalColors.push(newObj);	
        selectedAnnotation.setCustomData('originalColors', JSON.stringify(originalColors));	
        localStorage.setItem('originalColors', JSON.stringify(originalColors))	
        	
        let annotationType:any;	
        if(selectedAnnotation.Subject == 'Ellipse'){	
            annotationType = Core.Annotations.EllipseAnnotation;	
        }	
        else if(selectedAnnotation.Subject == 'Rectangle'){	
            annotationType = Core.Annotations.RectangleAnnotation;	
        }	
        else if(selectedAnnotation.Subject == 'Polygon'){	
            annotationType = Core.Annotations.PolygonAnnotation;	
        }	
        switch (value) {	
            case "Solid Fill":	
                selectedAnnotation.deleteCustomData('customFillStyle');	
                Core.annotationManager.redrawAnnotation(selectedAnnotation);	
                Core.annotationManager.trigger('annotationChanged', [[selectedAnnotation], 'modify', {}]);	
                break;	
            case "Hatch - Fine":	
            case "Hatch - Coarse" :	
                Core.Annotations.setCustomDrawHandler(annotationType, (ctx, pageMatrix, rotation, options) => {	
                    const { annotation, originalDraw } = options;	
                    // && selectedAnnotation.Id !== annotation.Id
                    if (annotation.getCustomData('customFillStyle') !== 'hatch') {	
                        // you can use this block if you want non-measurement ellipses to not have a hatch fill	
                        originalDraw(ctx, pageMatrix);	
                        return;	
                    }	
                    const hatchSize = (value == "Hatch - Fine") ? 10 : (value == "Hatch - Coarse" ? 16 : 0);	
                    selectedAnnotation.setCustomData('hatchSize', hatchSize.toString());
                    
                    if(annotation.Subject == 'Ellipse')	
                    {	
                        ctx.beginPath();	
                        annotation.setStyles(ctx, pageMatrix);	
                        
                        const x = annotation.X + annotation.fringe.x1;	
                        const y = annotation.Y + annotation.fringe.y1;	
                        let width = annotation.getWidth() - annotation.fringe.x1 - annotation.fringe.x2;	
                        let height = annotation.getHeight() - annotation.fringe.y1 - annotation.fringe.y2;	
                        
                        width = Math.max(width, 1);	
                        height = Math.max(height, 1);	
                        
                        // cap the linewidth if it is higher than the width or height	
                        const minDimension = Math.min(width, height);	
                        if (ctx.lineWidth >= minDimension) {	
                        ctx.lineWidth = Math.max(minDimension - 1, 1);	
                        }	
                        
                        ctx.lineWidth *= 2;	
                        
                        const whRatio = width / height;	
                        if (isNaN(whRatio) || whRatio === Infinity) {	
                        return;	
                        }	
                        ctx.save();	
                        // move annotation to (width / 2, 0), the center top point to start drawing the circle	
                        ctx.translate(x + (1 - whRatio) * width / 2, y);	
                        // scale the cirle to become an ellipse	
                        ctx.scale(whRatio, 1);	
                        
                        ctx.beginPath();	
                        // center-x, center-y, radius, start-angle, end-angle, ccw	
                        ctx.arc(width * 0.5, height * 0.5, Math.max(height * 0.5, 0), 0, Math.PI * 2, false);	
                        ctx.closePath();	
                        ctx.restore();	
                        ctx.clip();	
                        
                        ctx.stroke();	
                        
                        // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
                        ctx.strokeStyle = ctx.fillStyle;	
                        
                        // const hatchLineWidth = 1;	
                        ctx.lineWidth = annotation.StrokeThickness;	
                    }	
                    else if(annotation.Subject == 'Rectangle'){	
                        ctx.beginPath();	
                        annotation.setStyles(ctx, pageMatrix);	
                        
                        let width = annotation.getWidth() - annotation.fringe.x1 - annotation.fringe.x2;	
                        let height = annotation.getHeight() - annotation.fringe.y1 - annotation.fringe.y2;	
                        
                        width = Math.max(width, 1);	
                        height = Math.max(height, 1);	
                        
                        // cap the linewidth if it is higher than the width or height	
                        const minDimension = Math.min(width, height);	
                        if (ctx.lineWidth >= minDimension) {	
                        ctx.lineWidth = Math.max(minDimension - 1, 1);	
                        }	
                        
                        ctx.lineWidth *= 2;	
                        
                        const whRatio = width / height;	
                        if (isNaN(whRatio) || whRatio === Infinity) {	
                        return;	
                        }	
                        ctx.save();	
                        ctx.beginPath();	
                        ctx.rect(annotation.X, annotation.Y, annotation.getWidth(), annotation.getHeight());	
                        ctx.closePath();	
                        ctx.restore();	
                        ctx.clip();	
                        
                        ctx.stroke();	
                        
                        // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
                        ctx.strokeStyle = ctx.fillStyle;	
                    
                        // const hatchLineWidth = 1;	
                        ctx.lineWidth = annotation.StrokeThickness;	
                    }	
                    else if(annotation.Subject == 'Polygon'){	
                                                        
                        annotation.setStyles(ctx, pageMatrix);	
                        
                        // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
                        ctx.strokeStyle = ctx.fillStyle;
                        ctx.lineWidth = annotation.StrokeThickness;	
                            
                        let polygon = annotation.getPath();
                        ctx.beginPath();
                        ctx.moveTo(polygon[0].X, polygon[0].Y);

                        for(let item = 1; item < polygon.length; item++ ){
                            ctx.lineTo( polygon[item].X , polygon[item].Y )
                        }

                        ctx.closePath();
                        ctx.stroke();
                    }
                    
                    // vertical lines	
                    for (let i = annotation.X-100; i < annotation.X + annotation.Width+200; i += hatchSize) {	
                        ctx.beginPath();	
                        ctx.moveTo(i+140, annotation.Y-120);	
                        ctx.lineTo(i-230, annotation.Y + annotation.Height+250);	
                        ctx.stroke();	
                    }  	
                });	
                	
                Core.annotationManager.redrawAnnotation(selectedAnnotation);	
                Core.annotationManager.trigger('annotationChanged', [[selectedAnnotation], 'modify', {}]);	
                  	
                break;
                	
            default:	
                break;
        }
    }

result :
Annotaion

3 ) Polygon Annotation not being drawn :

  • as I shared above code snippet, you can see, I’ve used same logic for Polygon type annotations, but still its not working.
    result :
    Polygon Annotaion
  1. I’m not sure why you would get a page unresponsive error and crash when using setCustomDrawHandler, but it’s expected that this function will be called many times and you should try to make it as fast as possible. You also want to make sure you’re not triggering any infinite loops here. For example you should not call redrawAnnotation or trigger annotationChanged from inside of the draw handler. The draw handler is just a like a React render function, you should use it purely for drawing.

  2. I see you reference selectedAnnotation inside of the draw handler when you probably just want annotation.

  3. I suspect this is because you’re setting the ctx.strokeStyle = ctx.fillStyle; The fill style looks like it’s transparent in your gif so this custom draw handler would make your stroke also transparent which is why you aren’t seeing anything.

You probably want to have that line just before you draw the hatch lines instead of before you draw your normal polygon strokes.

Hi @Matt_Parizeau

for point #1,

  • sharing my code snippet, for the method which I am calling on the time when Webviewer Instance loaded.
loadHatchAnnotaion(instance){
        const { Core } = instance;

        //Ellipse Hatch
        Core.Annotations.setCustomDrawHandler(Core.Annotations.EllipseAnnotation, (ctx, pageMatrix, rotation, options) => {	
            const { annotation, originalDraw } = options;	
            if (annotation.getCustomData('customFillStyle') !== 'hatch') {	
                // you can use this block if you want non-measurement ellipses to not have a hatch fill	
                originalDraw(ctx, pageMatrix);	
                return;	
            }	
            const hatchSize = +annotation.getCustomData('hatchSize');	
            
            ctx.beginPath();	
            annotation.setStyles(ctx, pageMatrix);	
            
            const x = annotation.X + annotation.fringe.x1;	
            const y = annotation.Y + annotation.fringe.y1;	
            let width = annotation.getWidth() - annotation.fringe.x1 - annotation.fringe.x2;	
            let height = annotation.getHeight() - annotation.fringe.y1 - annotation.fringe.y2;	
            
            width = Math.max(width, 1);	
            height = Math.max(height, 1);	
            
            // cap the linewidth if it is higher than the width or height	
            const minDimension = Math.min(width, height);	
            if (ctx.lineWidth >= minDimension) {	
            ctx.lineWidth = Math.max(minDimension - 1, 1);	
            }	
            
            ctx.lineWidth *= 2;	
            
            const whRatio = width / height;	
            if (isNaN(whRatio) || whRatio === Infinity) {	
                return;	
            }	
            ctx.save();	
            // move annotation to (width / 2, 0), the center top point to start drawing the circle	
            ctx.translate(x + (1 - whRatio) * width / 2, y);	
            // scale the cirle to become an ellipse	
            ctx.scale(whRatio, 1);	
            
            ctx.beginPath();	
            // center-x, center-y, radius, start-angle, end-angle, ccw	
            ctx.arc(width * 0.5, height * 0.5, Math.max(height * 0.5, 0), 0, Math.PI * 2, false);	
            ctx.closePath();	
            ctx.restore();	
            ctx.clip();	
            
            ctx.stroke();	
            
            // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
            ctx.strokeStyle = ctx.fillStyle;	
            
            // const hatchLineWidth = 1;	
            ctx.lineWidth = annotation.StrokeThickness;	

            // vertical lines	
            for (let i = annotation.X-100; i < annotation.X + annotation.Width+200; i += hatchSize) {	
            ctx.beginPath();	
            ctx.moveTo(i+140, annotation.Y-120);	
            ctx.lineTo(i-230, annotation.Y + annotation.Height+250);	
            ctx.stroke();	
            }  	
        });

        //Rectangle Hatch
        Core.Annotations.setCustomDrawHandler(Core.Annotations.RectangleAnnotation, (ctx, pageMatrix, rotation, options) => {	
            const { annotation, originalDraw } = options;	
            if (annotation.getCustomData('customFillStyle') !== 'hatch') {	
                // you can use this block if you want non-measurement ellipses to not have a hatch fill	
                originalDraw(ctx, pageMatrix);	
                return;	
            }	
            const hatchSize = +annotation.getCustomData('hatchSize');	
            
            ctx.beginPath();	
            annotation.setStyles(ctx, pageMatrix);	
            
            let width = annotation.getWidth() - annotation.fringe.x1 - annotation.fringe.x2;	
            let height = annotation.getHeight() - annotation.fringe.y1 - annotation.fringe.y2;	
            
            width = Math.max(width, 1);	
            height = Math.max(height, 1);	
            
            // cap the linewidth if it is higher than the width or height	
            const minDimension = Math.min(width, height);	
            if (ctx.lineWidth >= minDimension) {	
            ctx.lineWidth = Math.max(minDimension - 1, 1);	
            }	
            
            ctx.lineWidth *= 2;	
            
            const whRatio = width / height;	
            if (isNaN(whRatio) || whRatio === Infinity) {	
            return;	
            }	
            ctx.save();	
            ctx.beginPath();	
            ctx.rect(annotation.X, annotation.Y, annotation.getWidth(), annotation.getHeight());	
            ctx.closePath();	
            ctx.restore();	
            ctx.clip();	
            
            ctx.stroke();	
            
            // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
            ctx.strokeStyle = ctx.fillStyle;	
        
            // const hatchLineWidth = 1;	
            ctx.lineWidth = annotation.StrokeThickness;	

            // vertical lines	
            for (let i = annotation.X-100; i < annotation.X + annotation.Width+200; i += hatchSize) {	
                ctx.beginPath();	
                ctx.moveTo(i+140, annotation.Y-120);	
                ctx.lineTo(i-230, annotation.Y + annotation.Height+250);	
                ctx.stroke();	
            }  	
        });	

        //polygon (Solid/Cloud) Hatch
        Core.Annotations.setCustomDrawHandler(Core.Annotations.PolygonAnnotation, (ctx, pageMatrix, rotation, options) => {	
            const { annotation, originalDraw } = options;	
            if (annotation.getCustomData('customFillStyle') !== 'hatch') {	
            // you can use this block if you want non-measurement ellipses to not have a hatch fill	
            originalDraw(ctx, pageMatrix);	
                return;	
            }	
            const hatchSize = +annotation.getCustomData('hatchSize');	
                                                
            annotation.setStyles(ctx, pageMatrix);	
            
            // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation	
            // ctx.strokeStyle = ctx.fillStyle;
            // ctx.lineWidth = annotation.StrokeThickness;	
                
            let polygon = annotation.getPath();
            ctx.beginPath();
            ctx.moveTo(polygon[0].X, polygon[0].Y);

            for(let item = 1; item < polygon.length; item++ ){
                ctx.lineTo( polygon[item].X , polygon[item].Y )
            }

            ctx.closePath();
            ctx.clip();
            ctx.stroke();
            
            // vertical lines	
            for (let i = annotation.X-100; i < annotation.X + annotation.Width+200; i += hatchSize) {	
                ctx.beginPath();	
                ctx.moveTo(i+140, annotation.Y-120);	
                ctx.lineTo(i-230, annotation.Y + annotation.Height+250);	
                ctx.stroke();	
            }  	
        });	
    }
  • this method I am calling after instance load, I have tried removing redrawAnnotation and annotationChange methods from drawHandler().
  • still facing performance issue, even if I am changing position of any annotation from document, it takes 1-2 second to update its position.( might cause of drawHandler() constantly running in webpage)

for point #2,

  • can you please tell where I have to change, because as I can see, selectedAnnotaion in draw handler method is used only to store custom Hatch size.

for point #3,

  • now I am getting hatch in polygon. works fine, but if I have cloud polygon, and I am doing hatching as given, so lines of that cloud annotation becomes solid. (attached image)
    image

Suggestion :-
Can you please add hatching option in annotation in Webviewer.
As we have to perform this hatching/drawing manually. It would great if we get option as currently we have Style(stroke/fill) kind of. :slight_smile:

Thanks in advance.

  1. Unfortunately I’m not able to reproduce any slowdown. Remember that it’s expected that the draw functions will be called many times. It’s the same for normal annotations as well.

I tried running your exact code and then updated the customFillStyle and hatchSize properties on a few of them. I moved them around and the performance was the same as moving around a normal ellipse/rectangle/polygon.

If you can provide more detailed steps to reproduce, for example a sample project, then I could investigate further.

  1. I’m not sure exactly, but I wasn’t able to reproduce any issue where fill styles were linked together when changing them on one annotation. If you could provide a more detailed code sample or sample project then I can investigate further.

  2. I had forgotten about our CanvasHelper methods which may be useful to simplify your drawing code. PDFTron WebViewer Class: CanvasHelper

Here I’ve updated your polygon custom drawing handler to handle clouds as well.

Core.Annotations.setCustomDrawHandler(Core.Annotations.PolygonAnnotation, (ctx, pageMatrix, rotation, options) => {
    const { annotation, originalDraw } = options;
    const canvasHelper = new Core.Annotations.CanvasHelper(ctx);

    if (annotation.getCustomData('customFillStyle') !== 'hatch') {
        // you can use this block if you want non-measurement ellipses to not have a hatch fill
        originalDraw(ctx, pageMatrix);
        return;
    }
    const hatchSize = +annotation.getCustomData('hatchSize');

    annotation.setStyles(ctx, pageMatrix);

    // uncomment the next line if you want the hatch line color to be the "FillColor" of the annotation
    // ctx.lineWidth = annotation.StrokeThickness;
    // ctx.strokeStyle = ctx.fillStyle;

    if (annotation.Style === 'cloudy') {
      canvasHelper.drawClouds(annotation, annotation.getPath(), {});
      ctx.stroke();
      canvasHelper.drawClouds(annotation, annotation.getPath(), { clipPath: true, isStateful: false });
    } else {
      canvasHelper.drawPolygon(annotation.getPath(), {});
      ctx.stroke();
      canvasHelper.drawPolygon(annotation.getPath(), { clipPath: true, isStateful: false });
    }

    // vertical lines
    for (let i = annotation.X-100; i < annotation.X + annotation.Width+200; i += hatchSize) {
        ctx.beginPath();
        ctx.moveTo(i+140, annotation.Y-120);
        ctx.lineTo(i-230, annotation.Y + annotation.Height+250);
        ctx.stroke();
    }
});

We can consider adding hatching in a future release but for now this would be the way to do it.