Carlos Eduardo Gatti Ferreira

Image Upload & Processing Audit Report

⚠️ Archived: This document is kept for historical reference and may be outdated.
See: Documentation Hub

Date: 2024-01-XX
Purpose: Analyze current image upload handling, identify mobile camera issues, and propose normalization solution
Status: Analysis Complete - Ready for Implementation Proposal


Executive Summary

Problem Identified: Discart-me uploads images directly from mobile cameras without any preprocessing, resulting in:

Solution Available: QRACK already has image processing logic in src/lib/imageUtils.ts that is NOT being used by Discart-me.

Recommendation: Extract and generalize the image processing logic from QRACK into a shared library (src/lib/image/) that both systems can use.


Current State Analysis

1. QRACK Image Handling ✅

Location: src/lib/imageUtils.ts

Features:

Usage:

Current Implementation:

// QRACK usage pattern
const compressedDataUrl = await resizeAndCompressImage(file, {
  maxWidth: 1200,
  maxHeight: 1200,
  quality: 0.8,
  maxSizeKB: 500,
  outputFormat: 'image/jpeg',
});
// Then sends data URL to API

Limitations:

2. Discart-me Image Handling ❌

Location: src/lib/upload.ts

Features:

Usage:

Current Implementation:

// Discart-me usage pattern (NO COMPRESSION)
const files = Array.from(e.dataTransfer.files).filter(
  (file) => file.type.startsWith('image/')
);
// Files uploaded directly via FormData
await uploadDiscartItemImages(values.imageFiles, token);

Problems:

  1. Mobile Camera Issue: iOS/Android cameras produce 5-10MB+ images
  2. Direct Upload: Files sent as-is, no preprocessing
  3. No Size Limits: Only profile page has 2MB check, item uploads have none
  4. Slow UX: Large files take forever to upload on mobile
  5. Storage Costs: Unnecessary storage of oversized images

3. Avatar Upload (Discart-me Profile)

Location: app/discart-me/profile/page.tsx

Validation:

Note: Even with 2MB limit, images are not compressed/resized before upload.


Image Processing Logic Analysis

Existing Logic (src/lib/imageUtils.ts)

Functions Available:

  1. resizeAndCompressImage(file, options): Promise<string>
    • Input: File object
    • Output: Base64 data URL string
    • Process:
      • Validates file type
      • Checks initial file size
      • Loads image via FileReader
      • Calculates new dimensions (maintains aspect ratio)
      • Creates canvas, draws resized image
      • Compresses via canvas.toBlob() with quality setting
      • If still too large, retries with lower quality
      • Returns data URL
    • Limitation: Only returns data URL, not File object
  2. validateImageFile(file, maxSizeMB): { valid, error? }
    • Validates file type and size
    • Generic, reusable
  3. getImageDimensions(file): Promise<{ width, height }>
    • Gets image dimensions without loading full image
    • Generic, reusable
  4. formatFileSize(bytes): string
    • Formats bytes to human-readable format
    • Generic, reusable

Current Limitations

  1. Output Format: Only returns data URLs, not File objects
    • Discart-me needs File objects for FormData uploads
    • QRACK uses data URLs (works for their API)
  2. EXIF Orientation: Not handled
    • Mobile cameras often store orientation in EXIF
    • Images may appear rotated incorrectly
    • Canvas doesn’t automatically apply EXIF orientation
  3. Domain-Specific Defaults:
    • Defaults hardcoded for QRACK (1200x1200, 500KB)
    • Should be configurable per use case
  4. Browser-Only: Uses FileReader, Image, Canvas APIs
    • Cannot run on server-side
    • Must be used in client components

Where Image Uploads Are Triggered

QRACK

  1. Item Edit Page (app/qrack/items/[id]/edit/page.tsx)
    • Single image upload
    • Uses resizeAndCompressImage()
    • Sends data URL to API
  2. Add Item to Container (app/qrack/containers/[id]/items/page.tsx)
    • Single image upload
    • Uses resizeAndCompressImage()
    • Sends data URL to API

