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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -6
- package/dist/index.js.map +1 -1
- package/dist/parsers/hwp-parser.d.ts +20 -0
- package/dist/parsers/hwp-parser.d.ts.map +1 -0
- package/dist/parsers/hwp-parser.js +474 -0
- package/dist/parsers/hwp-parser.js.map +1 -0
- package/dist/parsers/pptx-parser.d.ts +0 -4
- package/dist/parsers/pptx-parser.d.ts.map +1 -1
- package/dist/parsers/pptx-parser.js +92 -159
- package/dist/parsers/pptx-parser.js.map +1 -1
- package/dist/types/interfaces.d.ts +2 -0
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/interfaces.js +3 -1
- package/dist/types/interfaces.js.map +1 -1
- package/package.json +10 -8
- package/dist/utils/puppeteer-renderer.d.ts +0 -65
- package/dist/utils/puppeteer-renderer.d.ts.map +0 -1
- package/dist/utils/puppeteer-renderer.js +0 -393
- package/dist/utils/puppeteer-renderer.js.map +0 -1
- package/dist/utils/slide-renderer.d.ts +0 -119
- package/dist/utils/slide-renderer.d.ts.map +0 -1
- package/dist/utils/slide-renderer.js +0 -801
- package/dist/utils/slide-renderer.js.map +0 -1
@@ -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, '&')
|
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
|
-
}
|
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 += `\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
|