Applying the Watermark
In the ImageSign category the apply() method (Listing 4) embeds the watermark into the target image. It uses the LSB algorithm described earlier in this article. The method begins by converting the image data into a block of raw pixels, tBuf (lines 18-22). Then it renders the watermark hash into an unsigned character stream, tBgn (lines 27-29).
Listing Four
// Apply the watermark to the target image
- (NSImage *)apply:(NSData *)aMrk
{
CGFloat tRed, tGrn, tBlu, tAlp;
NSUInteger tR, tG, tB;
NSUInteger tX, tY, tXm, tYm;
NSUInteger tLen, tIdx, tCnt, tLSR;
NSImage *tMrk;
NSColor *tOld, *tNew;
NSData *tDat;
NSBitmapImageRep *tBuf;
unsigned long tByt, tLsb;
unsigned char *tBgn, *tTmp, tChr;
// convert the image data into a byte stream
tDat = [self TIFFRepresentation];
if (tDat != nil)
{
// prepare the pixel buffer
tBuf = [NSBitmapImageRep imageRepWithData:tDat];
[tBuf retain];
if (tBuf != nil)
{
// prepare the watermark buffer
tLen = [aMrk length];
tBgn = (unsigned char *)malloc(tLen);
[aMrk getBytes:tBgn];
// start parsing the buffer
tXm = [tBuf pixelsWide];
tYm = [tBuf pixelsHigh];
for (tY = 0; tY < tYm; tY++)
{
// read the first signature byte
tTmp = tBgn;
tChr = *tTmp++;
tByt = (unsigned long)tChr;
tIdx = 1;
tCnt = 8;
for (tX = 0; tX < tXm; tX++)
{
// read the original colour
tOld = [tBuf colorAtX:tX y:tY];
// extract the colour bytes
[tOld getRed:&tRed green:&tGrn
blue:&tBlu alpha:&tAlp];
// convert the normalised colours
tR = (long)(tRed * 255);
tG = (long)(tGrn * 255);
tB = (long)(tBlu * 255);
// mask out the least significant bits
tR &= 0xfc;
tG &= 0xfc;
tB &= 0xfc;
// insert the watermark bits
// -- colour:byte:red
tLSR = tCnt - 2;
tLsb = (tByt >> tLSR);
tLsb &= 0x03;
tR |= tLsb;
// -- colour:byte:green
tLSR = tCnt - 4;
tLsb = (tByt >> tLSR);
tLsb &= 0x03;
tG |= tLsb;
// -- colour:byte:blue
tLSR = tCnt - 6;
tLsb = (tByt >> tLSR);
tLsb &= 0x03;
tB |= tLsb;
// check the bit counter
tCnt -= 6;
if (tCnt < 6)
{
// check the byte index
if (tIdx == tLen)
{
// reset the watermark buffer
tTmp = tBgn;
tIdx = 1;
}
else
tIdx++;
// update the bit buffer
switch (tCnt)
{
case 2:
tByt &= 0x03;
tByt <<= 8;
break;
case 4:
tByt &= 0x0f;
tByt <<= 8;
break;
default:
tByt = 0;
} // switch (tCnt)
tChr = *tTmp++;
tByt |= (unsigned long)tChr;
tCnt += 8;
}
else if (tCnt == 6)
tByt &= 0x3f;
// prepare the normalised colours
tRed = tR / 255.0;
tGrn = tG / 255.0;
tBlu = tB / 255.0;
// prepare the modified colour
tNew = [NSColor colorWithDeviceRed:tRed
green:tGrn
blue:tBlu
alpha:tAlp];
// apply the modified colour
[tBuf setColor:tNew atX:tX y:tY];
} // for (tX = 0; tX < tXm; tX++)
} // for (tY = 0; tY < tYm; tY++)
// prepare the marked image
tDat = [tBuf TIFFRepresentation];
tMrk = [[NSImage alloc] initWithData:tDat];
// dispose the buffers
[tBuf release];
free(tBgn);
} // if (tBuf != nil)
} // if (tDat != nil)
// return the marked image
return ([tMrk autorelease]);
}
Next, the apply() method parses the raw pixel block, moving from top to bottom, left to right. At the start of each pixel row, the method reads the first byte from the watermark hash, tChr (lines 37-41). Then it reads a pixel from the block and divides that pixel into bytes of red, tR; green, tG; and blue, tB (lines 46-55). Note the method assumes a 24-bit RGB as the color model.
The apply() method masks out bits 0 and 1 from each color byte (lines 58-60). It reads two bits from the hash byte tChr and inserts them into the red byte tR. Then it repeats the same steps for green and blue (lines 64-79). Once all three color bytes were modified, apply()decrements the bit counter tCnt by six (line 82). If the counter falls below eight, the method reads the next watermark byte. It then shifts tChr to the left by eight and appends the new byte to the result (lines 93-108). But if all the watermark bytes were used, apply() resets the tTmp buffer back to its starting address (lines 89-90).
The apply()method then normalizes the modified color bytes and writes them to the pixel buffer, using the same position as the unchanged pixel (lines 117-128). After it finishes a row of pixels, the method resets the watermark buffer and the two counters (lines 37-41), and again reads the first watermark byte. Once it has modified the pixel buffer, apply() uses it to create a new NSImage object tMrk. It disposes the pixel and watermark buffers, and returns the image object with its autorelease flag set (lines 134-144).
Now the apply() expects its host NSImage to have a valid image data. Otherwise, apply()will fail quietly, without any errors.
Measuring the Watermark
The next method in the ImageSign category is the coverage() method (Listing 5). This one examines the image data and counts the number of intact watermarks it finds.
Listing Five
// Measure the watermark coverage on the target image
- (NSNumber *)coverage:(NSData *)aMrk;
{
CGFloat tRed, tGrn, tBlu, tAlp;
NSUInteger tR, tG, tB;
NSUInteger tX, tY, tXm, tYm;
NSUInteger tFnd, tCnt;
NSBitmapImageRep *tBuf;
NSColor *tPix;
NSData *tDat;
NSMutableData *tMrk;
unsigned long tByt;
unsigned char tChr;
double tRef, tPer;
// convert the image data into a byte stream
tDat = [self TIFFRepresentation];
tPer = 0.0;
if (tDat != nil)
{
// prepare the pixel buffer
tBuf = [NSBitmapImageRep imageRepWithData:tDat];
// prepare the watermark buffer
tMrk = [NSMutableData dataWithCapacity:0];
if ((tBuf != nil) && (tMrk != nil))
{
// initialise the following locals
tXm = [tBuf pixelsWide];
tYm = [tBuf pixelsHigh];
tFnd = 0;
// calculate the expected number of watermark matches
tRef = tXm * tYm * 3.0 / 64.0;
// start parsing the pixel buffer
for (tY = 0; tY <= tYm; tY++)
{
// prepare to parse a pixel row
tByt = 0x00;
tCnt = 0;
[tMrk initWithLength:0];
// parse the row
for (tX = 0; tX <= tXm; tX++)
{
// read the pixel colour
tPix = [tBuf colorAtX:tX y:tY];
// extract the colour bytes
[tPix getRed:&tRed green:&tGrn
blue:&tBlu alpha:&tAlp];
// convert the normalised colours
tR = (long)(tRed * 255);
tG = (long)(tGrn * 255);
tB = (long)(tBlu * 255);
// reconstruct the watermark byte
tR &= 0x03;
switch (tCnt)
{
case 0:
tR <<= 6;
tByt |= tR;
tCnt += 2;
break;
case 2:
tR <<= 4;
tByt |= tR;
tCnt += 2;
break;
case 4:
tR <<= 2;
tByt |= tR;
tCnt += 2;
break;
case 6:
tByt |= tR;
tCnt = 0;
// update the watermark buffer
tChr = (char)tByt;
[tMrk appendBytes:&tChr length:1];
if ([tMrk length] == [aMrk length])
{
if ([tMrk isEqualToData:aMrk])
tFnd++;
[tMrk initWithLength:0];
}
// prepare to receive the next watermark byte
tByt = 0x00;
}
tG &= 0x03;
switch (tCnt)
{
case 0:
tG <<= 6;
tByt |= tG;
tCnt += 2;
break;
case 2:
tG <<= 4;
tByt |= tG;
tCnt += 2;
break;
case 4:
tG <<= 2;
tByt |= tG;
tCnt += 2;
break;
case 6:
tByt |= tG;
tCnt = 0;
// update the watermark buffer
tChr = (char)tByt;
[tMrk appendBytes:&tChr length:1];
if ([tMrk length] == [aMrk length])
{
if ([tMrk isEqualToData:aMrk])
tFnd++;
[tMrk initWithLength:0];
}
// prepare to receive the next watermark byte
tByt = 0x00;
}
tB &= 0x03;
switch (tCnt)
{
case 0:
tB <<= 6;
tByt |= tB;
tCnt += 2;
break;
case 2:
tB <<= 4;
tByt |= tB;
tCnt += 2;
break;
case 4:
tB <<= 2;
tByt |= tB;
tCnt += 2;
break;
case 6:
tByt |= tB;
tCnt = 0;
// update the watermark buffer
tChr = (char)tByt;
[tMrk appendBytes:&tChr length:1];
if ([tMrk length] == [aMrk length])
{
if ([tMrk isEqualToData:aMrk])
tFnd++;
[tMrk initWithLength:0];
}
// prepare to receive the next watermark byte
tByt = 0x00;
}
} // for (tX = 0; tX <= tXm; tX++)
} // for (tY = 0; tY <= tYm; tY++)
// calculate the percent coverage
tPer = tFnd * 100.0 / tRef;
} // if ((tBuf != nil) && (tMrk != nil))
} // if (tDat != nil)
// return the metric result
return ([NSNumber numberWithDouble:tPer]);
}
Again, the method starts by rendering the image data into a block of raw pixels tBuf (lines 18-22). Then it creates an empty NSData object tMrk, with which to hold the watermark bytes (line 27). It also computes the expected number of watermarks, based on the image size and color depth (line 37).
Now the coverage() method parses the pixel buffer from top to bottom, left to right. At the start of each row, it resets its byte buffer tByt, its bit counter, tCnt and the watermark buffer tMrk (lines 43-45). Next, it reads a pixel from the buffer and divides the pixel into bytes of red, green and blue. From each byte, it extracts bits 0 and 1 and appends them to tByt (lines 66-83 for red byte).
Once tByt holds eight bits, coverage() appends the byte to the buffer tMrk (lines 121-122 for red byte). It resets the bit buffer tByt and the bit counter tCnt. Then it extracts the next eight bits. Once tMrk holds 16 bytes, the size of an MD5 hash value, coverage() compares it against the expected watermark aMrk. If both watermarks match, the method increments the tFnd counter (lines 90-92). And it clears tMrk, making it ready to hold the next watermark.
Finally, once coverage() has parsed all of the image pixels, it calculates how much of the image was covered by the watermark (line 172). It returns the result, a percentage, as an NSNumber object (line 178).


