Title: Add a watermark to all of the files in a directory in C#
I recently wanted to make a slide show where each picture displayed a logo or watermark. File Explorer can easily play a slide show. Simply follow these steps:
- Browse to the folder containing the images.
- Click one of the image files.
- On File Explorer's ribbon, open the Picture Tools tab and click Slide Show.
While the slide show is running, right-click on an image to change options such as speed and whether the show should shuffle the images instead of playing them in alphabetical order.
That works pretty well. I just needed to add watermarks to all of the images in a directory.
You could write a program to simply open the files and add the watermarks, but there's a catch. The slide show resizes the images so they are as large as possible without distorting them. That means images many be resized by different amounts to make them fill the screen. If you add all of the watermarks at the same size, then they will appear at different sizes in the slide show.
So I wrote a slightly different program that lets you specify how big your screen is. (The program could just look it up, but I wanted to let you prepare for a slide show with different resolution if necessary.) It then loops through the files, calculates the amount by which the file will be scaled during the slide show, and draws the watermark so it has a desired size when scaled.
That all worked, more or less, but the directory I wanted to process was fairly large so processing it would take some time. To let you know that the program was still running, I wanted the program to show you the names of the files processed and the updated images as they were produced. That worked for a while, but after about 10 or 12 pictures, the program froze and stopped displaying the revised files and no amount of calls to Refresh seemed to help.
The way around this is to use a BackgroundWorker. I don't use BackgroundWorker much (I suspect few people do), so this seemed like a good time to show how to use it.
There are three main parts to using a BackgroundWorker, so I'll describe them in the following sections.
Setup
A BackgroundWorker performs work on a separate thread. If you've worked with multiple threads before, you know that only the user interface (UI) thread can modify the user interface. The means the BackgroundWorker cannot update the user interface to show its progress.
In order to provide feedback to the user, the BackgroundWorker can raise its ProgressChanged event. The event handler runs on the UI thread, so it can manipulate the user interface to provide feedback. The worker will never raise that event, however, unless you set its WorkerReportsProgress property to true. Forgetting to set that property is a common and frustrating bug. The code is all correct, but the event doesn't fire because you forgot to set that property at design time. (You can set it in the form's Load event handler if you prefer.)
This example may take a while to process a large directory, so I also wanted to allow the user to cancel the process. You can tell a BackgroundWorker to stop running, but it won't stop unless you set its WorkerSupportsCancellation property to true. This is another common and confusing bug. If you forget to set that property, the code looks correct (because it is), but the program cannot stop the BackgroundWorker.
To summarize the setup steps:
- Create the BackgroundWorker component.
- If you want the worker to provide progress feedback, set its WorkerReportsProgress property to true.
- If you want to be able to cancel the worker, set its WorkerSupportsCancellation property to true.
After you finish the setup, the BackgroundWorker uses three event handlers to manage its work. The following sections describe those event handlers.
Starting and Stopping
When you click the program's Process Files button, the following code starts or stops the BackgroundWorker.
// Start or stop adding watermarks.
private void btnProcessFiles_Click(object sender, EventArgs e)
{
if (btnProcessFiles.Text == "Process Files")
{
// Launch the BackgroundWorker.
btnProcessFiles.Text = "Stop";
Cursor = Cursors.WaitCursor;
bwProcessImage.RunWorkerAsync();
}
else
{
// Stop.
btnProcessFiles.Text = "Process Files";
bwProcessImage.CancelAsync();
}
}
If the button's caption is "Process Files," then the worker is not running. The program changes the button's caption to "Stop" and calls the worker's RunWorkerAsync method to make it start running. This makes the worker raise its DoWork event handler, which is described in the next section.
If the button's caption is "Stop," then the worker is already running. In that case, the program changes the button's caption to "Process Files" and calls the worker's CancelAsync method to tell it to stop running. You'll see in the next section how that stops the worker.
DoWork
When you call the worker's RunWorkerAsync method, it raises its DoWork event. The following code shows the event handler used by this example.
// Add the watermark to the files in the background.
private int NumProcessed = 0;
private void bwProcessImage_DoWork(object sender, DoWorkEventArgs e)
{
// Get parameters.
int file_width = int.Parse(txtImageWidth.Text);
int file_height = int.Parse(txtImageHeight.Text);
int wm_width = int.Parse(txtWatermarkWidth.Text);
int wm_height = int.Parse(txtWatermarkHeight.Text);
int xmargin = int.Parse(txtMarginX.Text);
int ymargin = int.Parse(txtMarginY.Text);
float opacity = float.Parse(txtOpacity.Text);
string output_path = txtOutput.Text;
if (!output_path.EndsWith("\\")) output_path += "\\";
// Adjust the watermark's opacity.
Bitmap wm = SetOpacity(Watermark, opacity);
// Get the watermark's input rectangle.
RectangleF source_rect = new RectangleF(
0, 0, wm.Width, wm.Height);
// Loop through the files.
NumProcessed = 0;
FileInfo[] file_infos = null;
try
{
DirectoryInfo input_dir_info = new DirectoryInfo(txtInput.Text);
file_infos = input_dir_info.GetFiles();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
e.Cancel = true;
return;
}
foreach (FileInfo input_file_info in file_infos)
{
string filename = Path.Combine(output_path,
input_file_info.Name);
Bitmap bm = null;
try
{
// Load the input file.
bm = new Bitmap(input_file_info.FullName);
// Get the scale.
float xscale = file_width / (float)bm.Width;
float yscale = file_height / (float)bm.Height;
float scale = Math.Min(xscale, yscale);
// Make a destination rectangle so the watermark
// has the desired size when the image is scaled.
RectangleF dest_rect = new RectangleF(
xmargin, ymargin,
wm_width / scale, wm_height / scale);
// Draw the watermark on the image.
using (Graphics gr = Graphics.FromImage(bm))
{
gr.InterpolationMode =
InterpolationMode.HighQualityBicubic;
gr.DrawImage(wm, dest_rect, source_rect,
GraphicsUnit.Pixel);
}
// Save the result.
SaveImage(bm, filename);
}
catch (Exception ex)
{
Console.WriteLine("Skipped {0}. {1}",
input_file_info.Name, ex.Message);
}
// Show progress.
NumProcessed++;
int percent_complete =
(100 * NumProcessed) / file_infos.Length;
Progress progress = new Progress(bm, filename);
bwProcessImage.ReportProgress(
percent_complete, progress);
// See if we should cancel.
if (bwProcessImage.CancellationPending)
{
e.Cancel = true;
break;
}
}
}
This code gets the input parameters that the user entered in the form's text boxes. It then uses the SetOpacity method to adjust the watermark image's opacity. (For information about that method, see my post Adjust an image's opacity in C#.)
Next, the code creates a Rectangle that represents the watermark's area. This will be the area that the program copies onto the images.
The code then creates a DirectoryInfo object representing the input directory and uses its GetFiles method to get an array of FileInfo objects that represent the directory's files. It performs those steps inside a try catch block in case the directory doesn't exist.
The program then loops through the directory's files. It combines the file's name (without the path) and the output directory to get the name of the output file.
Next, the event handler loads the image file into a Bitmap. It does this inside a try catch block in case the file is not an image file. If the code successfully loads the file, it calculates the amounts by which it could scale the image to fill the desired screen dimensions horizontally and vertically. The smaller of the two scales is the amount by which the image will actually be scaled during the slide show.
The code divides the desired watermark width and height by the scale to get the size that the watermark should have so it is scaled to its desired size. It then uses those dimensions to create a destination Rectangle.
Now the program finally does some drawing. It creates a Graphics object associated with the file's Bitmap and draws the watermark onto it in the destination rectangle.
The program then calls the SaveImage method to save the modified image into the output file. (For information on the SaveImage method, see my post Save images with an appropriate format depending on the file name's extension in C#.)
After it has processed a file, or failed to process it if it is not an image file, the worker reports its progress to the UI thread. To do that, it calculates its completion percentage. It also creates a Progress object to pass information to the UI thread. The following code shows the Progress class.
class Progress
{
public Bitmap Image;
public string Filename;
public Progress(Bitmap image, string filename)
{
Image = image;
Filename = filename;
}
}
This class simply holds an image and a filename. The DoWork event handler places the name of the file just processed and its new image in a Progress object. It then calls the worker's ReportProgress method passing it the completion percentage and the Progress object. The ReportProgress method makes the worker fire the ProgressChanged event described in the next section.
The final thing that the DoWork event handler must do is check to see if the worker has been told to stop. It does that by checking the worker's CancellationPending property. If that property is true, then the worker has been told to stop. In that case, the DoWork event handler should stop doing whatever it is doing. In this example, that means it should break out of its file-processing loop.
The code also sets the event handler's e.Cancel parameter to true to indicate that the event handler was canceled and did not finish all of its work.
ProgressChanged
When the DoWork event handler calls the worker's ReportProgress method, the following event handler executes on the UI thread.
// Update the progress bar.
private void bwProcessImage_ProgressChanged(
object sender, ProgressChangedEventArgs e)
{
prgFiles.Value = e.ProgressPercentage;
Progress progress = (Progress)e.UserState;
lblFile.Text = progress.Filename;
picWatermark.Image = progress.Image;
picWatermark.Refresh();
Console.WriteLine("Saved file " + progress.Filename);
}
This event handler sets the prgFiles progress bar's Value property to indicate the completion percentage.
The event handler's e.UserState parameter contains whatever object the DoWork event handler passed into the ReportProgress method as its second parameter. (That parameter is optional, in case you only want to pass the completion percentage to the ProgressChanged event handler.)
The code converts e.UserState from a generic object to a Progress object. It then displays the processed file's name in the lblFile label. It also displays the newly watermarked image in the picWatermark PictureBox. Finally, the event handler writes the name of the modified file in the Output window.
RunWorkerCompleted
When the worker finishes, it fires its RunWorkerCompleted event so its event handler can take any action that is needed to clean up. This example uses the following event handler.
// Clean up.
private void bwProcessImage_RunWorkerCompleted(
object sender, RunWorkerCompletedEventArgs e)
{
prgFiles.Value = 0;
lblFile.Text = "";
picWatermark.Image = Watermark;
Cursor = Cursors.Default;
MessageBox.Show("Processed " +
NumProcessed.ToString() + " files");
.Text = "Process Files";
}
This code clears the progress bar and lblFile label. It displays the original watermark image in the picWatermark PictureBox, resets the form's cursor to the default, and displays a message box indicating the number of files that the program processed. Finally, it sets the btnProcessFiles button's caption to "Process Files."
At this point, the BackgroundWorker stops and the program is ready to start the whole process over again.
Conclusion
The BackgroundWorker is somewhat complicated, but it can be handy when you want to perform a long sequence of tasks. Here are the basic steps.
- Create the BackgroundWorker component.
- If you want the worker to provide progress feedback, set its WorkerReportsProgress property to true.
- If you want to be able to cancel the worker, set its WorkerSupportsCancellation property to true.
- Call the worker's RunWorkerAsync method to make it start working.
- The DoWork event handler performs the work.
- The DoWork method can occasionally call the worker's ReportProgress method to report progress. That causes the ProgressChanged event handler to execute on the UI thread to display progress information such as by updating a progress bar.
- The DoWork method should check the worker's CancellationPending property to see if it should stop. If CancellationPending is true, then the DoWork event handler should set e.Cancel to true and stop working.
- The UI thread can call the worker's CancelAsync method to tell the worker to stop. (That sets CancellationPending to true.)
- When DoWork finishes, either because it completed its work or because it was canceled, the worker fires its RunWorkerCompleted event. The event handler runs on the UI thread and can perform cleanup tasks.
This example performs a few other chores such as allowing the user to load a new watermark or browse for the input and output directories
Download the example to experiment with it and to see additional details.
|