feat: Add document scanner package

This commit is contained in:
Anton Stubenbord
2023-02-22 18:17:50 +01:00
parent a8a41b38a8
commit 9c5a45f329
105 changed files with 3998 additions and 0 deletions

View File

@@ -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,
)
],
);
}
}

View File

@@ -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),
),
),
],
),
);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}