feat: add best-effort PDF source converters (txt, html, docx)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||||
|
import { Document, Paragraph, TextRun, Packer } from 'docx';
|
||||||
import { register } from './registry.js';
|
import { register } from './registry.js';
|
||||||
|
|
||||||
async function renderHtmlToPdf(html, outputPath) {
|
async function renderHtmlToPdf(html, outputPath) {
|
||||||
@@ -42,9 +44,49 @@ async function convertDocxToPdf(inputPath, outputPath) {
|
|||||||
await renderHtmlToPdf(result.value, outputPath);
|
await renderHtmlToPdf(result.value, outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function extractPdfPageTexts(inputPath) {
|
||||||
|
const data = new Uint8Array(await fs.readFile(inputPath));
|
||||||
|
const doc = await pdfjsLib.getDocument({ data }).promise;
|
||||||
|
|
||||||
|
const pageTexts = [];
|
||||||
|
for (let pageNum = 1; pageNum <= doc.numPages; pageNum += 1) {
|
||||||
|
const page = await doc.getPage(pageNum);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
pageTexts.push(content.items.map((item) => item.str).join(' '));
|
||||||
|
}
|
||||||
|
return pageTexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertPdfToTxt(inputPath, outputPath) {
|
||||||
|
const pageTexts = await extractPdfPageTexts(inputPath);
|
||||||
|
await fs.writeFile(outputPath, pageTexts.join('\n\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertPdfToHtml(inputPath, outputPath) {
|
||||||
|
const pageTexts = await extractPdfPageTexts(inputPath);
|
||||||
|
const body = pageTexts.map((text) => `<p>${escapeHtml(text)}</p>`).join('\n');
|
||||||
|
await fs.writeFile(outputPath, `<html><body>${body}</body></html>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertPdfToDocx(inputPath, outputPath) {
|
||||||
|
const pageTexts = await extractPdfPageTexts(inputPath);
|
||||||
|
const doc = new Document({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
children: pageTexts.map((text) => new Paragraph({ children: [new TextRun(text)] })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const buffer = await Packer.toBuffer(doc);
|
||||||
|
await fs.writeFile(outputPath, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
export function registerDocumentConverters() {
|
export function registerDocumentConverters() {
|
||||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'html', convert: convertDocxToHtml });
|
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'html', convert: convertDocxToHtml });
|
||||||
register({ family: 'document', sourceFormat: 'txt', targetFormat: 'pdf', convert: convertTxtToPdf });
|
register({ family: 'document', sourceFormat: 'txt', targetFormat: 'pdf', convert: convertTxtToPdf });
|
||||||
register({ family: 'document', sourceFormat: 'html', targetFormat: 'pdf', convert: convertHtmlToPdf });
|
register({ family: 'document', sourceFormat: 'html', targetFormat: 'pdf', convert: convertHtmlToPdf });
|
||||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'pdf', convert: convertDocxToPdf });
|
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'pdf', convert: convertDocxToPdf });
|
||||||
|
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'txt', convert: convertPdfToTxt });
|
||||||
|
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'html', convert: convertPdfToHtml });
|
||||||
|
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'docx', convert: convertPdfToDocx });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
||||||
|
import { registerDocumentConverters } from '../../src/converters/document.js';
|
||||||
|
import { resolve } from '../../src/converters/registry.js';
|
||||||
|
|
||||||
|
let tmpDir;
|
||||||
|
let pdfFixturePath;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
registerDocumentConverters();
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'converter-document-from-pdf-'));
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const page = pdfDoc.addPage([600, 400]);
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
page.drawText('Extractable fixture text', { x: 50, y: 350, size: 24, font });
|
||||||
|
const bytes = await pdfDoc.save();
|
||||||
|
pdfFixturePath = path.join(tmpDir, 'fixture.pdf');
|
||||||
|
await fs.writeFile(pdfFixturePath, bytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PDF source document converters', () => {
|
||||||
|
it('extracts text from PDF to TXT', async () => {
|
||||||
|
const outputPath = path.join(tmpDir, 'output.txt');
|
||||||
|
const entry = resolve('pdf', 'txt');
|
||||||
|
|
||||||
|
await entry.convert(pdfFixturePath, outputPath);
|
||||||
|
|
||||||
|
const text = await fs.readFile(outputPath, 'utf8');
|
||||||
|
expect(text).toContain('Extractable fixture text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts text from PDF to HTML', async () => {
|
||||||
|
const outputPath = path.join(tmpDir, 'output.html');
|
||||||
|
const entry = resolve('pdf', 'html');
|
||||||
|
|
||||||
|
await entry.convert(pdfFixturePath, outputPath);
|
||||||
|
|
||||||
|
const html = await fs.readFile(outputPath, 'utf8');
|
||||||
|
expect(html).toContain('Extractable fixture text');
|
||||||
|
expect(html).toContain('<p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reconstructs PDF text into a DOCX (best-effort)', async () => {
|
||||||
|
const outputPath = path.join(tmpDir, 'output.docx');
|
||||||
|
const entry = resolve('pdf', 'docx');
|
||||||
|
|
||||||
|
await entry.convert(pdfFixturePath, outputPath);
|
||||||
|
|
||||||
|
const stat = await fs.stat(outputPath);
|
||||||
|
expect(stat.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const mammoth = await import('mammoth');
|
||||||
|
const result = await mammoth.default.convertToHtml({ path: outputPath });
|
||||||
|
expect(result.value).toContain('Extractable fixture text');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user