Skip to content

feat: Export XBM Image file #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc01993
Fixed sizing for multiple displays
Vishveshwara Jun 24, 2025
1762360
fixed formatting
Vishveshwara Jun 24, 2025
75b34cd
WIP: initial implementation of XBM export
Vishveshwara Jul 1, 2025
491ac49
Merge branch 'main' into export
Vishveshwara Jul 2, 2025
cfd3491
FIxed Build issue
Vishveshwara Jul 2, 2025
2596bcb
Merge branch 'main' into export
Vishveshwara Jul 2, 2025
cf6998b
Fixing Common Build
Vishveshwara Jul 3, 2025
9c1a923
removed import in xbmencoder
Vishveshwara Jul 3, 2025
f27f8f2
Added Color_Util to avoid code duplication
Vishveshwara Jul 4, 2025
4108413
Fixed XBM encoding logic issue
Vishveshwara Jul 4, 2025
1848c82
Code refactoring and documentation added
Vishveshwara Jul 7, 2025
2794d4b
Fixing info from flutter analyze
Vishveshwara Jul 7, 2025
d82bb27
fix common build
Vishveshwara Jul 7, 2025
e2ee492
dart format
Vishveshwara Jul 7, 2025
75d89d0
fixing common build
Vishveshwara Jul 7, 2025
c93ec47
common build fixed
Vishveshwara Jul 7, 2025
be4ff7b
Merge branch 'main' into export
Vishveshwara Jul 7, 2025
bff950f
Merge branch 'main' into export
Vishveshwara Jul 10, 2025
a0f2ba7
dart formatting
Vishveshwara Jul 10, 2025
38f6c55
fix error
Vishveshwara Jul 10, 2025
0e6f7d9
Added some presets for the displays from good display and wave share …
Vishveshwara Jul 11, 2025
e91f503
formatted
Vishveshwara Jul 11, 2025
576aa3f
Merge branch 'main' into export
Vishveshwara Jul 17, 2025
1ccaa47
Fixed merging errors
Vishveshwara Jul 17, 2025
ee260f5
Fixed UI and added file path
Vishveshwara Jul 18, 2025
95c4cc7
Merge branch 'main' into export
Vishveshwara Jul 23, 2025
483203b
Removed refresh options for the export screen and fixed conflicts
Vishveshwara Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
<application
android:label="magic_epaper_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is added for the File_saver module to work


