/* * ============================================================================= * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform * ----------------------------------------------------------------------------- * File: server/services/pdfService.ts * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Website: https://www.spboucher.ai * Demo: https://www.vquant.ai * License: MIT (see LICENSE) * * Copyright © 2026 Simon-Pierre Boucher. All rights reserved. * ============================================================================= */ import { exec } from 'child_process'; import { promisify } from 'util'; import fs from 'fs'; import path from 'path'; import puppeteer from 'puppeteer'; import { marked } from 'marked'; const execAsync = promisify(exec); // VQuant SVG logo as data URI for PDF embedding const VIBEQUANT_LOGO_SVG = ``; // Convert SVG to base64 data URI const LOGO_DATA_URI = `data:image/svg+xml;base64,${Buffer.from(VIBEQUANT_LOGO_SVG).toString('base64')}`; interface MarkdownPDFOptions { question: string; answer: string; } /** * Convert image paths to base64 data URLs for embedding in PDF */ function convertImagesToBase64(markdown: string): string { // Find all image references like  const imageRegex = /!\[([^\]]*)\]\(\/figures\/([^)]+)\)/g; let result = markdown; let match; while ((match = imageRegex.exec(markdown)) !== null) { const altText = match[1]; const filename = match[2]; const fullMatch = match[0]; // Try to read the image file const imagePath = path.join(process.cwd(), 'dist', 'public', 'figures', filename); try { if (fs.existsSync(imagePath)) { // Read image and convert to base64 const imageBuffer = fs.readFileSync(imagePath); const base64Image = imageBuffer.toString('base64'); const mimeType = 'image/png'; // All our figures are PNG // Replace with base64 data URL const dataUrl = `data:${mimeType};base64,${base64Image}`; const replacement = ``; result = result.replace(fullMatch, replacement); console.log(`✓ Converted image to base64: ${filename}`); } else { console.warn(`⚠ Image not found: ${imagePath}`); } } catch (error) { console.error(`✗ Error reading image ${filename}:`, error.message); } } return result; } /** * Convert markdown to HTML with styling */ function markdownToHTML(markdown: string, title: string): string { // Convert images to base64 first const markdownWithBase64Images = convertImagesToBase64(markdown); // Protect LaTeX math from markdown parser: // $$...$$ (display) and $...$ (inline) → placeholders, then restore after let protected_ = markdownWithBase64Images; const mathBlocks: string[] = []; // Display math $$...$$ protected_ = protected_.replace(/\$\$([\s\S]+?)\$\$/g, (_match, expr) => { const idx = mathBlocks.length; mathBlocks.push(expr); return `%%DISPLAYMATH${idx}%%`; }); // Inline math $...$ (not $$) protected_ = protected_.replace(/\$([^\$\n]+?)\$/g, (_match, expr) => { const idx = mathBlocks.length; mathBlocks.push(expr); return `%%INLINEMATH${idx}%%`; }); let htmlBody = marked.parse(protected_) as string; // Restore math with KaTeX spans htmlBody = htmlBody.replace(/%%DISPLAYMATH(\d+)%%/g, (_m, idx) => { return `\\[${mathBlocks[parseInt(idx)]}\\]`; }); htmlBody = htmlBody.replace(/%%INLINEMATH(\d+)%%/g, (_m, idx) => { return `\\(${mathBlocks[parseInt(idx)]}\\)`; }); return `