要在前端解析 PDF 文件并生成可编辑界面,我们可以使用 PDF.js 库来解析 PDF 内容,然后将其转换为可编辑的 HTML 元素。
主要特点和工作原理如下:
- PDF 解析:
使用 Mozilla 的 PDF.js 库解析 PDF 文件内容,提取文本信息。这是前端处理 PDF 最常用的库之一,功能强大且稳定。 - 用户界面:
简洁的上传区域,支持点击上传和拖拽上传
解析进度显示,提升用户体验
分页导航,方便浏览多页 PDF
编辑 / 预览模式切换 - 编辑功能:
将 PDF 文本转换为可编辑的 HTML 段落(使用 contenteditable 属性)
支持基本的文本编辑操作
保存编辑内容(前端暂存,实际应用中可发送到服务器) - 实现流程:
用户上传 PDF 文件
使用 PDF.js 加载并解析 PDF
将解析出的文本内容转换为可编辑的 HTML 元素
提供编辑工具和导航功能
支持保存更改和导出(导出功能在实际应用中需要额外库支持)
要注意的是,前端 PDF 编辑有一些局限性:
- 复杂的 PDF 布局(如多列、表格)可能无法完美转换
- 包含图片或复杂图形的 PDF 处理起来比较困难
- 前端生成 PDF 需要额外的库(如 jsPDF)支持
界面
代码
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF解析与编辑工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
neutral: '#6B7280',
light: '#F3F4F6',
dark: '#1F2937'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.transition-height {
transition: max-height 0.3s ease-out;
}
.editable-content [contenteditable="true"]:focus {
outline: 2px solid #3B82F6;
border-radius: 2px;
background-color: rgba(59, 130, 246, 0.05);
}
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fa fa-file-pdf-o text-red-500 text-2xl"></i>
<h1 class="text-xl font-bold text-dark">PDF解析与编辑工具</h1>
</div>
<div class="flex space-x-3">
<button id="saveBtn" class="bg-secondary hover:bg-green-600 text-white px-4 py-2 rounded-md flex items-center transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fa fa-save mr-2"></i>保存
</button>
<button id="downloadBtn" class="bg-primary hover:bg-blue-600 text-white px-4 py-2 rounded-md flex items-center transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fa fa-download mr-2"></i>导出PDF
</button>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<!-- 文件上传区域 -->
<section id="uploadSection" class="mb-8">
<div class="bg-white rounded-lg shadow-md p-8 text-center">
<label for="fileInput" class="cursor-pointer">
<div class="border-2 border-dashed border-neutral rounded-lg p-10 transition-colors duration-200 hover:border-primary">
<i class="fa fa-cloud-upload text-5xl text-primary mb-4"></i>
<h2 class="text-xl font-semibold mb-2">上传PDF文件</h2>
<p class="text-neutral mb-4">点击或拖拽文件到此处上传</p>
<p class="text-sm text-neutral">支持的格式: PDF</p>
<input id="fileInput" type="file" accept=".pdf" class="hidden">
</div>
</label>
<div id="fileInfo" class="mt-4 hidden">
<div class="flex items-center justify-center p-3 bg-light rounded-md">
<i class="fa fa-file-pdf-o text-red-500 mr-2"></i>
<span id="fileName" class="mr-2"></span>
<button id="removeFile" class="text-neutral hover:text-red-500 transition-colors">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
</section>
<!-- 解析进度 -->
<section id="progressSection" class="mb-8 hidden">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-lg font-semibold mb-4">正在解析PDF文件...</h2>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="progressBar" class="bg-primary h-2.5 rounded-full" style="width: 0%"></div>
</div>
<p id="progressText" class="text-sm text-neutral mt-2">准备中...</p>
</div>
</section>
<!-- 编辑区域 -->
<section id="editorSection" class="hidden">
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">PDF内容编辑</h2>
<div class="flex space-x-2">
<button id="editModeBtn" class="bg-primary hover:bg-blue-600 text-white px-3 py-1 rounded text-sm transition-colors">
<i class="fa fa-pencil mr-1"></i>编辑模式
</button>
<button id="previewModeBtn" class="bg-neutral hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition-colors">
<i class="fa fa-eye mr-1"></i>预览模式
</button>
</div>
</div>
<div id="pdfEditor" class="editable-content min-h-[500px]">
<!-- PDF内容将在这里显示 -->
</div>
</div>
</section>
<!-- 页面导航 -->
<section id="pageNavigation" class="flex justify-center mt-6 hidden">
<div class="flex items-center space-x-4">
<button id="prevPage" class="bg-white border border-neutral rounded-md px-3 py-1 hover:bg-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa fa-chevron-left"></i>
</button>
<div id="pageIndicator" class="text-neutral">第 1 页 / 共 0 页</div>
<button id="nextPage" class="bg-white border border-neutral rounded-md px-3 py-1 hover:bg-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</section>
</main>
<footer class="bg-dark text-white py-6 mt-12">
<div class="container mx-auto px-4 text-center">
<p>PDF解析与编辑工具 © 2025年7月16日</p>
<p class="text-sm text-gray-400 mt-1">使用PDF.js和Tailwind CSS构建</p>
</div>
</footer>
<script>
// 全局变量
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let isEditMode = true;
let pdfData = null;
// DOM元素
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const removeFile = document.getElementById('removeFile');
const uploadSection = document.getElementById('uploadSection');
const progressSection = document.getElementById('progressSection');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const editorSection = document.getElementById('editorSection');
const pdfEditor = document.getElementById('pdfEditor');
const pageNavigation = document.getElementById('pageNavigation');
const pageIndicator = document.getElementById('pageIndicator');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
const editModeBtn = document.getElementById('editModeBtn');
const previewModeBtn = document.getElementById('previewModeBtn');
const saveBtn = document.getElementById('saveBtn');
const downloadBtn = document.getElementById('downloadBtn');
// 初始化PDF.js
const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
// 事件监听
fileInput.addEventListener('change', handleFileUpload);
removeFile.addEventListener('click', removeSelectedFile);
prevPageBtn.addEventListener('click', goToPreviousPage);
nextPageBtn.addEventListener('click', goToNextPage);
editModeBtn.addEventListener('click', enableEditMode);
previewModeBtn.addEventListener('click', enablePreviewMode);
saveBtn.addEventListener('click', saveChanges);
downloadBtn.addEventListener('click', downloadAsPDF);
// 处理文件上传
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// 显示文件信息
fileName.textContent = file.name;
fileInfo.classList.remove('hidden');
uploadSection.classList.add('opacity-50');
// 准备解析
const fileReader = new FileReader();
fileReader.onload = function() {
pdfData = new Uint8Array(this.result);
loadPDF(pdfData);
};
fileReader.readAsArrayBuffer(file);
}
// 移除选中的文件
function removeSelectedFile() {
fileInput.value = '';
fileInfo.classList.add('hidden');
uploadSection.classList.remove('opacity-50');
resetPDFState();
}
// 重置PDF状态
function resetPDFState() {
pdfDoc = null;
currentPage = 1;
totalPages = 0;
pdfData = null;
progressSection.classList.add('hidden');
editorSection.classList.add('hidden');
pageNavigation.classList.add('hidden');
saveBtn.disabled = true;
downloadBtn.disabled = true;
}
// 加载PDF文件
function loadPDF(data) {
progressSection.classList.remove('hidden');
progressBar.style.width = '0%';
progressText.textContent = '正在加载PDF...';
pdfjsLib.getDocument(data).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
progressBar.style.width = '30%';
progressText.textContent = '解析PDF内容...';
updatePageIndicator();
renderPage(currentPage);
// 显示编辑区域和导航
editorSection.classList.remove('hidden');
pageNavigation.classList.remove('hidden');
saveBtn.disabled = false;
downloadBtn.disabled = false;
}).catch(function(error) {
console.error('加载PDF时出错:', error);
progressText.textContent = `加载失败: ${error.message}`;
});
}
// 渲染指定页面
function renderPage(pageNum) {
if (!pdfDoc) return;
pdfDoc.getPage(pageNum).then(function(page) {
// 获取页面内容
return page.getTextContent().then(function(textContent) {
// 更新进度
const progress = 30 + Math.round((pageNum / totalPages) * 70);
progressBar.style.width = `${progress}%`;
progressText.textContent = `正在处理第 ${pageNum} 页 / 共 ${totalPages} 页`;
// 清空编辑器
pdfEditor.innerHTML = '';
// 创建页面容器
const pageContainer = document.createElement('div');
pageContainer.className = 'pdf-page p-8 border border-gray-200 rounded-lg shadow-sm';
pageContainer.dataset.page = pageNum;
// 处理文本内容
let lastY = null;
let paragraph = document.createElement('p');
paragraph.className = 'mb-4 leading-relaxed';
paragraph.contentEditable = isEditMode;
textContent.items.forEach(function(item) {
// 当Y坐标变化较大时,创建新段落
if (lastY !== null && Math.abs(item.transform[5] - lastY) > 15) {
pageContainer.appendChild(paragraph);
paragraph = document.createElement('p');
paragraph.className = 'mb-4 leading-relaxed';
paragraph.contentEditable = isEditMode;
}
const span = document.createElement('span');
span.textContent = item.str;
paragraph.appendChild(span);
lastY = item.transform[5];
});
// 添加最后一个段落
if (paragraph.children.length > 0) {
pageContainer.appendChild(paragraph);
}
// 如果页面没有文本内容
if (textContent.items.length === 0) {
const emptyMsg = document.createElement('p');
emptyMsg.className = 'text-neutral italic text-center py-8';
emptyMsg.textContent = '此页面没有可编辑的文本内容。可能包含图像或其他非文本元素。';
pageContainer.appendChild(emptyMsg);
}
// 添加到编辑器
pdfEditor.appendChild(pageContainer);
// 更新按钮状态
updateNavigationButtons();
// 如果是最后一页,隐藏进度
if (pageNum === totalPages) {
setTimeout(() => {
progressSection.classList.add('hidden');
}, 500);
}
});
}).catch(function(error) {
console.error('渲染页面时出错:', error);
pdfEditor.innerHTML = `<p class="text-red-500">渲染页面时出错: ${error.message}</p>`;
});
}
// 更新页码指示器
function updatePageIndicator() {
pageIndicator.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
}
// 更新导航按钮状态
function updateNavigationButtons() {
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
}
// 上一页
function goToPreviousPage() {
if (currentPage > 1) {
currentPage--;
renderPage(currentPage);
updatePageIndicator();
}
}
// 下一页
function goToNextPage() {
if (currentPage < totalPages) {
currentPage++;
renderPage(currentPage);
updatePageIndicator();
}
}
// 启用编辑模式
function enableEditMode() {
isEditMode = true;
editModeBtn.classList.remove('bg-neutral', 'hover:bg-gray-600');
editModeBtn.classList.add('bg-primary', 'hover:bg-blue-600');
previewModeBtn.classList.remove('bg-primary', 'hover:bg-blue-600');
previewModeBtn.classList.add('bg-neutral', 'hover:bg-gray-600');
// 使所有段落可编辑
document.querySelectorAll('#pdfEditor [contenteditable]').forEach(el => {
el.contentEditable = true;
});
}
// 启用预览模式
function enablePreviewMode() {
isEditMode = false;
previewModeBtn.classList.remove('bg-neutral', 'hover:bg-gray-600');
previewModeBtn.classList.add('bg-primary', 'hover:bg-blue-600');
editModeBtn.classList.remove('bg-primary', 'hover:bg-blue-600');
editModeBtn.classList.add('bg-neutral', 'hover:bg-gray-600');
// 使所有段落不可编辑
document.querySelectorAll('#pdfEditor [contenteditable]').forEach(el => {
el.contentEditable = false;
});
}
// 保存更改(在实际应用中,这里会将数据发送到服务器)
function saveChanges() {
// 获取所有页面的内容
const pagesContent = [];
document.querySelectorAll('.pdf-page').forEach(pageEl => {
const pageNum = parseInt(pageEl.dataset.page);
const textContent = pageEl.innerText;
pagesContent.push({
page: pageNum,
content: textContent
});
});
// 显示保存成功提示
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<i class="fa fa-check mr-2"></i>已保存';
saveBtn.classList.remove('bg-secondary');
saveBtn.classList.add('bg-green-600');
setTimeout(() => {
saveBtn.innerHTML = originalText;
saveBtn.classList.remove('bg-green-600');
saveBtn.classList.add('bg-secondary');
}, 2000);
// 在实际应用中,这里会发送数据到服务器
console.log('保存的PDF内容:', pagesContent);
}
// 下载为PDF(实际应用中需要后端支持或使用jsPDF等库)
function downloadAsPDF() {
// 显示加载状态
const originalText = downloadBtn.innerHTML;
downloadBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>处理中...';
downloadBtn.disabled = true;
// 模拟PDF生成过程
setTimeout(() => {
// 这里仅做演示,实际应用中需要使用专门的库如jsPDF或调用后端API
alert('PDF导出功能在实际应用中需要额外的库或后端支持。');
// 恢复按钮状态
downloadBtn.innerHTML = originalText;
downloadBtn.disabled = false;
}, 1500);
}
// 支持拖拽上传
const dropArea = document.querySelector('#uploadSection .border-dashed');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('border-primary', 'bg-blue-50');
}
function unhighlight() {
dropArea.classList.remove('border-primary', 'bg-blue-50');
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const file = dt.files[0];
if (file && file.type === 'application/pdf') {
// 将文件设置到fileInput
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
// 触发change事件
const event = new Event('change', { bubbles: true });
fileInput.dispatchEvent(event);
} else {
alert('请上传PDF格式的文件');
}
}
</script>
</body>
</html>