image-video-optimizer 3.0.8 → 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.8",
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
@@ -34,7 +39,8 @@ function parseConfig(configPath) {
34
39
  // Skip comments and empty lines
35
40
  if (!trimmed || trimmed.startsWith('#')) continue;
36
41
 
37
- const [key, value] = trimmed.split('=').map(s => s.trim());
42
+ const [key, ...valueParts] = trimmed.split('=');
43
+ const value = valueParts.join('=').split('#')[0].trim();
38
44
 
39
45
  if (!key || !value) continue;
40
46
 
@@ -68,6 +74,8 @@ img_max_width=${DEFAULT_CONFIG.img_max_width} # Maximum width for image
68
74
  img_format=${DEFAULT_CONFIG.img_format} # Target format for image conversion
69
75
  video_max_width=${DEFAULT_CONFIG.video_max_width} # Maximum width for videos (pixels)
70
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
71
79
  `;
72
80
 
73
81
  try {
@@ -79,11 +87,101 @@ video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
79
87
  }
80
88
  }
81
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
+
82
179
  /**
83
180
  * Main optimization function
84
181
  * @param {string} targetDir - Target directory to optimize
182
+ * @param {boolean} verbose - Show detailed processing information
85
183
  */
86
- async function optimize(targetDir) {
184
+ async function optimize(targetDir, verbose = false) {
87
185
  // Validate directory exists
88
186
  if (!fs.existsSync(targetDir)) {
89
187
  throw new Error(`Directory does not exist: ${targetDir}`);
@@ -92,24 +190,42 @@ async function optimize(targetDir) {
92
190
  // Ensure config file exists
93
191
  ensureConfigFile(targetDir);
94
192
 
95
- // Load configuration
193
+ // Load configuration and status (create status file early)
96
194
  const configPath = path.join(targetDir, CONFIG_FILENAME);
97
195
  const config = parseConfig(configPath);
196
+ const status = loadStatus(targetDir);
98
197
 
99
198
  console.log('Configuration:');
100
199
  console.log(` Image Max Width: ${config.img_max_width}px`);
101
200
  console.log(` Image Format: ${config.img_format}`);
102
201
  console.log(` Video Max Width: ${config.video_max_width}px`);
103
- 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('');
104
210
 
105
211
  // Search for media files
106
212
  console.log('šŸ” Searching for media files...');
107
- const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
213
+ const { images, videos, audio, documents } = await fileSearcher.searchMediaFiles(targetDir, { verbose });
108
214
 
109
- 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));
110
220
 
111
- if (images.length === 0 && videos.length === 0) {
112
- 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.');
113
229
  return;
114
230
  }
115
231
 
@@ -118,34 +234,96 @@ async function optimize(targetDir) {
118
234
  let successCount = 0;
119
235
 
120
236
  // Process images
121
- if (images.length > 0) {
237
+ if (pendingImages.length > 0) {
122
238
  console.log('šŸ“· Processing images...');
123
- for (const imagePath of images) {
124
- const result = await imageProcessor.processImage(imagePath, config);
125
- console.log(result.message);
126
-
127
- totalOriginalSize += result.originalSize;
128
- totalOptimizedSize += result.optimizedSize;
129
-
130
- if (result.success) {
131
- 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);
132
255
  }
133
256
  }
134
257
  console.log('');
135
258
  }
136
259
 
137
260
  // Process videos
138
- if (videos.length > 0) {
261
+ if (pendingVideos.length > 0) {
139
262
  console.log('šŸŽ¬ Processing videos...');
140
- for (const videoPath of videos) {
141
- const result = await videoProcessor.processVideo(videoPath, config);
142
- console.log(result.message);
143
-
144
- totalOriginalSize += result.originalSize;
145
- totalOptimizedSize += result.optimizedSize;
146
-
147
- if (result.success) {
148
- 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);
149
327
  }
150
328
  }
151
329
  console.log('');
@@ -153,7 +331,13 @@ async function optimize(targetDir) {
153
331
 
154
332
  // Print summary
155
333
  console.log('šŸ“Š Summary:');
156
- 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
+ }
157
341
 
158
342
  if (totalOriginalSize > 0) {
159
343
  const savedSize = totalOriginalSize - totalOptimizedSize;
@@ -163,10 +347,32 @@ async function optimize(targetDir) {
163
347
  console.log(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`);
164
348
  console.log(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`);
165
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
+ }
166
371
  }
167
372
 
168
373
  module.exports = {
169
374
  optimize,
375
+ resetStatus,
170
376
  parseConfig,
171
377
  ensureConfigFile
172
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
+ };