Skip to content

Adding a Custom Annotation on a PDF Programmatically



There are times when you are required to add custom annotations programmatically. An example is when you need to render bounding box (BBox) annotations for an OCR application or a RAG application.

Vue PDF Viewer supports defining annotations using the bbox approach, allowing for flexible and responsive placement on PDF pages.

This tutorial will guide you through how to highlight an important section on a PDF page with a custom marker using a simple bbox approach.

Here is a quick overview of what you’ll learn in this tutorial:

  • Set up your PDF viewer with a custom component that hosts Vue PDF Viewer.
  • Render annotations using the raw PDF BBox coordinate values (exact coordinates).
  • Rendering multiple “layers” of annotations, each with its own style.

Tutorial for Adding Custom Annotation

1. Imports, Constants, and Types

At the heart of the system is the bounding box (BBox)—a rectangular region defined by its four edges in PDF coordinate space. In PDFs, the origin (0, 0) starts at the bottom-left of the page. The x-axis moves rightward, and the y-axis moves upward.

By storing both corners of a bounding box (left, bottom, right, top), we can easily calculate position, width, height, and aspect ratio. We also support an optional type for visual classification (e.g., "important" or "question"), and an optional note field for contextual tooltips.

To begin, we will define the basic setup for the annotation system. These constants, imports, and types form the foundation for rendering and interacting with custom annotations on top of the PDF pages.

vue
<script setup lang="ts">
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { ref, onMounted } from "vue";

const samplePDF = "your-pdf-file.pdf";

// describes each highlight’s rectangle, type, and optional note.
interface BBox {
  left: number; // X-coordinate of the lower-left corner
  bottom: number; // Y-coordinate of the lower-left corner
  right: number; // X-coordinate of the upper-right corner
  top: number; // Y-coordinate of the upper-right corner
  type?: "important" | "todo" | "question"; //optional
  note?: string; //optional
}

//* left and bottom mark the coordinates of the box’s lower-left corner.
//* right and top mark the coordinates of the box’s upper-right corner.
//* The box’s width is right − left, and its height is top − bottom.

// array of regions to annotate.
const pageHighlights: BBox[] = [
  {
    left: 220,
    bottom: 500,
    right: 380,
    top: 550,
    type: "important",
    note: "Critical requirement—do not skip.",
  },
  {
    left: 100,
    bottom: 300,
    right: 260,
    top: 360,
    type: "todo",
    note: "Verify numbers here before release.",
  },
  {
    left: 300,
    bottom: 700,
    right: 460,
    top: 760,
    type: "question",
    note: "Need clarification on this section.",
  },
];

// ensures highlights are only rendered once per load.
let highlightsAdded = false;
// keeps track of the current tooltip element for hover notes
const tooltipRef = ref<HTMLElement | null>(null);
</script>
vue
<script setup>
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { ref, onMounted } from "vue";

const samplePDF = "your-pdf-file.pdf";

const pageHighlights = [
  {
    left: 220, // X-coordinate of the lower-left
    bottom: 500, // Y-coordinate of the lower-left corner
    right: 380, // X-coordinate of the upper-right
    top: 550, // Y-coordinate of the upper-right corner
    type: "important",
    note: "Critical requirement—do not skip.",
  },
  {
    left: 100,
    bottom: 300,
    right: 260,
    top: 360,
    type: "todo",
    note: "Verify numbers here before release.",
  },
  {
    left: 300,
    bottom: 700,
    right: 460,
    top: 760,
    type: "question",
    note: "Need clarification on this section.",
  },
];
// ensures highlights are only rendered once per load.
let highlightsAdded = false;
// keeps track of the current tooltip element for hover notes
const tooltipRef = ref(null);
</script>
vue
<script lang="ts">
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { defineComponent } from "vue";

const samplePDF = "your-pdf-file.pdf";

// describes each highlight’s rectangle, type, and optional note.
interface BBox {
  left: number; // X-coordinate of the lower-left corner
  bottom: number; // Y-coordinate of the lower-left corner
  right: number; // X-coordinate of the upper-right corner
  top: number; // Y-coordinate of the upper-right corner
  type?: "important" | "todo" | "question"; //optional
  note?: string; //optional
}

//* left and bottom mark the coordinates of the box’s lower-left corner.
//* right and top mark the coordinates of the box’s upper-right corner.
//* The box’s width is right − left, and its height is top − bottom.

export default defineComponent({
  components: { VPdfViewer },
  data() {
    return {
      samplePDF: "your-pdf-file.pdf",
      pageHighlights: [
        {
          left: 220,
          bottom: 500,
          right: 380,
          top: 550,
          type: "important",
          note: "Critical requirement—do not skip.",
        },
        {
          left: 100,
          bottom: 300,
          right: 260,
          top: 360,
          type: "todo",
          note: "Verify numbers here before release.",
        },
        {
          left: 300,
          bottom: 700,
          right: 460,
          top: 760,
          type: "question",
          note: "Need clarification on this section.",
        },
      ] as BBox[],
      highlightsAdded: false, // ensures highlights are only rendered once per load.
      scaleObserver: null as MutationObserver | null,
      tooltipRef: null, // keeps track of the current tooltip element for hover notes
    };
  },
});
</script>
vue
<script>
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { defineComponent } from "vue";

const samplePDF = "your-pdf-file.pdf";

export default defineComponent({
  components: { VPdfViewer },
  data() {
    return {
      samplePDF: "your-pdf-file.pdf",
      pageHighlights: [
        {
          left: 220,
          bottom: 500,
          right: 380,
          top: 550,
          type: "important",
          note: "Critical requirement—do not skip.",
        },
        {
          left: 100,
          bottom: 300,
          right: 260,
          top: 360,
          type: "todo",
          note: "Verify numbers here before release.",
        },
        {
          left: 300,
          bottom: 700,
          right: 460,
          top: 760,
          type: "question",
          note: "Need clarification on this section.",
        },
      ],
      highlightsAdded: false, // ensures highlights are only rendered once per load.
      scaleObserver: null,
      tooltipRef: null, // keeps track of the current tooltip element for hover notes
    };
  },
});
</script>

