PDF解析与编辑工具代码详解(Angular下,识别pdf为word并可以在线编辑)

1. 环境配置与依赖安装

1.1 安装依赖

javascript 复制代码
# 安装 PDF.js 库(推荐稳定版)
npm install pdfjs-dist@2.16.105 @types/pdfjs-dist@2.16.105

# 或使用最新版(需自定义类型声明)
npm install pdfjs-dist@3.4.120

1.2 配置 Worker 路径

在组件或服务中初始化 PDF.js:

javascript 复制代码
import * as pdfjsLib from 'pdfjs-dist';

// 方案1:使用 CDN(需联网)
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';

// 方案2:使用本地文件(推荐,更可靠)
// 1. 下载 worker 文件到 `assets/` 目录
// 2. 配置路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'assets/pdf.worker.min.js';

1.3 代码变化

1.3.1 angular.jsin

1.3.2 package.json

1.3.3 package-lock.json

2. 核心功能实现

2.1 HTML

html 复制代码
<div class="app-container">
    <!-- 顶部导航栏 -->
    <header class="app-header">
        <div class="header-container">
            <div class="logo-container">
                <i class="fa fa-file-pdf-o header-icon"></i>
                <h1 class="app-title">PDF解析与编辑工具</h1>
            </div>
            <div class="header-buttons">
                <button #saveBtn class="btn btn-save" disabled>
                    <i class="fa fa-save button-icon"></i>保存
                </button>
                <button #downloadBtn class="btn btn-download" disabled>
                    <i class="fa fa-download button-icon"></i>导出PDF
                </button>
            </div>
        </div>
    </header>

    <div class="main-content">
        <!-- 文件上传区域 -->
        <section #uploadSection class="upload-section">
            <div class="upload-card">
                <label for="fileInput" class="file-upload-label">
                    <div class="upload-area"
                         (dragover)="onDragOver($event)"
                         (dragleave)="onDragLeave($event)"
                         (drop)="onFileDrop($event)"
                         [class.upload-area-drag-over]="isDragOver">
                        <i class="fa fa-cloud-upload upload-icon"></i>
                        <h2 class="upload-title">上传PDF文件</h2>
                        <p class="upload-text">点击或拖拽文件到此处上传</p>
                        <p class="upload-hint">支持的格式: PDF</p>
                        <input #fileInput id="fileInput" type="file" accept=".pdf" class="file-input" (change)="onFileSelected($event)">
                    </div>
                </label>

                <div #fileInfo class="file-info" [class.hidden]="!selectedFile">
                    <div class="file-info-card">
                        <i class="fa fa-file-pdf-o file-icon"></i>
                        <span #fileName class="file-name">{{ selectedFile?.name }}</span>
                        <button (click)="removeSelectedFile()" class="file-remove-btn">
                            <i class="fa fa-times"></i>
                        </button>
                    </div>
                </div>
            </div>
        </section>

        <!-- 解析进度 -->
        <section #progressSection class="progress-section" [class.hidden]="!isProcessing">
            <div class="progress-card">
                <h2 class="progress-title">正在解析PDF文件...</h2>
                <div class="progress-bar">
                    <div #progressBar class="progress-value" [style.width.%]="progressValue"></div>
                </div>
                <p #progressText class="progress-text">{{ progressMessage }}</p>
            </div>
        </section>

        <!-- 编辑区域 -->
        <section #editorSection class="editor-section" [class.hidden]="!isEditorVisible">
            <div class="editor-panel">
                <div class="editor-header">
                    <h2 class="editor-title">PDF内容编辑</h2>
                    <div class="editor-actions">
                        <button #editModeBtn class="btn btn-edit" (click)="enableEditMode()">
                            <i class="fa fa-pencil button-icon"></i>编辑模式
                        </button>
                        <button #previewModeBtn class="btn btn-preview" (click)="enablePreviewMode()">
                            <i class="fa fa-eye button-icon"></i>预览模式
                        </button>
                    </div>
                </div>

                <div #pdfEditor class="editor-content">
                    <!-- PDF内容将在这里渲染 -->
                </div>
            </div>
        </section>

        <!-- 页面导航 -->
        <div #pageNavigation class="page-navigation" [class.hidden]="!isNavigationVisible">
            <div class="navigation-container">
                <button #prevPage class="nav-button" (click)="goToPreviousPage()" [disabled]="currentPage <= 1">
                    <i class="fa fa-chevron-left"></i>
                </button>
                <div #pageIndicator class="page-indicator">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</div>
                <button #nextPage class="nav-button" (click)="goToNextPage()" [disabled]="currentPage >= totalPages">
                    <i class="fa fa-chevron-right"></i>
                </button>
            </div>
        </div>
    </div>

    <footer class="app-footer">
        <div class="footer-container">
            <p>PDF解析与编辑工具 &copy; {{ currentYear }}</p>
        </div>
    </footer>
