Skip to content

Commit

Permalink
Initial combine impl
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Mar 7, 2024
1 parent 740d3bb commit 8716c6a
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 16 deletions.
3 changes: 3 additions & 0 deletions NAPS2.Images/Bitwise/FillColorImageOp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ namespace NAPS2.Images.Bitwise;

public class FillColorImageOp : UnaryBitwiseImageOp
{
public static FillColorImageOp Black => new(0, 0, 0, 255);
public static FillColorImageOp White => new(255, 255, 255, 255);

private readonly byte _r, _g, _b, _a;

public FillColorImageOp(byte r, byte g, byte b, byte a)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using NAPS2.Images.Bitwise;
using NAPS2.Util;

namespace NAPS2.Images.Storage;
namespace NAPS2.Images.Transforms;

public abstract class AbstractImageTransformer<TImage> where TImage : IMemoryImage
{
Expand Down
7 changes: 7 additions & 0 deletions NAPS2.Images/Transforms/CombineOrientation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NAPS2.Images.Transforms;

public enum CombineOrientation
{
Horizontal,
Vertical
}
50 changes: 50 additions & 0 deletions NAPS2.Images/Transforms/MoreImageTransforms.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using NAPS2.Images.Bitwise;

namespace NAPS2.Images.Transforms;

public static class MoreImageTransforms
{
public static IMemoryImage Combine(IMemoryImage first, IMemoryImage second, CombineOrientation orientation,
double offset = 0.5)
{
var imageContext = first.ImageContext;
var pixelFormat = first.PixelFormat > second.PixelFormat
? first.PixelFormat
: second.PixelFormat;
int width = orientation == CombineOrientation.Horizontal
? first.Width + second.Width
: Math.Max(first.Width, second.Width);
int height = orientation == CombineOrientation.Vertical
? first.Height + second.Height
: Math.Max(first.Height, second.Height);

var combinedImage = imageContext.Create(width, height, pixelFormat);
combinedImage.SetResolution(
Math.Max(first.HorizontalResolution, second.HorizontalResolution),
Math.Max(first.VerticalResolution, second.VerticalResolution));

FillColorImageOp.White.Perform(combinedImage);

new CopyBitwiseImageOp
{
DestXOffset = orientation == CombineOrientation.Horizontal ? 0 :
first.Width > second.Width ? 0 :
(int) (offset * (second.Width - first.Width)),
DestYOffset = orientation == CombineOrientation.Vertical ? 0 :
first.Height > second.Height ? 0 :
(int) (offset * (second.Height - first.Height))
}.Perform(first, combinedImage);

new CopyBitwiseImageOp
{
DestXOffset = orientation == CombineOrientation.Horizontal ? first.Width :
second.Width > first.Width ? 0 :
(int) (offset * (first.Width - second.Width)),
DestYOffset = orientation == CombineOrientation.Vertical ? first.Height :
second.Height > first.Height ? 0 :
(int) (offset * (first.Height - second.Height))
}.Perform(second, combinedImage);

return combinedImage;
}
}
12 changes: 12 additions & 0 deletions NAPS2.Lib/EtoForms/Layout/C.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,16 @@ public static LayoutElement None()
{
return new SkipLayoutElement();
}

public static Button IconButton(Image icon, Action onClick)
{
var button = new Button
{
Image = icon,
ImagePosition = ButtonImagePosition.Overlay,
MinimumSize = new Size(icon.Width + 30, 0)
};
button.Click += (_, _) => onClick();
return button;
}
}
165 changes: 162 additions & 3 deletions NAPS2.Lib/EtoForms/Ui/CombineForm.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,175 @@
using Eto.Drawing;
using NAPS2.EtoForms.Widgets;
using NAPS2.EtoForms.Layout;
using NAPS2.Scan;

namespace NAPS2.EtoForms.Ui;

public class CombineForm : ImageFormBase
{
public CombineForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) :
private readonly IIconProvider _iconProvider;
private readonly ScanningContext _scanningContext;

private readonly LayoutVisibility _hVis = new(false);
private readonly LayoutVisibility _vVis = new(false);
private readonly LayoutVisibility _hAlignVis = new(true);
private readonly LayoutVisibility _vAlignVis = new(true);
private CombineOrientation _orientation;
private double _hOffset = 0.5;
private double _vOffset = 0.5;

public CombineForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController,
IIconProvider iconProvider, ScanningContext scanningContext) :
base(config, imageList, thumbnailController)
{
_iconProvider = iconProvider;
_scanningContext = scanningContext;
Icon = new Icon(1f, Icons.combine.ToEtoImage());
Title = UiStrings.Combine;
CanApplyToAllSelected = false;
ShowRevertButton = false;
}

private UiImage Image1 { get; set; } = null!;
private UiImage Image2 { get; set; } = null!;

private IMemoryImage WorkingImage1 { get; set; } = null!;
private IMemoryImage WorkingImage2 { get; set; } = null!;

