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.
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:
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:
For example, the config file might look like this:
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!
Finally, if you'd like to have the program work on all images in a directory, you might find this shellscript useful:
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!
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
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;
}
#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
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.


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


The Lagoons of the Santuario de la Naturaleza 2: Laguna Los Ángeles
Race Day At Leyda 4
El Tabo and the Central Hidroeléctrica El Sauce
Exploring The Hills Around Lampa
A Different Route To Baños De Colina
The Mines of the Cuesta La Dormida
The Frozen Lagoons of the Santuario de la Naturaleza
Second Mass Demonstration "For A Fair Tag"
First Mass Demonstration Against The 'Tag'
Enduro In Lagunillas
Embalse El Yeso and Termas Del Plomo
Ride To Peñuelas
Cerro Chena
Race Day at Leyda 3
Baños de Colina 2
Carretera Austral: Epilogue
The Little Giant and Termas del Plomo
Back on Two Wheels
2006 Photographic Retrospective
Race Day At Leyda 2
Quantum Optics III in Pucón
Meseta In Chicureo
Pick Up Your Beer Bottle And Fuck Off
Planes And Hills
Cut-Off Road
Lagunillas
Laguna Verde 2
Ride To Anywhere But Aculeo
Cerro El Roble, Second Attempt
Baños De Colina
Some Walk On Water...
Race Day At Leyda
Almost Cerro El Roble
Off To Curacaví with Andrés
La Serena, Part 3: Back To Santiago
A Bull, Two Cows and a Chilean Fox
Escape To Cuesta La Dormida
Valve Adjustment
La Serena, Part 2B: Valle Del Elqui
La Serena, Part 2A: Coquimbo and La Recova
Mud And Pine Trees
La Serena, Part 1
Pimp My Exhaust
Ride To Laguna Verde
Ride To La Mina
Ride To Termas El Plomo
Camping in Colliguay
Ride To Portillo
Ride To Olmué and Con Con
Siete Tazas
Watching The Departure Of The Day That Brought Me Here
Buenos Aires Motorbikes
Ride to Talca with the Adach Group
Las Trancas '05
Towers and Hills
María Pinto, Melipilla, Aculeo
Me and my Carb




1 Comments:
I should note that the source code is available here.
Post a Comment
<< Home