@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
215 lines (214 loc) • 8.22 kB
JavaScript
import path from 'path';
import fs from 'fs/promises';
import { capture } from '../utils.js';
// Direct imports
import pdfParse from 'pdf-parse';
import TurndownService from 'turndown';
// PDF output formats
export var PDFOutputFormat;
(function (PDFOutputFormat) {
PDFOutputFormat["TEXT"] = "text";
PDFOutputFormat["MARKDOWN"] = "markdown";
PDFOutputFormat["HTML"] = "html";
})(PDFOutputFormat || (PDFOutputFormat = {}));
/**
* Reads a PDF file and returns its content in the specified format
* @param filePath Path to the PDF file
* @param outputFormat Desired output format (text, markdown, or html)
* @returns FileResult object with the parsed content
*/
export async function readPDFFile(filePath, outputFormat = PDFOutputFormat.TEXT) {
try {
// Read the PDF file as a buffer
const dataBuffer = await fs.readFile(filePath);
// Parse the PDF
const pdfData = await pdfParse(dataBuffer);
// Extract the raw text
const rawText = pdfData.text;
let content;
let mimeType;
// Convert to the requested format
switch (outputFormat) {
case PDFOutputFormat.HTML:
// Convert to simple HTML (using paragraphs for line breaks)
content = convertPDFToHTML(rawText, pdfData, filePath);
mimeType = 'text/html';
break;
case PDFOutputFormat.MARKDOWN:
try {
// First convert to HTML, then to Markdown
const htmlContent = convertPDFToHTML(rawText, pdfData, filePath);
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*'
});
content = turndownService.turndown(htmlContent);
mimeType = 'text/markdown';
}
catch (error) {
capture('server_turndown_error', {
error: error instanceof Error ? error.message : String(error)
});
// Fallback to basic markdown conversion if turndown has an issue
content = convertPDFToBasicMarkdown(rawText, pdfData, filePath);
mimeType = 'text/markdown';
}
break;
// First convert to HTML, then to Markdown
const htmlContent = convertPDFToHTML(rawText, pdfData, filePath);
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*'
});
content = turndownService.turndown(htmlContent);
mimeType = 'text/markdown';
break;
case PDFOutputFormat.TEXT:
default:
// Format as readable text
content = formatPDFText(rawText, pdfData, filePath);
mimeType = 'text/plain';
break;
}
return {
content,
mimeType,
isImage: false
};
}
catch (error) {
capture('server_pdf_read_error', {
error: error instanceof Error ? error.name : 'Unknown',
errorMessage: error instanceof Error ? error.message : String(error)
});
throw new Error(`Failed to read PDF file: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Format PDF text to make it more readable as plain text
* @param text Raw text from PDF
* @param pdfData PDF data
* @param filePath Original file path (for filename)
* @returns Formatted text
*/
function formatPDFText(text, pdfData, filePath) {
const fileName = path.basename(filePath);
let output = `Document: ${fileName}\n`;
output += `Pages: ${pdfData.numpages}\n\n`;
// Add PDF metadata if available
if (pdfData.info) {
if (pdfData.info.Title)
output += `Title: ${pdfData.info.Title}\n`;
if (pdfData.info.Author)
output += `Author: ${pdfData.info.Author}\n`;
if (pdfData.info.Subject)
output += `Subject: ${pdfData.info.Subject}\n`;
if (pdfData.info.Keywords)
output += `Keywords: ${pdfData.info.Keywords}\n`;
if (pdfData.info.CreationDate)
output += `Creation Date: ${pdfData.info.CreationDate}\n`;
output += '\n';
}
// Format the actual text content
// Split by double line breaks to identify paragraphs
const paragraphs = text.split(/\n\s*\n/);
output += paragraphs.join('\n\n');
return output;
}
/**
* Convert PDF text to basic markdown format without using turndown
* @param text Raw text from PDF
* @param pdfData PDF data
* @param filePath Original file path (for filename)
* @returns Markdown text
*/
function convertPDFToBasicMarkdown(text, pdfData, filePath) {
const fileName = path.basename(filePath);
let output = `# ${fileName}\n\n`;
// Add PDF metadata if available
if (pdfData.info) {
if (pdfData.info.Title)
output += `**Title:** ${pdfData.info.Title}\n\n`;
if (pdfData.info.Author)
output += `**Author:** ${pdfData.info.Author}\n\n`;
if (pdfData.info.Subject)
output += `**Subject:** ${pdfData.info.Subject}\n\n`;
}
// Split by double line breaks to identify paragraphs
const paragraphs = text.split(/\n\s*\n/);
// Process each paragraph
paragraphs.forEach(paragraph => {
const trimmed = paragraph.trim();
if (trimmed) {
// Check if this looks like a heading (shorter, ends without punctuation)
if (trimmed.length < 80 && !trimmed.match(/[.,:;?!]$/)) {
output += `## ${trimmed}\n\n`;
}
else {
output += `${trimmed}\n\n`;
}
}
});
return output;
}
/**
* Convert PDF text to a simple HTML representation
* @param text Raw text from PDF
* @param pdfData PDF data
* @param filePath Original file path (for filename)
* @returns HTML string
*/
function convertPDFToHTML(text, pdfData, filePath) {
// This is a basic conversion that preserves paragraphs
const fileName = path.basename(filePath);
const title = pdfData.info && pdfData.info.Title ? pdfData.info.Title : fileName;
// Split by double line breaks to identify paragraphs
const paragraphs = text.split(/\n\s*\n/);
// Build HTML structure
let html = `<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { border-bottom: 1px solid #eee; padding-bottom: 10px; }
h2 { margin-top: 24px; }
pre { background-color: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
.metadata { color: #666; font-size: 0.9em; margin-bottom: 24px; }
</style>
</head>
<body>
<h1>${title}</h1>
`;
// Add metadata if available
if (pdfData.info) {
html += ' <div class="metadata">\n';
if (pdfData.info.Author)
html += ` <p><strong>Author:</strong> ${pdfData.info.Author}</p>\n`;
if (pdfData.info.Subject)
html += ` <p><strong>Subject:</strong> ${pdfData.info.Subject}</p>\n`;
if (pdfData.info.Keywords)
html += ` <p><strong>Keywords:</strong> ${pdfData.info.Keywords}</p>\n`;
if (pdfData.numpages)
html += ` <p><strong>Pages:</strong> ${pdfData.numpages}</p>\n`;
html += ' </div>\n';
}
// Add paragraphs
paragraphs.forEach(paragraph => {
const trimmed = paragraph.trim();
if (trimmed) {
// Check if this looks like a heading (shorter, ends without punctuation)
if (trimmed.length < 100 && !trimmed.match(/[.,:;?!]$/)) {
html += ` <h2>${trimmed}</h2>\n`;
}
else {
html += ` <p>${trimmed}</p>\n`;
}
}
});
html += `</body>
</html>`;
return html;
}