From 4400f3cc8808b5767eccbf69f3da4cdbf2fefcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Wed, 21 Feb 2024 20:36:52 +0100 Subject: [PATCH 1/8] First attempt to implement crop of images. Not finished needs debugging. --- lib/components/canvas/canvas_image.dart | 415 +++++++++++++++--- lib/components/canvas/image/editor_image.dart | 47 +- .../canvas/image/png_editor_image.dart | 11 + lib/components/canvas/shader_image.dart | 2 +- 4 files changed, 408 insertions(+), 67 deletions(-) diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index 39fc2a90f..74c2315ca 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -11,6 +11,31 @@ import 'package:saber/data/extensions/change_notifier_extensions.dart'; import 'package:saber/data/prefs.dart'; import 'package:saber/i18n/strings.g.dart'; +// type of activity with image +enum Active { + none, // is not active any image manipulation + destination, // setting destination rect + crop, // setting crop rect +} + +/// crop image by rect +class ImgClipper extends CustomClipper { + ImgClipper( + this.clipRect, + ); + final Rect clipRect; + + @override + Rect getClip(Size size) { + return clipRect.shift(-clipRect.topLeft); + } + + @override + bool shouldReclip(oldClipper) { + return false; + } +} + class CanvasImage extends StatefulWidget { CanvasImage({ required this.filePath, @@ -47,19 +72,25 @@ class CanvasImage extends StatefulWidget { } class _CanvasImageState extends State { - bool _active = false; + Active _activeType = Active.none; // activity with image - /// Whether this image can be dragged - bool get active => _active; - set active(bool value) { - if (active == value) return; + bool isActive(){ + /// return True if is active. + /// only if active type is not none + return(_activeType!=Active.none); + } + + Active get activeType => _activeType; + set activeType(Active value) { + if (activeType == value) return; // no change in activity - if (value) { + if (!isActive() && value!=Active.none) { + // activating Active - deactivate listeners CanvasImage.activeListener - .notifyListenersPlease(); // de-activate all other images + .notifyListenersPlease(); // de-activate all other images (even this image) } - - _active = value; + _activeType = value; // set active state + widget.image.showCroppedImage=_activeType!=Active.crop; // if active state is not crop, then show cropped image if (mounted) { try { @@ -70,12 +101,28 @@ class _CanvasImageState extends State { } } + Active setNextActive(){ + /// provide next active type to the current one + switch(_activeType) { + case Active.none: + return(Active.destination); + case Active.destination: + return(Active.crop); + case Active.crop: + return(Active.none); // not active + } + } + Brightness imageBrightness = Brightness.light; late ui.FragmentShader shader = InvertShader.create(); + // used when dragging or resizing image Rect panStartRect = Rect.zero; Offset panStartPosition = Offset.zero; + // used when cropping image + Rect cropRect = Rect.zero; + Rect cropStartRect = Rect.zero; @override void initState() { @@ -83,7 +130,7 @@ class _CanvasImageState extends State { if (widget.image.newImage) { // if the image is new, make it [active] - active = true; + activeType = Active.destination; // set active to destination rect widget.image.newImage = false; } @@ -94,7 +141,7 @@ class _CanvasImageState extends State { } void disableActive() { - active = false; + activeType = Active.none; } void imageListener() { @@ -103,8 +150,8 @@ class _CanvasImageState extends State { @override void didUpdateWidget(covariant CanvasImage oldWidget) { - if (widget.readOnly && active) { - active = false; + if (widget.readOnly && isActive()) { + activeType = Active.none; } if (widget.image != oldWidget.image) { oldWidget.image.removeListener(imageListener); @@ -130,20 +177,21 @@ class _CanvasImageState extends State { fit: StackFit.expand, children: [ MouseRegion( - cursor: active ? SystemMouseCursors.grab : MouseCursor.defer, + cursor: isActive() ? SystemMouseCursors.grab : MouseCursor.defer, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - active = !active; + activeType = setNextActive(); // set next type of activity with image }, - onLongPress: active ? showModal : null, - onSecondaryTap: active ? showModal : null, - onPanStart: active + onLongPress: isActive() ? showModal : null, + onSecondaryTap: isActive() ? showModal : null, + onPanStart: isActive() ? (details) { panStartRect = widget.image.dstRect; + cropRect= widget.image.dstRect; } : null, - onPanUpdate: active + onPanUpdate: isActive() ? (details) { setState(() { double fivePercent = min(widget.pageSize.width * 0.05, @@ -164,10 +212,11 @@ class _CanvasImageState extends State { widget.image.dstRect.width, widget.image.dstRect.height, ); + widget.image.dstFullRect=widget.image.getDstFullRect(); // update full rect }); } : null, - onPanEnd: active + onPanEnd: isActive() ? (details) { if (panStartRect == widget.image.dstRect) return; widget.image.onMoveImage?.call( @@ -185,7 +234,7 @@ class _CanvasImageState extends State { decoration: BoxDecoration( border: Border.all( color: - active ? colorScheme.onBackground : Colors.transparent, + isActive() ? colorScheme.onBackground : Colors.transparent, width: 2, ), ), @@ -193,30 +242,52 @@ class _CanvasImageState extends State { child: SizedBox( width: widget.isBackground ? widget.pageSize.width - : max(widget.image.dstRect.width, + : max(widget.image.dstFullRect.width, CanvasImage.minImageSize), height: widget.isBackground ? widget.pageSize.height - : max(widget.image.dstRect.height, + : max(widget.image.dstFullRect.height, CanvasImage.minImageSize), - child: SizedOverflowBox( - size: widget.image.srcRect.size, - child: Transform.translate( - offset: -widget.image.srcRect.topLeft, - child: widget.image.buildImageWidget( - context: context, - overrideBoxFit: widget.overrideBoxFit, - isBackground: widget.isBackground, - shaderEnabled: imageBrightness == Brightness.dark, - shaderBuilder: (ui.Image image, Size size) { - shader.setFloat(0, size.width); - shader.setFloat(1, size.height); - shader.setImageSampler(0, image); - return shader; - }, +// child: Container( +// color: Colors.red, + child: SizedOverflowBox( + size: widget.image.naturalSize, // size of full image + child: Transform.translate( + offset: Offset.zero, //-widget.image.srcRect.topLeft, +// child: Container( +// color: Colors.blue, + child: SizedBox( + height: widget.image.dstFullRect.height, + width: widget.image.dstFullRect.width, +// child: ClipRect( +// clipper: ImgClipper( +// widget.image.showCroppedImage ? +// widget.image.dstRect: // when image crop is active crop by dstRect +// widget.image.dstFullRect // show full image +// ), + child: Container( + height: widget.image.dstFullRect.height, + width: widget.image.dstFullRect.width, + color: Colors.yellow, + child: widget.image.buildImageWidget( + context: context, + overrideBoxFit: widget.overrideBoxFit, + isBackground: widget.isBackground, + shaderEnabled: imageBrightness == Brightness.dark, + shaderBuilder: (ui.Image image, Size size) { + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + shader.setImageSampler(0, image); + return shader; + }, + ), +// ), + ), + ), +// ), ), ), - ), +// ), ), ), ), @@ -227,16 +298,7 @@ class _CanvasImageState extends State { color: colorScheme.primary.withOpacity(0.5), ), if (!widget.readOnly) - for (double x = -20; x <= 20; x += 20) - for (double y = -20; y <= 20; y += 20) - if (x != 0 || y != 0) // ignore (0,0) - _CanvasImageResizeHandle( - active: active, - position: Offset(x, y), - image: widget.image, - parent: this, - afterDrag: () => setState(() {}), - ), + ...buildResizeHandles(_activeType), // add set of handles manipulating image to Stack using spread separator ... ], ), ); @@ -252,22 +314,89 @@ class _CanvasImageState extends State { child: unpositioned, ); } - return AnimatedPositioned( - // no animation if the image is being dragged or it's selected - duration: (panStartRect != Rect.zero || widget.selected) - ? Duration.zero - : const Duration(milliseconds: 300), - curve: Curves.fastLinearToSlowEaseIn, - - left: widget.image.dstRect.left, - top: widget.image.dstRect.top, - width: max(widget.image.dstRect.width, CanvasImage.minInteractiveSize), - height: max(widget.image.dstRect.height, CanvasImage.minInteractiveSize), - - child: unpositioned, - ); + if (_activeType!=Active.crop) { + return AnimatedPositioned( + // no animation if the image is being dragged or it's selected + duration: (panStartRect != Rect.zero || widget.selected) + ? Duration.zero + : const Duration(milliseconds: 300), + curve: Curves.fastLinearToSlowEaseIn, + left: widget.image.dstFullRect.left, + top: widget.image.dstFullRect.top, + width: max(widget.image.dstFullRect.width, CanvasImage.minInteractiveSize), + height: max( + widget.image.dstFullRect.height, CanvasImage.minInteractiveSize), + child: unpositioned, + ); + } + else { + // image is cropped, actualize image FullRect + widget.image.dstFullRect=widget.image.getDstFullRect(); + // and set that animated handles can move over Full image rectangle + return AnimatedPositioned( + // no animation if the image is being dragged or it's selected + duration: (panStartRect != Rect.zero || widget.selected) + ? Duration.zero + : const Duration(milliseconds: 300), + curve: Curves.fastLinearToSlowEaseIn, + left: widget.image.dstFullRect.left, + top: widget.image.dstFullRect.top, + width: max(widget.image.dstFullRect.width, CanvasImage.minInteractiveSize), + height: max( + widget.image.dstFullRect.height, CanvasImage.minInteractiveSize), + + child: unpositioned, + ); + } + } + + + /// create a set of handles to manipulate image depending of type of Active + List buildResizeHandles(Active activeType){ + /// create "handles" to resize image or crop image + List handles = []; + switch(activeType) { + case Active.none: + break; + case Active.destination: + // create handles in corners and centers of edges + for (double x = -20; x <= 20; x += 20){ + for (double y = -20; y <= 20; y += 20){ + if (x != 0 || y != 0){ // ignore (0,0) + handles.add( + _CanvasImageResizeHandle( + active: isActive(), + position: Offset (x, y), + image: widget.image, + parent: this, + afterDrag: () => setState(() {}), + ) + ); + } + } + } + case Active.crop: + // first record of dstRect to auxiliary cropRect + cropRect= widget.image.dstRect; + // create handles in corners + for (double x = -20; x <= 20; x += 40){ + for (double y = -20; y <= 20; y += 40){ + handles.add( + _CanvasImageCropHandle( + active: isActive(), + position: Offset (x, y), + image: widget.image, + parent: this, + afterDrag: () => setState(() {}), + ) + ); + } + } + } + return(handles); } + @override void dispose() { widget.image.loadOut(); @@ -317,8 +446,8 @@ class _CanvasImageResizeHandle extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Positioned( - left: (position.dx.sign + 1) / 2 * image.dstRect.width - 20, - top: (position.dy.sign + 1) / 2 * image.dstRect.height - 20, + left: (image.dstRect.topLeft-image.dstFullRect.topLeft).dx+ (position.dx.sign + 1) / 2 * image.dstRect.width - 20,// calculate position of handle according to current dstRect + top: (image.dstRect.topLeft-image.dstFullRect.topLeft).dy+ (position.dy.sign + 1) / 2 * image.dstRect.height - 20,// dstRect is recalculated when moving handles child: DeferPointer( paintOnTop: true, child: MouseRegion( @@ -405,6 +534,7 @@ class _CanvasImageResizeHandle extends StatelessWidget { newWidth, newHeight, ); + image.dstFullRect=image.getDstFullRect(); // update image full rect according new dstRect afterDrag(); } : null, @@ -444,3 +574,158 @@ class _CanvasImageResizeHandle extends StatelessWidget { ); } } + +class _CanvasImageCropHandle extends StatelessWidget { + // handles in corners of image handling croprect + const _CanvasImageCropHandle({ + required this.active, + required this.position, + required this.image, + required this.parent, + required this.afterDrag, + }); + + final bool active; + final Offset position; + final EditorImage image; + final _CanvasImageState parent; + final void Function() afterDrag; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Positioned( + left: (image.dstRect.topLeft-image.dstFullRect.topLeft).dx+ (position.dx.sign + 1) / 2 * parent.cropRect.width - 20, // use positions of handles according to cropRect stored in parent + top: (image.dstRect.topLeft-image.dstFullRect.topLeft).dy+ (position.dy.sign + 1) / 2 * parent.cropRect.height - 20, + child: DeferPointer( + paintOnTop: true, + child: MouseRegion( + cursor: () { + if (!active) return MouseCursor.defer; + + if (position.dx < 0 && position.dy < 0) + return SystemMouseCursors.resizeUpLeft; + if (position.dx < 0 && position.dy > 0) + return SystemMouseCursors.resizeDownLeft; + if (position.dx > 0 && position.dy < 0) + return SystemMouseCursors.resizeUpRight; + if (position.dx > 0 && position.dy > 0) + return SystemMouseCursors.resizeDownRight; + + return MouseCursor.defer; + }(), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanStart: active + ? (details) { + parent.panStartRect = parent.widget.image.dstRect; + parent.panStartPosition = details.localPosition; + } + : null, + onPanUpdate: active + ? (details) { + final Offset delta = + details.localPosition - parent.panStartPosition; + + double newWidth; + if (position.dx < 0) { + newWidth = parent.panStartRect.width - delta.dx; + } else if (position.dx > 0) { + newWidth = parent.panStartRect.width + delta.dx; + } else { + newWidth = parent.panStartRect.width; + } + + double newHeight; + if (position.dy < 0) { + newHeight = parent.panStartRect.height - delta.dy; + } else if (position.dy > 0) { + newHeight = parent.panStartRect.height + delta.dy; + } else { + newHeight = parent.panStartRect.height; + } + + if (newWidth <= 0 || newHeight <= 0) return; + + // resize from the correct corner + double left = parent.cropRect.left, top = parent.cropRect.top; + if (position.dx < 0) { + left = parent.cropRect.right - newWidth; + } + if (position.dy < 0) { + top = parent.cropRect.bottom - newHeight; + } + double right=left+newWidth; + double bottom=top+newHeight; + + // check if crop rect is not over full rect of image in destination coordinates + Rect fullImageRect=image.getDstFullRect(); + if (leftfullImageRect.right) { + newWidth=fullImageRect.right-left; + } + if (bottom>fullImageRect.bottom) { + newHeight=fullImageRect.bottom-top; + } + parent.cropRect=Rect.fromLTWH( // this is rectangle given by handles is cropRect and also dstRect + left, + top, + newWidth, + newHeight, + ); + // recalculate given crop rect given by current selection to image source coordinates + image.srcRect=image.transformRectFromDstToSrcDuringCrop(parent.cropRect); + image.dstRect=parent.cropRect; + // image dstFullRect is not changed, because we are not scaling image + afterDrag(); + } + : null, + onPanEnd: active + ? (details) { + if (parent.panStartRect == parent.cropRect) return; + // now parent.cropRect is new image.dstRect + // image.cropRect should be recalculated with respect to rectangle of image.srcRect + parent.cropRect=parent.panStartRect.topLeft&parent.panStartRect.size/2; + parent.cropRect=parent.cropRect.shift(Offset(50,50)); + image.srcRect=image.transformRectFromDstToSrcDuringCrop(parent.cropRect); // this is full rect + image.dstRect=parent.cropRect; // and set destination rect to size of cropRect + image.dstFullRect=image.getDstFullRect(); // update full rect +// image.onMoveImage?.call( +// image, +// Rect.fromLTRB( +// image.dstRect.left - parent.panStartRect.left, +// image.dstRect.top - parent.panStartRect.top, +// image.dstRect.right - parent.panStartRect.right, +// image.dstRect.bottom - parent.panStartRect.bottom, +// )); + parent.panStartRect = Rect.zero; + } + : null, + child: AnimatedOpacity( + opacity: active ? 1 : 0, + duration: const Duration(milliseconds: 100), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: colorScheme.onBackground, + shape: BoxShape.rectangle, + border: Border.all( + color: colorScheme.background, + width: 2, + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/canvas/image/editor_image.dart b/lib/components/canvas/image/editor_image.dart index f2fbead77..10dc64d4e 100644 --- a/lib/components/canvas/image/editor_image.dart +++ b/lib/components/canvas/image/editor_image.dart @@ -50,8 +50,10 @@ sealed class EditorImage extends ChangeNotifier { void Function()? onMiscChange; final VoidCallback? onLoad; + /// rectangle used to display image (image crop region to be displayed). Full image dimension is given by naturalSize Rect srcRect = Rect.zero; + /// rectangle used to display image on canvas (area of cropped image) Rect _dstRect = Rect.zero; Rect get dstRect => _dstRect; set dstRect(Rect dstRect) { @@ -59,7 +61,18 @@ sealed class EditorImage extends ChangeNotifier { notifyListeners(); } - /// Defines the aspect ratio of the image. + /// rectangle used to display full size of image (without cropping it) + Rect _dstFullRect = Rect.zero; + Rect get dstFullRect => _dstFullRect; + set dstFullRect(Rect dstFullRect) { + _dstFullRect = dstFullRect; + notifyListeners(); + } + + /// show only cropped image, if false then show full image (it is used to select crop rect) + bool showCroppedImage=true; + + /// Defines the aspect ratio of the original image - image size Size naturalSize; /// The size of the page this image is on, @@ -250,4 +263,36 @@ sealed class EditorImage extends ChangeNotifier { return Size(width, height); } + + + // cropping image functions + + Rect getDstFullRect(){ + /// function returning rectangle in destination coordinates to be used to draw full image + /// image is draw in full size, I must calculate destination rect according to it + double scaleX= dstRect.width/srcRect.width; + double scaleY= dstRect.height/srcRect.height; + Offset cs=srcRect.topLeft; // offset of crop origin (topleft) from image origin (0,0) + Offset srcOriginInDest=-Offset(cs.dx*scaleX,cs.dy*scaleY); // offset of image origin (0,0) with respect to cropped origin in canvas dst coordinates + dstFullRect=Rect.fromLTWH(srcOriginInDest.dx,srcOriginInDest.dy,naturalSize.width*scaleX,naturalSize.height*scaleY).shift(dstRect.topLeft); // this is Rect in dst coordinates of full size image + return(dstFullRect); + } + + Rect transformRectFromDstToSrcDuringCrop(Rect dstR){ + /// rescales rectangle from destination coordinates given by dstRect to source coordinates given by srcRect + /// function is called during defining cropped part of image + double scaleX= dstRect.width/srcRect.width; + double scaleY= dstRect.height/srcRect.height; + + Rect rct=dstR.shift(-dstFullRect.topLeft); // remove destination rect position - position 0,0 is top left corner cropRect + double dx=rct.left/scaleX; // offset of dstR from src topleft + double dy=rct.top/scaleY; + double width=rct.width/scaleX; + double height=rct.height/scaleY; + Rect crp=Rect.fromLTWH(0+dx,0+dy,width,height); + return(crp); // this is new srcRect of image + } + + + } diff --git a/lib/components/canvas/image/png_editor_image.dart b/lib/components/canvas/image/png_editor_image.dart index 5893d0979..6bd1bce14 100644 --- a/lib/components/canvas/image/png_editor_image.dart +++ b/lib/components/canvas/image/png_editor_image.dart @@ -169,11 +169,22 @@ class PngEditorImage extends EditorImage { dstRect = dstRect.topLeft & dstSize; } } + if (srcRect.topLeft==Offset.zero) { + srcRect = Rect.fromLTWH(324 / 4, 162 / 4, 3 / 4 * 324, 3 / 4 * 162); + dstRect=Rect.fromLTWH(srcRect.left,srcRect.top,srcRect.width*2,srcRect.height*2).shift(Offset(100,100)); + } + + dstFullRect=getDstFullRect(); + if (dstFullRect.left<0){ + dstRect=dstRect.shift(-dstFullRect.topLeft); + dstFullRect=dstFullRect.shift(-dstFullRect.topLeft); + } if (naturalSize.shortestSide == 0) { naturalSize = Size(srcRect.width, srcRect.height); } + if (isThumbnail) { isThumbnail = true; // updates bytes and srcRect } diff --git a/lib/components/canvas/shader_image.dart b/lib/components/canvas/shader_image.dart index d90302f7f..dd6a74112 100644 --- a/lib/components/canvas/shader_image.dart +++ b/lib/components/canvas/shader_image.dart @@ -174,7 +174,7 @@ class _ShaderImageRenderObject extends RenderBox { if (shader == null) { // no shader, just draw the image - context.canvas.drawImage( + context.canvas.drawImage(// image!, offset, paint, From cccb9c56e3a27fab368df6c87c726bdb330e441e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Wed, 21 Feb 2024 22:03:26 +0100 Subject: [PATCH 2/8] croping image works, but image is not redrawn when Action is changed without resizing. --- lib/components/canvas/canvas_image.dart | 39 ++++++++++++++++--------- lib/components/canvas/shader_image.dart | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index 74c2315ca..3b68e9822 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -18,16 +18,16 @@ enum Active { crop, // setting crop rect } -/// crop image by rect +/// crop image by given rect - used to crop image class ImgClipper extends CustomClipper { ImgClipper( this.clipRect, - ); - final Rect clipRect; + ); + final Rect clipRect; // rectangle used to crop Image @override - Rect getClip(Size size) { - return clipRect.shift(-clipRect.topLeft); + Rect getClip(Size size) {// + return clipRect; } @override @@ -124,6 +124,19 @@ class _CanvasImageState extends State { Rect cropRect = Rect.zero; Rect cropStartRect = Rect.zero; + // return clip rectangle used to crop image + Rect getClipRect(){ + if (widget.image.showCroppedImage) { + Offset o = widget.image.dstRect.topLeft-widget.image.dstFullRect.topLeft; + Rect clipRect=o&widget.image.dstRect.size; // offset dstRect by its distance from topLeft of full image + return (clipRect); + } + else { + return (Offset.zero & widget.image.dstFullRect.size); + } + } + + @override void initState() { widget.image.loadIn(); @@ -250,6 +263,10 @@ class _CanvasImageState extends State { CanvasImage.minImageSize), // child: Container( // color: Colors.red, + child: ClipRect( // cliping is done in the sized box. I means that full image topLeft is (0,0) + clipper: ImgClipper( + getClipRect() // return clip rectangle according to actual action + ), child: SizedOverflowBox( size: widget.image.naturalSize, // size of full image child: Transform.translate( @@ -259,12 +276,6 @@ class _CanvasImageState extends State { child: SizedBox( height: widget.image.dstFullRect.height, width: widget.image.dstFullRect.width, -// child: ClipRect( -// clipper: ImgClipper( -// widget.image.showCroppedImage ? -// widget.image.dstRect: // when image crop is active crop by dstRect -// widget.image.dstFullRect // show full image -// ), child: Container( height: widget.image.dstFullRect.height, width: widget.image.dstFullRect.width, @@ -281,10 +292,10 @@ class _CanvasImageState extends State { return shader; }, ), -// ), + ), ), - ), -// ), +// ), + ), ), ), // ), diff --git a/lib/components/canvas/shader_image.dart b/lib/components/canvas/shader_image.dart index dd6a74112..d90302f7f 100644 --- a/lib/components/canvas/shader_image.dart +++ b/lib/components/canvas/shader_image.dart @@ -174,7 +174,7 @@ class _ShaderImageRenderObject extends RenderBox { if (shader == null) { // no shader, just draw the image - context.canvas.drawImage(// + context.canvas.drawImage( image!, offset, paint, From f49c5a93c58c0676eb2a2d329df233f330fc5df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 05:45:14 +0100 Subject: [PATCH 3/8] Fixed imageCrop update of widget --- lib/components/canvas/canvas_image.dart | 46 ++++++++++--------- .../canvas/image/png_editor_image.dart | 20 ++++---- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index 3b68e9822..2b0025710 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -18,21 +18,25 @@ enum Active { crop, // setting crop rect } -/// crop image by given rect - used to crop image +/// crop image by given rect - used to crop image// class ImgClipper extends CustomClipper { ImgClipper( - this.clipRect, + this._clipRect, ); - final Rect clipRect; // rectangle used to crop Image + Rect _clipRect; // rectangle used to crop Image + Rect get clipRect => _clipRect; + set clipRect(Rect clipRect) { + _clipRect = clipRect; + } @override Rect getClip(Size size) {// - return clipRect; + return _clipRect; } @override - bool shouldReclip(oldClipper) { - return false; + bool shouldReclip(ImgClipper oldClipper) { + return (clipRect != oldClipper.clipRect); } } @@ -58,6 +62,7 @@ class CanvasImage extends StatefulWidget { final bool readOnly; final bool selected; + /// When notified, all [CanvasImages] will have their [active] property set to false. static ChangeNotifier activeListener = ChangeNotifier(); @@ -91,7 +96,6 @@ class _CanvasImageState extends State { } _activeType = value; // set active state widget.image.showCroppedImage=_activeType!=Active.crop; // if active state is not crop, then show cropped image - if (mounted) { try { setState(() {}); @@ -261,8 +265,8 @@ class _CanvasImageState extends State { ? widget.pageSize.height : max(widget.image.dstFullRect.height, CanvasImage.minImageSize), -// child: Container( -// color: Colors.red, + child: Container( + color: Colors.red, child: ClipRect( // cliping is done in the sized box. I means that full image topLeft is (0,0) clipper: ImgClipper( getClipRect() // return clip rectangle according to actual action @@ -298,7 +302,7 @@ class _CanvasImageState extends State { ), ), ), -// ), + ), ), ), ), @@ -398,7 +402,7 @@ class _CanvasImageState extends State { position: Offset (x, y), image: widget.image, parent: this, - afterDrag: () => setState(() {}), + afterDrag: () => setState(() {}), // force repaint ) ); } @@ -546,7 +550,7 @@ class _CanvasImageResizeHandle extends StatelessWidget { newHeight, ); image.dstFullRect=image.getDstFullRect(); // update image full rect according new dstRect - afterDrag(); +// afterDrag(); } : null, onPanEnd: active @@ -702,19 +706,17 @@ class _CanvasImageCropHandle extends StatelessWidget { if (parent.panStartRect == parent.cropRect) return; // now parent.cropRect is new image.dstRect // image.cropRect should be recalculated with respect to rectangle of image.srcRect - parent.cropRect=parent.panStartRect.topLeft&parent.panStartRect.size/2; - parent.cropRect=parent.cropRect.shift(Offset(50,50)); image.srcRect=image.transformRectFromDstToSrcDuringCrop(parent.cropRect); // this is full rect image.dstRect=parent.cropRect; // and set destination rect to size of cropRect image.dstFullRect=image.getDstFullRect(); // update full rect -// image.onMoveImage?.call( -// image, -// Rect.fromLTRB( -// image.dstRect.left - parent.panStartRect.left, -// image.dstRect.top - parent.panStartRect.top, -// image.dstRect.right - parent.panStartRect.right, -// image.dstRect.bottom - parent.panStartRect.bottom, -// )); + image.onMoveImage?.call( + image, + Rect.fromLTRB( + image.dstRect.left - parent.panStartRect.left, + image.dstRect.top - parent.panStartRect.top, + image.dstRect.right - parent.panStartRect.right, + image.dstRect.bottom - parent.panStartRect.bottom, + )); parent.panStartRect = Rect.zero; } : null, diff --git a/lib/components/canvas/image/png_editor_image.dart b/lib/components/canvas/image/png_editor_image.dart index 6bd1bce14..7dceacf45 100644 --- a/lib/components/canvas/image/png_editor_image.dart +++ b/lib/components/canvas/image/png_editor_image.dart @@ -169,16 +169,16 @@ class PngEditorImage extends EditorImage { dstRect = dstRect.topLeft & dstSize; } } - if (srcRect.topLeft==Offset.zero) { - srcRect = Rect.fromLTWH(324 / 4, 162 / 4, 3 / 4 * 324, 3 / 4 * 162); - dstRect=Rect.fromLTWH(srcRect.left,srcRect.top,srcRect.width*2,srcRect.height*2).shift(Offset(100,100)); - } - - dstFullRect=getDstFullRect(); - if (dstFullRect.left<0){ - dstRect=dstRect.shift(-dstFullRect.topLeft); - dstFullRect=dstFullRect.shift(-dstFullRect.topLeft); - } +// if (srcRect.topLeft==Offset.zero) { +// srcRect = Rect.fromLTWH(324 / 4, 162 / 4, 3 / 4 * 324, 3 / 4 * 162); +// dstRect=Rect.fromLTWH(srcRect.left,srcRect.top,srcRect.width*2,srcRect.height*2).shift(Offset(100,100)); +// } + + dstFullRect=getDstFullRect(); // calculate full image rect +// if (dstFullRect.left<0){ +// dstRect=dstRect.shift(-dstFullRect.topLeft); +// dstFullRect=dstFullRect.shift(-dstFullRect.topLeft); +// } if (naturalSize.shortestSide == 0) { naturalSize = Size(srcRect.width, srcRect.height); From 67691bb7165ef2710d53dd14d6ea0b3d0d13382d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 06:04:52 +0100 Subject: [PATCH 4/8] comments of functions updated --- lib/components/canvas/image/editor_image.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/components/canvas/image/editor_image.dart b/lib/components/canvas/image/editor_image.dart index 10dc64d4e..53560be98 100644 --- a/lib/components/canvas/image/editor_image.dart +++ b/lib/components/canvas/image/editor_image.dart @@ -264,12 +264,10 @@ sealed class EditorImage extends ChangeNotifier { return Size(width, height); } + // image cropping functions - // cropping image functions - + /// function returning rectangle in destination coordinates (canvas) to be used to draw full image Rect getDstFullRect(){ - /// function returning rectangle in destination coordinates to be used to draw full image - /// image is draw in full size, I must calculate destination rect according to it double scaleX= dstRect.width/srcRect.width; double scaleY= dstRect.height/srcRect.height; Offset cs=srcRect.topLeft; // offset of crop origin (topleft) from image origin (0,0) @@ -278,9 +276,10 @@ sealed class EditorImage extends ChangeNotifier { return(dstFullRect); } + /// recalculates rectangle from destination coordinates given by dstR to image source coordinates given by srcRect + /// function is called during defining cropped part of image - dstR is rectangle in canvas coordinates + /// and represents a part of dstFullRect. From their difference we calculate srcRect - part of image to be displayed Rect transformRectFromDstToSrcDuringCrop(Rect dstR){ - /// rescales rectangle from destination coordinates given by dstRect to source coordinates given by srcRect - /// function is called during defining cropped part of image double scaleX= dstRect.width/srcRect.width; double scaleY= dstRect.height/srcRect.height; From 42b25753cbcaa4babb31078ccc1c7a48d4472772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 06:05:38 +0100 Subject: [PATCH 5/8] cleaned unnecessary code --- lib/components/canvas/canvas_image.dart | 96 +++++++++++-------------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index 2b0025710..add65ae2b 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -18,7 +18,7 @@ enum Active { crop, // setting crop rect } -/// crop image by given rect - used to crop image// +/// crop image by given rect - used to crop image class ImgClipper extends CustomClipper { ImgClipper( this._clipRect, @@ -36,7 +36,7 @@ class ImgClipper extends CustomClipper { @override bool shouldReclip(ImgClipper oldClipper) { - return (clipRect != oldClipper.clipRect); + return (clipRect != oldClipper.clipRect); // when clipping Rectangle changes force repaint } } @@ -105,8 +105,8 @@ class _CanvasImageState extends State { } } + /// provide next active type to the current one Active setNextActive(){ - /// provide next active type to the current one switch(_activeType) { case Active.none: return(Active.destination); @@ -128,14 +128,16 @@ class _CanvasImageState extends State { Rect cropRect = Rect.zero; Rect cropStartRect = Rect.zero; - // return clip rectangle used to crop image + /// return clip rectangle used to crop image Rect getClipRect(){ if (widget.image.showCroppedImage) { + // clipRectangle is used Offset o = widget.image.dstRect.topLeft-widget.image.dstFullRect.topLeft; Rect clipRect=o&widget.image.dstRect.size; // offset dstRect by its distance from topLeft of full image return (clipRect); } else { + // show the whole image during clipping rectangle selection return (Offset.zero & widget.image.dstFullRect.size); } } @@ -248,58 +250,42 @@ class _CanvasImageState extends State { } : null, child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: - isActive() ? colorScheme.onBackground : Colors.transparent, - width: 2, - ), + decoration: BoxDecoration( + border: Border.all( + color: + isActive() ? colorScheme.onBackground : Colors.transparent, + width: 2, ), - child: Center( - child: SizedBox( - width: widget.isBackground - ? widget.pageSize.width - : max(widget.image.dstFullRect.width, - CanvasImage.minImageSize), - height: widget.isBackground - ? widget.pageSize.height - : max(widget.image.dstFullRect.height, - CanvasImage.minImageSize), - child: Container( - color: Colors.red, - child: ClipRect( // cliping is done in the sized box. I means that full image topLeft is (0,0) - clipper: ImgClipper( - getClipRect() // return clip rectangle according to actual action - ), - child: SizedOverflowBox( - size: widget.image.naturalSize, // size of full image - child: Transform.translate( - offset: Offset.zero, //-widget.image.srcRect.topLeft, -// child: Container( -// color: Colors.blue, - child: SizedBox( - height: widget.image.dstFullRect.height, - width: widget.image.dstFullRect.width, - child: Container( - height: widget.image.dstFullRect.height, - width: widget.image.dstFullRect.width, - color: Colors.yellow, - child: widget.image.buildImageWidget( - context: context, - overrideBoxFit: widget.overrideBoxFit, - isBackground: widget.isBackground, - shaderEnabled: imageBrightness == Brightness.dark, - shaderBuilder: (ui.Image image, Size size) { - shader.setFloat(0, size.width); - shader.setFloat(1, size.height); - shader.setImageSampler(0, image); - return shader; - }, - ), - ), - ), -// ), - ), + ), + child: SizedBox( + width: widget.isBackground + ? widget.pageSize.width + : max(widget.image.dstFullRect.width, + CanvasImage.minImageSize), + height: widget.isBackground + ? widget.pageSize.height + : max(widget.image.dstFullRect.height, + CanvasImage.minImageSize), + child: ClipRect( // cliping is done in the sized box. I means that full image topLeft is (0,0) + clipper: ImgClipper( + getClipRect() // return clip rectangle according to actual action + ), + child: SizedOverflowBox( + size: widget.image.naturalSize, // size of full image + child: SizedBox( + height: widget.image.dstFullRect.height, + width: widget.image.dstFullRect.width, + child: widget.image.buildImageWidget( + context: context, + overrideBoxFit: widget.overrideBoxFit, + isBackground: widget.isBackground, + shaderEnabled: imageBrightness == Brightness.dark, + shaderBuilder: (ui.Image image, Size size) { + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + shader.setImageSampler(0, image); + return shader; + }, ), ), ), From 35a33f1af64e25e00436244a876ecf580fa9abeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 19:13:52 +0100 Subject: [PATCH 6/8] srcRect can be zero (pdf files) --- lib/components/canvas/image/editor_image.dart | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/components/canvas/image/editor_image.dart b/lib/components/canvas/image/editor_image.dart index 53560be98..748dd27f9 100644 --- a/lib/components/canvas/image/editor_image.dart +++ b/lib/components/canvas/image/editor_image.dart @@ -268,8 +268,18 @@ sealed class EditorImage extends ChangeNotifier { /// function returning rectangle in destination coordinates (canvas) to be used to draw full image Rect getDstFullRect(){ - double scaleX= dstRect.width/srcRect.width; - double scaleY= dstRect.height/srcRect.height; + double scaleX; + double scaleY; + if (srcRect.width!=0 && srcRect.height!=0){ + scaleX= dstRect.width/srcRect.width; + scaleY= dstRect.height/srcRect.height; + } + else { + // src rect is not set. Assume it is naturalSize + // srcrect is zero for pdf images + scaleX= dstRect.width/naturalSize.width; + scaleY= dstRect.height/naturalSize.height; + } Offset cs=srcRect.topLeft; // offset of crop origin (topleft) from image origin (0,0) Offset srcOriginInDest=-Offset(cs.dx*scaleX,cs.dy*scaleY); // offset of image origin (0,0) with respect to cropped origin in canvas dst coordinates dstFullRect=Rect.fromLTWH(srcOriginInDest.dx,srcOriginInDest.dy,naturalSize.width*scaleX,naturalSize.height*scaleY).shift(dstRect.topLeft); // this is Rect in dst coordinates of full size image @@ -280,9 +290,18 @@ sealed class EditorImage extends ChangeNotifier { /// function is called during defining cropped part of image - dstR is rectangle in canvas coordinates /// and represents a part of dstFullRect. From their difference we calculate srcRect - part of image to be displayed Rect transformRectFromDstToSrcDuringCrop(Rect dstR){ - double scaleX= dstRect.width/srcRect.width; - double scaleY= dstRect.height/srcRect.height; - + double scaleX; + double scaleY; + if (srcRect.width!=0 && srcRect.height!=0){ + scaleX= dstRect.width/srcRect.width; + scaleY= dstRect.height/srcRect.height; + } + else { + // src rect is not set. Assume it is naturalSize + // srcrect is zero for pdf images + scaleX= dstRect.width/naturalSize.width; + scaleY= dstRect.height/naturalSize.height; + } Rect rct=dstR.shift(-dstFullRect.topLeft); // remove destination rect position - position 0,0 is top left corner cropRect double dx=rct.left/scaleX; // offset of dstR from src topleft double dy=rct.top/scaleY; From 0e251c0979c4b59d4041da57c99512aa53da89fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 19:15:07 +0100 Subject: [PATCH 7/8] In pdf image is set dstFullRect --- lib/components/canvas/image/pdf_editor_image.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/components/canvas/image/pdf_editor_image.dart b/lib/components/canvas/image/pdf_editor_image.dart index 36fd8baa5..1580bd404 100644 --- a/lib/components/canvas/image/pdf_editor_image.dart +++ b/lib/components/canvas/image/pdf_editor_image.dart @@ -135,6 +135,8 @@ class PdfEditorImage extends EditorImage { : naturalSize; dstRect = dstRect.topLeft & dstSize; } + // each image should have set dstFullRect. + dstFullRect=getDstFullRect(); // calculate full image rect } @override From d0562520a6c0eddfd4e766ba7f535e3b056d9fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Fri, 23 Feb 2024 19:15:34 +0100 Subject: [PATCH 8/8] cleaned from debug code --- lib/components/canvas/canvas_image.dart | 38 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index add65ae2b..b0388f186 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -96,6 +96,10 @@ class _CanvasImageState extends State { } _activeType = value; // set active state widget.image.showCroppedImage=_activeType!=Active.crop; // if active state is not crop, then show cropped image + if (widget.isBackground){ + // calculate dstFullRect for background image + getClipRect(); + } if (mounted) { try { setState(() {}); @@ -130,6 +134,15 @@ class _CanvasImageState extends State { /// return clip rectangle used to crop image Rect getClipRect(){ + if (widget.isBackground) { + // image is background image, must fit image to full page -> recalculate dstFullRect + Rect fullRect = Offset.zero & Size(widget.pageSize.width, widget.pageSize.height); + // now calculate size of cropped image to fit page Rectangle + final FittedSizes sizes = applyBoxFit(BoxFit.contain, widget.image.dstRect.size, fullRect.size); + final Rect dstBackgroundRect = Alignment.center.inscribe(sizes.destination, fullRect); // determine dstRect + widget.image.dstRect = dstBackgroundRect; + widget.image.dstFullRect =widget.image.getDstFullRect(); // update full rect + } if (widget.image.showCroppedImage) { // clipRectangle is used Offset o = widget.image.dstRect.topLeft-widget.image.dstFullRect.topLeft; @@ -250,21 +263,18 @@ class _CanvasImageState extends State { } : null, child: DecoratedBox( - decoration: BoxDecoration( + decoration: BoxDecoration( border: Border.all( color: isActive() ? colorScheme.onBackground : Colors.transparent, width: 2, + ), ), - ), - child: SizedBox( - width: widget.isBackground - ? widget.pageSize.width - : max(widget.image.dstFullRect.width, + child: SizedBox( // size of image box is allways image.dstFullRect. Cropping is limiting image size in ClipRect + // offset of SizedBox to dstFullRect.topleft is done in AnimatedPositioned later + width: max(widget.image.dstFullRect.width, CanvasImage.minImageSize), - height: widget.isBackground - ? widget.pageSize.height - : max(widget.image.dstFullRect.height, + height: max(widget.image.dstFullRect.height, CanvasImage.minImageSize), child: ClipRect( // cliping is done in the sized box. I means that full image topLeft is (0,0) clipper: ImgClipper( @@ -305,13 +315,13 @@ class _CanvasImageState extends State { ); if (widget.isBackground) { - return AnimatedPositioned( + return AnimatedPositioned(// duration: const Duration(milliseconds: 300), curve: Curves.fastLinearToSlowEaseIn, - left: 0, - top: 0, - right: 0, - bottom: 0, + left: widget.image.dstFullRect.left, + top: widget.image.dstFullRect.top, + width: widget.image.dstFullRect.width, + height: widget.image.dstFullRect.height, child: unpositioned, ); }