Skip to content

Commit

Permalink
Initial split impl
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Mar 9, 2024
1 parent 5b78a8f commit 03bd237
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 26 deletions.
4 changes: 2 additions & 2 deletions NAPS2.Images/Transforms/AbstractImageTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ protected virtual TImage PerformTransform(TImage image, CropTransform transform)

protected virtual TImage PerformTransform(TImage image, ScaleTransform transform)
{
var width = (int) Math.Round(image.Width * transform.ScaleFactor);
var height = (int) Math.Round(image.Height * transform.ScaleFactor);
var width = (int) Math.Max(Math.Round(image.Width * transform.ScaleFactor), 1);
var height = (int) Math.Max(Math.Round(image.Height * transform.ScaleFactor), 1);
return PerformTransform(image, new ResizeTransform(width, height));
}

Expand Down
7 changes: 3 additions & 4 deletions NAPS2.Images/Transforms/ThumbnailTransform.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local

namespace NAPS2.Images.Transforms;

Expand Down Expand Up @@ -30,7 +29,7 @@ public ThumbnailTransform(int size)
width = Size;
left = 0;
// Scale the drawing height to match the original bitmap's aspect ratio
height = (int)(originalHeight * (Size / (double)originalWidth));
height = (int) Math.Max(originalHeight * (Size / (double) originalWidth), 1);
// Center the drawing vertically
top = (Size - height) / 2;
}
Expand All @@ -40,7 +39,7 @@ public ThumbnailTransform(int size)
height = Size;
top = 0;
// Scale the drawing width to match the original bitmap's aspect ratio
width = (int)(originalWidth * (Size / (double)originalHeight));
width = (int) Math.Max(originalWidth * (Size / (double) originalHeight), 1);
// Center the drawing horizontally
left = (Size - width) / 2;
}
Expand Down
235 changes: 233 additions & 2 deletions NAPS2.Lib/EtoForms/Ui/SplitForm.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,246 @@
using Eto.Drawing;
using Eto.Forms;
using NAPS2.EtoForms.Layout;

namespace NAPS2.EtoForms.Ui;

public class SplitForm : UnaryImageFormBase
{
public SplitForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) :
private const int HANDLE_WIDTH = 1;
private const double HANDLE_RADIUS_RATIO = 0.2;
private const int HANDLE_MIN_RADIUS = 50;

private static CropTransform? _lastTransform;

private readonly ColorScheme _colorScheme;
private readonly Button _vSplit;
private readonly Button _hSplit;

// Mouse down location
private PointF _mouseOrigin;

// Crop amounts from each side as a fraction of the total image size (updated as the user drags)
private float _cropX, _cropY;

// Crop amounts from each side as pixels of the image to be cropped (updated on mouse up)
private float _realX, _realY;

private bool _dragging;
private SplitOrientation _orientation;

public SplitForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController,
IIconProvider iconProvider, ColorScheme colorScheme) :
base(config, imageList, thumbnailController)
{
_colorScheme = colorScheme;
Icon = new Icon(1f, Icons.split.ToEtoImage());
Title = UiStrings.Split;

_vSplit = C.IconButton(iconProvider.GetIcon("split")!, () => SetOrientation(SplitOrientation.Vertical));
_hSplit = C.IconButton(iconProvider.GetIcon("split_hor")!, () => SetOrientation(SplitOrientation.Horizontal));
Overlay.MouseDown += Overlay_MouseDown;
Overlay.MouseMove += Overlay_MouseMove;
Overlay.MouseUp += Overlay_MouseUp;
}

protected override List<Transform> Transforms => throw new NotSupportedException();

private int HandleClickRadius =>
(int) Math.Max(Math.Round((_orientation == SplitOrientation.Horizontal ? _overlayH : _overlayW) * HANDLE_RADIUS_RATIO), HANDLE_MIN_RADIUS);

protected override void OnPreLoad(EventArgs e)
{
base.OnPreLoad(e);
if (_lastTransform != null && _lastTransform.OriginalWidth == RealImageWidth &&
_lastTransform.OriginalHeight == RealImageHeight)
{
_realX = _lastTransform.Left == 0 ? RealImageWidth / 2f : _lastTransform.Left;
_realY = _lastTransform.Top == 0 ? RealImageHeight / 2f : _lastTransform.Top;
_cropX = _realX / RealImageWidth;
_cropY = _realY / RealImageHeight;
}
else
{
_cropX = 0.5f;
_cropY = 0.5f;
_realX = RealImageWidth / 2f;
_realY = RealImageHeight / 2f;
}
}

