From 31efe63e6ddb72b2e3e67e195cdf7e89b37d921f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 27 Jul 2016 23:23:08 +1000 Subject: [PATCH] Add ColorBlindness Former-commit-id: 5ce61ca1e508ed598370b5817fa54fa6aaf7ecff Former-commit-id: b04c8676420dad90a6cb9515bdc8b9db67752576 Former-commit-id: 98da045e3b41b5369a5d8ef393b2b22b8c011665 --- .../Filters/ColorBlindness.cs | 96 +++++++++++++++++++ .../Filters/Options/ColorBlindness.cs | 53 ++++++++++ .../ColorBlindness/AchromatomalyProcessor.cs | 36 +++++++ .../ColorBlindness/AchromatopsiaProcessor.cs | 36 +++++++ .../ColorBlindness/DeuteranomalyProcessor.cs | 33 +++++++ .../ColorBlindness/DeuteranopiaProcessor.cs | 33 +++++++ .../ColorBlindness/ProtanomalyProcessor.cs | 33 +++++++ .../ColorBlindness/ProtanopiaProcessor.cs | 33 +++++++ .../ColorMatrix/ColorBlindness/README.md | 4 + .../ColorBlindness/TritanomalyProcessor.cs | 33 +++++++ .../ColorBlindness/TritanopiaProcessor.cs | 33 +++++++ .../ColorMatrix/ColorMatrixFilter.cs | 10 +- src/ImageProcessorCore/Image.cs | 6 +- src/ImageProcessorCore/Image/Image.cs | 4 +- src/ImageProcessorCore/Image/ImageBase.cs | 2 + .../Image/ImageExtensions.cs | 2 +- src/ImageProcessorCore/Samplers/Pad.cs | 2 +- .../Processors/Filters/ColorBlindnessTest.cs | 52 ++++++++++ 18 files changed, 492 insertions(+), 9 deletions(-) create mode 100644 src/ImageProcessorCore/Filters/ColorBlindness.cs create mode 100644 src/ImageProcessorCore/Filters/Options/ColorBlindness.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/README.md create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs create mode 100644 tests/ImageProcessorCore.Tests/Processors/Filters/ColorBlindnessTest.cs diff --git a/src/ImageProcessorCore/Filters/ColorBlindness.cs b/src/ImageProcessorCore/Filters/ColorBlindness.cs new file mode 100644 index 000000000..b5848a514 --- /dev/null +++ b/src/ImageProcessorCore/Filters/ColorBlindness.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// ------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessorCore +{ + using Processors; + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Applies the given colorblindness simulator to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The type of color blindness simulator to apply. + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image ColorBlindness(this Image source, ColorBlindness colorBlindness, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + return ColorBlindness(source, colorBlindness, source.Bounds, progressHandler); + } + + /// + /// Applies the given colorblindness simulator to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The type of color blindness simulator to apply. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image ColorBlindness(this Image source, ColorBlindness colorBlindness, Rectangle rectangle, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + IImageProcessor processor; + + switch (colorBlindness) + { + case ImageProcessorCore.ColorBlindness.Achromatomaly: + processor = new AchromatomalyProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Achromatopsia: + processor = new AchromatopsiaProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Deuteranomaly: + processor = new DeuteranomalyProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Deuteranopia: + processor = new DeuteranopiaProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Protanomaly: + processor = new ProtanomalyProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Protanopia: + processor = new ProtanopiaProcessor(); + break; + + case ImageProcessorCore.ColorBlindness.Tritanomaly: + processor = new TritanomalyProcessor(); + break; + + default: + processor = new TritanopiaProcessor(); + break; + } + + processor.OnProgress += progressHandler; + + try + { + return source.Process(rectangle, processor); + } + finally + { + processor.OnProgress -= progressHandler; + } + } + } +} diff --git a/src/ImageProcessorCore/Filters/Options/ColorBlindness.cs b/src/ImageProcessorCore/Filters/Options/ColorBlindness.cs new file mode 100644 index 000000000..6d7fe849b --- /dev/null +++ b/src/ImageProcessorCore/Filters/Options/ColorBlindness.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + /// + /// Enumerates the various types of color blindness. + /// + public enum ColorBlindness + { + /// + /// Partial color desensitivity. + /// + Achromatomaly, + + /// + /// Complete color desensitivity (Monochrome) + /// + Achromatopsia, + + /// + /// Green weak + /// + Deuteranomaly, + + /// + /// Green blind + /// + Deuteranopia, + + /// + /// Red weak + /// + Protanomaly, + + /// + /// Red blind + /// + Protanopia, + + /// + /// Blue weak + /// + Tritanomaly, + + /// + /// Blue blind + /// + Tritanopia + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs new file mode 100644 index 000000000..e8dd077eb --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Achromatomaly (Color desensitivity) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class AchromatomalyProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = .618f, + M12 = .163f, + M13 = .163f, + M21 = .320f, + M22 = .775f, + M23 = .320f, + M31 = .062f, + M32 = .062f, + M33 = .516f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs new file mode 100644 index 000000000..0e7f69a13 --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Achromatopsia (Monochrome) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class AchromatopsiaProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = .299f, + M12 = .299f, + M13 = .299f, + M21 = .587f, + M22 = .587f, + M23 = .587f, + M31 = .114f, + M32 = .114f, + M33 = .114f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs new file mode 100644 index 000000000..b7f48ea1d --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Deuteranomaly (Green-Weak) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class DeuteranomalyProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.8f, + M12 = 0.258f, + M21 = 0.2f, + M22 = 0.742f, + M23 = 0.142f, + M33 = 0.858f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs new file mode 100644 index 000000000..82f50d07f --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Deuteranopia (Green-Blind) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class DeuteranopiaProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.625f, + M12 = 0.7f, + M21 = 0.375f, + M22 = 0.3f, + M23 = 0.3f, + M33 = 0.7f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs new file mode 100644 index 000000000..32380cba1 --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Protanopia (Red-Weak) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class ProtanomalyProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.817f, + M12 = 0.333f, + M21 = 0.183f, + M22 = 0.667f, + M23 = 0.125f, + M33 = 0.875f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs new file mode 100644 index 000000000..891b6e06a --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Protanopia (Red-Blind) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class ProtanopiaProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.567f, + M12 = 0.558f, + M21 = 0.433f, + M22 = 0.442f, + M23 = 0.242f, + M33 = 0.758f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/README.md b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/README.md new file mode 100644 index 000000000..209f3b67b --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/README.md @@ -0,0 +1,4 @@ +Color blindness matrices adapted from and tested against: + +http://web.archive.org/web/20090413045433/http://nofunc.org/Color_Matrix_Library +http://www.color-blindness.com/coblis-color-blindness-simulator/ \ No newline at end of file diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs new file mode 100644 index 000000000..88e5fd5dc --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Tritanomaly (Blue-Weak) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class TritanomalyProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.967f, + M21 = 0.33f, + M22 = 0.733f, + M23 = 0.183f, + M32 = 0.267f, + M33 = 0.817f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs new file mode 100644 index 000000000..b708927a2 --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + + /// + /// Converts the colors of the image recreating Tritanopia (Blue-Blind) color blindness. + /// + /// The pixel format. + /// The packed format. long, float. + public class TritanopiaProcessor : ColorMatrixFilter + where T : IPackedVector + where TP : struct + { + /// + public override Matrix4x4 Matrix => new Matrix4x4() + { + M11 = 0.95f, + M21 = 0.05f, + M22 = 0.433f, + M23 = 0.475f, + M32 = 0.567f, + M33 = 0.525f + }; + + /// + public override bool Compand => false; + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorMatrixFilter.cs b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorMatrixFilter.cs index 0b7600fbc..cbab86a1c 100644 --- a/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorMatrixFilter.cs +++ b/src/ImageProcessorCore/Filters/Processors/ColorMatrix/ColorMatrixFilter.cs @@ -9,8 +9,10 @@ namespace ImageProcessorCore.Processors using System.Threading.Tasks; /// - /// The color matrix filter. + /// The color matrix filter. Inherit from this class to perform operation involving color matrices. /// + /// The pixel format. + /// The packed format. long, float. public abstract class ColorMatrixFilter : ImageProcessor, IColorMatrixFilter where T : IPackedVector where TP : struct @@ -63,11 +65,11 @@ namespace ImageProcessorCore.Processors // color = Color.Expand(color); //} - Vector4 transformed = Vector4.Transform(color.ToVector4(), matrix); - //Vector3 transformed = Vector3.Transform(color.ToVector3(), matrix); + Vector4 vector = color.ToVector4(); + Vector3 transformed = Vector3.Transform(new Vector3(vector.X, vector.Y, vector.Z), matrix); //return compand ? Color.Compress(new Color(transformed, color.A)) : new Color(transformed, color.A); T packed = default(T); - packed.PackVector(transformed); + packed.PackVector(new Vector4(transformed.X, transformed.Y, transformed.Z, vector.W)); return packed; } } diff --git a/src/ImageProcessorCore/Image.cs b/src/ImageProcessorCore/Image.cs index 9ab3e73ba..e4e2186b9 100644 --- a/src/ImageProcessorCore/Image.cs +++ b/src/ImageProcessorCore/Image.cs @@ -5,12 +5,14 @@ namespace ImageProcessorCore { - using System; + using System.Diagnostics; using System.IO; /// - /// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha. + /// Represents an image. Each pixel is a made up four 8-bit components red, green, blue, and alpha + /// packed into a single unsigned integer value. /// + [DebuggerDisplay("Image: {Width}x{Height}")] public class Image : Image { /// diff --git a/src/ImageProcessorCore/Image/Image.cs b/src/ImageProcessorCore/Image/Image.cs index 0b716f0dd..a53c87c5f 100644 --- a/src/ImageProcessorCore/Image/Image.cs +++ b/src/ImageProcessorCore/Image/Image.cs @@ -13,12 +13,14 @@ namespace ImageProcessorCore using System.Linq; using Formats; + using System.Diagnostics; /// /// Encapsulates an image, which consists of the pixel data for a graphics image and its attributes. /// /// The pixel format. /// The packed format. long, float. + [DebuggerDisplay("Image: {Width}x{Height}")] public class Image : ImageBase where T : IPackedVector where TP : struct @@ -75,6 +77,7 @@ namespace ImageProcessorCore /// The other image, where the clone should be made from. /// is null. public Image(Image other) + : base(other) { foreach (ImageFrame frame in other.Frames) { @@ -84,7 +87,6 @@ namespace ImageProcessorCore } } - this.Quality = other.Quality; this.RepeatCount = other.RepeatCount; this.HorizontalResolution = other.HorizontalResolution; this.VerticalResolution = other.VerticalResolution; diff --git a/src/ImageProcessorCore/Image/ImageBase.cs b/src/ImageProcessorCore/Image/ImageBase.cs index 3a7a8f99a..adbe70d1b 100644 --- a/src/ImageProcessorCore/Image/ImageBase.cs +++ b/src/ImageProcessorCore/Image/ImageBase.cs @@ -6,6 +6,7 @@ namespace ImageProcessorCore { using System; + using System.Diagnostics; /// /// The base class of all images. Encapsulates the basic properties and methods required to manipulate @@ -13,6 +14,7 @@ namespace ImageProcessorCore /// /// The pixel format. /// The packed format. long, float. + [DebuggerDisplay("Image: {Width}x{Height}")] public abstract class ImageBase : IImageBase where T : IPackedVector where TP : struct diff --git a/src/ImageProcessorCore/Image/ImageExtensions.cs b/src/ImageProcessorCore/Image/ImageExtensions.cs index c97b6dfa6..0b1bac55e 100644 --- a/src/ImageProcessorCore/Image/ImageExtensions.cs +++ b/src/ImageProcessorCore/Image/ImageExtensions.cs @@ -174,7 +174,7 @@ namespace ImageProcessorCore : new Image { // Several properties require copying - // TODO: Check why we need to set these? + FrameDelay = source.FrameDelay, Quality = source.Quality, HorizontalResolution = source.HorizontalResolution, VerticalResolution = source.VerticalResolution, diff --git a/src/ImageProcessorCore/Samplers/Pad.cs b/src/ImageProcessorCore/Samplers/Pad.cs index 2bb6acc33..0b1b959b0 100644 --- a/src/ImageProcessorCore/Samplers/Pad.cs +++ b/src/ImageProcessorCore/Samplers/Pad.cs @@ -21,7 +21,7 @@ namespace ImageProcessorCore /// The new width. /// The new height. /// A delegate which is called as progress is made processing the image. - /// The . + /// The . public static Image Pad(this Image source, int width, int height, ProgressEventHandler progressHandler = null) where T : IPackedVector where TP : struct diff --git a/tests/ImageProcessorCore.Tests/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageProcessorCore.Tests/Processors/Filters/ColorBlindnessTest.cs new file mode 100644 index 000000000..5b2300885 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Processors/Filters/ColorBlindnessTest.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System.IO; + + using Xunit; + + public class ColorBlindnessTest : FileTestBase + { + public static readonly TheoryData ColorBlindnessFilters + = new TheoryData + { + ColorBlindness.Achromatomaly, + ColorBlindness.Achromatopsia, + ColorBlindness.Deuteranomaly, + ColorBlindness.Deuteranopia, + ColorBlindness.Protanomaly, + ColorBlindness.Protanopia, + ColorBlindness.Tritanomaly, + ColorBlindness.Tritanopia + }; + + [Theory] + [MemberData("ColorBlindnessFilters")] + public void ImageShouldApplyColorBlindnessFilter(ColorBlindness colorBlindness) + { + const string path = "TestOutput/ColorBlindness"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + string filename = Path.GetFileNameWithoutExtension(file) + "-" + colorBlindness + Path.GetExtension(file); + Image image = new Image(stream); + using (FileStream output = File.OpenWrite($"{path}/{filename}")) + { + image.ColorBlindness(colorBlindness) + .Save(output); + } + } + } + } + } +} \ No newline at end of file