protected override LayoutElement CreateControls()
{
// TODO: Why is there a form size change when we first toggle orientation?
return L.Row(
C.Filler(),
L.Row(
L.Row(
C.IconButton(_iconProvider.GetIcon("shape_align_left")!, () => SetHOffset(0)),
C.IconButton(_iconProvider.GetIcon("shape_align_center")!, () => SetHOffset(0.5)),
C.IconButton(_iconProvider.GetIcon("shape_align_right")!, () => SetHOffset(1.0))
).Visible(_hAlignVis),
C.IconButton(_iconProvider.GetIcon("combine_hor")!, () => SetOrientation(CombineOrientation.Horizontal))
.Padding(left: 20),
C.IconButton(_iconProvider.GetIcon("switch")!, SwapImages)
).Visible(_vVis),
L.Row(
L.Row(
C.IconButton(_iconProvider.GetIcon("shape_align_top")!, () => SetVOffset(0)),
C.IconButton(_iconProvider.GetIcon("shape_align_middle")!, () => SetVOffset(0.5)),
C.IconButton(_iconProvider.GetIcon("shape_align_bottom")!, () => SetVOffset(1.0))
).Visible(_vAlignVis),
C.IconButton(_iconProvider.GetIcon("combine")!, () => SetOrientation(CombineOrientation.Vertical))
.Padding(left: 20),
C.IconButton(_iconProvider.GetIcon("switch_hor")!, SwapImages)
).Visible(_hVis),
C.Filler()
);
}

private void SwapImages()
{
(Image1, Image2) = (Image2, Image1);
(WorkingImage1, WorkingImage2) = (WorkingImage2, WorkingImage1);
UpdatePreviewBox();
}

private void SetHOffset(double value)
{
_hOffset = value;
UpdatePreviewBox();
}

private void SetVOffset(double value)
{
_vOffset = value;
UpdatePreviewBox();
}

private void SetOrientation(CombineOrientation orientation)
{
_orientation = orientation;
_hVis.IsVisible = _orientation == CombineOrientation.Horizontal;
_vVis.IsVisible = _orientation == CombineOrientation.Vertical;
UpdatePreviewBox();
}

protected override void InitDisplayImage()
{
// If there's an image after this one, then this is the first image, and the subsequent image is the second.
// Otherwise, we look for the previous image in the list, which should be considered the first image, and then
// this image is the second.
var nextImage = SelectedImages?.ElementAtOrDefault(1) ??
_imageList.Images.ElementAtOrDefault(_imageList.Images.IndexOf(Image) + 1);
Image1 = nextImage != null
? Image
: _imageList.Images.ElementAtOrDefault(_imageList.Images.IndexOf(Image) - 1) ??
throw new InvalidOperationException("No image to combine with");
Image2 = nextImage ?? Image;

using var processedImage1 = Image1.GetClonedImage();
WorkingImage1 = processedImage1.Render();
using var processedImage2 = Image2.GetClonedImage();
WorkingImage2 = processedImage2.Render();

_orientation = WorkingImage1.Width + WorkingImage2.Width > WorkingImage1.Height + WorkingImage2.Height
? CombineOrientation.Vertical
: CombineOrientation.Horizontal;
_hVis.IsVisible = _orientation == CombineOrientation.Horizontal;
_vVis.IsVisible = _orientation == CombineOrientation.Vertical;
// We could make these visibilities different (i.e. hAlignVis is only based on if widths are different), but
// that means the button alignment can change underneath the mouse which isn't great.
_hAlignVis.IsVisible = WorkingImage1.Width != WorkingImage2.Width || WorkingImage1.Height != WorkingImage2.Height;
_vAlignVis.IsVisible = WorkingImage1.Width != WorkingImage2.Width || WorkingImage1.Height != WorkingImage2.Height;

var workingArea = GetScreenWorkingArea();
var widthRatio1 = WorkingImage1.Width / workingArea.Width;
var heightRatio1 = WorkingImage1.Height / workingArea.Height;
var widthRatio2 = WorkingImage1.Width / workingArea.Width;
var heightRatio2 = WorkingImage1.Height / workingArea.Height;
var maxRatio = new[] { widthRatio1, heightRatio1, widthRatio2, heightRatio2 }.Max();
if (maxRatio > 1)
{
WorkingImage1 = WorkingImage1.PerformTransform(new ScaleTransform(1 / maxRatio));
WorkingImage2 = WorkingImage2.PerformTransform(new ScaleTransform(1 / maxRatio));
}

// TODO: We probably want to scale up any lower-res images to match the higher resolution. Here and in Apply().

DisplayImage = RenderPreview();
}

protected override IMemoryImage RenderPreview()
{
return CombineImages(WorkingImage1, WorkingImage2);
}

protected override List<Transform> Transforms => [];
private IMemoryImage CombineImages(IMemoryImage first, IMemoryImage second)
{
return MoreImageTransforms.Combine(first, second, _orientation,
_orientation == CombineOrientation.Horizontal ? _vOffset : _hOffset);
}

