file2md 1.1.8 → 1.1.10

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.
@@ -5,30 +5,80 @@ import libre from 'libreoffice-convert';
5
5
  import { fromBuffer } from 'pdf2pic';
6
6
  import { promisify } from 'node:util';
7
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';
8
12
  // Promisify libreoffice-convert
9
13
  const convertAsync = promisify(libre.convert);
10
14
  export class SlideRenderer {
11
15
  outputDir;
16
+ converter;
12
17
  constructor(outputDir) {
13
18
  this.outputDir = outputDir;
19
+ this.converter = new LibreOfficeConverter();
14
20
  }
15
21
  /**
16
22
  * Convert PPTX buffer to individual slide images
17
23
  */
18
24
  async renderSlidesToImages(pptxBuffer, options = {}) {
19
- const { quality = 90, density = 150, format = 'png', saveBase64 = false } = options;
25
+ const { quality = 90, density = 150, format = 'png', saveBase64 = false, useVisualLayouts = true, usePuppeteer = false, puppeteerOptions } = options;
20
26
  try {
21
27
  // Ensure output directory exists
22
28
  await fs.mkdir(this.outputDir, { recursive: true });
23
29
  console.log('Created slide output directory:', this.outputDir);
24
- // Step 1: Convert PPTX to PDF using LibreOffice
25
- console.log('Converting PPTX to PDF...');
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...');
26
71
  const pdfBuffer = await this.convertPptxToPdf(pptxBuffer);
27
72
  console.log('PPTX to PDF conversion successful, PDF size:', pdfBuffer.length);
28
- // Step 2: Convert PDF to individual slide images
73
+ // Step 3: Convert PDF to individual slide images
29
74
  console.log('Converting PDF to slide images...');
30
75
  const slideImages = await this.convertPdfToSlideImages(pdfBuffer, { quality, density, format, saveBase64 });
31
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
+ }
32
82
  // Verify images were actually created
33
83
  for (const slide of slideImages) {
34
84
  const exists = await fs.access(slide.savedPath).then(() => true).catch(() => false);
@@ -37,10 +87,12 @@ export class SlideRenderer {
37
87
  return {
38
88
  slideImages,
39
89
  slideCount: slideImages.length,
90
+ visualLayouts,
40
91
  metadata: {
41
92
  format,
42
93
  quality,
43
- density
94
+ density,
95
+ hasVisualLayouts: visualLayouts !== undefined
44
96
  }
45
97
  };
46
98
  }
@@ -51,24 +103,60 @@ export class SlideRenderer {
51
103
  }
52
104
  }
53
105
  /**
54
- * Convert PPTX buffer to PDF buffer using multiple methods
106
+ * Convert PPTX buffer to PDF buffer using enhanced LibreOffice converter
55
107
  */
56
108
  async convertPptxToPdf(pptxBuffer) {
57
- // Try LibreOffice first
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
58
127
  try {
59
- console.log('Trying LibreOffice conversion...');
60
- const pdfBuffer = await convertAsync(pptxBuffer, '.pdf', undefined);
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
+ });
61
136
  if (pdfBuffer && pdfBuffer.length > 0) {
62
- console.log('LibreOffice conversion successful, PDF size:', pdfBuffer.length);
137
+ console.log('Enhanced LibreOffice conversion successful, PDF size:', pdfBuffer.length);
63
138
  return pdfBuffer;
64
139
  }
65
140
  }
66
141
  catch (libreOfficeError) {
67
142
  const message = libreOfficeError instanceof Error ? libreOfficeError.message : 'Unknown error';
68
- console.log('LibreOffice conversion failed:', message);
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
+ }
69
157
  }
70
- // LibreOffice failed, try alternative approach
71
- console.log('Attempting alternative slide screenshot generation...');
158
+ // All LibreOffice methods failed, try alternative approach
159
+ console.log('All LibreOffice methods failed, falling back to alternative slide screenshot generation...');
72
160
  return await this.createAlternativeSlideImages(pptxBuffer);
73
161
  }
74
162
  /**
@@ -146,7 +234,7 @@ export class SlideRenderer {
146
234
  }
147
235
  generatedSlideImages = [];
148
236
  /**
149
- * Render a single slide to image using canvas
237
+ * Render a single slide to image using canvas with multi-language font support
150
238
  */
