Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-lamps-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@embedpdf/plugin-capture': patch
---

Add reactive state for marquee capture mode
44 changes: 36 additions & 8 deletions examples/vue-tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,50 @@
"version": "0.0.0",
"type": "module",
"license": "MIT",
"exports": {
"./*": "./dist/examples/*.js"
},
"typesVersions": {
"*": {
"*": [
"./dist/examples/*.d.ts"
]
}
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "pnpm build:app && pnpm build:examples",
"build:app": "vite build",
"build:examples": "vite --config vite.lib.config.ts build",
"preview": "vite preview",
"lint": "eslint . --ext ts,vue --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@embedpdf/core": "workspace:*",
"@embedpdf/engines": "workspace:*",
"@embedpdf/models": "workspace:*",
"@embedpdf/plugin-annotation": "workspace:*",
"@embedpdf/plugin-capture": "workspace:*",
"@embedpdf/plugin-export": "workspace:*",
"@embedpdf/plugin-fullscreen": "workspace:*",
"@embedpdf/plugin-history": "workspace:*",
"@embedpdf/plugin-interaction-manager": "workspace:*",
"@embedpdf/plugin-loader": "workspace:*",
"@embedpdf/plugin-viewport": "workspace:*",
"@embedpdf/plugin-scroll": "workspace:*",
"@embedpdf/plugin-pan": "workspace:*",
"@embedpdf/plugin-print": "workspace:*",
"@embedpdf/plugin-redaction": "workspace:*",
"@embedpdf/plugin-render": "workspace:*",
"@embedpdf/plugin-tiling": "workspace:*",
"@embedpdf/plugin-interaction-manager": "workspace:*",
"@embedpdf/plugin-selection": "workspace:*",
"@embedpdf/plugin-rotate": "workspace:*",
"@embedpdf/plugin-fullscreen": "workspace:*",
"@embedpdf/plugin-scroll": "workspace:*",
"@embedpdf/plugin-search": "workspace:*",
"@embedpdf/plugin-selection": "workspace:*",
"@embedpdf/plugin-spread": "workspace:*",
"@embedpdf/plugin-thumbnail": "workspace:*",
"@embedpdf/plugin-tiling": "workspace:*",
"@embedpdf/plugin-viewport": "workspace:*",
"@embedpdf/plugin-zoom": "workspace:*",
"vue": "^3.5.0"
},
Expand All @@ -33,8 +58,11 @@
"@vitejs/plugin-vue": "^5.0.4",
"eslint": "^9.17.0",
"eslint-plugin-vue": "^9.25.0",
"glob": "^10.4.5",
"tailwindcss": "^4.1.11",
"typescript": "^5.7.3",
"vite": "^6.3.5"
"vite": "^6.3.5",
"vite-plugin-dts": "^4.5.4",
"vue-tsc": "^3.0.1"
}
}
106 changes: 106 additions & 0 deletions examples/vue-tailwind/src/examples/annotation-example-content.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Viewport } from '@embedpdf/plugin-viewport/vue';
import { Scroller } from '@embedpdf/plugin-scroll/vue';
import { RenderLayer } from '@embedpdf/plugin-render/vue';
import { AnnotationLayer, useAnnotationCapability } from '@embedpdf/plugin-annotation/vue';
import { PagePointerProvider } from '@embedpdf/plugin-interaction-manager/vue';
import { SelectionLayer } from '@embedpdf/plugin-selection/vue';

const activeTool = ref<string | null>(null);
const canDelete = ref(false);

const { provides: annotationApi } = useAnnotationCapability();

const tools = [
{ id: 'stampCheckmark', name: 'Checkmark (stamp)' },
{ id: 'stampCross', name: 'Cross (stamp)' },
{ id: 'ink', name: 'Pen' },
{ id: 'square', name: 'Square' },
{ id: 'highlight', name: 'Highlight' },
];

let unsubscribeToolChange: (() => void) | undefined;
let unsubscribeStateChange: (() => void) | undefined;

onMounted(() => {
if (!annotationApi.value) return;

unsubscribeToolChange = annotationApi.value.onActiveToolChange((tool) => {
activeTool.value = tool?.id ?? null;
});

unsubscribeStateChange = annotationApi.value.onStateChange((state) => {
canDelete.value = !!state.selectedUid;
});
});

onUnmounted(() => {
unsubscribeToolChange?.();
unsubscribeStateChange?.();
});

