> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/edit-page-content/coordinates.md).

# Get and convert PDF coordinates using JavaScript

Learn how to get & convert PDF coordinates using JavaScript. Understand the difference between PDF page coordinates and viewer page coordinates for accurate location handling. The Apryse Web SDK strea

When working with locations in WebViewer, it's important to understand which coordinate space they use. For example, when you set the (x, y) value of an annotation through the WebViewer API, the location is relative to the unzoomed page in viewer page coordinates, not PDF page coordinates.

## PDF page coordinates

In a PDF document, the location (0, 0) is at the bottom-left corner of the page. The x-axis extends horizontally to the right, and the y-axis extends vertically upward.

Here are some APIs to get a page's width and height.

* [getPageHeight](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#getPageHeight)
* [getPageWidth](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#getPageWidth)

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2F0XSkqwQ6lU7Z4ldKk0Af%2Fimage.png?alt=media&amp;token=d1906ea7-9920-4ab6-8074-3e0e816a564a" alt=""><figcaption></figcaption></figure></div>

A page may have a rotation or translation associated with it; in that case, (0, 0) may no longer correspond to the bottom-left corner of the page relative to the viewer.

For example, here is the same page as above but rotated 90 degrees clockwise. Notice how the coordinates have all stayed the same relative to each other, but (0, 0) is now in the top-left corner relative to the viewport.

Here is the [getPageRotation](https://sdk.apryse.com/api/web/Core.Document.html#getPageRotation) API to get a page's rotation.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2Fgit-blob-bce23327ddce91fdb75938224aafc290a3282634%2Fbda8e646ac52e1e02a98f120dd6b8f4e73ddb95a-450x405.png?alt=media" alt=""><figcaption></figcaption></figure></div>

Generally, when you use WebViewer, you won't deal with PDF coordinates directly.

## Viewer page coordinates

When reading or writing annotation locations in WebViewer, these values are in viewer coordinates. The **(0, 0)** point is located at the top left of the page. The x-axis extends horizontally to the right, and the y-axis extends vertically downward.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2Fgit-blob-c217c0df4e7555e57bafe64a8ea63084bb9063f6%2F4acd21aac5c68a794b8fa780945b7c8e42281b75-465x450.png?alt=media" alt=""><figcaption></figcaption></figure></div>

Viewer page coordinates are tied to the underlying page, not to how the page appears on screen. When you rotate a page, the same coordinates point to a different visual location in the viewport. To position annotations based on how they appear to the user, see [Viewport-relative annotation positioning](#viewport-relative-annotation-positioning).

## Convert between PDF and viewer coordinates

WebViewer uses a transformation matrix for each page to convert between PDF and viewer coordinates. The matrix accounts for flipped y values and possible translation or scaling.

Since XOD files are considered 96 DPI and PDF files 72 DPI, the scaling factor for XOD files is 96/72, or 4/3. For PDF files, there is no scaling applied, just the flipped y values.

Annotation locations inside XFDF are in PDF coordinates. When WebViewer imports an XFDF, it automatically transforms the locations into viewer coordinates using the matrix. When exporting XFDF, WebViewer reverses the process and converts back to PDF coordinates.

If you ever need to convert between PDF and viewer coordinates yourself, you can use the [`getPDFCoordinates`](https://sdk.apryse.com/api/web/Core.Document.html#getPDFCoordinates__anchor) function to get PDF coordinates or the [`getXODCoordinates`](https://sdk.apryse.com/api/web/Core.Document.html#getXODCoordinates__anchor) (for XOD files) or [`getViewerCoordinates`](https://sdk.apryse.com/api/web/Core.Document.html#getViewerCoordinates__anchor) (for PDF or Office files) functions.

For example:

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { documentViewer } = instance.Core;

    documentViewer.addEventListener('documentLoaded', () => {
      const doc = documentViewer.getDocument();
      // top left corner in viewer coordinates
      const x = 0;
      const y = 0;
      const pageNumber = 1;

      const pdfCoords = doc.getPDFCoordinates(pageNumber, x, y);
      // top left corner has a high y value in PDF coordinates
      // example return value { x: 0, y: 792 }

      // convert back to viewer coordinates
      const viewerCoords = doc.getViewerCoordinates(pageNumber, pdfCoords.x, pdfCoords.y);
      // { x: 0, y: 0 }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#getViewerCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getViewerCoordinates)
* [DocumentViewer#getPDFCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getPDFCoordinates__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;

    docViewer.on('documentLoaded', () => {
      const doc = docViewer.getDocument();
      // top left corner in viewer coordinates
      const x = 0;
      const y = 0;
      const pageNumber = 1;

      const pdfCoords = doc.getPDFCoordinates(pageNumber, x, y);
      // top left corner has a high y value in PDF coordinates
      // example return value { x: 0, y: 792 }

      // convert back to viewer coordinates
      const viewerCoords = doc.getViewerCoordinates(pageNumber, pdfCoords.x, pdfCoords.y);
      // { x: 0, y: 0 }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#getViewerCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getViewerCoordinates)
* [DocumentViewer#getPDFCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getPDFCoordinates__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;

    docViewer.on('documentLoaded', () => {
      const doc = docViewer.getDocument();
      // top left corner in viewer coordinates
      const x = 0;
      const y = 0;
      const pageNumber = 1;

      const pdfCoords = doc.getPDFCoordinates(pageNumber, x, y);
      // top left corner has a high y value in PDF coordinates
      // example return value { x: 0, y: 792 }

      // convert back to viewer coordinates
      const viewerCoords = doc.getViewerCoordinates(pageNumber, pdfCoords.x, pdfCoords.y);
      // { x: 0, y: 0 }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#getViewerCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getViewerCoordinates)
* [DocumentViewer#getPDFCoordinates](https://sdk.apryse.com/api/web/Core.Document.html#getPDFCoordinates__anchor)
  {% endtab %}
  {% endtabs %}

## Window coordinates

These coordinates are relative to the browser window, with (0, 0) in the top-left corner. The x-axis extends to the right, and the y-axis extends downwards as you scroll through the document content.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2FadX2qPKWbkFOwyQniwMF%2Fd5c6ab1eb68b94fd16c2dc838de300db19905ab3-870x498.jpg?alt=media&amp;token=73eb26e0-63f6-4027-b16a-3703b6cb7c27" alt="Screenshot of the WebViewer document viewer with the thumbnails panel open, showing three red coordinate markers: (0, 0) at the top-left corner of the browser window, (780, 119) at the top-left corner of the document page, and (1319, 816) at the bottom-right corner of the document page, illustrating the window coordinate system."><figcaption><p>Screenshot of the WebViewer document viewer with the thumbnails panel open, showing three red coordinate markers: (0, 0) at the top-left corner of the browser window, (780, 119) at the top-left corner of the document page, and (1319, 816) at the bottom-right corner of the document page, illustrating the window coordinate system.</p></figcaption></figure></div>

Note that the viewer's scroll position does not affect these coordinates. For example, if the user has scrolled to page 10, the window coordinates of the first page remain the same.

In the following image, you can see an example of the document scrolling downward, but the window coordinates for the pages stay the same.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2Fgit-blob-ba5286a92193e8484354222d782f84f93c139618%2F3bdb8e964c92e2cfafd02176dd36733190a29257-501x650.png?alt=media" alt=""><figcaption></figcaption></figure></div>

## Converting between window and viewer page coordinates

WebViewer provides functions on the current display mode to convert between window and page coordinates. The [`pageToWindow`](https://sdk.apryse.com/api/web/Core.DisplayMode.html#pageToWindow__anchor) and [`windowToPage`](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor) functions. For example:

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { documentViewer } = instance.Core;

    const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
    const pageNumber = 1;
    const pagePoint = {
      x: 0,
      y: 0
    };

    const windowPoint = displayMode.pageToWindow(pagePoint, pageNumber);
    // { x: 212, y: 46 }

    const originalPagePoint = displayMode.windowToPage(windowPoint, pageNumber);
    // { x: 0, y: 0 }
  });
```

{% endcode %}

**Related links**

* [DisplayMode#pageToWindow](https://sdk.apryse.com/api/web/Core.DisplayMode.html#pageToWindow__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;

    const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
    const pageNumber = 1;
    const pagePoint = {
      x: 0,
      y: 0
    };

    const windowPoint = displayMode.pageToWindow(pagePoint, pageNumber);
    // { x: 212, y: 46 }

    const originalPagePoint = displayMode.windowToPage(windowPoint, pageNumber);
    // { x: 0, y: 0 }
  });
```

{% endcode %}

**Related links**

* [DisplayMode#pageToWindow](https://sdk.apryse.com/api/web/Core.DisplayMode.html#pageToWindow__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;

    const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
    const pageNumber = 1;
    const pagePoint = {
      x: 0,
      y: 0
    };

    const windowPoint = displayMode.pageToWindow(pagePoint, pageNumber);
    // { x: 212, y: 46 }

    const originalPagePoint = displayMode.windowToPage(windowPoint, pageNumber);
    // { x: 0, y: 0 }
  });
```

{% endcode %}

**Related links**

* [DisplayMode#pageToWindow](https://sdk.apryse.com/api/web/Core.DisplayMode.html#pageToWindow__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
  {% endtab %}
  {% endtabs %}

## Convert between mouse locations and window coordinates

Mouse locations are relative to the viewport, so to convert to window coordinates you only need the viewer's `scrollLeft` and `scrollTop` values. If you're inside of a tool (for example, your own custom tool), you can use [`this.getMouseLocation(e)`](https://sdk.apryse.com/api/web/Core.Tools.Tool.html#getMouseLocation__anchor) to get window coordinates from a mouse event.

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
mouseLeftUp: function(e) {
  const windowCoords = this.getMouseLocation(e);
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

Or manually using the scroll values from the viewer element:

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
  const scrollElement = documentViewer.getScrollViewElement();
  const scrollLeft = scrollElement.scrollLeft || 0;
  const scrollTop = scrollElement.scrollTop || 0;
  return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
const scrollElement = docViewer.getScrollViewElement();
const scrollLeft = scrollElement.scrollLeft || 0;
const scrollTop = scrollElement.scrollTop || 0;
return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
  const scrollElement = docViewer.getScrollViewElement();
  const scrollLeft = scrollElement.scrollLeft || 0;
  const scrollTop = scrollElement.scrollTop || 0;
  return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

This is what happens internally in the default tools, and as we saw above, you can transform window coordinates to page coordinates with a function call.

## In practice

What if you want to double-click the page and add a DOM element there? You probably want the element to scroll with the page and be repositionable after the zoom changes. The easiest way to do this is to position it absolutely inside the `pageContainer` element.

You'll get the event object from the mouse double-click event, transform it into window coordinates, and convert those into page coordinates. DocumentViewer triggers a [`dblClick` event](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:dblClick__anchor) so we can use that to get double clicks inside the viewing area.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { documentViewer } = instance.Core;
    documentViewer.addEventListener('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
    });
});
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;
    docViewer.on('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
    });
});
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer } = instance;
    docViewer.on('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
    });
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

You also need to figure out what page you double-clicked on, and you can use the [getSelectedPages](https://sdk.apryse.com/api/web/Core.DisplayMode.html#getSelectedPages__anchor) function to do this:

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
// takes a start and end point but we just want to see where a single point is located
const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
const clickedPage = (page.first !== null) ? page.first : docViewer.getCurrentPage();
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
// takes a start and end point but we just want to see where a single point is located
const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
const clickedPage = (page.first !== null) ? page.first : docViewer.getCurrentPage();
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
// takes a start and end point but we just want to see where a single point is located
const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
const clickedPage = (page.first !== null) ? page.first : docViewer.getCurrentPage() - 1;
```

{% endcode %}
{% endtab %}
{% endtabs %}

Once you have the page, you can get the page coordinates from the window coordinates:

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
const pageCoordinates = displayMode.windowToPage(windowCoordinates, clickedPage);
```

{% endcode %}
{% endtab %}
{% endtabs %}

Then you can create your custom element and add it to the page container element. Note that you need to scale the position and size by the zoom level.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
const { iframeWindow } = instance.UI;
const zoom = documentViewer.getZoom();
const customElement = document.createElement('div');
customElement.style.position = 'absolute';
customElement.style.left = pageCoordinates.x * zoom;
customElement.style.top = pageCoordinates.y * zoom;
customElement.style.width = 100 * zoom;
customElement.style.height = 25 * zoom;
customElement.style.backgroundColor = 'blue';
customElement.style.zIndex = 35;
const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
pageContainer.appendChild(customElement);
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
const { iframeWindow } = instance;
const zoom = docViewer.getZoom();
const customElement = document.createElement('div');
customElement.style.position = 'absolute';
customElement.style.left = pageCoordinates.x * zoom;
customElement.style.top = pageCoordinates.y * zoom;
customElement.style.width = 100 * zoom;
customElement.style.height = 25 * zoom;
customElement.style.backgroundColor = 'blue';
customElement.style.zIndex = 35;
const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
pageContainer.appendChild(customElement);
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
const { iframeWindow } = instance;
const zoom = docViewer.getZoom();
const customElement = document.createElement('div');
customElement.style.position = 'absolute';
customElement.style.left = pageCoordinates.x * zoom;
customElement.style.top = pageCoordinates.y * zoom;
customElement.style.width = 100 * zoom;
customElement.style.height = 25 * zoom;
customElement.style.backgroundColor = 'blue';
customElement.style.zIndex = 35;
const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
pageContainer.appendChild(customElement);
```

{% endcode %}
{% endtab %}
{% endtabs %}

You'll notice that if you change the zoom or make the page rerender, the elements will disappear. This is because when a page rerenders, it resizes and re-adds itself to the DOM.

To handle this, you can keep track of your custom elements and add them to the updated pageContainer on the [`pageComplete`](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:pageComplete__anchor) event. The full code looks like this:

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
  const scrollElement = document.getElementById('DocumentViewer');
  const scrollLeft = scrollElement.scrollLeft || 0;
  const scrollTop = scrollElement.scrollTop || 0;
  return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
const domElements = {};
const elementWidth = 100;
const elementHeight = 25;
WebViewer(...)
  .then(instance => {
    const { iframeWindow } = instance.UI;
    const { documentViewer } = instance.Core;
    documentViewer.addEventListener('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
      const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
      const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
      const clickedPage = (page.first !== null) ? page.first : documentViewer.getCurrentPage();
      const pageCoordinates = displayMode.windowToPage(windowCoordinates, clickedPage);
      const zoom = documentViewer.getZoom();
      const customElement = document.createElement('div');
      customElement.style.position = 'absolute';
      customElement.style.left = pageCoordinates.x * zoom;
      customElement.style.top = pageCoordinates.y * zoom;
      customElement.style.width = 100 * zoom;
      customElement.style.height = 25 * zoom;
      customElement.style.backgroundColor = 'blue';
      customElement.style.zIndex = 35;
      const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
      pageContainer.appendChild(customElement);
      if (!domElements[clickedPage]) {
        domElements[clickedPage] = [];
      }
      // save left and top so we can scale them when the zoom changes
      domElements[clickedPage].push({
        element: customElement,
        left: pageCoordinates.x,
        top: pageCoordinates.y
      });
    });
    documentViewer.addEventListener('pageComplete', pageNumber => {
      if (domElements[pageNumber]) {
        const zoom = documentViewer.getZoom();
        const pageContainer = iframeWindow.document.getElementById('pageContainer' + pageNumber);
        // add back and scale elements for the rerendered page
        domElements[pageNumber].forEach(elementData => {
          elementData.element.style.left = elementData.left * zoom;
          elementData.element.style.top = elementData.top * zoom;
          elementData.element.style.width = elementWidth * zoom;
          elementData.element.style.height = elementHeight * zoom;
          pageContainer.appendChild(elementData.element);
        });
      }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#pageComplete](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:pageComplete__anchor)
* [DocumentViewer#getZoom](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#getZoom__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
* [DisplayMode#getSelectedPages](https://sdk.apryse.com/api/web/Core.DisplayMode.html#getSelectedPages__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v7.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
  const scrollElement = document.getElementById('DocumentViewer');
  const scrollLeft = scrollElement.scrollLeft || 0;
  const scrollTop = scrollElement.scrollTop || 0;
  return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
const domElements = {};
const elementWidth = 100;
const elementHeight = 25;
WebViewer(...)
  .then(instance => {
    const { docViewer, iframeWindow } = instance;
    docViewer.on('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
      const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
      const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
      const clickedPage = (page.first !== null) ? page.first : docViewer.getCurrentPage();
      const pageCoordinates = displayMode.windowToPage(windowCoordinates, clickedPage);
      const zoom = docViewer.getZoom();
      const customElement = document.createElement('div');
      customElement.style.position = 'absolute';
      customElement.style.left = pageCoordinates.x * zoom;
      customElement.style.top = pageCoordinates.y * zoom;
      customElement.style.width = 100 * zoom;
      customElement.style.height = 25 * zoom;
      customElement.style.backgroundColor = 'blue';
      customElement.style.zIndex = 35;
      const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
      pageContainer.appendChild(customElement);
      if (!domElements[clickedPage]) {
        domElements[clickedPage] = [];
      }
      // save left and top so we can scale them when the zoom changes
      domElements[clickedPage].push({
        element: customElement,
        left: pageCoordinates.x,
        top: pageCoordinates.y
      });
    });
    docViewer.on('pageComplete', pageNumber => {
      if (domElements[pageNumber]) {
        const zoom = docViewer.getZoom();
        const pageContainer = iframeWindow.document.getElementById('pageContainer' + pageNumber);
        // add back and scale elements for the rerendered page
        domElements[pageNumber].forEach(elementData => {
          elementData.element.style.left = elementData.left * zoom;
          elementData.element.style.top = elementData.top * zoom;
          elementData.element.style.width = elementWidth * zoom;
          elementData.element.style.height = elementHeight * zoom;
          pageContainer.appendChild(elementData.element);
        });
      }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#pageComplete](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:pageComplete__anchor)
* [DocumentViewer#getZoom](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#getZoom__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
* [DisplayMode#getSelectedPages](https://sdk.apryse.com/api/web/Core.DisplayMode.html#getSelectedPages__anchor)
  {% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
const getMouseLocation = e => {
  const scrollElement = document.getElementById('DocumentViewer');
  const scrollLeft = scrollElement.scrollLeft || 0;
  const scrollTop = scrollElement.scrollTop || 0;
  return {
    x: e.pageX + scrollLeft,
    y: e.pageY + scrollTop
  };
};
const domElements = {};
const elementWidth = 100;
const elementHeight = 25;
WebViewer(...)
  .then(instance => {
    const { docViewer, iframeWindow } = instance;
    docViewer.on('dblClick', e => {
      // refer to getMouseLocation implementation above
      const windowCoordinates = getMouseLocation(e);
      const displayMode = docViewer.getDisplayModeManager().getDisplayMode();
      const page = displayMode.getSelectedPages(windowCoordinates, windowCoordinates);
      const clickedPage = (page.first !== null) ? page.first : docViewer.getCurrentPage() - 1;
      const pageCoordinates = displayMode.windowToPage(windowCoordinates, clickedPage);
      const zoom = docViewer.getZoom();
      const customElement = document.createElement('div');
      customElement.style.position = 'absolute';
      customElement.style.left = pageCoordinates.x * zoom;
      customElement.style.top = pageCoordinates.y * zoom;
      customElement.style.width = 100 * zoom;
      customElement.style.height = 25 * zoom;
      customElement.style.backgroundColor = 'blue';
      customElement.style.zIndex = 35;
      const pageContainer = iframeWindow.document.getElementById('pageContainer' + clickedPage);
      pageContainer.appendChild(customElement);
      if (!domElements[clickedPage]) {
        domElements[clickedPage] = [];
      }
      // save left and top so we can scale them when the zoom changes
      domElements[clickedPage].push({
        element: customElement,
        left: pageCoordinates.x,
        top: pageCoordinates.y
      });
    });
    docViewer.on('pageComplete', pageIndex => {
      if (domElements[pageIndex]) {
        const zoom = docViewer.getZoom();
        const pageContainer = iframeWindow.document.getElementById('pageContainer' + pageIndex);
        // add back and scale elements for the rerendered page
        domElements[pageIndex].forEach(elementData => {
          elementData.element.style.left = elementData.left * zoom;
          elementData.element.style.top = elementData.top * zoom;
          elementData.element.style.width = elementWidth * zoom;
          elementData.element.style.height = elementHeight * zoom;
          pageContainer.appendChild(elementData.element);
        });
      }
    });
  });
```

{% endcode %}

**Related links**

* [DocumentViewer#pageComplete](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:pageComplete__anchor)
* [DocumentViewer#getZoom](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#getZoom__anchor)
* [DisplayMode#windowToPage](https://sdk.apryse.com/api/web/Core.DisplayMode.html#windowToPage__anchor)
* [DisplayMode#getSelectedPages](https://sdk.apryse.com/api/web/Core.DisplayMode.html#getSelectedPages__anchor)
  {% endtab %}
  {% endtabs %}

## Viewport-relative annotation positioning

Annotations normally store their location in page coordinates. This helps because the annotation stays tied to the underlying document page, but page rotation can make programmatic positioning less intuitive.

For example, consider a rectangle annotation placed near the top-left corner of a page. If you copy it to a page rotated by 90 degrees, copying the stored page coordinates directly can place the rectangle in a different visual corner or change its displayed orientation.

Viewport-relative positioning solves this by describing an annotation as it appears to the user in the current viewport. The viewport-relative values account for the page's effective rotation, including document page rotation and viewer rotation.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2Ffj8hw1r0AvgBfrXXurGC%2F8e1611ac2e113bc6845931302e2ce7c680620293-1152x768.svg?alt=media&amp;token=cc89c2f3-0503-4fd8-a95c-02df24b3659d" alt="Two workflows showing rectangle annotation copy and paste across a 90-degree page rotation. In the disabled workflow, the pasted rectangle becomes vertically oriented and shifts position. In the enabled workflow, the rectangle remains horizontal and preserves its x and y offsets relative to the page&#x27;s top-left corner, maintaining the same visual placement after paste."><figcaption><p>Two workflows showing rectangle annotation copy and paste across a 90-degree page rotation. In the disabled workflow, the pasted rectangle becomes vertically oriented and shifts position. In the enabled workflow, the rectangle remains horizontal and preserves its x and y offsets relative to the page's top-left corner, maintaining the same visual placement after paste.</p></figcaption></figure></div>

### Get viewport-relative annotation properties

Use [getViewportProperties](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#getViewportProperties__anchor) to read the position, dimensions, and rotation of a rectangular annotation in viewport-relative coordinates.

The returned object contains:

* `x` and `y`: The annotation's top-left position in the displayed page.
* `width` and `height`: The annotation's displayed dimensions.
* `rotation`: The annotation's visual rotation in degrees, from `0` up to but not including `360`.

{% tabs %}
{% tab title="JavaScript (SDK v12.1+)" %}
{% code lineNumbers="true" %}

```js
const viewportProperties =
  annotation.getViewportProperties(documentViewer);

  console.log(viewportProperties);
  // Example:
  // {
  //   x: 48,
  //   y: 72,
  //   width: 180,
  //   height: 40,
  //   rotation: 0
  // }
```

{% endcode %}

**Related links**

* [getViewportProperties](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#getViewportProperties__anchor)
  {% endtab %}
  {% endtabs %}

These values describe where the annotation appears on the displayed page. This makes them useful when an application needs to align annotations with UI elements, templates, or coordinates received from another viewport-based workflow.

<figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2FoSg27yyvShQpXnMHGW2i%2Fimage.png?alt=media&amp;token=c450860c-35b2-433b-8230-09bc46804a5a" alt=""><figcaption></figcaption></figure>

### Set viewport-relative annotation properties

Use [`setViewportProperties`](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#setViewportProperties__anchor) to position and rotate a rectangular annotation using viewport-relative values.

WebViewer converts the values back to the annotation's stored page coordinates automatically. The `x`, `y`, `width`, and `height` properties are required. The `rotation` property is optional and defaults to `0`.

{% tabs %}
{% tab title="JavaScript (SDK v12.1+)" %}
{% code lineNumbers="true" %}

```js
annotation.setViewportProperties({
  x: 48,
  y: 72,
  width: 180,
  height: 40,
  rotation: 0,
}, documentViewer);

annotationManager.redrawAnnotation(annotation);
```

{% endcode %}

**Related links**

* [setViewportProperties](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#setViewportProperties__anchor)
  {% endtab %}
  {% endtabs %}

`setViewportProperties` keeps the annotation within the viewport page bounds before converting it to page coordinates. If the requested geometry extends beyond the page, WebViewer adjusts the position and dimensions to fit.

{% hint style="info" %}
**Note:** `setViewportProperties` updates the annotation data but does not redraw it. Call `annotationManager.redrawAnnotation(annotation)` after changing its properties.
{% endhint %}

### Work with path annotations

Path-based annotations, such as lines and freehand annotations, can use viewport-relative path APIs. These APIs are useful when an annotation's visual shape must remain aligned with the displayed page after rotation.

Use `getViewportPath` and `setViewportPath` to read or write an entire path.

{% tabs %}
{% tab title="JavaScript (SDK v12.1+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then((instance) => {
    const { documentViewer, annotationManager } = instance.Core;

    documentViewer.addEventListener('documentLoaded', () => {
      const annotation = annotationManager.getAnnotationsList()
        .find((annotation) => typeof annotation.getPath === 'function');

      if (!annotation) {
        return;
      }

      const viewportPath = annotation.getViewportPath(documentViewer);

      const movedViewportPath = viewportPath.map((point) => ({
        x: point.x + 20,
        y: point.y + 20,
      }));

      annotation.setViewportPath(movedViewportPath, documentViewer);
      annotationManager.redrawAnnotation(annotation);
    });
  });
```

{% endcode %}

**Related links**

* [getViewportPath](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#getViewportPath__anchor)
* [setViewportPath](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#setViewportPath__anchor)
  {% endtab %}
  {% endtabs %}

For workflows that edit a single path point, use `getViewportPathPoint` and `setViewportPathPoint`.

{% tabs %}
{% tab title="JavaScript (SDK v12.1+)" %}
{% code lineNumbers="true" %}

```js
const firstPoint = annotation.getViewportPathPoint(0, documentViewer);

if (firstPoint) {
  annotation.setViewportPathPoint(0, {
    x: firstPoint.x + 20,
    y: firstPoint.y,
  }, documentViewer);

  annotationManager.redrawAnnotation(annotation);
}
```

{% endcode %}

**Related links**

* [getViewportPathPoint](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#getViewportPathPoint__anchor)
* [setViewportPathPoint](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#setViewportPathPoint__anchor)
  {% endtab %}
  {% endtabs %}

### Copy annotations between rotated pages

Use the `viewportRelative` paste option when copying annotations between pages that have different rotations. WebViewer transforms the copied annotation through viewport space, so its visual position and orientation are preserved on the target page.

{% tabs %}
{% tab title="JavaScript (SDK v12.1+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then((instance) => {
    const { documentViewer, annotationManager } = instance.Core;

    documentViewer.addEventListener('documentLoaded', () => {
      const sourcePageNumber = 1;
      const targetPageNumber = 2;

      documentViewer.setCurrentPage(sourcePageNumber);

      const annotation = annotationManager.getAnnotationsList()
        .find((annotation) => annotation.PageNumber === sourcePageNumber);

      if (!annotation) {
        return;
      }

      annotationManager.selectAnnotation(annotation);
      annotationManager.updateCopiedAnnotations();

      documentViewer.setCurrentPage(targetPageNumber);

      annotationManager.pasteCopiedAnnotations(false, {
        viewportRelative: true,
      });
    });
  });
```

{% endcode %}

**Related links**

* [pasteCopiedAnnotations](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#pasteCopiedAnnotations__anchor)
* [PasteOptions](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#.PasteOptions)
  {% endtab %}
  {% endtabs %}

When `viewportRelative` is `true`, WebViewer preserves the copied annotation's visual placement and orientation as closely as the target page allows. This is especially useful for stamps, free text, shapes, and freehand annotations.

If the source and target pages have different dimensions, WebViewer fits the result within the target page bounds.

{% hint style="info" %}
**Info:** The `viewportRelative` option ignores `useMouseLocation`. Use viewport-relative paste when preserving visual placement is more important than pasting at the cursor position.
{% endhint %}

### Toggle viewport-relative annotation positioning

The Settings Modal includes an **Enable Viewport-Relative Annotation Positioning** toggle under **Advanced Settings** > **Annotations**.

<div align="left"><figure><img src="https://3532544125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX9YnTSKIHvV7m0A36LbO%2Fuploads%2FwQDklMhgf75eqI4IHNkM%2F8a20818c7403cc39c4c4a03acf2f243be87a43bb-897x519.png?alt=media&amp;token=7c9aafba-ccd1-4407-ab1a-0a884725c398" alt="Screenshot of the Settings Modal which contains an Enable Viewport-Relative Annotation Positioning section and a toggle button to turn this feature on/off"><figcaption><p>Screenshot of the Settings Modal which contains an Enable Viewport-Relative Annotation Positioning section and a toggle button to turn this feature on/off</p></figcaption></figure></div>

When enabled, standard keyboard paste uses viewport-relative positioning. Copying an annotation from one page and pasting it onto a page with a different rotation preserves the annotation's visual position and orientation.

When disabled, WebViewer uses the page-coordinate paste behavior. This maintains compatibility with workflows that expect annotation geometry to be copied directly in page coordinates.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.apryse.com/web/edit-page-content/coordinates.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