2. Locating the Page Element

The findPageElemByPageIdx function maps a logical page index (like 0, 1, 2, etc.) to the corresponding DOM element in the viewer. If the element isn’t available yet (e.g., the page hasn’t finished rendering), the function safely returns null, so you can retry later without crashing.

Why use data-page-index?

Each PDF page rendered by <VPdfViewer> is wrapped in an element with a data-page-index attribute. This gives you a reliable selector for targeting and injecting annotations into specific pages.

vue
<script setup lang="ts">
function findPageElemByPageIdx(index: number): HTMLElement | null {
  // find the element to annotate based on page index
  return document.querySelector<HTMLElement>(`[data-page-index="${index}"]`);
}
</script>
vue
<script setup>
function findPageElemByPageIdx(index) {
  // find the element to annotate based on page index
  return document.querySelector(`[data-page-index="${index}"]`);
}
</script>
vue
<script lang="ts">
methods: {
 findPageElemByPageIdx(index: number): HTMLElement | null {
  // find the element to annotate based on page index
  return document.querySelector<HTMLElement>(`[data-page-index="${index}"]`)
 },
},
</script>
vue
<script>
methods: {
 findPageElemByPageIdx(index) {
  // find the element to annotate based on page index
  return document.querySelector(`[data-page-index="${index}"]`)
 }
},
</script>

3. Tooltip Function

This set of helper functions manages the lifecycle of a floating tooltip. These tooltips appear when users hover over an annotated area and follow the cursor’s position.

The functions handle creating the tooltip, updating its position, and removing it cleanly, ensuring no leftover DOM elements remain.

vue
<script setup lang="ts">
function createTooltip(): HTMLElement {
  // Remove existing tooltip if it exists
  if (tooltipRef.value) {
    tooltipRef.value.remove()
  }

  // Create a new tooltip element
  const tooltip = document.createElement('div')
  tooltip.className = 'highlight-tooltip'

  // Append it to the body and store the reference
  document.body.appendChild(tooltip)
  tooltipRef.value = tooltip

  return tooltip
}

function positionTooltip(e: MouseEvent) {
  // If tooltip exists, move it near the cursor (15px offset)
  if (!tooltipRef.value) return
  tooltipRef.value.style.left = `${e.clientX + 15}px`
  tooltipRef.value.style.top  = `${e.clientY - 15}px`
}

function removeTooltip() {
  // Remove tooltip from DOM and clear the reference
  if (tooltipRef.value) {
    tooltipRef.value.remove()
    tooltipRef.value = null
  }
}
</script>
vue
<script setup>
function createTooltip() {
  // Remove existing tooltip if present
  if (tooltipRef.value) {
    tooltipRef.value.remove()
  }

  // Create a fresh tooltip div
  const tooltip = document.createElement('div')
  tooltip.className = 'highlight-tooltip'

  // Add it to the document and save reference
  document.body.appendChild(tooltip)
  tooltipRef.value = tooltip

  return tooltip
}

function positionTooltip(e) {
  // Move tooltip next to mouse cursor (with offset)
  if (!tooltipRef.value) return
  tooltipRef.value.style.left = `${e.clientX + 15}px`
  tooltipRef.value.style.top = `${e.clientY - 15}px`
}

function removeTooltip() {
  // Clean up tooltip and unset the ref
  if (tooltipRef.value) {
    tooltipRef.value.remove()
    tooltipRef.value = null
  }
}
</script>
vue
<script lang="ts">
methods: {
  createTooltip(): HTMLElement {
    // Remove old tooltip if one is already on screen
    if (this.tooltipRef) {
      this.tooltipRef.remove()
    }

    // Create a new tooltip and attach it to the body
    const tooltip = document.createElement('div')
    tooltip.className = 'highlight-tooltip'
    document.body.appendChild(tooltip)

    // Save the tooltip reference for reuse/removal
    this.tooltipRef = tooltip
    return tooltip
  },

  positionTooltip(e: MouseEvent) {
    // Update tooltip position relative to mouse
    if (!this.tooltipRef) return
    this.tooltipRef.style.left = `${e.clientX + 15}px`
    this.tooltipRef.style.top = `${e.clientY - 15}px`
  },

  removeTooltip() {
    // Remove tooltip from DOM and clear reference
    if (this.tooltipRef) {
      this.tooltipRef.remove()
      this.tooltipRef = null
    }
  }
}
</script>
vue
<script>
methods: {
  createTooltip() {
    // Remove existing tooltip if already there
    if (this.tooltipRef) {
      this.tooltipRef.remove()
    }

    // Make a new tooltip and append to body
    const tooltip = document.createElement('div')
    tooltip.className = 'highlight-tooltip'
    document.body.appendChild(tooltip)

    // Store reference for future use
    this.tooltipRef = tooltip
    return tooltip
  },

  positionTooltip(e) {
    // Set tooltip position relative to mouse (with offset)
    if (!this.tooltipRef) return
    this.tooltipRef.style.left = `${e.clientX + 15}px`
    this.tooltipRef.style.top = `${e.clientY - 15}px`
  },

  removeTooltip() {
    // Remove tooltip and clear reference
    if (this.tooltipRef) {
      this.tooltipRef.remove()
      this.tooltipRef = null
    }
  }
}
</script>

image of vue-pdf-viewer-annotation-tooltip

4. Rendering a Highlight

The addHighlight function takes a single BBox, calculates its pixel-based position and dimensions, and creates a styled, absolutely positioned <div> overlay. If the BBox includes a note, the function wires up tooltip events to display that content on hover.

