mediaforge_flutter

pub package pub points license

Privacy-first media processing and CDN upload for Flutter and Dart.

Strip EXIF metadata on-device or in cloud, compress, resize, convert formats — then upload to MediaForge CDN. Your server never sees the raw file.

Why MediaForge?

Feature mediaforge_flutter flutter_image_compress cloudinary_flutter
EXIF metadata reading Yes (typed, categorized) No No
Metadata stripping Yes (with verification) Side-effect only No
Selective stripping Yes (GPS only, etc.) No No
Post-strip verification Yes (unique) No No
Compress / resize Yes Yes No (server-side)
Format conversion Yes (JPEG/PNG) Yes No (server-side)
CDN upload Yes (Cloudflare R2 edge) No Yes (raw file!)
Cloud strip streaming Yes (/v1/developer/strip) No No
Video support Yes (MP4, MOV on Forge) No Yes
App backend sees raw metadata? Never (stripped on-device) N/A Always
Pure Dart (no native) Yes No Yes
Works on Web Yes Limited Yes

Installation

dependencies:
  mediaforge_flutter: ^0.3.0

Or run:

dart pub add mediaforge_flutter

Quick Start — Local Processing (No API key needed)

Local on-device processing requires zero setup and works offline.

Read Metadata

import 'package:mediaforge_flutter/mediaforge_flutter.dart';

final metadata = await MediaForge.readMetadata(imageBytes);

if (metadata.hasLocation) {
  print('GPS: ${metadata.gpsLatitude}, ${metadata.gpsLongitude}');
  print('Maps: ${metadata.googleMapsUrl}');
}

if (metadata.hasDeviceInfo) {
  print('Camera: ${metadata.cameraMake} ${metadata.cameraModel}');
  print('Serial: ${metadata.serialNumber}');
}

print('Total fields: ${metadata.fieldCount}');
print('Has sensitive data: ${metadata.hasSensitiveData}');

Strip Metadata On-Device

// Strip ALL metadata locally (100% lossless for JPEG & WebP: zero re-encode)
final cleanBytes = await MediaForge.stripMetadata(imageBytes);

// Deep container verification (scans raw headers for EXIF, XMP, IPTC, COM, PNG chunks)
final isClean = await MediaForge.verifyClean(cleanBytes);
print('Clean: $isClean'); // true

// Strip and verify in one call
final result = await MediaForge.stripAndVerify(imageBytes);
print('Removed ${result.fieldsRemoved} fields');
print('Verified: ${result.isVerified}');

Strip GPS Only

// Remove location data, keep camera info and timestamps
final safe = await MediaForge.stripGps(imageBytes);

Process Images

// Using ProcessingOptions
final processed = await MediaForge.process(imageBytes, ProcessingOptions(
  stripMetadata: true,
  outputFormat: ImageFormat.jpeg,
  quality: 85,
  resizeWidth: 800,
  resizeHeight: 600,
  resizeMode: ResizeMode.fit,
));

Lossless Stripping & Format notes: Metadata stripping on both JPEG and WebP is 100% lossless at the byte level (APP1/APP13/COM segments and RIFF container chunks are stripped directly without decoding or re-encoding pixels, preserving exact image quality). For image resizing and compression, the on-device engine encodes JPEG and PNG output. Pure-Dart WebP encoding is not available; for WebP output, use cloud processing.

Processing Pipeline (Chainable & Background Isolates)

// Chain processing steps (automatically runs in background isolate for images >500KB)
final result = await MediaForge.pipeline(imageBytes)
  .stripMetadata()
  .resize(width: 800, height: 600, mode: ResizeMode.fit)
  .compress(quality: 85)
  .convert(format: ImageFormat.jpeg)
  .execute();

print('Output: ${result.sizeBytes} bytes');
print('Savings: ${result.savingsDisplay}'); // e.g., "-42%"
print('Dimensions: ${result.width}x${result.height}');

// Use the processed bytes
final processedBytes = result.bytes;

Fluent Pipeline-to-Upload Chaining (v0.3.0)

Chain directly from processing into a CDN upload in a single statement:

final uploaded = await MediaForge.pipeline(rawBytes)
  .stripMetadata()
  .compress(quality: 80)
  .upload(filename: 'clean.jpg');

print('CDN URL: ${uploaded.cdnUrl}');

Direct File & Stream Support (OOM Prevention)

Process and stream camera images without loading the entire 20MB–50MB file into memory:

// Stream directly from a File or XFile
await MediaForge.readMetadataFile(file);
await MediaForge.stripMetadataFile(file, destinationFile: cleanFile);
final uploaded = await MediaForge.uploadFile(file);

// Or stream arbitrary chunks
final streamed = await MediaForge.uploadStream(
  file.openRead(),
  length: await file.length(),
  filename: 'camera.jpg',
);

Strict Offline Privacy (MediaForge.local & local.dart)

For strict privacy audits and zero network permissions, use the explicit MediaForge.local namespace or import package:mediaforge_flutter/local.dart (which contains zero HTTP dependencies):

import 'package:mediaforge_flutter/local.dart';

// In-memory operations:
final metadata = await MediaForgeLocal.readMetadata(bytes);
final clean = await MediaForgeLocal.stripMetadata(bytes);

// On-device file operations (bounded memory & safe temp-write verification):
final fileMeta = await LocalFileSupport.readMetadataFile(file);
final cleanFile = await LocalFileSupport.stripMetadataFile(file);

Safety & Bounded Memory: stripMetadataFile enforces a 50MB ceiling (LocalFileSupport.maxStripFileSizeBytes) to protect low-memory devices, and writes via verified temporary files so original source files are preserved intact if writing fails.


