csharp/0xC0000054/pdn-gmic/src/GmicPipeServer.cs

GmicPipeServer.cs
/*
*  This file is part of pdn-gmic, a Paint.NET Effect that
*  that provides integration with G'MIC-Qt.
*
*  Copyright (C) 2018, 2019, 2020, 2021 Nicholas Hayes
*
*  pdn-gmic is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  pdn-gmic is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program.  If not, see .
*
*/

using PaintDotNet;
using PaintDotNet.IO;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO.MemoryMappedFiles;
using System.IO.Pipes;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;

namespace GmicEffectPlugin
{
    internal sealed clast GmicPipeServer : IDisposable
    {
#pragma warning disable IDE0032 // Use auto property
        private readonly List layers;
        private readonly List memoryMappedFiles;
        private NamedPipeServerStream server;
        private bool disposed;

        private readonly string pipeName;
        private readonly string fullPipeName;
        private readonly SynchronizationContext synchronizationContext;
        private readonly SendOrPostCallback outputImageCallback;
#pragma warning restore IDE0032 // Use auto property

        private static readonly RectangleF WholeImageCropRect = new RectangleF(0.0f, 0.0f, 1.0f, 1.0f);

        /// 
        /// Initializes a new instance of the  clast.
        /// 
        public GmicPipeServer() : this(null)
        {
        }

        /// 
        /// Initializes a new instance of the  clast.
        /// 
        /// The synchronization context.
        public GmicPipeServer(SynchronizationContext synchronizationContext)
        {
            pipeName = "PDN_GMIC" + Guid.NewGuid().ToString();
            fullPipeName = @"\\.\pipe\" + pipeName;
            this.synchronizationContext = synchronizationContext;
            outputImageCallback = new SendOrPostCallback(OutputImageChangedCallback);
            layers = new List();
            memoryMappedFiles = new List();
            disposed = false;
        }

        public string FullPipeName => fullPipeName;

        public OutputImageState OutputImageState { get; private set; }

        public event EventHandler OutputImageChanged;

        private enum InputMode
        {
            NoInput = 0,
            ActiveLayer,
            AllLayers,
            ActiveAndBelow,
            ActiveAndAbove,
            AllVisibleLayers,
            AllHiddenLayers,
            AllVisibleLayersDescending,
            AllHiddenLayersDescending
        }

        private enum OutputMode
        {
            InPlace = 0,
            NewLayers,
            NewActiveLayers,
            NewImage
        }

        /// 
        /// Adds the layers.
        /// 
        /// The collection.
        ///  is null.
        /// The clast has been disposed.
        public void AddLayers(IEnumerable collection)
        {
            VerifyNotDisposed();

            layers.AddRange(collection);
        }

        /// 
        /// Performs application-defined tasks astociated with freeing, releasing, or resetting unmanaged resources.
        /// 
        public void Dispose()
        {
            if (!disposed)
            {
                disposed = true;

                for (int i = 0; i < layers.Count; i++)
                {
                    layers[i].Dispose();
                }

                for (int i = 0; i < memoryMappedFiles.Count; i++)
                {
                    memoryMappedFiles[i].Dispose();
                }

                if (OutputImageState != null)
                {
                    OutputImageState.Dispose();
                    OutputImageState = null;
                }

                if (server != null)
                {
                    server.Dispose();
                    server = null;
                }
            }
        }

        /// 
        /// Starts the server.
        /// 
        /// Must call AddLayers with at least one layer before calling Start.
        /// The clast has been disposed.
        public void Start()
        {
            VerifyNotDisposed();
            if (layers.Count == 0)
            {
                throw new InvalidOperationException("Must call AddLayers with at least one layer before calling Start.");
            }

            server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            server.BeginWaitForConnection(WaitForConnectionCallback, null);
        }

        private void WaitForConnectionCallback(IAsyncResult result)
        {
            if (server == null)
            {
                return;
            }

            try
            {
                server.EndWaitForConnection(result);
            }
            catch (ObjectDisposedException)
            {
                return;
            }

            byte[] replySizeBuffer = new byte[sizeof(int)];
            server.ProperRead(replySizeBuffer, 0, replySizeBuffer.Length);

            int messageLength = BitConverter.ToInt32(replySizeBuffer, 0);

            byte[] messageBytes = new byte[messageLength];

            server.ProperRead(messageBytes, 0, messageLength);

            List parameters = DecodeMessageBuffer(messageBytes);

            if (!TryGetValue(parameters[0], "command=", out string command))
            {
                throw new InvalidOperationException("The first item must be a command.");
            }

            if (command.Equals("gmic_qt_get_max_layer_size", StringComparison.Ordinal))
            {
                if (!TryGetValue(parameters[1], "mode=", out string mode))
                {
                    throw new InvalidOperationException("The second item must be the input mode.");
                }

                InputMode inputMode = ParseInputMode(mode);

#if DEBUG
                System.Diagnostics.Debug.WriteLine("'gmic_qt_get_max_layer_size' received. mode=" + inputMode.ToString());
#endif
                string reply = GetMaxLayerSize(inputMode);

                SendMessage(server, reply);
            }
            else if (command.Equals("gmic_qt_get_cropped_images", StringComparison.Ordinal))
            {
                if (!TryGetValue(parameters[1], "mode=", out string mode))
                {
                    throw new InvalidOperationException("The second item must be the input mode.");
                }

                if (!TryGetValue(parameters[2], "croprect=", out string packedCropRect))
                {
                    throw new InvalidOperationException("The third item must be the crop rectangle.");
                }

                InputMode inputMode = ParseInputMode(mode);
                RectangleF cropRect = GetCropRectangle(packedCropRect);

#if DEBUG
                System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture,
                                                                 "'gmic_qt_get_cropped_images' received. mode={0}, cropRect={1}",
                                                                 inputMode.ToString(), cropRect.ToString()));
#endif
                string reply = PrepareCroppedLayers(inputMode, cropRect);

                SendMessage(server, reply);
            }
            else if (command.Equals("gmic_qt_output_images", StringComparison.Ordinal))
            {
                if (!TryGetValue(parameters[1], "mode=", out string mode))
                {
                    throw new InvalidOperationException("The second item must be the output mode.");
                }

                OutputMode outputMode = ParseOutputMode(mode);

#if DEBUG
                System.Diagnostics.Debug.WriteLine("'gmic_qt_output_images' received. mode=" + outputMode.ToString());
#endif

                List outputLayers = parameters.GetRange(2, parameters.Count - 2);

                string reply = ProcessOutputImage(outputLayers, outputMode);
                SendMessage(server, reply);
            }
            else if (command.Equals("gmic_qt_release_shared_memory", StringComparison.Ordinal))
            {
#if DEBUG
                System.Diagnostics.Debug.WriteLine("'gmic_qt_release_shared_memory' received.");
#endif

                for (int i = 0; i < memoryMappedFiles.Count; i++)
                {
                    memoryMappedFiles[i].Dispose();
                }
                memoryMappedFiles.Clear();

                SendMessage(server, "done");
            }
            else if (command.Equals("gmic_qt_get_max_layer_data_length", StringComparison.Ordinal))
            {
                // This command is used to prevent images larger than 4GB from being used on a 32-bit version of G'MIC.
                // Attempting to map an image that size into memory would cause an integer overflow when casting a 64-bit
                // integer to the unsigned 32-bit size_t type.
                long maxDataLength = 0;

                foreach (GmicLayer layer in layers)
                {
                    maxDataLength = Math.Max(maxDataLength, layer.Surface.Scan0.Length);
                }

                server.Write(BitConverter.GetBytes(sizeof(long)), 0, 4);
                server.Write(BitConverter.GetBytes(maxDataLength), 0, 8);
            }

