cropImageNative method

  1. @override
Future<String?> cropImageNative({
  1. required String path,
  2. required double cropX,
  3. required double cropY,
  4. required double cropW,
  5. required double cropH,
  6. required double containerW,
  7. required double containerH,
  8. int rotation = 0,
  9. int quality = 85,
  10. String format = 'jpeg',
  11. int maxSize = 1200,
})
override

Crops an image natively without going through a Dart isolate.

Uses Android's BitmapFactory.inSampleSize / platform-native equivalents to decode only a fraction of the pixels, making resize ~50–150 ms instead of the ~10 s that pure-Dart decode takes on a 2+ MB JPEG.

path absolute path (or object URL on web) of the source image. cropX/cropY top-left of the crop rect in container coordinates. cropW/cropH size of the crop rect in container coordinates. containerW/containerH size of the Flutter widget that displayed the image. rotation clockwise degrees applied before cropping (0, 90, 180, 270). quality JPEG/WebP encode quality 0–100 (default 85, ignored for PNG). format output format: "jpeg" | "png" | "webp_lossy" | "webp_lossless". maxSize max edge length used when decoding the source (default 1200).

Returns the absolute path of the cropped file, or null on failure.

Implementation

@override
Future<String?> cropImageNative({
  required String path,
  required double cropX,
  required double cropY,
  required double cropW,
  required double cropH,
  required double containerW,
  required double containerH,
  int rotation = 0,
  int quality = 85,
  String format = 'jpeg',
  int maxSize = 1200,
}) async {
  try {
    // ── Step 1: load the source blob URL ─────────────────────────────
    final img = web.document.createElement('img') as web.HTMLImageElement;
    final loadCompleter = Completer<bool>();
    img.addEventListener(
      'load',
      (web.Event _) {
        loadCompleter.complete(true);
      }.toJS,
    );
    img.addEventListener(
      'error',
      (web.Event _) {
        loadCompleter.complete(false);
      }.toJS,
    );
    img.src = path;
    if (!await loadCompleter.future) return null;

    final origW = img.naturalWidth;
    final origH = img.naturalHeight;

    // ── Step 2: apply rotation to a temp canvas ───────────────────────
    final rotRad = rotation * 3.141592653589793 / 180.0;
    final bool swapped = rotation == 90 || rotation == 270;
    final rotW = swapped ? origH : origW;
    final rotH = swapped ? origW : origH;

    final rotCanvas =
        web.document.createElement('canvas') as web.HTMLCanvasElement;
    rotCanvas.width = rotW;
    rotCanvas.height = rotH;
    final rotCtx = rotCanvas.getContext('2d') as web.CanvasRenderingContext2D;
    rotCtx.translate((rotW / 2).toDouble(), (rotH / 2).toDouble());
    rotCtx.rotate(rotRad);
    rotCtx.drawImage(img, (-origW / 2).toDouble(), (-origH / 2).toDouble());

    final imgW = rotW.toDouble();
    final imgH = rotH.toDouble();

    // ── Step 3: map crop rect from container coords → image coords ────
    final imgAspect = imgW / imgH;
    final contAspect = containerW / containerH;
    final double displayedW, displayedH, offsetX, offsetY;
    if (imgAspect > contAspect) {
      displayedW = containerW;
      displayedH = containerW / imgAspect;
      offsetX = 0;
      offsetY = (containerH - displayedH) / 2;
    } else {
      displayedH = containerH;
      displayedW = containerH * imgAspect;
      offsetX = (containerW - displayedW) / 2;
      offsetY = 0;
    }
    final scaleX = imgW / displayedW;
    final scaleY = imgH / displayedH;
    final px = ((cropX - offsetX) * scaleX).clamp(0, imgW - 1);
    final py = ((cropY - offsetY) * scaleY).clamp(0, imgH - 1);
    final pw = (cropW * scaleX).clamp(1, imgW - px);
    final ph = (cropH * scaleY).clamp(1, imgH - py);

    // ── Step 4: draw cropped region onto output canvas ────────────────
    final outCanvas =
        web.document.createElement('canvas') as web.HTMLCanvasElement;
    outCanvas.width = pw.round();
    outCanvas.height = ph.round();
    final outCtx = outCanvas.getContext('2d') as web.CanvasRenderingContext2D;
    outCtx.drawImage(rotCanvas, px, py, pw, ph, 0, 0, pw, ph);

    // ── Step 5: encode to chosen format ──────────────────────────────
    final String mimeType;
    switch (format) {
      case 'png':
        mimeType = 'image/png';
        break;
      case 'webp_lossy':
      case 'webp_lossless':
        mimeType = 'image/webp';
        break;
      default:
        mimeType = 'image/jpeg';
    }
    final q = quality / 100.0;
    final dataUrl = outCanvas.toDataURL(mimeType, q.toJS);
    final base64 = dataUrl.split(',').last;
    final bytes = _base64ToBytes(base64);
    final blob = web.Blob(
      [bytes.toJS].toJS,
      web.BlobPropertyBag(type: mimeType),
    );
    return web.URL.createObjectURL(blob);
  } catch (_) {
    return null;
  }
}