const handleToolClick = (toolId: string) => {
annotationApi.value?.setActiveTool(activeTool.value === toolId ? null : toolId);
};

const handleDelete = () => {
const selection = annotationApi.value?.getSelectedAnnotation();
if (selection) {
annotationApi.value?.deleteAnnotation(selection.object.pageIndex, selection.object.id);
}
};
</script>

<template>
<div style="height: 600px; display: flex; flex-direction: column; user-select: none">
<div
class="mb-4 mt-4 flex flex-wrap items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-sm"
>
<button
v-for="tool in tools"
:key="tool.id"
@click="handleToolClick(tool.id)"
:class="[
'rounded-md px-3 py-1 text-sm font-medium transition-colors',
activeTool === tool.id ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200',
]"
>
{{ tool.name }}
</button>
<div class="h-6 w-px bg-gray-200"></div>
<button
@click="handleDelete"
:disabled="!canDelete"
class="rounded-md bg-red-500 px-3 py-1 text-sm font-medium text-white transition-colors hover:bg-red-600 disabled:cursor-not-allowed disabled:bg-red-300"
>
Delete Selected
</button>
</div>
<div class="flex-grow" style="position: relative">
<Viewport style="width: 100%; height: 100%; position: absolute; background-color: #f1f3f5">
<Scroller>
<template #default="{ page }">
<PagePointerProvider
:page-index="page.pageIndex"
:page-width="page.width"
:page-height="page.height"
:rotation="page.rotation"
:scale="page.scale"
>
<RenderLayer :page-index="page.pageIndex" style="pointer-events: none" />
<SelectionLayer :page-index="page.pageIndex" :scale="page.scale" />
<AnnotationLayer
:page-index="page.pageIndex"
:scale="page.scale"
:page-width="page.width"
:page-height="page.height"
:rotation="page.rotation"
/>
</PagePointerProvider>
</template>
</Scroller>
</Viewport>
</div>
</div>
</template>
2 changes: 2 additions & 0 deletions examples/vue-tailwind/src/examples/annotation-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Component from './annotation-example.vue';
export default Component;
84 changes: 84 additions & 0 deletions examples/vue-tailwind/src/examples/annotation-example.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { usePdfiumEngine } from '@embedpdf/engines/vue';
import { EmbedPDF } from '@embedpdf/core/vue';
import { createPluginRegistration } from '@embedpdf/core';
import { LoaderPluginPackage } from '@embedpdf/plugin-loader/vue';
import { ViewportPluginPackage } from '@embedpdf/plugin-viewport/vue';
import { ScrollPluginPackage } from '@embedpdf/plugin-scroll/vue';
import { RenderPluginPackage } from '@embedpdf/plugin-render/vue';
import {
AnnotationPlugin,
AnnotationPluginPackage,
AnnotationTool,
} from '@embedpdf/plugin-annotation/vue';
import { InteractionManagerPluginPackage } from '@embedpdf/plugin-interaction-manager/vue';
import { SelectionPluginPackage } from '@embedpdf/plugin-selection/vue';
import { HistoryPluginPackage } from '@embedpdf/plugin-history/vue';
import type { PluginRegistry } from '@embedpdf/core';
import AnnotationExampleContent from './annotation-example-content.vue';
import { PdfAnnotationSubtype, PdfStampAnnoObject } from '@embedpdf/models';

const { engine, isLoading } = usePdfiumEngine();

const plugins = [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
type: 'url',
pdfFile: {
id: 'example-pdf',
url: 'https://snippet.embedpdf.com/ebook.pdf',
},
},
}),
createPluginRegistration(ViewportPluginPackage),
createPluginRegistration(ScrollPluginPackage),
createPluginRegistration(RenderPluginPackage),
createPluginRegistration(InteractionManagerPluginPackage),
createPluginRegistration(SelectionPluginPackage),
createPluginRegistration(HistoryPluginPackage),
createPluginRegistration(AnnotationPluginPackage, {
annotationAuthor: 'EmbedPDF User',
}),
];

