feat: add comprehensive PDF support

- Add jsPDF for PDF generation from text/Markdown/HTML
- Add PDF.js for PDF text extraction (read PDFs)
- Support PDF → Text/Markdown conversions
- Support Markdown/HTML/Text → PDF conversions
- Implement page-by-page PDF text extraction
- Automatic pagination and formatting for generated PDFs

Supported PDF operations:
- Extract text from PDF files (all pages)
- Convert PDF to Markdown or plain text
- Create formatted PDFs from Markdown, HTML, or plain text
- Automatic text wrapping and page breaks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-17 11:13:09 +01:00
parent 9de639b138
commit b899989b3e
5 changed files with 658 additions and 2 deletions

View File

@@ -7,7 +7,7 @@ A modern, browser-based file conversion application built with Next.js 16, Tailw
- **🎬 Video Conversion** - Convert between MP4, WebM, AVI, MOV, MKV, and GIF
- **🎵 Audio Conversion** - Convert between MP3, WAV, OGG, AAC, and FLAC
- **🖼️ Image Conversion** - Convert between PNG, JPG, WebP, GIF, BMP, TIFF, and SVG
- **📄 Document Conversion** - Convert between Markdown, HTML, and Plain Text
- **📄 Document Conversion** - Convert between PDF, Markdown, HTML, and Plain Text
- **🔒 Privacy First** - All conversions happen locally in your browser, no server uploads
- **⚡ Fast & Efficient** - Powered by WebAssembly for near-native performance
- **🎨 Beautiful UI** - Modern, responsive design with dark/light theme support
@@ -26,6 +26,8 @@ A modern, browser-based file conversion application built with Next.js 16, Tailw
- **Marked** - Markdown to HTML conversion
- **Turndown** - HTML to Markdown conversion
- **DOMPurify** - HTML sanitization
- **jsPDF** - PDF generation
- **PDF.js** - PDF text extraction
- **Fuse.js** - Fuzzy search for format selection
- **Lucide React** - Beautiful icon library
@@ -115,13 +117,21 @@ convert-ui/
- **Input/Output:** PNG, JPG, WebP, GIF, BMP, TIFF, SVG
### Documents
- **PDF → Text/Markdown** - Extract text from PDF files with page-by-page processing
- **Markdown/HTML/Text → PDF** - Generate formatted PDF documents
- **Markdown → HTML** - Full GitHub Flavored Markdown support with styling
- **HTML → Markdown** - Clean conversion with formatting preservation
- **Markdown ↔ Plain Text** - Strip or add basic formatting
- **HTML → Plain Text** - Extract text content
- **Plain Text → HTML** - Convert to formatted HTML document
**Note:** Uses lightweight JavaScript libraries (marked, turndown) instead of Pandoc WASM for fast, reliable conversions.
**Supported PDF Operations:**
- Read PDFs and extract all text content
- Convert extracted text to Markdown or plain text
- Create PDFs from Markdown, HTML, or plain text
- Automatic pagination and formatting
**Note:** Uses PDF.js for reading and jsPDF for generation. Lightweight JavaScript libraries (marked, turndown) used instead of Pandoc WASM for fast, reliable conversions.
## How It Works

View File

