diff --git a/lib/main.dart b/lib/main.dart index 6ba7f93..aa5a546 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/provider/color_adjustment_provider.dart'; import 'package:magic_epaper_app/provider/getitlocator.dart'; import 'package:magic_epaper_app/provider/image_loader.dart'; import 'package:provider/provider.dart'; @@ -8,6 +9,7 @@ void main() { setupLocator(); runApp(MultiProvider(providers: [ ChangeNotifierProvider(create: (context) => ImageLoader()), + ChangeNotifierProvider(create: (context) => ColorAdjustmentProvider()), ], child: const MyApp())); } diff --git a/lib/provider/color_adjustment_provider.dart b/lib/provider/color_adjustment_provider.dart new file mode 100644 index 0000000..140e015 --- /dev/null +++ b/lib/provider/color_adjustment_provider.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class ColorAdjustmentProvider extends ChangeNotifier { + Map _weights = {}; + + Map get weights => _weights; + + void resetWeights(List colors) { + _weights = {for (var color in colors) color: 1.0}; + } + + void updateWeights(Map newWeights) { + _weights = newWeights; + notifyListeners(); + } +} diff --git a/lib/util/epd/epd.dart b/lib/util/epd/epd.dart index 38c2a41..dfbe19b 100644 --- a/lib/util/epd/epd.dart +++ b/lib/util/epd/epd.dart @@ -7,7 +7,8 @@ import 'package:magic_epaper_app/util/image_processing/image_processing.dart'; abstract class Epd { int get width; int get height; - final processingMethods = []; + final processingMethods = + )>[]; String get name; String get modelId; String get imgPath; diff --git a/lib/util/image_editor_utils.dart b/lib/util/image_editor_utils.dart index 89d0ff3..b9099a7 100644 --- a/lib/util/image_editor_utils.dart +++ b/lib/util/image_editor_utils.dart @@ -1,9 +1,11 @@ +import 'dart:ui'; import 'package:image/image.dart' as img; import 'package:magic_epaper_app/util/epd/epd.dart'; List processImages({ required img.Image originalImage, required Epd epd, + required Map weights, }) { final List processedImgs = []; @@ -14,7 +16,7 @@ List processImages({ ); for (final method in epd.processingMethods) { - processedImgs.add(method(transformed)); + processedImgs.add(method(transformed, weights)); } return processedImgs; diff --git a/lib/util/image_processing/image_processing.dart b/lib/util/image_processing/image_processing.dart index cef88a1..cd042f8 100644 --- a/lib/util/image_processing/image_processing.dart +++ b/lib/util/image_processing/image_processing.dart @@ -5,81 +5,95 @@ import 'extract_quantizer.dart'; import 'remap_quantizer.dart'; class ImageProcessing { - static img.Image bwFloydSteinbergDither(img.Image orgImg) { + static img.Image bwFloydSteinbergDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, quantizer: img.BinaryQuantizer()); } - static img.Image bwFalseFloydSteinbergDither(img.Image orgImg) { + static img.Image bwFalseFloydSteinbergDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, quantizer: img.BinaryQuantizer(), kernel: img.DitherKernel.falseFloydSteinberg); } - static img.Image bwStuckiDither(img.Image orgImg) { + static img.Image bwStuckiDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, quantizer: img.BinaryQuantizer(), kernel: img.DitherKernel.stucki); } - static img.Image bwAtkinsonDither(img.Image orgImg) { + static img.Image bwAtkinsonDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, quantizer: img.BinaryQuantizer(), kernel: img.DitherKernel.atkinson); } - static img.Image bwThreshold(img.Image orgImg) { + static img.Image bwThreshold(img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, quantizer: img.BinaryQuantizer(), kernel: img.DitherKernel.none); } - static img.Image bwHalftoneDither(img.Image orgImg) { + static img.Image bwHalftoneDither( + img.Image orgImg, Map weights) { final image = img.Image.from(orgImg); img.grayscale(image); img.colorHalftone(image, size: 3); return img.ditherImage(image, quantizer: img.BinaryQuantizer()); } - static img.Image bwrHalftone(img.Image orgImg) { + static img.Image bwrHalftone(img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); img.colorHalftone(image, size: 3); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.floydSteinberg); } - static img.Image bwrFloydSteinbergDither(img.Image orgImg) { + static img.Image bwrFloydSteinbergDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.floydSteinberg); } - static img.Image bwrFalseFloydSteinbergDither(img.Image orgImg) { + static img.Image bwrFalseFloydSteinbergDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.falseFloydSteinberg); } - static img.Image bwrStuckiDither(img.Image orgImg) { + static img.Image bwrStuckiDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.stucki); } - static img.Image bwrTriColorAtkinsonDither(img.Image orgImg) { + static img.Image bwrTriColorAtkinsonDither( + img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.atkinson); } @@ -91,11 +105,12 @@ class ImageProcessing { kernel: img.DitherKernel.none); } - static img.Image bwrThreshold(img.Image orgImg) { + static img.Image bwrThreshold(img.Image orgImg, Map weights) { var image = img.Image.from(orgImg); return img.ditherImage(image, - quantizer: RemapQuantizer(palette: _createTriColorPalette()), + quantizer: + RemapQuantizer(palette: _createTriColorPalette(), weights: weights), kernel: img.DitherKernel.none); } } diff --git a/lib/util/image_processing/remap_quantizer.dart b/lib/util/image_processing/remap_quantizer.dart index 14fd5ed..e3d9889 100644 --- a/lib/util/image_processing/remap_quantizer.dart +++ b/lib/util/image_processing/remap_quantizer.dart @@ -1,5 +1,6 @@ import 'package:image/image.dart' as img; import 'dart:typed_data'; +import 'package:flutter/material.dart'; class RemapQuantizer extends img.Quantizer { @override @@ -10,11 +11,12 @@ class RemapQuantizer extends img.Quantizer { late final Uint8List _paletteR; late final Uint8List _paletteG; late final Uint8List _paletteB; + final Map weights; final Map _colorCache = {}; static const int _maxCacheSize = 1024; - RemapQuantizer({required this.palette}) { + RemapQuantizer({required this.palette, this.weights = const {}}) { final numColors = palette.numColors; _paletteR = Uint8List(numColors); @@ -88,7 +90,15 @@ class RemapQuantizer extends img.Quantizer { final dr = r - _paletteR[paletteIndex]; final dg = g - _paletteG[paletteIndex]; final db = b - _paletteB[paletteIndex]; - return dr * dr + dg * dg + db * db; + final distanceSq = dr * dr + dg * dg + db * db; + final paletteColor = Color.fromARGB(255, _paletteR[paletteIndex], + _paletteG[paletteIndex], _paletteB[paletteIndex]); + + final weight = weights[paletteColor] ?? 1.0; + + if (weight <= 0) return 2147483647; + + return (distanceSq / weight).round(); } void _addToCache(int key, int value) { diff --git a/lib/view/image_editor.dart b/lib/view/image_editor.dart index 71d752e..dcb3bb7 100644 --- a/lib/view/image_editor.dart +++ b/lib/view/image_editor.dart @@ -1,8 +1,12 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:magic_epaper_app/pro_image_editor/features/movable_background_image.dart'; +import 'package:magic_epaper_app/provider/color_adjustment_provider.dart'; +import 'package:magic_epaper_app/provider/color_palette_provider.dart'; +import 'package:magic_epaper_app/provider/getitlocator.dart'; import 'package:magic_epaper_app/util/image_editor_utils.dart'; import 'package:magic_epaper_app/view/widget/image_list.dart'; +import 'package:pro_image_editor/pro_image_editor.dart'; import 'package:provider/provider.dart'; import 'package:image/image.dart' as img; @@ -11,6 +15,8 @@ import 'package:magic_epaper_app/util/epd/epd.dart'; import 'package:magic_epaper_app/constants/color_constants.dart'; import 'package:magic_epaper_app/util/protocol.dart'; +final _colors = getIt().colors; + class ImageEditor extends StatefulWidget { final Epd epd; const ImageEditor({super.key, required this.epd}); @@ -27,6 +33,15 @@ class _ImageEditorState extends State { img.Image? _processedSourceImage; List _rawImages = []; List _processedPngs = []; + Map _currentWeights = {}; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().resetWeights(_colors); + }); + } void _onFilterSelected(int index) { if (_selectedFilterIndex != index) { @@ -48,7 +63,8 @@ class _ImageEditorState extends State { }); } - void _updateProcessedImages(img.Image? sourceImage) { + void _updateProcessedImages( + img.Image? sourceImage, Map weights) { if (sourceImage == null) { if (_rawImages.isNotEmpty) { setState(() { @@ -60,13 +76,15 @@ class _ImageEditorState extends State { return; } - if (_processedSourceImage == sourceImage) { + if (_processedSourceImage == sourceImage && + _currentWeights.toString() == weights.toString()) { return; } _rawImages = processImages( originalImage: sourceImage, epd: widget.epd, + weights: weights, ); _processedPngs = _rawImages @@ -75,16 +93,25 @@ class _ImageEditorState extends State { setState(() { _processedSourceImage = sourceImage; - _selectedFilterIndex = 0; - flipHorizontal = false; - flipVertical = false; + _currentWeights = weights; + if (_processedSourceImage != sourceImage) { + _selectedFilterIndex = 0; + flipHorizontal = false; + flipVertical = false; + } }); } @override Widget build(BuildContext context) { var imgLoader = context.watch(); - _updateProcessedImages(imgLoader.image); + var colorAdjuster = context.watch(); + + if (imgLoader.image != null && colorAdjuster.weights.isEmpty) { + colorAdjuster.resetWeights(_colors); + } + + _updateProcessedImages(imgLoader.image, colorAdjuster.weights); return Scaffold( backgroundColor: Colors.white, @@ -153,6 +180,7 @@ class _ImageEditorState extends State { bottomNavigationBar: BottomActionMenu( epd: widget.epd, imgLoader: imgLoader, + colors: _colors, ), ); } @@ -161,15 +189,26 @@ class _ImageEditorState extends State { class BottomActionMenu extends StatelessWidget { final Epd epd; final ImageLoader imgLoader; + final List colors; const BottomActionMenu({ super.key, required this.epd, required this.imgLoader, + required this.colors, }); + void _showColorAdjustmentSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + builder: (_) => ColorAdjustmentSliders(colors: colors), + ); + } + @override Widget build(BuildContext context) { + var colorAdjuster = context.watch(); return Container( height: 75, decoration: BoxDecoration( @@ -195,6 +234,7 @@ class BottomActionMenu extends StatelessWidget { label: 'Import New', onTap: () { imgLoader.pickImage(width: epd.width, height: epd.height); + colorAdjuster.resetWeights(_colors); }, ), _buildActionButton( @@ -210,6 +250,7 @@ class BottomActionMenu extends StatelessWidget { ), ); if (canvasBytes != null) { + colorAdjuster.resetWeights(_colors); imgLoader.updateImage( bytes: canvasBytes, width: epd.width, @@ -218,6 +259,66 @@ class BottomActionMenu extends StatelessWidget { } }, ), + _buildActionButton( + context: context, + icon: Icons.tune_rounded, + label: 'Adjust', + onTap: () async { + if (imgLoader.image != null) { + final canvasBytes = await Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => ProImageEditor.memory( + img.encodeJpg(imgLoader.image!), + callbacks: ProImageEditorCallbacks( + onImageEditingComplete: (Uint8List bytes) async { + Navigator.pop(context, bytes); + }, + ), + configs: const ProImageEditorConfigs( + paintEditor: PaintEditorConfigs(enabled: false), + textEditor: TextEditorConfigs(enabled: false), + cropRotateEditor: CropRotateEditorConfigs( + enabled: false, + ), + emojiEditor: EmojiEditorConfigs(enabled: false), + ), + ), + )); + if (canvasBytes != null) { + imgLoader.updateImage( + bytes: canvasBytes, + width: epd.width, + height: epd.height, + ); + colorAdjuster.resetWeights(_colors); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + duration: Durations.medium4, + content: Text('Import an image first!'), + backgroundColor: colorPrimary), + ); + } + }, + ), + _buildActionButton( + context: context, + icon: Icons.palette_outlined, + label: 'Adjust Colors', + onTap: () { + if (imgLoader.image != null) { + _showColorAdjustmentSheet(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + duration: Durations.medium4, + content: Text('Import an image first!'), + backgroundColor: colorPrimary), + ); + } + }, + ), ], ), ), @@ -252,3 +353,110 @@ class BottomActionMenu extends StatelessWidget { ); } } + +class ColorAdjustmentSliders extends StatefulWidget { + final List colors; + const ColorAdjustmentSliders({super.key, required this.colors}); + + @override + State createState() => _ColorAdjustmentSlidersState(); +} + +class _ColorAdjustmentSlidersState extends State { + late Map _localWeights; + + @override + void initState() { + super.initState(); + _localWeights = Map.from( + context.read().weights); + } + + String _getColorName(Color color) { + if (color == Colors.black) return 'Black'; + if (color == Colors.white) return 'White'; + if (color == Colors.red) return 'Red'; + return 'Color'; + } + + @override + Widget build(BuildContext context) { + final colorAdjuster = context.read(); + + return Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Adjust Color Intensity', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + ...widget.colors.map((color) { + return Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: Colors.grey.shade400), + ), + ), + const SizedBox(width: 12), + Text(_getColorName(color), + style: const TextStyle(fontSize: 16)), + Expanded( + child: Slider( + value: _localWeights[color] ?? 1.0, + min: 0.0, + max: 10.0, + divisions: 30, + label: (_localWeights[color] ?? 1.0).toStringAsFixed(1), + activeColor: colorAccent, + onChanged: (newValue) { + setState(() { + _localWeights[color] = newValue; + }); + }, + ), + ), + ], + ); + }), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + setState(() { + for (var key in _localWeights.keys) { + _localWeights[key] = 1.0; + } + }); + }, + child: const Text('Reset'), + ), + const SizedBox(width: 8), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colorAccent, + foregroundColor: Colors.white, + ), + onPressed: () { + colorAdjuster.updateWeights(_localWeights); + Navigator.pop(context); + }, + child: const Text('Apply'), + ), + ], + ) + ], + ), + ); + } +}