Discart-me

  1. New Item Page (app/discart-me/items/new/page.tsx)
    • Multiple image uploads
    • NO compression
    • Uploads files directly via uploadDiscartItemImages()
  2. Edit Item Page (app/discart-me/items/[id]/edit/page.tsx)
    • Multiple image uploads
    • NO compression
    • Uploads files directly via uploadDiscartItemImages()
  3. Profile Avatar (app/discart-me/profile/page.tsx)
    • Single image upload
    • NO compression
    • Only validates 2MB max size
    • Uploads file directly via uploadAvatar()
  4. ItemForm Component (src/components/discart-me/ItemForm.tsx)
    • Drag & drop area
    • Accepts multiple files
    • Filters by file.type.startsWith('image/') only
    • NO compression

Reusability Evaluation

Can imageUtils.ts be Extracted?

✅ YES - The logic is generic enough but needs modifications:

Already Reusable:

Needs Modification:

Current Coupling:

Extraction Strategy

  1. Move to Shared Location: src/lib/image/
  2. Rename for Generality: processImage.ts instead of imageUtils.ts
  3. Add File Output Option: Support both data URL and File object outputs
  4. Make Defaults Configurable: Remove hardcoded QRACK defaults
  5. Add EXIF Support: Optional orientation correction
  6. Keep Backward Compatibility: QRACK can continue using data URL output

Mobile-First Considerations

iOS Camera Behavior

Android Camera Behavior

For Marketplace Items (Discart-me):

For Container Items (QRACK):

For Avatars:

EXIF Orientation Handling

Problem: Mobile cameras store rotation in EXIF, canvas doesn’t auto-apply it.

Solution Options:

  1. Use EXIF-JS library (add dependency): Read EXIF orientation, rotate canvas before drawing
  2. Use browser-image-compression library (add dependency): Handles EXIF automatically
  3. Manual rotation (complex): Parse EXIF, apply rotation manually

Recommendation: Start without EXIF handling (can add later), but document the limitation.


Proposed Shared Image Processing Library

Structure

src/lib/image/
  ├── processImage.ts      # Main processing function
  ├── types.ts             # TypeScript interfaces
  ├── constants.ts         # Default configurations per use case
  └── utils.ts             # Helper functions (validate, format, etc.)

Proposed API

Types (types.ts)

export interface ImageProcessingOptions {
  maxWidth?: number;
  maxHeight?: number;
  quality?: number; // 0.1 to 1.0
  maxSizeKB?: number; // Target max file size in KB
  outputFormat?: 'image/jpeg' | 'image/png' | 'image/webp';
  outputType?: 'dataUrl' | 'file'; // NEW: support both outputs
  maintainAspectRatio?: boolean; // Default: true
  // EXIF handling (future):
  // correctOrientation?: boolean; // Default: false (requires library)
}

export interface ProcessedImage {
  dataUrl?: string; // Present if outputType === 'dataUrl'
  file?: File;      // Present if outputType === 'file'
  originalSize: number;
  processedSize: number;
  originalDimensions: { width: number; height: number };
  processedDimensions: { width: number; height: number };
}

export interface ImageValidationResult {
  valid: boolean;
  error?: string;
}

Main Function (processImage.ts)

/**
 * Process an image file: resize, compress, and optionally correct orientation
 * 
 * @param file - Input image file
 * @param options - Processing options
 * @returns Processed image (data URL or File object)
 */
export async function processImage(
  file: File,
  options: ImageProcessingOptions = {}
): Promise<ProcessedImage>

/**
 * Validate image file type and size
 */
export function validateImageFile(
  file: File,
  maxSizeMB?: number
): ImageValidationResult

/**
 * Get image dimensions without fully loading
 */
export function getImageDimensions(
  file: File
): Promise<{ width: number; height: number }>

/**
 * Format bytes to human-readable string
 */
export function formatFileSize(bytes: number): string

Constants (constants.ts)

export const IMAGE_PRESETS = {
  marketplace: {
    maxWidth: 2048,
    maxHeight: 2048,
    quality: 0.85,
    maxSizeKB: 1500, // 1.5MB
    outputFormat: 'image/jpeg' as const,
  },
  container: {
    maxWidth: 1200,
    maxHeight: 1200,
    quality: 0.8,
    maxSizeKB: 500,
    outputFormat: 'image/jpeg' as const,
  },
  avatar: {
    maxWidth: 512,
    maxHeight: 512,
    quality: 0.8,
    maxSizeKB: 200,
    outputFormat: 'image/jpeg' as const,
  },
} as const;

