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.
- package/dist/parsers/pptx-parser.d.ts +3 -0
- package/dist/parsers/pptx-parser.d.ts.map +1 -1
- package/dist/parsers/pptx-parser.js +82 -2
- package/dist/parsers/pptx-parser.js.map +1 -1
- package/dist/utils/libreoffice-converter.d.ts +33 -0
- package/dist/utils/libreoffice-converter.d.ts.map +1 -0
- package/dist/utils/libreoffice-converter.js +169 -0
- package/dist/utils/libreoffice-converter.js.map +1 -0
- package/dist/utils/libreoffice-detector.d.ts +57 -0
- package/dist/utils/libreoffice-detector.d.ts.map +1 -0
- package/dist/utils/libreoffice-detector.js +295 -0
- package/dist/utils/libreoffice-detector.js.map +1 -0
- package/dist/utils/pptx-visual-parser.d.ts +190 -0
- package/dist/utils/pptx-visual-parser.d.ts.map +1 -0
- package/dist/utils/pptx-visual-parser.js +648 -0
- package/dist/utils/pptx-visual-parser.js.map +1 -0
- package/dist/utils/puppeteer-renderer.d.ts +65 -0
- package/dist/utils/puppeteer-renderer.d.ts.map +1 -0
- package/dist/utils/puppeteer-renderer.js +393 -0
- package/dist/utils/puppeteer-renderer.js.map +1 -0
- package/dist/utils/slide-renderer.d.ts +55 -3
- package/dist/utils/slide-renderer.d.ts.map +1 -1
- package/dist/utils/slide-renderer.js +478 -52
- package/dist/utils/slide-renderer.js.map +1 -1
- package/package.json +6 -1
@@ -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:
|
25
|
-
|
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
|
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
|
106
|
+
* Convert PPTX buffer to PDF buffer using enhanced LibreOffice converter
|
55
107
|
*/
|
56
108
|
async convertPptxToPdf(pptxBuffer) {
|
57
|
-
//
|
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('
|
60
|
-
const pdfBuffer = await
|
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.
|
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('
|
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 =
|
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 =
|
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
|
-
|
182
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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, '&')
|
428
|
+
.replace(/</g, '<')
|
429
|
+
.replace(/>/g, '>')
|
430
|
+
.replace(/"/g, '"')
|
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
|
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
|
-
//
|
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:
|
531
|
+
density: optimizedOptions.density,
|
276
532
|
saveFilename: 'slide',
|
277
533
|
savePath: this.outputDir,
|
278
534
|
format: options.format,
|
279
|
-
width:
|
280
|
-
height:
|
281
|
-
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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
-
|
313
|
-
|
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
|