mediaforge_flutter
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/COMsegments 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:
stripMetadataFileenforces 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 ceilingAuthException— missing, invalid, or revoked API keyPlanLimitException— storage or monthly operation limit exceeded, pre-flight payload exceedsmaxUploadBytesNetworkException— 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.
Links
Libraries
- local
- Offline-only, zero-network entry point for MediaForge Flutter SDK.
- mediaforge_flutter
- MediaForge Flutter SDK