            // Wait for the acknowledgment that the client is done reading.
            if (server.IsConnected)
            {
                byte[] doneMessageBuffer = new byte[4];
                int bytesRead = 0;
                int bytesToRead = doneMessageBuffer.Length;

                do
                {
                    int n = server.Read(doneMessageBuffer, bytesRead, bytesToRead);

                    bytesRead += n;
                    bytesToRead -= n;

                } while (bytesToRead > 0 && server.IsConnected);
            }

            // Start a new server and wait for the next connection.
            server.Dispose();
            server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);

            server.BeginWaitForConnection(WaitForConnectionCallback, null);
        }

        private static List DecodeMessageBuffer(byte[] bytes)
        {
            const byte Separator = (byte)'\n';

            int startOffset = 0;
            int count = 0;

            List messageParameters = new List();

            if (bytes[bytes.Length - 1] == Separator)
            {
                // A message with multiple values uses \n as the separator and terminator.
                for (int i = 0; i < bytes.Length; i++)
                {
                    if (bytes[i] == Separator)
                    {
                        // Empty strings are skipped.
                        if (count > 0)
                        {
                            messageParameters.Add(Encoding.UTF8.GetString(bytes, startOffset, count));
                        }
                        startOffset = i + 1;
                        count = 0;
                    }
                    else
                    {
                        count++;
                    }
                }
            }
            else
            {
                messageParameters.Add(Encoding.UTF8.GetString(bytes));
            }

            return messageParameters;
        }

        private static InputMode ParseInputMode(string item)
        {
            if (Enum.TryParse(item, out InputMode temp))
            {
                if (temp >= InputMode.NoInput && temp = OutputMode.InPlace && temp = 0; i--)
                {
                    reversed.Add(layers[i]);
                }

                return reversed;
            }
            else
            {
                switch (mode)
                {
                    case InputMode.AllLayers:
                    case InputMode.ActiveAndAbove:
                    case InputMode.AllVisibleLayers:
                        return layers;
                    case InputMode.AllHiddenLayers:
                    case InputMode.AllHiddenLayersDescending:
                        return Array.Empty();
                    default:
                        throw new ArgumentException("The mode was not handled: " + mode.ToString());
                }
            }
        }

        private unsafe string PrepareCroppedLayers(InputMode inputMode, RectangleF cropRect)
        {
            if (inputMode == InputMode.NoInput)
            {
                return string.Empty;
            }

            IReadOnlyList layers = GetRequestedLayers(inputMode);

            if (layers.Count == 0)
            {
                return string.Empty;
            }

            if (memoryMappedFiles.Capacity < layers.Count)
            {
                memoryMappedFiles.Capacity = layers.Count;
            }

            StringBuilder reply = new StringBuilder();

            foreach (GmicLayer layer in layers)
            {
                Surface surface = layer.Surface;
                bool disposeSurface = false;
                int destinationImageStride = surface.Stride;

                if (cropRect != WholeImageCropRect)
                {
                    int cropX = (int)Math.Floor(cropRect.X * layer.Width);
                    int cropY = (int)Math.Floor(cropRect.Y * layer.Height);
                    int cropWidth = (int)Math.Min(layer.Width - cropX, 1 + Math.Ceiling(cropRect.Width * layer.Width));
                    int cropHeight = (int)Math.Min(layer.Height - cropY, 1 + Math.Ceiling(cropRect.Height * layer.Height));

                    try
                    {
                        surface = layer.Surface.CreateWindow(cropX, cropY, cropWidth, cropHeight);
                    }
                    catch (ArgumentOutOfRangeException ex)
                    {
                        throw new InvalidOperationException(string.Format("Surface.CreateWindow bounds invalid, cropRect={0}", cropRect.ToString()), ex);
                    }
                    disposeSurface = true;
                    destinationImageStride = cropWidth * ColorBgra.SizeOf;
                }

                string mapName = "pdn_" + Guid.NewGuid().ToString();

                try
                {
                    MemoryMappedFile file = MemoryMappedFile.CreateNew(mapName, surface.Scan0.Length);
                    memoryMappedFiles.Add(file);

                    using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor())
                    {
                        byte* destination = null;
                        RuntimeHelpers.PrepareConstrainedRegions();
                        try
                        {
                            accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref destination);

                            for (int y = 0; y < surface.Height; y++)
                            {
                                ColorBgra* src = surface.GetRowAddressUnchecked(y);
                                byte* dst = destination + (y * destinationImageStride);

                                Buffer.MemoryCopy(src, dst, destinationImageStride, destinationImageStride);
                            }
                        }
                        finally
                        {
                            if (destination != null)
                            {
                                accessor.SafeMemoryMappedViewHandle.ReleasePointer();
                            }
                        }
                    }
                }
                finally
                {
                    if (disposeSurface)
                    {
                        surface.Dispose();
                    }
                }

                reply.AppendFormat(
                    CultureInfo.InvariantCulture,
                    "{0},{1},{2},{3}\n",
                    mapName,
                    surface.Width.ToString(CultureInfo.InvariantCulture),
                    surface.Height.ToString(CultureInfo.InvariantCulture),
                    destinationImageStride.ToString(CultureInfo.InvariantCulture));
            }

            return reply.ToString();
        }

