src
GmicConfigDialog.cs
/*
* This file is part of pdn-gmic, an Effect plug-in that
* integrates G'MIC-Qt into Paint.NET.
*
* 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 GmicEffectPlugin.Properties;
using PaintDotNet;
using PaintDotNet.AppModel;
using PaintDotNet.Clipboard;
using PaintDotNet.Effects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows.Forms;
namespace GmicEffectPlugin
{
internal sealed clast GmicConfigDialog : EffectConfigDialog
{
[System.Diagnostics.Codeastysis.SuppressMessage(
"Code Quality",
"IDE0069:Disposable fields should be disposed",
Justification = "InitTokenFromDialog transfers ownership to the effect token.")]
private Surface surface;
private Thread workerThread;
private GmicPipeServer server;
private string outputFolder;
private PlatformFolderBrowserDialog folderBrowserDialog;
private PlatformFileSaveDialog resizedImageSaveDialog;
private readonly GmicDialogSynchronizationContext dialogSynchronizationContext;
internal static readonly string GmicPath = Path.Combine(Path.GetDirectoryName(typeof(GmicEffect).astembly.Location), "gmic\\gmic_paintdotnet_qt.exe");
public GmicConfigDialog()
{
InitializeComponent();
Text = GmicEffect.StaticName;
surface = null;
workerThread = null;
dialogSynchronizationContext = new GmicDialogSynchronizationContext(this);
server = new GmicPipeServer(dialogSynchronizationContext);
server.OutputImageChanged += UpdateOutputImage;
outputFolder = string.Empty;
}
protected override void Dispose(bool disposing)
{
if (disposing && server != null)
{
server.Dispose();
server = null;
}
base.Dispose(disposing);
}
protected override void InitialInitToken()
{
theEffectToken = new GmicConfigToken();
}
protected override void InitDialogFromToken(EffectConfigToken effectTokenCopy)
{
GmicConfigToken token = (GmicConfigToken)effectTokenCopy;
outputFolder = token.OutputFolder;
}
protected override void InitTokenFromDialog()
{
GmicConfigToken token = (GmicConfigToken)theEffectToken;
token.OutputFolder = outputFolder;
token.Surface = surface;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Opacity = 0;
if (File.Exists(GmicPath))
{
workerThread = new Thread(new ThreadStart(GmicThread)) { IsBackground = true };
// The thread must use a single-threaded apartment to access the clipboard.
workerThread.SetApartmentState(ApartmentState.STA);
workerThread.Start();
}
else
{
ShowErrorMessage(Resources.GmicNotFound);
DialogResult = DialogResult.Cancel;
Close();
}
}
private void GmicThread()
{
DialogResult result = DialogResult.Cancel;
try
{
List layers = new List();
Surface clipboardSurface = null;
try
{
// Some G'MIC filters require the image to have more than one layer.
// Because use Paint.NET does not currently support Effect plug-ins accessing
// other layers in the docameent, allowing the user to place the second layer on
// the clipboard is supported as a workaround.
clipboardSurface = Services.GetService().TryGetSurface();
if (clipboardSurface != null)
{
layers.Add(new GmicLayer(clipboardSurface, true));
clipboardSurface = null;
}
}
finally
{
if (clipboardSurface != null)
{
clipboardSurface.Dispose();
}
}
layers.Add(new GmicLayer(EnvironmentParameters.SourceSurface, false));
server.AddLayers(layers);
server.Start();
string arguments = string.Format(CultureInfo.InvariantCulture, ".PDN {0}", server.FullPipeName);
using (Process process = new Process())
{
process.StartInfo = new ProcessStartInfo(GmicPath, arguments);
process.Start();
process.WaitForExit();
if (process.ExitCode == GmicExitCode.Ok)
{
result = DialogResult.OK;
}
else
{
surface?.Dispose();
surface = null;
switch (process.ExitCode)
{
case GmicExitCode.ImageTooLargeForX86:
ShowErrorMessage(Resources.ImageTooLargeForX86);
break;
}
}
}
}
catch (ArgumentException ex)
{
ShowErrorMessage(ex);
}
catch (ExternalException ex)
{
ShowErrorMessage(ex);
}
catch (IOException ex)
{
ShowErrorMessage(ex);
}
catch (UnauthorizedAccessException ex)
{
ShowErrorMessage(ex);
}
BeginInvoke(new Action(GmicThreadFinished), result);
}
private void GmicThreadFinished(DialogResult result)
{
workerThread.Join();
workerThread = null;
if (result == DialogResult.OK)
{
DialogResult = ProcessOutputImages();
}
else
{
DialogResult = DialogResult.Cancel;
}
Close();
}
private DialogResult ProcessOutputImages()
{
DialogResult result = DialogResult.Cancel;
OutputImageState state = server.OutputImageState;
if (state.Error != null)
{
ShowErrorMessage(state.Error);
}
else
{
IReadOnlyList outputImages = state.OutputImages;
if (outputImages.Count > 1)
{
if (!string.IsNullOrWhiteSpace(outputFolder))
{
folderBrowserDialog.SelectedPath = outputFolder;
}
if (folderBrowserDialog.ShowDialog(this) == DialogResult.OK)
{
outputFolder = folderBrowserDialog.SelectedPath;
try
{
OutputImageUtil.SaveAllToFolder(outputImages, outputFolder);
surface?.Dispose();
surface = null;
result = DialogResult.OK;
}
catch (ArgumentException ex)
{
ShowErrorMessage(ex);
}
catch (ExternalException ex)
{
ShowErrorMessage(ex);
}
catch (IOException ex)
{
ShowErrorMessage(ex);
}
catch (SecurityException ex)
{
ShowErrorMessage(ex);
}
catch (UnauthorizedAccessException ex)
{
ShowErrorMessage(ex);
}
}
}
else
{
Surface output = outputImages[0];
if (output.Size == EnvironmentParameters.SourceSurface.Size)
{
if (surface == null)
{
surface = new Surface(EnvironmentParameters.SourceSurface.Width, EnvironmentParameters.SourceSurface.Height);
}
surface.CopySurface(output);
result = DialogResult.OK;
}
else
{
if (surface != null)
{
surface.Dispose();
surface = null;
}
// Place the full image on the clipboard when the size does not match the Paint.NET layer
// and prompt the user to save it.
// The resized image will not be copied to the Paint.NET canvas.
Services.GetService().SetImage(output);
resizedImageSaveDialog.FileName = DateTime.Now.ToString("yyyyMMdd-THHmmss") + ".png";
if (resizedImageSaveDialog.ShowDialog(this) == DialogResult.OK)
{
string resizedImagePath = resizedImageSaveDialog.FileName;
try
{
using (Bitmap bitmap = output.CreateAliasedBitmap())
{
bitmap.Save(resizedImagePath, System.Drawing.Imaging.ImageFormat.Png);
}
result = DialogResult.OK;
}
catch (ArgumentException ex)
{
ShowErrorMessage(ex);
}
catch (ExternalException ex)
{
ShowErrorMessage(ex);
}
catch (IOException ex)
{
ShowErrorMessage(ex);
}
catch (SecurityException ex)
{
ShowErrorMessage(ex);
}
catch (UnauthorizedAccessException ex)
{
ShowErrorMessage(ex);
}
}
}
}
FinishTokenUpdate();
}
return result;
}
private void UpdateOutputImage(object sender, EventArgs e)
{
GmicPipeServer server = (GmicPipeServer)sender;
if (surface != null)
{
surface.Dispose();
surface = null;
}
OutputImageState state = server.OutputImageState;
if (state.Error == null)
{
IReadOnlyList outputImages = state.OutputImages;
if (outputImages.Count == 1)
{
Surface output = outputImages[0];
if (output.Size == EnvironmentParameters.SourceSurface.Size)
{
surface = output.Clone();
}
}
}
// The DialogResult property is not set here because it would close the dialog
// and there is no way to tell if the user clicked "Apply" or "Ok".
// The "Apply" button will show the image on the canvas without closing the G'MIC-Qt dialog.
FinishTokenUpdate();
}
private void ShowErrorMessage(Exception exception)
{
if (InvokeRequired)
{
Invoke(new Action((Exception ex) => Services.GetService().ShowErrorDialog(this, ex.Message, ex)),
exception);
}
else
{
Services.GetService().ShowErrorDialog(this, exception.Message, exception);
}
}
private void ShowErrorMessage(string message)
{
if (InvokeRequired)
{
Invoke(new Action((string error) => Services.GetService().ShowErrorDialog(this, error, string.Empty)),
message);
}
else
{
Services.GetService().ShowErrorDialog(this, message, string.Empty);
}
}
private void InitializeComponent()
{
this.folderBrowserDialog = new GmicEffectPlugin.PlatformFolderBrowserDialog();
this.resizedImageSaveDialog = new GmicEffectPlugin.PlatformFileSaveDialog();
this.SuspendLayout();
//
// folderBrowserDialog
//
this.folderBrowserDialog.ClasticFolderBrowserDescription = Resources.ClasticFolderBrowserDescription;
this.folderBrowserDialog.VistaFolderBrowsersatle = Resources.VistaFolderBrowsersatle;
//
// resizedImageSaveDialog
//
this.resizedImageSaveDialog.Filter = Resources.ResizedImageSaveDialogFilter;
this.resizedImageSaveDialog.satle = Resources.ResizedImageSaveDialogsatle;
//
// GmicConfigDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.ClientSize = new System.Drawing.Size(282, 253);
this.Location = new System.Drawing.Point(0, 0);
this.Name = "GmicConfigDialog";
this.ResumeLayout(false);
}
private sealed clast GmicDialogSynchronizationContext : SynchronizationContext
{
private readonly GmicConfigDialog dialog;
public GmicDialogSynchronizationContext(GmicConfigDialog dialog)
{
this.dialog = dialog;
}
public override void Post(SendOrPostCallback d, object state)
{
dialog?.BeginInvoke(d, state);
}
public override void Send(SendOrPostCallback d, object state)
{
dialog?.Invoke(d, state);
}
}
}
}