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