Реализация PDF-просмотрщика на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация PDF-просмотрщика на сайте
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация PDF-просмотрщика на сайте

Встроенный просмотрщик PDF нужен в кабинетах документооборота, страницах договоров, порталах нормативных документов, системах отчётности. <iframe src="file.pdf" title="Embedded content"> — нет: поведение зависит от браузера, мобильный Chrome скачивает файл вместо показа, нет никакого контроля над UI.

PDF.js

Mozilla PDF.js — стандарт для браузерного рендеринга PDF. Использует Canvas для рендеринга каждой страницы. Именно его использует Firefox для встроенного просмотрщика.

npm install pdfjs-dist
npm install react-pdf  # React-обёртка над PDF.js

react-pdf: простая реализация

import { Document, Page, pdfjs } from 'react-pdf'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'

// Указываем worker — обязательно
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()

interface PDFViewerProps {
  url: string
}

export function PDFViewer({ url }: PDFViewerProps) {
  const [numPages, setNumPages] = useState<number>(0)
  const [pageNumber, setPageNumber] = useState<number>(1)
  const [scale, setScale] = useState<number>(1.0)
  const [isLoading, setIsLoading] = useState(true)

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setNumPages(numPages)
    setIsLoading(false)
  }

  return (
    <div className="flex flex-col items-center">
      {/* Тулбар */}
      <div className="flex items-center gap-4 p-3 bg-gray-800 text-white w-full">
        <button
          onClick={() => setPageNumber((p) => Math.max(1, p - 1))}
          disabled={pageNumber <= 1}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          ←
        </button>
        <span className="text-sm">
          {pageNumber} / {numPages}
        </span>
        <button
          onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))}
          disabled={pageNumber >= numPages}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          →
        </button>
        <div className="ml-auto flex items-center gap-2">
          <button onClick={() => setScale((s) => Math.max(0.5, s - 0.25))}>−</button>
          <span className="text-sm w-12 text-center">{Math.round(scale * 100)}%</span>
          <button onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button>
        </div>
      </div>

      {/* Документ */}
      <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}>
        <Document
          file={url}
          onLoadSuccess={onDocumentLoadSuccess}
          loading={<div className="p-8 text-center">Загрузка...</div>}
          error={<div className="p-8 text-center text-red-500">Ошибка загрузки PDF</div>}
        >
          <Page
            pageNumber={pageNumber}
            scale={scale}
            renderTextLayer={true}     // Выделяемый текст
            renderAnnotationLayer={true} // Кликабельные ссылки
            className="shadow-lg mx-auto my-4"
          />
        </Document>
      </div>
    </div>
  )
}

Показ всех страниц (scroll mode)

Для длинных документов удобнее прокрутка, а не пагинация:

function PDFScrollViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [containerWidth, setContainerWidth] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!containerRef.current) return
    const observer = new ResizeObserver(([entry]) => {
      setContainerWidth(entry.contentRect.width)
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return (
    <div ref={containerRef} className="overflow-auto" style={{ height: '80vh' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i + 1}
            pageNumber={i + 1}
            width={containerWidth - 32}  // Адаптивная ширина
            className="mb-4 shadow mx-4"
            renderTextLayer={true}
          />
        ))}
      </Document>
    </div>
  )
}

Виртуализация страниц для больших PDF

100-страничный PDF рендерить весь сразу — не нужно. Используем виртуализацию:

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualPDFViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [pageHeight] = useState(842) // A4 при scale=1
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: numPages,
    getScrollElement: () => parentRef.current,
    estimateSize: () => pageHeight + 16, // высота страницы + отступ
    overscan: 2,
  })

  return (
    <div ref={parentRef} style={{ height: '80vh', overflow: 'auto' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
          {virtualizer.getVirtualItems().map((virtualItem) => (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualItem.start}px)`,
                width: '100%',
                padding: '8px 16px',
              }}
            >
              <Page
                pageNumber={virtualItem.index + 1}
                width={600}
                renderTextLayer={false}  // Отключаем для скорости
              />
            </div>
          ))}
        </div>
      </Document>
    </div>
  )
}

Защищённые PDF через Blob URL

Чтобы пользователь не мог скачать файл напрямую по URL:

async function loadProtectedPDF(documentId: string): Promise<string> {
  const response = await fetch(`/api/documents/${documentId}/content`, {
    headers: { Authorization: `Bearer ${getToken()}` },
  })

  if (!response.ok) throw new Error('Access denied')

  const blob = await response.blob()
  return URL.createObjectURL(blob)
  // Blob URL действителен только в рамках сессии браузера
  // Прямой ссылки нет — только через API с авторизацией
}

Что делаем

Настраиваем PDF.js с react-pdf, выбираем режим отображения (постраничный или скролл), добавляем тулбар с зумом и навигацией. Для больших документов — виртуализация страниц. Для закрытых документов — загрузка через авторизованный API с Blob URL.

Срок: базовый просмотрщик — 1 день. С виртуализацией и защитой доступа — 2–3 дня.