image_pixels_plus 1.0.0 copy "image_pixels_plus: ^1.0.0" to clipboard
image_pixels_plus: ^1.0.0 copied to clipboard

Read the width/height and the color of any pixel of an image — raster or SVG. A modern, Flutter-web-compatible fork of image_pixels.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_pixels_plus/image_pixels_plus.dart';

void main() {
  runApp(const ShowcaseApp());
}

/// The flutter logo used across the demos.
const AssetImage flutterLogo = AssetImage("lib/images/FlutterLogo.jpg");

class ShowcaseApp extends StatelessWidget {
  const ShowcaseApp({super.key});

  @override
  Widget build(BuildContext context) {
    const seed = Color(0xFF5865F2);

    return MaterialApp(
      title: 'image_pixels_plus',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: seed),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorScheme:
            ColorScheme.fromSeed(seedColor: seed, brightness: Brightness.dark),
      ),
      home: const ShowcaseHome(),
    );
  }
}

class ShowcaseHome extends StatelessWidget {
  const ShowcaseHome({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          toolbarHeight: 84,
          centerTitle: false,
          title: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('image_pixels_plus',
                  style: TextStyle(fontWeight: FontWeight.bold)),
              Text(
                'Read the size and color of any image pixel.',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.wallpaper), text: 'Container'),
              Tab(icon: Icon(Icons.colorize), text: 'Image'),
              Tab(icon: Icon(Icons.polyline), text: 'SVG'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            _BackdropDemo(),
            _RasterPickerDemo(),
            _SvgPickerDemo(),
          ],
        ),
      ),
    );
  }
}

// ------------------------------------------------------------------------------------------------------------------
// DEMO 1: Extend the background color of an image.
// ------------------------------------------------------------------------------------------------------------------

class _BackdropDemo extends StatefulWidget {
  const _BackdropDemo();

  @override
  State<_BackdropDemo> createState() => _BackdropDemoState();
}

class _BackdropDemoState extends State<_BackdropDemo> {
  Alignment _colorAlignment = Alignment.topLeft;

