Kỹ Thuật Xây Dựng Công Cụ Ghép PDF Bằng JavaScript
Khi xây dựng các công cụ xử lý tài liệu trên web, cách dễ nhất thường là đẩy phần việc nặng nhọc sang Backend (máy chủ). Bạn tải file PDF lên, máy chủ chạy một tập lệnh Python hoặc Node.js (dùng Ghostscript hoặc PDFtk), sau đó gửi trả lại tệp đã ghép.
Tuy nhiên, cách tiếp cận này đi kèm với những nhược điểm lớn: chi phí máy chủ cao, tốc độ tải lên/tải xuống chậm, và những lo ngại về quyền riêng tư.
Bằng cách đưa khối lượng công việc sang xử lý hoàn toàn ở phía Client (trình duyệt web của người dùng) bằng JavaScript, bạn tạo ra một công cụ thao tác siêu tốc, cực kỳ bảo mật và dễ dàng bảo trì. Trong bài viết này, chúng tôi sẽ giải thích kiến trúc kỹ thuật đằng sau công cụ Ghép PDF của mình.
Bộ Công Cụ Công Nghệ (Tech Stack)
Để xây dựng một trình quản lý PDF mạnh mẽ trên trình duyệt, bạn cần hai thứ: một thư viện chỉnh sửa cấu trúc PDF và một thư viện để kết xuất (render) hình ảnh các trang để hiển thị lên UI.
- UI Framework: React (Next.js)
- Thao tác PDF:
pdf-lib(dùng để đọc, hợp nhất và ghi dữ liệu PDF mới) - Render hình ảnh PDF:
pdfjs-dist(thư viện PDF.js của Mozilla, dùng để vẽ ảnh bìa Thumbnail cho các tệp PDF)

1. Kết xuất (Render) Thumbnail với PDF.js
Để có trải nghiệm người dùng tốt, họ cần nhìn thấy ảnh thu nhỏ (thumbnail) của các file PDF tải lên. pdfjs-dist rất hoàn hảo cho việc này, tuy nhiên việc render PDF tốn rất nhiều CPU. Để tránh làm đơ giao diện trình duyệt, bắt buộc phải sử dụng Web Worker.
Dưới đây là cách chúng tôi render trang đầu tiên của file PDF sang một chuỗi ảnh JPEG (Data URL):
import * as pdfjsLib from "pdfjs-dist";
// Cấu hình URL của Web Worker
pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
async function renderPdfThumbnail(file: File): Promise<string | null> {
const buf = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
const page = await pdf.getPage(1);
// Render ở tỷ lệ thu nhỏ để tiết kiệm bộ nhớ (ví dụ: 60%)
const viewport = page.getViewport({ scale: 0.6 });
const canvas = document.createElement("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext("2d");
if (!ctx) return null;
await page.render({ canvasContext: ctx, viewport }).promise;
// Chuyển đổi thành ảnh JPEG tốc độ cao
const dataUrl = canvas.toDataURL("image/jpeg", 0.75);
pdf.destroy();
return dataUrl;
}
2. Ghép Các Tệp PDF bằng pdf-lib
Việc hợp nhất các file diễn ra khi người dùng nhấn nút "Ghép PDF". Chúng tôi chọn pdf-lib vì nó xử lý rất mượt việc sao chép (copy) các trang giữa các Instance PDFDocument khác nhau mà không làm hỏng siêu dữ liệu (metadata) hay tài nguyên font chữ.
Quy trình hoạt động:
- Tạo một
PDFDocumentmới, trống. - Lặp qua danh sách các file người dùng đã sắp xếp.
- Phân tích từng file dưới dạng
PDFDocument. - Sao chép các trang của nó và thêm vào tài liệu mới tạo ở bước 1.
- Lưu tài liệu mới thành
Uint8Array(Blob) và kích hoạt hành động Download.
import { PDFDocument, degrees } from "pdf-lib";
async function mergePdfs(files: { file: File, rotation: number }[]) {
// 1. Tạo một Document mới
const mergedDoc = await PDFDocument.create();
for (const pdfFile of files) {
// 2. Tải Document nguồn
const buf = await pdfFile.file.arrayBuffer();
const srcDoc = await PDFDocument.load(buf, { ignoreEncryption: true });
// 3. Xử lý các thao tác Xoay (Rotate) của người dùng
if (pdfFile.rotation !== 0) {
srcDoc.getPages().forEach((p) => {
const currentAngle = p.getRotation().angle;
p.setRotation(degrees((currentAngle + pdfFile.rotation) % 360));
});
}
// 4. Sao chép tất cả các trang từ file gốc sang file ghép
const copiedPages = await mergedDoc.copyPages(srcDoc, srcDoc.getPageIndices());
copiedPages.forEach((p) => mergedDoc.addPage(p));
}
// 5. Lưu kết quả và tạo link tải xuống
const mergedBytes = await mergedDoc.save();
const blob = new Blob([mergedBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
// Logic kích hoạt tải xuống...
}
3. Xử Lý Kéo Thả và Thay Đổi Thứ Tự
Để tạo khả năng kéo thả (drag and drop) mượt mà trong React, chúng tôi tự triển khai các trình xử lý sự kiện mặc định là onDragStart, onDragEnter và onDragEnd của HTML5.
Khi một phần tử được kéo đè lên một phần tử khác, React State sẽ lập tức cập nhật vị trí của chúng. Điều này tạo ra hiệu ứng phản hồi chuyển động tức thì mà không cần cài cắm thêm các thư viện kéo thả (như dnd-kit) làm nặng mã nguồn.
Kết Luận
Bằng cách đẩy hoàn toàn mọi quy trình xử lý tài liệu sang trình duyệt, các ứng dụng web ngày nay có sức mạnh ngang ngửa với phần mềm trên máy tính Desktop trong khi vượt xa về độ tiện lợi, sự miễn phí và tính riêng tư. Kết hợp pdf-lib cho cấu trúc dữ liệu và pdfjs-dist cho hiển thị hình ảnh là công thức tối thượng để thiết lập nên một hệ sinh thái chỉnh sửa PDF mạnh mẽ.