vue
<script setup lang="ts">
function addHighlight(bbox: BBox, pageElement: HTMLElement, scale = 1) {
    
  // Coordinate math: convert PDF-space to screen-space
  const vh = pageElement.offsetHeight
  const x = bbox.left * scale
  const y = vh - bbox.top * scale
  const width = (bbox.right - bbox.left) * scale
  const height = (bbox.top - bbox.bottom) * scale

  // Element setup: create a highlight div with appropriate classes
  const hl = document.createElement('div')
  hl.classList.add('highlight', bbox.type)
    
  // Inline styles: position and size the element over PDF content
  Object.assign(hl.style, {
    left: `${x}px`,
    top: `${y}px`,
    width: `${width}px`,
    height: `${height}px`,
    borderRadius: '2px'
  })
  hl.style.pointerEvents = 'auto'
  hl.style.cursor = bbox.note ? 'help' : 'default'

  // Tooltip wiring: attach events if a note exists
  if (bbox.note) {
    hl.dataset.note = bbox.note
    hl.addEventListener('mouseenter', (e) => {
      const tooltip = createTooltip()
      tooltip.textContent = bbox.note!
      positionTooltip(e as MouseEvent)
    })
    hl.addEventListener('mousemove', positionTooltip)
    hl.addEventListener('mouseleave', removeTooltip)
  }

  // DOM injection: add the highlight to the correct PDF page
  pageElement.appendChild(hl)
}
</script>
vue
<script setup>
function addHighlight(bbox, pageElement, scale = 1) {

  // Coordinate math: convert PDF-space to screen-space
  const vh = pageElement.offsetHeight
  const x = bbox.left * scale
  const y = vh - bbox.top * scale
  const width = (bbox.right - bbox.left) * scale
  const height = (bbox.top - bbox.bottom) * scale

  // Create the highlight element
  const hl = document.createElement('div')
  hl.classList.add('highlight', bbox.type)

  // Apply inline positioning styles
  Object.assign(hl.style, {
    left: `${x}px`,
    top: `${y}px`,
    width: `${width}px`,
    height: `${height}px`,
    borderRadius: '2px'
  })
  hl.style.pointerEvents = 'auto'
  hl.style.cursor = bbox.note ? 'help' : 'default'

  // Handle tooltip if note is present
  if (bbox.note) {
    hl.dataset.note = bbox.note
    hl.addEventListener('mouseenter', (e) => {
      const tooltip = createTooltip()
      tooltip.textContent = bbox.note
      positionTooltip(e)
    })
    hl.addEventListener('mousemove', positionTooltip)
    hl.addEventListener('mouseleave', removeTooltip)
  }

  // Inject into DOM
  pageElement.appendChild(hl)
}
</script>
vue
<script lang="ts">
methods: {
  addHighlight(bbox: BBox, pageElement: HTMLElement, scale = 1) {

    // Coordinate math: convert PDF-space to screen-space
    const vh = pageElement.offsetHeight
    const x = bbox.left * scale
    const y = vh - bbox.top * scale
    const width = (bbox.right - bbox.left) * scale
    const height = (bbox.top - bbox.bottom) * scale

    // Create the highlight element
    const hl = document.createElement('div')
    hl.classList.add('highlight', bbox.type)

    // Inline style setup
    Object.assign(hl.style, {
      left: `${x}px`,
      top: `${y}px`,
      width: `${width}px`,
      height: `${height}px`,
      borderRadius: '2px'
    })
    hl.style.pointerEvents = 'auto'
    hl.style.cursor = bbox.note ? 'help' : 'default'

    // Tooltip if note is defined
    if (bbox.note) {
      hl.dataset.note = bbox.note
      hl.addEventListener('mouseenter', (e) => {
        const tooltip = this.createTooltip()
        tooltip.textContent = bbox.note!
        this.positionTooltip(e as MouseEvent)
      })
      hl.addEventListener('mousemove', this.positionTooltip)
      hl.addEventListener('mouseleave', this.removeTooltip)
    }

    // Append to page element
    pageElement.appendChild(hl)
  }
}
</script>
vue
<script>
methods: {
  addHighlight(bbox, pageElement, scale = 1) {

    // Coordinate math: convert PDF-space to screen-space
    const vh = pageElement.offsetHeight
    const x = bbox.left * scale
    const y = vh - bbox.top * scale
    const width = (bbox.right - bbox.left) * scale
    const height = (bbox.top - bbox.bottom) * scale

    // Create highlight div
    const hl = document.createElement('div')
    hl.classList.add('highlight', bbox.type)

    // Apply inline positioning/sizing
    Object.assign(hl.style, {
      left: `${x}px`,
      top: `${y}px`,
      width: `${width}px`,
      height: `${height}px`,
      borderRadius: '2px'
    })
    hl.style.pointerEvents = 'auto'
    hl.style.cursor = bbox.note ? 'help' : 'default'

    // Handle hover tooltip events if note exists
    if (bbox.note) {
      hl.dataset.note = bbox.note
      hl.addEventListener('mouseenter', (e) => {
        const tooltip = this.createTooltip()
        tooltip.textContent = bbox.note
        this.positionTooltip(e)
      })
      hl.addEventListener('mousemove', this.positionTooltip)
      hl.addEventListener('mouseleave', this.removeTooltip)
    }

    // Inject into the PDF page element
    pageElement.appendChild(hl)
  }
}
</script>

5. Initial Load & Highlight Injection

This step ensures highlights are injected only once, after the PDF page is fully rendered. It uses polling to wait for the target DOM element to appear, then iterates over all BBox annotations and applies them.

It also respects the viewer’s current scale factor to accurately position highlights even on zoomed-in pages.

vue
<script setup lang="ts">
function handlePDFLoaded() {
  const checkAndAddHighlights = () => {
    // Skip if already added
    if (highlightsAdded.value) return

    // Retry until page element is ready
    const pageEl = findPageElemByPageIdx(0)
    if (!pageEl) {
      setTimeout(checkAndAddHighlights, 500)
      return
    }

    // Prevent duplicates
    if (pageEl.querySelector('.highlight')) return

    // Read the scale factor from CSS
    const wrapper = pageEl.closest('.vpv-pages-inner-wrapper') as HTMLElement | null
    if (!wrapper) return

    const scale = parseFloat(
      getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
    )

    // Apply all highlights
    pageHighlights.value.forEach((bbox) => addHighlight(bbox, pageEl, scale))

    // Set completion flag
    highlightsAdded.value = true
  }

  // Called on @loaded
  checkAndAddHighlights()
}
</script>

