native_image_compress 0.1.0
native_image_compress: ^0.1.0 copied to clipboard
Compress images to JPEG, PNG, WebP, HEIC, or AVIF using the platform's native encoders. Android only for now.
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:native_image_compress/image_compress.dart';
void main() {
runApp(const MyApp());
}
enum _Format { jpeg, png, webp, heic, avif }
extension on _Format {
String get label => switch (this) {
_Format.jpeg => 'JPEG',
_Format.png => 'PNG',
_Format.webp => 'WebP',
_Format.heic => 'HEIC',
_Format.avif => 'AVIF',
};
bool get supportsQuality => this != _Format.png;
double get defaultQuality => switch (this) {
_Format.jpeg => 80,
_Format.webp => 75,
_Format.heic => 70,
_Format.avif => 55,
_Format.png => 100,
};
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _picker = ImagePicker();
Uint8List? _original;
Uint8List? _compressed;
_Format _format = _Format.jpeg;
final Map<_Format, double> _quality = {
for (final f in _Format.values) f: f.defaultQuality,
};
bool _isCompressing = false;
String? _error;
Future<void> _pickImage() async {
final picked = await _picker.pickImage(source: ImageSource.gallery);
if (picked == null) return;
final bytes = await picked.readAsBytes();
setState(() {
_original = bytes;
_compressed = null;
_error = null;
});
}
Future<void> _compress() async {
final original = _original;
if (original == null) return;
setState(() {
_isCompressing = true;
_error = null;
});
try {
final quality = _quality[_format]!.round();
final result = switch (_format) {
_Format.jpeg => await compressToJpeg(original, quality: quality),
_Format.png => await compressToPng(original),
_Format.webp => await compressToWebp(original, quality: quality),
_Format.heic => await compressToHeic(original, quality: quality),
_Format.avif => await compressToAvif(original, quality: quality),
};
setState(() => _compressed = result);
} catch (e) {
setState(() => _error = e.toString());
} finally {
setState(() => _isCompressing = false);
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
final kb = bytes / 1024;
if (kb < 1024) return '${kb.toStringAsFixed(1)} KB';
return '${(kb / 1024).toStringAsFixed(2)} MB';
}
@override
Widget build(BuildContext context) {
final original = _original;
final compressed = _compressed;
final reduction = (original != null && compressed != null)
? 100 * (1 - compressed.length / original.length)
: null;
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('native_image_compress example')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.image),
label: const Text('Pick image'),
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SegmentedButton<_Format>(
segments: _Format.values
.map(
(f) => ButtonSegment(value: f, label: Text(f.label)),
)
.toList(),
selected: {_format},
onSelectionChanged: (selection) =>
setState(() => _format = selection.first),
),
),
const SizedBox(height: 8),
if (_format.supportsQuality)
Row(
children: [
const Text('Quality'),
Expanded(
child: Slider(
value: _quality[_format]!,
min: 1,
max: 100,
divisions: 99,
label: _quality[_format]!.round().toString(),
onChanged: (v) =>
setState(() => _quality[_format] = v),
),
),
Text(_quality[_format]!.round().toString()),
],
),
const SizedBox(height: 8),
FilledButton.icon(
onPressed: (_original == null || _isCompressing)
? null
: _compress,
icon: _isCompressing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.compress),
label: Text(_isCompressing ? 'Compressing...' : 'Compress'),
),
if (_error != null) ...[
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
],
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: _ImagePreview(
title: 'Original',
bytes: original,
subtitle: original == null
? null
: _formatBytes(original.length),
),
),
const SizedBox(width: 16),
Expanded(
child: _ImagePreview(
title: 'Compressed',
bytes: compressed,
subtitle: compressed == null
? null
: _formatBytes(compressed.length),
),
),
],
),
if (reduction != null) ...[
const SizedBox(height: 16),
Text(
reduction >= 0
? '${reduction.toStringAsFixed(1)}% smaller'
: '${(-reduction).toStringAsFixed(1)}% larger',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
],
],
),
),
),
),
);
}
}
class _ImagePreview extends StatelessWidget {
const _ImagePreview({
required this.title,
required this.bytes,
this.subtitle,
});
final String title;
final Uint8List? bytes;
final String? subtitle;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(title, style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
AspectRatio(
aspectRatio: 1,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
),
child: switch (bytes) {
null => const Icon(Icons.image_outlined, size: 48),
final b => Image.memory(
b,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.insert_drive_file_outlined, size: 48),
),
},
),
),
if (subtitle != null) ...[const SizedBox(height: 4), Text(subtitle!)],
],
);
}
}