PDFLince: Client-Side PDF Manipulation с Next.js 15 и TypeScript

#nextjs#typescript#webworkers#frontend

TL;DR: Разбор архитектуры PDFLince — open-source инструмента для работы с PDF полностью на клиенте. Используем Next.js 15, pdf-lib и Web Workers для тяжелых операций без компромиссов в приватности.

Введение: Почему Client-Side PDF Processing?

В эпоху GDPR и CCPA приватность данных становится must-have, а не nice-to-have. Традиционные PDF-сервисы требуют загрузки документов на сервер, что создает риски утечки конфиденциальной информации. PDFLince решает эту проблему, выполняя все операции в браузере.

Core Architecture

1. Next.js 15 как Foundation

Используем App Router с React Server Components для статических страниц и Client Components для интерактивных частей:

// app/page.tsx
export default function Home() {
  return (
    <main className="container">
      <ClientComponent />
    </main>
  )
}

Ключевые преимущества:

2. pdf-lib: Работа с PDF на клиенте

Основной engine для манипуляций с PDF:

import { PDFDocument } from 'pdf-lib'

const mergePDFs = async (pdfs: ArrayBuffer[]) => {
  const mergedPdf = await PDFDocument.create()
  
  for (const pdfBytes of pdfs) {
    const pdfDoc = await PDFDocument.load(pdfBytes)
    const pages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices())
    pages.forEach(page => mergedPdf.addPage(page))
  }

  return await mergedPdf.save()
}

Особенности реализации:

3. Web Workers для CPU-Intensive Tasks

Выносим тяжелые операции в отдельные потоки:

// worker.ts
self.onmessage = async (e) => {
  const { type, payload } = e.data
  switch (type) {
    case 'MERGE_PDFS':
      const mergedPdf = await mergePDFs(payload)
      self.postMessage({ type: 'MERGE_COMPLETE', payload: mergedPdf })
      break
    // другие операции
  }
}

// main thread
const worker = new ComlinkWorker<typeof import('./worker')>(
  new URL('./worker', import.meta.url)
)

const result = await worker.mergePDFs(pdfBuffers)

Оптимизации Производительности

  1. Lazy Loading PDF-lib:
const PDFModule = await import('pdf-lib')
const { PDFDocument } = PDFModule
  1. Chunked Processing для больших документов:
const processInChunks = async (pages: Page[], chunkSize = 10) => {
  for (let i = 0; i < pages.length; i += chunkSize) {
    const chunk = pages.slice(i, i + chunkSize)
    await processChunk(chunk)
    updateProgress(i / pages.length)
  }
}
  1. Memory Management:
// Освобождаем память после обработки
const processPDF = async (buffer: ArrayBuffer) => {
  const doc = await PDFDocument.load(buffer)
  // ... обработка
  doc.cleanup()
}

Security Considerations

  1. Sandboxing:
Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'wasm-unsafe-eval';
  style-src 'self' 'unsafe-inline'
  1. Data Lifetime:
// Удаляем данные после использования
URL.revokeObjectURL(tempUrl)

Практическое Применение

Реализация Drag&Drop Upload

const onDrop = useCallback((acceptedFiles: File[]) => {
  const readers = acceptedFiles.map(file => {
    return new Promise<ArrayBuffer>((resolve) => {
      const reader = new FileReader()
      reader.onload = () => resolve(reader.result as ArrayBuffer)
      reader.readAsArrayBuffer(file)
    })
  })

  Promise.all(readers).then(processPDFs)
}, [])

Поддержка TypeScript Strict Mode

interface PDFOperationResult {
  success: boolean
  data?: Uint8Array
  error?: {
    code: 'INVALID_FILE' | 'PASSWORD_REQUIRED' | 'CORRUPTED'
    message: string
  }
}

const validatePDF = async (buffer: ArrayBuffer): Promise<PDFOperationResult> => {
  try {
    await PDFDocument.load(buffer)
    return { success: true }
  } catch (err) {
    // Type guard для ошибок pdf-lib
    if (err instanceof PDFLibError) {
      return {
        success: false,
        error: {
          code: mapPdfLibError(err),
          message: err.message
        }
      }
    }
    throw err
  }
}

Заключение

PDFLince демонстрирует, что сложные инструменты можно реализовать полностью на клиенте без компромиссов в:

Такой подход особенно актуален для:

Дальнейшее развитие может включать:


Источник: https://www.reddit.com/r/reactjs/comments/1r7zygt/i_built_a_privacy_focused_pdf_tool_with_nextjs_15/