151
239
  async renderSlideToImage(slideData, slideNumber, zip) {
152
240
  try {
@@ -163,23 +251,31 @@ export class SlideRenderer {
163
251
  const height = 1080;
164
252
  const canvas = Canvas.createCanvas(width, height);
165
253
  const ctx = canvas.getContext('2d');
254
+ // Register fonts for international character support
255
+ await this.registerFontsForCanvas(Canvas);
166
256
  // Set background
167
257
  ctx.fillStyle = '#ffffff';
168
258
  ctx.fillRect(0, 0, width, height);
169
- // Add slide number
259
+ // Add slide number with multi-language font
170
260
  ctx.fillStyle = '#333333';
171
- ctx.font = '48px Arial';
261
+ ctx.font = this.getUniversalFont(48);
172
262
  ctx.textAlign = 'center';
173
263
  ctx.fillText(`Slide ${slideNumber}`, width / 2, 100);
174
264
  // Extract and render text content
175
265
  const textContent = this.extractSlideText(slideData);
176
266
  if (textContent.length > 0) {
177
- ctx.font = '32px Arial';
267
+ ctx.font = this.getUniversalFont(32);
178
268
  ctx.textAlign = 'left';
179
269
  let yPos = 200;
180
270
  for (const text of textContent.slice(0, 20)) { // Limit to 20 lines
181
- ctx.fillText(text.substring(0, 80), 100, yPos); // Limit line length
182
- yPos += 50;
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
+ }
183
279
  if (yPos > height - 100)
184
280
  break;
185
281
  }
@@ -195,17 +291,174 @@ export class SlideRenderer {
195
291
  return await this.createTextBasedSlideImage(slideData, slideNumber);
196
292
  }
197
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
+ }
198
386
  /**
199
387
  * Create a text-based slide image when canvas is not available
200
388
  */
201
389
  async createTextBasedSlideImage(slideData, slideNumber) {
202
- // Create a simple text-based representation
203
- // This is a fallback when canvas is not available
204
- const textContent = this.extractSlideText(slideData);
205
- const slideText = `SLIDE ${slideNumber}\n\n${textContent.join('\n')}`;
206
- // Create a simple PNG with text (this is a basic fallback)
207
- // In a real implementation, you might use a simpler image generation library
208
- return Buffer.from(`Slide ${slideNumber} Content:\n${slideText}`);
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
+ }
209
462
  }
210
463
  /**
211
464
  * Extract text content from slide XML data
@@ -258,7 +511,7 @@ export class SlideRenderer {
258
511
  return Buffer.from(placeholderText);
259
512
  }
260
513
  /**
261
- * Convert PDF buffer to individual slide images using pdf2pic or return pre-generated images
514
+ * Convert PDF buffer to individual slide images with optimization
262
515
  */
263
516
  async convertPdfToSlideImages(pdfBuffer, options) {
264
517
  try {
@@ -267,24 +520,21 @@ export class SlideRenderer {
267
520
  console.log('Using pre-generated slide images from alternative method');
268
521
  return this.generatedSlideImages;
269
522
  }
270
- // Standard PDF to image conversion using pdf2pic
523
+ // Standard PDF to image conversion using pdf2pic with optimization
271
524
  await fs.mkdir(this.outputDir, { recursive: true });
272
525
  console.log('PDF to images: Output directory created:', this.outputDir);
273
- // Configure pdf2pic
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
274
530
  const convert = fromBuffer(pdfBuffer, {
275
- density: options.density,
531
+ density: optimizedOptions.density,
276
532
  saveFilename: 'slide',
277
533
  savePath: this.outputDir,
278
534
  format: options.format,
279
- width: undefined, // Let pdf2pic calculate based on density
280
- height: undefined,
281
- quality: options.quality
282
- });
283
- console.log('PDF2PIC configuration:', {
284
- density: options.density,
285
- format: options.format,
286
- quality: options.quality,
287
- outputDir: this.outputDir
535
+ width: optimizedOptions.width,
536
+ height: optimizedOptions.height,
537
+ quality: optimizedOptions.quality
288
538
  });
289
539
  // Get total number of pages first
290
540
  const storeAsImage = convert.bulk(-1, true);
@@ -297,22 +547,39 @@ export class SlideRenderer {
297
547
  const filename = `slide-${slideNumber.toString().padStart(3, '0')}.${options.format}`;
298
548
  const savedPath = path.join(this.outputDir, filename);
299
549
  console.log(`Processing slide ${slideNumber}, expected file: ${filename}`);
300
- // Save the image file
301
- const imageBuffer = result.buffer;
302
- if (imageBuffer) {
303
- await fs.writeFile(savedPath, imageBuffer);
304
- console.log(`Saved slide image: ${savedPath} (${imageBuffer.length} bytes)`);
305
- slideImages.push({
306
- originalPath: `slide${slideNumber}`, // Virtual path for consistency
307
- savedPath: savedPath,
308
- size: imageBuffer.length,
309
- format: options.format
310
- });
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
+ }
311
573
  }
312
- else {
313
- console.warn(`No buffer found for slide ${slideNumber}`);
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
314
578
  }
315
579
  }
580
+ if (slideImages.length === 0) {
581
+ throw new Error('No slide images were successfully created');
582
+ }
316
583
  console.log(`Successfully created ${slideImages.length} slide images`);
317
584
  return slideImages;
318
585
  }
@@ -322,6 +589,53 @@ export class SlideRenderer {
322
589
  throw new ParseError('SlideRenderer', `PDF to images conversion failed: ${message}`, error);
323
590
  }
324
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
+ }
325
639
  /**
326
640
  * Generate markdown with slide images
327
641
  */
@@ -341,6 +655,94 @@ export class SlideRenderer {
341
655
  }
342
656
  return markdown.trim();
343
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
+ }
344
746
  /**
345
747
  * Clean up generated image files
346
748
  */
@@ -357,6 +759,24 @@ export class SlideRenderer {
357
759
  // Ignore cleanup errors
358
760
  }
359
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
+ }
360
780
  /**
361
781
  * Check if LibreOffice is available on the system
362
782
  */
@@ -371,5 +791,11 @@ export class SlideRenderer {
371
791
  return false;
372
792
  }
373
793
  }
794
+ /**
795
+ * Check if Puppeteer is available on the system
796
+ */
797
+ static async checkPuppeteerAvailability() {
798
+ return await PuppeteerRenderer.isAvailable();
799
+ }
374
800
  }
375
801
  //# sourceMappingURL=slide-renderer.js.map