mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 20:07:55 -06:00
feat: Add document scanner package
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CameraView extends StatelessWidget {
|
||||
const CameraView({super.key, required this.controller});
|
||||
|
||||
final CameraController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!controller.value.isInitialized) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: CameraPreview(controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_document_scanner/types/edge_detection_result.dart';
|
||||
|
||||
import 'edge_detection_shape/edge_detection_shape.dart';
|
||||
|
||||
class ImagePreview extends StatefulWidget {
|
||||
const ImagePreview({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.edgeDetectionResult,
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
final EdgeDetectionResult? edgeDetectionResult;
|
||||
|
||||
@override
|
||||
State<ImagePreview> createState() => _ImagePreviewState();
|
||||
}
|
||||
|
||||
class _ImagePreviewState extends State<ImagePreview> {
|
||||
GlobalKey imageWidgetKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext mainContext) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
const Center(child: Text('Loading ...')),
|
||||
Image.file(File(widget.imagePath),
|
||||
fit: BoxFit.contain, key: imageWidgetKey),
|
||||
FutureBuilder<ui.Image>(
|
||||
future: loadUiImage(widget.imagePath),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: Text("Loading..."),
|
||||
);
|
||||
} else {
|
||||
return _getEdgePaint(snapshot.data!, context);
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getEdgePaint(
|
||||
ui.Image image,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (widget.edgeDetectionResult == null) return Container();
|
||||
|
||||
final keyContext = imageWidgetKey.currentContext;
|
||||
|
||||
if (keyContext == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final box = keyContext.findRenderObject() as RenderBox;
|
||||
|
||||
return EdgeDetectionShape(
|
||||
originalImageSize: Size(
|
||||
image.width.toDouble(),
|
||||
image.height.toDouble(),
|
||||
),
|
||||
renderedImageSize: Size(box.size.width, box.size.height),
|
||||
edgeDetectionResult: widget.edgeDetectionResult!,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Image> loadUiImage(String imageAssetPath) async {
|
||||
final Uint8List data = await File(imageAssetPath).readAsBytes();
|
||||
final Completer<ui.Image> completer = Completer();
|
||||
ui.decodeImageFromList(Uint8List.view(data.buffer), (ui.Image image) {
|
||||
return completer.complete(image);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedTouchBubblePart extends StatefulWidget {
|
||||
AnimatedTouchBubblePart({
|
||||
required this.dragging,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final bool dragging;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
_AnimatedTouchBubblePartState createState() =>
|
||||
_AnimatedTouchBubblePartState();
|
||||
}
|
||||
|
||||
class _AnimatedTouchBubblePartState extends State<AnimatedTouchBubblePart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Color?> _colorAnimation;
|
||||
late Animation<double> _sizeAnimation;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_sizeAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(_controller);
|
||||
|
||||
_colorAnimation = ColorTween(
|
||||
begin: Theme.of(context).colorScheme.primary,
|
||||
end: Theme.of(context).colorScheme.primary,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.5, 1.0),
|
||||
),
|
||||
);
|
||||
|
||||
_controller.repeat();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: widget.dragging ? 0 : widget.size / 2,
|
||||
height: widget.dragging ? 0 : widget.size / 2,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentColor.withOpacity(0.5),
|
||||
borderRadius: widget.dragging
|
||||
? BorderRadius.circular(widget.size)
|
||||
: BorderRadius.circular(widget.size / 4)))),
|
||||
AnimatedBuilder(
|
||||
builder: (context, child) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: widget.dragging
|
||||
? 0
|
||||
: widget.size * _sizeAnimation.value,
|
||||
height: widget.dragging
|
||||
? 0
|
||||
: widget.size * _sizeAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _colorAnimation.value ?? Colors.transparent,
|
||||
width: widget.size / 20),
|
||||
borderRadius: widget.dragging
|
||||
? BorderRadius.zero
|
||||
: BorderRadius.circular(
|
||||
widget.size * _sizeAnimation.value / 2))));
|
||||
},
|
||||
animation: _controller,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_document_scanner/paperless_document_scanner.dart';
|
||||
import 'package:paperless_document_scanner/types/edge_detection_result.dart';
|
||||
|
||||
import 'edge_painter.dart';
|
||||
import 'magnifier.dart' as m;
|
||||
import 'touch_bubble.dart';
|
||||
|
||||
class EdgeDetectionShape extends StatefulWidget {
|
||||
const EdgeDetectionShape({
|
||||
super.key,
|
||||
required this.renderedImageSize,
|
||||
required this.originalImageSize,
|
||||
required this.edgeDetectionResult,
|
||||
});
|
||||
|
||||
final Size renderedImageSize;
|
||||
final Size originalImageSize;
|
||||
final EdgeDetectionResult edgeDetectionResult;
|
||||
|
||||
@override
|
||||
State<EdgeDetectionShape> createState() => _EdgeDetectionShapeState();
|
||||
}
|
||||
|
||||
class _EdgeDetectionShapeState extends State<EdgeDetectionShape> {
|
||||
late double edgeDraggerSize;
|
||||
|
||||
List<Offset> points = [];
|
||||
|
||||
late Offset _topLeft;
|
||||
late Offset _topRight;
|
||||
late Offset _bottomLeft;
|
||||
late Offset _bottomRight;
|
||||
|
||||
late double renderedImageWidth;
|
||||
late double renderedImageHeight;
|
||||
late double top;
|
||||
late double left;
|
||||
|
||||
Offset? currentDragPosition;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
double shortestSide = min(
|
||||
MediaQuery.of(context).size.width, MediaQuery.of(context).size.height);
|
||||
edgeDraggerSize = shortestSide / 12;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
top = 0.0;
|
||||
left = 0.0;
|
||||
_topLeft = widget.edgeDetectionResult.topLeft;
|
||||
_topRight = widget.edgeDetectionResult.topRight;
|
||||
_bottomLeft = widget.edgeDetectionResult.bottomLeft;
|
||||
_bottomRight = widget.edgeDetectionResult.bottomRight;
|
||||
|
||||
double widthFactor =
|
||||
widget.renderedImageSize.width / widget.originalImageSize.width;
|
||||
double heightFactor =
|
||||
widget.renderedImageSize.height / widget.originalImageSize.height;
|
||||
double sizeFactor = min(widthFactor, heightFactor);
|
||||
|
||||
renderedImageHeight = widget.originalImageSize.height * sizeFactor;
|
||||
top = ((widget.renderedImageSize.height - renderedImageHeight) / 2);
|
||||
|
||||
renderedImageWidth = widget.originalImageSize.width * sizeFactor;
|
||||
left = ((widget.renderedImageSize.width - renderedImageWidth) / 2);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return m.Magnifier(
|
||||
visible: currentDragPosition != null,
|
||||
position: currentDragPosition ?? Offset.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildTouchBubbles(),
|
||||
CustomPaint(
|
||||
painter: EdgePainter(
|
||||
points: points,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Offset _getNewPositionAfterDrag(Offset position) {
|
||||
return Offset(
|
||||
position.dx / renderedImageWidth,
|
||||
position.dy / renderedImageHeight,
|
||||
);
|
||||
}
|
||||
|
||||
Offset _clampOffset(Offset givenOffset) {
|
||||
double absoluteX = givenOffset.dx * renderedImageWidth;
|
||||
double absoluteY = givenOffset.dy * renderedImageHeight;
|
||||
|
||||
return Offset(absoluteX.clamp(0.0, renderedImageWidth) / renderedImageWidth,
|
||||
absoluteY.clamp(0.0, renderedImageHeight) / renderedImageHeight);
|
||||
}
|
||||
|
||||
Widget _buildTouchBubbles() {
|
||||
points = [
|
||||
Offset(
|
||||
left + _topLeft.dx * renderedImageWidth,
|
||||
top + _topLeft.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _topRight.dx * renderedImageWidth,
|
||||
top + _topRight.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _bottomRight.dx * renderedImageWidth,
|
||||
top + _bottomRight.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _bottomLeft.dx * renderedImageWidth,
|
||||
top + _bottomLeft.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _topLeft.dx * renderedImageWidth,
|
||||
top + _topLeft.dy * renderedImageHeight,
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
width: widget.renderedImageSize.width,
|
||||
height: widget.renderedImageSize.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: points[0].dx - (edgeDraggerSize / 2),
|
||||
top: points[0].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
onDrag: (position) {
|
||||
setState(
|
||||
() {
|
||||
currentDragPosition = Offset(points[0].dx, points[0].dy);
|
||||
_topLeft = _clampOffset(
|
||||
widget.edgeDetectionResult.topLeft +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[1].dx - (edgeDraggerSize / 2),
|
||||
top: points[1].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
currentDragPosition = Offset(points[1].dx, points[1].dy);
|
||||
_topRight = _clampOffset(
|
||||
widget.edgeDetectionResult.topRight +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[2].dx - (edgeDraggerSize / 2),
|
||||
top: points[2].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
currentDragPosition = Offset(points[2].dx, points[2].dy);
|
||||
_bottomRight = _clampOffset(
|
||||
widget.edgeDetectionResult.bottomRight +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[3].dx - (edgeDraggerSize / 2),
|
||||
top: points[3].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
_bottomLeft = _clampOffset(
|
||||
widget.edgeDetectionResult.bottomLeft +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
currentDragPosition = Offset(points[3].dx, points[3].dy);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EdgePainter extends CustomPainter {
|
||||
EdgePainter({required this.points, required this.color});
|
||||
|
||||
final List<Offset> points;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color.withOpacity(0.5)
|
||||
..strokeWidth = 2
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawPoints(PointMode.polygon, points, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'magnifier_painter.dart';
|
||||
|
||||
class Magnifier extends StatefulWidget {
|
||||
const Magnifier({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.position,
|
||||
this.visible = true,
|
||||
this.scale = 1.5,
|
||||
this.size = const Size(160, 160),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Offset position;
|
||||
final bool visible;
|
||||
final double scale;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
_MagnifierState createState() => _MagnifierState();
|
||||
}
|
||||
|
||||
class _MagnifierState extends State<Magnifier> {
|
||||
late Size _magnifierSize;
|
||||
late double _scale;
|
||||
late Matrix4 _matrix;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_magnifierSize = widget.size;
|
||||
_scale = widget.scale;
|
||||
_calculateMatrix();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Magnifier oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
_calculateMatrix();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
if (widget.visible && widget.position != null) _getMagnifier(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _calculateMatrix() {
|
||||
if (widget.position == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
double newX = widget.position.dx - (_magnifierSize.width / 2 / _scale);
|
||||
double newY = widget.position.dy - (_magnifierSize.height / 2 / _scale);
|
||||
|
||||
final Matrix4 updatedMatrix = Matrix4.identity()
|
||||
..scale(_scale, _scale)
|
||||
..translate(-newX, -newY);
|
||||
|
||||
_matrix = updatedMatrix;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getMagnifier(BuildContext context) {
|
||||
return Align(
|
||||
alignment: _getAlignment(),
|
||||
child: ClipOval(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.matrix(_matrix.storage),
|
||||
child: CustomPaint(
|
||||
painter: MagnifierPainter(color: Theme.of(context).accentColor),
|
||||
size: _magnifierSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Alignment _getAlignment() {
|
||||
if (_bubbleCrossesMagnifier()) {
|
||||
return Alignment.topRight;
|
||||
}
|
||||
|
||||
return Alignment.topLeft;
|
||||
}
|
||||
|
||||
bool _bubbleCrossesMagnifier() =>
|
||||
widget.position.dx < widget.size.width &&
|
||||
widget.position.dy < widget.size.height;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MagnifierPainter extends CustomPainter {
|
||||
const MagnifierPainter({required this.color, this.strokeWidth = 5});
|
||||
|
||||
final double strokeWidth;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawCircle(canvas, size);
|
||||
_drawCrosshair(canvas, size);
|
||||
}
|
||||
|
||||
void _drawCircle(Canvas canvas, Size size) {
|
||||
Paint paintObject = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..color = color;
|
||||
|
||||
canvas.drawCircle(
|
||||
size.center(Offset(0, 0)), size.longestSide / 2, paintObject);
|
||||
}
|
||||
|
||||
void _drawCrosshair(Canvas canvas, Size size) {
|
||||
Paint crossPaint = Paint()
|
||||
..strokeWidth = strokeWidth / 2
|
||||
..color = color;
|
||||
|
||||
double crossSize = size.longestSide * 0.04;
|
||||
|
||||
canvas.drawLine(size.center(Offset(-crossSize, -crossSize)),
|
||||
size.center(Offset(crossSize, crossSize)), crossPaint);
|
||||
|
||||
canvas.drawLine(size.center(Offset(crossSize, -crossSize)),
|
||||
size.center(Offset(-crossSize, crossSize)), crossPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'animated_touch_bubble_part.dart';
|
||||
|
||||
class TouchBubble extends StatefulWidget {
|
||||
const TouchBubble({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.onDrag,
|
||||
required this.onDragFinished,
|
||||
});
|
||||
|
||||
final double size;
|
||||
final Function onDrag;
|
||||
final Function onDragFinished;
|
||||
|
||||
@override
|
||||
State<TouchBubble> createState() => _TouchBubbleState();
|
||||
}
|
||||
|
||||
class _TouchBubbleState extends State<TouchBubble> {
|
||||
bool dragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanStart: _startDragging,
|
||||
onPanUpdate: _drag,
|
||||
onPanCancel: _cancelDragging,
|
||||
onPanEnd: (_) => _cancelDragging(),
|
||||
child: Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(widget.size / 2)),
|
||||
child: AnimatedTouchBubblePart(
|
||||
dragging: dragging,
|
||||
size: widget.size,
|
||||
)));
|
||||
}
|
||||
|
||||
void _startDragging(DragStartDetails data) {
|
||||
setState(() {
|
||||
dragging = true;
|
||||
});
|
||||
widget
|
||||
.onDrag(data.localPosition - Offset(widget.size / 2, widget.size / 2));
|
||||
}
|
||||
|
||||
void _cancelDragging() {
|
||||
setState(() {
|
||||
dragging = false;
|
||||
});
|
||||
widget.onDragFinished();
|
||||
}
|
||||
|
||||
void _drag(DragUpdateDetails data) {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onDrag(data.delta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:paperless_document_scanner/paperless_document_scanner.dart';
|
||||
import 'package:paperless_document_scanner/types/edge_detection_result.dart';
|
||||
|
||||
class EdgeDetector {
|
||||
static Future<void> startEdgeDetectionFromFileIsolate(
|
||||
EdgeDetectionFromFileInput edgeDetectionInput,
|
||||
) async {
|
||||
EdgeDetectionResult result =
|
||||
await EdgeDetection.detectEdgesFromFile(edgeDetectionInput.inputPath);
|
||||
edgeDetectionInput.sendPort.send(result);
|
||||
}
|
||||
|
||||
static Future<void> startEdgeDetectionIsolate(
|
||||
EdgeDetectionInput edgeDetectionInput,
|
||||
) async {
|
||||
EdgeDetectionResult result =
|
||||
await EdgeDetection.detectEdges(edgeDetectionInput.bytes);
|
||||
edgeDetectionInput.sendPort.send(result);
|
||||
}
|
||||
|
||||
static Future<void> processImageIsolate(
|
||||
ProcessImageInput processImageInput) async {
|
||||
ImageProcessing.processImage(
|
||||
processImageInput.bytes,
|
||||
processImageInput.edgeDetectionResult,
|
||||
);
|
||||
processImageInput.sendPort.send(true);
|
||||
}
|
||||
|
||||
Future<EdgeDetectionResult> detectEdgesFromFile(String filePath) async {
|
||||
final port = ReceivePort();
|
||||
|
||||
_spawnIsolate<EdgeDetectionInput>(
|
||||
startEdgeDetectionIsolate,
|
||||
EdgeDetectionFromFileInput(
|
||||
inputPath: filePath,
|
||||
sendPort: port.sendPort,
|
||||
),
|
||||
port,
|
||||
);
|
||||
|
||||
return await _subscribeToPort<EdgeDetectionResult>(port);
|
||||
}
|
||||
|
||||
Future<bool> processImageFromFile(
|
||||
String filePath, EdgeDetectionResult edgeDetectionResult) async {
|
||||
final port = ReceivePort();
|
||||
|
||||
_spawnIsolate<ProcessImageInput>(
|
||||
processImageIsolate,
|
||||
ProcessImageFromFileInput(
|
||||
inputPath: filePath,
|
||||
edgeDetectionResult: edgeDetectionResult,
|
||||
sendPort: port.sendPort),
|
||||
port);
|
||||
|
||||
return await _subscribeToPort<bool>(port);
|
||||
}
|
||||
|
||||
void _spawnIsolate<T>(
|
||||
void Function(T) function,
|
||||
dynamic input,
|
||||
ReceivePort port,
|
||||
) {
|
||||
Isolate.spawn<T>(function, input,
|
||||
onError: port.sendPort, onExit: port.sendPort);
|
||||
}
|
||||
|
||||
Future<T> _subscribeToPort<T>(ReceivePort port) async {
|
||||
late StreamSubscription sub;
|
||||
|
||||
var completer = Completer<T>();
|
||||
|
||||
sub = port.listen((result) async {
|
||||
print(result);
|
||||
await sub.cancel();
|
||||
completer.complete(await result);
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
class EdgeDetectionFromFileInput {
|
||||
EdgeDetectionFromFileInput({
|
||||
required this.inputPath,
|
||||
required this.sendPort,
|
||||
});
|
||||
|
||||
final String inputPath;
|
||||
final SendPort sendPort;
|
||||
}
|
||||
|
||||
class EdgeDetectionInput {
|
||||
EdgeDetectionInput({
|
||||
required this.bytes,
|
||||
required this.sendPort,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final SendPort sendPort;
|
||||
}
|
||||
|
||||
class ProcessImageInput {
|
||||
ProcessImageInput({
|
||||
required this.bytes,
|
||||
required this.edgeDetectionResult,
|
||||
required this.sendPort,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final EdgeDetectionResult edgeDetectionResult;
|
||||
final SendPort sendPort;
|
||||
}
|
||||
|
||||
class ProcessImageFromFileInput {
|
||||
ProcessImageFromFileInput({
|
||||
required this.inputPath,
|
||||
required this.edgeDetectionResult,
|
||||
required this.sendPort,
|
||||
});
|
||||
|
||||
final String inputPath;
|
||||
final EdgeDetectionResult edgeDetectionResult;
|
||||
final SendPort sendPort;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageView extends StatefulWidget {
|
||||
const ImageView({super.key, required this.imagePath});
|
||||
|
||||
final String imagePath;
|
||||
|
||||
@override
|
||||
State<ImageView> createState() => _ImageViewState();
|
||||
}
|
||||
|
||||
class _ImageViewState extends State<ImageView> {
|
||||
GlobalKey imageWidgetKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext mainContext) {
|
||||
return Center(
|
||||
child: Image.file(
|
||||
File(widget.imagePath),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
packages/paperless_document_scanner/example/lib/main.dart
Normal file
113
packages/paperless_document_scanner/example/lib/main.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:image/image.dart' as imglib;
|
||||
import 'scan.dart';
|
||||
|
||||
late final List<CameraDescription> cameras;
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
cameras = await availableCameras();
|
||||
runApp(const EdgeDetectionApp());
|
||||
}
|
||||
|
||||
class EdgeDetectionApp extends StatefulWidget {
|
||||
const EdgeDetectionApp({super.key});
|
||||
|
||||
@override
|
||||
State<EdgeDetectionApp> createState() => _EdgeDetectionAppState();
|
||||
}
|
||||
|
||||
class _EdgeDetectionAppState extends State<EdgeDetectionApp> {
|
||||
CameraImage? _image;
|
||||
late final CameraController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
() async {
|
||||
_controller = CameraController(
|
||||
cameras
|
||||
.where(
|
||||
(element) => element.lensDirection == CameraLensDirection.back)
|
||||
.first,
|
||||
ResolutionPreset.low,
|
||||
enableAudio: false,
|
||||
);
|
||||
await _controller.initialize();
|
||||
_controller.startImageStream((image) {
|
||||
setState(() => _image = image);
|
||||
});
|
||||
}();
|
||||
}
|
||||
|
||||
Uint8List concatenatePlanes(List<Plane> planes) {
|
||||
final WriteBuffer allBytes = WriteBuffer();
|
||||
for (final plane in planes) {
|
||||
allBytes.putUint8List(plane.bytes);
|
||||
}
|
||||
return allBytes.done().buffer.asUint8List();
|
||||
}
|
||||
|
||||
Image convertYUV420toImageColor(CameraImage image) {
|
||||
final int width = image.width;
|
||||
final int height = image.height;
|
||||
final int uvRowStride = image.planes[1].bytesPerRow;
|
||||
final int uvPixelStride = image.planes[1].bytesPerPixel!;
|
||||
|
||||
print("uvRowStride: " + uvRowStride.toString());
|
||||
print("uvPixelStride: " + uvPixelStride.toString());
|
||||
|
||||
// imgLib -> Image package from https://pub.dartlang.org/packages/image
|
||||
var img = imglib.Image(
|
||||
width: width,
|
||||
height: height,
|
||||
); // Create Image buffer
|
||||
|
||||
// Fill image buffer with plane[0] from YUV420_888
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
final int uvIndex =
|
||||
uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor();
|
||||
final int index = y * width + x;
|
||||
|
||||
final yp = image.planes[0].bytes[index];
|
||||
final up = image.planes[1].bytes[uvIndex];
|
||||
final vp = image.planes[2].bytes[uvIndex];
|
||||
// Calculate pixel color
|
||||
int r = (yp + vp * 1436 / 1024 - 179).round().clamp(0, 255);
|
||||
int g = (yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91)
|
||||
.round()
|
||||
.clamp(0, 255);
|
||||
int b = (yp + up * 1814 / 1024 - 227).round().clamp(0, 255);
|
||||
// color: 0x FF FF FF FF
|
||||
// A B G R
|
||||
img.data?.setPixelRgb(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
imglib.PngEncoder pngEncoder = new imglib.PngEncoder(level: 0);
|
||||
final png = pngEncoder.encode(img);
|
||||
return Image.memory(png);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: _image != null
|
||||
? convertYUV420toImageColor(_image!)
|
||||
: Placeholder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
174
packages/paperless_document_scanner/example/lib/scan.dart
Normal file
174
packages/paperless_document_scanner/example/lib/scan.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_document_scanner/types/edge_detection_result.dart';
|
||||
|
||||
import 'camera_view.dart';
|
||||
import 'cropping_preview.dart';
|
||||
import 'edge_detector.dart';
|
||||
import 'image_view.dart';
|
||||
|
||||
class Scan extends StatefulWidget {
|
||||
const Scan({
|
||||
super.key,
|
||||
required,
|
||||
required this.cameras,
|
||||
});
|
||||
final List<CameraDescription> cameras;
|
||||
@override
|
||||
State<Scan> createState() => _ScanState();
|
||||
}
|
||||
|
||||
class _ScanState extends State<Scan> {
|
||||
late final CameraController controller;
|
||||
String? imagePath;
|
||||
String? croppedImagePath;
|
||||
EdgeDetectionResult? edgeDetectionResult;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = CameraController(
|
||||
widget.cameras[0],
|
||||
ResolutionPreset.veryHigh,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
enableAudio: false,
|
||||
);
|
||||
() async {
|
||||
await controller.initialize();
|
||||
log(controller.value.toString());
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
_getMainWidget(),
|
||||
_getBottomBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getMainWidget() {
|
||||
if (croppedImagePath != null) {
|
||||
return ImageView(imagePath: croppedImagePath!);
|
||||
}
|
||||
|
||||
if (imagePath == null && edgeDetectionResult == null) {
|
||||
return CameraView(controller: controller);
|
||||
}
|
||||
|
||||
return ImagePreview(
|
||||
imagePath: imagePath!,
|
||||
edgeDetectionResult: edgeDetectionResult,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getButtonRow() {
|
||||
if (imagePath != null) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: FloatingActionButton(
|
||||
child: Icon(Icons.check),
|
||||
onPressed: () async {
|
||||
if (croppedImagePath == null) {
|
||||
return _processImage(imagePath!, edgeDetectionResult!);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
imagePath = null;
|
||||
edgeDetectionResult = null;
|
||||
croppedImagePath = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.camera_alt),
|
||||
onPressed: onTakePictureButtonPressed,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
Future<String> takePicture() async {
|
||||
if (!controller.value.isInitialized) {
|
||||
throw Exception("Select camera first!");
|
||||
}
|
||||
|
||||
final file = await controller.takePicture();
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
Future _detectEdges(String filePath) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
imagePath = filePath;
|
||||
});
|
||||
|
||||
EdgeDetectionResult result =
|
||||
await EdgeDetector().detectEdgesFromFile(filePath);
|
||||
|
||||
setState(() {
|
||||
edgeDetectionResult = result;
|
||||
});
|
||||
}
|
||||
|
||||
Future _processImage(
|
||||
String filePath, EdgeDetectionResult edgeDetectionResult) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool result = await EdgeDetector()
|
||||
.processImageFromFile(filePath, edgeDetectionResult);
|
||||
|
||||
if (result == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
imageCache.clearLiveImages();
|
||||
imageCache.clear();
|
||||
croppedImagePath = imagePath;
|
||||
});
|
||||
}
|
||||
|
||||
void onTakePictureButtonPressed() async {
|
||||
String filePath = await takePicture();
|
||||
|
||||
log('Picture saved to $filePath');
|
||||
|
||||
await _detectEdges(filePath);
|
||||
}
|
||||
|
||||
Padding _getBottomBar() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
child: Align(alignment: Alignment.bottomCenter, child: _getButtonRow()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user