file2md 1.1.10 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,801 +0,0 @@
1
- import path from 'node:path';
2
- import fs from 'node:fs/promises';
3
- import { Buffer } from 'node:buffer';
4
- import libre from 'libreoffice-convert';
5
- import { fromBuffer } from 'pdf2pic';
6
- import { promisify } from 'node:util';
7
- import { ParseError } from '../types/errors.js';
8
- import { LibreOfficeDetector } from './libreoffice-detector.js';
9
- import { LibreOfficeConverter } from './libreoffice-converter.js';
10
- import { PptxVisualParser } from './pptx-visual-parser.js';
11
- import { PuppeteerRenderer } from './puppeteer-renderer.js';
12
- // Promisify libreoffice-convert
13
- const convertAsync = promisify(libre.convert);
14
- export class SlideRenderer {
15
- outputDir;
16
- converter;
17
- constructor(outputDir) {
18
- this.outputDir = outputDir;
19
- this.converter = new LibreOfficeConverter();
20
- }
21
- /**
22
- * Convert PPTX buffer to individual slide images
23
- */
24
- async renderSlidesToImages(pptxBuffer, options = {}) {
25
- const { quality = 90, density = 150, format = 'png', saveBase64 = false, useVisualLayouts = true, usePuppeteer = false, puppeteerOptions } = options;
26
- try {
27
- // Ensure output directory exists
28
- await fs.mkdir(this.outputDir, { recursive: true });
29
- console.log('Created slide output directory:', this.outputDir);
30
- // Step 1: Optionally parse visual layouts for enhanced rendering
31
- let visualLayouts;
32
- if (useVisualLayouts || usePuppeteer) {
33
- try {
34
- console.log('Parsing visual layouts for enhanced rendering...');
35
- const visualParser = new PptxVisualParser();
36
- visualLayouts = await visualParser.parseVisualElements(pptxBuffer);
37
- console.log(`Extracted visual layouts for ${visualLayouts.length} slides`);
38
- }
39
- catch (visualError) {
40
- console.warn('Visual layout parsing failed, continuing with standard rendering:', visualError);
41
- }
42
- }
43
- // Step 2: Try Puppeteer rendering first if requested and visual layouts are available
44
- if (usePuppeteer && visualLayouts && visualLayouts.length > 0) {
45
- try {
46
- console.log('Attempting Puppeteer browser-based rendering...');
47
- const puppeteerResult = await this.renderWithPuppeteer(visualLayouts, {
48
- ...puppeteerOptions,
49
- format: format === 'jpg' ? 'jpeg' : format,
50
- quality
51
- });
52
- if (puppeteerResult.slideImages.length > 0) {
53
- console.log(`Puppeteer rendering successful: ${puppeteerResult.slideImages.length} slides`);
54
- return {
55
- slideImages: puppeteerResult.slideImages,
56
- slideCount: puppeteerResult.slideCount,
57
- visualLayouts,
58
- metadata: {
59
- ...puppeteerResult.metadata,
60
- hasVisualLayouts: true
61
- }
62
- };
63
- }
64
- }
65
- catch (puppeteerError) {
66
- console.warn('Puppeteer rendering failed, falling back to LibreOffice:', puppeteerError);
67
- }
68
- }
69
- // Step 3: Fallback to LibreOffice PDF rendering
70
- console.log('Converting PPTX to PDF using LibreOffice...');
71
- const pdfBuffer = await this.convertPptxToPdf(pptxBuffer);
72
- console.log('PPTX to PDF conversion successful, PDF size:', pdfBuffer.length);
73
- // Step 3: Convert PDF to individual slide images
74
- console.log('Converting PDF to slide images...');
75
- const slideImages = await this.convertPdfToSlideImages(pdfBuffer, { quality, density, format, saveBase64 });
76
- console.log(`Generated ${slideImages.length} slide images`);
77
- // Step 4: Enhance slides using visual layouts if available
78
- if (visualLayouts && slideImages.length === visualLayouts.length) {
79
- console.log('Enhancing slide images with visual layout information...');
80
- await this.enhanceSlideImagesWithLayouts(slideImages, visualLayouts, { quality, density, format });
81
- }
82
- // Verify images were actually created
83
- for (const slide of slideImages) {
84
- const exists = await fs.access(slide.savedPath).then(() => true).catch(() => false);
85
- console.log(`Slide image ${slide.savedPath} exists:`, exists);
86
- }
87
- return {
88
- slideImages,
89
- slideCount: slideImages.length,
90
- visualLayouts,
91
- metadata: {
92
- format,
93
- quality,
94
- density,
95
- hasVisualLayouts: visualLayouts !== undefined
96
- }
97
- };
98
- }
99
- catch (error) {
100
- const message = error instanceof Error ? error.message : 'Unknown error';
101
- console.error('SlideRenderer error:', message);
102
- throw new ParseError('SlideRenderer', `Failed to render slides: ${message}`, error);
103
- }
104
- }
105
- /**
106
- * Convert PPTX buffer to PDF buffer using enhanced LibreOffice converter
107
- */
108
- async convertPptxToPdf(pptxBuffer) {
109
- // Check LibreOffice installation first
110
- const detector = LibreOfficeDetector.getInstance();
111
- const libreOfficeInfo = await detector.checkLibreOfficeInstallation();
112
- if (!libreOfficeInfo.installed) {
113
- console.error('LibreOffice is not installed on this system.');
114
- console.error('Error:', libreOfficeInfo.error);
115
- console.log('\n' + detector.getInstallationInstructions());
116
- console.log('Download URL:', detector.getDownloadUrl());
117
- // Continue with alternative method
118
- console.log('Attempting alternative slide screenshot generation...');
119
- return await this.createAlternativeSlideImages(pptxBuffer);
120
- }
121
- // Check version compatibility
122
- if (!detector.isVersionSupported(libreOfficeInfo.version)) {
123
- console.warn(`LibreOffice version ${libreOfficeInfo.version} is below the recommended version 7.0`);
124
- }
125
- console.log(`LibreOffice detected: ${libreOfficeInfo.path} (version ${libreOfficeInfo.version})`);
126
- // Try enhanced LibreOffice conversion with progress tracking
127
- try {
128
- console.log('Starting enhanced LibreOffice conversion...');
129
- const pdfBuffer = await this.converter.convertWithProgress(pptxBuffer, {
130
- quality: 'maximum',
131
- timeout: 60000,
132
- additionalArgs: ['--nofirststartwizard']
133
- }, (progress) => {
134
- console.log(`Conversion progress: ${progress.stage} - ${progress.message}`);
135
- });
136
- if (pdfBuffer && pdfBuffer.length > 0) {
137
- console.log('Enhanced LibreOffice conversion successful, PDF size:', pdfBuffer.length);
138
- return pdfBuffer;
139
- }
140
- }
141
- catch (libreOfficeError) {
142
- const message = libreOfficeError instanceof Error ? libreOfficeError.message : 'Unknown error';
143
- console.error('Enhanced LibreOffice conversion failed:', message);
144
- // Fallback to simple conversion method
145
- try {
146
- console.log('Trying fallback LibreOffice conversion...');
147
- const pdfBuffer = await convertAsync(pptxBuffer, '.pdf', undefined);
148
- if (pdfBuffer && pdfBuffer.length > 0) {
149
- console.log('Fallback LibreOffice conversion successful, PDF size:', pdfBuffer.length);
150
- return pdfBuffer;
151
- }
152
- }
153
- catch (fallbackError) {
154
- const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Unknown error';
155
- console.error('Fallback LibreOffice conversion also failed:', fallbackMessage);
156
- }
157
- }
158
- // All LibreOffice methods failed, try alternative approach
159
- console.log('All LibreOffice methods failed, falling back to alternative slide screenshot generation...');
160
- return await this.createAlternativeSlideImages(pptxBuffer);
161
- }
162
- /**
163
- * Create slide images without LibreOffice using direct PPTX parsing
164
- */
165
- async createAlternativeSlideImages(pptxBuffer) {
166
- try {
167
- // Import required modules dynamically
168
- const JSZip = (await import('jszip')).default;
169
- const { parseStringPromise } = await import('xml2js');
170
- // Parse PPTX to get slide information
171
- const zip = await JSZip.loadAsync(pptxBuffer);
172
- // Get slide files
173
- const slideFiles = [];
174
- zip.forEach((relativePath, file) => {
175
- if (relativePath.startsWith('ppt/slides/slide') && relativePath.endsWith('.xml')) {
176
- slideFiles.push({
177
- path: relativePath,
178
- file: file,
179
- slideNumber: parseInt(relativePath.match(/slide(\d+)\.xml/)?.[1] || '0')
180
- });
181
- }
182
- });
183
- slideFiles.sort((a, b) => a.slideNumber - b.slideNumber);
184
- console.log(`Found ${slideFiles.length} slides to convert`);
185
- // Create individual slide images directly
186
- const slideImages = [];
187
- for (let i = 0; i < slideFiles.length; i++) {
188
- const slideFile = slideFiles[i];
189
- const slideNumber = i + 1;
190
- try {
191
- // Parse slide XML to extract content
192
- const xmlContent = await slideFile.file.async('string');
193
- const slideData = await parseStringPromise(xmlContent);
194
- // Generate slide image using canvas-based rendering
195
- const slideImageBuffer = await this.renderSlideToImage(slideData, slideNumber, zip);
196
- if (slideImageBuffer) {
197
- const filename = `slide-${slideNumber.toString().padStart(3, '0')}.png`;
198
- const savedPath = path.join(this.outputDir, filename);
199
- // Save the generated slide image
200
- await fs.writeFile(savedPath, slideImageBuffer);
201
- console.log(`Generated slide screenshot: ${filename}`);
202
- slideImages.push({
203
- originalPath: `slide${slideNumber}`,
204
- savedPath: savedPath,
205
- size: slideImageBuffer.length,
206
- format: 'png'
207
- });
208
- }
209
- }
210
- catch (slideError) {
211
- console.warn(`Failed to generate slide ${slideNumber}:`, slideError);
212
- // Create a placeholder image for failed slides
213
- const placeholderBuffer = await this.createPlaceholderSlideImage(slideNumber);
214
- const filename = `slide-${slideNumber.toString().padStart(3, '0')}.png`;
215
- const savedPath = path.join(this.outputDir, filename);
216
- await fs.writeFile(savedPath, placeholderBuffer);
217
- slideImages.push({
218
- originalPath: `slide${slideNumber}`,
219
- savedPath: savedPath,
220
- size: placeholderBuffer.length,
221
- format: 'png'
222
- });
223
- }
224
- }
225
- // Return a fake PDF buffer to satisfy the interface
226
- // The actual slide images have been saved to disk
227
- this.generatedSlideImages = slideImages;
228
- return Buffer.from('FAKE_PDF_FOR_ALTERNATIVE_METHOD');
229
- }
230
- catch (error) {
231
- const message = error instanceof Error ? error.message : 'Unknown error';
232
- throw new ParseError('SlideRenderer', `Alternative slide conversion failed: ${message}`, error);
233
- }
234
- }
235
- generatedSlideImages = [];
236
- /**
237
- * Render a single slide to image using canvas with multi-language font support
238
- */
239
- async renderSlideToImage(slideData, slideNumber, zip) {
240
- try {
241
- // Import canvas dynamically (make it optional)
242
- let Canvas;
243
- try {
244
- Canvas = await import('canvas');
245
- }
246
- catch {
247
- console.log('Canvas module not available, creating text-based slide image');
248
- return await this.createTextBasedSlideImage(slideData, slideNumber);
249
- }
250
- const width = 1920;
251
- const height = 1080;
252
- const canvas = Canvas.createCanvas(width, height);
253
- const ctx = canvas.getContext('2d');
254
- // Register fonts for international character support
255
- await this.registerFontsForCanvas(Canvas);
256
- // Set background
257
- ctx.fillStyle = '#ffffff';
258
- ctx.fillRect(0, 0, width, height);
259
- // Add slide number with multi-language font
260
- ctx.fillStyle = '#333333';
261
- ctx.font = this.getUniversalFont(48);
262
- ctx.textAlign = 'center';
263
- ctx.fillText(`Slide ${slideNumber}`, width / 2, 100);
264
- // Extract and render text content
265
- const textContent = this.extractSlideText(slideData);
266
- if (textContent.length > 0) {
267
- ctx.font = this.getUniversalFont(32);
268
- ctx.textAlign = 'left';
269
- let yPos = 200;
270
- for (const text of textContent.slice(0, 20)) { // Limit to 20 lines
271
- // Handle long text with proper wrapping
272
- const wrappedLines = this.wrapText(ctx, text, width - 200); // Leave margins
273
- for (const line of wrappedLines.slice(0, 2)) { // Max 2 lines per text element
274
- ctx.fillText(line, 100, yPos);
275
- yPos += 50;
276
- if (yPos > height - 100)
277
- break;
278
- }
279
- if (yPos > height - 100)
280
- break;
281
- }
282
- }
283
- // Add border
284
- ctx.strokeStyle = '#cccccc';
285
- ctx.lineWidth = 4;
286
- ctx.strokeRect(0, 0, width, height);
287
- return canvas.toBuffer('image/png');
288
- }
289
- catch (error) {
290
- console.warn('Canvas rendering failed:', error);
291
- return await this.createTextBasedSlideImage(slideData, slideNumber);
292
- }
293
- }
294
- /**
295
- * Register system fonts and fallback fonts for international character support
296
- */
297
- async registerFontsForCanvas(Canvas) {
298
- try {
299
- // Try to register common system fonts that support international characters
300
- const fontPaths = [
301
- // Windows fonts
302
- 'C:\\Windows\\Fonts\\arial.ttf',
303
- 'C:\\Windows\\Fonts\\SimSun.ttc', // Chinese (Simplified)
304
- 'C:\\Windows\\Fonts\\mingliu.ttc', // Chinese (Traditional)
305
- 'C:\\Windows\\Fonts\\malgun.ttf', // Korean
306
- 'C:\\Windows\\Fonts\\meiryo.ttc', // Japanese
307
- 'C:\\Windows\\Fonts\\NotoSansCJK-Regular.ttc', // Noto CJK
308
- // macOS fonts
309
- '/System/Library/Fonts/Arial.ttf',
310
- '/System/Library/Fonts/PingFang.ttc', // Chinese
311
- '/System/Library/Fonts/AppleGothic.ttf', // Korean
312
- '/System/Library/Fonts/Hiragino Sans GB.ttc', // Japanese/Chinese
313
- // Linux fonts
314
- '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
315
- '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
316
- '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
317
- ];
318
- const fs = await import('fs/promises');
319
- for (const fontPath of fontPaths) {
320
- try {
321
- await fs.access(fontPath);
322
- Canvas.registerFont(fontPath, {
323
- family: this.getFontFamily(fontPath)
324
- });
325
- console.log(`Registered font: ${fontPath}`);
326
- }
327
- catch {
328
- // Font file doesn't exist, skip silently
329
- }
330
- }
331
- }
332
- catch (error) {
333
- console.warn('Font registration failed:', error);
334
- // Continue without custom fonts - will use Canvas defaults
335
- }
336
- }
337
- /**
338
- * Get font family name from font path
339
- */
340
- getFontFamily(fontPath) {
341
- const filename = fontPath.split(/[/\\]/).pop() || '';
342
- if (filename.includes('SimSun') || filename.includes('PingFang'))
343
- return 'SimSun';
344
- if (filename.includes('malgun') || filename.includes('AppleGothic'))
345
- return 'Malgun Gothic';
346
- if (filename.includes('meiryo') || filename.includes('Hiragino'))
347
- return 'Meiryo';
348
- if (filename.includes('Noto'))
349
- return 'Noto Sans CJK';
350
- if (filename.includes('Liberation'))
351
- return 'Liberation Sans';
352
- if (filename.includes('DejaVu'))
353
- return 'DejaVu Sans';
354
- return 'Arial'; // Default fallback
355
- }
356
- /**
357
- * Get universal font string with fallbacks for international characters
358
- */
359
- getUniversalFont(size) {
360
- // Use a comprehensive font stack that covers most international characters
361
- return `${size}px "Noto Sans CJK", "SimSun", "Malgun Gothic", "Meiryo", "Liberation Sans", "DejaVu Sans", "Arial Unicode MS", Arial, sans-serif`;
362
- }
363
- /**
364
- * Wrap text to fit within specified width
365
- */
366
- wrapText(ctx, text, maxWidth) {
367
- const words = text.split(' ');
368
- const lines = [];
369
- let currentLine = '';
370
- for (const word of words) {
371
- const testLine = currentLine ? `${currentLine} ${word}` : word;
372
- const metrics = ctx.measureText(testLine);
373
- if (metrics.width > maxWidth && currentLine) {
374
- lines.push(currentLine);
375
- currentLine = word;
376
- }
377
- else {
378
- currentLine = testLine;
379
- }
380
- }
381
- if (currentLine) {
382
- lines.push(currentLine);
383
- }
384
- return lines;
385
- }
386
- /**
387
- * Create a text-based slide image when canvas is not available
388
- */
389
- async createTextBasedSlideImage(slideData, slideNumber) {
390
- try {
391
- // Try to use a simple image generation approach
392
- const textContent = this.extractSlideText(slideData);
393
- // Create a minimal SVG that can be converted to PNG
394
- const svgContent = this.createSVGSlideImage(slideNumber, textContent);
395
- // Try to convert SVG to PNG buffer
396
- return await this.convertSVGToPNG(svgContent);
397
- }
398
- catch (error) {
399
- console.warn('SVG fallback failed:', error);
400
- // Ultimate fallback - return a simple text buffer
401
- const textContent = this.extractSlideText(slideData);
402
- const slideText = `SLIDE ${slideNumber}\n\n${textContent.join('\n')}`;
403
- return Buffer.from(`Slide ${slideNumber} Content:\n${slideText}`);
404
- }
405
- }
406
- /**
407
- * Create SVG representation of slide content
408
- */
409
- createSVGSlideImage(slideNumber, textContent) {
410
- const width = 1920;
411
- const height = 1080;
412
- let svgContent = `<?xml version="1.0" encoding="UTF-8"?>
413
- <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
414
- <!-- Background -->
415
- <rect width="${width}" height="${height}" fill="white" stroke="#cccccc" stroke-width="4"/>
416
-
417
- <!-- Slide Number -->
418
- <text x="${width / 2}" y="100" text-anchor="middle" font-family="Arial, sans-serif" font-size="48" fill="#333333">Slide ${slideNumber}</text>
419
-
420
- <!-- Content -->`;
421
- let yPos = 200;
422
- for (const text of textContent.slice(0, 15)) { // Limit to 15 lines
423
- if (yPos > height - 100)
424
- break;
425
- // Escape HTML entities for SVG
426
- const escapedText = text
427
- .replace(/&/g, '&amp;')
428
- .replace(/</g, '&lt;')
429
- .replace(/>/g, '&gt;')
430
- .replace(/"/g, '&quot;')
431
- .substring(0, 80); // Limit line length
432
- svgContent += `
433
- <text x="100" y="${yPos}" font-family="Arial, sans-serif" font-size="32" fill="#333333">${escapedText}</text>`;
434
- yPos += 50;
435
- }
436
- svgContent += `
437
- </svg>`;
438
- return svgContent;
439
- }
440
- /**
441
- * Convert SVG to PNG buffer
442
- */
443
- async convertSVGToPNG(svgContent) {
444
- try {
445
- // Try to use Canvas to convert SVG to PNG
446
- const Canvas = await import('canvas');
447
- const { createCanvas, loadImage } = Canvas;
448
- // Convert SVG string to data URL
449
- const svgDataUrl = `data:image/svg+xml;base64,${Buffer.from(svgContent).toString('base64')}`;
450
- const canvas = createCanvas(1920, 1080);
451
- const ctx = canvas.getContext('2d');
452
- // Load the SVG as an image
453
- const img = await loadImage(svgDataUrl);
454
- ctx.drawImage(img, 0, 0);
455
- return canvas.toBuffer('image/png');
456
- }
457
- catch (error) {
458
- console.warn('SVG to PNG conversion failed:', error);
459
- // Return simple placeholder buffer
460
- throw error;
461
- }
462
- }
463
- /**
464
- * Extract text content from slide XML data
465
- */
466
- extractSlideText(slideData) {
467
- const textElements = [];
468
- function extractText(obj) {
469
- if (typeof obj === 'object' && obj !== null) {
470
- if (Array.isArray(obj)) {
471
- for (const item of obj) {
472
- extractText(item);
473
- }
474
- }
475
- else {
476
- // Look for text content
477
- if (obj['a:t']) {
478
- if (Array.isArray(obj['a:t'])) {
479
- for (const textItem of obj['a:t']) {
480
- if (typeof textItem === 'string' && textItem.trim()) {
481
- textElements.push(textItem.trim());
482
- }
483
- else if (textItem && typeof textItem === 'object' && '_' in textItem) {
484
- const text = textItem._;
485
- if (text && text.trim()) {
486
- textElements.push(text.trim());
487
- }
488
- }
489
- }
490
- }
491
- }
492
- // Recursively process nested objects
493
- for (const key in obj) {
494
- if (key !== 'a:t') {
495
- extractText(obj[key]);
496
- }
497
- }
498
- }
499
- }
500
- }
501
- extractText(slideData);
502
- return textElements;
503
- }
504
- /**
505
- * Create placeholder slide image for failed conversions
506
- */
507
- async createPlaceholderSlideImage(slideNumber) {
508
- // Create a simple placeholder
509
- const placeholderText = `Slide ${slideNumber}\n\n[Slide content could not be rendered]\n\nThis slide contains the original presentation content\nbut could not be converted to an image.`;
510
- // Return a minimal buffer (in real implementation, create a proper placeholder image)
511
- return Buffer.from(placeholderText);
512
- }
513
- /**
514
- * Convert PDF buffer to individual slide images with optimization
515
- */
516
- async convertPdfToSlideImages(pdfBuffer, options) {
517
- try {
518
- // Check if we already generated slide images using alternative method
519
- if (pdfBuffer.toString() === 'FAKE_PDF_FOR_ALTERNATIVE_METHOD') {
520
- console.log('Using pre-generated slide images from alternative method');
521
- return this.generatedSlideImages;
522
- }
523
- // Standard PDF to image conversion using pdf2pic with optimization
524
- await fs.mkdir(this.outputDir, { recursive: true });
525
- console.log('PDF to images: Output directory created:', this.outputDir);
526
- // Optimize settings based on quality
527
- const optimizedOptions = this.optimizePdfToImageSettings(options);
528
- console.log('Optimized PDF2PIC settings:', optimizedOptions);
529
- // Configure pdf2pic with optimized settings
530
- const convert = fromBuffer(pdfBuffer, {
531
- density: optimizedOptions.density,
532
- saveFilename: 'slide',
533
- savePath: this.outputDir,
534
- format: options.format,
535
- width: optimizedOptions.width,
536
- height: optimizedOptions.height,
537
- quality: optimizedOptions.quality
538
- });
539
- // Get total number of pages first
540
- const storeAsImage = convert.bulk(-1, true);
541
- const results = await storeAsImage;
542
- console.log(`PDF2PIC processed ${results.length} pages`);
543
- const slideImages = [];
544
- for (let i = 0; i < results.length; i++) {
545
- const result = results[i];
546
- const slideNumber = i + 1;
547
- const filename = `slide-${slideNumber.toString().padStart(3, '0')}.${options.format}`;
548
- const savedPath = path.join(this.outputDir, filename);
549
- console.log(`Processing slide ${slideNumber}, expected file: ${filename}`);
550
- // Save the image file with error handling
551
- try {
552
- const imageBuffer = result.buffer;
553
- if (imageBuffer && imageBuffer.length > 0) {
554
- await fs.writeFile(savedPath, imageBuffer);
555
- // Verify file was written correctly
556
- const stats = await fs.stat(savedPath);
557
- if (stats.size > 0) {
558
- console.log(`Saved slide image: ${savedPath} (${stats.size} bytes)`);
559
- slideImages.push({
560
- originalPath: `slide${slideNumber}`,
561
- savedPath: savedPath,
562
- size: stats.size,
563
- format: options.format
564
- });
565
- }
566
- else {
567
- console.warn(`Slide ${slideNumber} file is empty after writing`);
568
- }
569
- }
570
- else {
571
- console.warn(`No buffer found for slide ${slideNumber}`);
572
- }
573
- }
574
- catch (slideError) {
575
- const slideMessage = slideError instanceof Error ? slideError.message : 'Unknown error';
576
- console.warn(`Failed to process slide ${slideNumber}: ${slideMessage}`);
577
- // Continue processing other slides
578
- }
579
- }
580
- if (slideImages.length === 0) {
581
- throw new Error('No slide images were successfully created');
582
- }
583
- console.log(`Successfully created ${slideImages.length} slide images`);
584
- return slideImages;
585
- }
586
- catch (error) {
587
- const message = error instanceof Error ? error.message : 'Unknown error';
588
- console.error('PDF to images conversion error:', error);
589
- throw new ParseError('SlideRenderer', `PDF to images conversion failed: ${message}`, error);
590
- }
591
- }
592
- /**
593
- * Optimize PDF to image conversion settings based on quality requirements
594
- */
595
- optimizePdfToImageSettings(options) {
596
- // Standard slide dimensions (16:9 aspect ratio)
597
- const baseWidth = 1920;
598
- const baseHeight = 1080;
599
- // Adjust settings based on quality
600
- let optimizedDensity = options.density;
601
- let optimizedQuality = options.quality;
602
- let width = baseWidth;
603
- let height = baseHeight;
604
- if (options.quality >= 95) {
605
- // Ultra-high quality
606
- optimizedDensity = Math.max(options.density, 300);
607
- optimizedQuality = 100;
608
- width = 2560;
609
- height = 1440;
610
- }
611
- else if (options.quality >= 85) {
612
- // High quality
613
- optimizedDensity = Math.max(options.density, 200);
614
- optimizedQuality = Math.max(options.quality, 90);
615
- width = 1920;
616
- height = 1080;
617
- }
618
- else if (options.quality >= 70) {
619
- // Medium quality
620
- optimizedDensity = Math.max(options.density, 150);
621
- optimizedQuality = Math.max(options.quality, 80);
622
- width = 1600;
623
- height = 900;
624
- }
625
- else {
626
- // Lower quality for faster processing
627
- optimizedDensity = Math.max(options.density, 100);
628
- optimizedQuality = options.quality;
629
- width = 1280;
630
- height = 720;
631
- }
632
- return {
633
- density: optimizedDensity,
634
- quality: optimizedQuality,
635
- width,
636
- height
637
- };
638
- }
639
- /**
640
- * Generate markdown with slide images
641
- */
642
- generateSlideMarkdown(slideImages, title) {
643
- let markdown = '';
644
- if (title) {
645
- markdown += `# ${title}\n\n`;
646
- }
647
- for (let i = 0; i < slideImages.length; i++) {
648
- const slide = slideImages[i];
649
- const slideNumber = i + 1;
650
- markdown += `## Slide ${slideNumber}\n\n`;
651
- // Use relative path for markdown image reference
652
- const relativePath = path.relative(process.cwd(), slide.savedPath)
653
- .replace(/\\/g, '/'); // Ensure forward slashes for markdown
654
- markdown += `![Slide ${slideNumber}](${relativePath})\n\n`;
655
- }
656
- return markdown.trim();
657
- }
658
- /**
659
- * Enhance slide images using visual layout information
660
- */
661
- async enhanceSlideImagesWithLayouts(slideImages, visualLayouts, options) {
662
- try {
663
- // Try to import Canvas for enhanced rendering
664
- let Canvas;
665
- try {
666
- Canvas = await import('canvas');
667
- }
668
- catch {
669
- console.log('Canvas module not available for slide enhancement');
670
- return;
671
- }
672
- for (let i = 0; i < slideImages.length && i < visualLayouts.length; i++) {
673
- const slideImage = slideImages[i];
674
- const layout = visualLayouts[i];
675
- try {
676
- // Read existing slide image
677
- const originalImageBuffer = await fs.readFile(slideImage.savedPath);
678
- // Create canvas from original image
679
- const originalImage = await Canvas.loadImage(originalImageBuffer);
680
- const canvas = Canvas.createCanvas(originalImage.width, originalImage.height);
681
- const ctx = canvas.getContext('2d');
682
- // Draw original image as base
683
- ctx.drawImage(originalImage, 0, 0);
684
- // Overlay additional visual elements if needed
685
- await this.addVisualEnhancements(ctx, layout, {
686
- width: originalImage.width,
687
- height: originalImage.height,
688
- slideNumber: i + 1
689
- });
690
- // Save enhanced image (optional - only if we made changes)
691
- const enhancedBuffer = canvas.toBuffer(`image/${options.format}`);
692
- // Only replace if the enhancement actually improved the image
693
- if (enhancedBuffer.length > 0) {
694
- console.log(`Enhanced slide ${i + 1} with visual layout information`);
695
- // For now, we'll keep the original to avoid overwriting working images
696
- // In the future, this could replace the original with the enhanced version
697
- }
698
- }
699
- catch (slideError) {
700
- console.warn(`Failed to enhance slide ${i + 1}:`, slideError);
701
- // Continue with other slides
702
- }
703
- }
704
- }
705
- catch (error) {
706
- console.warn('Slide enhancement failed:', error);
707
- // Enhancement is optional, so we don't throw errors
708
- }
709
- }
710
- /**
711
- * Add visual enhancements based on layout information
712
- */
713
- async addVisualEnhancements(ctx, layout, dimensions) {
714
- try {
715
- // Register fonts for international character support
716
- const Canvas = await import('canvas');
717
- await this.registerFontsForCanvas(Canvas);
718
- // Add subtle overlay indicators for different element types
719
- const textElements = layout.elements.filter(e => e.type === 'text');
720
- const imageElements = layout.elements.filter(e => e.type === 'image');
721
- const chartElements = layout.elements.filter(e => e.type === 'chart');
722
- // Add corner annotations for element counts (very subtle)
723
- if (textElements.length > 0 || imageElements.length > 0 || chartElements.length > 0) {
724
- ctx.save();
725
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
726
- ctx.font = this.getUniversalFont(12);
727
- let annotationY = dimensions.height - 30;
728
- if (textElements.length > 0) {
729
- ctx.fillText(`📝 ${textElements.length}`, 10, annotationY);
730
- annotationY -= 15;
731
- }
732
- if (imageElements.length > 0) {
733
- ctx.fillText(`🖼️ ${imageElements.length}`, 10, annotationY);
734
- annotationY -= 15;
735
- }
736
- if (chartElements.length > 0) {
737
- ctx.fillText(`📊 ${chartElements.length}`, 10, annotationY);
738
- }
739
- ctx.restore();
740
- }
741
- }
742
- catch (error) {
743
- console.warn('Failed to add visual enhancements:', error);
744
- }
745
- }
746
- /**
747
- * Clean up generated image files
748
- */
749
- async cleanup() {
750
- try {
751
- const files = await fs.readdir(this.outputDir);
752
- const slideFiles = files.filter(file => file.startsWith('slide-') && (file.endsWith('.png') || file.endsWith('.jpg')));
753
- for (const file of slideFiles) {
754
- const filePath = path.join(this.outputDir, file);
755
- await fs.unlink(filePath);
756
- }
757
- }
758
- catch (error) {
759
- // Ignore cleanup errors
760
- }
761
- }
762
- /**
763
- * Render slides using Puppeteer browser-based rendering
764
- */
765
- async renderWithPuppeteer(visualLayouts, options = {}) {
766
- const puppeteerRenderer = new PuppeteerRenderer(this.outputDir);
767
- try {
768
- const result = await puppeteerRenderer.renderSlidesFromLayouts(visualLayouts, options);
769
- return {
770
- slideImages: [...result.slideImages],
771
- slideCount: result.slideCount,
772
- metadata: result.metadata
773
- };
774
- }
775
- finally {
776
- // Always cleanup Puppeteer resources
777
- await puppeteerRenderer.cleanup();
778
- }
779
- }
780
- /**
781
- * Check if LibreOffice is available on the system
782
- */
783
- static async checkLibreOfficeAvailability() {
784
- try {
785
- // Create a minimal test document to verify LibreOffice works
786
- const testBuffer = Buffer.from('test');
787
- await convertAsync(testBuffer, '.pdf', undefined);
788
- return true;
789
- }
790
- catch {
791
- return false;
792
- }
793
- }
794
- /**
795
- * Check if Puppeteer is available on the system
796
- */
797
- static async checkPuppeteerAvailability() {
798
- return await PuppeteerRenderer.isAvailable();
799
- }
800
- }
801
- //# sourceMappingURL=slide-renderer.js.map