<template>
  <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
</template>
vue
<script setup>
function handlePDFLoaded() {
  const checkAndAddHighlights = () => {
    // Already done? Skip
    if (highlightsAdded.value) return

    // Wait for page DOM
    const pageEl = findPageElemByPageIdx(0)
    if (!pageEl) {
      setTimeout(checkAndAddHighlights, 500)
      return
    }

    // Avoid double-inserting highlights
    if (pageEl.querySelector('.highlight')) return

    // Read scale from viewer
    const wrapper = pageEl.closest('.vpv-pages-inner-wrapper')
    if (!wrapper) return

    const scale = parseFloat(
      getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
    )

    // Apply highlights for each BBox
    pageHighlights.value.forEach((bbox) => addHighlight(bbox, pageEl, scale))

    // Remember that highlights are added
    highlightsAdded.value = true
  }

  // Trigger this after PDF loads
  checkAndAddHighlights()
}
</script>

<template>
  <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
</template>
vue
<script lang="ts">
export default {
  methods: {
    handlePDFLoaded() {
      const checkAndAddHighlights = () => {
        // Duplicate guard: skip if highlights already added
        if (this.highlightsAdded) return

        // Polling loop: wait until the page element exists
        const pageEl = this.findPageElemByPageIdx(0)
        if (!pageEl) {
          setTimeout(checkAndAddHighlights, 500)
          return
        }

        // Skip if highlight divs already exist
        if (pageEl.querySelector('.highlight')) return

        // Scale retrieval: get scale from viewer CSS variable
        const wrapper = pageEl.closest('.vpv-pages-inner-wrapper') as HTMLElement | null
        if (!wrapper) return

        const scale = parseFloat(
          getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
        )

        // Highlight loop: add highlight divs for each region
        this.pageHighlights.forEach((bbox) => this.addHighlight(bbox, pageEl, scale))

        // Mark as done
        this.highlightsAdded = true
      }

      // Trigger: on viewer’s @loaded event
      checkAndAddHighlights()
    }
  }
}
</script>

<template>
  <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
</template>
vue
<script>
export default {
  methods: {
    handlePDFLoaded() {
      const checkAndAddHighlights = () => {
        // Skip if highlights were already rendered
        if (this.highlightsAdded) return

        // Wait for the page DOM element
        const pageEl = this.findPageElemByPageIdx(0)
        if (!pageEl) {
          setTimeout(checkAndAddHighlights, 500)
          return
        }

        // Avoid duplicates
        if (pageEl.querySelector('.highlight')) return

        // Grab wrapper to read scale factor
        const wrapper = pageEl.closest('.vpv-pages-inner-wrapper')
        if (!wrapper) return

        const scale = parseFloat(
          getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
        )

        // Loop over bounding boxes and add highlights
        this.pageHighlights.forEach((bbox) => this.addHighlight(bbox, pageEl, scale))

        // Mark flag to prevent re-highlighting
        this.highlightsAdded = true
      }

      // Run on viewer load
      checkAndAddHighlights()
    }
  }
}
</script>

<template>
  <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
</template>

6. Keeping Highlights in Sync

This function tears down every existing .highlight and tooltip, resets the "added" flag, and re-invokes your load logic after a brief delay—so annotations always realign when the viewer is resized or zoomed.

vue
<script setup lang="ts">
function handleScaleChange() {
  // Remove all highlights from the DOM
  document.querySelectorAll('.highlight').forEach((el) => el.remove())

  // Clear tooltip if visible
  removeTooltip()

  // Reset flag to allow highlights to be redrawn
  highlightsAdded = false

  // Re-run highlight logic after short delay (to allow re-scaling)
  setTimeout(() => handlePDFLoaded(), 300)
}
</script>
vue
<script setup>
function handleScaleChange() {
  // Remove all highlights from the DOM
  document.querySelectorAll('.highlight').forEach((el) => el.remove())

  // Clear tooltip if visible
  removeTooltip()

  // Reset flag to trigger re-rendering
  highlightsAdded = false

  // Re-run highlight logic after short delay (to allow re-scaling)
  setTimeout(() => handlePDFLoaded(), 300)
}
</script>
vue
<script lang="ts">
export default {
  methods: {
    handleScaleChange() {
      // Remove all highlights from the DOM
      document.querySelectorAll('.highlight').forEach((el) => el.remove())

      // Clear tooltip if visible
      this.removeTooltip()

      // Reset flag to trigger re-rendering
      this.highlightsAdded = false

      // Re-run highlight logic after short delay (to allow re-scaling)
      setTimeout(() => this.handlePDFLoaded(), 300)
    }
  }
}
</script>
vue
<script>
export default {
  methods: {
    handleScaleChange() {
      // Remove all highlight overlays
      document.querySelectorAll('.highlight').forEach((el) => el.remove())

      // Clear tooltip if visible
      this.removeTooltip()

      // Reset flag to trigger re-rendering
      this.highlightsAdded = false

      // Re-run highlight logic after short delay (to allow re-scaling)
      setTimeout(() => this.handlePDFLoaded(), 300)
    }
  }
}
</script>

7. Lifecycle Hook & Cleanup

The onMounted block sets up everything needed to keep annotations responsive. It:

  • Listens for window resize events
  • Listens for pdfViewerScaleChanged events emitted by <VPdfViewer>
  • Attaches a MutationObserver to detect structural changes in the viewer

It also returns a cleanup function that removes all registered listeners and cleans up any remaining tooltips.

