Client-side image compression pipeline implementation
epic-receipt-capture-and-attachment-foundation-task-014 — Implement the ReceiptImageCompressor service that accepts raw image bytes and a target quality configuration, applies JPEG compression using Flutter's dart:ui Image and ByteData APIs, and returns the compressed Uint8List with metadata (original size, compressed size, compression ratio). The pipeline must target approximately 800px max dimension and configurable JPEG quality (default 75).
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use dart:ui for pixel-accurate resize and JPEG encoding: decode raw bytes with instantiateImageCodec, read the first frame, get the image handle, then use toByteData(format: ImageByteFormat.rawRgba) to get pixels, scale using a Canvas with drawImageRect onto a Picture, and encode back with encodeImageProvider or toByteData(format: ImageByteFormat.png) — note that dart:ui does not natively encode to JPEG, so use the flutter_image_compress package or the image Dart package for JPEG encoding at configurable quality. Prefer the image package (pure Dart) over native plugins for testability. The resize-then-compress order is important: resize first to reduce pixel count, then JPEG encode to reduce file size. Define CompressionConfig as an immutable data class with const constructor.
Run the heavy computation in a compute() isolate to keep the UI thread free. File location: lib/features/receipts/domain/receipt_image_compressor.dart.
Testing Requirements
Unit tests are covered in task-016. For this implementation: manually test with at least three image sizes (small <100KB, medium ~1MB, large ~5MB) and verify output dimensions and file sizes meet the 800px and quality targets. Verify the service works in a Flutter test environment using flutter_test with a real image fixture. Confirm that CompressionException is thrown and typed correctly for invalid input — not a generic Exception.
Supabase Storage RLS policies using org/user/claim path scoping may not enforce correctly if claim ownership is not present in the JWT or if path segments are constructed differently at upload vs. read time, leading to data leakage or access denial for legitimate users.
Mitigation & Contingency
Mitigation: Define and test RLS policies in isolation before wiring to app code. Write integration tests that assert cross-org and cross-user access is denied. Use service-role key only in edge functions, never in client code.
Contingency: If client-side RLS proves insufficient, route all storage reads through a Supabase Edge Function that validates ownership before generating signed URLs, adding a controlled server-side enforcement layer.
Aggressive image compression may reduce receipt legibility below the threshold required for financial auditing, causing claim rejections or compliance failures despite technically successful uploads.
Mitigation & Contingency
Mitigation: Define minimum legibility requirements with HLF finance team before implementation. Set compression targets conservatively (e.g., max 1MB, min 80% JPEG quality) and validate with sample receipt images. Provide compression statistics in verbose/debug mode.
Contingency: If post-compression quality is disputed by auditors, increase the quality floor at the cost of larger file sizes, and add a manual override allowing users to skip compression for PDFs and high-quality scans.
The Flutter image_picker package behaves differently on iOS 17+ (PHPicker) vs older Android (Intent-based), particularly for file types, permission flows, and PDF selection, which may cause platform-specific failures not caught in development.
Mitigation & Contingency
Mitigation: Test image picker integration on physical devices for both platforms early in the sprint. Pin the image_picker package version and review changelogs before updates. Write widget tests using mock file results for each platform branch.
Contingency: If PHPicker or Android Intent differences cause blocking issues, implement separate platform-specific picker delegates behind the unified interface, allowing platform-specific fixes without breaking the shared API.