@@ -1,6 +1,13 @@
import { marked } from 'marked';
import TurndownService from 'turndown';
import type { ConversionOptions, ProgressCallback, ConversionResult } from '@/types/conversion';
import {
pdfToText,
pdfToMarkdown,
markdownToPDF,
htmlToPDF,
plainTextToPDF,
} from './pdfService';
// Import DOMPurify only on client side
let DOMPurify: any;
@@ -34,6 +41,31 @@ export async function convertWithPandoc(
if (onProgress) onProgress(50);
// Handle PDF conversions
if (inputExt === 'pdf') {
// PDF input
if (outputFormat === 'txt') {
return await pdfToText(file, onProgress);
} else if (outputFormat === 'md' || outputFormat === 'markdown') {
return await pdfToMarkdown(file, onProgress);
} else {
throw new Error(`Conversion from PDF to ${outputFormat} not supported`);
}
}
// Handle conversions TO PDF
if (outputFormat === 'pdf') {
if (inputExt === 'md' || inputExt === 'markdown') {
return await markdownToPDF(file, onProgress);
} else if (inputExt === 'html' || inputExt === 'htm') {
return await htmlToPDF(file, onProgress);
} else if (inputExt === 'txt') {
return await plainTextToPDF(file, onProgress);
} else {
throw new Error(`Conversion from ${inputExt} to PDF not supported`);
}
}
// Perform conversion based on input and output formats
if (inputExt === 'md' || inputExt === 'markdown') {
// Markdown input

View File

@@ -0,0 +1,334 @@
import { jsPDF } from 'jspdf';
import type { ConversionOptions, ProgressCallback, ConversionResult } from '@/types/conversion';
/**
* Extract text from PDF file
*/
export async function extractTextFromPDF(file: File, onProgress?: ProgressCallback): Promise<string> {
if (onProgress) onProgress(10);
// Dynamically import pdfjs-dist (client-side only)
const pdfjsLib = await import('pdfjs-dist');
// Set worker source
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.mjs`;
if (onProgress) onProgress(20);
// Read file as ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
if (onProgress) onProgress(30);
// Load PDF document
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
if (onProgress) onProgress(50);
const numPages = pdf.numPages;
let fullText = '';
// Extract text from each page
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
// Combine text items
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + '\n\n';
// Update progress
if (onProgress) {
const progress = 50 + (pageNum / numPages) * 40;
onProgress(Math.round(progress));
}
}
if (onProgress) onProgress(100);
return fullText.trim();
}
/**
* Convert PDF to text
*/
export async function pdfToText(
file: File,
onProgress?: ProgressCallback
): Promise<ConversionResult> {
const startTime = Date.now();
try {
const text = await extractTextFromPDF(file, onProgress);
const blob = new Blob([text], { type: 'text/plain' });
return {
success: true,
blob,
duration: Date.now() - startTime,
};
} catch (error) {
console.error('[PDF Converter] PDF to text error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to extract text from PDF',
duration: Date.now() - startTime,
};
}
}
/**
* Convert PDF to Markdown
*/
export async function pdfToMarkdown(
file: File,
onProgress?: ProgressCallback
): Promise<ConversionResult> {
const startTime = Date.now();
try {
const text = await extractTextFromPDF(file, (progress) => {
if (onProgress) onProgress(progress * 0.9); // Use 90% for extraction
});
// Basic text to markdown conversion (paragraphs)
const markdown = text
.split('\n\n')
.filter(p => p.trim())
.join('\n\n');
if (onProgress) onProgress(100);
const blob = new Blob([markdown], { type: 'text/markdown' });
return {
success: true,
blob,
duration: Date.now() - startTime,
};
} catch (error) {
console.error('[PDF Converter] PDF to markdown error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to convert PDF to Markdown',
duration: Date.now() - startTime,
};
}
}
/**
* Convert text to PDF
*/
export async function textToPDF(
text: string,
filename: string = 'document.pdf',
onProgress?: ProgressCallback
): Promise<Blob> {
if (onProgress) onProgress(20);
const doc = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
});
if (onProgress) onProgress(40);
// Set font and size
doc.setFont('helvetica');
doc.setFontSize(12);
// Page dimensions
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 20;
const maxWidth = pageWidth - 2 * margin;
const lineHeight = 7;
let y = margin;
if (onProgress) onProgress(60);
// Split text into lines
const lines = doc.splitTextToSize(text, maxWidth);
// Add lines to PDF
for (let i = 0; i < lines.length; i++) {
// Check if we need a new page
if (y + lineHeight > pageHeight - margin) {
doc.addPage();
y = margin;
}
doc.text(lines[i], margin, y);
y += lineHeight;
// Update progress
if (onProgress && i % 10 === 0) {
const progress = 60 + (i / lines.length) * 30;
onProgress(Math.round(progress));
}
}
if (onProgress) onProgress(90);
// Generate PDF blob
const pdfBlob = doc.output('blob');
if (onProgress) onProgress(100);
return pdfBlob;
}
/**
* Convert Markdown to PDF
*/
export async function markdownToPDF(
file: File,
onProgress?: ProgressCallback
): Promise<ConversionResult> {
const startTime = Date.now();
try {
if (onProgress) onProgress(10);
// Read markdown content
const markdown = await file.text();
if (onProgress) onProgress(20);
// Import marked for markdown parsing
const { marked } = await import('marked');
// Parse markdown to HTML
const html = await marked.parse(markdown);
if (onProgress) onProgress(40);
// Strip HTML tags for plain text
const text = html
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
if (onProgress) onProgress(60);
// Generate PDF
const pdfBlob = await textToPDF(text, file.name.replace(/\.md$/, '.pdf'), (progress) => {
if (onProgress) onProgress(60 + progress * 0.4);
});
return {
success: true,
blob: pdfBlob,
duration: Date.now() - startTime,
};
} catch (error) {
console.error('[PDF Converter] Markdown to PDF error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to convert Markdown to PDF',
duration: Date.now() - startTime,
};
}
}
/**
* Convert HTML to PDF
*/
export async function htmlToPDF(
file: File,
onProgress?: ProgressCallback
): Promise<ConversionResult> {
const startTime = Date.now();
try {
if (onProgress) onProgress(10);
// Read HTML content
const html = await file.text();
if (onProgress) onProgress(30);
// Strip HTML tags for plain text
const text = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
if (onProgress) onProgress(50);
// Generate PDF
const pdfBlob = await textToPDF(text, file.name.replace(/\.html?$/, '.pdf'), (progress) => {
if (onProgress) onProgress(50 + progress * 0.5);
});
return {
success: true,
blob: pdfBlob,
duration: Date.now() - startTime,
};
} catch (error) {
console.error('[PDF Converter] HTML to PDF error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to convert HTML to PDF',
duration: Date.now() - startTime,
};
}
}
/**
* Convert plain text to PDF
*/
export async function plainTextToPDF(
file: File,
onProgress?: ProgressCallback
): Promise<ConversionResult> {
const startTime = Date.now();
try {
if (onProgress) onProgress(10);
const text = await file.text();
if (onProgress) onProgress(30);
const pdfBlob = await textToPDF(text, file.name.replace(/\.txt$/, '.pdf'), (progress) => {
if (onProgress) onProgress(30 + progress * 0.7);
});
return {
success: true,
blob: pdfBlob,
duration: Date.now() - startTime,
};
} catch (error) {
console.error('[PDF Converter] Text to PDF error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to convert text to PDF',
duration: Date.now() - startTime,
};
}
}

View File

@@ -15,9 +15,11 @@
"clsx": "^2.1.1",
"dompurify": "^3.2.2",
"fuse.js": "^7.1.0",
"jspdf": "^2.5.2",
"lucide-react": "^0.553.0",
"marked": "^15.0.4",
"next": "^16.0.0",
"pdfjs-dist": "^4.10.38",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1",

278
pnpm-lock.yaml generated
View File

@@ -26,6 +26,9 @@ importers:
fuse.js:
specifier: ^7.1.0
version: 7.1.0
jspdf:
specifier: ^2.5.2
version: 2.5.2
lucide-react:
specifier: ^0.553.0
version: 0.553.0(react@19.2.0)
@@ -35,6 +38,9 @@ importers:
next:
specifier: ^16.0.0
version: 16.0.3(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
pdfjs-dist:
specifier: ^4.10.38
version: 4.10.38
react:
specifier: ^19.0.0
version: 19.2.0
@@ -137,6 +143,10 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -383,6 +393,70 @@ packages:
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
'@napi-rs/canvas-android-arm64@0.1.82':
resolution: {integrity: sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.82':
resolution: {integrity: sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.82':
resolution: {integrity: sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.82':
resolution: {integrity: sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.82':
resolution: {integrity: sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.82':
resolution: {integrity: sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.82':
resolution: {integrity: sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.82':
resolution: {integrity: sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.82':
resolution: {integrity: sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-win32-x64-msvc@0.1.82':
resolution: {integrity: sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.82':
resolution: {integrity: sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -565,6 +639,9 @@ packages:
'@types/node@22.19.1':
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -796,6 +873,11 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
hasBin: true
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -811,6 +893,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
baseline-browser-mapping@2.8.28:
resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==}
hasBin: true
@@ -830,6 +916,11 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
btoa@1.2.1:
resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
engines: {node: '>= 0.4.0'}
hasBin: true
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -849,6 +940,10 @@ packages:
caniuse-lite@1.0.30001755:
resolution: {integrity: sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==}
canvg@3.0.11:
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
engines: {node: '>=10.0.0'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -873,10 +968,16 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
csstype@3.2.2:
resolution: {integrity: sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==}
@@ -931,6 +1032,9 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dompurify@2.5.8:
resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==}
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
@@ -1133,6 +1237,9 @@ packages:
picomatch:
optional: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -1256,6 +1363,10 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1423,6 +1534,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
jspdf@2.5.2:
resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==}
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -1663,6 +1777,13 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
pdfjs-dist@4.10.38:
resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==}
engines: {node: '>=20'}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1700,6 +1821,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
react-dom@19.2.0:
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
peerDependencies:
@@ -1716,6 +1840,9 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
@@ -1740,6 +1867,10 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rgbcolor@1.0.1:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -1814,6 +1945,10 @@ packages:
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -1870,6 +2005,10 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
svg-pathdata@6.0.3:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@@ -1880,6 +2019,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -1954,6 +2096,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -2076,6 +2221,8 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@babel/runtime@7.28.4': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -2300,6 +2447,50 @@ snapshots:
'@mixmark-io/domino@2.2.0': {}
'@napi-rs/canvas-android-arm64@0.1.82':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.82':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.82':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.82':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.82':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.82':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.82':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.82':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.82':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.82':
optional: true
'@napi-rs/canvas@0.1.82':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.82
'@napi-rs/canvas-darwin-arm64': 0.1.82
'@napi-rs/canvas-darwin-x64': 0.1.82
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.82
'@napi-rs/canvas-linux-arm64-gnu': 0.1.82
'@napi-rs/canvas-linux-arm64-musl': 0.1.82
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.82
'@napi-rs/canvas-linux-x64-gnu': 0.1.82
'@napi-rs/canvas-linux-x64-musl': 0.1.82
'@napi-rs/canvas-win32-x64-msvc': 0.1.82
optional: true
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.7.1
@@ -2441,6 +2632,9 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/raf@3.4.3':
optional: true
'@types/react-dom@19.2.3(@types/react@19.2.5)':
dependencies:
'@types/react': 19.2.5
@@ -2698,6 +2892,8 @@ snapshots:
async-function@1.0.0: {}
atob@2.1.2: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -2708,6 +2904,9 @@ snapshots:
balanced-match@1.0.2: {}
base64-arraybuffer@1.0.2:
optional: true
baseline-browser-mapping@2.8.28: {}
brace-expansion@1.1.12:
@@ -2731,6 +2930,8 @@ snapshots:
node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.28.0)
btoa@1.2.1: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -2752,6 +2953,18 @@ snapshots:
caniuse-lite@1.0.30001755: {}
canvg@3.0.11:
dependencies:
'@babel/runtime': 7.28.4
'@types/raf': 3.4.3
core-js: 3.46.0
raf: 3.4.1
regenerator-runtime: 0.13.11
rgbcolor: 1.0.1
stackblur-canvas: 2.7.0
svg-pathdata: 6.0.3
optional: true
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -2771,12 +2984,20 @@ snapshots:
convert-source-map@2.0.0: {}
core-js@3.46.0:
optional: true
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
optional: true
csstype@3.2.2: {}
damerau-levenshtein@1.0.8: {}
@@ -2827,6 +3048,9 @@ snapshots:
dependencies:
esutils: 2.0.3
dompurify@2.5.8:
optional: true
dompurify@3.3.0:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -3184,6 +3408,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fflate@0.8.2: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -3306,6 +3532,12 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
optional: true
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -3470,6 +3702,18 @@ snapshots:
json5@2.2.3: {}
jspdf@2.5.2:
dependencies:
'@babel/runtime': 7.28.4
atob: 2.1.2
btoa: 1.2.1
fflate: 0.8.2
optionalDependencies:
canvg: 3.0.11
core-js: 3.46.0
dompurify: 2.5.8
html2canvas: 1.4.1
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.9
@@ -3692,6 +3936,13 @@ snapshots:
path-parse@1.0.7: {}
pdfjs-dist@4.10.38:
optionalDependencies:
'@napi-rs/canvas': 0.1.82
performance-now@2.1.0:
optional: true
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -3724,6 +3975,11 @@ snapshots:
queue-microtask@1.2.3: {}
raf@3.4.1:
dependencies:
performance-now: 2.1.0
optional: true
react-dom@19.2.0(react@19.2.0):
dependencies:
react: 19.2.0
@@ -3744,6 +4000,9 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
regenerator-runtime@0.13.11:
optional: true
regexp.prototype.flags@1.5.4:
dependencies:
call-bind: 1.0.8
@@ -3771,6 +4030,9 @@ snapshots:
reusify@1.1.0: {}
rgbcolor@1.0.1:
optional: true
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -3892,6 +4154,9 @@ snapshots:
stable-hash@0.0.5: {}
stackblur-canvas@2.7.0:
optional: true
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -3964,12 +4229,20 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svg-pathdata@6.0.3:
optional: true
tailwind-merge@3.4.0: {}
tailwindcss@4.1.17: {}
tapable@2.3.0: {}
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
optional: true
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -4089,6 +4362,11 @@ snapshots:
dependencies:
punycode: 2.3.1
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
optional: true
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0