protected override LayoutElement CreateControls()
{
return L.Row(
C.Filler(),
L.Row(_vSplit, _hSplit),
C.Filler()
);
}

protected override void InitDisplayImage()
{
base.InitDisplayImage();
_orientation = WorkingImage!.Width > WorkingImage!.Height
? SplitOrientation.Vertical
: SplitOrientation.Horizontal;
}

protected override void OnShown(EventArgs e)
{
base.OnShown(e);
(_orientation == SplitOrientation.Horizontal ? _hSplit : _vSplit).Focus();
}

private void SetOrientation(SplitOrientation orientation)
{
_orientation = orientation;
UpdatePreviewBox();
}

protected override IMemoryImage RenderPreview()
{
return WorkingImage!.Clone();
}

protected override void Revert()
{
_cropX = _cropY = 0.5f;
_realX = RealImageWidth / 2f;
_realY = RealImageHeight / 2f;
Overlay.Invalidate();
}

protected override List<Transform> Transforms => [];
private void Overlay_MouseDown(object? sender, MouseEventArgs e)
{
_dragging = IsHandleUnderMouse(e);
_mouseOrigin = e.Location;
Overlay.Invalidate();
}

private void Overlay_MouseUp(object? sender, MouseEventArgs e)
{
_realX = _cropX * RealImageWidth;
_realY = _cropY * RealImageHeight;
_dragging = false;
Overlay.Invalidate();
}

private void Overlay_MouseMove(object? sender, MouseEventArgs e)
{
Overlay.Cursor = _dragging || IsHandleUnderMouse(e)
? _orientation == SplitOrientation.Horizontal
? Cursors.HorizontalSplit
: Cursors.VerticalSplit
: Cursors.Arrow;
if (_dragging)
{
UpdateCrop(e.Location);
Overlay.Invalidate();
}
}

protected override void PaintOverlay(object? sender, PaintEventArgs e)
{
base.PaintOverlay(sender, e);

if (_overlayW == 0 || _overlayH == 0)
{
return;
}

var offsetX = _cropX * _overlayW;
var offsetY = _cropY * _overlayH;
var fillColor = new Color(0.3f, 0.3f, 0.3f, 0.3f);
var handlePen = new Pen(_colorScheme.CropColor, HANDLE_WIDTH);

if (_overlayW >= 1 && _overlayH >= 1)
{
// Fade out cropped-out portions of the image
if (_orientation == SplitOrientation.Horizontal)
{
e.Graphics.FillRectangle(fillColor, _overlayL, _overlayT + offsetY, _overlayW, _overlayH - offsetY);
}
else
{
e.Graphics.FillRectangle(fillColor, _overlayL + offsetX, _overlayT, _overlayW - offsetX, _overlayH);
}
}

if (_orientation == SplitOrientation.Horizontal)
{
var y = _overlayT + offsetY - HANDLE_WIDTH / 2f;
e.Graphics.DrawLine(handlePen, _overlayL, y, _overlayR - 1, y);
}
else
{
var x = _overlayL + offsetX - HANDLE_WIDTH / 2f;
e.Graphics.DrawLine(handlePen, x, _overlayT, x, _overlayB - 1);
}
}

private bool IsHandleUnderMouse(MouseEventArgs e)
{
var radius = HandleClickRadius;
if (_orientation == SplitOrientation.Horizontal)
{
var y = _overlayT + _cropY * _overlayH;
return e.Location.Y > y - radius && e.Location.Y < y + radius && e.Location.X > _overlayL && e.Location.X < _overlayR;
}
else
{
var x = _overlayL + _cropX * _overlayW;
return e.Location.X > x - radius && e.Location.X < x + radius && e.Location.Y > _overlayT && e.Location.Y < _overlayB;
}
}

private void UpdateCrop(PointF mousePos)
{
var delta = mousePos - _mouseOrigin;
if (_orientation == SplitOrientation.Vertical)
{
_cropX = (_realX / RealImageWidth + delta.X / _overlayW)
.Clamp(1f / RealImageWidth, (RealImageWidth - 1f) / RealImageWidth);
}
else
{
_cropY = (_realY / RealImageHeight + delta.Y / _overlayH)
.Clamp(1f / RealImageHeight, (RealImageHeight - 1f) / RealImageHeight);
}
}