vue
<script setup lang="ts">
onMounted(() => {
  // Mount-time setup
  // Listen for window resize and custom viewer scale change events
  window.addEventListener('resize', handleScaleChange)
  document.addEventListener('pdfViewerScaleChanged', handleScaleChange)

  // Create a MutationObserver to detect inline style changes (e.g. scale animations)
  const scaleObserver = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
        handleScaleChange()
        break
      }
    }
  })

  // Delay observing until wrapper exists
  setTimeout(() => {
    const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
    if (wrapper) {
      scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
    }
  }, 1000)

  // Cleanup function: remove all listeners and observer
  return () => {
    window.removeEventListener('resize', handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', handleScaleChange)
    scaleObserver.disconnect()
    removeTooltip()
  }
})
</script>
vue
<script setup>
onMounted(() => {
  // Mount-time setup
  // Listen for window resize and custom viewer scale change events
  window.addEventListener('resize', handleScaleChange)
  document.addEventListener('pdfViewerScaleChanged', handleScaleChange)

  // Create a MutationObserver to detect inline style changes (e.g. scale animations)
  const scaleObserver = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
        handleScaleChange()
        break
      }
    }
  })

  // Delay observing until wrapper exists
  setTimeout(() => {
    const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
    if (wrapper) {
      scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
    }
  }, 1000)

  // Cleanup function: remove all listeners and observer
  return () => {
    window.removeEventListener('resize', handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', handleScaleChange)
    scaleObserver.disconnect()
    removeTooltip()
  }
})
</script>
vue
<script lang="ts">
export default {
  mounted() {
    // Mount-time setup
    window.addEventListener('resize', this.handleScaleChange)
    document.addEventListener('pdfViewerScaleChanged', this.handleScaleChange)

    // Create and store observer
    this.scaleObserver = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
          this.handleScaleChange()
          break
        }
      }
    })

    // Delay attaching observer until wrapper appears
    setTimeout(() => {
      const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
      if (wrapper && this.scaleObserver) {
        this.scaleObserver.observe(wrapper, {
          attributes: true,
          attributeFilter: ['style']
        })
      }
    }, 1000)
  },

  beforeUnmount() {
    // Cleanup function
    window.removeEventListener('resize', this.handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', this.handleScaleChange)
    this.scaleObserver?.disconnect()
    this.removeTooltip()
  }
}
</script>
vue
<script>
export default {
  mounted() {
    // Mount-time setup
    window.addEventListener('resize', this.handleScaleChange)
    document.addEventListener('pdfViewerScaleChanged', this.handleScaleChange)

    // Create and store observer
    this.scaleObserver = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
          this.handleScaleChange()
          break
        }
      }
    })

    // Delay attaching observer until wrapper appears
    setTimeout(() => {
      const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
      if (wrapper) {
        this.scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
      }
    }, 1000)
  },

  beforeUnmount() {
    // Cleanup function
    window.removeEventListener('resize', this.handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', this.handleScaleChange)
    this.scaleObserver.disconnect()
    this.removeTooltip()
  }
}
</script>

Complete Example

Here’s how everything fits together:

  • The VPdfViewer component loads and renders the PDF.
  • When the PDF is loaded, the viewer checks for highlights to display.
  • Each highlight is rendered as a styled overlay box, with optional notes shown on hover.
  • Highlights automatically adjust on zoom or resize.
vue
<script setup lang="ts">
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import { ref, onMounted } from 'vue'

const samplePDF = 'your-pdf-file.pdf'

interface BBox {
  left: number
  bottom: number
  right: number
  top: number
  type: 'important' | 'todo' | 'question'
  note?: string
}

const pageHighlights: BBox[] = [
  {
    left: 220,
    bottom: 500,
    right: 380,
    top: 550,
    type: 'important',
    note: 'Critical requirement—do not skip.'
  },
  {
    left: 100,
    bottom: 300,
    right: 260,
    top: 360,
    type: 'todo',
    note: 'Verify numbers here before release.'
  },
  {
    left: 300,
    bottom: 700,
    right: 460,
    top: 760,
    type: 'question',
    note: 'Need clarification on this section.'
  }
]

let highlightsAdded = false
const tooltipRef = ref<HTMLElement | null>(null)

function findPageElemByPageIdx(index: number): HTMLElement | null {
  return document.querySelector<HTMLElement>(`[data-page-index="${index}"]`)
}

function addHighlight(bbox: BBox, pageElement: HTMLElement, scale = 1) {
  const vh = pageElement.offsetHeight
  const x = bbox.left * scale
  const y = vh - bbox.top * scale
  const width = (bbox.right - bbox.left) * scale
  const height = (bbox.top - bbox.bottom) * scale

  const hl = document.createElement('div')
  hl.classList.add('highlight', bbox.type)
  Object.assign(hl.style, {
    left: `${x}px`,
    top: `${y}px`,
    width: `${width}px`,
    height: `${height}px`,
    borderRadius: '2px'
  })
  hl.style.pointerEvents = 'auto'
  hl.style.cursor = bbox.note ? 'help' : 'default'

  if (bbox.note) {
    hl.dataset.note = bbox.note
    hl.addEventListener('mouseenter', (e) => {
      const tooltip = createTooltip()
      tooltip.textContent = bbox.note!
      positionTooltip(e as MouseEvent)
    })
    hl.addEventListener('mousemove', positionTooltip)
    hl.addEventListener('mouseleave', removeTooltip)
  }

  pageElement.appendChild(hl)
}

function createTooltip(): HTMLElement {
  if (tooltipRef.value) {
    tooltipRef.value.remove()
  }
  const tooltip = document.createElement('div')
  tooltip.className = 'highlight-tooltip'
  document.body.appendChild(tooltip)
  tooltipRef.value = tooltip
  return tooltip
}

function positionTooltip(e: MouseEvent) {
  if (!tooltipRef.value) return
  tooltipRef.value.style.left = `${e.clientX + 15}px`
  tooltipRef.value.style.top = `${e.clientY - 15}px`
}

function removeTooltip() {
  if (tooltipRef.value) {
    tooltipRef.value.remove()
    tooltipRef.value = null
  }
}

