GitHub アドレス:
連絡先メール:
機能概要(pdf.jsプラグイン使用)
1.複数ページまたは単一ページのPDFをプレビューできます
2.PDFをズームイン、ズームアウト、ドラッグ操作できます
3.PDFをダウンロードできます
4.PDF内のテキストを取得できます
デモリンク:デモリンク
PDFコンポテンスの作成
<template>
<div class="tw-container tw-mx-auto tw-p-4">
<div class="tw-flex tw-justify-between tw-mb-4">
<div>
<v-btn @click="prevPage" class="tw-btn" :disabled="isAllPages">
{{ $t('views.pdf.prevPage') }}
</v-btn>
<v-btn @click="nextPage" class="tw-btn" :disabled="isAllPages">
{{ $t('views.pdf.nextPage') }}
</v-btn>
<v-btn @click="toggleView" class="tw-btn">
{{
isAllPages
? $t('views.pdf.singlePageView')
: $t('views.pdf.allPagesView')
}}
</v-btn>
</div>
<div class="tw-flex tw-items-center">
<v-btn @click="zoomOut" :disabled="zoomOutDisabled" class="tw-btn">
{{ $t('views.pdf.zoomOut') }}
</v-btn>
<input
type="number"
v-model.number="zoomPercentage"
@change="updateZoom"
class="tw-mx-2 tw-text-center tw-w-20"
min="10"
step="1"
/>
<v-btn @click="zoomIn" :disabled="zoomInDisabled" class="tw-btn">
{{ $t('views.pdf.zoomIn') }}
</v-btn>
</div>
<div>
<v-btn @click="downloadPDF" class="tw-btn">{{
$t('views.pdf.download')
}}</v-btn>
<v-btn @click="reset" class="tw-btn tw-bg-red-500">{{
$t('views.pdf.reset')
}}</v-btn>
<v-btn @click="rotateLeft" class="tw-btn">{{
$t('views.pdf.rotateLeft')
}}</v-btn>
<v-btn @click="rotateRight" class="tw-btn">{{
$t('views.pdf.rotateRight')
}}</v-btn>
<v-btn @click="alertText" class="tw-btn">{{
$t('views.pdf.alertText')
}}</v-btn>
</div>
</div>
<div
ref="pdfContainer"
class="tw-relative tw-overflow-auto tw-border tw-border-gray-300"
@mousedown="startDrag"
@mouseup="stopDrag"
@mousemove="drag"
>
<canvas ref="pdfCanvas" class="tw-canvas"></canvas>
</div>
<div class="tw-text-center tw-mt-4">
<template v-if="isAllPages">
{{ $t('views.pdf.totalPages', { count: totalPages }) }}
</template>
<template v-else>
{{
$t('views.pdf.pageInfo', { current: currentPage, total: totalPages })
}}
</template>
</div>
<div ref="textLayer" class="tw-text-layer" v-show="false"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
import { useResizeObserver } from '@vueuse/core';
import { TextLayerBuilder } from 'pdfjs-dist/web/pdf_viewer.mjs';
import 'pdfjs-dist/web/pdf_viewer.css';
// PDF.jsのワーカーのパスを設定
GlobalWorkerOptions.workerSrc =
'../../../node_modules/pdfjs-dist/build/pdf.worker.mjs';
// コンポーネントのプロパティを定義
const props = defineProps({
pdfUrl: {
type: String,
default: '',
},
pdfName: {
type: String,
default: 'test.pdf',
},
initialZoom: {
type: Number,
default: 150,
},
initialRotation: {
type: Number,
default: 0,
},
});
const pdfUrl = ref(props.pdfUrl);
const pdfCanvas = ref<HTMLCanvasElement | null>(null);
const pdfContainer = ref<HTMLDivElement | null>(null);
const currentPage = ref(1);
const totalPages = ref(0);
const scale = ref(props.initialZoom / 100);
const zoomPercentage = ref(props.initialZoom);
const maxZoom = 4.0;
const minZoom = 0.1;
const rotation = ref(props.initialRotation);
const isAllPages = ref(false);
const initialScale = 1.5;
const initialZoomPercentage = 150;
const initialOffsetX = 0;
const initialOffsetY = 0;
const zoomInDisabled = computed(() => scale.value >= maxZoom);
const zoomOutDisabled = computed(() => scale.value <= minZoom);
let renderTask: Promise<void> | null = null;
let pendingRender = false;
let isDragging = false;
let startX = 0;
let startY = 0;
let offsetX = initialOffsetX;
let offsetY = initialOffsetY;
const textLayer = ref<HTMLDivElement | null>(null);
// PDFをレンダリングする関数
const renderPDF = async (pageNum?: number) => {
if (pdfCanvas.value && textLayer.value) {
if (renderTask) {
await renderTask;
}
if (pendingRender) {
return;
}
const context = pdfCanvas.value.getContext('2d');
if (context) {
context.clearRect(0, 0, pdfCanvas.value.width, pdfCanvas.value.height);
}
pendingRender = true;
const loadingTask = getDocument(pdfUrl.value);
const pdf = await loadingTask.promise;
totalPages.value = pdf.numPages;
if (isAllPages.value) {
let totalHeight = 0;
let maxWidth = 0;
for (let i = 1; i <= totalPages.value; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: scale.value });
totalHeight += viewport.height;
maxWidth = Math.max(maxWidth, viewport.width);
}
pdfCanvas.value.height = totalHeight * window.devicePixelRatio;
pdfCanvas.value.width = maxWidth * window.devicePixelRatio;
context?.scale(window.devicePixelRatio, window.devicePixelRatio);
for (let i = 1; i <= totalPages.value; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: scale.value });
const renderContext = {
canvasContext: context,
viewport: viewport,
};
renderTask = page.render(renderContext).promise;
await renderTask;
context.translate(0, viewport.height);
}
} else {
const page = await pdf.getPage(pageNum || 1);
const viewport = page.getViewport({ scale: scale.value });
pdfCanvas.value.height = viewport.height * window.devicePixelRatio;
pdfCanvas.value.width = viewport.width * window.devicePixelRatio;
context?.scale(window.devicePixelRatio, window.devicePixelRatio);
const renderContext = {
canvasContext: context,
viewport: viewport,
};
renderTask = page.render(renderContext).promise;
await renderTask;
}
// 以前のテキストレイヤーの内容をクリア
if (textLayer.value) {
textLayer.value.innerHTML = '';
}
if (isAllPages.value) {
for (let i = 1; i <= totalPages.value; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: scale.value });
const textLayerBuilder = new TextLayerBuilder({
pdfPage: page,
highlighter: null,
accessibilityManager: null,
enablePermissions: false,
});
textLayerBuilder.div = textLayer.value!;
await textLayerBuilder.render(viewport);
}
} else {
const page = await pdf.getPage(pageNum || 1);
const viewport = page.getViewport({ scale: scale.value });
const textLayerBuilder = new TextLayerBuilder({
pdfPage: page,
highlighter: null,
accessibilityManager: null,
enablePermissions: false,
});
textLayerBuilder.div = textLayer.value!;
await textLayerBuilder.render(viewport);
}
pendingRender = false;
centerCanvas();
}
};
// ズームインの処理
const zoomIn = () => {
if (scale.value < maxZoom) {
scale.value *= 1.1;
zoomPercentage.value = Math.round(scale.value * 100);
renderPDF(isAllPages.value ? undefined : currentPage.value);
}
};
// ズームアウトの処理
const zoomOut = () => {
if (scale.value > minZoom) {
scale.value /= 1.1;
zoomPercentage.value = Math.round(scale.value * 100);
renderPDF(isAllPages.value ? undefined : currentPage.value);
}
};
// ズームパーセンテージの更新処理
const updateZoom = () => {
if (zoomPercentage.value < 10) {
zoomPercentage.value = 10;
} else if (zoomPercentage.value > 400) {
zoomPercentage.value = 400;
}
scale.value = zoomPercentage.value / 100;
renderPDF(isAllPages.value ? undefined : currentPage.value);
};
// 前のページに移動する処理
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
renderPDF();
}
};
// 次のページに移動する処理
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
renderPDF();
}
};
// 単一ページ表示と全ページ表示の切り替え処理
const toggleView = () => {
isAllPages.value = !isAllPages.value;
currentPage.value = 1;
renderPDF();
};
// PDFのダウンロード処理
const downloadPDF = () => {
const link = document.createElement('a');
link.href = pdfUrl.value;
link.download = props.pdfName;
link.click();
};
// PDFのリセット処理
const reset = () => {
scale.value = initialScale;
zoomPercentage.value = initialZoomPercentage;
rotation.value = 0;
offsetX = initialOffsetX;
offsetY = initialOffsetY;
renderPDF(isAllPages.value ? undefined : currentPage.value);
};
// PDFの回転処理(左回転)
const rotateLeft = () => {
rotation.value -= 90;
if (rotation.value < 0) rotation.value = 270;
renderPDF(isAllPages.value ? undefined : currentPage.value);
};
// PDFの回転処理(右回転)
const rotateRight = () => {
rotation.value += 90;
if (rotation.value >= 360) rotation.value = 0;
renderPDF(isAllPages.value ? undefined : currentPage.value);
};
// テキストのアラート処理
const alertText = () => {
alert('テキスト機能はまだ未実装です。');
};
// ドラッグ操作の開始
const startDrag = (event: MouseEvent) => {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
};
// ドラッグ操作の停止
const stopDrag = () => {
isDragging = false;
};
// ドラッグ操作の処理
const drag = (event: MouseEvent) => {
if (isDragging) {
const dx = event.clientX - startX;
const dy = event.clientY - startY;
offsetX += dx;
offsetY += dy;
pdfContainer.value!.scrollLeft -= dx;
pdfContainer.value!.scrollTop -= dy;
startX = event.clientX;
startY = event.clientY;
}
};
// キャンバスの中央揃え
const centerCanvas = () => {
if (pdfCanvas.value && pdfContainer.value) {
const canvasWidth = pdfCanvas.value.width;
const containerWidth = pdfContainer.value.clientWidth;
const canvasHeight = pdfCanvas.value.height;
const containerHeight = pdfContainer.value.clientHeight;
const left = (containerWidth - canvasWidth) / 2;
const top = (containerHeight - canvasHeight) / 2;
pdfCanvas.value.style.left = `${left}px`;
pdfCanvas.value.style.top = `${top}px`;
}
};
// サイズ変更オブザーバーの設定
onMounted(() => {
if (pdfContainer.value) {
const { stop } = useResizeObserver(pdfContainer, () => {
renderPDF(isAllPages.value ? undefined : currentPage.value);
});
onUnmounted(stop);
}
});
// 初回レンダリング
onMounted(() => {
renderPDF(isAllPages.value ? undefined : currentPage.value);
});
</script>
<style scoped>
.tw-container {
max-width: 100%;
margin: auto;
}
.tw-btn {
margin: 0 4px;
padding: 8px 16px;
font-size: 14px;
}
.tw-canvas {
border: 1px solid #ddd;
background-color: #fff;
}
.tw-text-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.tw-mx-2 {
margin-left: 8px;
margin-right: 8px;
}
.tw-text-center {
text-align: center;
}
.tw-w-20 {
width: 80px;
}
.tw-bg-red-500 {
background-color: #f56565;
}
</style>
まとめ: pdf.jsは市場で最も人気のあるPDF関連のプラグインであり、ユーザー層が広く、機能が充実しており、無料で利用できます。これが私がpdf.jsを選んだ理由です。これにより、プログラムがより柔軟になり、拡張が容易になります。