const handleInitialized = async (registry: PluginRegistry) => {
const annotation = registry.getPlugin<AnnotationPlugin>('annotation')?.provides();

annotation?.addTool<AnnotationTool<PdfStampAnnoObject>>({
id: 'stampCheckmark',
name: 'Checkmark',
interaction: {
exclusive: false,
cursor: 'crosshair',
},
matchScore: () => 0,
defaults: {
type: PdfAnnotationSubtype.STAMP,
imageSrc: '/circle-checkmark.png',
imageSize: { width: 30, height: 30 },
},
});

annotation?.addTool<AnnotationTool<PdfStampAnnoObject>>({
id: 'stampCross',
name: 'Cross',
interaction: {
exclusive: false,
cursor: 'crosshair',
},
matchScore: () => 0,
defaults: {
type: PdfAnnotationSubtype.STAMP,
imageSrc: '/circle-cross.png',
imageSize: { width: 30, height: 30 },
},
});
};
</script>

<template>
<div v-if="isLoading || !engine">Loading PDF Engine...</div>
<EmbedPDF v-else :engine="engine" :plugins="plugins" @initialized="handleInitialized">
<AnnotationExampleContent />
</EmbedPDF>
</template>
102 changes: 102 additions & 0 deletions examples/vue-tailwind/src/examples/capture-example-content.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Viewport } from '@embedpdf/plugin-viewport/vue';
import { Scroller } from '@embedpdf/plugin-scroll/vue';
import { RenderLayer } from '@embedpdf/plugin-render/vue';
import { PagePointerProvider } from '@embedpdf/plugin-interaction-manager/vue';
import { MarqueeCapture, useCapture, type CaptureAreaEvent } from '@embedpdf/plugin-capture/vue';

// Now this is safe because we're guaranteed to be inside <EmbedPDF>
const { provides: capture, isMarqueeCaptureActive } = useCapture();
const captureResult = ref<CaptureAreaEvent | null>(null);
const imageUrl = ref<string | null>(null);

let unsubscribeCapture: (() => void) | undefined;

onMounted(() => {
if (!capture.value) return;
unsubscribeCapture = capture.value.onCaptureArea((result) => {
captureResult.value = result;
if (imageUrl.value) URL.revokeObjectURL(imageUrl.value);
imageUrl.value = URL.createObjectURL(result.blob);
});
});

onUnmounted(() => {
unsubscribeCapture?.();
if (imageUrl.value) URL.revokeObjectURL(imageUrl.value);
});

const downloadImage = () => {
if (!imageUrl.value || !captureResult.value) return;
const a = document.createElement('a');
a.href = imageUrl.value;
a.download = `capture-page-${captureResult.value.pageIndex + 1}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
</script>

<template>
<div style="height: 500px; display: flex; flex-direction: column">
<div
class="mb-4 mt-4 flex items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-sm"
>
<button
@click="capture?.toggleMarqueeCapture()"
:class="[
'rounded-md px-3 py-1 text-sm font-medium transition-colors',
isMarqueeCaptureActive ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200',
]"
>
{{ isMarqueeCaptureActive ? 'Cancel Capture' : 'Capture Area' }}
</button>
</div>
<div class="flex-grow" style="position: relative; overflow: hidden">
<Viewport
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #f1f3f5"
>
<Scroller>
<template #default="{ page }">
<PagePointerProvider
:page-index="page.pageIndex"
:page-width="page.width"
:page-height="page.height"
:rotation="page.rotation"
:scale="page.scale"
>
<RenderLayer :page-index="page.pageIndex" />
<MarqueeCapture :page-index="page.pageIndex" :scale="page.scale" />
</PagePointerProvider>
</template>
</Scroller>
</Viewport>
</div>
</div>
<div
v-if="!captureResult || !imageUrl"
class="mt-4 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-4 text-center"
>
<p class="text-sm text-gray-500">
Click "Capture Area" and drag a rectangle on the PDF to create a snapshot.
</p>
</div>
<div v-else class="mt-4 rounded-lg border border-gray-300 bg-white p-4 shadow-sm">
<h3 class="text-md font-medium text-gray-800">Capture Result</h3>
<p class="text-sm text-gray-500">
Captured from page {{ captureResult.pageIndex + 1 }} at {{ captureResult.scale }}x resolution.
</p>
<img
:src="imageUrl"
alt="Captured area from PDF"
class="mt-2 max-w-full rounded border border-gray-200"
/>
<button
@click="downloadImage"
class="mt-3 rounded-md bg-blue-500 px-3 py-1 text-sm font-medium text-white transition-colors hover:bg-blue-600"
>
Download Image
</button>
</div>
</template>
2 changes: 2 additions & 0 deletions examples/vue-tailwind/src/examples/capture-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Component from './capture-example.vue';
export default Component;
Loading