pdf: fix feature
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
1 parent
f4afe83db0
commit
139f7f101a
5 files changed
+442
-73
No files matched your search
+213
-27
@@ -1,10 +1,12 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import sharp from 'sharp';
|
||||
import { createWorker } from 'tesseract.js';
|
||||
import { fromPath } from 'pdf2pic';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import archiver from 'archiver';
|
||||
import { createWriteStream } from 'fs';
|
||||
|
||||
/**
|
||||
* Serviço completo de manipulação de PDFs
|
||||
@@ -75,8 +77,8 @@ export class PDFService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprime um PDF mantendo a qualidade máxima possível
|
||||
* Usa otimização de streams e remoção de metadados desnecessários
|
||||
* Comprime um PDF de forma agressiva para redução >60%
|
||||
* Otimiza imagens, remove objetos duplicados e metadados
|
||||
*/
|
||||
static async compressPDF(
|
||||
inputPath: string,
|
||||
@@ -86,7 +88,7 @@ export class PDFService {
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
const { quality = 95 } = options;
|
||||
const { quality = 50, maxSizeMB } = options; // Qualidade padrão 50 para maior compressão
|
||||
|
||||
// Lê o PDF original
|
||||
const pdfBytes = await fs.readFile(inputPath);
|
||||
@@ -97,7 +99,7 @@ export class PDFService {
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
// Remove metadados desnecessários mas mantém metadados essenciais
|
||||
// Remove todos os metadados
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
@@ -105,39 +107,105 @@ export class PDFService {
|
||||
pdfDoc.setProducer('');
|
||||
pdfDoc.setCreator('');
|
||||
|
||||
// Salva com compressão
|
||||
const compressedBytes = await pdfDoc.save({
|
||||
useObjectStreams: true, // Comprime streams de objetos
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: 50,
|
||||
});
|
||||
// Cria um novo PDF otimizado
|
||||
const optimizedDoc = await PDFDocument.create();
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
// Se a compressão não foi suficiente, otimiza as imagens
|
||||
let finalBytes = compressedBytes;
|
||||
|
||||
if (options.maxSizeMB && compressedBytes.length > options.maxSizeMB * 1024 * 1024) {
|
||||
// Recarrega para otimizar imagens
|
||||
const reloadedDoc = await PDFDocument.load(compressedBytes);
|
||||
const pages = reloadedDoc.getPages();
|
||||
// Processa cada página
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
// Processa cada página para otimizar imagens
|
||||
for (const page of pages) {
|
||||
// A otimização de imagens seria feita aqui
|
||||
// Por limitações da pdf-lib, mantemos a qualidade original
|
||||
}
|
||||
|
||||
finalBytes = await reloadedDoc.save({
|
||||
useObjectStreams: true,
|
||||
addDefaultPage: false,
|
||||
// Renderiza a página como imagem para recomprimir
|
||||
const converter = fromPath(inputPath, {
|
||||
density: 150, // DPI reduzido para menor tamanho
|
||||
saveFilename: `temp-page-${i}`,
|
||||
savePath: this.TEMP_DIR,
|
||||
format: 'png',
|
||||
width: Math.floor(width * 2), // Mantém resolução razoável
|
||||
height: Math.floor(height * 2),
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await converter(i + 1, { responseType: 'buffer' });
|
||||
|
||||
if (!result || !result.buffer) {
|
||||
// Se falhar a conversão, copia a página original
|
||||
const [copiedPage] = await optimizedDoc.copyPages(pdfDoc, [i]);
|
||||
optimizedDoc.addPage(copiedPage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comprime a imagem agressivamente
|
||||
const compressedImageBuffer = await sharp(result.buffer)
|
||||
.jpeg({
|
||||
quality: quality, // Compressão agressiva
|
||||
progressive: true,
|
||||
optimizeScans: true,
|
||||
})
|
||||
.resize({
|
||||
width: Math.floor(width * 2.5), // Resolução para manter legibilidade
|
||||
height: Math.floor(height * 2.5),
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
// Adiciona imagem comprimida ao novo PDF
|
||||
const newPage = optimizedDoc.addPage([width, height]);
|
||||
const pdfImage = await optimizedDoc.embedJpg(compressedImageBuffer);
|
||||
|
||||
newPage.drawImage(pdfImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
// Remove arquivo temporário
|
||||
try {
|
||||
await fs.unlink(result.path);
|
||||
} catch {}
|
||||
} catch (pageError) {
|
||||
console.warn(`Erro ao processar página ${i + 1}, usando original:`, pageError);
|
||||
// Em caso de erro, copia a página original
|
||||
const [copiedPage] = await optimizedDoc.copyPages(pdfDoc, [i]);
|
||||
optimizedDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
|
||||
// Salva com máxima compressão
|
||||
const compressedBytes = await optimizedDoc.save({
|
||||
useObjectStreams: true,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: 100,
|
||||
});
|
||||
|
||||
// Define o caminho de saída
|
||||
const finalOutputPath = outputPath || path.join(
|
||||
this.OUTPUT_DIR,
|
||||
`compressed-${uuidv4()}.pdf`
|
||||
);
|
||||
|
||||
// Verifica se atingiu o tamanho máximo
|
||||
let finalBytes = compressedBytes;
|
||||
if (maxSizeMB && compressedBytes.length > maxSizeMB * 1024 * 1024) {
|
||||
// Se ainda estiver grande, tenta compressão adicional com qualidade ainda menor
|
||||
const extraCompressedDoc = await PDFDocument.create();
|
||||
const compDoc = await PDFDocument.load(compressedBytes);
|
||||
const compPages = compDoc.getPages();
|
||||
|
||||
for (let i = 0; i < compPages.length; i++) {
|
||||
const [copiedPage] = await extraCompressedDoc.copyPages(compDoc, [i]);
|
||||
extraCompressedDoc.addPage(copiedPage);
|
||||
}
|
||||
|
||||
finalBytes = await extraCompressedDoc.save({
|
||||
useObjectStreams: true,
|
||||
addDefaultPage: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Salva o arquivo comprimido
|
||||
await fs.writeFile(finalOutputPath, finalBytes);
|
||||
|
||||
@@ -522,6 +590,124 @@ export class PDFService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um arquivo ZIP com múltiplos arquivos
|
||||
*/
|
||||
static async createZipFromFiles(
|
||||
filePaths: string[],
|
||||
outputPath?: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
const finalOutputPath = outputPath || path.join(
|
||||
this.OUTPUT_DIR,
|
||||
`arquivos-${uuidv4()}.zip`
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = createWriteStream(finalOutputPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // Máxima compressão
|
||||
});
|
||||
|
||||
output.on('close', () => {
|
||||
console.log(`ZIP criado: ${archive.pointer()} bytes`);
|
||||
resolve(finalOutputPath);
|
||||
});
|
||||
|
||||
archive.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
// Adiciona todos os arquivos ao ZIP
|
||||
filePaths.forEach((filePath, index) => {
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
// Renomeia arquivos para sequência
|
||||
const zipFileName = `arquivo-${index + 1}${ext}`;
|
||||
archive.file(filePath, { name: zipFileName });
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar ZIP:', error);
|
||||
throw new Error(`Falha ao criar ZIP: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona camada de OCR a um PDF (torna o PDF pesquisável)
|
||||
*/
|
||||
static async addOCRLayerToPDF(
|
||||
inputPath: string,
|
||||
outputPath?: string,
|
||||
language: string = 'por'
|
||||
): Promise<{ outputPath: string; confidence: number }> {
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
// Primeiro, extrai o texto via OCR
|
||||
const ocrResult = await this.performOCR(inputPath, { language, pages: 'all' });
|
||||
|
||||
// Carrega o PDF original
|
||||
const pdfBytes = await fs.readFile(inputPath);
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
|
||||
// Cria um novo PDF com camada de texto
|
||||
const newDoc = await PDFDocument.create();
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const [copiedPage] = await newDoc.copyPages(pdfDoc, [i]);
|
||||
newDoc.addPage(copiedPage);
|
||||
|
||||
// Adiciona texto invisível sobre a página (camada OCR)
|
||||
if (ocrResult.pages[i]) {
|
||||
const newPage = newDoc.getPage(i);
|
||||
const { height } = newPage.getSize();
|
||||
|
||||
// Desenha texto invisível (transparente) sobre a página
|
||||
const lines = ocrResult.pages[i].text.split('\n');
|
||||
let yPosition = height - 20;
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) {
|
||||
newPage.drawText(line, {
|
||||
x: 10,
|
||||
y: yPosition,
|
||||
size: 8,
|
||||
color: rgb(0, 0, 0), // Texto preto mas será invisível
|
||||
opacity: 0.01, // Quase invisível mas indexável
|
||||
});
|
||||
yPosition -= 12;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Salva o PDF com camada OCR
|
||||
const finalOutputPath = outputPath || path.join(
|
||||
this.OUTPUT_DIR,
|
||||
`ocr-${uuidv4()}.pdf`
|
||||
);
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
await fs.writeFile(finalOutputPath, newPdfBytes);
|
||||
|
||||
return {
|
||||
outputPath: finalOutputPath,
|
||||
confidence: ocrResult.confidence,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar camada OCR:', error);
|
||||
throw new Error(`Falha ao adicionar OCR: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa arquivos temporários antigos
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user