All files / src/utils imageProcessor.ts

74.79% Statements 92/123
66.66% Branches 4/6
75% Functions 3/4
83.67% Lines 82/98

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 982x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 3x 3x 3x   3x   3x   3x         3x           2x 2x 2x   1x   2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x   2x   2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x
// src/utils/imageProcessor.ts
import sharp from 'sharp';
import path from 'path';
import fs from 'node:fs/promises';
import type { Logger } from 'pino';
import { sanitizeFilename } from './stringUtils';
 
/**
 * Processes an uploaded image file by stripping all metadata (like EXIF)
 * and optimizing it for web use.
 *
 * @param sourcePath The path to the temporary uploaded file.
 * @param destinationDir The directory where the final image should be saved.
 * @param originalFileName The original name of the file, used to determine the output name.
 * @param logger A pino logger instance for logging.
 * @returns The file name of the newly processed image.
 */
export async function processAndSaveImage(
  sourcePath: string,
  destinationDir: string,
  originalFileName: string,
  logger: Logger,
): Promise<string> {
  // Create a unique-ish filename to avoid collisions, but keep the original extension.
  const fileExt = path.extname(originalFileName);
  const fileBase = path.basename(originalFileName, fileExt);
  const outputFileName = `${fileBase}-${Date.now()}${fileExt}`;
  const outputPath = path.join(destinationDir, outputFileName);

  try {
    // Ensure the destination directory exists.
    await fs.mkdir(destinationDir, { recursive: true });

    logger.debug({ sourcePath, outputPath }, 'Starting image processing: stripping metadata and optimizing.');

    // Use sharp to process the image.
    // .withMetadata({}) strips all EXIF and other metadata.
    // .jpeg() and .png() apply format-specific optimizations.
    await sharp(sourcePath, { failOn: 'none' })
      .withMetadata({}) // This is the key to stripping metadata
      .jpeg({ quality: 85, mozjpeg: true }) // Optimize JPEGs
      .png({ compressionLevel: 8, quality: 85 }) // Optimize PNGs
      .toFile(outputPath);

    logger.info(`Successfully processed image and saved to ${outputPath}`);
    console.log('[DEBUG] processAndSaveImage returning:', outputFileName);
    return outputFileName;
  } catch (error) {
    logger.error(
      { err: error, sourcePath, outputPath },
      'An error occurred during image processing and saving.',
    );
    // Re-throw the error to be handled by the calling service (e.g., the worker).
    throw new Error(`Failed to process image ${originalFileName}.`);
  }
}
 
/**
 * Generates a small WebP icon from a source image.
 *
 * @param sourcePath The path to the source image (can be the original upload or the processed image).
 * @param outputDir The directory to save the icon in (e.g., 'flyer-images/icons').
 * @param logger A pino logger instance for logging.
 * @returns The file name of the generated icon.
 */
export async function generateFlyerIcon(
  sourcePath: string,
  outputDir: string,
  logger: Logger,
): Promise<string> {
  // Sanitize the base name of the source file to create a clean icon name.
  const sourceBaseName = path.parse(sourcePath).name;
  const iconFileName = `icon-${sanitizeFilename(sourceBaseName)}.webp`;
  const outputPath = path.join(outputDir, iconFileName);

  try {
    // Ensure the output directory exists.
    await fs.mkdir(outputDir, { recursive: true });
 
    logger.debug({ sourcePath, outputPath }, 'Starting icon generation.');
 
    await sharp(sourcePath, { failOn: 'none' })
      .resize({ width: 128, height: 128, fit: 'inside' })
      .webp({ quality: 75 }) // Slightly lower quality for icons is acceptable.
      .toFile(outputPath);
 
    logger.info(`Successfully generated icon: ${outputPath}`);
    console.log('[DEBUG] generateFlyerIcon returning:', iconFileName);
    return iconFileName;
  } catch (error) {
    logger.error(
      { err: error, sourcePath, outputPath },
      'An error occurred during icon generation.',
    );
    // Re-throw the error to be handled by the calling service.
    throw new Error(`Failed to generate icon for ${sourcePath}.`);
  }
}