image_pixels_plus 1.0.0
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.
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;
}