Watermarking Your Printouts
Written by Larry Salomon Jr.
In the "how did they do that" category, watermarking is a concept that looks easy but, for those with little graphics experience (like me), is more difficult than apparent at first glance. For those who are not familiar with the term "watermark," it is a textual element that has the following properties:
- It is drawn behind all other elements on a printed page.
- It is unobtrusive so that it does detract from the reader's ability to easily read what else is on the page
- It is rotated at some arbitrary angle, usually spanning the page from the lower left corner to the upper right corner
Watermarking is usually performed to indicate a security level of a document ("Confidential") or to indicate that the printout comes from a demo or unregistered version of an application. There are, however, many other possible uses which you will discover.
This article will discuss how watermarking can be done in OS/2 and will briefly cover topics such as colour use, fonts, graphics boundary determination, and rotational transformations.
How Would You Do It?
Before we begin coding, we should consider a high-level design so that we know in what direction we are travelling; this will serve as our flowchart for the function which will do the watermarking.
Our first decision will be to determine what the interface is. This allows us to consider in advance the flexibility that we will offer the application developer using this function and design with this flexibility in mind. Since watermarking can be used in a variety of situations, the text to be used should be specified by the caller. The angle of rotation should also be specified by the caller to give more control over the look of the watermark. Finally, we need the handle of the presentation space to be used. We have, therefore, the following prototype:
BOOL doWatermark(HPS hpsPrn,PCHAR pchText,USHORT usAngle);
As a corollary exercise to designing the prototype, let us consider any limitations and/or restrictions that we will impose.
- As we will see, rotations can only be done on a "normal" presentation space, so the HPS must have been created with the GPIT_NORMAL flag in the call to GpiCreatePS().
- The presentation space must be associated with a printer device.
- The printer driver and/or device must support colour or dithering (i.e. no monochrome printers)
- If we were to rotate the text more than 90 degrees, it wouldn't be easily readable, so the angle should be in the range 0-90 inclusive.
Now we may proceed to determining the procedure for drawing the watermark.
Consider the following problem: we want the text to be as large as possible (without having it clipped on the printed page) but we cannot determine, at compile time, the size of each character. How do we calculate this at run time?
The solution is easy to describe: we set the size of the character box to some value that we know to be too big and then draw the text; if the text is too big, we reduce the size of the character box by some amount and repeat. When the text completely fits within the printed page, we stop looping.
Sounds easy, huh? Since this is the only "difficult" part of the design, we can now describe the procedure as such:
- Indicate a rotation of the number of degrees specified.
- Set the font to something "nice."
- Set the character size to be very large.
- Draw the text.
- If the text is too big, reduce the character size by some amount and go to step 4.
- Set the text colour to be very light.
- Draw the text using the calculated character size.
- Restore the font to the one originally in use.
This procedure will work, as we will see. For those of you sharp enough to catch the caveat that drawing the text in the determination part will obliterate the printout, keep reading.
Although the Gpi subsystem added the concept of a colour palette, I have never investigated whether or not palettes will work with hardcopy devices. To save myself the aggregation, I decided to stick with logical colour tables in RGB mode, which amounts to the same thing. Logical colour tables are a dated concept that worked well when the majority of displays were EGA or VGA and dithering had to be used to simulate the colours that we really wanted. Now that SVGA is considered standard equipment, logical colour tables are considered obsolete. In other words, I probably doomed myself by using them. They work for the moment, however, so I'll use them anyway.
A logical colour table is a table of colours which is requested by the application for use within a presentation space. Gpi maps these "logical" colours to the "physical" colours defined by the hardware palette. For those colours that cannot by described adequately, Gpi uses dithering (alternating two or three colours in a 2x2 or 3x3 matrix) to simulate the colour requested. These are normally used when the default RGB values for the CLR_ constants are not good enough and also when you want to specify the RGB value directly in the Gpi calls. We need the latter capability, so let's tell the Gpi subsystem this:
#define DW_TEXT_COLOR 0x00CCCCCC else if (!GpiCreateLogColorTable(hpsPrn,0,LCOLF_RGB,0,0,NULL)) ; else if (!GpiSetColor(hpsPrn,DW_TEXT_COLOR)) ; <pre> ''Figure 2: Switching to RGB mode.'' GpiCreateLogColorTable() takes the following parameters: <pre> BOOL GpiCreateLogColorTable(HPS hpsHps, ULONG ulFlags, LONG lMode, LONG lStart, LONG lCount, PLONG plTable);
hpsHps specifies the presentation space. ulFlags specifies zero or more LCOL_ flags and is unused by us. lMode specifies the mode of the colour table and affects how the remaining parameters are interpreted and may be one of the following:
- LCOLF_DEFAULT - returns the colour table to the default mode, which is indexed. This uses the CLR_ constants that we all know and love.
- LCOLF_INDRGB - indicates that plTable should be interpreted as an array of pairs of LONG values, where the first LONG is the index into the colour table where the second LONG (which is an RGB value) is to be placed.
- LCOLF_CONSECRGB - indicates that plTable specifies an array of LONG values (which is lCount large and contains RGB values) which replace the corresponding entries in the colour table beginning at lStart.
- LCOLF_RGB - indicates that lStart, lCount, and plTable are to be ignored and that RGB values will be used in any Gpi call that requires a colour.
Note how we specify a very light grey for the colour (0xCC is used for the red, green, and blue components of the colour). The results in almost transparent text.
In his book, Charles Petzold described a nice, generic way to enumerate the fonts on a system and set the current font to be one of your own choosing. While this is great for the general case and gleefully covers the situation when a font is not installed on a system, it is my opinion that, if you need one of the ATM fonts shipped with the system, this is too much work. Given the awe that high-quality graphics generate and the ever-shrinking cost of disk space, it is my claim that few people (if any) will choose not to install all of the fonts that OS/2 includes. With this in mind, I now present the "poor man's method of selecting an ATM font:" initialize a FATTRS structure with zeros (except for three fields), call GpiCreateLogFont(), and then call GpiSetCharSet().
Done. See how easy that was? Let's look at the details:
faFont.usRecordLength=sizeof(faFont); faFont.fsSelection=0; faFont.lMatch=0; strcpy(faFont.szFacename,"Helvetica"); faFont.idRegistry=0; faFont.usCodePage=GpiQueryCp(hpsPrn); faFont.lMaxBaselineExt=0; faFont.lAveCharWidth=0; faFont.fsType=0; faFont.fsFontUse=FATTR_FONTUSE_OUTLINE | FATTR_FONTUSE_TRANSFORMABLE;
Figure 3: Initializing the FATTRS structure.
As you can see, the only three fields that we initialize are the font name (FATTRS.szFacename), the codepage (FATTRS.usCodePage), and the flags describing what type of font we want (FATTRS.usFontUse). As you can also see above, initializing these fields is trivial. The flags FONTUSE_TRANSFORMABLE and FONTUSE_OUTLINE indicate that we want a font that is "transformable" (i.e. rotatable and resizeable) and is an ATM font, respectively.
Shown below is the code to actually acquire the font for use.
#define DW_FONT_HELV 1 //----------------------------------- // Create the logical font. //----------------------------------- else if (GpiCreateLogFont (hpsPrn, NULL, DW_FONT_HELV, &faFont) == GPI_ERROR) ; //----------------------------------- // Set the font into the HPS. //----------------------------------- else if (!GpiSetCharSet(hpsPrn,DW_FONT_HELV)) ;
Figure 4: Creating the logical font and setting it is the current font.
GpiCreateLogFont() creates an association between the HPS and the physical font. It takes the following parameters:
LONG GpiCreateLogFont(HPS hpsHps, PSTR8 psName, LONG lFontId, PFATTRS pfaAttrs);
hpsHps specifies the presentation space in which a logical font which represents the physical font will be created. psName is used for creating SAA-conforming metafiles and is not used normally (specify NULL for this). lFontId is the identifier by which the font will be known and must be unique for this HPS. pfaAttrs points to the FATTRS structure which describes the font we want. It returns FONT_MATCH if the font was found, FONT_DEFAULT if the font was not found but a substitute font was, or GPI_ERROR if an error occurred.
GpiSetCharSet() specifies the current font to be used and takes the following parameters.
BOOL GpiSetCharSet(HPS hpsHps,LONG lFontId);
hpsHps specifies the presentation space. lFontId is the identifier of the font used in the GpiCreateLogFont() call.
Now that we have the font we want, we have to know how to specify the size that we want. This only pertains to transformable fonts, which is why we specified FONTUSE_TRANSFORMABLE in the FATTRS structure.
Fixed Decimal Arithmetic
When the Gpi subsystem was developed, floating point processors were not part of the CPU, nor were they present as separate chips in the majority of computers used at that time. So, to avoid the penalty of emulating floating point operations in software, the Gpi developers decided to use fixed point arithmetic when non-integral numbers were needed. For those of you unfamiliar with the concept, fixed point arithmetic implies a decimal point, even though one is not actually specified in the number.
The Gpi subsystem adds a twist to this, however. Instead of using base-10, it uses base-10 only for the integer part of the number and base-64K for the fractional part. In other words, if you have fixed point number i.f, you convert this to a floating point number using the formula i+f/64K. This denominator was not chosen to be sadistic. You will remember that 64K-1 is the maximum value expressible in 16-bits, which allows us to imply the denominator and yet get a complete range of fractional values. If you restrict the integer portion to 16-bits also, the total number fits nicely in a ULONG.
Why is all of this relevant? Because character sizes are specified using fixed-point numbers, of course. Let me now introduce the SIZEF structure which is similar to the SIZEL structure except that the cx and cy fields are of type FIXED instead of LONG. Yes, yes, they amount to the same thing, but it's the principle that matters. The only other piece of information that you need to know is that fonts are designed such that they display with no distortion if and only if the horizontal component of the character size is equal to the vertical component. (The box that has the dimensions of the character size is known as the "character box.")
The code to set the character box (initially) is shown below. Note the (necessary) call to GpiSetCharMode() which tells Gpi that we want to rotate text in the presentation space.
//----------------------------------- // Get the HPS dimensions. // GpiQueryPS() does not have an // error return code defined. //----------------------------------- GpiQueryPS(hpsPrn,&szlHps); szfChar.cx=MAKEFIXED(szlHps.cy/2,0); szfChar.cy=szfChar.cx; : : //----------------------------------- // Set the character mode to 3 // so that we can rotate them. //----------------------------------- else if (!GpiSetCharMode(hpsPrn,CM_MODE3)) ; else if (!GpiSetCharBox(hpsPrn,&szfChar)) ;
Figure 5: Setting the character box to its initial value.
Again, see how easy this is? And you thought fonts were difficult!
One class that I had to take as a Computer Science major which was both interesting and boring at the same time was Linear Algebra. In this class we learned everything about matrices we ever wanted to know and even some stuff that we never wanted to know. (How many of you actually used your knowledge of Eigen vectors after graduating?) One interesting application of matrices is transformations. In a nutshell, by properly initializing a matrix of the appropriate size, one can scale, rotate, shear, or even map a point in 3-D space to 2-D space.
The Gpi subsystem provides two ways of doing transformations, just as it does fonts: the hard way and the easy way. Because we'll never finish this article if we describe the hard way, we'll use the easy way. As with fonts, the easy way doesn't provide the same flexibility as the hard way, but we don't need to perform all sorts of fancy transformations either. Just a simple rotation would be sufficient.
Fortunately, the easy way also includes a function to do this for us: GpiRotate(). It applies a rotation of an arbitrary angle to an entity called (as you would imagine) a transformation matrix.
BOOL GpiRotate(HPS hpsHps, PMATRIXLF pmlMatrix, LONG lFlags, FIXED fAngle, PPOINTL pptlOrigin);
hpsHps specifies the presentation space. pmlMatrix points to the transformation matrix, which we will not describe here. lFlags specifies how the rotation is to be performed. fAngle specifies the angle of rotation in degrees. pptlOrigin specifies the point around which the rotation is to be performed.
lFlags can be one of the following values:
- TRANSFORM_REPLACE - specifies that the result is to replace the transformation already specified in pmlMatrix.
- TRANSFORM_ADD - specifies that the rotation is to be applied after the transformation specified in pmlMatrix.
- TRANSFORM_PREEMPT - specifies that the rotation is to be applied before the transformation specified in pmlMatrix.
The resulting transformation matrix is returned in pmlMatrix regardless of the option specified.
What does this do for us? When the resulting transformation matrix is applied to the HPS, it allows us to draw using a coordinate system oriented in the usual fashion (i.e. the x axis is horizontal and increases to the right and the y axis is vertical and increases upward). The output, however, is rotated by the angle specified.
The Gpi subsystem provides many layers of transformation, and we will not describe them here because they are too advanced for the scope of this article (read: I don't understand them myself). Suffice it to say that the function we need to effect the transformation represented by pmlMatrix is GpiSetModelTransformMatrix(). It takes the following parameters:
BOOL GpiSetModelTransformMatrix(HPS hpsHps, LONG lInitFields, PMATRIXLF pmlMatrix, LONG lFlags);
hpsHps specifies the presentation space. lInitFields is the number of fields in pmlMatrix that are initialized and, for us, will always be 9 (meaning that a transformation matrix has size 3x3). pmlMatrix specifies the transform to be applied. lFlags specifies how the transformation is to be applied and can have the same value as the equivalent parameter in GpiRotate().
The code to set the rotation is shown below:
//----------------------------------- // Rotate about the center of the page. //----------------------------------- else if (!GpiRotate(hpsPrn, &mxRotate, TRANSFORM_REPLACE, MAKEFIXED(usAngle,0), &ptlPoint)); else if (!GpiSetModelTransformMatrix(hpsPrn, 9, &mxRotate, TRANSFORM_REPLACE));
Figure 6: Setting up the rotation.
The title of this section implies that it is possible to determine the boundaries of any given set of graphics commands. Since it would be useless otherwise, it makes sense to assume that it is also possible to do so without affecting the current output in the HPS.
There is a little known, but powerful, Gpi function which performs both of these actions: GpiSetDrawControl(). It, in a general sense, controls whether or not specific functionality in the Gpi is in effect. (Yes, this is confusing. Keep reading and it should make sense.) It takes the following parameters:
BOOL GpiSetDrawControl(HPS hpsHps,LONG lControl,LONG lValue);
hpsHps specifies the presentation space. lControl specifies a DCTL_ constant indicating which attribute of the Gpi subsystem you which to change. lValue specifies DCTL_ON or DCTL_OFF to turn the attribute on or off, respectively.
The two values of lControl that we're interested in are DCTL_DISPLAY and DCTL_BOUNDARY:
DCTL_DISPLAY specifies whether or not anything is to be drawn on the output device. With the exception of GpiErase() any drawing function results in a NOP if DCTL_OFF is specified.
DCTL_BOUNDARY specifies if the Gpi subsystem should keep track of the extents of the units (pels, low English, etc.) that were affected by a drawing command. This information can be retrieved using GpiQueryBoundaryData(). It is not affected by the state of the DCTL_DISPLAY attribute.
You should already be able to see how we are going to use this function.
//----------------------------------- // Turn off drawing and turn on // boundary collection. //----------------------------------- else if (!GpiSetDrawControl(hpsPrn, DCTL_DISPLAY, DCTL_OFF)) ; else if (!GpiSetDrawControl(hpsPrn, DCTL_BOUNDARY, DCTL_ON)) ; : : //----------------------------------- // Turn on drawing and turn // off boundary collection. //----------------------------------- else if (!GpiSetDrawControl(hpsPrn, DCTL_DISPLAY, DCTL_ON)) ; else if (!GpiSetDrawControl(hpsPrn, DCTL_BOUNDARY, DCTL_OFF)) ;
Figure 7: Setting the drawing and boundary data collection attributes.
The code above allows us to, inside the bracket, determine the character box size for the watermark.
This stuff is so easy, you should be writing this article instead of me.
We now have enough knowledge to successfully implement the design that we deduced earlier. The function as well as the application which calls the function are in the sample source code that accompanies this article. Although you should be able to understand the code completely, any questions or comments are welcome via email. My address is >. Enjoy!