How to create tools for insert and replace text

Q:

Is it possible to have tools like Adobe’s insert text and replace text tools that create caret annotations?

A:

Yes, the following code will work with WebViewer 7.0. For older versions the allQuads index values will be 0-indexed, so add one to the page number.

const { Annotations, Tools, annotManager, docViewer } = instance;

// shared helper function that creates caret annotation at the end of the selection
const createCaretAnnotation = (quads, data) => {
  const lastQuad = quads[quads.length - 1];

  const maxY = Math.max(lastQuad.y1, lastQuad.y2, lastQuad.y3, lastQuad.y4);
  const minY = Math.min(lastQuad.y1, lastQuad.y2, lastQuad.y3, lastQuad.y4);

  const maxX = Math.max(lastQuad.x1, lastQuad.x2, lastQuad.x3, lastQuad.x4);
  const minX = Math.min(lastQuad.x1, lastQuad.x2, lastQuad.x3, lastQuad.x4);

  let textRotation;
  if (lastQuad.x1 === minX && lastQuad.y1 === maxY) {
    textRotation = 0;
  } else if (lastQuad.x1 === maxX && lastQuad.y1 === maxY) {
    textRotation = 90;
  } else if (lastQuad.x1 === maxX && lastQuad.y1 === minY) {
    textRotation = 180;
  } else if (lastQuad.x1 === minX && lastQuad.y1 === minY) {
    textRotation = 270;
  }

  const caret = new Annotations.CaretAnnotation();
  let quadHeight;

  if (textRotation === 0 || textRotation === 180) {
    quadHeight = maxY - minY;
  } else {
    quadHeight = maxX - minX;
  }
  const caretSize = quadHeight / 2;
  caret.PageNumber = data.pageNumber;
  // position center of caret at the edge of strikeout
  // center relative to the strikeout
  if (textRotation === 0) {
    caret.X = maxX - (caretSize / 2);
    caret.Y = maxY - caretSize;
  } else if (textRotation === 90) {
    caret.X = maxX - caretSize;
    caret.Y = minY - (caretSize / 2);
  } else if (textRotation === 180) {
    caret.X = minX - (caretSize / 2);
    caret.Y = minY;
  } else if (textRotation === 270) {
    caret.X = minX;
    caret.Y = maxY - (caretSize / 2);
  }

  caret.Width = caretSize;
  caret.Height = caretSize;
  caret.Author = annotManager.getCurrentUser();
  caret.StrokeColor = new Annotations.Color(0, 0, 255);
  caret.Rotation = textRotation;

  annotManager.addAnnotation(caret);
  if (data.annotation) {
    annotManager.groupAnnotations(data.annotation, [caret]);
  }
  annotManager.redrawAnnotation(caret);
  return caret;
};

// Replace Text Tool
const ReplaceTextTool = function() {
  Tools.TextStrikeoutCreateTool.apply(this, arguments);
};

ReplaceTextTool.prototype = new Tools.TextStrikeoutCreateTool();

const replaceToolName = 'ReplaceTextTool';

const replaceTextTool = new ReplaceTextTool(docViewer);
replaceTextTool.on('annotationAdded', function(annotation) {
  // adds the caret annotation when the strikeout is added with the custom tool
  createCaretAnnotation(annotation.Quads, {
    annotation,
    pageNumber: annotation.PageNumber
  });
  instance.focusNote(annotation.Id);
});

instance.registerTool({
  toolName: replaceToolName,
  toolObject: replaceTextTool,
  buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
    '<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>' +
    '<path d="M0 0h24v24H0z" fill="none"/>' +
  '</svg>',
  buttonName: 'replaceTextToolButton',
  tooltip: 'Replace Text'
}, Annotations.CaretAnnotation);

instance.setHeaderItems(function(header) {
  const replaceTextButton = {
    type: 'toolButton',
    toolName: replaceToolName
  };
  header.push(replaceTextButton);
});


// Insert Text Tool
const InsertTextTool = function() {
  Tools.TextSelectTool.apply(this, arguments);
};

