Export selection to image: added progress bar and button to cancel export

This commit is contained in:
biwa 2020-12-24 15:00:16 +01:00 committed by spherallic
parent a4bc90c754
commit e65cc32991
4 changed files with 451 additions and 150 deletions

View File

@ -890,29 +890,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
}
ImageExportSettingsForm form = new ImageExportSettingsForm();
if (form.ShowDialog() == DialogResult.OK)
{
ImageExportSettings settings = new ImageExportSettings(Path.GetDirectoryName(form.FilePath), Path.GetFileNameWithoutExtension(form.FilePath), Path.GetExtension(form.FilePath), form.Floor, form.Fullbright, form.Brightmap, form.Tiles, form.ImageScale, form.GetPixelFormat(), form.GetImageFormat());
ImageExporter exporter = new ImageExporter(sectors, settings);
string text = "The following images will be created:\n\n" + string.Join("\n", exporter.GetImageNames().ToArray());
DialogResult result = MessageBox.Show(text, "Export to image", MessageBoxButtons.OKCancel);
if (result == DialogResult.OK)
{
try
{
exporter.Export();
MessageBox.Show("Export successful.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (ArgumentException e) // Happens if there's not enough consecutive memory to create the file
{
MessageBox.Show("Exporting failed. There's likely not enough consecutive free memory to create the image. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
form.ShowDialog();
}
#endregion

View File

@ -27,6 +27,7 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;
using CodeImp.DoomBuilder.Data;
@ -68,12 +69,29 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
#endregion
#region ================== Exceptions
[Serializable]
public class ImageExportCanceledException : Exception { }
[Serializable]
public class ImageExportImageTooBigException : Exception { }
#endregion
internal class ImageExporter
{
#region ================== Variables
private ICollection<Sector> sectors;
private ImageExportSettings settings;
private int numitems;
private int doneitems;
private int donepercent;
private Action addprogress;
private Action<string> showphase;
private Func<bool> checkcanelexport;
private bool cancelexport;
#endregion
@ -85,10 +103,14 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
#region ================== Constructors
public ImageExporter(ICollection<Sector> sectors, ImageExportSettings settings)
public ImageExporter(ICollection<Sector> sectors, ImageExportSettings settings, Action addprogress, Action<string> showphase, Func<bool> checkcanelexport)
{
this.sectors = sectors;
this.settings = settings;
this.addprogress = addprogress;
this.showphase = showphase;
this.checkcanelexport = checkcanelexport;
cancelexport = false;
}
#endregion
@ -105,26 +127,63 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
GetSizeAndOffset(out size, out offset);
// Count the number of triangles for reporting progress
numitems = 0;
doneitems = 0;
donepercent = 0;
foreach (Sector s in sectors)
numitems += s.Triangles.Vertices.Count / 3;
if (settings.Tiles)
numitems += GetNumTiles();
// If exporting a brightmap everything has to be done twice
if (settings.Brightmap)
numitems *= 2;
// Use the same image for the normal texture and the brightmap because of memory concerns
showphase("Preparing");
using (Bitmap image = new Bitmap((int)(size.x * settings.Scale), (int)(size.y * settings.Scale), settings.PixelFormat))
{
showphase("Creating normal image");
// Normal texture image
CreateImage(image, offset, settings.Scale, false);
if (settings.Tiles)
{
showphase("Saving 64x64 tile images (" + GetNumTiles() + ")");
SaveImageAsTiles(image);
}
else
{
showphase("Saving normal image");
try
{
image.Save(Path.Combine(settings.Path, settings.Name) + settings.Extension, settings.ImageFormat);
}
catch(ExternalException)
{
throw new ImageExportImageTooBigException();
}
}
// The brightmap
if (settings.Brightmap)
{
showphase("Creating brightmap image");
CreateImage(image, offset, settings.Scale, true);
showphase("Saving brightmap image");
if (settings.Tiles)
{
showphase("Saving 64x64 tile images (" + GetNumTiles() + ")");
SaveImageAsTiles(image, "_brightmap");
}
else
{
image.Save(Path.Combine(settings.Path, settings.Name) + "_brightmap" + settings.Extension, settings.ImageFormat);
showphase("Saving normal image");
}
}
}
}
@ -138,18 +197,17 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
/// <returns>The image to be exported</returns>
private void CreateImage(Bitmap texturebitmap, Vector2D offset, float scale, bool asbrightmap)
{
Graphics gtexture = null;
// The texture
gtexture = Graphics.FromImage(texturebitmap);
using (Graphics gtexture = Graphics.FromImage(texturebitmap))
{
gtexture.Clear(Color.Black); // If we don't clear to black we'll see seams where the sectors touch, due to the AA
gtexture.InterpolationMode = InterpolationMode.HighQualityBilinear;
gtexture.CompositingQuality = CompositingQuality.HighQuality;
gtexture.PixelOffsetMode = PixelOffsetMode.HighQuality;
gtexture.SmoothingMode = SmoothingMode.AntiAlias; // Without AA the sector edges will be quite rough
GraphicsPath gpath = new GraphicsPath();
using (GraphicsPath gpath = new GraphicsPath())
{
foreach (Sector s in sectors)
{
float rotation = (float)s.Fields.GetValue("rotationfloor", 0.0);
@ -169,12 +227,25 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
gpath.AddLine((float)v1.x, (float)v1.y, (float)v2.x, (float)v2.y);
gpath.AddLine((float)v2.x, (float)v2.y, (float)v3.x, (float)v3.y);
gpath.CloseFigure();
doneitems++;
int newpercent = (int)(((double)doneitems / numitems) * 100);
if (newpercent > donepercent)
{
donepercent = newpercent;
addprogress();
if (checkcanelexport())
throw new ImageExportCanceledException();
}
}
if (asbrightmap)
{
// Create the brightmap based on the sector brightness
using (SolidBrush sbrush = new SolidBrush(Color.FromArgb(255, s.Brightness, s.Brightness, s.Brightness)))
int brightness = General.Clamp(s.Brightness, 0, 255);
using (SolidBrush sbrush = new SolidBrush(Color.FromArgb(255, brightness, brightness, brightness)))
gtexture.FillPath(sbrush, gpath);
}
else
@ -225,7 +296,10 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
matrix.Scale((float)texturescale.x, (float)texturescale.y);
if (!settings.Fullbright)
AdjustBrightness(ref brushtexture, s.Brightness > 0 ? s.Brightness / 255.0f : 0.0f);
{
int brightness = General.Clamp(s.Brightness, 0, 255);
AdjustBrightness(ref brushtexture, brightness > 0 ? brightness / 255.0f : 0.0f);
}
if (scale > 1.0f)
ResizeImage(ref brushtexture, brushtexture.Width * (int)scale, brushtexture.Height * (int)scale);
@ -246,10 +320,8 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
// Reset the graphics path
gpath.Reset();
}
// Dispose unneeded objects
gpath.Dispose();
gtexture.Dispose();
}
}
}
/// <summary>
@ -277,7 +349,6 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
if(y * TILE_SIZE + TILE_SIZE > image.Size.Height)
height = image.Size.Height - (y * TILE_SIZE);
using (Bitmap bitmap = new Bitmap(TILE_SIZE, TILE_SIZE))
using (Graphics g = Graphics.FromImage(bitmap))
{
@ -288,6 +359,21 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
}
imagenum++;
doneitems++;
int newpercent = (int)(((double)doneitems / numitems) * 100);
if (newpercent > donepercent)
{
donepercent = newpercent;
addprogress();
if (checkcanelexport())
throw new ImageExportCanceledException();
}
if (checkcanelexport())
throw new ImageExportCanceledException();
}
}
}
@ -325,7 +411,23 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO
}
return imagenames;
}
/// <summary>
/// Gets the total number of tiles
/// </summary>
/// <returns>Number of tiles</returns>
public int GetNumTiles()
{
Vector2D size;
Vector2D offset;
GetSizeAndOffset(out size, out offset);
int xnum = (int)Math.Ceiling(size.x * settings.Scale / (double)TILE_SIZE);
int ynum = (int)Math.Ceiling(size.y * settings.Scale / (double)TILE_SIZE);
return xnum * ynum;
}
/// <summary>

