native_image_compress 0.2.4 copy "native_image_compress: ^0.2.4" to clipboard
native_image_compress: ^0.2.4 copied to clipboard

Compress images to JPEG, PNG, WebP, HEIC, or AVIF using the platform's native encoders.

example/lib/main.dart

import 'dart:typed_data';
import 'dart:ui' as ui;

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,
  };
}

Future<Size?> _decodeImageSize(Uint8List bytes) async {
  try {
    final codec = await ui.instantiateImageCodec(bytes);
    final frame = await codec.getNextFrame();
    final size = Size(
      frame.image.width.toDouble(),
      frame.image.height.toDouble(),
    );
    frame.image.dispose();
    codec.dispose();
    return size;
  } catch (_) {
    return null;
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _picker = ImagePicker();

  Uint8List? _original;
  Size? _originalSize;
  Uint8List? _compressed;
  _Format _format = _Format.jpeg;
  final Map<_Format, double> _quality = {
    for (final f in _Format.values) f: f.defaultQuality,
  };

  bool _cropEnabled = false;
  double _cropX = 0;
  double _cropY = 0;
  double _cropWidth = 1;
  double _cropHeight = 1;

  bool _resizeEnabled = false;
  double _resizeMaxWidth = 1;
  double _resizeMaxHeight = 1;

  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();
    final size = await _decodeImageSize(bytes);

    setState(() {
      _original = bytes;
      _originalSize = size;
      _compressed = null;
      _error = null;
      _cropEnabled = false;
      _resizeEnabled = false;
      if (size != null) {
        _cropX = 0;
        _cropY = 0;
        _cropWidth = size.width;
        _cropHeight = size.height;
        _resizeMaxWidth = (size.width / 2).clamp(1, size.width);
        _resizeMaxHeight = (size.height / 2).clamp(1, size.height);
      }
    });
  }

  Future<void> _compress() async {
    final original = _original;
    if (original == null) return;

    setState(() {
      _isCompressing = true;
      _error = null;
    });

    try {
      final quality = _quality[_format]!.round();
      final cropOptions = _cropEnabled
          ? CropOptions(
              x: _cropX.round(),
              y: _cropY.round(),
              width: _cropWidth.round(),
              height: _cropHeight.round(),
            )
          : null;
      final resizeOptions = _resizeEnabled
          ? ResizeOptions(
              maxWidth: _resizeMaxWidth.round(),
              maxHeight: _resizeMaxHeight.round(),
            )
          : null;

      final result = switch (_format) {
        _Format.jpeg => await compressToJpeg(
          original,
          quality: quality,
          cropOptions: cropOptions,
          resizeOptions: resizeOptions,
        ),
        _Format.png => await compressToPng(
          original,
          cropOptions: cropOptions,
          resizeOptions: resizeOptions,
        ),
        _Format.webp => await compressToWebp(
          original,
          quality: quality,
          cropOptions: cropOptions,
          resizeOptions: resizeOptions,
        ),
        _Format.heic => await compressToHeic(
          original,
          quality: quality,
          cropOptions: cropOptions,
          resizeOptions: resizeOptions,
        ),
        _Format.avif => await compressToAvif(
          original,
          quality: quality,
          cropOptions: cropOptions,
          resizeOptions: resizeOptions,
        ),
      };
      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';
  }

  Widget _valueSlider({
    required String label,
    required double value,
    required double min,
    required double max,
    required ValueChanged<double> onChanged,
  }) {
    return Row(
      children: [
        SizedBox(width: 56, child: Text(label)),
        Expanded(
          child: Slider(
            value: value.clamp(min, max),
            min: min,
            max: max,
            label: value.round().toString(),
            onChanged: onChanged,
          ),
        ),
        SizedBox(width: 48, child: Text(value.round().toString())),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final original = _original;
    final originalSize = _originalSize;
    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()),
                    ],
                  ),
                if (originalSize != null) ...[
                  const Divider(height: 24),
                  SwitchListTile(
                    contentPadding: EdgeInsets.zero,
                    title: const Text('Crop'),
                    value: _cropEnabled,
                    onChanged: (v) => setState(() => _cropEnabled = v),
                  ),
                  if (_cropEnabled) ...[
                    _valueSlider(
                      label: 'X',
                      value: _cropX,
                      min: 0,
                      max: originalSize.width,
                      onChanged: (v) => setState(() => _cropX = v),
                    ),
                    _valueSlider(
                      label: 'Y',
                      value: _cropY,
                      min: 0,
                      max: originalSize.height,
                      onChanged: (v) => setState(() => _cropY = v),
                    ),
                    _valueSlider(
                      label: 'Width',
                      value: _cropWidth,
                      min: 1,
                      max: originalSize.width,
                      onChanged: (v) => setState(() => _cropWidth = v),
                    ),
                    _valueSlider(
                      label: 'Height',
                      value: _cropHeight,
                      min: 1,
                      max: originalSize.height,
                      onChanged: (v) => setState(() => _cropHeight = v),
                    ),
                  ],
                  SwitchListTile(
                    contentPadding: EdgeInsets.zero,
                    title: const Text('Resize'),
                    value: _resizeEnabled,
                    onChanged: (v) => setState(() => _resizeEnabled = v),
                  ),
                  if (_resizeEnabled) ...[
                    _valueSlider(
                      label: 'Max W',
                      value: _resizeMaxWidth,
                      min: 1,
                      max: originalSize.width,
                      onChanged: (v) => setState(() => _resizeMaxWidth = v),
                    ),
                    _valueSlider(
                      label: 'Max H',
                      value: _resizeMaxHeight,
                      min: 1,
                      max: originalSize.height,
                      onChanged: (v) => setState(() => _resizeMaxHeight = v),
                    ),
                  ],
                  const Divider(height: 24),
                ],
                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!)],
      ],
    );
  }
}
1
likes
0
points
439
downloads

Publisher

verified publishernexosphere.xyz

Weekly Downloads

Compress images to JPEG, PNG, WebP, HEIC, or AVIF using the platform's native encoders.

Repository
View/report issues

Topics

#image #compress #webp #heic #avif

License

unknown (license)

Dependencies

flutter, meta, plugin_platform_interface

More

Packages that depend on native_image_compress

Packages that implement native_image_compress