#pragma warning disable IDE0060 // Remove unused parameter
        private unsafe string ProcessOutputImage(List outputLayers, OutputMode outputMode)
#pragma warning restore IDE0060 // Remove unused parameter
        {
            string reply = string.Empty;

            List outputImages = null;
            Exception error = null;

            try
            {
                outputImages = new List(outputLayers.Count);

                for (int i = 0; i < outputLayers.Count; i++)
                {
                    if (!TryGetValue(outputLayers[i], "layer=", out string packedLayerArgs))
                    {
                        throw new InvalidOperationException("Expected a layer message argument.");
                    }

                    string[] layerArgs = packedLayerArgs.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                    if (layerArgs.Length != 4)
                    {
                        throw new InvalidOperationException("A layer message argument must have 4 values.");
                    }

                    string sharedMemoryName = layerArgs[0];
                    int width = int.Parse(layerArgs[1], NumberStyles.Integer, CultureInfo.InvariantCulture);
                    int height = int.Parse(layerArgs[2], NumberStyles.Integer, CultureInfo.InvariantCulture);
                    int stride = int.Parse(layerArgs[3], NumberStyles.Integer, CultureInfo.InvariantCulture);

                    Surface output = null;
                    bool disposeOutput = true;

                    try
                    {
                        output = new Surface(width, height);

                        using (MemoryMappedFile file = MemoryMappedFile.OpenExisting(sharedMemoryName))
                        {
                            using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor())
                            {
                                byte* sourceScan0 = null;
                                RuntimeHelpers.PrepareConstrainedRegions();
                                try
                                {
                                    accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref sourceScan0);

                                    for (int y = 0; y < output.Height; y++)
                                    {
                                        byte* src = sourceScan0 + (y * stride);
                                        ColorBgra* dst = output.GetRowAddressUnchecked(y);

                                        Buffer.MemoryCopy(src, dst, stride, stride);
                                    }
                                }
                                finally
                                {
                                    if (sourceScan0 != null)
                                    {
                                        accessor.SafeMemoryMappedViewHandle.ReleasePointer();
                                    }
                                }
                            }
                        }

                        outputImages.Add(output);
                        disposeOutput = false;
                    }
                    finally
                    {
                        if (disposeOutput)
                        {
                            output?.Dispose();
                        }
                    }
                }

                // Set the first output layer as the active layer.
                // This allows multiple G'MIC effects to be "layered" using the Apply button.
                layers[0].Dispose();
                layers[0] = new GmicLayer(outputImages[0].Clone(), true);
            }
            catch (Exception ex)
            {
                error = ex;
            }

            OutputImageState?.Dispose();
            OutputImageState = new OutputImageState(error, outputImages);

            RaiseOutputImageChanged();

            return reply;
        }

        private void RaiseOutputImageChanged()
        {
            if (synchronizationContext != null)
            {
                synchronizationContext.Send(outputImageCallback, null);
            }
            else
            {
                OnOutputImageChanged();
            }
        }

        private void OutputImageChangedCallback(object state)
        {
            OnOutputImageChanged();
        }

        private void OnOutputImageChanged()
        {
            OutputImageChanged?.Invoke(this, EventArgs.Empty);
        }

        private void VerifyNotDisposed()
        {
            if (disposed)
            {
                throw new ObjectDisposedException(nameof(GmicPipeServer));
            }
        }

        private static bool TryGetValue(string item, string prefix, out string value)
        {
            if (item != null && item.StartsWith(prefix, StringComparison.Ordinal))
            {
                value = item.Substring(prefix.Length);

                return !string.IsNullOrWhiteSpace(value);
            }

            value = null;
            return false;
        }

        private static void SendMessage(NamedPipeServerStream stream, string message)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            int messageLength = Encoding.UTF8.GetByteCount(message);

            byte[] messageBytes = new byte[sizeof(int) + messageLength];

            messageBytes[0] = (byte)(messageLength & 0xff);
            messageBytes[1] = (byte)((messageLength >> 8) & 0xff);
            messageBytes[2] = (byte)((messageLength >> 16) & 0xff);
            messageBytes[3] = (byte)((messageLength >> 24) & 0xff);
            Encoding.UTF8.GetBytes(message, 0, message.Length, messageBytes, 4);

            stream.Write(messageBytes, 0, messageBytes.Length);
        }
    }
}