View File

@ -31,7 +31,7 @@
this.tbExportPath = new System.Windows.Forms.TextBox();
this.browse = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.cancel = new System.Windows.Forms.Button();
this.close = new System.Windows.Forms.Button();
this.export = new System.Windows.Forms.Button();
this.saveFileDialog = new System.Windows.Forms.SaveFileDialog();
this.cbImageFormat = new System.Windows.Forms.ComboBox();
@ -45,6 +45,8 @@
this.cbTiles = new System.Windows.Forms.CheckBox();
this.cbScale = new System.Windows.Forms.ComboBox();
this.label4 = new System.Windows.Forms.Label();
this.progress = new System.Windows.Forms.ProgressBar();
this.lbPhase = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// tbExportPath
@ -73,16 +75,16 @@
this.label1.TabIndex = 4;
this.label1.Text = "Path:";
//
// cancel
// close
//
this.cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancel.Location = new System.Drawing.Point(360, 153);
this.cancel.Name = "cancel";
this.cancel.Size = new System.Drawing.Size(75, 23);
this.cancel.TabIndex = 7;
this.cancel.Text = "Cancel";
this.cancel.UseVisualStyleBackColor = true;
this.cancel.Click += new System.EventHandler(this.cancel_Click);
this.close.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.close.Location = new System.Drawing.Point(360, 153);
this.close.Name = "close";
this.close.Size = new System.Drawing.Size(75, 23);
this.close.TabIndex = 7;
this.close.Text = "Close";
this.close.UseVisualStyleBackColor = true;
this.close.Click += new System.EventHandler(this.close_Click);
//
// export
//
@ -219,13 +221,34 @@
this.label4.TabIndex = 18;
this.label4.Text = "Scale:";
//
// progress
//
this.progress.Location = new System.Drawing.Point(12, 153);
this.progress.Name = "progress";
this.progress.Size = new System.Drawing.Size(261, 23);
this.progress.Step = 1;
this.progress.TabIndex = 19;
this.progress.Visible = false;
//
// lbPhase
//
this.lbPhase.AutoSize = true;
this.lbPhase.Location = new System.Drawing.Point(14, 127);
this.lbPhase.Name = "lbPhase";
this.lbPhase.Size = new System.Drawing.Size(45, 13);
this.lbPhase.TabIndex = 20;
this.lbPhase.Text = "lbPhase";
this.lbPhase.Visible = false;
//
// ImageExportSettingsForm
//
this.AcceptButton = this.export;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancel;
this.CancelButton = this.close;
this.ClientSize = new System.Drawing.Size(447, 188);
this.Controls.Add(this.lbPhase);
this.Controls.Add(this.progress);
this.Controls.Add(this.label4);
this.Controls.Add(this.cbScale);
this.Controls.Add(this.cbTiles);
@ -237,7 +260,7 @@
this.Controls.Add(this.label2);
this.Controls.Add(this.cbPixelFormat);
this.Controls.Add(this.cbImageFormat);
this.Controls.Add(this.cancel);
this.Controls.Add(this.close);
this.Controls.Add(this.export);
this.Controls.Add(this.label1);
this.Controls.Add(this.browse);
@ -247,6 +270,7 @@
this.MinimizeBox = false;
this.Name = "ImageExportSettingsForm";
this.Text = "Image export settings";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ImageExportSettingsForm_FormClosing);
this.ResumeLayout(false);
this.PerformLayout();
@ -257,7 +281,7 @@
private System.Windows.Forms.Button browse;
private System.Windows.Forms.TextBox tbExportPath;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Button cancel;
private System.Windows.Forms.Button close;
private System.Windows.Forms.Button export;
private System.Windows.Forms.SaveFileDialog saveFileDialog;
private System.Windows.Forms.ComboBox cbImageFormat;
@ -271,5 +295,7 @@
private System.Windows.Forms.CheckBox cbTiles;
private System.Windows.Forms.ComboBox cbScale;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.ProgressBar progress;
private System.Windows.Forms.Label lbPhase;
}
}

