flutter_image_compress

repository·main·Indexed 20 days ago

https://github.com/fluttercandies/flutter_image_compress

A high-performance Flutter plugin for image compression using native platform encoders (Kotlin/Android, Swift/Objective-C/iOS). It supports various input types (File, Asset, Bytes) and output formats including JPEG, PNG, WebP, and HEIF/HEIC. Key features include aspect-preserving scaling via minWidth and minHeight, EXIF metadata preservation, and automatic orientation correction.

Tokens
5.8K
Snippets
16
Records
25
Agent score
72%

What's inside flutter_image_compress

  1. Configure compression with minWidth and minHeight

    main

    Use minWidth and minHeight to bound the output size while preserving the source aspect ratio.

    Important Behavior:

    • The plugin calculates a scale based on these bounds. It will scale the image down to fit within these dimensions, but it will never upscale an image. If the source is already smaller than the provided bounds, the scale is clamped to 1.
    • Despite the names, these act like maximum bounds for the output dimensions.
    // Illustrative Dart port of the native logic.
    import 'dart:math' as math;
    
    void main() {
      final scale = calcScale(
        srcWidth: 4000,
        srcHeight: 2000,
        minWidth: 1920,
        minHeight: 1080,
      );
    
      print('scale = $scale'); // 1.8518518518518519
      print('target = ${4000 / scale} × ${2000 / scale}'); // 2160.0 × 1080.0
    }
    
    double calcScale({
      required double srcWidth,
      required double srcHeight,
      required double minWidth,
      required double minHeight,
    }) {
      final scaleW = srcWidth / minWidth;
      final scaleH = srcHeight / minHeight;
      return math.max(1.0, math.min(scaleW, scaleH));
    }
  2. Configure quality and format

    main

    Control the output file characteristics using quality and format:

    • quality: A value from 0 to 100. Note that on iOS, quality is ignored for PNG files because PNG is lossless.
    • format: Set via the CompressFormat enum.
      • JPEG/PNG: Supported on all platforms.
      • WebP: Supported on Android, iOS, and Web (browser dependent). Not supported on macOS.
      • HEIF/HEIC: Supported on Android (API 28+ with hardware encoder) and iOS (11+). Not supported on Web or OpenHarmony.
  3. Configure rotation and autoCorrectionAngle

    main

    You can control the orientation of the output image using rotate and autoCorrectionAngle:

    • rotate: Rotates the output by the specified number of degrees. Set to 0 to skip rotation.
    • autoCorrectionAngle: (Default: true) When enabled, the plugin reads the source's EXIF orientation and automatically rotates the image so the output is upright.

    Warning: If you provide both a non-zero rotate value and autoCorrectionAngle: true, the rotations will compound. To avoid double rotation, either set rotate: 0 or set autoCorrectionAngle: false.

  4. Handle return values (List<int> vs File)

    main

    The return types of the API calls differ in how they handle failure:

    • Byte-based APIs (compressWithList, etc.): These return a List<int>. They never return null; instead, they return an empty list if compression fails.
    • File-based APIs (compressAndGetFile): These return a File?. They can return null if compression fails. Additionally, always verify the file exists on disk before using it, as the file might be missing even if a value is returned.

    Converting List<int> to Uint8List for display: To use the result in an Image widget, convert the list to Uint8List:

    final image = Uint8List.fromList(imageList);
    final ImageProvider provider = MemoryImage(image);
    // Displaying the result in an Image widget:
    Future<Widget> _compressImage() async {
      final List<int> image = await testCompressFile(file);
      final ImageProvider provider = MemoryImage(Uint8List.fromList(image));
      return Image(image: provider);
    }
    
    // Writing the result to disk:
    Future<void> writeToFile(List<int> image, String filePath) {
      return File(filePath).writeAsBytes(image, flush: true);
    }
  5. How minWidth and minHeight work

    main

    Despite their names, minWidth and minHeight act as aspect-preserving upper bounds on the output. The plugin scales the image down (never up) so that both dimensions fit within the specified box while maintaining the original aspect ratio. No cropping occurs.

    Examples:

    • Source 4032×3024, minWidth: 1920, minHeight: 1920 $\rightarrow$ output ~1920×1440.
    • Source 1000×1000, minWidth: 500, minHeight: 500 $\rightarrow$ output 500×500.
    • Source 800×600, minWidth: 1920, minHeight: 1080 $\rightarrow$ output 800×600 (no upscale).
  6. Configure EXIF metadata retention

    main

    By default, keepExif is set to false, meaning the output contains only minimal container metadata (dimensions, color space). Setting keepExif: true copies source EXIF data to the output, but support depends on the platform and format:

    • iOS + macOS: Supports full sub-dict passthrough (EXIF, TIFF, GPS, IPTC, PNG chunks) for JPEG, PNG, and HEIC. WebP is not supported via ImageIO.
    • Android: Supports ~90-tag copy via androidx.exifinterface for JPEG, PNG, and WebP. HEIC is not supported (throws warning/refuses write).
    • Web + OpenHarmony: Metadata is stripped during the encoding pipeline.

    Note: The Orientation tag is always normalized to 1 (ORIENTATION_NORMAL) because the rotation is baked into the pixels during compression.

  7. Configure keepExif for metadata preservation

    main

    When keepExif is set to true, the plugin attempts to copy the source's EXIF metadata to the compressed output. Defaults to false.

    Important Considerations:

    • Orientation: The Orientation tag is always normalized to 1 (ORIENTATION_NORMAL) because the rotation is baked into the pixels.
    • Platform/Format Limitations: Metadata support varies. For example, WebP on iOS and HEIC on Android do not support metadata passthrough via this plugin.
    • Failure Mode: keepExif: true will never fail the compression call itself; if metadata cannot be written, the plugin simply returns a valid image without the EXIF data.
  8. Customize iOS launch screen assets

    main

    To change the launch screen image for the iOS version of your Flutter app, you can either replace the image files directly in the ios/Runner/Assets.xcassets/LaunchImage.imageset/ directory or use Xcode for a more visual approach.

    Using Xcode:

    1. Open your Flutter project's iOS workspace by running open ios/Runner.xcworkspace in your terminal.
    2. In the Xcode Project Navigator, navigate to Runner/Assets.xcassets.
    3. Drag and drop your desired images into the asset catalog to replace the existing launch screen assets.
    open ios/Runner.xcworkspace
  9. Using FlutterImageCompress in a background isolate

    main

    To use the plugin inside a background isolate or via compute(), you must initialize the binary messenger using the rootIsolateToken. Without this, you will encounter an UnimplementedError because platform channels require an initialized messenger.

    1. Obtain the rootIsolateToken in the main isolate using RootIsolateToken.instance!.
    2. Pass this token to your background isolate.
    3. Call BackgroundIsolateBinaryMessenger.ensureInitialized(token) inside the isolate before calling any FlutterImageCompress methods.
    // Inside the background isolate
    BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
    final out = await FlutterImageCompress.compressWithList(bytes, quality: 80);
    BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
    final out = await FlutterImageCompress.compressWithList(bytes, quality: 80);
  10. Migrate from 1.x to 2.x

    main

    When upgrading from version 1.x to 2.x, the primary breaking change is the return type of the FlutterImageCompress.compressAndGetFile method. It has changed from returning a File to returning an XFile (from the cross_file package).

    Key changes to implement:

    1. Update the variable type from File to XFile.
    2. Add the await keyword to the compressAndGetFile call, as it is now asynchronous.
    3. Update file operations (like getting length or reading bytes) to use the asynchronous XFile methods (length() and readAsBytes()) instead of the synchronous File methods (lengthSync() and readAsBytesSync()).
    // 2.0 implementation
    final XFile file = await FlutterImageCompress.compressAndGetFile(
          file.absolute.path,
          targetPath,
          quality: 90,
          minWidth: 1024,
          minHeight: 1024,
          rotate: 90,
        );
    
    int length = await file.length();
    Uint8List buffer = await file.readAsBytes();
  11. Compress an image to a target file size

    main

    Since quality and minWidth/minHeight do not map linearly to output bytes, you must use an iterative approach to hit a specific size limit. Start with a high quality and decrement it until the file size is within your threshold. If the file is still too large, you should also reduce minWidth and minHeight, as pixel count is the most significant factor in file size.

    Future<Uint8List> compressToUnder(Uint8List src, int limitBytes) async {
      var quality = 88;
      var out = src;
      while (quality > 10) {
        out = await FlutterImageCompress.compressWithList(
          src,
          quality: quality,
          minWidth: 1920,
          minHeight: 1920,
        );
        if (out.lengthInBytes <= limitBytes) return out;
        quality -= 10;
      }
      return out;
    }