InsertTextTool.prototype = new Tools.TextSelectTool();

InsertTextTool.prototype.switchIn = function() {
  Tools.TextSelectTool.prototype.switchIn.apply(this, arguments);
  Tools.Tool.ENABLE_AUTO_SWITCH = false;
};

InsertTextTool.prototype.switchOut = function() {
  Tools.TextSelectTool.prototype.switchOut.apply(this, arguments);
  Tools.Tool.ENABLE_AUTO_SWITCH = true;
};

const insertToolName = 'InsertTextTool';

const insertTextTool = new InsertTextTool(docViewer);
insertTextTool.on('selectionComplete', (startLocation, allQuads) => {
  const selectedPageNumbers = Object.keys(allQuads);
  const pageNumber = selectedPageNumbers[selectedPageNumbers.length - 1];

  const caret = createCaretAnnotation(allQuads[pageNumber], { pageNumber });
  instance.focusNote(caret.Id);
});

instance.registerTool({
  toolName: insertToolName,
  toolObject: insertTextTool,
  buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
    '<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>' +
    '<path d="M0 0h24v24H0z" fill="none"/>' +
  '</svg>',
  buttonName: 'insertTextToolButton',
  tooltip: 'Insert Text'
}, Annotations.CaretAnnotation);

instance.setHeaderItems(function(header) {
  const insertTextButton = {
    type: 'toolButton',
    toolName: insertToolName
  };
  header.push(insertTextButton);
});

Hi Matt,

Thanks for this sample code. To make it correctly work in 7.0 - call to registerTool() must include third parameter, which is a "customAnnotationCheckFunc" - a function that determines if given annotation was created by the tool that is being registered. Without it - showing annotation panel crashes the viewer.
There is also one more thing missing (or something has changed in 7.0?) - after adding one of those annotations user should be redirected to annotation panel to enter the text (to be inserted/replaced). How to achieve that?

Regards,
Tomasz Poradowski

Hi Tomasz,

Thanks for the catch on the customAnnotationCheckFunc. I’ve updated it to include a second parameter to registerTool but we’ll also look into fixing that error since it shouldn’t error out just because the parameter wasn’t added since it’s supposed to be optional.

I’ve also updated the code snippet in the original post so that it calls instance.focusNote to open the notes panel to enter text https://www.pdftron.com/api/web/WebViewerInstance.html#focusNote

Let me know if that works for you.

Matt Parizeau
Software Developer
PDFTron Systems Inc.

Hi Matt,

I hope you are well.

I have tried to use both tools but the issue is if the page rotation is not 0 degrees, they are not working properly. Is there any way to support those cases?

Thanks

Q:

Is it possible to have tools like Adobe’s insert text and replace text tools that create caret annotations?

A:

Yes, the following code will work with WebViewer 7.0. For older versions the allQuads index values will be 0-indexed, so add one to the page number. It takes into account all the feedback from previous messages.

const { Annotations, Tools, annotManager, docViewer } = instance;

