file_compression_plus 1.0.0
file_compression_plus: ^1.0.0 copied to clipboard
Compress images and PDFs in Flutter. Hit an exact target file size, batch compress with progress, convert JPG PNG WebP HEIC, and shrink PDF documents.
example/lib/main.dart
import 'dart:io';
import 'package:file_compression_plus/file_compression_plus.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const CompressionDemoApp());
}
class CompressionDemoApp extends StatelessWidget {
const CompressionDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'File Compression Plus',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const CompressionDemoPage(),
);
}
}
class CompressionDemoPage extends StatefulWidget {
const CompressionDemoPage({super.key});
@override
State<CompressionDemoPage> createState() => _CompressionDemoPageState();
}
class _CompressionDemoPageState extends State<CompressionDemoPage> {
static const _anyGroup = XTypeGroup(
label: 'Images and PDF',
extensions: ['jpg', 'jpeg', 'png', 'webp', 'heic', 'pdf'],
);
double _quality = 80;
double _targetMb = 1;
PdfCompressionLevel _pdfLevel = PdfCompressionLevel.best;
bool _isBusy = false;
double? _batchProgress;
CompressionResult? _result;
BatchCompressionResult? _batchResult;
String? _error;
/// Compresses one file, letting the package pick image or PDF by extension.
Future<void> _compressOne() async {
final picked = await openFile(acceptedTypeGroups: [_anyGroup]);
if (picked == null) return;
await _run(() => FileCompressor.compress(
file: File(picked.path),
quality: _quality.round(),
pdfCompressionLevel: _pdfLevel,
));
}
/// Compresses an image down to a byte budget, the usual upload-limit case.
Future<void> _compressToTarget() async {
final picked = await openFile(acceptedTypeGroups: [
const XTypeGroup(
label: 'Images',
extensions: ['jpg', 'jpeg', 'png', 'webp', 'heic'],
)
]);
if (picked == null) return;
await _run(() => FileCompressor.compressImageToTargetSize(
file: File(picked.path),
targetBytes: (_targetMb * 1024 * 1024).round(),
));
}
/// Compresses many files at once and follows along with a progress bar.
Future<void> _compressBatch() async {
final picked = await openFiles(acceptedTypeGroups: [_anyGroup]);
if (picked.isEmpty) return;
setState(() {
_isBusy = true;
_result = null;
_batchResult = null;
_error = null;
_batchProgress = 0;
});
try {
final batch = await FileCompressor.compressBatch(
files: picked.map((file) => File(file.path)).toList(),
quality: _quality.round(),
pdfCompressionLevel: _pdfLevel,
onProgress: (progress) {
if (mounted) setState(() => _batchProgress = progress.fraction);
},
);
if (!mounted) return;
setState(() => _batchResult = batch);
} catch (error) {
if (!mounted) return;
setState(() => _error = '$error');
} finally {
if (mounted) {
setState(() {
_isBusy = false;
_batchProgress = null;
});
}
}
}
/// Runs one compression job and moves the screen through busy, done, error.
Future<void> _run(Future<CompressionResult> Function() job) async {
setState(() {
_isBusy = true;
_result = null;
_batchResult = null;
_error = null;
});
try {
final result = await job();
if (!mounted) return;
setState(() => _result = result);
} catch (error) {
if (!mounted) return;
setState(() => _error = '$error');
} finally {
if (mounted) setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(
title: const Text('File Compression Plus'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('Image quality: ${_quality.round()}',
style: textTheme.titleMedium),
Slider(
value: _quality,
min: 10,
max: 100,
divisions: 18,
label: '${_quality.round()}',
onChanged:
_isBusy ? null : (value) => setState(() => _quality = value),
),
Text('PDF compression level', style: textTheme.titleMedium),
const SizedBox(height: 8),
SegmentedButton<PdfCompressionLevel>(
segments: const [
ButtonSegment(
value: PdfCompressionLevel.none, label: Text('None')),
ButtonSegment(
value: PdfCompressionLevel.normal, label: Text('Normal')),
ButtonSegment(
value: PdfCompressionLevel.best, label: Text('Best')),
],
selected: {_pdfLevel},
onSelectionChanged: _isBusy
? null
: (selection) => setState(() => _pdfLevel = selection.first),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: _isBusy ? null : _compressOne,
icon: const Icon(Icons.compress),
label: const Text('Compress one file'),
),
const SizedBox(height: 12),
FilledButton.tonalIcon(
onPressed: _isBusy ? null : _compressBatch,
icon: const Icon(Icons.library_add_check_outlined),
label: const Text('Compress many files'),
),
const Divider(height: 40),
Text('Target size: ${_targetMb.toStringAsFixed(1)} MB',
style: textTheme.titleMedium),
Slider(
value: _targetMb,
min: 0.1,
max: 5,
divisions: 49,
label: '${_targetMb.toStringAsFixed(1)} MB',
onChanged:
_isBusy ? null : (value) => setState(() => _targetMb = value),
),
FilledButton.tonalIcon(
onPressed: _isBusy ? null : _compressToTarget,
icon: const Icon(Icons.straighten),
label: const Text('Compress an image to that size'),
),
const SizedBox(height: 24),
if (_isBusy)
_BusyIndicator(progress: _batchProgress)
else ...[
if (_error != null) _ErrorCard(message: _error!),
if (_result != null) _ResultCard(result: _result!),
if (_batchResult != null) _BatchCard(batch: _batchResult!),
],
],
),
);
}
}
class _BusyIndicator extends StatelessWidget {
const _BusyIndicator({this.progress});
final double? progress;
@override
Widget build(BuildContext context) {
if (progress == null) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
LinearProgressIndicator(value: progress),
const SizedBox(height: 8),
Text('${(progress! * 100).round()} percent'),
],
);
}
}
class _ResultCard extends StatelessWidget {
const _ResultCard({required this.result});
final CompressionResult result;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
result.wasSkipped ? 'Already small enough' : 'Done',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text('Before: ${result.readableOriginalSize}'),
Text('After: ${result.readableCompressedSize}'),
Text('Saved: ${result.savedPercentage.toStringAsFixed(1)} percent'),
const SizedBox(height: 8),
SelectableText(result.path),
],
),
),
);
}
}
class _BatchCard extends StatelessWidget {
const _BatchCard({required this.batch});
final BatchCompressionResult batch;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Batch finished',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('${batch.successes.length} compressed, '
'${batch.failures.length} failed'),
Text('Before: ${formatBytes(batch.totalOriginalSize)}'),
Text('After: ${formatBytes(batch.totalCompressedSize)}'),
Text('Saved: ${formatBytes(batch.totalSavedBytes)} '
'(${batch.savedPercentage.toStringAsFixed(1)} percent)'),
for (final failure in batch.failures) ...[
const SizedBox(height: 8),
Text('Failed: ${failure.file.path}',
style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
],
),
),
);
}
}
class _ErrorCard extends StatelessWidget {
const _ErrorCard({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(message),
),
);
}
}