Adding Custom Bounding Boxes
There are times when you need to add custom bounding boxes programmatically. An example is when you need to render bounding box (BBox) for an OCR application or a RAG application.
Vue PDF Viewer provides the pageOverlay slot to achieve this objective. This tutorial will guide you on how to draw interactive bounding boxes that highlight specific regions on PDF pages and scale properly with the PDF zoom level.
INFO
The pageOverlay slot is only available for Organization license users.
Here is a technical summary of what the example will consist of:
- VPdfViewer: Display the PDF document
- pageOverlay Slot: Render custom bounding box overlays on each page
- Scale-Responsive Positioning: Ensure bounding boxes scale and position correctly with PDF zoom
- Interactive Features: Add hover effects and click handlers to bounding boxes
Breakdown step by step
1. Setting Up the PDF Viewer
The VPdfViewer component is responsible for rendering the PDF file inside your Vue application. To initialize and load the viewer, you need to provide a valid PDF file source through the src prop.
Here is how to create a VPdfViewer component with the pageOverlay slot:
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale }">
<!-- Bounding boxes will go here -->
</template>
</VPdfViewer>2. Defining Bounding Box Data
First, we need to define the bounding boxes we want to display. Each bounding box should contain position and size information, along with the page it belongs to.
Here is how to define bounding box data:
<script setup lang="ts">
import { ref, type StyleValue } from 'vue';
interface BoundingBox {
pageIndex: number;
x: number; // X position (percentage of page width)
y: number; // Y position (percentage of page height)
width: number; // Width (percentage of page width)
height: number; // Height (percentage of page height)
label?: string; // Optional label
color?: string; // Optional color
}
const boundingBoxes = ref<BoundingBox[]>([
{
pageIndex: 0,
x: 10, // 10% from left
y: 16, // 16% from top
width: 80, // 80% of page width
height: 20, // 20% of page height
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)'
},
{
pageIndex: 0,
x: 50,
y: 40,
width: 42,
height: 24,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)'
},
]);
const getPageBoundingBoxes = (pageIndex: number) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex)
}
const getBoundingBoxStyle = (box: BoundingBox, scale: number) => {
return {
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: `${2 * scale}px solid ${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
} as StyleValue
}
</script>
<template>
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
:key="index"
class="bounding-box"
:style="getBoundingBoxStyle(box, scale)"
>
</div>
</template>
<style scoped>
.bounding-box {
position: absolute;
pointer-events: auto;
}
</style>
3. Implementing Scale-Responsive Bounding Boxes
To ensure bounding boxes scale properly with PDF zoom, we use percentages for positioning and sizing, then multiply pixel-based properties (like borders and font sizes) by the scale factor. This approach uses CSS custom properties (CSS variables) for cleaner code and better maintainability.
Here is how to create a helper function for scale-responsive bounding box styling:
<script setup lang="ts">
const getBoundingBoxStyle = (box: BoundingBox, scale: number) => {
return {
// Create darker border color by increasing opacity from 0.3 to 0.8
'--border-color': `${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
// Define border style with scaled width
'--border-style': `${2 * scale}px solid var(--border-color)`,
// Position and size using percentages (scale-independent)
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: 'var(--border-style)',
// Scale border radius with zoom level
borderRadius: `${4 * scale}px`,
pointerEvents: 'auto' // Enable mouse interactions
} as StyleValue
}
</script>
<template>
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
:key="index"
class="bounding-box"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
></div>
</template>
<style scoped>
.bounding-box {
/* Extract data attributes as CSS variables */
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
}
.bounding-box::before {
/* Display label above the box using ::before pseudo-element */
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale)); /* Position above with scaled offset */
left: calc(-0.125rem * var(--scale)); /* Slight left alignment */
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale)); /* Scale font with zoom */
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
/* Add borders to match the box */
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
</style>Key benefits of this approach:
- Border width and border radius scale proportionally with the PDF zoom level
- Labels maintain readability at any zoom level
- CSS variables reduce code duplication and improve maintainability
- Percentage-based positioning ensures boxes stay aligned with PDF content
4. Adding Interactive Features
Make bounding boxes interactive by adding hover effects and click handlers. This enables features like displaying tooltips, showing additional information, or triggering custom actions when users interact with specific regions of the PDF.
Here is how to add interactive features to bounding boxes:
<script setup lang="ts">
const handleBoxClick = (box: BoundingBox) => {
console.log("Clicked bounding box:", box);
// Add your custom logic here
};
</script>
<template #pageOverlay="{ pageIndex, scale }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
:key="index"
class="bounding-box"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@click="handleBoxClick(box)"
>
</div>
</template>
<style scoped>
/* ... (same styles as step 3) ... */
/* Add visual feedback on hover */
.bounding-box:hover::before {
font-weight: bold; /* Emphasize label on hover */
}
</style>5. Filtering Bounding Boxes by Page
To display only the bounding boxes that belong to a specific page, create a helper function that filters based on the pageIndex. This is essential when you have bounding boxes on multiple pages, as it ensures each page only displays its own boxes.
Here is how to filter bounding boxes by page:
<script setup lang="ts">
const boundingBoxes = ref<BoundingBox[]>([
{
pageIndex: 0, // First page
x: 10,
y: 16.5,
width: 80,
height: 20,
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)'
},
{
pageIndex: 0, // First page (multiple boxes can be on same page)
x: 50,
y: 42,
width: 42,
height: 22,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)'
},
{
pageIndex: 1, // Second page
x: 0,
y: 10,
width: 10,
height: 5,
label: 'Box#1',
color: 'rgba(0, 255, 0, 0.3)'
}
])
// Filter boxes for the current page being rendered
const getPageBoundingBoxes = (pageIndex: number) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex);
};
</script>
<template><!-- Same as previous --></template>
<style scoped>/* ... */</style>Complete Example
How Each Part Works Together
- VPdfViewer Component: This component displays the PDF file in the application.
- pageOverlay Slot: Provides access to
pageIndexandscaleprops for each page. - Bounding Box Data: Stores position, size, and styling information for each bounding box.
- Style Function: Calculates responsive styles based on the current scale.
- Filter Function: Returns only the bounding boxes for the current page.
- Event Handlers: Handle user interactions with bounding boxes.
Here is a complete example of how you can draw custom bounding boxes on PDF pages:
<script setup lang="ts">
import { ref, type StyleValue } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
// Define the structure for bounding box data
interface BoundingBox {
pageIndex: number;
x: number; // Percentage from left (0-100)
y: number; // Percentage from top (0-100)
width: number; // Percentage of page width (0-100)
height: number; // Percentage of page height (0-100)
label?: string;
color?: string; // RGBA color with opacity
}
const pdfFileSource =
"https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
// Define bounding boxes for different pages
const boundingBoxes = ref<BoundingBox[]>([
{
pageIndex: 0, // First page
x: 10,
y: 16.5,
width: 80,
height: 20,
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)' // Red with 30% opacity
},
{
pageIndex: 0, // First page (multiple boxes per page)
x: 50,
y: 42,
width: 42,
height: 22,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)' // Blue with 30% opacity
},
{
pageIndex: 1, // Second page
x: 0,
y: 10,
width: 10,
height: 5,
label: 'Box#1',
color: 'rgba(0, 255, 0, 0.3)' // Green with 30% opacity
}
]);
// Filter boxes for the current page
const getPageBoundingBoxes = (pageIndex: number) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex);
};
// Generate inline styles with scale-responsive properties
const getBoundingBoxStyle = (box: BoundingBox, scale: number) => {
return {
// Create darker border by increasing opacity
'--border-color': `${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
'--border-style': `${2 * scale}px solid var(--border-color)`,
// Use percentages for position/size (scale-independent)
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: 'var(--border-style)',
borderRadius: `${4 * scale}px`, // Scale with zoom
pointerEvents: 'auto'
} as StyleValue
}
// Handle click events on bounding boxes
const handleBoxClick = (box: BoundingBox) => {
console.log("Clicked bounding box:", box);
// Add your custom logic here
};
</script>
<template>
<div :style="{ width: '1028px', height: '700px' }">
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
class="bounding-box"
:key="index"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@click="handleBoxClick(box)"
>
</div>
</template>
</VPdfViewer>
</div>
</template>
<style scoped>
.bounding-box {
/* Extract data attributes as CSS variables */
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
border-top-left-radius: 0px !important; /* Remove top-left radius for label */
transition: all 0.2s ease; /* Smooth hover transitions */
}
.bounding-box::before {
/* Display label above the box */
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale)); /* Position above with scaled offset */
left: calc(-0.125rem * var(--scale));
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale)); /* Scale font with zoom */
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
/* Match box borders */
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
.bounding-box:hover {
filter: brightness(1.2); /* Brighten on hover */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); /* Add shadow */
font-weight: bold;
}
</style><script setup>
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { ref } from "vue";
const pdfFileSource =
"https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
// Define bounding boxes for different pages
const boundingBoxes = ref([
{
pageIndex: 0, // First page
x: 10,
y: 16.5,
width: 80,
height: 20,
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)'
},
{
pageIndex: 0, // First page (multiple boxes per page)
x: 50,
y: 42,
width: 42,
height: 22,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)'
},
{
pageIndex: 1, // Second page
x: 0,
y: 10,
width: 10,
height: 5,
label: 'Box#1',
color: 'rgba(0, 255, 0, 0.3)'
}
]);
// Filter boxes for the current page
const getPageBoundingBoxes = (pageIndex) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex);
};
// Generate inline styles with scale-responsive properties
const getBoundingBoxStyle = (box, scale) => {
return {
// Create darker border by increasing opacity
'--border-color': `${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
'--border-style': `${2 * scale}px solid var(--border-color)`,
// Use percentages for position/size (scale-independent)
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: 'var(--border-style)',
borderRadius: `${4 * scale}px`, // Scale with zoom
pointerEvents: 'auto'
}
}
// Handle click events on bounding boxes
const handleBoxClick = (box) => {
console.log("Clicked bounding box:", box);
// Add your custom logic here
};
</script>
<template>
<div :style="{ width: '1028px', height: '700px' }">
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
class="bounding-box"
:key="index"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@click="handleBoxClick(box)"
>
</div>
</template>
</VPdfViewer>
</div>
</template>
<style scoped>
/* Same styles as Composition TS example */
.bounding-box {
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
border-top-left-radius: 0px !important;
transition: all 0.2s ease;
}
.bounding-box::before {
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale));
left: calc(-0.125rem * var(--scale));
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale));
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
.bounding-box:hover {
filter: brightness(1.2);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
font-weight: bold;
}
</style><script lang="ts">
import { defineComponent, ref, type StyleValue } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
// Define the structure for bounding box data
interface BoundingBox {
pageIndex: number;
x: number; // Percentage from left (0-100)
y: number; // Percentage from top (0-100)
width: number; // Percentage of page width (0-100)
height: number; // Percentage of page height (0-100)
label?: string;
color?: string; // RGBA color with opacity
}
export default defineComponent({
components: { VPdfViewer },
setup() {
const pdfFileSource =
"https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
// Define bounding boxes for different pages
const boundingBoxes = ref<BoundingBox[]>([
{
pageIndex: 0, // First page
x: 10,
y: 16.5,
width: 80,
height: 20,
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)'
},
{
pageIndex: 0, // First page (multiple boxes per page)
x: 50,
y: 42,
width: 42,
height: 22,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)'
},
{
pageIndex: 1, // Second page
x: 0,
y: 10,
width: 10,
height: 5,
label: 'Box#1',
color: 'rgba(0, 255, 0, 0.3)'
}
]);
// Filter boxes for the current page
const getPageBoundingBoxes = (pageIndex: number) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex);
};
// Generate inline styles with scale-responsive properties
const getBoundingBoxStyle = (box: BoundingBox, scale: number) => {
return {
// Create darker border by increasing opacity
'--border-color': `${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
'--border-style': `${2 * scale}px solid var(--border-color)`,
// Use percentages for position/size (scale-independent)
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: 'var(--border-style)',
borderRadius: `${4 * scale}px`, // Scale with zoom
pointerEvents: 'auto'
} as StyleValue
}
// Handle click events on bounding boxes
const handleBoxClick = (box: BoundingBox) => {
console.log("Clicked bounding box:", box);
// Add your custom logic here
};
return {
pdfFileSource,
boundingBoxes,
getPageBoundingBoxes,
getBoundingBoxStyle,
handleBoxClick,
};
},
});
</script>
<template>
<div :style="{ width: '1028px', height: '700px' }">
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
class="bounding-box"
:key="index"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@click="handleBoxClick(box)"
>
</div>
</template>
</VPdfViewer>
</div>
</template>
<style scoped>
.bounding-box {
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
border-top-left-radius: 0px !important;
transition: all 0.2s ease;
}
.bounding-box::before {
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale));
left: calc(-0.125rem * var(--scale));
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale));
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
.bounding-box:hover {
filter: brightness(1.2);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
font-weight: bold;
}
</style><script>
import { ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
export default {
components: { VPdfViewer },
setup() {
const pdfFileSource =
"https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
// Define bounding boxes for different pages
const boundingBoxes = ref([
{
pageIndex: 0, // First page
x: 10,
y: 16.5,
width: 80,
height: 20,
label: 'Box#1',
color: 'rgba(255, 0, 0, 0.3)'
},
{
pageIndex: 0, // First page (multiple boxes per page)
x: 50,
y: 42,
width: 42,
height: 22,
label: 'Box#2',
color: 'rgba(0, 0, 255, 0.3)'
},
{
pageIndex: 1, // Second page
x: 0,
y: 10,
width: 10,
height: 5,
label: 'Box#1',
color: 'rgba(0, 255, 0, 0.3)'
}
]);
// Filter boxes for the current page
const getPageBoundingBoxes = (pageIndex) => {
return boundingBoxes.value.filter((box) => box.pageIndex === pageIndex);
};
// Generate inline styles with scale-responsive properties
const getBoundingBoxStyle = (box, scale) => {
return {
// Create darker border by increasing opacity
'--border-color': `${box.color?.replace('0.3', '0.8') || 'rgba(255, 165, 0, 0.8)'}`,
'--border-style': `${2 * scale}px solid var(--border-color)`,
// Use percentages for position/size (scale-independent)
left: `${box.x}%`,
top: `${box.y}%`,
width: `${box.width}%`,
height: `${box.height}%`,
backgroundColor: box.color || 'rgba(255, 165, 0, 0.3)',
border: 'var(--border-style)',
borderRadius: `${4 * scale}px`, // Scale with zoom
pointerEvents: 'auto'
}
}
// Handle click events on bounding boxes
const handleBoxClick = (box) => {
console.log("Clicked bounding box:", box);
// Add your custom logic here
};
return {
pdfFileSource,
boundingBoxes,
getPageBoundingBoxes,
getBoundingBoxStyle,
handleBoxClick,
};
},
};
</script>
<template>
<div :style="{ width: '1028px', height: '700px' }">
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
class="bounding-box"
:key="index"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@click="handleBoxClick(box)"
>
</div>
</template>
</VPdfViewer>
</div>
</template>
<style scoped>
.bounding-box {
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
border-top-left-radius: 0px !important;
transition: all 0.2s ease;
}
.bounding-box::before {
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale));
left: calc(-0.125rem * var(--scale));
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale));
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
.bounding-box:hover {
filter: brightness(1.2);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
font-weight: bold;
}
</style>Advanced: Draggable Bounding Boxes
Important
The code example below builds upon the Complete Example section above. Before implementing draggable bounding boxes, ensure you have reviewed and understood the "How Each Part Works Together" section, as the advanced example relies on the same data structures, helper functions, and styling approach defined in the complete example. Copying only the draggable code without the complete example will not work.
You can create draggable bounding boxes by tracking mouse events and updating the bounding box positions dynamically.
Here is a basic implementation of draggable bounding boxes:
<script setup lang="ts">
// ...
// NOTE: Include the complete example code here:
// - BoundingBox interface
// - pdfFileSource
// - boundingBoxes ref
// - getPageBoundingBoxes() function
// - getBoundingBoxStyle() function
// ...
const isDragging = ref(false);
const draggedBoxIndex = ref<number | null>(null);
const dragStartPos = ref({ x: 0, y: 0 });
const handleMouseDown = (event: MouseEvent, index: number, box: BoundingBox) => {
isDragging.value = true;
draggedBoxIndex.value = index;
dragStartPos.value = { x: event.clientX, y: event.clientY };
};
const handleMouseMove = (event: MouseEvent, scale: number, pageElement: HTMLElement) => {
if (!isDragging.value || draggedBoxIndex.value === null) return;
const { width: pageWidth, height: pageHeight } = pageElement.getBoundingClientRect()
const deltaX = (event.clientX - dragStartPos.value.x) / (pageWidth * scale / 100);
const deltaY = (event.clientY - dragStartPos.value.y) / (pageHeight * scale / 100);
const box = boundingBoxes.value[draggedBoxIndex.value] as BoundingBox;
box.x = Math.max(0, Math.min(100 - box.width, box.x + deltaX));
box.y = Math.max(0, Math.min(100 - box.height, box.y + deltaY));
dragStartPos.value = { x: event.clientX, y: event.clientY };
};
const handleMouseUp = () => {
isDragging.value = false;
draggedBoxIndex.value = null;
};
</script>
<template>
<div :style="{ width: '1028px', height: '700px' }">
<VPdfViewer :src="pdfFileSource">
<template #pageOverlay="{ pageIndex, scale, pageElement }">
<div
v-for="(box, index) in getPageBoundingBoxes(pageIndex)"
:key="index"
class="bounding-box"
:style="getBoundingBoxStyle(box, scale)"
:data-label="box.label"
:data-scale="scale"
@mousedown="handleMouseDown($event, index, box)"
@mousemove="handleMouseMove($event, scale, pageElement as HTMLElement)"
@mouseup="handleMouseUp"
>
</div>
</template>
</VPdfViewer>
</div>
</template>
<style scoped>
.bounding-box {
--label: attr(data-label);
--scale: attr(data-scale type(<number>));
position: absolute;
pointer-events: auto;
border-top-left-radius: 0px !important;
transition: all 0.2s ease;
cursor: move; /* Show move cursor for draggable boxes */
}
.bounding-box::before {
content: var(--label);
position: absolute;
top: calc(-0.7rem * var(--scale));
left: calc(-0.125rem * var(--scale));
color: white;
background-color: var(--border-color);
font-size: calc(0.5rem * var(--scale));
line-height: calc(0.5rem * var(--scale));
padding: 0px 10px 0px;
border-left: var(--border-style);
border-right: var(--border-style);
border-top: var(--border-style);
}
.bounding-box:hover {
filter: brightness(1.2);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
</style>Use Cases
Document Analysis
- Highlight specific sections for review
- Mark regions of interest for data extraction
- Visualize document structure
Annotation and Markup
- Add visual markers to important areas
- Create interactive regions for tooltips or popups
- Build custom annotation tools
OCR and Text Extraction
- Visualize text recognition results
- Display confidence scores for detected text
- Show bounding boxes for extracted data
Form Field Detection
- Highlight detected form fields
- Show field types and validation status
- Create interactive field editors
Tips and Best Practices
Performance Optimization
- Keep the number of bounding boxes per page reasonable (< 50)
- Use CSS transforms for animations instead of recalculating positions
- Debounce mouse move events for draggable boxes
- Consider using
will-changeCSS property for frequently updated boxes
Positioning Accuracy
- Use percentage-based positioning for scale independence
- Store coordinates relative to the page dimensions
- Account for page rotation when calculating positions
- Test with different zoom levels to ensure accuracy
Visual Design
- Use semi-transparent backgrounds to avoid obscuring content
- Add clear visual feedback for hover and active states
- Choose contrasting colors for better visibility
- Keep labels concise and readable
Accessibility
- Add
pointer-events: autoto make boxes interactive - Provide keyboard navigation for interactive boxes
- Include ARIA labels for screen readers
- Ensure sufficient color contrast for labels