function handlePDFLoaded() {
  const checkAndAddHighlights = () => {
    if (highlightsAdded) return
    const pageEl = findPageElemByPageIdx(0)
    if (!pageEl) {
      setTimeout(checkAndAddHighlights, 500)
      return
    }
    if (pageEl.querySelector('.highlight')) return
    const wrapper = pageEl.closest('.vpv-pages-inner-wrapper') as HTMLElement
    if (!wrapper) return
    const scale = parseFloat(getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1')
    pageHighlights.forEach((bbox) => addHighlight(bbox, pageEl, scale))
    highlightsAdded = true
  }
  checkAndAddHighlights()
}

function handleScaleChange() {
  document.querySelectorAll('.highlight').forEach((el) => el.remove())
  removeTooltip()
  highlightsAdded = false
  setTimeout(() => handlePDFLoaded(), 300)
}

onMounted(() => {
  window.addEventListener('resize', handleScaleChange)
  document.addEventListener('pdfViewerScaleChanged', handleScaleChange)
  const scaleObserver = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
        handleScaleChange()
        break
      }
    }
  })
  setTimeout(() => {
    const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
    if (wrapper) {
      scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
    }
  }, 1000)

  return () => {
    window.removeEventListener('resize', handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', handleScaleChange)
    scaleObserver.disconnect()
    removeTooltip()
  }
})
</script>

<template>
  <div class="pdf">
    <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
  </div>
</template>

<style>
.pdf {
  width: 80%;
  height: 80vh;
  margin: 0 auto;
}
.boxCenter {
  text-align: center;
  margin-bottom: 20px;
}
.highlight {
  position: absolute;
  z-index: 5;
  border-radius: 2px;
  transition: opacity 0.2s ease;
  pointer-events: auto !important;
}
.highlight:hover {
  opacity: 0.8;
  border: 1px solid #ffffffb3;
}
.highlight.important {
  background-color: #ffa50066;
}
.highlight.todo {
  background-color: #0080004d;
}
.highlight.question {
  background-color: #0000ff4d;
}
.highlight-tooltip {
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-size: 14px;
  border-radius: 4px;
  pointer-events: none;
  white-space: pre-wrap;
  z-index: 9999;
  max-width: 300px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  position: fixed;
}
</style>
vue
<script setup>
import { VPdfViewer } from '@vue-pdf-viewer/viewer'
import { ref, onMounted } from 'vue'
    
const samplePDF = 'your-pdf-file.pdf'

const pageHighlights = [
  {
    left: 220,
    bottom: 500,
    right: 380,
    top: 550,
    type: 'important',
    note: 'Critical requirement—do not skip.'
  },
  {
    left: 100,
    bottom: 300,
    right: 260,
    top: 360,
    type: 'todo',
    note: 'Verify numbers here before release.'
  },
  {
    left: 300,
    bottom: 700,
    right: 460,
    top: 760,
    type: 'question',
    note: 'Need clarification on this section.'
  }
]
let highlightsAdded = false
const tooltipRef = ref(null)

function findPageElemByPageIdx(index) {
  return document.querySelector(`[data-page-index="${index}"]`)
}

function createTooltip() {
  if (tooltipRef.value) {
    tooltipRef.value.remove()
  }
  const tooltip = document.createElement('div')
  tooltip.className = 'highlight-tooltip'
  document.body.appendChild(tooltip)
  tooltipRef.value = tooltip
  return tooltip
}

function positionTooltip(e) {
  if (!tooltipRef.value) return
  tooltipRef.value.style.left = `${e.clientX + 15}px`
  tooltipRef.value.style.top = `${e.clientY - 15}px`
}

function removeTooltip() {
  if (tooltipRef.value) {
    tooltipRef.value.remove()
    tooltipRef.value = null
  }
}

function addHighlight(bbox, pageElement, scale = 1) {
  const vh = pageElement.offsetHeight
  const x = bbox.left * scale
  const y = vh - bbox.top * scale
  const width = (bbox.right - bbox.left) * scale
  const height = (bbox.top - bbox.bottom) * scale

  const hl = document.createElement('div')
  hl.classList.add('highlight', bbox.type)
  Object.assign(hl.style, {
    left: `${x}px`,
    top: `${y}px`,
    width: `${width}px`,
    height: `${height}px`,
    borderRadius: '2px'
  })
  hl.style.pointerEvents = 'auto'
  hl.style.cursor = bbox.note ? 'help' : 'default'

  if (bbox.note) {
    hl.dataset.note = bbox.note
    hl.addEventListener('mouseenter', (e) => {
      const tooltip = createTooltip()
      tooltip.textContent = bbox.note
      positionTooltip(e)
    })
    hl.addEventListener('mousemove', positionTooltip)
    hl.addEventListener('mouseleave', removeTooltip)
  }
  pageElement.appendChild(hl)
}

function handlePDFLoaded() {
  const checkAndAddHighlights = () => {
    if (highlightsAdded) return
    const pageEl = findPageElemByPageIdx(0)
    if (!pageEl) {
      setTimeout(checkAndAddHighlights, 500)
      return
    }
    if (pageEl.querySelector('.highlight')) return
    const wrapper = pageEl.closest('.vpv-pages-inner-wrapper')
    if (!wrapper) return
    const scale = parseFloat(getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1')
    pageHighlights.forEach((bbox) => addHighlight(bbox, pageEl, scale))
    highlightsAdded = true
  }
  checkAndAddHighlights()
}

function handleScaleChange() {
  document.querySelectorAll('.highlight').forEach((el) => el.remove())
  removeTooltip()
  highlightsAdded = false
  setTimeout(() => handlePDFLoaded(), 300)
}

onMounted(() => {
  window.addEventListener('resize', handleScaleChange)
  document.addEventListener('pdfViewerScaleChanged', handleScaleChange)
  const scaleObserver = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
        handleScaleChange()
        break
      }
    }
  })
  setTimeout(() => {
    const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
    if (wrapper) {
      scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
    }
  }, 1000)

  return () => {
    window.removeEventListener('resize', handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', handleScaleChange)
    scaleObserver.disconnect()
    removeTooltip()
  }
})
</script>