<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
Binary file added assets/images/displays/export_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/constants/asset_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class ImageAssets {
static const String blackBoard = 'assets/canvas/black.png';
static const String epaper37Bwr = 'assets/images/displays/epaper_3.7_bwr.png';
static const String epaper37Bw = 'assets/images/displays/epaper_3.7_bw.PNG';
static const String customExport = 'assets/images/displays/export_image.png';
static const String tempIcon = 'assets/icons/icon.png';
static const String githubIcon = 'assets/icons/github.png';
static const String badgeIcon = 'assets/icons/badge.png';
Expand Down
12 changes: 0 additions & 12 deletions lib/pro_image_editor/core/constants/example_constants.dart

This file was deleted.

18 changes: 10 additions & 8 deletions lib/pro_image_editor/core/mixin/example_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:vibration/vibration.dart';

import '../../features/preview/preview_img.dart';
import '../constants/example_constants.dart';
export '../../shared/widgets/prepare_image_widget.dart';

const kImageEditorExampleIsDesktopBreakPoint = 900;

/// A mixin that provides helper methods and state management for image editing
/// using the [ProImageEditor]. It is intended to be used in a [StatefulWidget].
mixin ExampleHelperState<T extends StatefulWidget> on State<T> {
Expand Down Expand Up @@ -152,11 +153,11 @@ mixin ExampleHelperState<T extends StatefulWidget> on State<T> {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
precacheImage(
assetPath != null
? AssetImage(assetPath)
: NetworkImage(networkUrl!) as ImageProvider,
context)
.whenComplete(() {
assetPath != null
? AssetImage(assetPath)
: NetworkImage(networkUrl!) as ImageProvider,
context,
).whenComplete(() {
if (!mounted) return;
isPreCached = true;
setState(() {});
Expand Down Expand Up @@ -185,8 +186,9 @@ mixin ExampleHelperState<T extends StatefulWidget> on State<T> {
/// This will set `deviceCanCustomVibrate` anyway to true so it's
/// impossible to fake it.
Vibration.vibrate();
Future.delayed(const Duration(milliseconds: 3))
.whenComplete(Vibration.cancel);
Future.delayed(
const Duration(milliseconds: 3),
).whenComplete(Vibration.cancel);
}
}
}
3 changes: 2 additions & 1 deletion lib/pro_image_editor/features/movable_background_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ class _MovableBackgroundImageExampleState
offset: Offset.zero,
scale: _initScale,
widget: Image.asset(
'assets/canvas/${_currentCanvasColor}.png',
'assets/canvas/$_currentCanvasColor.png',
width: _canvasWidth,
height: _canvasHeight,
fit: BoxFit.cover,
Expand Down Expand Up @@ -515,6 +515,7 @@ class _MovableBackgroundImageExampleState
? _editorSize.height
: _editorSize.width) /
_initScale,
// ignore: deprecated_member_use
buildStickers: (setLayer, scrollController) {
// Optionally your code to pick layers
return const SizedBox();
Expand Down
1 change: 0 additions & 1 deletion lib/pro_image_editor/features/reorder_layer_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:pro_image_editor/pro_image_editor.dart';

// Project imports:
import '../core/mixin/example_helper.dart';
import '../shared/widgets/prepare_image_widget.dart';

/// A widget that demonstrates the ability to reorder layers within a UI.
///
Expand Down
72 changes: 72 additions & 0 deletions lib/util/color_util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';

/// A utility class for color-related operations and mappings.
class ColorUtils {
/// A mapping from [Color] objects to their display names.
static final Map<Color, String> _colorMap = {
Colors.black: 'Black',
Colors.white: 'White',
Colors.red: 'Red',
Colors.yellow: 'Yellow',
Colors.orange: 'Orange',
Colors.green: 'Green',
Colors.blue: 'Blue',
};

/// Compares two [Color] objects for equality based on their ARGB values.
///
/// Returns `true` if both colors have the same ARGB value, otherwise `false`.
static bool colorsEqual(Color a, Color b) {
return a.toARGB32() == b.toARGB32();
}

/// Checks if a given [color] exists within the provided [colorList].
///
/// Returns `true` if the color is found in the list, otherwise `false`.
static bool colorExistsInList(Color color, List<Color> colorList) {
return colorList.any((c) => colorsEqual(c, color));
}

/// Returns a user-friendly display name for the given [color].
///
/// If the color is not found in the predefined color map, returns 'Color'.
static String getColorDisplayName(Color color) {
for (final entry in _colorMap.entries) {
if (colorsEqual(entry.key, color)) {
return entry.value;
}
}
return 'Color';
}

/// Returns a file-friendly name for the given [color], used in exports.
///
/// If the color is not a predefined one, returns its ARGB value as a hex string.
static String getColorFileName(Color color) {
if (colorsEqual(color, Colors.black)) return 'black';
if (colorsEqual(color, Colors.red)) return 'red';
if (colorsEqual(color, Colors.yellow)) return 'yellow';
if (colorsEqual(color, Colors.blue)) return 'blue';
if (colorsEqual(color, Colors.green)) return 'green';
if (colorsEqual(color, Colors.orange)) return 'orange';
return color.toARGB32().toRadixString(16);
}

/// Returns a label for the given [color], using available color choices as fallback.
///
/// If the color is not a predefined one, checks [availableColorChoices] for a matching label.
/// If no match is found, returns 'Color'.
static String getColorLabel(
Color color, List<dynamic> availableColorChoices) {
final displayName = getColorDisplayName(color);
if (displayName != 'Color') return displayName;

for (final choice in availableColorChoices) {
if (colorsEqual(choice.color, color)) {
return choice.label;
}
}

return 'Color';
}
}
174 changes: 174 additions & 0 deletions lib/util/epd/configurable_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:magic_epaper_app/constants/asset_paths.dart';
import 'package:magic_epaper_app/util/epd/driver/driver.dart';
import 'package:magic_epaper_app/util/epd/driver/uc8253.dart';
import 'package:magic_epaper_app/util/image_processing/image_processing.dart';
import 'package:image/image.dart' as img;
import 'epd.dart';

/// Represents a named image filter for use in e-paper image processing.
///
/// Associates a filter function with a display name for UI and export purposes.
class NamedImageFilter {
/// The image filter function to apply.
final img.Image Function(img.Image) filter;

/// The display name of the filter.
final String name;

/// Creates a [NamedImageFilter] with the given [filter] and [name].
NamedImageFilter(this.filter, this.name);
}

/// A dynamically configurable e-paper display for custom export and processing.
///
/// This class is used to export dithered images and supports:
/// - Changing the color palette for dithering (custom colors)
/// - Setting the height and width of the display
/// - Generating XBM output for custom displays
///
/// It allows users to experiment with different display configurations and image processing methods.
class ConfigurableEpd extends Epd {
/// The width of the display in pixels.
@override
final int width;

/// The height of the display in pixels.
@override
final int height;

/// The display name for UI and export.
@override
final String name;

/// The list of supported colors for dithering and export.
@override
final List<Color> colors;

/// The model identifier for this configurable export display.
@override
String get modelId => 'NA';

/// The asset path for the display image (custom export icon).
@override
String get imgPath => ImageAssets.customExport;

/// Using a dummy driver as this is only for exporting images.
@override
Driver get controller => Uc8253() as Driver;

/// The list of named image processing methods available for this display.
final List<NamedImageFilter> namedProcessingMethods = [];

@override
List<img.Image Function(img.Image)> get processingMethods =>
namedProcessingMethods.map((f) => f.filter).toList();

List<String> get processingMethodNames =>
namedProcessingMethods.map((f) => f.name).toList();

/// Creates a [ConfigurableEpd] with the given [width], [height], [colors], and optional [name].
///
/// The color palette and display size can be customized, and dithering methods can be chosen.
ConfigurableEpd({
required this.width,
required this.height,
required this.colors,
this.name = 'Custom Export',
}) {
_addProcessingMethods();
}

/// Creates a palette for the 'image' library from a list of Flutter [Color]s.
///
/// Used for custom dithering with custom color palettes.
img.PaletteUint8 _createDynamicPalette() {
final palette = img.PaletteUint8(colors.length, 3);
for (int i = 0; i < colors.length; i++) {
final color = colors[i];
palette.setRgb(i, (color.r * 255.0).round(), (color.g * 255.0).round(),
(color.b * 255.0).round());
}
return palette;
}

/// Populates the list of processing methods based on the color palette.
void _addProcessingMethods() {
namedProcessingMethods.clear();
final isBlackAndWhite = colors.length == 2;
if (isBlackAndWhite) {
namedProcessingMethods.add(
NamedImageFilter(
ImageProcessing.bwFloydSteinbergDither,
'Floyd-Steinberg',
),
);
namedProcessingMethods.add(
NamedImageFilter(
ImageProcessing.bwFalseFloydSteinbergDither,
'False Floyd-Steinberg',
),
);
namedProcessingMethods.add(
NamedImageFilter(ImageProcessing.bwStuckiDither, 'Stucki'),
);
namedProcessingMethods.add(
NamedImageFilter(ImageProcessing.bwAtkinsonDither, 'Atkinson'),
);
namedProcessingMethods.add(
NamedImageFilter(ImageProcessing.bwHalftoneDither, 'Halftone'),
);
namedProcessingMethods.add(
NamedImageFilter(ImageProcessing.bwThreshold, 'Threshold'),
);
} else {
final dynamicPalette = _createDynamicPalette();
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) => ImageProcessing.customFloydSteinbergDither(
orgImg,
dynamicPalette,
),
'Floyd-Steinberg',
),
);
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) => ImageProcessing.customFalseFloydSteinbergDither(
orgImg,
dynamicPalette,
),
'False Floyd-Steinberg',
),
);
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) =>
ImageProcessing.customStuckiDither(orgImg, dynamicPalette),
'Stucki',
),
);
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) =>
ImageProcessing.customAtkinsonDither(orgImg, dynamicPalette),
'Atkinson',
),
);
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) =>
ImageProcessing.customHalftoneDither(orgImg, dynamicPalette),
'Halftone',
),
);
namedProcessingMethods.add(
NamedImageFilter(
(img.Image orgImg) =>
ImageProcessing.customThreshold(orgImg, dynamicPalette),
'Threshold',
),
);
}
}
}
4 changes: 4 additions & 0 deletions lib/util/epd/epd.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,9 @@ abstract class Epd {
}
return retList;
}

img.Image extractColorPlaneAsImage(Color color, img.Image orgImage) {
return ImageProcessing.extract(color, orgImage);
}
// TODO: howToAdjust ???
}
Loading