PDF.js библиотека для обработки PDF-документов на JavaScript

Раздел: Библиотеки -> Форматы документов

Обзор библиотеки PDF.js

PDF.js - это мощная библиотека на JavaScript для работы с PDF-документами прямо в браузере. Она позволяет загружать, отображать, извлекать текст, обрабатывать формы и многое другое без серверной части. Основана на Mozilla и широко используется в веб-приложениях.

Основное решение: рендеринг PDF через Canvas

Как отобразить первую страницу PDF из локального файла или URL?

// Установка: npm install pdfjs-dist
import * as pdfjsLib from 'pdfjs-dist';

const loadingTask = pdfjsLib.getDocument('example.pdf');
loadingTask.promise.then(pdf => {
  pdf.getPage(1).then(page => {
    const scale = 1.5;
    const viewport = page.getViewport({ scale });
    const canvas = document.getElementById('pdf-canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const ctx = canvas.getContext('2d');
    page.render({ canvasContext: ctx, viewport }).promise.then(() => {
      console.log('PDF rendered');
    });
  });
});

Pdf js (pdf.js библиотека)

Пояснение: getDocument возвращает объект PDF, метод getPage выбранную страницу. getViewport вычисляет размеры для заданного масштаба. Рендеринг происходит в canvas.

Типичные ошибки:

  • PDF загружается из другого домена без CORS - ошибка доступа. Решение: настроить сервер или использовать прокси.
  • Не обновлён размер canvas после загрузки - отображается пустой холст. Решение: проверять вызов canvas.width/height.
  • Слишком крупный scale ведёт к падению памяти. Решение: ограничить масштаб (например, до 3).

Как реализовать навигацию по страницам (вперёд/назад)?

let currentPage = 1;
let pdfDoc = null;

function renderPage(pageNum) {
  pdfDoc.getPage(pageNum).then(page => { /* рендеринг как выше */ });
}

document.getElementById('next').addEventListener('click', () => {
  if (currentPage < pdfDoc.numPages) {
    currentPage++;
    renderPage(currentPage);
  }
});
document.getElementById('prev').addEventListener('click', () => {
  if (currentPage > 1) {
    currentPage--;
    renderPage(currentPage);
  }
});

Пояснение: numPages общее количество страниц. Переключение возможно после загрузки всего PDF.

Проблема: при быстром клике несколько рендеров могут конфликтовать. Решение: отменять предыдущий рендер через возвращаемый промис (cancel).

Как извлечь текст из PDF?

pdf.getPage(pageNum).then(page => {
  page.getTextContent().then(textContent => {
    const text = textContent.items.map(item => item.str).join(' ');
    console.log(text);
  });
});

Метод getTextContent возвращает массив фрагментов текста с координатами. Для простого текста достаточно собрать str.

Некоторые PDF содержат шрифты без кодировки или изображения текста - извлечение невозможно. Решение: использовать OCR как дополнение.

Как использовать встроенный PDF.js Viewer (pdf_viewer.js)?

import { PDFViewer } from 'pdfjs-dist/web/pdf_viewer';
import 'pdfjs-dist/web/pdf_viewer.css';

const viewer = new PDFViewer({
  container: document.getElementById('viewerContainer'),
  viewer: document.getElementById('viewer'),
});
const loadingTask = pdfjsLib.getDocument('example.pdf');
loadingTask.promise.then(pdf => {
  viewer.setDocument(pdf);
});

Viewer поддерживает скролл, масштабирование, навигацию. Подходит для полноценного просмотрщика.

Проблема: инициализация требует определённой структуры HTML (контейнер и элемент viewer). Решение: смотреть документацию по разметке.

Как рендерить страницы в высоком разрешении или с масштабированием?

const scale = window.devicePixelRatio;
const viewport = page.getViewport({ scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = viewport.width / scale + 'px';
canvas.style.height = viewport.height / scale + 'px';

Умножаем scale на devicePixelRatio для retina-экранов. CSS-стили сжимают canvas до логических пикселей.

Рендеринг с высоким scale нагружает GPU. Решение: использовать viewport с ratio не более 2-3.

Как обработать ошибки при загрузке PDF?

const loadingTask = pdfjsLib.getDocument('broken.pdf');
loadingTask.promise.catch(err => {
  if (err instanceof pdfjsLib.PasswordException) { /* запрос пароля */ }
  else if (err instanceof pdfjsLib.InvalidPDFException) { /* неверный формат */ }
  else { /* другая ошибка */ }
});

PDF.js выбрасывает типизированные исключения. Можно обрабатывать отдельно.

Иногда ошибка не приходит в catch, если PDF частично повреждён. Решение: установить таймаут или проверять loadingTask.destroy().

Как работать с формами и полями ввода?

pdf.getPage(pageNum).then(page => {
  const annotations = page.getAnnotations();
  annotations.filter(ann => ann.subtype === 'Widget').forEach(widget => {
    if (widget.fieldType === 'Tx') {
      console.log('Text field:', widget.fieldName);
    }
  });
});

getAnnotations возвращает все аннотации страницы. Widget-аннотации соответствуют полям форм. Доступны имя поля, значение, тип.

Для заполнения формы требуется низкоуровневое редактирование (через AnnotationLayer) - сложно. Решение: использовать сторонние библиотеки для заполнения или генерации PDF.

Как получить метаданные PDF (автор, название)?

pdf.getMetadata().then(meta => {
  console.log(meta.info.Title);
  console.log(meta.info.Author);
  console.log(meta.metadata);
});

Метаданные извлекаются асинхронно. Часть данных может отсутствовать.

Некоторые PDF не содержат метаданных вовсе. Решение: проверять существование свойств через if (meta.info).

Расширенные примеры работы с PDF.js

1. Загрузка PDF из ArrayBuffer и рендеринг

Пример
fetch('example.pdf')
  .then(res => res.arrayBuffer())
  .then(buffer => {
    const loadingTask = pdfjsLib.getDocument({ data: buffer });
    return loadingTask.promise;
  })
  .then(pdf => {
    pdf.getPage(1).then(page => { /* рендеринг */ });
  });

Результат: PDF отображается из бинарных данных, без URL.

canvas с первой страницей PDF.

2. Рендеринг всех страниц в один большой canvas

Пример
async function renderAllPages(pdf) {
  const canvases = [];
  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const viewport = page.getViewport({ scale: 1 });
    const canvas = document.createElement('canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const ctx = canvas.getContext('2d');
    await page.render({ canvasContext: ctx, viewport }).promise;
    canvases.push(canvas);
  }
  // Размещение в контейнере
  const container = document.getElementById('pdf-pages');
  canvases.forEach(c => container.appendChild(c));
}

Результат: все страницы отображены вертикально.

серия канвасов, по одному на страницу.

3. Извлечение текста с координатами и стилями

Пример
page.getTextContent().then(content => {
  content.items.forEach(item => {
    console.log(`Текст: ${item.str}, x: ${item.transform[4]}, y: ${item.transform[5]}, шрифт: ${item.fontName}`);
  });
});

Результат: в консоли список фрагментов с позицией.

Текст: Hello, x: 72, y: 720, шрифт: g_d1_f2

4. Интеграция с React (функциональный компонент)

Пример
import { useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist';

function PDFViewer({ url }) {
  const canvasRef = useRef();

  useEffect(() => {
    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise.then(pdf => {
      pdf.getPage(1).then(page => {
        const viewport = page.getViewport({ scale: 1.5 });
        const canvas = canvasRef.current;
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        const ctx = canvas.getContext('2d');
        page.render({ canvasContext: ctx, viewport });
      });
    });
  }, [url]);

  return <canvas ref={canvasRef}></canvas>;
}

Результат: React-компонент, отображающий первую страницу.

Рендеринг в canvas, встроенный в React.

5. Работа с PDF, защищённым паролем

Пример
const loadingTask = pdfjsLib.getDocument({ url: 'protected.pdf', password: 'userpass' });
loadingTask.promise.then(pdf => { /* рендеринг */ }).catch(err => {
  if (err instanceof pdfjsLib.PasswordException) {
    // запросить другой пароль
  }
});

Результат: PDF открывается, если пароль верен.

Страница отрисована.

6. Кастомизация рендеринга: создание текстового слоя поверх canvas

Пример
page.getTextContent().then(textContent => {
  const textLayer = document.getElementById('text-layer');
  textLayer.innerHTML = '';
  textContent.items.forEach(item => {
    const span = document.createElement('span');
    span.style.left = item.transform[4] + 'px';
    span.style.top = item.transform[5] + 'px';
    span.textContent = item.str;
    textLayer.appendChild(span);
  });
});

Результат: текст доступен для выделения мышью.

Прозрачный слой с элементами span, позиционированными над canvas.

7. Рендеринг в Web Worker (параллельная загрузка)

Пример
// Основной поток
const worker = new Worker('pdfWorker.js');
worker.postMessage({ url: 'example.pdf', page: 1 });
worker.onmessage = (e) => {
  const { imageData } = e.data;
  // отобразить imageData на canvas
};

// pdfWorker.js
importScripts('pdfjs-dist/build/pdf.worker.min.js');
self.onmessage = async (e) => {
  const { url, page } = e.data;
  const pdf = await pdfjsLib.getDocument(url).promise;
  const pg = await pdf.getPage(page);
  const viewport = pg.getViewport({ scale: 1 });
  const canvas = new OffscreenCanvas(viewport.width, viewport.height);
  const ctx = canvas.getContext('2d');
  await pg.render({ canvasContext: ctx, viewport }).promise;
  const bitmap = canvas.transferToImageBitmap();
  self.postMessage({ imageData: bitmap }, [bitmap]);
};

Результат: рендеринг не блокирует UI.

Быстрый рендеринг страницы.

8. Кастомный Viewer с кэшированием страниц

Пример
const cache = new Map();

function renderPageWithCache(pdf, pageNum) {
  if (cache.has(pageNum)) {
    // использовать кэшированный canvas
    return Promise.resolve();
  }
  return pdf.getPage(pageNum).then(page => {
    const viewport = page.getViewport({ scale: 1 });
    const canvas = document.createElement('canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const ctx = canvas.getContext('2d');
    return page.render({ canvasContext: ctx, viewport }).promise.then(() => {
      cache.set(pageNum, canvas);
    });
  });
}

Результат: повторный рендеринг ускорен.

Страницы переключаются без задержки.

PDF.js библиотека - comments

En
Pdf js (javascript)