</div>

2.2 SCSS

css 复制代码
/* 基础变量 */
$primary-color: #3b82f6;
$secondary-color: #10b981;
$neutral-color: #737373;
$light-color: #f5f5f5;
$dark-color: #1a202c;
$danger-color: #ef4444;

/* 全局样式 */
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: #f9fafb;
    margin: 0;
    padding: 0;
}

.app-container {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.hidden {
    display: none !important;
}

/* 顶部导航 */
.app-header {
    background-color: white;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    position: sticky;
    top: 0;
    z-index: 50;
    padding: 1rem 0;

    .header-container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 0 1rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .logo-container {
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }

    .header-icon {
        color: $danger-color;
        font-size: 1.5rem;
    }

    .app-title {
        font-size: 1.25rem;
        font-weight: bold;
        color: $dark-color;
        margin: 0;
    }

    .header-buttons {
        display: flex;
        gap: 0.75rem;
    }
}

/* 主内容区 */
.main-content {
    flex: 1;
    max-width: 1200px;
    width: 100%;
    margin: 0 auto;
    padding: 2rem 1rem;
}

/* 上传区域 */
.upload-section {
    margin-bottom: 2rem;

    .upload-card {
        background-color: white;
        border-radius: 0.5rem;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 2rem;
        text-align: center;
    }

    .file-upload-label {
        cursor: pointer;
    }

    .upload-area {
        border: 2px dashed #d4d4d4;
        border-radius: 0.5rem;
        padding: 2.5rem;
        transition: all 0.2s ease;

        &:hover {
            border-color: $primary-color;
        }

        &-drag-over {
            border-color: $primary-color;
            background-color: rgba(59, 130, 246, 0.05);
        }
    }

    .upload-icon {
        font-size: 3rem;
        color: $primary-color;
        margin-bottom: 1rem;
    }

    .upload-title {
        font-size: 1.25rem;
        font-weight: 600;
        margin-bottom: 0.5rem;
        color: $dark-color;
    }

    .upload-text, .upload-hint {
        color: $neutral-color;
        margin-bottom: 1rem;
    }

    .upload-hint {
        font-size: 0.875rem;
    }

    .file-input {
        display: none;
    }

    .file-info {
        margin-top: 1rem;
    }

    .file-info-card {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0.75rem;
        background-color: $light-color;
        border-radius: 0.375rem;
    }

    .file-icon {
        color: $danger-color;
        margin-right: 0.5rem;
    }

    .file-name {
        margin-right: 0.5rem;
        color: $dark-color;
    }

    .file-remove-btn {
        color: $neutral-color;
        background: none;
        border: none;
        cursor: pointer;
        transition: color 0.2s;

        &:hover {
            color: $danger-color;
        }
    }
}

/* 进度条 */
.progress-section {
    margin-bottom: 2rem;

    .progress-card {
        background-color: white;
        border-radius: 0.5rem;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 1.5rem;
    }

    .progress-title {
        font-size: 1.125rem;
        font-weight: 600;
        margin-bottom: 1rem;
        color: $dark-color;
    }

    .progress-bar {
        width: 100%;
        height: 0.5rem;
        background-color: #e5e7eb;
        border-radius: 0.25rem;
        overflow: hidden;
    }

    .progress-value {
        height: 100%;
        background-color: $primary-color;
        transition: width 0.3s ease;
    }

    .progress-text {
        font-size: 0.875rem;
        color: $neutral-color;
        margin-top: 0.5rem;
    }
}

/* 编辑区域 */
.editor-section {
    margin-bottom: 2rem;

    .editor-panel {
        background-color: white;
        border-radius: 0.5rem;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 1.5rem;
    }

    .editor-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 1.5rem;
    }

    .editor-title {
        font-size: 1.25rem;
        font-weight: 600;
        color: $dark-color;
    }

    .editor-actions {
        display: flex;
        gap: 0.5rem;
    }

    .editor-content {
        min-height: 500px;
        border: 1px solid #e5e7eb;
        border-radius: 0.5rem;
        padding: 1rem;

        .pdf-page {
            padding: 1.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
            margin-bottom: 1rem;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);

            p {
                margin-bottom: 1rem;
                line-height: 1.6;
            }
        }
    }
}