// shared helper function that creates caret annotation at the end of the selection
const createCaretAnnotation = (quads, data) => {
  const lastQuad = quads[quads.length - 1];

  const maxY = Math.max(lastQuad.y1, lastQuad.y2, lastQuad.y3, lastQuad.y4);
  const minY = Math.min(lastQuad.y1, lastQuad.y2, lastQuad.y3, lastQuad.y4);

  const maxX = Math.max(lastQuad.x1, lastQuad.x2, lastQuad.x3, lastQuad.x4);
  const minX = Math.min(lastQuad.x1, lastQuad.x2, lastQuad.x3, lastQuad.x4);

  let textRotation;
  if (lastQuad.x1 === minX && lastQuad.y1 === maxY) {
    textRotation = 0;
  } else if (lastQuad.x1 === maxX && lastQuad.y1 === maxY) {
    textRotation = 90;
  } else if (lastQuad.x1 === maxX && lastQuad.y1 === minY) {
    textRotation = 180;
  } else if (lastQuad.x1 === minX && lastQuad.y1 === minY) {
    textRotation = 270;
  }

  const caret = new Annotations.CaretAnnotation();
  let quadHeight;

  if (textRotation === 0 || textRotation === 180) {
    quadHeight = maxY - minY;
  } else {
    quadHeight = maxX - minX;
  }
  const caretSize = quadHeight / 2;
  caret.PageNumber = data.pageNumber;
  // position center of caret at the edge of strikeout
  // center relative to the strikeout
  if (textRotation === 0) {
    caret.X = maxX - (caretSize / 2);
    caret.Y = maxY - caretSize;
  } else if (textRotation === 90) {
    caret.X = maxX - caretSize;
    caret.Y = minY - (caretSize / 2);
  } else if (textRotation === 180) {
    caret.X = minX - (caretSize / 2);
    caret.Y = minY;
  } else if (textRotation === 270) {
    caret.X = minX;
    caret.Y = maxY - (caretSize / 2);
  }

  caret.Width = caretSize;
  caret.Height = caretSize;
  caret.Author = annotManager.getCurrentUser();
  caret.StrokeColor = new Annotations.Color(0, 0, 255);
  caret.Rotation = textRotation;

  annotManager.addAnnotation(caret);
  if (data.annotation) {
    annotManager.groupAnnotations(data.annotation, [caret]);
  }
  annotManager.redrawAnnotation(caret);
  return caret;
};

// Replace Text Tool
const ReplaceTextTool = function() {
  Tools.TextStrikeoutCreateTool.apply(this, arguments);
};

ReplaceTextTool.prototype = new Tools.TextStrikeoutCreateTool();

const replaceToolName = 'ReplaceTextTool';

const replaceTextTool = new ReplaceTextTool(docViewer);
replaceTextTool.on('annotationAdded', function(annotation) {
  // adds the caret annotation when the strikeout is added with the custom tool
  createCaretAnnotation(annotation.Quads, {
    annotation,
    pageNumber: annotation.PageNumber
  });
  instance.focusNote(annotation.Id);
});

instance.registerTool({
  toolName: replaceToolName,
  toolObject: replaceTextTool,
  buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
    '<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>' +
    '<path d="M0 0h24v24H0z" fill="none"/>' +
  '</svg>',
  buttonName: 'replaceTextToolButton',
  tooltip: 'Replace Text'
}, Annotations.CaretAnnotation);

instance.setHeaderItems(function(header) {
  const replaceTextButton = {
    type: 'toolButton',
    toolName: replaceToolName
  };
  header.push(replaceTextButton);
});


// Insert Text Tool
const InsertTextTool = function() {
  Tools.TextSelectTool.apply(this, arguments);
};

InsertTextTool.prototype = new Tools.TextSelectTool();

InsertTextTool.prototype.switchIn = function() {
  Tools.TextSelectTool.prototype.switchIn.apply(this, arguments);
  Tools.Tool.ENABLE_AUTO_SWITCH = false;
};

InsertTextTool.prototype.switchOut = function() {
  Tools.TextSelectTool.prototype.switchOut.apply(this, arguments);
  Tools.Tool.ENABLE_AUTO_SWITCH = true;
};

const insertToolName = 'InsertTextTool';

const insertTextTool = new InsertTextTool(docViewer);
insertTextTool.on('selectionComplete', (startLocation, allQuads) => {
  const selectedPageNumbers = Object.keys(allQuads);
  const pageNumber = selectedPageNumbers[selectedPageNumbers.length - 1];

  const caret = createCaretAnnotation(allQuads[pageNumber], { pageNumber });
  instance.focusNote(caret.Id);
});

instance.registerTool({
  toolName: insertToolName,
  toolObject: insertTextTool,
  buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
    '<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>' +
    '<path d="M0 0h24v24H0z" fill="none"/>' +
  '</svg>',
  buttonName: 'insertTextToolButton',
  tooltip: 'Insert Text'
}, Annotations.CaretAnnotation);

instance.setHeaderItems(function(header) {
  const insertTextButton = {
    type: 'toolButton',
    toolName: insertToolName
  };
  header.push(insertTextButton);
});