image-video-optimizer 3.0.9 → 3.3.3

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/bin/cli.js CHANGED
@@ -1,21 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
3
+ const { Command } = require('commander');
4
4
  const path = require('path');
5
- const optimizer = require('../src/index');
5
+ const { optimize, resetStatus } = require('../src/index');
6
+
7
+ const program = new Command();
6
8
 
7
9
  program
8
10
  .name('image-video-optimizer')
9
- .description('Optimize and compress images and videos in a directory')
10
- .version('3.0.5')
11
- .argument('[target-dir]', 'Target directory to optimize (default: current directory)', process.cwd())
12
- .action(async (targetDir) => {
11
+ .description('CLI tool to optimize and compress images, videos, audio, and PDFs')
12
+ .version('3.2.0');
13
+
14
+ program
15
+ .argument('[directory]', 'Target directory to optimize', process.cwd())
16
+ .option('-r, --reset', 'Reset status file and start fresh')
17
+ .option('-v, --verbose', 'Show detailed processing information')
18
+ .action(async (directory, options) => {
13
19
  try {
14
- const resolvedPath = path.resolve(targetDir);
20
+ const targetDir = path.resolve(directory);
21
+
22
+ if (options.reset) {
23
+ resetStatus(targetDir);
24
+ return;
25
+ }
26
+
15
27
  console.log('\nšŸš€ Image Video Optimizer\n');
16
- console.log('Target directory: ' + resolvedPath);
28
+ console.log('Target directory: ' + targetDir);
17
29
 
18
- await optimizer.optimize(resolvedPath);
30
+ await optimize(targetDir, options.verbose);
19
31
 
20
32
  console.log('\nāœ… Optimization complete!\n');
21
33
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.9",
3
+ "version": "3.3.3",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,99 @@
1
+ const ffmpeg = require('fluent-ffmpeg');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Process audio file with compression
7
+ * @param {string} audioPath - Path to audio file
8
+ * @param {object} config - Configuration object
9
+ * @returns {Promise<object>} Processing result
10
+ */
11
+ async function processAudio(audioPath, config = {}) {
12
+ return new Promise((resolve, reject) => {
13
+ const originalSize = fs.statSync(audioPath).size;
14
+ const parsedPath = path.parse(audioPath);
15
+ const outputExt = config.audio_ext || 'mp3';
16
+ const finalPath = path.join(parsedPath.dir, `${parsedPath.name}.${outputExt}`);
17
+
18
+ // Check if already in target format and no processing needed
19
+ if (path.extname(audioPath).toLowerCase().slice(1) === outputExt) {
20
+ // For now, assume audio files are already optimized if they're in target format
21
+ resolve({
22
+ success: false,
23
+ originalSize,
24
+ optimizedSize: originalSize,
25
+ message: `ā—‹ ${path.basename(audioPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
26
+ filePath: audioPath
27
+ });
28
+ return;
29
+ }
30
+
31
+ // Use temporary file to avoid conflicts
32
+ const tmpPath = path.join(parsedPath.dir, `${parsedPath.name}_tmp.${outputExt}`);
33
+
34
+ const command = ffmpeg(audioPath)
35
+ .audioCodec('libmp3lame')
36
+ .audioChannels(1)
37
+ .audioFrequency(16000)
38
+ .audioBitrate('32k')
39
+ .outputOptions([
40
+ '-ar 16000',
41
+ '-q:a 9'
42
+ ])
43
+ .output(tmpPath);
44
+
45
+ command
46
+ .on('end', async () => {
47
+ try {
48
+ // Wait a bit for the file to be fully written
49
+ await new Promise(resolve => setTimeout(resolve, 100));
50
+
51
+ const tmpStat = await new Promise((res, rej) => {
52
+ fs.stat(tmpPath, (err, stat) => {
53
+ if (err) rej(err);
54
+ else res(stat);
55
+ });
56
+ });
57
+
58
+ const optimizedSize = tmpStat.size;
59
+
60
+ // Always replace with compressed version for audio
61
+ await fs.promises.unlink(audioPath);
62
+ await fs.promises.rename(tmpPath, finalPath);
63
+
64
+ resolve({
65
+ success: true,
66
+ originalSize,
67
+ optimizedSize,
68
+ message: `āœ“ ${path.basename(audioPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
69
+ filePath: finalPath
70
+ });
71
+ } catch (error) {
72
+ // Clean up temp file on error
73
+ try {
74
+ await fs.promises.unlink(tmpPath);
75
+ } catch (cleanupError) {
76
+ // Ignore cleanup errors
77
+ }
78
+ reject(error);
79
+ }
80
+ })
81
+ .on('error', async (error) => {
82
+ // Clean up temp file on error
83
+ try {
84
+ await fs.promises.unlink(tmpPath);
85
+ } catch (cleanupError) {
86
+ // Ignore cleanup errors
87
+ }
88
+
89
+ const errorMessage = error.message || 'Unknown error';
90
+ reject(new Error(`Audio processing failed for ${path.basename(audioPath)}: ${errorMessage}`));
91
+ });
92
+
93
+ command.run();
94
+ });
95
+ }
96
+
97
+ module.exports = {
98
+ processAudio
99
+ };
@@ -3,6 +3,8 @@ const path = require('path');
3
3
 
4
4
  const SUPPORTED_IMAGES = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
5
5
  const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4'];
6
+ const SUPPORTED_AUDIO = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'];
7
+ const SUPPORTED_DOCUMENTS = ['pdf'];
6
8
 
7
9
  /**
8
10
  * Recursively search for media files in a directory
@@ -13,6 +15,8 @@ const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4
13
15
  async function searchMediaFiles(dirPath, options = {}) {
14
16
  const images = [];
15
17
  const videos = [];
18
+ const audio = [];
19
+ const documents = [];
16
20
  const verbose = options.verbose || false;
17
21
 
18
22
  async function walk(currentPath) {
@@ -39,6 +43,16 @@ async function searchMediaFiles(dirPath, options = {}) {
39
43
  if (verbose) {
40
44
  console.log(` Found video: ${fullPath}`);
41
45
  }
46
+ } else if (SUPPORTED_AUDIO.includes(ext)) {
47
+ audio.push(fullPath);
48
+ if (verbose) {
49
+ console.log(` Found audio: ${fullPath}`);
50
+ }
51
+ } else if (SUPPORTED_DOCUMENTS.includes(ext)) {
52
+ documents.push(fullPath);
53
+ if (verbose) {
54
+ console.log(` Found document: ${fullPath}`);
55
+ }
42
56
  }
43
57
  }
44
58
  } catch (error) {
@@ -59,7 +73,9 @@ async function searchMediaFiles(dirPath, options = {}) {
59
73
  return {
60
74
  images,
61
75
  videos,
62
- total: images.length + videos.length
76
+ audio,
77
+ documents,
78
+ total: images.length + videos.length + audio.length + documents.length
63
79
  };
64
80
  }
65
81
 
@@ -95,5 +111,7 @@ module.exports = {
95
111
  getFileSize,
96
112
  formatFileSize,
97
113
  SUPPORTED_IMAGES,
98
- SUPPORTED_VIDEOS
114
+ SUPPORTED_VIDEOS,
115
+ SUPPORTED_AUDIO,
116
+ SUPPORTED_DOCUMENTS
99
117
  };
package/src/index.js CHANGED
@@ -3,15 +3,20 @@ const path = require('path');
3
3
  const fileSearcher = require('./fileSearcher');
4
4
  const imageProcessor = require('./imageProcessor');
5
5
  const videoProcessor = require('./videoProcessor');
6
+ const audioProcessor = require('./audioProcessor');
7
+ const pdfProcessor = require('./pdfProcessor');
6
8
 
7
9
  const DEFAULT_CONFIG = {
8
10
  img_max_width: 1080,
9
11
  img_format: 'jpg',
10
12
  video_max_width: 720,
11
- video_encode: 'h264'
13
+ video_encode: 'h264',
14
+ audio_ext: 'mp3',
15
+ pdf_compress: true
12
16
  };
13
17
 
14
18
  const CONFIG_FILENAME = '.image-video-optimizer.conf';
19
+ const STATUS_FILENAME = '.image-video-optimizer-status.json';
15
20
 
16
21
  /**
17
22
  * Parse configuration file
@@ -69,6 +74,8 @@ img_max_width=${DEFAULT_CONFIG.img_max_width} # Maximum width for image
69
74
  img_format=${DEFAULT_CONFIG.img_format} # Target format for image conversion
70
75
  video_max_width=${DEFAULT_CONFIG.video_max_width} # Maximum width for videos (pixels)
71
76
  video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
77
+ audio_ext=${DEFAULT_CONFIG.audio_ext} # Audio extension for audio files
78
+ pdf_compress=${DEFAULT_CONFIG.pdf_compress} # Enable/disable PDF compression
72
79
  `;
73
80
 
74
81
  try {
@@ -80,11 +87,101 @@ video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
80
87
  }
81
88
  }
82
89
 
90
+ /**
91
+ * Load status file
92
+ * @param {string} targetDir - Target directory
93
+ * @returns {object} Status object
94
+ */
95
+ function loadStatus(targetDir) {
96
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
97
+ try {
98
+ if (fs.existsSync(statusPath)) {
99
+ const content = fs.readFileSync(statusPath, 'utf8');
100
+ return JSON.parse(content);
101
+ }
102
+ } catch (error) {
103
+ console.warn(`Warning: Could not read status file: ${error.message}`);
104
+ }
105
+
106
+ // Create initial status file if it doesn't exist
107
+ const initialStatus = {
108
+ processed: [],
109
+ failed: [],
110
+ startTime: null,
111
+ lastRun: null
112
+ };
113
+
114
+ try {
115
+ fs.writeFileSync(statusPath, JSON.stringify(initialStatus, null, 2));
116
+ } catch (error) {
117
+ console.warn(`Warning: Could not create status file: ${error.message}`);
118
+ }
119
+
120
+ return initialStatus;
121
+ }
122
+
123
+ /**
124
+ * Save status file
125
+ * @param {string} targetDir - Target directory
126
+ * @param {object} status - Status object
127
+ */
128
+ function saveStatus(targetDir, status) {
129
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
130
+ try {
131
+ status.lastRun = new Date().toISOString();
132
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
133
+ } catch (error) {
134
+ console.warn(`Warning: Could not save status file: ${error.message}`);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if file was already processed
140
+ * @param {string} filePath - File path to check
141
+ * @param {object} status - Status object
142
+ * @returns {boolean} True if already processed
143
+ */
144
+ function isProcessed(filePath, status) {
145
+ return status.processed.includes(filePath);
146
+ }
147
+
148
+ /**
149
+ * Mark file as processed
150
+ * @param {string} filePath - File path to mark
151
+ * @param {object} status - Status object
152
+ * @param {string} targetDir - Target directory for saving status
153
+ */
154
+ function markProcessed(filePath, status, targetDir) {
155
+ if (!status.processed.includes(filePath)) {
156
+ status.processed.push(filePath);
157
+ // Save status immediately after each file is processed
158
+ saveStatus(targetDir, status);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Mark file as failed
164
+ * @param {string} filePath - File path to mark
165
+ * @param {string} error - Error message
166
+ * @param {object} status - Status object
167
+ * @param {string} targetDir - Target directory for saving status
168
+ */
169
+ function markFailed(filePath, error, status, targetDir) {
170
+ status.failed.push({
171
+ file: filePath,
172
+ error: error,
173
+ timestamp: new Date().toISOString()
174
+ });
175
+ // Save status immediately after each file fails
176
+ saveStatus(targetDir, status);
177
+ }
178
+
83
179
  /**
84
180
  * Main optimization function
85
181
  * @param {string} targetDir - Target directory to optimize
182
+ * @param {boolean} verbose - Show detailed processing information
86
183
  */
87
- async function optimize(targetDir) {
184
+ async function optimize(targetDir, verbose = false) {
88
185
  // Validate directory exists
89
186
  if (!fs.existsSync(targetDir)) {
90
187
  throw new Error(`Directory does not exist: ${targetDir}`);
@@ -93,24 +190,42 @@ async function optimize(targetDir) {
93
190
  // Ensure config file exists
94
191
  ensureConfigFile(targetDir);
95
192
 
96
- // Load configuration
193
+ // Load configuration and status (create status file early)
97
194
  const configPath = path.join(targetDir, CONFIG_FILENAME);
98
195
  const config = parseConfig(configPath);
196
+ const status = loadStatus(targetDir);
99
197
 
100
198
  console.log('Configuration:');
101
199
  console.log(` Image Max Width: ${config.img_max_width}px`);
102
200
  console.log(` Image Format: ${config.img_format}`);
103
201
  console.log(` Video Max Width: ${config.video_max_width}px`);
104
- console.log(` Video Encode: ${config.video_encode}\n`);
202
+ console.log(` Video Encode: ${config.video_encode}`);
203
+ console.log(` Audio Extension: ${config.audio_ext}`);
204
+ console.log(` PDF Compression: ${config.pdf_compress ? 'Enabled' : 'Disabled'}`);
205
+
206
+ if (status.processed.length > 0) {
207
+ console.log(` Resume: ${status.processed.length} files already processed`);
208
+ }
209
+ console.log('');
105
210
 
106
211
  // Search for media files
107
212
  console.log('šŸ” Searching for media files...');
108
- const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
213
+ const { images, videos, audio, documents } = await fileSearcher.searchMediaFiles(targetDir, { verbose });
109
214
 
110
- console.log(`Found ${images.length} image(s) and ${videos.length} video(s)\n`);
215
+ // Filter out already processed files
216
+ const pendingImages = images.filter(img => !isProcessed(img, status));
217
+ const pendingVideos = videos.filter(vid => !isProcessed(vid, status));
218
+ const pendingAudio = audio.filter(aud => !isProcessed(aud, status));
219
+ const pendingDocuments = documents.filter(doc => !isProcessed(doc, status));
111
220
 
112
- if (images.length === 0 && videos.length === 0) {
113
- console.log('No media files found to optimize.');
221
+ const totalFound = images.length + videos.length + audio.length + documents.length;
222
+ const totalPending = pendingImages.length + pendingVideos.length + pendingAudio.length + pendingDocuments.length;
223
+ const alreadyProcessed = totalFound - totalPending;
224
+
225
+ console.log(`Found ${totalFound} file(s) (${alreadyProcessed} already processed, ${totalPending} pending)\n`);
226
+
227
+ if (totalPending === 0) {
228
+ console.log('All files have been processed. Use --reset to clear status and start over.');
114
229
  return;
115
230
  }
116
231
 
@@ -119,34 +234,96 @@ async function optimize(targetDir) {
119
234
  let successCount = 0;
120
235
 
121
236
  // Process images
122
- if (images.length > 0) {
237
+ if (pendingImages.length > 0) {
123
238
  console.log('šŸ“· Processing images...');
124
- for (const imagePath of images) {
125
- const result = await imageProcessor.processImage(imagePath, config);
126
- console.log(result.message);
127
-
128
- totalOriginalSize += result.originalSize;
129
- totalOptimizedSize += result.optimizedSize;
130
-
131
- if (result.success) {
132
- successCount++;
239
+ for (const imagePath of pendingImages) {
240
+ try {
241
+ const result = await imageProcessor.processImage(imagePath, config);
242
+ console.log(result.message);
243
+
244
+ totalOriginalSize += result.originalSize;
245
+ totalOptimizedSize += result.optimizedSize;
246
+
247
+ // Mark as processed if success OR if already optimized (no processing needed)
248
+ if (result.success || result.message.includes('Already optimized')) {
249
+ successCount++;
250
+ markProcessed(imagePath, status, targetDir);
251
+ }
252
+ } catch (error) {
253
+ console.log(`āŒ ${path.basename(imagePath)}: ${error.message}`);
254
+ markFailed(imagePath, error.message, status, targetDir);
133
255
  }
134
256
  }
135
257
  console.log('');
136
258
  }
137
259
 
138
260
  // Process videos
139
- if (videos.length > 0) {
261
+ if (pendingVideos.length > 0) {
140
262
  console.log('šŸŽ¬ Processing videos...');
141
- for (const videoPath of videos) {
142
- const result = await videoProcessor.processVideo(videoPath, config);
143
- console.log(result.message);
144
-
145
- totalOriginalSize += result.originalSize;
146
- totalOptimizedSize += result.optimizedSize;
147
-
148
- if (result.success) {
149
- successCount++;
263
+ for (const videoPath of pendingVideos) {
264
+ try {
265
+ const result = await videoProcessor.processVideo(videoPath, config);
266
+ console.log(result.message);
267
+
268
+ totalOriginalSize += result.originalSize;
269
+ totalOptimizedSize += result.optimizedSize;
270
+
271
+ // Mark as processed if success OR if already optimized (no processing needed)
272
+ if (result.success || result.message.includes('Already optimized')) {
273
+ successCount++;
274
+ markProcessed(videoPath, status, targetDir);
275
+ }
276
+ } catch (error) {
277
+ console.log(`āŒ ${path.basename(videoPath)}: ${error.message}`);
278
+ markFailed(videoPath, error.message, status, targetDir);
279
+ }
280
+ }
281
+ console.log('');
282
+ }
283
+
284
+ // Process audio
285
+ if (pendingAudio.length > 0) {
286
+ console.log('šŸŽµ Processing audio...');
287
+ for (const audioPath of pendingAudio) {
288
+ try {
289
+ const result = await audioProcessor.processAudio(audioPath, config);
290
+ console.log(result.message);
291
+
292
+ totalOriginalSize += result.originalSize;
293
+ totalOptimizedSize += result.optimizedSize;
294
+
295
+ // Mark as processed if success OR if already optimized (no processing needed)
296
+ if (result.success || result.message.includes('Already optimized')) {
297
+ successCount++;
298
+ markProcessed(audioPath, status, targetDir);
299
+ }
300
+ } catch (error) {
301
+ console.log(`āŒ ${path.basename(audioPath)}: ${error.message}`);
302
+ markFailed(audioPath, error.message, status, targetDir);
303
+ }
304
+ }
305
+ console.log('');
306
+ }
307
+
308
+ // Process documents
309
+ if (pendingDocuments.length > 0) {
310
+ console.log('šŸ“„ Processing documents...');
311
+ for (const docPath of pendingDocuments) {
312
+ try {
313
+ const result = await pdfProcessor.processPDF(docPath, config);
314
+ console.log(result.message);
315
+
316
+ totalOriginalSize += result.originalSize;
317
+ totalOptimizedSize += result.optimizedSize;
318
+
319
+ // Mark as processed if success OR if already optimized (no processing needed)
320
+ if (result.success || result.message.includes('Already optimized')) {
321
+ successCount++;
322
+ markProcessed(docPath, status, targetDir);
323
+ }
324
+ } catch (error) {
325
+ console.log(`āŒ ${path.basename(docPath)}: ${error.message}`);
326
+ markFailed(docPath, error.message, status, targetDir);
150
327
  }
151
328
  }
152
329
  console.log('');
@@ -154,7 +331,13 @@ async function optimize(targetDir) {
154
331
 
155
332
  // Print summary
156
333
  console.log('šŸ“Š Summary:');
157
- console.log(` Files optimized: ${successCount}/${images.length + videos.length}`);
334
+ console.log(` Files processed: ${successCount}/${totalPending}`);
335
+ console.log(` Total files in directory: ${totalFound}`);
336
+ console.log(` Already processed: ${alreadyProcessed}`);
337
+
338
+ if (status.failed.length > 0) {
339
+ console.log(` Failed: ${status.failed.length}`);
340
+ }
158
341
 
159
342
  if (totalOriginalSize > 0) {
160
343
  const savedSize = totalOriginalSize - totalOptimizedSize;
@@ -164,10 +347,32 @@ async function optimize(targetDir) {
164
347
  console.log(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`);
165
348
  console.log(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`);
166
349
  }
350
+
351
+ // Save status
352
+ saveStatus(targetDir, status);
353
+ }
354
+
355
+ /**
356
+ * Reset status file
357
+ * @param {string} targetDir - Target directory
358
+ */
359
+ function resetStatus(targetDir) {
360
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
361
+ try {
362
+ if (fs.existsSync(statusPath)) {
363
+ fs.unlinkSync(statusPath);
364
+ console.log(`āœ“ Status file reset: ${STATUS_FILENAME}`);
365
+ } else {
366
+ console.log('No status file found to reset.');
367
+ }
368
+ } catch (error) {
369
+ console.warn(`Warning: Could not reset status file: ${error.message}`);
370
+ }
167
371
  }
168
372
 
169
373
  module.exports = {
170
374
  optimize,
375
+ resetStatus,
171
376
  parseConfig,
172
377
  ensureConfigFile
173
378
  };
@@ -0,0 +1,139 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Process PDF file with compression
6
+ * @param {string} pdfPath - Path to PDF file
7
+ * @param {object} config - Configuration object
8
+ * @returns {Promise<object>} Processing result
9
+ */
10
+ async function processPDF(pdfPath, config = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ // Check if PDF compression is disabled
13
+ if (config.pdf_compress === false) {
14
+ const originalSize = fs.statSync(pdfPath).size;
15
+ resolve({
16
+ success: false,
17
+ originalSize,
18
+ optimizedSize: originalSize,
19
+ message: `ā—‹ ${path.basename(pdfPath)} - Already optimized (PDF compression disabled)`,
20
+ filePath: pdfPath
21
+ });
22
+ return;
23
+ }
24
+
25
+ const originalSize = fs.statSync(pdfPath).size;
26
+ const parsedPath = path.parse(pdfPath);
27
+
28
+ // For now, assume PDF files are already optimized (no size check)
29
+ // In a real implementation, you might check file size, quality, etc.
30
+ resolve({
31
+ success: false,
32
+ originalSize,
33
+ optimizedSize: originalSize,
34
+ message: `ā—‹ ${path.basename(pdfPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
35
+ filePath: pdfPath
36
+ });
37
+ return;
38
+
39
+ // Use temporary file to avoid conflicts
40
+ const tmpPath = path.join(parsedPath.dir, `${parsedPath.name}_tmp.pdf`);
41
+
42
+ // Try to use ghostscript for PDF compression
43
+ const command = require('child_process').spawn('gs', [
44
+ '-sDEVICE=pdfwrite',
45
+ '-dCompatibilityLevel=1.4',
46
+ '-dPDFSETTINGS=/screen',
47
+ '-dNOPAUSE',
48
+ '-dQUIET',
49
+ '-dBATCH',
50
+ '-sOutputFile=' + tmpPath,
51
+ pdfPath
52
+ ]);
53
+
54
+ command.on('close', async (code) => {
55
+ try {
56
+ if (code !== 0 && code !== null) {
57
+ // If ghostscript fails, fall back to copy
58
+ console.warn(`Warning: Ghostscript failed (code ${code}), falling back to copy for ${path.basename(pdfPath)}`);
59
+ const copyCommand = require('child_process').spawn('cp', [pdfPath, tmpPath]);
60
+
61
+ copyCommand.on('close', async (copyCode) => {
62
+ if (copyCode !== 0) {
63
+ throw new Error(`PDF copy failed with code ${copyCode}`);
64
+ }
65
+ await processResult();
66
+ });
67
+
68
+ copyCommand.on('error', (error) => {
69
+ reject(new Error(`PDF copy failed for ${path.basename(pdfPath)}: ${error.message}`));
70
+ });
71
+
72
+ return;
73
+ }
74
+
75
+ await processResult();
76
+ } catch (error) {
77
+ reject(error);
78
+ }
79
+ });
80
+
81
+ async function processResult() {
82
+ try {
83
+ const tmpStat = await new Promise((res, rej) => {
84
+ fs.stat(tmpPath, (err, stat) => {
85
+ if (err) rej(err);
86
+ else res(stat);
87
+ });
88
+ });
89
+
90
+ const optimizedSize = tmpStat.size;
91
+
92
+ // Always replace with compressed version for PDFs
93
+ await fs.promises.unlink(pdfPath);
94
+ await fs.promises.rename(tmpPath, pdfPath);
95
+
96
+ resolve({
97
+ success: true,
98
+ originalSize,
99
+ optimizedSize,
100
+ message: `āœ“ ${path.basename(pdfPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
101
+ filePath: pdfPath
102
+ });
103
+ } catch (error) {
104
+ // Clean up temp file on error
105
+ try {
106
+ await fs.promises.unlink(tmpPath);
107
+ } catch (cleanupError) {
108
+ // Ignore cleanup errors
109
+ }
110
+ reject(error);
111
+ }
112
+ }
113
+
114
+ command.on('error', async (error) => {
115
+ // If ghostscript is not available, fall back to copy
116
+ if (error.message.includes('ENOENT')) {
117
+ console.warn(`Warning: Ghostscript not found, falling back to copy for ${path.basename(pdfPath)}`);
118
+ const copyCommand = require('child_process').spawn('cp', [pdfPath, tmpPath]);
119
+
120
+ copyCommand.on('close', async (copyCode) => {
121
+ if (copyCode !== 0) {
122
+ throw new Error(`PDF copy failed with code ${copyCode}`);
123
+ }
124
+ await processResult();
125
+ });
126
+
127
+ copyCommand.on('error', (copyError) => {
128
+ reject(new Error(`PDF copy failed for ${path.basename(pdfPath)}: ${copyError.message}`));
129
+ });
130
+ } else {
131
+ reject(new Error(`PDF processing failed for ${path.basename(pdfPath)}: ${error.message}`));
132
+ }
133
+ });
134
+ });
135
+ }
136
+
137
+ module.exports = {
138
+ processPDF
139
+ };