diff --git a/NAPS2.Images/Bitwise/BitwisePrimitives.cs b/NAPS2.Images/Bitwise/BitwisePrimitives.cs
index 650bfd6978..f4e35bf24f 100644
--- a/NAPS2.Images/Bitwise/BitwisePrimitives.cs
+++ b/NAPS2.Images/Bitwise/BitwisePrimitives.cs
@@ -34,6 +34,7 @@ public static unsafe void Fill(BitwiseImageData data, byte value, int partStart
{
if (partStart == -1) partStart = 0;
if (partEnd == -1) partEnd = data.h;
+ if (data.invertColorSpace) value = (byte) ~value;
var longCount = data.stride / 8;
var remainingStart = longCount * 8;
diff --git a/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs b/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs
index db9854b7e2..bdbed0a960 100644
--- a/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs
+++ b/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs
@@ -59,8 +59,14 @@ protected override void ValidateCore(BitwiseImageData src, BitwiseImageData dst)
protected override void PerformCore(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd)
{
if (src.BitLayout == dst.BitLayout &&
- (src.bytesPerPixel > 0 || (SourceXOffset % 8 == 0 && DestXOffset % 8 == 0)) &&
- DestChannel == ColorChannel.All)
+ DestChannel == ColorChannel.All &&
+ (src.bytesPerPixel > 0 ||
+ // For Black & White images, to use the fast copy path, we must have that:
+ // 1. The offsets are to whole bytes
+ // 2a. Either we copy whole bytes, or
+ // 2b. We end at the far-right side of the destination (so any excess bits copied will be ignored)
+ (SourceXOffset % 8 == 0 && DestXOffset % 8 == 0 &&
+ (src.w % 8 == 0 || src.w + DestXOffset == dst.w))))
{
FastCopy(src, dst, partStart, partEnd);
}
@@ -254,6 +260,7 @@ private unsafe void UnalignedBitCopy(BitwiseImageData src, BitwiseImageData dst,
var dstPixelIndex = j + DestXOffset;
var dstPtr = dstRow + dstPixelIndex / 8;
var dstByte = *dstPtr;
+ dstByte &= (byte) ~(1 << (7 - dstPixelIndex % 8));
dstByte |= (byte) (bit << (7 - dstPixelIndex % 8));
*dstPtr = dstByte;
}
diff --git a/NAPS2.Images/Bitwise/FillColorImageOp.cs b/NAPS2.Images/Bitwise/FillColorImageOp.cs
index 48baf92ff0..027402e469 100644
--- a/NAPS2.Images/Bitwise/FillColorImageOp.cs
+++ b/NAPS2.Images/Bitwise/FillColorImageOp.cs
@@ -15,12 +15,20 @@ public FillColorImageOp(byte r, byte g, byte b, byte a)
_a = a;
}
+ protected override void ValidateCore(BitwiseImageData data)
+ {
+ }
+
protected override void PerformCore(BitwiseImageData data, int partStart, int partEnd)
{
if (data.bytesPerPixel is 1 or 3 or 4)
{
PerformRgba(data, partStart, partEnd);
}
+ else if (data.bitsPerPixel == 1 && (_r, _g, _b, _a) is (0, 0, 0, 255) or (255, 255, 255, 255))
+ {
+ PerformBw(data, partStart, partEnd);
+ }
else
{
throw new InvalidOperationException("Unsupported pixel format");
@@ -54,4 +62,10 @@ private unsafe void PerformRgba(BitwiseImageData data, int partStart, int partEn
}
}
}
+
+ private void PerformBw(BitwiseImageData data, int partStart, int partEnd)
+ {
+ byte fill = (byte) (_r == 255 ? 0xFF : 0x00);
+ BitwisePrimitives.Fill(data, fill, partStart, partEnd);
+ }
}
\ No newline at end of file
diff --git a/NAPS2.Sdk.Tests/ImageResources.Designer.cs b/NAPS2.Sdk.Tests/ImageResources.Designer.cs
index d61065dd35..306aa8becc 100644
--- a/NAPS2.Sdk.Tests/ImageResources.Designer.cs
+++ b/NAPS2.Sdk.Tests/ImageResources.Designer.cs
@@ -99,6 +99,16 @@ internal static byte[] bw_alternating {
}
}
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] cat {
+ get {
+ object obj = ResourceManager.GetObject("cat", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Byte[].
///
@@ -299,6 +309,26 @@ internal static byte[] dog_c_p300 {
}
}
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] dog_cat_combined {
+ get {
+ object obj = ResourceManager.GetObject("dog_cat_combined", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] dog_cat_combined_bw {
+ get {
+ object obj = ResourceManager.GetObject("dog_cat_combined_bw", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Byte[].
///
diff --git a/NAPS2.Sdk.Tests/ImageResources.resx b/NAPS2.Sdk.Tests/ImageResources.resx
index e78800f40e..430f3d067e 100644
--- a/NAPS2.Sdk.Tests/ImageResources.resx
+++ b/NAPS2.Sdk.Tests/ImageResources.resx
@@ -202,6 +202,9 @@
Resources\skewed_bw.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Resources\cat.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
Resources\stock-cat.jpeg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
@@ -304,4 +307,10 @@
Resources\dog_exif.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Resources\dog_cat_combined.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Resources\dog_cat_combined_bw.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
\ No newline at end of file
diff --git a/NAPS2.Sdk.Tests/Images/TransformTests.cs b/NAPS2.Sdk.Tests/Images/TransformTests.cs
index 9cd32f37f5..860b3ff6fb 100644
--- a/NAPS2.Sdk.Tests/Images/TransformTests.cs
+++ b/NAPS2.Sdk.Tests/Images/TransformTests.cs
@@ -464,6 +464,32 @@ public void Thumbnail()
AssertOwnership(original, transformed);
}
+ [Fact]
+ public void Combine()
+ {
+ var first = LoadImage(ImageResources.dog);
+ var second = LoadImage(ImageResources.cat);
+ var expected = LoadImage(ImageResources.dog_cat_combined);
+
+ var transformed = MoreImageTransforms.Combine(first, second, CombineOrientation.Vertical);
+ Assert.Equal(ImagePixelFormat.RGB24, transformed.PixelFormat);
+
+ ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD);
+ }
+
+ [Fact]
+ public void CombineBlackAndWhite()
+ {
+ var first = LoadImage(ImageResources.dog).PerformTransform(new BlackWhiteTransform());
+ var second = LoadImage(ImageResources.cat).PerformTransform(new BlackWhiteTransform());
+ var expected = LoadImage(ImageResources.dog_cat_combined_bw).PerformTransform(new BlackWhiteTransform());
+
+ var transformed = MoreImageTransforms.Combine(first, second, CombineOrientation.Vertical);
+ Assert.Equal(ImagePixelFormat.BW1, transformed.UpdateLogicalPixelFormat());
+
+ ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD);
+ }
+
private void AssertOwnership(IMemoryImage original, IMemoryImage transformed)
{
// The contract for a transform is that either it returns the original image or it disposes the original and
diff --git a/NAPS2.Sdk.Tests/Resources/cat.jpg b/NAPS2.Sdk.Tests/Resources/cat.jpg
new file mode 100644
index 0000000000..ec7d3f404d
Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/cat.jpg differ
diff --git a/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg b/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg
new file mode 100644
index 0000000000..980b8897c4
Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg differ
diff --git a/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg b/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg
new file mode 100644
index 0000000000..9f1007a5a7
Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg differ