/* 页面导航 */
.page-navigation {
    margin-top: 1.5rem;

    .navigation-container {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 1rem;
    }

    .nav-button {
        background-color: white;
        border: 1px solid #d4d4d4;
        border-radius: 0.375rem;
        padding: 0.25rem 0.75rem;
        cursor: pointer;
        transition: background-color 0.2s;

        &:hover:not(:disabled) {
            background-color: $light-color;
        }

        &:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
    }

    .page-indicator {
        color: $neutral-color;
    }
}

/* 按钮通用样式 */
.btn {
    display: flex;
    align-items: center;
    padding: 0.5rem 1rem;
    border-radius: 0.375rem;
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
    border: none;

    &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }

    .button-icon {
        margin-right: 0.5rem;
    }

    &-save {
        background-color: $secondary-color;
        color: white;

        &:hover:not(:disabled) {
            background-color: darken($secondary-color, 10%);
        }
    }

    &-download {
        background-color: $primary-color;
        color: white;

        &:hover:not(:disabled) {
            background-color: darken($primary-color, 10%);
        }
    }

    &-edit {
        background-color: $primary-color;
        color: white;
    }

    &-preview {
        background-color: $neutral-color;
        color: white;
    }
}

/* 页脚 */
.app-footer {
    background-color: $dark-color;
    color: white;
    padding: 1.5rem 0;
    margin-top: 3rem;

    .footer-container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 0 1rem;
        text-align: center;
    }
}

2.3 ts

javascript 复制代码
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import * as pdfjsLib from 'pdfjs-dist';

@Component({
    selector: 'app-pdf-editor',
    templateUrl: './pdf-editor.component.html',
    styleUrls: ['./pdf-editor.component.scss']
})
export class PdfEditorComponent implements AfterViewInit, OnDestroy {
    // 视图引用
    @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
    @ViewChild('fileInfo') fileInfo!: ElementRef<HTMLDivElement>;
    @ViewChild('progressBar') progressBar!: ElementRef<HTMLDivElement>;
    @ViewChild('progressText') progressText!: ElementRef<HTMLParagraphElement>;
    @ViewChild('editorSection') editorSection!: ElementRef<HTMLElement>;
    @ViewChild('pdfEditor') pdfEditor!: ElementRef<HTMLDivElement>;
    @ViewChild('pageNavigation') pageNavigation!: ElementRef<HTMLElement>;
    @ViewChild('editModeBtn') editModeBtn!: ElementRef<HTMLButtonElement>;
    @ViewChild('previewModeBtn') previewModeBtn!: ElementRef<HTMLButtonElement>;
    @ViewChild('saveBtn') saveBtn!: ElementRef<HTMLButtonElement>;
    @ViewChild('downloadBtn') downloadBtn!: ElementRef<HTMLButtonElement>;
    @ViewChild('prevPage') prevPage!: ElementRef<HTMLButtonElement>;
    @ViewChild('nextPage') nextPage!: ElementRef<HTMLButtonElement>;

    // PDF相关状态
    pdfDoc: any = null;
    currentPage = 1;
    totalPages = 0;
    isEditMode = true;
    pdfData: Uint8Array | null = null;
    currentYear = new Date().getFullYear();
    isDragOver = false;
    progressValue = 0;
    progressMessage = '准备中...';
    selectedFile: File | null = null;

    // 状态标志
    isProcessing = false;
    isEditorVisible = false;
    isNavigationVisible = false;

    ngAfterViewInit() {
        this.initPdfJs();
    }

    ngOnDestroy() {}

    private initPdfJs() {
        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
    }