protected override void Apply()
{
var transform1 = _orientation == SplitOrientation.Horizontal
? new CropTransform(0, 0, 0, RealImageHeight - (int) Math.Round(_realY), RealImageWidth, RealImageHeight)
: new CropTransform(0, RealImageWidth - (int) Math.Round(_realX), 0, 0, RealImageWidth, RealImageHeight);
var transform2 = _orientation == SplitOrientation.Horizontal
? new CropTransform(0, 0, (int) Math.Round(_realY), 0, RealImageWidth, RealImageHeight)
: new CropTransform((int) Math.Round(_realX), 0, 0, 0, RealImageWidth, RealImageHeight);

var thumb1 = WorkingImage!.Clone()
.PerformAllTransforms([transform1, new ThumbnailTransform(ThumbnailController.RenderSize)]);
var thumb2 = WorkingImage.Clone()
.PerformAllTransforms([transform2, new ThumbnailTransform(ThumbnailController.RenderSize)]);

// We keep the second image as the original UiImage reference so that any InsertAfter points come after the
// pair of images. For example, if I'm in the middle of scanning and I split the most-recently scanned image,
// the next scanned image should appear at the end of the list, not in between the split images.
var oldTransforms = Image.TransformState;
var image1 = new UiImage(Image.GetClonedImage());
var image2 = Image;
image1.AddTransform(transform1, thumb1);
image2.AddTransform(transform2, thumb2);
ImageList.Mutate(new ListMutation<UiImage>.InsertBefore(image1, image2));
ImageList.AddToSelection(image1);
ImageList.PushUndoElement(
new SplitUndoElement(ImageList, image1, image2, oldTransforms, transform1, transform2));

_lastTransform = transform2;
}

private enum SplitOrientation
{
Horizontal,
Vertical
}
}
46 changes: 46 additions & 0 deletions NAPS2.Lib/Images/SplitUndoElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace NAPS2.Images;

public class SplitUndoElement(
UiImageList imageList,
UiImage image1,
UiImage image2,
TransformState oldTransforms,
CropTransform transform1,
CropTransform transform2)
: IUndoElement
{
public void ApplyUndo()
{
if (imageList.Images.Contains(image1) && imageList.Images.Contains(image2) &&
image1.TransformState == oldTransforms.AddOrSimplify(transform1) &&
image2.TransformState == oldTransforms.AddOrSimplify(transform2))
{
image1.ReplaceTransformState(image1.TransformState, oldTransforms);
image2.ReplaceTransformState(image2.TransformState, oldTransforms);
if (imageList.Selection.Contains(image1))
{
imageList.AddToSelection(image2);
}
imageList.Mutate(new ListMutation<UiImage>.DeleteSelected(), ListSelection.Of(image1),
updateUndoStack: false, disposeDeleted: false);
image1.GetImageWeakReference().ProcessedImage.Dispose();
}
}

public void ApplyRedo()
{
if (imageList.Images.Contains(image2) && !imageList.Images.Contains(image1) &&
image2.TransformState == oldTransforms && !image1.IsDisposed)
{
image1.ReplaceInternalImage(image2.GetClonedImage());
image1.AddTransform(transform1);
image2.AddTransform(transform2);
imageList.Mutate(new ListMutation<UiImage>.InsertBefore(image1, image2), ListSelection.Empty<UiImage>(),
updateUndoStack: false);
if (imageList.Selection.Contains(image2))
{
imageList.AddToSelection(image1);
}
}
}
}
10 changes: 10 additions & 0 deletions NAPS2.Lib/Images/UiImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ public void ReplaceTransformState(TransformState oldTransformState, TransformSta
ThumbnailInvalidated?.Invoke(this, EventArgs.Empty);
}

public void ReplaceInternalImage(ProcessedImage newImage)
{
lock (this)
{
_processedImage = newImage;
_saved = false;
}
ThumbnailInvalidated?.Invoke(this, EventArgs.Empty);
}

public void ResetTransforms()
{
lock (this)
Expand Down
Loading

0 comments on commit 03bd237

Please sign in to comment.