  String get _alignmentName => _AlignmentPicker.optionName(_colorAlignment);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const _Caption(
                  'ImagePixelsPlus.container paints the background with '
                  'the color of the pixel you choose. Tap a dot.'),
              const SizedBox(height: 24),
              ClipRRect(
                borderRadius: BorderRadius.circular(24),
                child: ImagePixelsPlus.container(
                  imageProvider: flutterLogo,
                  colorAlignment: _colorAlignment,
                  child: Container(
                    padding: const EdgeInsets.symmetric(
                        vertical: 48, horizontal: 48),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        const SizedBox(
                          width: 130,
                          child: Image(image: flutterLogo),
                        ),
                        const SizedBox(height: 20),
                        _TagChip('Alignment.$_alignmentName'),
                      ],
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 28),
              _AlignmentPicker(
                selected: _colorAlignment,
                onChanged: (Alignment alignment) =>
                    setState(() => _colorAlignment = alignment),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ------------------------------------------------------------------------------------------------------------------
// DEMO 2: Pick the color of any pixel of a regular image.
// ------------------------------------------------------------------------------------------------------------------

class _RasterPickerDemo extends StatefulWidget {
  const _RasterPickerDemo();

  @override
  State<_RasterPickerDemo> createState() => _RasterPickerDemoState();
}

class _RasterPickerDemoState extends State<_RasterPickerDemo> {
  /// Pointer position in DISPLAY coordinates.
  Offset? _pointer;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SingleChildScrollView(
        child: ImagePixelsPlus(
          imageProvider: flutterLogo,
          builder: (BuildContext context, ImgDetails img) {
            //
            // While the image downloads, show a progress indicator.
            //
            if (!img.hasImage || img.uiImage == null) {
              return const Center(child: CircularProgressIndicator());
            }

            final int w = img.width!;
            final int h = img.height!;

            // Display the image scaled-to-fit, and remember the scale so we
            // can convert pointer coordinates into pixel coordinates.
            final double scale = _scaleFor(w, h);
            final Size displaySize = Size(w * scale, h * scale);

            // Convert the pointer position into pixel coordinates.
            Offset? pixelPos;
            if (_pointer != null &&
                _pointer!.dx >= 0 &&
                _pointer!.dy >= 0 &&
                _pointer!.dx < displaySize.width &&
                _pointer!.dy < displaySize.height) {
              pixelPos = Offset(
                (_pointer!.dx / scale).floorToDouble(),
                (_pointer!.dy / scale).floorToDouble(),
              );
            }

            final Color? sampled = pixelPos == null
                ? null
                : img.pixelColorAt(pixelPos.dx.toInt(), pixelPos.dy.toInt());

            return Center(
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const _Caption('Move, tap or hover over the image.'),
                    const SizedBox(height: 16),
                    ClipRRect(
                      borderRadius: BorderRadius.circular(16),
                      child: MouseRegion(
                        onExit: (_) => setState(() => _pointer = null),
                        child: Listener(
                          onPointerDown: (details) =>
                              setState(() => _pointer = details.localPosition),
                          onPointerMove: (details) =>
                              setState(() => _pointer = details.localPosition),
                          onPointerHover: (details) =>
                              setState(() => _pointer = details.localPosition),
                          child: SizedBox(
                            width: displaySize.width,
                            height: displaySize.height,
                            child: FittedBox(
                              fit: BoxFit.fill,
                              child: RawImage(image: img.uiImage),
                            ),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 20),
                    _ColorReadout(
                      color: sampled,
                      pixelPosition: pixelPos,
                      imageSize: Size(w.toDouble(), h.toDouble()),
                    ),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

// ------------------------------------------------------------------------------------------------------------------
// DEMO 3: Same, but with an SVG rasterized into pixels.
// ------------------------------------------------------------------------------------------------------------------

class _SvgPickerDemo extends StatefulWidget {
  const _SvgPickerDemo();

  @override
  State<_SvgPickerDemo> createState() => _SvgPickerDemoState();
}

class _SvgPickerDemoState extends State<_SvgPickerDemo> {
  static const double rasterSide = 240;

  Offset? _pointer;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SingleChildScrollView(
        child: ImagePixelsPlusSvg.asset(
          assetName: 'lib/images/FlutterLogo.svg',
          width: rasterSide,
          height: rasterSide,
          placeholder: (context) =>
              const Center(child: CircularProgressIndicator()),
          errorBuilder: (context, error) =>
              Center(child: Text('SVG error: $error')),
          builder: (BuildContext context, ImgDetails img) {
            final double scale = _scaleFor(rasterSide, rasterSide);
            final Size displaySize =
                Size(rasterSide * scale, rasterSide * scale);

            Offset? pixelPos;
            if (_pointer != null &&
                _pointer!.dx >= 0 &&
                _pointer!.dy >= 0 &&
                _pointer!.dx < displaySize.width &&
                _pointer!.dy < displaySize.height) {
              pixelPos = Offset(
                (_pointer!.dx / scale).floorToDouble(),
                (_pointer!.dy / scale).floorToDouble(),
              );
            }

            final Color? sampled = pixelPos == null
                ? null
                : img.pixelColorAt(pixelPos.dx.toInt(), pixelPos.dy.toInt());

            return Center(
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const _Caption('Move, tap or hover over the image. '
                        '\nTransparent areas report their alpha too.'),
                    const SizedBox(height: 16),
                    ClipRRect(
                      borderRadius: BorderRadius.circular(16),
                      child: MouseRegion(
                        onExit: (_) => setState(() => _pointer = null),
                        child: Listener(
                          onPointerDown: (details) =>
                              setState(() => _pointer = details.localPosition),
                          onPointerMove: (details) =>
                              setState(() => _pointer = details.localPosition),
                          onPointerHover: (details) =>
                              setState(() => _pointer = details.localPosition),
                          child: Container(
                            width: displaySize.width,
                            height: displaySize.height,
                            decoration: BoxDecoration(
                              border: Border.all(
                                  color: Theme.of(context).dividerColor),
                            ),
                            child: FittedBox(
                              fit: BoxFit.fill,
                              child: RawImage(image: img.uiImage),
                            ),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 20),
                    _ColorReadout(
                      color: sampled,
                      pixelPosition: pixelPos,
                      imageSize: Size(img.width?.toDouble() ?? 0,
                          img.height?.toDouble() ?? 0),
                    ),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

// ------------------------------------------------------------------------------------------------------------------
// Shared widgets and helpers.
// ------------------------------------------------------------------------------------------------------------------

/// The explanatory text above each demo.
class _Caption extends StatelessWidget {
  const _Caption(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24),
      child: Text(
        text,
        textAlign: TextAlign.center,
        style: Theme.of(context)
            .textTheme
            .bodyMedium
            ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
      ),
    );
  }
}

/// A 3×3 grid of dots to choose an [Alignment], like in design tools.
class _AlignmentPicker extends StatelessWidget {
  const _AlignmentPicker({
    required this.selected,
    required this.onChanged,
  });

  final Alignment selected;

  final ValueChanged<Alignment> onChanged;

  static const List<(Alignment, String)> options = [
    (Alignment.topLeft, 'topLeft'),
    (Alignment.topCenter, 'topCenter'),
    (Alignment.topRight, 'topRight'),
    (Alignment.centerLeft, 'centerLeft'),
    (Alignment.center, 'center'),
    (Alignment.centerRight, 'centerRight'),
    (Alignment.bottomLeft, 'bottomLeft'),
    (Alignment.bottomCenter, 'bottomCenter'),
    (Alignment.bottomRight, 'bottomRight'),
  ];

  static String optionName(Alignment alignment) =>
      options.firstWhere((option) => option.$1 == alignment).$2;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    Widget cell((Alignment, String) option) {
      final bool isSelected = option.$1 == selected;

      return Padding(
        padding: const EdgeInsets.all(5),
        child: InkWell(
          key: Key('align-${option.$2}'),
          onTap: () => onChanged(option.$1),
          customBorder: const CircleBorder(),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 180),
            width: 28,
            height: 28,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: isSelected ? scheme.primary : Colors.transparent,
              border: Border.all(
                color: isSelected ? scheme.primary : scheme.outlineVariant,
                width: 2,
              ),
            ),
            child: isSelected
                ? Icon(Icons.check_rounded, size: 16, color: scheme.onPrimary)
                : null,
          ),
        ),
      );
    }

    return Column(
      children: [
        for (int row = 0; row < 3; row++)
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              for (int col = 0; col < 3; col++) cell(options[row * 3 + col]),
            ],
          ),
      ],
    );
  }
}

/// Small pill used to annotate the demos.
class _TagChip extends StatelessWidget {
  const _TagChip(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceDim,
        borderRadius: BorderRadius.circular(100),
      ),
      child: Text(
        text,
        style: Theme.of(context)
            .textTheme
            .labelSmall
            ?.copyWith(fontFamily: 'monospace', fontWeight: FontWeight.bold),
      ),
    );
  }
}

/// The big color swatch plus the readout of the sampled pixel.
class _ColorReadout extends StatelessWidget {
  const _ColorReadout({
    required this.color,
    required this.pixelPosition,
    required this.imageSize,
  });

  final Color? color;

  /// Position in PIXEL coordinates, or null when nothing was sampled.
  final Offset? pixelPosition;

  final Size imageSize;

  static const double swatchSide = 96;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    if (color == null || pixelPosition == null) {
      return Container(
        width: swatchSide,
        height: 56,
        alignment: Alignment.center,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(14),
          border: Border.all(color: scheme.outlineVariant),
        ),
        child: Text(
          'sample me',
          style: Theme.of(context)
              .textTheme
              .labelMedium
              ?.copyWith(color: scheme.onSurfaceVariant),
        ),
      );
    }

    final Color c = color!;

    return Column(
      children: [
        GestureDetector(
          onTap: () => _copy(context),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 180),
            width: swatchSide,
            height: swatchSide,
            decoration: BoxDecoration(
              color: c,
              borderRadius: BorderRadius.circular(18),
              border: Border.all(color: scheme.outlineVariant),
              boxShadow: [
                BoxShadow(
                  blurRadius: 12,
                  spreadRadius: -4,
                  color: c.withAlpha((c.a * 255.0 * 0.9).round()),
                ),
              ],
            ),
            child: Center(
              child: Icon(
                Icons.copy_rounded,
                size: 22,
                color: c.computeLuminance() > 0.5
                    ? Colors.black54
                    : Colors.white70,
              ),
            ),
          ),
        ),
        const SizedBox(height: 12),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SelectableText(
              c.hex6,
              style: const TextStyle(
                  fontFamily: 'monospace', fontWeight: FontWeight.bold),
            ),
            const SizedBox(width: 16),
            SelectableText(
              'rgba(${c.rInt}, ${c.gInt}, ${c.bInt}, ${(c.a * 100).toStringAsFixed(0)}%)',
              style: TextStyle(
                fontFamily: 'monospace',
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
            ),
          ],
        ),
        const SizedBox(height: 6),
        Text(
          'pixel (${pixelPosition!.dx.toInt()}, ${pixelPosition!.dy.toInt()}) '
          'of ${imageSize.width.toInt()}×${imageSize.height.toInt()}',
          style: Theme.of(context)
              .textTheme
              .bodySmall
              ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
        ),
      ],
    );
  }

  void _copy(BuildContext context) {
    Clipboard.setData(ClipboardData(text: color!.hex6));
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          content: Text('${color!.hex6} copied'),
          duration: const Duration(seconds: 1),
          width: 220,
          behavior: SnackBarBehavior.floating,
        ),
      );
  }
}

extension _ColorChannels on Color {
  int get rInt => (r * 255.0).round();

  int get gInt => (g * 255.0).round();

  int get bInt => (b * 255.0).round();

  String _hexChannel(int v) =>
      v.toRadixString(16).toUpperCase().padLeft(2, '0');

  String get hex6 =>
      '#${_hexChannel(rInt)}${_hexChannel(gInt)}${_hexChannel(bInt)}';
}

/// Scale that fits [w]×[h] into a 320×320 box (never above 2×, so tiny images
/// stay recognizable).
double _scaleFor(num w, num h) {
  final double fit = 320.0 / ((w > h ? w : h).toDouble());
  return fit > 2.0 ? 2.0 : fit;
}
3
likes
160
points
66
downloads
screenshot

Documentation

API reference

Publisher

verified publisheresentis.dev

Weekly Downloads

Read the width/height and the color of any pixel of an image — raster or SVG. A modern, Flutter-web-compatible fork of image_pixels.

Repository (GitHub)
View/report issues

Topics

#image #image-processing #image-pixels #image-colors #widget

License

MIT (license)

Dependencies

flutter, flutter_svg

More

Packages that depend on image_pixels_plus