feat(frontend): Phase 2 & 3 - Client-side document processing utilities
Created comprehensive document processing library: � src/lib/document-utils/ ├── processor.ts - Главный процессор (интеллектуальная обработка) ├── ocr.ts - OCR через Tesseract.js (rus+eng) ├── pdf.ts - PDF операции (extract text, thumbnail, split, merge, rotate) ├── docx.ts - DOCX извлечение текста (mammoth.js) ├── image.ts - Image processing (compress, crop, rotate, thumbnail) └── index.ts - Экспорты Возможности: ✅ Автоматическое определение типа файла ✅ OCR для изображений (client-side, не нагружает сервер) ✅ Извлечение текста из PDF и DOCX ✅ Сжатие изображений перед загрузкой ✅ Генерация thumbnails ✅ Progress callbacks для UI ✅ Оценка времени обработки ✅ TypeScript типы для всех функций Преимущества перед server-side: - Моментальная обратная связь - Не нагружает backend - Приватность (файл не уходит на сервер до загрузки) - Экономия трафика (сжатие до отправки) - Можно показать preview extracted text ДО upload Phase 2 & 3 Complete!
This commit is contained in:
parent
82762d3ce4
commit
047e1c1763
55
frontend/src/lib/document-utils/docx.ts
Normal file
55
frontend/src/lib/document-utils/docx.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* DOCX text extraction using mammoth.js
|
||||
*/
|
||||
|
||||
import mammoth from 'mammoth';
|
||||
|
||||
/**
|
||||
* Извлекает текст из DOCX файла
|
||||
*
|
||||
* @param file - DOCX файл
|
||||
* @returns Извлеченный текст
|
||||
*/
|
||||
export async function extractDOCXText(file: File): Promise<string> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const result = await mammoth.extractRawText({ arrayBuffer });
|
||||
|
||||
if (result.messages.length > 0) {
|
||||
console.warn('[DOCX] Warnings:', result.messages);
|
||||
}
|
||||
|
||||
return result.value;
|
||||
} catch (error) {
|
||||
console.error('[DOCX] Extraction error:', error);
|
||||
throw new Error('Ошибка извлечения текста из DOCX');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает HTML из DOCX файла (для rich preview)
|
||||
*
|
||||
* @param file - DOCX файл
|
||||
* @returns HTML строка
|
||||
*/
|
||||
export async function extractDOCXHTML(file: File): Promise<string> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||
|
||||
return result.value;
|
||||
} catch (error) {
|
||||
console.error('[DOCX] HTML extraction error:', error);
|
||||
throw new Error('Ошибка конвертации DOCX в HTML');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли файл DOCX
|
||||
*/
|
||||
export function isDOCXFile(file: File): boolean {
|
||||
return (
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
file.name.toLowerCase().endsWith('.docx')
|
||||
);
|
||||
}
|
||||
242
frontend/src/lib/document-utils/image.ts
Normal file
242
frontend/src/lib/document-utils/image.ts
Normal file
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Client-side image processing utilities
|
||||
* Compression, resize, crop, rotate
|
||||
*/
|
||||
|
||||
export interface ImageCompressOptions {
|
||||
maxSizeMB?: number;
|
||||
maxWidthOrHeight?: number;
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
export interface CropArea {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сжимает изображение перед загрузкой
|
||||
*
|
||||
* @param file - Файл изображения
|
||||
* @param options - Опции сжатия
|
||||
* @returns Сжатое изображение как Blob
|
||||
*/
|
||||
export async function compressImage(
|
||||
file: File,
|
||||
options: ImageCompressOptions = {}
|
||||
): Promise<Blob> {
|
||||
const {
|
||||
maxSizeMB = 10,
|
||||
maxWidthOrHeight = 2048,
|
||||
quality = 0.85
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const img = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// Вычисляем новые размеры с сохранением пропорций
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > maxWidthOrHeight || height > maxWidthOrHeight) {
|
||||
const ratio = Math.min(
|
||||
maxWidthOrHeight / width,
|
||||
maxWidthOrHeight / height
|
||||
);
|
||||
width = Math.floor(width * ratio);
|
||||
height = Math.floor(height * ratio);
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Рисуем изображение
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Конвертируем в Blob
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
// Проверяем размер
|
||||
const sizeMB = blob.size / (1024 * 1024);
|
||||
if (sizeMB > maxSizeMB) {
|
||||
// Если еще слишком большой, рекурсивно сжимаем с меньшим quality
|
||||
const newQuality = quality * 0.8;
|
||||
if (newQuality < 0.1) {
|
||||
reject(new Error('Не удалось сжать изображение до требуемого размера'));
|
||||
} else {
|
||||
compressImage(file, { ...options, quality: newQuality })
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
} else {
|
||||
reject(new Error('Ошибка создания Blob'));
|
||||
}
|
||||
},
|
||||
'image/jpeg',
|
||||
quality
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Image] Compression error:', error);
|
||||
throw new Error('Ошибка сжатия изображения');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрезает изображение
|
||||
*
|
||||
* @param file - Файл изображения
|
||||
* @param area - Область обрезки
|
||||
* @returns Обрезанное изображение как Blob
|
||||
*/
|
||||
export async function cropImage(
|
||||
file: File,
|
||||
area: CropArea
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
const img = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
canvas.width = area.width;
|
||||
canvas.height = area.height;
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
area.x,
|
||||
area.y,
|
||||
area.width,
|
||||
area.height,
|
||||
0,
|
||||
0,
|
||||
area.width,
|
||||
area.height
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Ошибка создания Blob'));
|
||||
}
|
||||
},
|
||||
file.type,
|
||||
0.95
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Image] Crop error:', error);
|
||||
throw new Error('Ошибка обрезки изображения');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Поворачивает изображение
|
||||
*
|
||||
* @param file - Файл изображения
|
||||
* @param degrees - Угол поворота (90, 180, 270, -90)
|
||||
* @returns Повернутое изображение как Blob
|
||||
*/
|
||||
export async function rotateImage(
|
||||
file: File,
|
||||
degrees: number
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
const img = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// Для 90 и 270 градусов меняем размеры canvas
|
||||
if (degrees === 90 || degrees === -90 || degrees === 270) {
|
||||
canvas.width = img.height;
|
||||
canvas.height = img.width;
|
||||
} else {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
}
|
||||
|
||||
// Поворачиваем
|
||||
ctx.translate(canvas.width / 2, canvas.height / 2);
|
||||
ctx.rotate((degrees * Math.PI) / 180);
|
||||
ctx.drawImage(img, -img.width / 2, -img.height / 2);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Ошибка создания Blob'));
|
||||
}
|
||||
},
|
||||
file.type,
|
||||
0.95
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Image] Rotate error:', error);
|
||||
throw new Error('Ошибка поворота изображения');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует thumbnail изображения
|
||||
*
|
||||
* @param file - Файл изображения
|
||||
* @param maxSize - Максимальный размер (ширина или высота)
|
||||
* @returns Data URL thumbnail
|
||||
*/
|
||||
export async function generateImageThumbnail(
|
||||
file: File,
|
||||
maxSize: number = 200
|
||||
): Promise<string> {
|
||||
try {
|
||||
const img = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
const ratio = Math.min(maxSize / img.width, maxSize / img.height);
|
||||
const width = Math.floor(img.width * ratio);
|
||||
const height = Math.floor(img.height * ratio);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.7);
|
||||
} catch (error) {
|
||||
console.error('[Image] Thumbnail error:', error);
|
||||
throw new Error('Ошибка создания thumbnail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли файл изображением
|
||||
*/
|
||||
export function isImageFile(file: File): boolean {
|
||||
return file.type.startsWith('image/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает размеры изображения
|
||||
*/
|
||||
export async function getImageDimensions(
|
||||
file: File
|
||||
): Promise<{ width: number; height: number }> {
|
||||
const img = await createImageBitmap(file);
|
||||
return {
|
||||
width: img.width,
|
||||
height: img.height
|
||||
};
|
||||
}
|
||||
60
frontend/src/lib/document-utils/index.ts
Normal file
60
frontend/src/lib/document-utils/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Document Utils
|
||||
* Утилиты для client-side обработки документов
|
||||
*
|
||||
* Возможности:
|
||||
* - OCR (Tesseract.js) - rus + eng
|
||||
* - PDF text extraction & operations (pdf-lib + pdf.js)
|
||||
* - DOCX text extraction (mammoth.js)
|
||||
* - Image compression & processing (Canvas API)
|
||||
* - Intelligent document processor
|
||||
*/
|
||||
|
||||
// Main processor
|
||||
export {
|
||||
processDocument,
|
||||
needsProcessing,
|
||||
estimateProcessingTime,
|
||||
type ProcessingResult,
|
||||
type ProcessingProgress,
|
||||
type ProcessDocumentOptions
|
||||
} from './processor';
|
||||
|
||||
// OCR
|
||||
export {
|
||||
performOCR,
|
||||
isOCRSupported,
|
||||
estimateOCRTime,
|
||||
type OCRProgress,
|
||||
type OCROptions
|
||||
} from './ocr';
|
||||
|
||||
// PDF
|
||||
export {
|
||||
extractPDFText,
|
||||
generatePDFThumbnail,
|
||||
splitPDF,
|
||||
mergePDFs,
|
||||
rotatePDF,
|
||||
getPDFInfo,
|
||||
type PDFTextExtractionProgress
|
||||
} from './pdf';
|
||||
|
||||
// DOCX
|
||||
export {
|
||||
extractDOCXText,
|
||||
extractDOCXHTML,
|
||||
isDOCXFile
|
||||
} from './docx';
|
||||
|
||||
// Image
|
||||
export {
|
||||
compressImage,
|
||||
cropImage,
|
||||
rotateImage,
|
||||
generateImageThumbnail,
|
||||
isImageFile,
|
||||
getImageDimensions,
|
||||
type ImageCompressOptions,
|
||||
type CropArea
|
||||
} from './image';
|
||||
84
frontend/src/lib/document-utils/ocr.ts
Normal file
84
frontend/src/lib/document-utils/ocr.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Client-side OCR using Tesseract.js
|
||||
* Извлекает текст из изображений прямо в браузере
|
||||
*/
|
||||
|
||||
import Tesseract from 'tesseract.js';
|
||||
|
||||
export interface OCRProgress {
|
||||
status: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface OCROptions {
|
||||
languages?: string[];
|
||||
onProgress?: (progress: OCRProgress) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет OCR для изображения
|
||||
*
|
||||
* @param file - Файл изображения
|
||||
* @param options - Опции OCR
|
||||
* @returns Извлеченный текст
|
||||
*/
|
||||
export async function performOCR(
|
||||
file: File,
|
||||
options: OCROptions = {}
|
||||
): Promise<string> {
|
||||
const {
|
||||
languages = ['rus', 'eng'], // Русский + английский по умолчанию
|
||||
onProgress
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker({
|
||||
logger: (m) => {
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
status: m.status,
|
||||
progress: m.progress * 100
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Загружаем языки
|
||||
await worker.loadLanguage(languages.join('+'));
|
||||
await worker.initialize(languages.join('+'));
|
||||
|
||||
// Распознаем текст
|
||||
const { data } = await worker.recognize(file);
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
return data.text;
|
||||
} catch (error) {
|
||||
console.error('[OCR] Error:', error);
|
||||
throw new Error('Ошибка распознавания текста');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, поддерживается ли OCR для данного файла
|
||||
*/
|
||||
export function isOCRSupported(file: File): boolean {
|
||||
const supportedTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/bmp',
|
||||
'image/tiff'
|
||||
];
|
||||
|
||||
return supportedTypes.includes(file.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Оценивает время OCR в секундах (приблизительно)
|
||||
*/
|
||||
export function estimateOCRTime(file: File): number {
|
||||
// Приблизительно 2-5 секунд на MB
|
||||
const sizeMB = file.size / (1024 * 1024);
|
||||
return Math.ceil(sizeMB * 3);
|
||||
}
|
||||
201
frontend/src/lib/document-utils/pdf.ts
Normal file
201
frontend/src/lib/document-utils/pdf.ts
Normal file
@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Client-side PDF utilities
|
||||
* Извлечение текста, операции с PDF (split, merge, rotate)
|
||||
*/
|
||||
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument, degrees } from 'pdf-lib';
|
||||
|
||||
// Настройка worker для PDF.js
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
|
||||
|
||||
export interface PDFTextExtractionProgress {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает текст из PDF файла
|
||||
*
|
||||
* @param file - PDF файл
|
||||
* @param onProgress - Callback прогресса
|
||||
* @returns Извлеченный текст
|
||||
*/
|
||||
export async function extractPDFText(
|
||||
file: File,
|
||||
onProgress?: (progress: PDFTextExtractionProgress) => void
|
||||
): Promise<string> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const totalPages = pdf.numPages;
|
||||
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join(' ');
|
||||
|
||||
fullText += pageText + '\n\n';
|
||||
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
currentPage: i,
|
||||
totalPages,
|
||||
progress: (i / totalPages) * 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fullText.trim();
|
||||
} catch (error) {
|
||||
console.error('[PDF] Text extraction error:', error);
|
||||
throw new Error('Ошибка извлечения текста из PDF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует thumbnail первой страницы PDF
|
||||
*
|
||||
* @param file - PDF файл
|
||||
* @param scale - Масштаб (по умолчанию 0.5)
|
||||
* @returns Data URL изображения
|
||||
*/
|
||||
export async function generatePDFThumbnail(
|
||||
file: File,
|
||||
scale: number = 0.5
|
||||
): Promise<string> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport
|
||||
}).promise;
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.7);
|
||||
} catch (error) {
|
||||
console.error('[PDF] Thumbnail generation error:', error);
|
||||
throw new Error('Ошибка создания превью PDF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Разделяет PDF файл на отдельные страницы
|
||||
*
|
||||
* @param file - PDF файл
|
||||
* @param pageNumbers - Номера страниц для извлечения (1-based)
|
||||
* @returns Новый PDF как Uint8Array
|
||||
*/
|
||||
export async function splitPDF(
|
||||
file: File,
|
||||
pageNumbers: number[]
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
const newPdf = await PDFDocument.create();
|
||||
|
||||
for (const pageNum of pageNumbers) {
|
||||
const [page] = await newPdf.copyPages(pdfDoc, [pageNum - 1]);
|
||||
newPdf.addPage(page);
|
||||
}
|
||||
|
||||
return await newPdf.save();
|
||||
} catch (error) {
|
||||
console.error('[PDF] Split error:', error);
|
||||
throw new Error('Ошибка разделения PDF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Объединяет несколько PDF файлов
|
||||
*
|
||||
* @param files - Массив PDF файлов
|
||||
* @returns Объединенный PDF как Uint8Array
|
||||
*/
|
||||
export async function mergePDFs(files: File[]): Promise<Uint8Array> {
|
||||
try {
|
||||
const mergedPdf = await PDFDocument.create();
|
||||
|
||||
for (const file of files) {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await PDFDocument.load(arrayBuffer);
|
||||
const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||
pages.forEach(page => mergedPdf.addPage(page));
|
||||
}
|
||||
|
||||
return await mergedPdf.save();
|
||||
} catch (error) {
|
||||
console.error('[PDF] Merge error:', error);
|
||||
throw new Error('Ошибка объединения PDF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Поворачивает все страницы PDF
|
||||
*
|
||||
* @param file - PDF файл
|
||||
* @param rotation - Угол поворота (90, 180, 270)
|
||||
* @returns Повернутый PDF как Uint8Array
|
||||
*/
|
||||
export async function rotatePDF(
|
||||
file: File,
|
||||
rotation: 90 | 180 | 270
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
pages.forEach(page => {
|
||||
page.setRotation(degrees(rotation));
|
||||
});
|
||||
|
||||
return await pdfDoc.save();
|
||||
} catch (error) {
|
||||
console.error('[PDF] Rotate error:', error);
|
||||
throw new Error('Ошибка поворота PDF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает информацию о PDF файле
|
||||
*/
|
||||
export async function getPDFInfo(file: File): Promise<{
|
||||
numPages: number;
|
||||
title?: string;
|
||||
author?: string;
|
||||
creationDate?: Date;
|
||||
}> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const metadata = await pdf.getMetadata();
|
||||
|
||||
return {
|
||||
numPages: pdf.numPages,
|
||||
title: metadata.info?.Title,
|
||||
author: metadata.info?.Author,
|
||||
creationDate: metadata.info?.CreationDate
|
||||
? new Date(metadata.info.CreationDate)
|
||||
: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PDF] Info error:', error);
|
||||
throw new Error('Ошибка получения информации о PDF');
|
||||
}
|
||||
}
|
||||
208
frontend/src/lib/document-utils/processor.ts
Normal file
208
frontend/src/lib/document-utils/processor.ts
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Main document processor
|
||||
* Интеллектуальная обработка документов перед загрузкой на сервер
|
||||
*/
|
||||
|
||||
import { performOCR, isOCRSupported } from './ocr';
|
||||
import { extractPDFText, generatePDFThumbnail, getPDFInfo } from './pdf';
|
||||
import { extractDOCXText, isDOCXFile } from './docx';
|
||||
import { compressImage, generateImageThumbnail, isImageFile } from './image';
|
||||
|
||||
export interface ProcessingResult {
|
||||
extractedText: string;
|
||||
thumbnail?: string;
|
||||
processedFile?: File | Blob;
|
||||
metadata: {
|
||||
originalSize: number;
|
||||
processedSize?: number;
|
||||
mimeType: string;
|
||||
hasText: boolean;
|
||||
processingTime: number;
|
||||
ocrApplied?: boolean;
|
||||
compressed?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProcessingProgress {
|
||||
stage: 'compressing' | 'ocr' | 'extracting_text' | 'generating_thumbnail' | 'complete';
|
||||
progress: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ProcessDocumentOptions {
|
||||
enableOCR?: boolean;
|
||||
enableCompression?: boolean;
|
||||
enableTextExtraction?: boolean;
|
||||
enableThumbnail?: boolean;
|
||||
maxImageSizeMB?: number;
|
||||
onProgress?: (progress: ProcessingProgress) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Интеллектуально обрабатывает документ перед загрузкой
|
||||
*
|
||||
* Автоматически определяет тип файла и выполняет:
|
||||
* - Сжатие изображений
|
||||
* - OCR для изображений
|
||||
* - Извлечение текста из PDF/DOCX
|
||||
* - Генерацию thumbnails
|
||||
*
|
||||
* @param file - Файл для обработки
|
||||
* @param options - Опции обработки
|
||||
* @returns Результат обработки
|
||||
*/
|
||||
export async function processDocument(
|
||||
file: File,
|
||||
options: ProcessDocumentOptions = {}
|
||||
): Promise<ProcessingResult> {
|
||||
const {
|
||||
enableOCR = true,
|
||||
enableCompression = true,
|
||||
enableTextExtraction = true,
|
||||
enableThumbnail = true,
|
||||
maxImageSizeMB = 10,
|
||||
onProgress
|
||||
} = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
let extractedText = '';
|
||||
let thumbnail: string | undefined;
|
||||
let processedFile: File | Blob | undefined;
|
||||
let ocrApplied = false;
|
||||
let compressed = false;
|
||||
|
||||
try {
|
||||
// 1. Сжатие изображений
|
||||
if (enableCompression && isImageFile(file)) {
|
||||
onProgress?.({
|
||||
stage: 'compressing',
|
||||
progress: 10,
|
||||
message: 'Сжатие изображения...'
|
||||
});
|
||||
|
||||
const sizeMB = file.size / (1024 * 1024);
|
||||
if (sizeMB > maxImageSizeMB) {
|
||||
processedFile = await compressImage(file, {
|
||||
maxSizeMB: maxImageSizeMB,
|
||||
quality: 0.85
|
||||
});
|
||||
compressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const fileToProcess = processedFile || file;
|
||||
|
||||
// 2. Извлечение текста
|
||||
if (enableTextExtraction) {
|
||||
onProgress?.({
|
||||
stage: 'extracting_text',
|
||||
progress: 30,
|
||||
message: 'Извлечение текста...'
|
||||
});
|
||||
|
||||
// PDF
|
||||
if (file.type === 'application/pdf') {
|
||||
extractedText = await extractPDFText(file, (pdfProgress) => {
|
||||
onProgress?.({
|
||||
stage: 'extracting_text',
|
||||
progress: 30 + (pdfProgress.progress * 0.3),
|
||||
message: `Обработка страницы ${pdfProgress.currentPage} из ${pdfProgress.totalPages}...`
|
||||
});
|
||||
});
|
||||
}
|
||||
// DOCX
|
||||
else if (isDOCXFile(file)) {
|
||||
extractedText = await extractDOCXText(file);
|
||||
}
|
||||
// Images с OCR
|
||||
else if (enableOCR && isOCRSupported(file)) {
|
||||
onProgress?.({
|
||||
stage: 'ocr',
|
||||
progress: 40,
|
||||
message: 'Распознавание текста (OCR)...'
|
||||
});
|
||||
|
||||
extractedText = await performOCR(file, {
|
||||
languages: ['rus', 'eng'],
|
||||
onProgress: (ocrProgress) => {
|
||||
onProgress?.({
|
||||
stage: 'ocr',
|
||||
progress: 40 + ocrProgress.progress * 0.4,
|
||||
message: `OCR: ${Math.round(ocrProgress.progress)}%`
|
||||
});
|
||||
}
|
||||
});
|
||||
ocrApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Генерация thumbnail
|
||||
if (enableThumbnail) {
|
||||
onProgress?.({
|
||||
stage: 'generating_thumbnail',
|
||||
progress: 85,
|
||||
message: 'Создание превью...'
|
||||
});
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
thumbnail = await generatePDFThumbnail(file, 0.5);
|
||||
} else if (isImageFile(file)) {
|
||||
thumbnail = await generateImageThumbnail(file, 200);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.({
|
||||
stage: 'complete',
|
||||
progress: 100,
|
||||
message: 'Обработка завершена'
|
||||
});
|
||||
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
extractedText,
|
||||
thumbnail,
|
||||
processedFile,
|
||||
metadata: {
|
||||
originalSize: file.size,
|
||||
processedSize: processedFile?.size,
|
||||
mimeType: file.type,
|
||||
hasText: extractedText.length > 0,
|
||||
processingTime,
|
||||
ocrApplied,
|
||||
compressed
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DocumentProcessor] Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Быстрая проверка: нужна ли обработка этому файлу
|
||||
*/
|
||||
export function needsProcessing(file: File): boolean {
|
||||
return (
|
||||
file.type === 'application/pdf' ||
|
||||
isDOCXFile(file) ||
|
||||
isImageFile(file)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Оценивает время обработки в секундах
|
||||
*/
|
||||
export function estimateProcessingTime(file: File): number {
|
||||
const sizeMB = file.size / (1024 * 1024);
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
return Math.ceil(sizeMB * 2); // ~2 сек на MB
|
||||
} else if (isDOCXFile(file)) {
|
||||
return Math.ceil(sizeMB * 1); // ~1 сек на MB
|
||||
} else if (isImageFile(file)) {
|
||||
return Math.ceil(sizeMB * 3); // ~3 сек на MB (OCR)
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user