    // 文件选择处理
    onFileSelected(event: Event) {
        const input = event.target as HTMLInputElement;
        const file = input.files?.[0];
        if (!file) return;

        this.handleFileSelection(file);
    }

    // 文件拖放处理
    onDragOver(event: DragEvent) {
        event.preventDefault();
        this.isDragOver = true;
    }

    onDragLeave(event: DragEvent) {
        event.preventDefault();
        this.isDragOver = false;
    }

    onFileDrop(event: DragEvent) {
        event.preventDefault();
        this.isDragOver = false;

        const files = event.dataTransfer?.files;
        if (files && files.length > 0) {
            const file = files[0];
            if (file.type === 'application/pdf') {
                // 创建新的FileList对象
                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(file);
                this.fileInput.nativeElement.files = dataTransfer.files;

                // 触发change事件
                const changeEvent = new Event('change');
                this.fileInput.nativeElement.dispatchEvent(changeEvent);
            } else {
                alert('请上传PDF格式的文件');
            }
        }
    }
    private arrayBufferToBase64(buffer: ArrayBuffer): string {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        const len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary);
    }

    private handleFileSelection(file: File) {
        this.selectedFile = file;
        this.isProcessing = true;
        this.progressValue = 0;
        this.progressMessage = '正在加载PDF...';

        const fileReader = new FileReader();
        fileReader.onload = () => {
            this.pdfData = new Uint8Array(fileReader.result as ArrayBuffer);
            // 转换为 base64
            const pdfBase64 = this.arrayBufferToBase64(this.pdfData.buffer);
            console.log('PDF base64:', pdfBase64);
            const dataUrl = this.getDataUrlFromArrayBuffer(pdfBase64);
            console.log('dataUrl:', dataUrl);
            this.loadPDF(this.pdfData);
        };
        fileReader.readAsArrayBuffer(file);
    }
    getDataUrlFromArrayBuffer(buffer): Promise<string> {
        return new Promise((resolve) => {
            const blob = new Blob([buffer], { type: 'application/pdf' });
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result as string);
            reader.readAsDataURL(blob);
        });
    }

    removeSelectedFile() {
        this.selectedFile = null;
        this.fileInput.nativeElement.value = '';
        this.resetPDFState();
    }

    resetPDFState() {
        this.pdfDoc = null;
        this.currentPage = 1;
        this.totalPages = 0;
        this.pdfData = null;
        this.isProcessing = false;
        this.isEditorVisible = false;
        this.isNavigationVisible = false;

        if (this.saveBtn) this.saveBtn.nativeElement.disabled = true;
        if (this.downloadBtn) this.downloadBtn.nativeElement.disabled = true;
    }

    loadPDF(data: Uint8Array) {
        this.progressValue = 10;
        this.progressMessage = '正在加载PDF...';
        console.log(data);
        pdfjsLib.getDocument(data).promise.then((pdf: any) => {
            this.pdfDoc = pdf;
            this.totalPages = pdf.numPages;

            this.progressValue = 30;
            this.progressMessage = '解析PDF内容...';

            this.renderPage(this.currentPage);

            this.isEditorVisible = true;
            this.isNavigationVisible = true;

            if (this.saveBtn) this.saveBtn.nativeElement.disabled = false;
            if (this.downloadBtn) this.downloadBtn.nativeElement.disabled = false;
        }).catch((error: any) => {
            console.error('加载PDF时出错:', error);
            this.progressMessage = `加载失败: ${error.message}`;
        });
    }

    renderPage(pageNum: number) {
        if (!this.pdfDoc) return;

        this.pdfDoc.getPage(pageNum).then((page: any) => {
            return page.getTextContent().then((textContent: any) => {
                // 更新进度
                const progress = 30 + Math.round((pageNum / this.totalPages) * 70);
                this.progressValue = progress;
                this.progressMessage = `正在处理第 ${pageNum} 页 / 共 ${this.totalPages} 页`;

                // 清空编辑器内容
                this.pdfEditor.nativeElement.innerHTML = '';

                const pageContainer = document.createElement('div');
                pageContainer.className = 'pdf-page';
                pageContainer.dataset.page = pageNum.toString();

                let lastY: number | null = null;
                let paragraph = document.createElement('p');
                paragraph.contentEditable = this.isEditMode.toString();

                textContent.items.forEach((item: any) => {
                    if (lastY !== null && Math.abs(item.transform[5] - lastY) > 15) {
                        pageContainer.appendChild(paragraph);
                        paragraph = document.createElement('p');
                        paragraph.contentEditable = this.isEditMode.toString();
                    }

                    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);
                }

                this.pdfEditor.nativeElement.appendChild(pageContainer);

                // 如果是最后一页,完成处理
                if (pageNum === this.totalPages) {
                    setTimeout(() => {
                        this.isProcessing = false;
                    }, 500);
                }
            });
        }).catch((error: any) => {
            console.error('渲染页面时出错:', error);
            this.pdfEditor.nativeElement.innerHTML = `<p class="error">渲染页面时出错: ${error.message}</p>`;
        });
    }

    goToPreviousPage() {
        if (this.currentPage > 1) {
            this.currentPage--;
            this.renderPage(this.currentPage);
        }
    }

    goToNextPage() {
        if (this.currentPage < this.totalPages) {
            this.currentPage++;
            this.renderPage(this.currentPage);
        }
    }

    enableEditMode() {
        this.isEditMode = true;

        // 更新按钮样式
        if (this.editModeBtn) {
            this.editModeBtn.nativeElement.classList.add('btn-edit');
            this.editModeBtn.nativeElement.classList.remove('btn-preview');
        }

        if (this.previewModeBtn) {
            this.previewModeBtn.nativeElement.classList.add('btn-preview');
            this.previewModeBtn.nativeElement.classList.remove('btn-edit');
        }

        // 启用内容编辑
        const editableElements = this.pdfEditor.nativeElement.querySelectorAll('[contenteditable]');
        editableElements.forEach((el: any) => {
            el.contentEditable = 'true';
        });
    }

    enablePreviewMode() {
        this.isEditMode = false;

        // 更新按钮样式
        if (this.previewModeBtn) {
            this.previewModeBtn.nativeElement.classList.add('btn-edit');
            this.previewModeBtn.nativeElement.classList.remove('btn-preview');
        }

        if (this.editModeBtn) {
            this.editModeBtn.nativeElement.classList.add('btn-preview');
            this.editModeBtn.nativeElement.classList.remove('btn-edit');
        }

        // 禁用内容编辑
        const editableElements = this.pdfEditor.nativeElement.querySelectorAll('[contenteditable]');
        editableElements.forEach((el: any) => {
            el.contentEditable = 'false';
        });
    }

    saveChanges() {
        if (!this.saveBtn) return;

        const originalText = this.saveBtn.nativeElement.innerHTML;
        this.saveBtn.nativeElement.innerHTML = '<i class="fa fa-check button-icon"></i>已保存';
        this.saveBtn.nativeElement.classList.add('saved');

        // 收集所有页面内容
        const pagesContent: Array<{ page: number, content: string }> = [];
        const pageElements = this.pdfEditor.nativeElement.querySelectorAll('.pdf-page');

        pageElements.forEach((pageEl: any) => {
            const pageNum = parseInt(pageEl.dataset.page);
            const textContent = pageEl.innerText;
            pagesContent.push({
                page: pageNum,
                content: textContent
            });
        });

        // 实际应用中,这里可以调用服务将数据发送到后端
        console.log('保存的PDF内容:', pagesContent);

        // 重置按钮状态
        setTimeout(() => {
            if (this.saveBtn) {
                this.saveBtn.nativeElement.innerHTML = originalText;
                this.saveBtn.nativeElement.classList.remove('saved');
            }
        }, 2000);
    }

    downloadAsPDF() {
        if (!this.downloadBtn) return;

        const originalText = this.downloadBtn.nativeElement.innerHTML;
        this.downloadBtn.nativeElement.innerHTML = '<i class="fa fa-spinner fa-spin button-icon"></i>处理中...';
        this.downloadBtn.nativeElement.disabled = true;

        // 模拟导出处理
        setTimeout(() => {
            alert('PDF导出功能在实际应用中需要额外的库或后端支持。');

            if (this.downloadBtn) {
                this.downloadBtn.nativeElement.innerHTML = originalText;
                this.downloadBtn.nativeElement.disabled = false;
            }
        }, 1500);
    }
}

3 效果截图

3.1 总览

3.2 可修改

3.3可保存传后台

该部分功能没有写通。