<template>
  <div class="pdf">
    <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
  </div>
</template>

<style>
.pdf {
  width: 80%;
  height: 80vh;
  margin: 0 auto;
}
.boxCenter {
  text-align: center;
  margin-bottom: 20px;
}
.highlight {
  position: absolute;
  z-index: 5;
  border-radius: 2px;
  transition: opacity 0.2s ease;
  pointer-events: auto !important;
}
.highlight:hover {
  opacity: 0.8;
  border: 1px solid #ffffffb3;
}
.highlight.important {
  background-color: #ffa50066;
}
.highlight.todo {
  background-color: #0080004d;
}
.highlight.question {
  background-color: #0000ff4d;
}
.highlight-tooltip {
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-size: 14px;
  border-radius: 4px;
  pointer-events: none;
  white-space: pre-wrap;
  z-index: 9999;
  max-width: 300px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  position: fixed;
}
</style>
vue
<script lang="ts">
import { defineComponent } from 'vue'
import { VPdfViewer } from '@vue-pdf-viewer/viewer'

interface BBox {
  left: number
  bottom: number
  right: number
  top: number
  type: 'important' | 'todo' | 'question'
  note?: string
}

export default defineComponent({
  components: { VPdfViewer },
  data() {
    return {
      samplePDF: 'your-pdf-file.pdf',
      pageHighlights: [
        {
          left: 220,
          bottom: 500,
          right: 380,
          top: 550,
          type: 'important',
          note: 'Critical requirement—do not skip.'
        },
        {
          left: 100,
          bottom: 300,
          right: 260,
          top: 360,
          type: 'todo',
          note: 'Verify numbers here before release.'
        },
        {
          left: 300,
          bottom: 700,
          right: 460,
          top: 760,
          type: 'question',
          note: 'Need clarification on this section.'
        }
      ] as BBox[],
      highlightsAdded: false,
      tooltipRef: null as HTMLElement | null,
      scaleObserver: null as MutationObserver | null
    }
  },
  methods: {
    findPageElemByPageIdx(index: number): HTMLElement | null {
      return document.querySelector<HTMLElement>(`[data-page-index="${index}"]`)
    },
    addHighlight(bbox: BBox, pageElement: HTMLElement, scale = 1) {
      const vh = pageElement.offsetHeight
      const x = bbox.left * scale
      const y = vh - bbox.top * scale
      const width = (bbox.right - bbox.left) * scale
      const height = (bbox.top - bbox.bottom) * scale

      const hl = document.createElement('div')
      hl.classList.add('highlight', bbox.type)
      Object.assign(hl.style, {
        left: `${x}px`,
        top: `${y}px`,
        width: `${width}px`,
        height: `${height}px`,
        borderRadius: '2px'
      })
      hl.style.pointerEvents = 'auto'
      hl.style.cursor = bbox.note ? 'help' : 'default'

      if (bbox.note) {
        hl.dataset.note = bbox.note
        hl.addEventListener('mouseenter', (e) => {
          const tooltip = this.createTooltip()
          tooltip.textContent = bbox.note!
          this.positionTooltip(e as MouseEvent)
        })
        hl.addEventListener('mousemove', this.positionTooltip)
        hl.addEventListener('mouseleave', this.removeTooltip)
      }

      pageElement.appendChild(hl)
    },

    createTooltip(): HTMLElement {
      if (this.tooltipRef) {
        this.tooltipRef.remove()
      }
      const tooltip = document.createElement('div')
      tooltip.className = 'highlight-tooltip'
      document.body.appendChild(tooltip)
      this.tooltipRef = tooltip
      return tooltip
    },

    positionTooltip(e: MouseEvent) {
      if (!this.tooltipRef) return
      this.tooltipRef.style.left = `${e.clientX + 15}px`
      this.tooltipRef.style.top = `${e.clientY - 15}px`
    },

    removeTooltip() {
      if (this.tooltipRef) {
        this.tooltipRef.remove()
        this.tooltipRef = null
      }
    },
    handlePDFLoaded() {
      const checkAndAddHighlights = () => {
        if (this.highlightsAdded) return

        const pageEl = this.findPageElemByPageIdx(0)
        if (!pageEl) {
          setTimeout(checkAndAddHighlights, 500)
          return
        }
        if (pageEl.querySelector('.highlight')) return

        const wrapper = pageEl.closest('.vpv-pages-inner-wrapper') as HTMLElement | null
        if (!wrapper) return

        const scale = parseFloat(
          getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
        )
        this.pageHighlights.forEach((bbox) => this.addHighlight(bbox, pageEl, scale))
        this.highlightsAdded = true
      }

      checkAndAddHighlights()
    },
    handleScaleChange() {
      document.querySelectorAll('.highlight').forEach((el) => el.remove())
      this.highlightsAdded = false
      setTimeout(() => this.handlePDFLoaded(), 300)
    }
  },
  mounted() {
    window.addEventListener('resize', this.handleScaleChange)
    document.addEventListener('pdfViewerScaleChanged', this.handleScaleChange)

    this.scaleObserver = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
          this.handleScaleChange()
          break
        }
      }
    })

    setTimeout(() => {
      const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
      if (wrapper && this.scaleObserver) {
        this.scaleObserver.observe(wrapper, {
          attributes: true,
          attributeFilter: ['style']
        })
      }
    }, 1000)
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', this.handleScaleChange)
    this.scaleObserver?.disconnect()
  }
})
</script>

<template>
  <h1 class="boxCenter">PDF Viewer with Annotations</h1>
  <div class="pdf">
    <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
  </div>
</template>

