Last active
May 23, 2024 12:34
-
-
Save LokieVikky/a9a2a10b705e189ed0ec4fac42905220 to your computer and use it in GitHub Desktop.
Image Editor API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'dart:math'; | |
import 'dart:ui' as ui; | |
import 'package:flutter/material.dart'; | |
import 'package:image_editor/image_editor_v2.dart'; | |
class ImageEditor { | |
double imageHeight = 0.0; | |
double imageWidth = 0.0; | |
Rect? areaToCrop; | |
Orientation currentOrientation = Orientation.vertical; | |
late ui.PictureRecorder pr; | |
late ui.Image image; | |
ColorFilter? colorFilter; | |
late Canvas canvas; | |
List<double> getDynamicColorFilter(num brightness, num contrast, num midtone) { | |
final double b = brightness / 100.0; | |
final double c = contrast / 100.0 + 1.0; | |
final double m = midtone / 100.0; | |
return [ | |
c, | |
0, | |
0, | |
0, | |
b - (c * 0.5) + m, | |
0, | |
c, | |
0, | |
0, | |
b - (c * 0.5) + m, | |
0, | |
0, | |
c, | |
0, | |
b - (c * 0.5) + m, | |
0, | |
0, | |
0, | |
1, | |
0, | |
]; | |
} | |
Future<ui.Image> editFromJson(Map<String, dynamic> editParams) async { | |
try { | |
String? filePath = editParams['fileName']; | |
if (filePath == null) { | |
throw Exception('"fileName" not found'); | |
} | |
File file = File(filePath); | |
ui.Codec codec = await ui.instantiateImageCodec(await file.readAsBytes()); | |
ui.FrameInfo frame = await codec.getNextFrame(); | |
image = frame.image; | |
imageHeight = image.height.toDouble(); | |
imageWidth = image.width.toDouble(); | |
pr = ui.PictureRecorder(); | |
canvas = Canvas(pr); | |
ImageFrame? imageFrame; | |
if (editParams.containsKey('exposure')) { | |
Map<String, dynamic> exposure = editParams['exposure']; | |
bool auto = exposure['auto'] ?? false; | |
double brightness = (exposure['brightness'] ?? 0).toDouble(); | |
double contrast = (exposure['contrast'] ?? 0).toDouble(); | |
double midTone = (exposure['midtone'] ?? 0).toDouble(); | |
colorFilter = _getColorFilter(brightness: brightness, contrast: contrast, midTone: midTone); | |
// colorFilter = _calculateColorFilter(brightness: 0,contrast: 0,midTone: 10); | |
drawImage(image, colorFilter); | |
await updateImage("filter"); | |
} | |
if (editParams.containsKey('eraseEdges')) { | |
Map<String, dynamic> frame = editParams['eraseEdges']; | |
String? colorCode = frame['fillColor']; | |
Color color = getColorFromString(colorCode); | |
if (frame['makeAllEdgesSame'] ?? false) { | |
imageFrame = ImageFrame( | |
Paint()..color = color, | |
FrameInsets.all((frame['topEdge'] ?? 0).toDouble()), | |
getUnitsFromString(frame['units'])); | |
} else { | |
imageFrame = ImageFrame( | |
Paint()..color = color, | |
FrameInsets.only( | |
left: (frame['leftEdge'] ?? 0).toDouble(), | |
bottom: (frame['bottomEdge'] ?? 0).toDouble(), | |
right: (frame['rightEdge'] ?? 0).toDouble(), | |
top: (frame['topEdge'] ?? 0).toDouble(), | |
), | |
getUnitsFromString(frame['units']), | |
); | |
} | |
} | |
if (editParams.containsKey('deleteSeletion')) { | |
List actions = editParams['deleteSeletion'] ?? []; | |
for (int i = 0; i < actions.length; i++) { | |
Map<String, dynamic> todo = actions[i]; | |
String? action = todo["option"]; | |
if (action == null) continue; | |
switch (action) { | |
case "crop": | |
Rect? areaToCrop = Rect.fromLTWH( | |
todo['cropImageX'].toDouble(), | |
todo['cropImageY'].toDouble(), | |
todo['cropImageWidth'].toDouble(), | |
todo['cropImageHeight'].toDouble()); | |
await crop(areaToCrop); | |
break; | |
case "delete": | |
Rect? areasToDelete = Rect.fromLTWH(todo['deleteImageX'], todo['deleteImageY'], | |
todo['deleteImageWidth'], todo['deleteImageHeight']); | |
DeleteArea deleteArea = DeleteArea( | |
areasToDelete, Paint()..color = getColorFromString(todo['deleteFillColor'])); | |
await deleteSelection([deleteArea]); | |
break; | |
case "rotate": | |
int? angle = todo['deleteRotationAngel']; | |
if (angle == null) continue; | |
if (angle < 90 || angle > 270) continue; | |
await rotate(getImageRotationFromAngle(angle)); | |
break; | |
case "straighten": | |
int? angle = todo['deleteRotationAngel']; | |
if (angle == null) continue; | |
await straighten(angle.toDouble()); | |
break; | |
} | |
} | |
await drawFrame(imageFrame); | |
} | |
return pr.endRecording().toImageSync(imageWidth.toInt(), imageHeight.toInt()); | |
// switch (currentOrientation) { | |
// case Orientation.vertical: | |
// return pr.endRecording().toImageSync(imageWidth.toInt(), imageHeight.toInt()); | |
// case Orientation.horizontal: | |
// return pr.endRecording().toImageSync(imageHeight.toInt(), imageWidth.toInt()); | |
// } | |
} catch (e) { | |
rethrow; | |
} | |
} | |
// Helper functions | |
Units getUnitsFromString(String? unit) { | |
if (unit == null) return Units.cm; | |
try { | |
if (unit == 'Cm') { | |
return Units.cm; | |
} | |
return Units.inch; | |
} catch (e) { | |
debugPrint("Error occurred while converting Frame Units, so setting to Cm"); | |
return Units.cm; | |
} | |
} | |
ImageRotation getImageRotationFromAngle(int angle) { | |
if (angle >= 90 && angle < 180) { | |
return ImageRotation.rotate90; | |
} else if (angle >= 180 && angle < 270) { | |
return ImageRotation.rotate180; | |
} else { | |
return ImageRotation.rotate270; | |
} | |
} | |
Color getColorFromString(String? color) { | |
try { | |
if (color == null) return Colors.white; | |
List<String> rgba = color.split(','); | |
if (rgba.length != 4) return Colors.white; | |
return Color.fromRGBO( | |
int.parse(rgba[0]), int.parse(rgba[1]), int.parse(rgba[2]), double.parse(rgba[3])); | |
} catch (e) { | |
debugPrint("Error occurred while converting color code, so setting to White"); | |
return Colors.white; | |
} | |
} | |
void swapOrientation(Orientation orientation) { | |
if (currentOrientation == orientation) { | |
return; | |
} | |
currentOrientation = orientation; | |
double temp = imageWidth; | |
imageWidth = imageHeight; | |
imageHeight = temp; | |
} | |
double _unitToPixel(Units unit, double value) { | |
switch (unit) { | |
case Units.cm: | |
return cmToPixel(cm: value); | |
case Units.inch: | |
return inchToPixel(inch: value); | |
} | |
} | |
double cmToPixel({required double cm, int dpi = 300}) => (dpi / 2.54) * cm; | |
double inchToPixel({required double inch, int dpi = 300}) => inch * dpi; | |
Offset findActualOffset(Offset offset, Size widgetSize, Size imageSize) { | |
return Offset(((offset.dx / widgetSize.width) * 100) * imageSize.width, | |
((offset.dy / widgetSize.height) * 100) * imageSize.height); | |
} | |
/// Canvas core functions | |
void drawImage(ui.Image image, ColorFilter? colorFilter) { | |
canvas.drawImage(image, Offset.zero, Paint()..colorFilter = colorFilter); | |
} | |
Future<void> deleteSelection(List<DeleteArea> areasToDelete) async { | |
for (DeleteArea element in areasToDelete) { | |
canvas.drawRect( | |
element.rect, | |
element.paint, | |
); | |
} | |
await updateImage("delete"); | |
} | |
Future<void> drawFrame(ImageFrame? imageFrame) async { | |
await saveAndClearCanvas(); | |
canvas.drawColor(Colors.white, BlendMode.color); | |
drawImage(image, null); | |
await updateImage("frame"); | |
if (imageFrame != null) { | |
double leftPixels = _unitToPixel(imageFrame.unit, imageFrame.frame.left); | |
double topPixels = _unitToPixel(imageFrame.unit, imageFrame.frame.top); | |
double rightPixels = _unitToPixel(imageFrame.unit, imageFrame.frame.right); | |
double bottomPixels = _unitToPixel(imageFrame.unit, imageFrame.frame.bottom); | |
Rect leftRect = Rect.fromLTWH(0.0, 0.0, leftPixels, imageHeight); | |
Rect topRect = Rect.fromLTWH(0.0, 0.0, imageWidth, topPixels); | |
Rect rightRect = Rect.fromLTWH(imageWidth - rightPixels, 0.0, rightPixels, imageHeight); | |
Rect bottomRect = Rect.fromLTWH(0.0, imageHeight - bottomPixels, imageWidth, bottomPixels); | |
canvas.drawRect(leftRect, imageFrame.paint); | |
canvas.drawRect(topRect, imageFrame.paint); | |
canvas.drawRect(rightRect, imageFrame.paint); | |
canvas.drawRect(bottomRect, imageFrame.paint); | |
} | |
} | |
Future<void> crop(Rect? areaToCrop) async { | |
if (areaToCrop != null) { | |
// await saveAndClearCanvas(); | |
imageHeight = areaToCrop.height; | |
imageWidth = areaToCrop.width; | |
canvas.drawImageRect( | |
image, areaToCrop, Rect.fromLTWH(0, 0, imageWidth, imageHeight), Paint()); | |
// await updateImage("crop"); | |
} | |
} | |
Future<void> rotate(ImageRotation? imageRotation) async { | |
if (imageRotation != null) { | |
int angle; | |
switch (imageRotation) { | |
case ImageRotation.rotate90: | |
angle = 90; | |
break; | |
case ImageRotation.rotate180: | |
angle = 180; | |
break; | |
case ImageRotation.rotate270: | |
angle = 270; | |
break; | |
} | |
var radians = angle * pi / 180; | |
final double r = sqrt(imageWidth * imageWidth + imageHeight * imageHeight) / 2; | |
double alpha; | |
double beta; | |
double shiftY; | |
double shiftX; | |
double translateX; | |
double translateY; | |
alpha = atan(imageHeight / imageWidth); | |
beta = alpha + radians; | |
shiftY = r * sin(beta); | |
shiftX = r * cos(beta); | |
if (angle == 90 || angle == 270) { | |
translateX = imageHeight / 2 - shiftX; | |
translateY = imageWidth / 2 - shiftY; | |
swapOrientation(Orientation.horizontal); | |
} else { | |
translateX = imageWidth / 2 - shiftX; | |
translateY = imageHeight / 2 - shiftY; | |
swapOrientation(Orientation.vertical); | |
} | |
// await saveAndClearCanvas(); | |
canvas.translate(translateX, translateY); | |
canvas.rotate(radians); | |
drawImage(image, null); | |
await updateImage("rotate"); | |
} | |
} | |
Future<void> straighten(double? angle) async { | |
if (angle == null) { | |
return; | |
} | |
if (angle < -45 || angle > 45) { | |
throw Exception("Straighten angle can only range from -45 to +45"); | |
} | |
var radians = angle * pi / 180; | |
final double r = sqrt(imageWidth * imageWidth + imageHeight * imageHeight) / 2; | |
double alpha; | |
double beta; | |
double shiftY; | |
double shiftX; | |
double translateX; | |
double translateY; | |
alpha = atan(imageHeight / imageWidth); | |
beta = alpha + radians; | |
shiftY = r * sin(beta); | |
shiftX = r * cos(beta); | |
translateX = imageWidth / 2 - shiftX; | |
translateY = imageHeight / 2 - shiftY; | |
// await saveAndClearCanvas(); | |
canvas.translate(translateX, translateY); | |
canvas.rotate(radians); | |
drawImage(image, null); | |
await updateImage("straighten"); | |
} | |
Future<void> saveAndClearCanvas() async { | |
image = pr.endRecording().toImageSync(imageWidth.toInt(), imageHeight.toInt()); | |
pr = ui.PictureRecorder(); | |
canvas = Canvas(pr); | |
} | |
Future<void> updateImage(String action) async { | |
image = pr.endRecording().toImageSync(imageWidth.toInt(), imageHeight.toInt()); | |
pr = ui.PictureRecorder(); | |
canvas = Canvas(pr); | |
drawImage(image, null); | |
} | |
ColorFilter _getColorFilter({double brightness = 0, double contrast = 0, double midTone = 0}) { | |
return ColorFilter.matrix(_multiplyMatrices([ | |
// _getBrightnessFilter(brightness), | |
// _getContrastFilter(contrast), | |
// _getMidToneFilter(midTone), | |
])); | |
} | |
List<double> _getBrightnessFilter(num value) { | |
double brightnessScale = value.abs() / 100.0; | |
brightnessScale = value >= 0 ? brightnessScale + 1.0 : 1.0 - brightnessScale; | |
List<double> colorMatrix = <double>[ | |
brightnessScale, | |
0, | |
0, | |
0, | |
0, | |
0, | |
brightnessScale, | |
0, | |
0, | |
0, | |
0, | |
0, | |
brightnessScale, | |
0, | |
0, | |
0, | |
0, | |
0, | |
1, | |
0, | |
]; | |
return colorMatrix; | |
} | |
List<double> _getContrastFilter(num value) { | |
double contrast = (value + 100) / 100.0; | |
double factor = contrast * contrast; | |
double offset = 0.5 * (1 - factor); | |
return [ | |
factor, | |
0, | |
0, | |
0, | |
offset, | |
0, | |
factor, | |
0, | |
0, | |
offset, | |
0, | |
0, | |
factor, | |
0, | |
offset, | |
0, | |
0, | |
0, | |
1, | |
0, | |
]; | |
} | |
/* /// Brightness adjustment | |
List<double> _getBrightnessFilter(double value) { | |
if (value <= 0) { | |
value = value * 255; | |
} else { | |
value = value * 100; | |
} | |
if (value == 0) { | |
return [ | |
1, | |
0, | |
0, | |
0, | |
0, | |
0, | |
1, | |
0, | |
0, | |
0, | |
0, | |
0, | |
1, | |
0, | |
0, | |
0, | |
0, | |
0, | |
1, | |
0, | |
]; | |
} | |
return List<double>.from( | |
<double>[1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0]) | |
.map((i) => i.toDouble()) | |
.toList(); | |
} | |
/// Contrast adjustment | |
List<double> _getContrastFilter(double value) { | |
value = value/100; | |
// RGBA contrast(RGBA color, num adj) { | |
// adj *= 255; | |
// double factor = (259 * (adj + 255)) / (255 * (259 - adj)); | |
// return new RGBA( | |
// red: (factor * (color.red - 128) + 128), | |
// green: (factor * (color.green - 128) + 128), | |
// blue: (factor * (color.blue - 128) + 128), | |
// alpha: color.alpha, | |
// ); | |
// } | |
double adj = value * 255; | |
double factor = (259 * (adj + 255)) / (255 * (259 - adj)); | |
return [ | |
factor, | |
0, | |
0, | |
0, | |
128 * (1 - factor), | |
0, | |
factor, | |
0, | |
0, | |
128 * (1 - factor), | |
0, | |
0, | |
factor, | |
0, | |
128 * (1 - factor), | |
0, | |
0, | |
0, | |
1, | |
0, | |
]; | |
}*/ | |
List<double> _getMidToneFilter(num midTone) { | |
double value = 0; | |
if (midTone >= 0 && midTone <= 99) { | |
value = value - (20 - (1.9 * midTone)); | |
value = value < 1 ? 1 : value; | |
} else if (midTone >= 100 && midTone <= 200) { | |
value = value - (20 - (1.9 * midTone)); | |
value = value > 60 ? 60 : value; | |
} | |
return _getBrightnessFilter(value); | |
} | |
List<double> _multiplyMatrices(List<List<double>> matrices) { | |
List<List<double>> result = List.generate(4, (_) => List.filled(5, 0.0)); | |
for (int row = 0; row < 4; row++) { | |
for (int col = 0; col < 5; col++) { | |
double sum = 0.0; | |
for (int k = 0; k < matrices.length; k++) { | |
sum += matrices[k][row * 5 + col]; | |
} | |
result[row][col] = sum; | |
} | |
} | |
return result.expand((row) => row).toList(); | |
} | |
} | |
// Helper Classes and enums | |
enum Sequence { rotate, crop, delete, straighten, drawImage, drawFrame } | |
enum Units { cm, inch } | |
enum ImageRotation { | |
rotate90, | |
rotate180, | |
rotate270, | |
} | |
enum Orientation { vertical, horizontal } | |
class DeleteArea { | |
Rect rect; | |
Paint paint; | |
DeleteArea(this.rect, this.paint); | |
} | |
class FrameInsets { | |
double left = 0.0; | |
double top = 0.0; | |
double right = 0.0; | |
double bottom = 0.0; | |
FrameInsets.all(double value) | |
: left = value, | |
top = value, | |
right = value, | |
bottom = value; | |
FrameInsets.only({ | |
this.left = 0.0, | |
this.top = 0.0, | |
this.right = 0.0, | |
this.bottom = 0.0, | |
}); | |
} | |
class ImageFrame { | |
Paint paint; | |
FrameInsets frame; | |
Units unit; | |
ImageFrame(this.paint, this.frame, this.unit); | |
} | |
// Usage | |
// Json Param Format | |
/* | |
Map<String, dynamic> params = { | |
"fileName":"C:\\Users\\Lokesh\\Downloads\\1678442336877edit.jpeg", | |
"exposure": {"auto": false, "brightness": 0, "contrast": 42, "midtone": 0}, | |
"eraseEdges": { | |
"erase": false, | |
"makeAllEdgesSame": true, | |
"topEdge": 1, | |
"bottomEdge": 0, | |
"leftEdge": 0, | |
"rightEdge": 0, | |
"fillColor": "255, 99, 71, 0.5", | |
"units": "Inch" | |
}, | |
"deleteSeletion": [ | |
{ | |
"cropSelectionEnabled": true, | |
"cropFillColor": 1, | |
"cropImageX": 1.6948497479951952e-13, | |
"cropImageY": 443.00000000000017, | |
"cropImageWidth": 2518.999999999999, | |
"cropImageHeight": 2528, | |
"cropRotationAngel": 0, | |
"option": "crop" | |
}, | |
{ | |
"deleteSelectionEnabled": true, | |
"deleteFillColor": "255,255,255,1", | |
"deleteImageX": 1014.6584938704029, | |
"deleteImageY": 1431.1891418563926, | |
"deleteImageWidth": 524.9754816112085, | |
"deleteImageHeight": 511.74080560420316, | |
"deleteRotationAngel": 0, | |
"option": "delete" | |
}, | |
{ | |
"deleteSelectionEnabled": true, | |
"deleteFillColor": "255,255,255,1", | |
"deleteImageX": 0, | |
"deleteImageY": 0, | |
"deleteImageWidth": 0, | |
"deleteImageHeight": 0, | |
"deleteRotationAngel": 90, | |
"option": "rotate" | |
}, | |
{ | |
"deleteSelectionEnabled": true, | |
"deleteFillColor": "255,255,255,1", | |
"deleteImageX": 569.091068301226, | |
"deleteImageY": 1619.0420315236427, | |
"deleteImageWidth": 202.9316987740806, | |
"deleteImageHeight": 710.2609457092819, | |
"deleteRotationAngel": 0, | |
"option": "delete" | |
}, | |
{ | |
"deleteSelectionEnabled": true, | |
"deleteFillColor": "255,255,255,1", | |
"deleteImageX": 0, | |
"deleteImageY": 0, | |
"deleteImageWidth": 0, | |
"deleteImageHeight": 0, | |
"deleteRotationAngel": -45, | |
"option": "straighten" | |
}, | |
] | |
}; | |
*/ | |
/* | |
ImageEditor ie = ImageEditor(); | |
ui.Image editedImage = await ie.editFromJson(params); | |
ByteData? byteData = await editedImage.toByteData(format: ui.ImageByteFormat.png); | |
writeToFile(byteData, 'C:\\Users\\Lokesh\\Documents\\edited.jpeg'); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment