Friday, June 16, 2006

How to Watermark your Photos

I have been posting increasingly-large images on my other blog, El Cantar de la Lluvia, and wanted to embed at least some ownership info in the pics. I don't mean something fancy like Digimarc, or even a full-image ghosted overlay. I just wanted an easy and automated way to add a string to the image along the right hand side, rotated, and hopefully using a tool smart enough to detect if the image was horizontal or vertical.

I tried quite a few OS X apps, some of them free, some demos, and hated them all. I decided to write my own tool to do it. After all, I do have an excellent and easy to use image handling library at my disposal (which, I might add --most un-humbly--, I wrote myself) called PNGwriter.

I wrote PNGwriter to let amateur programmers just get at the darned pixels, and to spare them the trouble of using some horrible format like PPM if they didn't have the time or knowledge to learn the interface to an image library.

My objectives for this project were as follows.

  • Write something that takes the 630-pixel-wide images I export from iPhoto, once I've selected them for my blog, and adds a text string to them.
  • The text string should be easily modifiable, so no hard coding it into the program.
  • The program should accept UTF-8 strings (my love for the UTF-8 text encoding knows no bounds).
  • The text string should ideally be placed along the right edge of the image, rotated so as to be as unobtrusive as possible.
  • The text string should be automatically written in black on light backgrounds, and in white on dark backgrounds.
  • The text string should possibly be blended with the background, with a transparency effect.
  • The program should be able to be called easily from a shell script, so as to automate the processing of many images.
  • The program should add the string -wmk to the basename of the watermarked image, while the original should be kept intact.
  • I should stop adding items to this list now.


After a few hours of writing code that consisted mostly of mistakes and blunders (I haven't programmed for about a year) I managed to produce something that satisfied the above requirements. Along the way I realised that having the program accept the "copyright string", or the string to be added to the image, was not such a hot idea, as getting the shell script to play nice with spaces and unicode characters was beyond my almost inexistent shell script knowledge. In the end I opted for the simplest alternative.

The program, which I decided to call watermarker, takes three arguments, no more, no less. An example use of the program is as follows:
./watermarker watermarker.config copyrightstring-utf8.txt IMG00001.png

The names are arbitrary and the relevant files can be in any location. The only requirement is that the input image be a 24 byte-per-pixel (this is important), that the file copyrightstring-utf8.txt contain the relevant UTF-8 formatted text string to use (or just plain text, because plain characters in UTF-8 are just plain characters); and that watermarker.config contain the following information:

  • X-offset, in pixels, of the bottom right corner of the (vertical) text to be plotted, as measured from the right side of the image.
  • Y-offset, in pixels, of the bottom right corner of the text to be plotted, as measured from the bottom of the image.
  • Font size
  • Blending coefficient, a decimal value from 0.0 to 1.0 (1.0 is fully opaque)
  • A valid TrueType font file

For example, the config file might look like this:
2
2
8
0.6
FreeSansoBold.ttf

Here is the program's code. Feel free to use it however you like, after all, it's just a few lines of code, but I'd appreciate credit ("Based on..." or "Inspired by...") if you derive something from it. In any case, if you find it useful, please leave a comment and tell me about it!
#include <pngwriter.h>
#include <stdlib.h>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;

int main(int argc, char * argv[])
{
if(argc != 4)
{
std::cout << "Wrong number of args (" << argc - 1 << "). Usage: ";
std::cout << "watermarker <config file> <UTF8 string file>";
std::cout << "<png image file>\n";
std::cout << "UTF8 string file should be a plain text or";
std::cout << "UTF8-formatted text file containing the copyright string";
std::cout << "to be used. \nConfig file should be a text file containing,";
std::cout << "on separate lines, \n";
std::cout << " pos_x;\n pos_y\n font_size\n blend coef.\n fontfile\n";
exit(1);
}

// Get copyright string into a binary array.
char * copyrightfile = argv[2];
char * copyright;
long size;
ifstream file (copyrightfile, ios::in|ios::binary|ios::ate);
if (! file.is_open())
{
std::cout << "Error opening UTF8 text file."; exit (1);
}
size = file.tellg();
file.seekg (0, ios::beg);
copyright = new char [size];
file.read (copyright, size);
file.close();

// Load configuration parameters.
std::ifstream params (argv[1]);
if (! params.is_open())
{
std::cout << "Error opening config file"; exit (1);
}

string filename(argv[3]);
string fontfile;
int pos_x;
int pos_y ;
int font_size;
double angle;
double blend;

params >> pos_x;
params >> pos_y;
params >> font_size;
params >> blend;
params >> fontfile;
params.close();

// Create PNGwriter instance
pngwriter image(1,1,0,"caca.png");
image.readfromfile(filename.c_str());

char * fn;
fn = new char[1024];
strcpy( fn, fontfile.c_str());

double average;
int minx, miny, maxx, maxy;
average= 0.0;
angle = 1.57079633;

minx = image.getwidth() - pos_x;
maxx = image.getwidth();
maxy = pos_y + image.get_text_width_utf8(fn, font_size, copyright);
miny = pos_y;

// Calculate the average intensity of the image
// behind the text.
for(int xx=minx; xx <=maxx; xx++)
{
for(int yy=miny; yy <=maxy; yy++)
{
average = average + image.dread(xx,yy);
}
}
average = average/( (double)( (maxx-minx)*(maxy-miny) ));
std::cout << "Average image intensity in text area: " << average;
std::cout << ", Number of pixels taken: " << (maxx-minx)*(maxy-miny);
std::cout << ", text will be";

// Plot the text on the image.
if(average>0.5)
{
std::cout << " black." <<std::endl;
image.plot_text_utf8_blend( fn, font_size,
image.getwidth() - pos_x, pos_y, angle,
copyright,
blend,
0.0, 0.0, 0.0);
}
else
{
std::cout << " white." <<std::endl;
image.plot_text_utf8_blend( fn, font_size,
image.getwidth() - pos_x, pos_y, angle,
copyright,
blend,
1.0, 1.0, 1.0);
}



// Rename the file, add -wmk to the basename.
char * newfilename;
char fl[1024];
strcpy(fl, filename.c_str());
newfilename = strtok (fl, "." );
strcat(newfilename, "-wmk.png" );
image.pngwriter_rename(newfilename);

image.settext(filename.c_str(), "Paul Blackburn",
"Copyright 2006 Paul Blackburn, paulwb AT gmail.com, All Rights Reserved.",
"Watermaked with PNGwriter plus a small program called watermarker.");

// Write the PNG image to disk.
image.close();

delete [] fn;

return 0;
}

Finally, if you'd like to have the program work on all images in a directory, you might find this shellscript useful:
#!/bin/bash

for caca in *.jpg
do
base=`basename $caca .jpg`
png=${base}.png
convert -depth 24 $caca $png
./watermarker watermarker.config copyrightstring.txt $png
convert -depth 8 ${base}-wmk.png ${base}-wmk.jpg
echo "Finished " ${base}-wmk.jpg
done

Here are two images that I've watermarked rather unobtrusively. The program selected white text for the first, and black text for the second.





I went through many iterations of this program before I found one that I liked. I wrote versions that detected portrait or landscape pictures and placed the text accordingly, versions that allowed an arbitrary angle to be set and so on. I think the current setup is the best balance between versatility and simplicity. If you find this does not suit your needs, go ahead and modify the code.

And that's how to watermark your photos, when none of the watermarking apps are good enough for you!


1 Comments:

Blogger durandal said...

I should note that the source code is available here.

10:11 PM  

Post a Comment

<< Home