<style>
.pdf {
  width: 80%;
  height: 80vh;
  margin: 0 auto;
}
.boxCenter {
  text-align: center;
  margin-bottom: 20px;
}
.highlight {
  position: absolute;
  z-index: 5;
  border-radius: 2px;
  transition: opacity 0.2s ease;
  pointer-events: auto !important;
}
.highlight:hover {
  opacity: 0.8;
  border: 1px solid #ffffffb3;
}
.highlight.important {
  background-color: #ffa50066;
}
.highlight.todo {
  background-color: #0080004d;
}
.highlight.question {
  background-color: #0000ff4d;
}
.highlight-tooltip {
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-size: 14px;
  border-radius: 4px;
  pointer-events: none;
  white-space: pre-wrap;
  z-index: 9999;
  max-width: 300px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  position: fixed;
}
</style>
vue
<script>
import { VPdfViewer } from '@vue-pdf-viewer/viewer'

export default {
  components: {
    VPdfViewer
  },
  data() {
    return {
      samplePDF: 'your-pdf-file.pdf',
      pageHighlights: [
        {
          left: 220,
          bottom: 500,
          right: 380,
          top: 550,
          type: 'important',
          note: 'Critical requirement—do not skip.'
        },
        {
          left: 100,
          bottom: 300,
          right: 260,
          top: 360,
          type: 'todo',
          note: 'Verify numbers here before release.'
        },
        {
          left: 300,
          bottom: 700,
          right: 460,
          top: 760,
          type: 'question',
          note: 'Need clarification on this section.'
        }
      ],
      highlightsAdded: false,
      scaleObserver: null,
      tooltipRef: null
    }
  },
  methods: {
    findPageElemByPageIdx(index) {
      return document.querySelector(`[data-page-index="${index}"]`)
    },

    addHighlight(bbox, pageElement, scale = 1) {
      const vh = pageElement.offsetHeight
      const x = bbox.left * scale
      const y = vh - bbox.top * scale
      const width = (bbox.right - bbox.left) * scale
      const height = (bbox.top - bbox.bottom) * scale

      const hl = document.createElement('div')
      hl.classList.add('highlight', bbox.type)
      Object.assign(hl.style, {
        left: `${x}px`,
        top: `${y}px`,
        width: `${width}px`,
        height: `${height}px`,
        borderRadius: '2px'
      })
      hl.style.pointerEvents = 'auto'
      hl.style.cursor = bbox.note ? 'help' : 'default'

      if (bbox.note) {
        hl.dataset.note = bbox.note
        hl.addEventListener('mouseenter', (e) => {
          const tooltip = this.createTooltip()
          tooltip.textContent = bbox.note
          this.positionTooltip(e)
        })
        hl.addEventListener('mousemove', this.positionTooltip)
        hl.addEventListener('mouseleave', this.removeTooltip)
      }
      pageElement.appendChild(hl)
    },

    createTooltip() {
      if (this.tooltipRef) {
        this.tooltipRef.remove()
      }
      const tooltip = document.createElement('div')
      tooltip.className = 'highlight-tooltip'
      document.body.appendChild(tooltip)
      this.tooltipRef = tooltip
      return tooltip
    },

    positionTooltip(e) {
      if (!this.tooltipRef) return
      this.tooltipRef.style.left = `${e.clientX + 15}px`
      this.tooltipRef.style.top = `${e.clientY - 15}px`
    },

    removeTooltip() {
      if (this.tooltipRef) {
        this.tooltipRef.remove()
        this.tooltipRef = null
      }
    },

    handlePDFLoaded() {
      const checkAndAddHighlights = () => {
        if (this.highlightsAdded) return

        const pageEl = this.findPageElemByPageIdx(0)
        if (!pageEl) {
          setTimeout(checkAndAddHighlights, 500)
          return
        }
        if (pageEl.querySelector('.highlight')) return

        const wrapper = pageEl.closest('.vpv-pages-inner-wrapper')
        if (!wrapper) return

        const scale = parseFloat(
          getComputedStyle(wrapper).getPropertyValue('--scale-factor') || '1'
        )
        this.pageHighlights.forEach((bbox) => this.addHighlight(bbox, pageEl, scale))
        this.highlightsAdded = true
      }
      checkAndAddHighlights()
    },

    handleScaleChange() {
      document.querySelectorAll('.highlight').forEach((el) => el.remove())
      this.highlightsAdded = false
      setTimeout(() => this.handlePDFLoaded(), 300)
    }
  },

  mounted() {
    window.addEventListener('resize', this.handleScaleChange)
    document.addEventListener('pdfViewerScaleChanged', this.handleScaleChange)

    this.scaleObserver = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style' && m.target.nodeName === 'DIV') {
          this.handleScaleChange()
          break
        }
      }
    })

    setTimeout(() => {
      const wrapper = document.querySelector('.vpv-pages-inner-wrapper')
      if (wrapper) {
        this.scaleObserver.observe(wrapper, { attributes: true, attributeFilter: ['style'] })
      }
    }, 1000)
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.handleScaleChange)
    document.removeEventListener('pdfViewerScaleChanged', this.handleScaleChange)
    this.scaleObserver.disconnect()
  }
}
</script>

<template>
  <div class="pdf">
    <VPdfViewer :src="samplePDF" @loaded="handlePDFLoaded" />
  </div>
</template>

<style>
.pdf {
  width: 80%;
  height: 80vh;
  margin: 0 auto;
}
.boxCenter {
  text-align: center;
  margin-bottom: 20px;
}
.highlight {
  position: absolute;
  z-index: 5;
  border-radius: 2px;
  transition: opacity 0.2s ease;
  pointer-events: auto !important;
}
.highlight:hover {
  opacity: 0.8;
  border: 1px solid #ffffffb3;
}
.highlight.important {
  background-color: #ffa50066;
}
.highlight.todo {
  background-color: #0080004d;
}
.highlight.question {
  background-color: #0000ff4d;
}
.highlight-tooltip {
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-size: 14px;
  border-radius: 4px;
  pointer-events: none;
  white-space: pre-wrap;
  z-index: 9999;
  max-width: 300px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  position: fixed;
}
</style>