View File

@ -31,9 +31,19 @@ using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.BuilderModes.IO;
namespace CodeImp.DoomBuilder.BuilderModes.Interface
{
enum ImageExportResult
{
OK,
Canceled,
OutOfMemory,
ImageTooBig
}
public partial class ImageExportSettingsForm : Form
{
#region ================== Properties
@ -47,6 +57,20 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface
#endregion
#region ================== Delegates
private delegate void CallVoidMethodDeletage();
private delegate void CallStringMethodDeletage(string s);
private delegate void CallImageExportResultMethodDeletage(ImageExportResult ier);
#endregion
#region ================== Variables
bool exporting;
bool cancelexport;
#endregion
#region ================== Constructor
@ -56,6 +80,8 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface
cbImageFormat.SelectedIndex = 0;
cbPixelFormat.SelectedIndex = 0;
exporting = false;
cancelexport = false;
string name = Path.GetFileNameWithoutExtension(General.Map.FileTitle) + "_" + General.Map.Options.LevelName + "_" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName());
@ -104,6 +130,153 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface
}
}
/// <summary>
/// Starts exporting the image(s). Disables all controls and starts the thread that does the actual exporting.
/// </summary>
private void StartExport()
{
ICollection<Sector> sectors = General.Map.Map.SelectedSectorsCount == 0 ? General.Map.Map.Sectors : General.Map.Map.GetSelectedSectors(true);
exporting = true;
cancelexport = false;
progress.Maximum = 100; //sectors.Count * (Brightmap ? 2 : 1);
progress.Value = 0;
progress.Visible = true;
lbPhase.Text = "";
lbPhase.Visible = true;
foreach (Control c in Controls)
{
if (!(c is ProgressBar || c is Label))
c.Enabled = false;
}
export.Enabled = true;
export.Text = "Cancel";
ImageExportSettings settings = new ImageExportSettings(Path.GetDirectoryName(FilePath), Path.GetFileNameWithoutExtension(FilePath), Path.GetExtension(FilePath), Floor, Fullbright, Brightmap, Tiles, ImageScale, GetPixelFormat(), GetImageFormat());
RunExport(settings);
}
/// <summary>
/// Enables all controls. This has to be called when the export is finished (either successfully or unsuccessfully)
/// </summary>
/// <param name="ier">Image export result</param>
private void StopExport(ImageExportResult ier)
{
if (this.InvokeRequired)
{
CallImageExportResultMethodDeletage d = StopExport;
this.Invoke(d, ier);
}
else
{
progress.Visible = false;
lbPhase.Visible = false;
foreach (Control c in Controls)
{
if (!(c is ProgressBar || c is Label))
c.Enabled = true;
}
export.Text = "Export";
if (ier == ImageExportResult.OK)
MessageBox.Show("Export successful.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information);
else if(ier == ImageExportResult.Canceled)
MessageBox.Show("Export canceled.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (ier == ImageExportResult.OutOfMemory)
MessageBox.Show("Exporting failed. There's likely not enough consecutive free memory to create the image. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
else if(ier == ImageExportResult.ImageTooBig)
MessageBox.Show("Exporting failed. The image is likely too big for the current settings. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
exporting = false;
}
private bool CheckCancelExport()
{
return cancelexport;
}
/// <summary>
/// Shows the current phase in textual form. Is called by the exporter
/// </summary>
/// <param name="text"></param>
private void ShowPhase(string text)
{
if (this.InvokeRequired)
{
CallStringMethodDeletage d = ShowPhase;
this.Invoke(d, text);
}
else
{
lbPhase.Text = text;
}
}
/// <summary>
/// Adds progress to the progress bar. Is called by the exporter
/// </summary>
private void AddProgress()
{
if (progress.InvokeRequired)
{
CallVoidMethodDeletage d = AddProgress;
progress.Invoke(d);
}
else
{
// Just winforms things to make the progress bar animation not lag behind
int value = progress.Value + 1;
progress.Value = value;
progress.Value = value - 1;
progress.Value = value;
}
}
/// <summary>
/// Runs the actual exporter
/// </summary>
/// <param name="settings">Export settings</param>
private void RunExport(ImageExportSettings settings)
{
ICollection<Sector> sectors = General.Map.Map.SelectedSectorsCount == 0 ? General.Map.Map.Sectors : General.Map.Map.GetSelectedSectors(true);
ImageExporter exporter = new ImageExporter(sectors, settings, AddProgress, ShowPhase, CheckCancelExport);
try
{
exporter.Export();
}
catch (ArgumentException) // Happens if there's not enough consecutive memory to create the file
{
StopExport(ImageExportResult.OutOfMemory);
return;
}
catch(ImageExportCanceledException)
{
StopExport(ImageExportResult.Canceled);
return;
}
catch(ImageExportImageTooBigException)
{
StopExport(ImageExportResult.ImageTooBig);
return;
}
StopExport(ImageExportResult.OK);
}
#endregion
#region ================== Events
private void browse_Click(object sender, EventArgs e)
{
if (saveFileDialog.ShowDialog() == DialogResult.OK)
@ -124,23 +297,35 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface
}
}
#endregion
private void cancel_Click(object sender, EventArgs e)
private void close_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
private void export_Click(object sender, EventArgs e)
{
if (exporting)
{
cancelexport = true;
export.Enabled = false;
}
else
{
General.Settings.WritePluginSetting("imageexportfullbright", cbFullbright.Checked);
General.Settings.WritePluginSetting("imageexportbrightmap", cbBrightmap.Checked);
General.Settings.WritePluginSetting("imageexporttiles", cbTiles.Checked);
General.Settings.WritePluginSetting("imageexportscale", cbScale.SelectedIndex);
this.DialogResult = DialogResult.OK;
this.Close();
// Exporting works like this:
// In here StartExport() is called
// StartExport() disables all controls and creates a thread that runs RunExport() in the background; then the StartExport method ends
// RunExport() creates an instance of ImageExporter and starts the actual export
// When ImageExporter finishes its job it runs StopExport()
// StopExport() enables all controls again
StartExport();
}
}
private void cbImageFormat_SelectedIndexChanged(object sender, EventArgs e)
@ -159,5 +344,15 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface
tbExportPath.Text = Path.ChangeExtension(tbExportPath.Text, newextension);
}
private void ImageExportSettingsForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Do not allow closing the form while the export is running
if (exporting)
e.Cancel = true;
}
#endregion
}
}