Developer Cloud APIs & CDN Upload (v0.3.0+)

Cloud capabilities require an API key (mf_live_... or mf_test_...) from your MediaForge Dashboard.

1. Initialize the SDK

MediaForge.init(
  apiKey: 'mf_live_xxxx',
  timeout: const Duration(seconds: 30),        // Metadata timeout
  uploadTimeout: const Duration(seconds: 120), // Generous upload timeout
  maxUploadBytes: 10 * 1024 * 1024,            // 10MB pre-flight ceiling
);

2. Verify Health & Key Connectivity

final health = await MediaForge.checkHealth();
print('Status: ${health.status}');           // 'ok'
print('Authenticated: ${health.authenticated}');
print('Plan: ${health.apiKey?.plan}');       // 'forge' or 'forge+'

3. Direct CDN Edge Upload

Upload clean media directly to Cloudflare R2 edge storage with real-time byte progress or structured phase updates:

final uploaded = await MediaForge.upload(
  cleanBytes,
  filename: 'profile.jpg',
  // Structured phase updates (buffering -> edgeProcessing -> completed)
  onProgressEvent: (event) {
    switch (event.phase) {
      case UploadPhase.buffering:
        print('Sending: ${(event.progress * 100).round()}%');
      case UploadPhase.edgeProcessing:
        print('Processing and committing on edge…');
      case UploadPhase.completed:
        print('Upload complete!');
    }
  },
  // Or standard callback (reports 1.0 only after server confirms)
  onProgress: (progress) {
    print('Upload: ${(progress * 100).toStringAsFixed(1)}%');
  },
);

print('Public CDN URL: ${uploaded.cdnUrl}');
print('File ID: ${uploaded.fileId}');

4. Process and Upload in One Call

final result = await MediaForge.processAndUpload(
  rawBytes,
  options: const ProcessingOptions(
    stripMetadata: true,
    resizeWidth: 1080,
    outputFormat: ImageFormat.jpeg,
  ),
  filename: 'banner.jpg',
  onProgress: (p) => print('${(p * 100).round()}%'),
);
print('CDN URL: ${result.cdnUrl}');

5. Server-Side Strip Stream (Cloud Offloading)

Offload stripping to the server without storing the file on the CDN:

final cleanStream = await MediaForge.stripCloud(largeImageBytes);

6. Check Real-Time Account Quotas

final usage = await MediaForge.getUsage();
print('Plan: ${usage.plan}');
print('Operations: ${usage.operationsUsed} / ${usage.operationsLimit}');
print('Storage: ${usage.storageUsedBytes} / ${usage.storageLimitBytes} bytes');
print('CDN Upload Allowed: ${usage.cdnUploadAllowed}');
print('Video Allowed: ${usage.allowVideo}');

7. Remote Asset Management & Batch Deletion

// List uploaded files (paginated)
final fileList = await MediaForge.listFiles(page: 1, limit: 20);
for (final file in fileList.files) {
  print('${file.filename} -> ${file.cdnUrl}');
}

// Delete a single asset
await MediaForge.deleteFile(fileList.files.first.id);

// Batch delete multiple assets in parallel
final batchResult = await MediaForge.deleteFiles(['file-id-1', 'file-id-2']);
print('Deleted: ${batchResult.deleted.length}, Failed: ${batchResult.failed.length}');

Direct Client Instance

If you prefer dependency injection or multiple accounts/keys, use MediaForgeClient directly:

final client = MediaForgeClient(
  apiKey: 'mf_live_custom_key',
  baseUrl: 'https://api.mediaforge.tech',
);

final health = await client.checkHealth();
final usage = await client.getUsage();
final file = await client.uploadToCdn(bytes, filename: 'hero.png');

client.close();

Error Handling

All errors extend MediaForgeException for typed, predictable handling:

try {
  final uploaded = await MediaForge.upload(bytes);
} on AuthException catch (e) {
  print('Authentication failed: ${e.message}');
} on PlanLimitException catch (e) {
  print('Plan limit reached: ${e.message} (${e.limitName})');
} on ProcessingException catch (e) {
  print('Processing failed: ${e.message}');
} on NetworkException catch (e) {
  print('Network issue: ${e.message} (Status: ${e.statusCode})');
} on MediaForgeException catch (e) {
  print('MediaForge error: ${e.message}');
}

Exception types:

  • ProcessingException — corrupt file, unsupported format, file exceeds local 50MB ceiling
  • AuthException — missing, invalid, or revoked API key
  • PlanLimitException — storage or monthly operation limit exceeded, pre-flight payload exceeds maxUploadBytes
  • NetworkException — connectivity, timeout, HTTP 429 rate limit, 5xx server errors, edge HTML errors, malformed responses

Privacy Architecture

User's Device                          MediaForge Server
--------------                         ----------------
[Raw Image + EXIF]
       |
  MediaForge SDK
  (runs locally)
       |
  Strip metadata
  Compress / resize
  Convert format
       |
[Clean Image ONLY] ---- upload ----> [Stores processed file on R2]
                                      [Returns public CDN URL]

The server NEVER receives:
  - GPS coordinates
  - Camera serial numbers
  - Original timestamps
  - Author information
  - Any raw/unprocessed data

Platform Support

Platform Status
Android Supported
iOS Supported
macOS Supported
Windows Supported
Linux Supported
Web Supported

Pure Dart — no platform-specific native code required.

API Reference

See the full API documentation.

License

Apache License 2.0. See LICENSE for details.

Libraries

local
Offline-only, zero-network entry point for MediaForge Flutter SDK.
mediaforge_flutter
MediaForge Flutter SDK