critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

ReceiptImageCompressor.compress(Uint8List inputBytes, {CompressionConfig config}) returns a CompressionResult containing compressed Uint8List, originalSizeBytes int, compressedSizeBytes int, and compressionRatio double
Output image max dimension (width or height) does not exceed 800px when input exceeds that dimension
Images that are already smaller than 800px in both dimensions are not upscaled
Default JPEG quality is 75 when no config is provided
Compression ratio is calculated as compressedSizeBytes / originalSizeBytes and is always between 0.0 and 1.0 for effective compression
The output is a valid JPEG byte stream (verifiable by decoding the result back through dart:ui)
A zero-byte input throws a typed CompressionException with message 'Input image is empty'
A corrupt byte input (not decodable as an image) throws a CompressionException with message 'Failed to decode image'
The service is exposed as a Riverpod Provider<ReceiptImageCompressor>
The method is async and does not block the UI thread

Technical Requirements

frameworks
Flutter
Riverpod
dart:ui
apis
dart:ui Image
dart:ui ByteData
dart:typed_data Uint8List
data models
CompressionConfig
CompressionResult
CompressionException
performance requirements
Compression of a 3MB JPEG must complete in under 3 seconds on a mid-range device
Memory usage during compression must not exceed 3x the input file size
The compress() method must run off the UI isolate to prevent jank
security requirements
Input bytes must not be logged or persisted beyond the compression operation
Output bytes must be cleared from memory after being handed to the caller

Execution Context

Execution Tier
Tier 1

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.

Component
Receipt Image Compressor
service medium
Epic Risks (3)
high impact medium prob security

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.

high impact medium prob technical

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.

medium impact medium prob dependency

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.