using System; using System.Drawing; namespace Blacklight.Core.UnitTests { public class MipChainTests : UnitTestSharp.TestFixture { public static byte[] Pixel(int red, int green, int blue, int alpha) { return new[] { (byte)red, (byte)green, (byte)blue, (byte)alpha }; } public static DecodedImage Image( Size size, Func pixelAt, DecodedImage.AlphaMode alpha = DecodedImage.AlphaMode.Straight, bool isLinear = false) { var rgba = new byte[size.Width * size.Height * DecodedImage.BytesPerPixel]; for (int y = 0; y < size.Height; ++y) { for (int x = 0; x < size.Width; ++x) { Array.Copy(pixelAt(x, y), 0, rgba, (y * size.Width + x) * DecodedImage.BytesPerPixel, DecodedImage.BytesPerPixel); } } return new DecodedImage(rgba, size, alpha, isLinear); } public static DecodedImage Solid( Size size, byte[] pixel, DecodedImage.AlphaMode alpha = DecodedImage.AlphaMode.Straight, bool isLinear = false) { return Image(size, (x, y) => pixel, alpha, isLinear); } public static byte[] PixelAt(DecodedImage image, int x, int y) { var returnMe = new byte[DecodedImage.BytesPerPixel]; Array.Copy(image.Rgba, (y * image.Width + x) * DecodedImage.BytesPerPixel, returnMe, 0, DecodedImage.BytesPerPixel); return returnMe; } public class LevelCountForTests : UnitTestSharp.TestFixture { public void OnePixelCell_HasOneLevel() { CheckEqual(1, MipChain.LevelCountFor(new Size(1, 1))); } public void PowerOfTwoCell_HasALevelPerHalvingDownToOnePixel() { CheckEqual(5, MipChain.LevelCountFor(new Size(16, 16))); } public void NonSquareCell_StopsAtTheSmallerSide() { CheckEqual(3, MipChain.LevelCountFor(new Size(8, 4))); CheckEqual(3, MipChain.LevelCountFor(new Size(4, 8))); } public void OddCell_RoundsTheHalvingDown() { CheckEqual(4, MipChain.LevelCountFor(new Size(12, 12))); CheckEqual(2, MipChain.LevelCountFor(new Size(3, 3))); } public void ZeroWidth_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); MipChain.LevelCountFor(new Size(0, 4)); } public void ZeroHeight_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); MipChain.LevelCountFor(new Size(4, 0)); } } public class BuildTilingTests : UnitTestSharp.TestFixture { public static DecodedImage LinearRow(params int[] values) { return Image(new Size(values.Length, 1), (x, y) => Pixel(values[x], 0, 0, 255), isLinear: true); } public void FirstLevelIsTheGivenImage() { DecodedImage image = Solid(new Size(4, 4), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.BuildTiling(image); CheckTrue(ReferenceEquals(image, levels[0])); } public void OnePixelImage_HasOnlyTheGivenImage() { CheckEqual(1, MipChain.BuildTiling(Solid(new Size(1, 1), Pixel(1, 2, 3, 255))).Length); } public void PowerOfTwoImage_HalvesDownToOnePixel() { DecodedImage[] levels = MipChain.BuildTiling(Solid(new Size(8, 8), Pixel(1, 2, 3, 255))); CheckEqual(4, levels.Length); CheckEqual(new Size(8, 8), levels[0].Size); CheckEqual(new Size(4, 4), levels[1].Size); CheckEqual(new Size(2, 2), levels[2].Size); CheckEqual(new Size(1, 1), levels[3].Size); } public void OddImage_HalvesRoundingDown() { DecodedImage[] levels = MipChain.BuildTiling(Solid(new Size(7, 5), Pixel(1, 2, 3, 255))); CheckEqual(3, levels.Length); CheckEqual(new Size(3, 2), levels[1].Size); CheckEqual(new Size(1, 1), levels[2].Size); } public void NonSquareImage_StopsShrinkingASideAtOnePixel() { DecodedImage[] levels = MipChain.BuildTiling(Solid(new Size(8, 2), Pixel(1, 2, 3, 255))); CheckEqual(4, levels.Length); CheckEqual(new Size(4, 1), levels[1].Size); CheckEqual(new Size(2, 1), levels[2].Size); CheckEqual(new Size(1, 1), levels[3].Size); } public void FlatImageOfAnySize_KeepsItsColorAtEveryLevel() { byte[] color = Pixel(10, 130, 250, 200); DecodedImage[] levels = MipChain.BuildTiling(Solid(new Size(13, 9), color)); foreach (DecodedImage level in levels) { for (int y = 0; y < level.Height; ++y) { for (int x = 0; x < level.Width; ++x) { CheckEqual(color, PixelAt(level, x, y)); } } } } public void PowerOfTwoImage_AveragesEachTwoByTwoBlock() { DecodedImage image = Image(new Size(4, 2), (x, y) => { int value = new[] { 0, 40, 100, 200 }[x] + y * 20; return Pixel(value, 0, 0, 255); }, isLinear: true); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(Pixel(30, 0, 0, 255), PixelAt(levels[1], 0, 0)); CheckEqual(Pixel(160, 0, 0, 255), PixelAt(levels[1], 1, 0)); } public void ThreePixelsToOne_AveragesAllThreeEqually() { DecodedImage[] levels = MipChain.BuildTiling(LinearRow(30, 60, 90)); CheckEqual(Pixel(60, 0, 0, 255), PixelAt(levels[1], 0, 0)); } public void FivePixelsToTwo_WeighsEachByHowMuchOfItTheOutputCovers() { DecodedImage[] levels = MipChain.BuildTiling(LinearRow(10, 20, 30, 40, 50)); CheckEqual(Pixel(18, 0, 0, 255), PixelAt(levels[1], 0, 0)); CheckEqual(Pixel(42, 0, 0, 255), PixelAt(levels[1], 1, 0)); } public void ColumnOfOnePixelWidth_AveragesOnlyDown() { DecodedImage image = Image(new Size(1, 4), (x, y) => Pixel(y < 2 ? 0 : 100, 0, 0, 255), isLinear: true); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(Pixel(0, 0, 0, 255), PixelAt(levels[1], 0, 0)); CheckEqual(Pixel(100, 0, 0, 255), PixelAt(levels[1], 0, 1)); } public void SrgbImage_AveragesInLinearSpace() { DecodedImage image = Image(new Size(2, 1), (x, y) => x == 0 ? Pixel(0, 0, 0, 255) : Pixel(255, 255, 255, 255)); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(Pixel(188, 188, 188, 255), PixelAt(levels[1], 0, 0)); } public void SeeThroughPixels_DontTintTheAverage() { DecodedImage image = Image(new Size(2, 1), (x, y) => x == 0 ? Pixel(255, 0, 0, 255) : Pixel(0, 255, 0, 0)); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(Pixel(255, 0, 0, 128), PixelAt(levels[1], 0, 0)); } public void PremultipliedImage_AveragesColorWithoutWeightingByAlpha() { DecodedImage image = Image(new Size(2, 1), (x, y) => x == 0 ? Pixel(200, 0, 0, 200) : Pixel(0, 0, 0, 0), DecodedImage.AlphaMode.Premultiplied, isLinear: true); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(Pixel(100, 0, 0, 100), PixelAt(levels[1], 0, 0)); } public void KeepsTheAlphaModeAndColorSpaceOfTheImage() { DecodedImage image = Solid(new Size(2, 2), Pixel(1, 2, 3, 4), DecodedImage.AlphaMode.Premultiplied, isLinear: true); DecodedImage[] levels = MipChain.BuildTiling(image); CheckEqual(DecodedImage.AlphaMode.Premultiplied, levels[1].Alpha); CheckTrue(levels[1].IsLinear); } public void NullImage_Throws() { CheckThrow(typeof(ArgumentNullException)); MipChain.BuildTiling(null); } } public class BuildTests : UnitTestSharp.TestFixture { public void FirstLevelIsTheGivenImage() { DecodedImage image = Solid(new Size(4, 4), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build(image, new Size(4, 4), new Size(4, 4)); CheckTrue(ReferenceEquals(image, levels[0])); } public void OnePixelCell_HasOnlyTheGivenImage() { DecodedImage image = Solid(new Size(3, 2), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build(image, new Size(1, 1), new Size(1, 1)); CheckEqual(1, levels.Length); } public void EachLevelHalvesTheStrideOfEveryCell() { DecodedImage image = Solid(new Size(16, 8), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build(image, new Size(4, 4), new Size(8, 8)); CheckEqual(3, levels.Length); CheckEqual(new Size(16, 8), levels[0].Size); CheckEqual(new Size(8, 4), levels[1].Size); CheckEqual(new Size(4, 2), levels[2].Size); } public void NonSquareCell_HalvesEachSideIndependently() { DecodedImage image = Solid(new Size(8, 4), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build(image, new Size(8, 4), new Size(8, 4)); CheckEqual(3, levels.Length); CheckEqual(new Size(4, 2), levels[1].Size); CheckEqual(new Size(2, 1), levels[2].Size); } public void FlatCell_KeepsItsColorAtEveryLevel() { byte[] color = Pixel(10, 130, 250, 200); DecodedImage image = Solid(new Size(16, 16), color); DecodedImage[] levels = MipChain.Build(image, new Size(16, 16), new Size(16, 16)); foreach (DecodedImage level in levels) { for (int y = 0; y < level.Height; ++y) { for (int x = 0; x < level.Width; ++x) { CheckEqual(color, PixelAt(level, x, y)); } } } } public void FlatPremultipliedSrgbCell_KeepsItsColorAtEveryLevel() { byte[] color = Pixel(100, 50, 20, 128); DecodedImage image = Solid(new Size(4, 4), color, DecodedImage.AlphaMode.Premultiplied); DecodedImage[] levels = MipChain.Build(image, new Size(4, 4), new Size(4, 4)); CheckEqual(color, PixelAt(levels[2], 0, 0)); } public void LinearImage_AveragesTheStoredValues() { DecodedImage image = Image(new Size(2, 2), (x, y) => x == 0 ? Pixel(0, 0, 0, 255) : Pixel(255, 255, 255, 255), isLinear: true); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(128, 128, 128, 255), PixelAt(levels[1], 0, 0)); } public void SrgbImage_AveragesInLinearSpace() { DecodedImage image = Image(new Size(2, 2), (x, y) => x == 0 ? Pixel(0, 0, 0, 255) : Pixel(255, 255, 255, 255)); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(188, 188, 188, 255), PixelAt(levels[1], 0, 0)); } public void SeeThroughPixels_DontTintTheAverage() { DecodedImage image = Image(new Size(2, 2), (x, y) => x == 0 ? Pixel(255, 0, 0, 255) : Pixel(0, 255, 0, 0)); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(255, 0, 0, 128), PixelAt(levels[1], 0, 0)); } public void FullyTransparentBlock_HasNoColor() { DecodedImage image = Solid(new Size(2, 2), Pixel(90, 80, 70, 0)); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 0, 0)); } public void PremultipliedImage_AveragesColorWithoutWeightingByAlpha() { DecodedImage image = Image(new Size(2, 2), (x, y) => x == 0 ? Pixel(200, 0, 0, 200) : Pixel(0, 0, 0, 0), DecodedImage.AlphaMode.Premultiplied, isLinear: true); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(100, 0, 0, 100), PixelAt(levels[1], 0, 0)); } public void NeighboringCellsDontBleedIntoEachOther() { DecodedImage image = Image(new Size(4, 4), (x, y) => { if (x < 2) { return y < 2 ? Pixel(255, 0, 0, 255) : Pixel(0, 255, 0, 255); } return y < 2 ? Pixel(0, 0, 255, 255) : Pixel(255, 255, 255, 255); }); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(Pixel(255, 0, 0, 255), PixelAt(levels[1], 0, 0)); CheckEqual(Pixel(0, 0, 255, 255), PixelAt(levels[1], 1, 0)); CheckEqual(Pixel(0, 255, 0, 255), PixelAt(levels[1], 0, 1)); CheckEqual(Pixel(255, 255, 255, 255), PixelAt(levels[1], 1, 1)); } public void OddCell_AveragesOnlyThePixelsItHas() { byte[] color = Pixel(10, 20, 30, 255); DecodedImage image = Image(new Size(4, 4), (x, y) => x < 3 && y < 3 ? color : Pixel(0, 0, 0, 0)); DecodedImage[] levels = MipChain.Build(image, new Size(3, 3), new Size(4, 4)); CheckEqual(color, PixelAt(levels[1], 1, 1)); CheckEqual(color, PixelAt(levels[1], 1, 0)); } public void SpaceBeyondTheCell_StaysEmpty() { DecodedImage image = Image(new Size(8, 8), (x, y) => x < 5 && y < 5 ? Pixel(10, 20, 30, 255) : Pixel(0, 0, 0, 0)); DecodedImage[] levels = MipChain.Build(image, new Size(5, 5), new Size(8, 8)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 3, 0)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 0, 3)); CheckEqual(Pixel(10, 20, 30, 255), PixelAt(levels[1], 2, 2)); } public void KeepsTheAlphaModeAndColorSpaceOfTheImage() { DecodedImage image = Solid(new Size(2, 2), Pixel(1, 2, 3, 4), DecodedImage.AlphaMode.Premultiplied, isLinear: true); DecodedImage[] levels = MipChain.Build(image, new Size(2, 2), new Size(2, 2)); CheckEqual(DecodedImage.AlphaMode.Premultiplied, levels[1].Alpha); CheckTrue(levels[1].IsLinear); } public void NullImage_Throws() { CheckThrow(typeof(ArgumentNullException)); MipChain.Build(null, new Size(2, 2), new Size(2, 2)); } public void StrideSmallerThanTheCell_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(4, 4), Pixel(0, 0, 0, 0)), new Size(4, 4), new Size(2, 4)); } public void ImageThatIsntWholeStrides_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(6, 4), Pixel(0, 0, 0, 0)), new Size(4, 4), new Size(4, 4)); } public void StrideThatDoesntHalveForEveryLevel_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(6, 6), Pixel(0, 0, 0, 0)), new Size(4, 4), new Size(6, 6)); } public void ZeroSizeCell_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); MipChain.Build(Solid(new Size(4, 4), Pixel(0, 0, 0, 0)), new Size(0, 4), new Size(4, 4)); } public void Frames_FirstLevelIsTheGivenImage() { DecodedImage image = Solid(new Size(8, 4), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build( image, new[] { new Rectangle(0, 0, 4, 4), new Rectangle(4, 0, 4, 4) }); CheckTrue(ReferenceEquals(image, levels[0])); } public void Frames_EachHasAsManyLevelsAsItsOwnSizeAllows() { byte[] big = Pixel(255, 0, 0, 255); byte[] small = Pixel(0, 0, 255, 255); DecodedImage image = Image(new Size(16, 8), (x, y) => { if (x < 8) { return big; } return x < 12 && y < 4 ? small : Pixel(0, 0, 0, 0); }); DecodedImage[] levels = MipChain.Build( image, new[] { new Rectangle(0, 0, 8, 8), new Rectangle(8, 0, 4, 4) }); CheckEqual(4, levels.Length); CheckEqual(new Size(2, 1), levels[3].Size); CheckEqual(big, PixelAt(levels[3], 0, 0)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[3], 1, 0)); CheckEqual(small, PixelAt(levels[2], 2, 0)); } public void Frames_OfDifferentSizesDontBleedIntoEachOther() { byte[] red = Pixel(255, 0, 0, 255); byte[] blue = Pixel(0, 0, 255, 255); DecodedImage image = Image(new Size(8, 4), (x, y) => { if (x < 4) { return red; } return x < 6 && y < 2 ? blue : Pixel(0, 0, 0, 0); }); DecodedImage[] levels = MipChain.Build( image, new[] { new Rectangle(0, 0, 4, 4), new Rectangle(4, 0, 2, 2) }); CheckEqual(red, PixelAt(levels[1], 1, 1)); CheckEqual(blue, PixelAt(levels[1], 2, 0)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 3, 0)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 2, 1)); } public void Frames_ThatDontStartAtTheCornerAreAveragedFromWhereTheyAre() { byte[] color = Pixel(10, 20, 30, 255); DecodedImage image = Image(new Size(8, 4), (x, y) => x >= 4 && x < 7 && y < 3 ? color : Pixel(0, 0, 0, 0)); DecodedImage[] levels = MipChain.Build(image, new[] { new Rectangle(4, 0, 3, 3) }); CheckEqual(color, PixelAt(levels[1], 2, 0)); CheckEqual(color, PixelAt(levels[1], 3, 1)); CheckEqual(Pixel(0, 0, 0, 0), PixelAt(levels[1], 1, 0)); } public void Frames_ThatTouchButDontOverlap_AreAllowed() { DecodedImage image = Solid(new Size(8, 4), Pixel(1, 2, 3, 255)); DecodedImage[] levels = MipChain.Build( image, new[] { new Rectangle(0, 0, 4, 4), new Rectangle(4, 0, 4, 4) }); CheckEqual(3, levels.Length); } public void Frames_NullImage_Throws() { CheckThrow(typeof(ArgumentNullException)); MipChain.Build(null, new[] { new Rectangle(0, 0, 2, 2) }); } public void Frames_Null_Throws() { CheckThrow(typeof(ArgumentNullException)); MipChain.Build(Solid(new Size(2, 2), Pixel(0, 0, 0, 0)), (Rectangle[])null); } public void Frames_None_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(2, 2), Pixel(0, 0, 0, 0)), new Rectangle[0]); } public void Frames_ZeroSize_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); MipChain.Build(Solid(new Size(4, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(0, 0, 0, 4) }); } public void Frames_PastTheImagesEdge_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(4, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(2, 0, 4, 4) }); } public void Frames_BeforeTheImagesEdge_Throws() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(4, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(-2, 0, 2, 2) }); } public void Frames_ThatDontStartOnAWholePixelAtEveryLevel_Throw() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(12, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(2, 0, 4, 4) }); } public void Frames_ThatOverlap_Throw() { CheckThrow(typeof(ArgumentException)); MipChain.Build( Solid(new Size(8, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(0, 0, 4, 4), new Rectangle(0, 0, 2, 2) }); } public void Frames_InAnImageThatDoesntHalveForEveryLevel_Throw() { CheckThrow(typeof(ArgumentException)); MipChain.Build(Solid(new Size(6, 4), Pixel(0, 0, 0, 0)), new[] { new Rectangle(0, 0, 4, 4) }); } } } }