Usage Patterns

Discart-me (File Output)

import { processImage, IMAGE_PRESETS } from '@/src/lib/image';

const processed = await processImage(file, {
  ...IMAGE_PRESETS.marketplace,
  outputType: 'file', // Returns File object for FormData
});

// Use processed.file for upload
await uploadDiscartItemImages([processed.file], token);

QRACK (Data URL Output - Backward Compatible)

import { processImage, IMAGE_PRESETS } from '@/src/lib/image';

const processed = await processImage(file, {
  ...IMAGE_PRESETS.container,
  outputType: 'dataUrl', // Returns data URL (current behavior)
});

// Use processed.dataUrl for API
setImageUrl(processed.dataUrl);

Where It Should Be Used

Integration Points:

  1. Discart-me ItemForm (src/components/discart-me/ItemForm.tsx)
    • Process files when selected in ImageUploadArea
    • Replace raw File objects with processed File objects
    • Show processing indicator during compression
  2. Discart-me Profile (app/discart-me/profile/page.tsx)
    • Process avatar before upload
    • Use IMAGE_PRESETS.avatar preset
  3. QRACK Item Pages (already using, but migrate to new API)
    • Continue using data URL output
    • Update imports to new location
    • Optionally use IMAGE_PRESETS.container

Synchronous vs. Async

Current: Async (uses Promise) ✅
Recommendation: Keep async (image processing is inherently async due to FileReader and canvas operations)

Browser-Only Constraints

Current: ✅ Browser-only (FileReader, Image, Canvas APIs)
Recommendation: Document clearly, use 'use client' directive, add runtime checks if needed


Risks and Considerations

1. Breaking Changes

Risk: Migrating QRACK from imageUtils.ts to new library could break existing code.

Mitigation:

2. Performance

Risk: Processing multiple images could block UI thread.

Mitigation:

3. Memory Usage

Risk: Large images loaded into memory (FileReader, Image, Canvas).

Mitigation:

4. Browser Compatibility

Risk: Canvas/FileReader APIs may not work in older browsers.

Mitigation:

5. EXIF Orientation (Future)

Risk: Images may appear rotated incorrectly on mobile.

Current: Not handled (document as limitation)
Future: Add EXIF-JS or browser-image-compression library

6. File Type Support

Risk: HEIC format (iOS) may not be supported by Canvas.

Mitigation:


Recommendation

Primary Recommendation: Extract & Generalize

Action: Extract image processing logic from src/lib/imageUtils.ts into a shared library src/lib/image/ with the following enhancements:

  1. ✅ Support both File and data URL outputs
  2. ✅ Configurable presets per use case
  3. ✅ Keep backward compatibility for QRACK
  4. ⚠️ Document EXIF orientation limitation (add handling later)
  5. ✅ No breaking changes during migration

Implementation Phases

Phase 1: Create Shared Library

Phase 2: Integrate in Discart-me

Phase 3: Migrate QRACK (optional)

Why Not Create New?

Existing logic is solid:

Only needs generalization:

Alternative: Use Library

Could use browser-image-compression (npm package):

Recommendation: Stick with custom solution (already works, just needs generalization).


Success Criteria


Follow-up Tasks (Not in Scope)

  1. EXIF Orientation Handling: Add support using EXIF-JS or similar
  2. Web Workers: Process images in background thread for multiple files
  3. HEIC Support: Handle iOS HEIC format conversion
  4. Progressive Upload: Show progress during processing
  5. Image Cropping: Add crop functionality for avatars
  6. Format Detection: Auto-detect best format (JPEG vs. WebP)

Conclusion

The codebase already has image processing logic in QRACK that is not being used by Discart-me. The recommended approach is to extract and generalize this logic into a shared library that supports both systems while maintaining backward compatibility.

The primary gap is that Discart-me uploads raw camera images without preprocessing, causing the mobile upload performance issues. The solution exists but needs to be shared and adapted for Discart-me’s File-based upload flow.

Next Step: Implementation proposal (separate task).


Document Status: ✅ Analysis Complete
Ready For: Implementation Proposal Phase