protected override void Apply()
{
// TODO: Consider the latency of this, especially with "Apply all". Does it make sense to be async? Have a operation?
using var processedImage1 = Image1.GetClonedImage();
using var renderedImage1 = processedImage1.Render();
using var processedImage2 = Image2.GetClonedImage();
using var renderedImage2 = processedImage2.Render();
using var combinedImage = CombineImages(renderedImage1, renderedImage2);

// TODO: Use working images for thumbnail?
var thumbnail = combinedImage.Clone().PerformTransform(new ThumbnailTransform(_thumbnailController.RenderSize));
var ppd = new PostProcessingData { Thumbnail = thumbnail, ThumbnailTransformState = TransformState.Empty };
var processedImage = _scanningContext.CreateProcessedImage(combinedImage).WithPostProcessingData(ppd, true);

_imageList.Mutate(new ListMutation<UiImage>.InsertAfter(new UiImage(processedImage), Image));
// TODO: Maybe have a checkbox to keep the original images?
_imageList.Mutate(new ListMutation<UiImage>.DeleteSelected(), ListSelection.Of(Image1, Image2));
}

protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
WorkingImage1.Dispose();
WorkingImage2.Dispose();
}
}
1 change: 1 addition & 0 deletions NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ protected virtual void UpdateToolbar()
Commands.ZoomOut.Enabled = ImageList.Images.Any() && _thumbnailController.VisibleSize > ThumbnailSizes.MIN_SIZE;
Commands.NewProfile.Enabled =
!(Config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked));
Commands.Combine.Enabled = ImageList.Images.Count > 1;
}

private void UpdateScanButton()
Expand Down
33 changes: 21 additions & 12 deletions NAPS2.Lib/EtoForms/Ui/ImageFormBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace NAPS2.EtoForms.Ui;

public abstract class ImageFormBase : EtoDialogBase
{
private readonly UiImageList _imageList;
private readonly ThumbnailController _thumbnailController;
protected readonly UiImageList _imageList;
protected readonly ThumbnailController _thumbnailController;

private readonly ImageView _imageView = new();
private readonly CheckBox _applyToSelected = new();
Expand Down Expand Up @@ -41,9 +41,9 @@ protected override void BuildLayout()
LayoutController.Content = L.Column(
Overlay.Scale(),
CreateControls(),
SelectedImages is { Count: > 1 } ? _applyToSelected : C.None(),
SelectedImages is { Count: > 1 } && CanApplyToAllSelected ? _applyToSelected : C.None(),
L.Row(
_revert,
ShowRevertButton ? _revert : C.None(),
C.Filler(),
L.OkCancel(
C.OkButton(this, beforeClose: Apply),
Expand All @@ -55,8 +55,8 @@ protected override void BuildLayout()
protected int ImageHeight { get; set; }
protected int ImageWidth { get; set; }

protected IMemoryImage? WorkingImage { get; private set; }
protected IMemoryImage? DisplayImage { get; private set; }
protected IMemoryImage? WorkingImage { get; set; }
protected IMemoryImage? DisplayImage { get; set; }
protected Drawable Overlay { get; } = new();
protected int OverlayBorderSize { get; set; }

Expand Down Expand Up @@ -135,6 +135,10 @@ protected virtual LayoutElement CreateControls()

protected bool CanScaleWorkingImage { get; set; } = true;

protected bool CanApplyToAllSelected { get; set; } = true;

protected bool ShowRevertButton { get; set; } = true;

protected virtual List<Transform> Transforms => throw new NotImplementedException();

private bool TransformMultiple => SelectedImages != null && _applyToSelected.IsChecked();
Expand Down Expand Up @@ -168,6 +172,15 @@ protected override void OnLoad(EventArgs e)
base.OnLoad(e);
_applyToSelected.Text = string.Format(UiStrings.ApplyToSelected, SelectedImages?.Count);

InitDisplayImage();
ImageWidth = DisplayImage!.Width;
ImageHeight = DisplayImage.Height;
InitTransform();
UpdatePreviewBox();
}

protected virtual void InitDisplayImage()
{
using var imageToRender = Image.GetClonedImage();
WorkingImage = imageToRender.Render();

Expand All @@ -184,13 +197,9 @@ protected override void OnLoad(EventArgs e)
}

DisplayImage = WorkingImage.Clone();
ImageWidth = DisplayImage.Width;
ImageHeight = DisplayImage.Height;
InitTransform();
UpdatePreviewBox();
}

private RectangleF GetScreenWorkingArea()
protected RectangleF GetScreenWorkingArea()
{
try
{
Expand All @@ -215,7 +224,7 @@ protected void UpdatePreviewBox()
_renderThrottle.RunAction();
}

private void Apply()
protected virtual void Apply()
{
IMemoryImage? firstImageThumb = null;
if (WorkingImage != null)
Expand Down
Loading

0 comments on commit 8716c6a

Please sign in to comment.