2006-05-08

Shading in PostScript

I've recently been beating my head against PostScript's gradient ("shading") system. Plenty of documentation and examples are available for when you're working in the DeviceGray color space, but when you're in DeviceRGB, it gets hard. Part of this is due to the fact that the PostScript Language Reference Manual's description of shadings and the functions that power them is overly abstract and decorated with too much math for my language-oriented mind.

So, here I'll try and explain it more clearly, particularly by explaining functions in the context of shadings, rather than separately.

A shading in PostScript is represented by a dictionary. Valid keys in the shading dictionary are:

ShadingType
The type of gradient: axial, radial, etc.
ColorSpace
The color space of colors returned by the function.
Coords
The start and end of the gradient on the page. Defined more specifically by the PLRM.
Function
A function dictionary, or an array of them.

Functions aren't as you expect them to be. Rather than a callable object (a procedure), a function is a dictionary. (This, BTW, is the architectural reason for Quartz's CGFunction class, although CGFunction actually uses a C function callback — what would be a procedure in PostScript.) There are three types of function dictionaries; I'll only deal with type 0, a sampled function (a "sample" is a step in the gradient). A sampled function contains, or fetches from a file, the samples it will return, in a "data source".

FunctionType
Guess.
Domain
Two numbers for every input, constraining that input to a range. If an input is below the first number, it will be replaced with the first number; if it is above the second number, it will be replaced by the second number. Generally, these two numbers are 0 and 1. In an axial shading function, there is only one input, so your domain should be two numbers (e.g. [0 1]).
Range
The other end from Domain. Works the same way, but it constrains your outputs rather than your inputs. For a shading function, there is one pair of numbers for each component. So, in DeviceGray, Range should be two numbers; in DeviceRGB, it should be six.
BitsPerSample
Named slightly misleadingly (the PLRM clarifies), this is actually the number of bits per component. So if you're doing 24-bit RGB, this should be 8.
Size
An array containing the number of samples as an int.
DataSource
Either a file or a string from which to get the sample data.

You might expect that the samples should be given as an array. You would be wrong. Instead, PostScript wants pure binary data, either from a file or encoded as a string. So, for example, in RGB, every three characters (bytes) are one sample. I use a hexadecimal literal, or when I want to use an array, this procedure (which you may consider to be under the modified BSD license):

% array -> hexstr -> string % Converts an array of integers to a string. [ 65 66 67 13 10 ] -> (ABC\r\n). /hexstr { 4 dict begin /elements exch def /len elements length def /str len string def /i 0 def { i len ge { exit } if str i %The element of the array, as a hexadecimal string. If it exceeds 16#FF, this will fail with a rangecheck. elements i get cvi put /i i 1 add def } loop str end } def

And yes, I know that that procedure has nothing to do with hexadecimal. I was probably tired when I wrote it.

Note: If you use hexstr to create a shading dynamically from an array of samples, be aware that when you divide the length of the array by 1 (gray) or 3 (RGB) or 4 (CMYK) to get the proper value for Size, you must use cvi on the quotient, because div returns a real and makepattern demands an int.

Technorati tags: .

0 comments:

Post a Comment

<< Home