[gs-devel] colour, colour spaces and Ghostscript

Ken Sharp ken.sharp at artifex.com
Sat May 17 03:43:37 PDT 2008


Morning all,

For the last few weeks I've been looking into the way Ghostscript currently 
handles color and more particularly color spaces in PostScript. Ralph asked 
me to summarise what I've found so far, and also to document a problem I've 
run up against. The first part of this (how it works now) is probably of 
limited interest to anyone outside the color group, feel free to skip this 
(jump ahead to "DeviceN bug").

However I'd welcome any opinions on the second part, which deals with what 
I think is a bug in Ghostscript relating to DeviceN and potentially other 
color spaces. If someone can tell me what I'm doing wrong I'd be grateful. 
Alex, there's a question about the PDF interpreter in there which I'd 
really like your input on please.

How it works now
=================
I was asked to look into converting the existing complex PostScript 
structure into C, to dispense, at least as far as possible, with the 
PostScritp interpreter. Because PostScript spaces use tint transform 
procedures, which can be written in arbitrary PostScript, dispensing with 
the PostScript interpreter entirely isn't an option, at least for 
PostScript color spaces.

If the PDF interpreter were to be re-implemented in C it would be possible 
(for PDF), since the tint transform procedure is already written as a 
Function, and we don't need to run a full PostScript interpreter (NB 
PostScript calculator functions would still need some kind of interpreter).

More useful would be the ability to set ICC color spaces without relying on 
the PostScript interpreter, since these are the basis of color in XPS and 
likely to be so for any other page description languages.

Anyway, following the usual Ghostscript design policy for the PostScript 
interpreter, much of the work involved in color is currently done in 
PostScript, with specific custom operators (eg .setdevicenspace) to perform 
the actual kernel work. Surprisingly perhaps, the kernel 'C'  setcolorspace 
operator (zsetcolorspace in zcolor.c) does almost nothing, the actual work 
being done by the custom operators.

The current implementation creates PostScript redefinitions of setcolor, 
setcolorspace, setgray, setrgbcolor, sethsbcolor, setcmykcolor, 
currentgray, curretnrgbcolor, currenthsbcolor and curretncmykcolor. In 
addition a dictionary 'colorspacedict' is defined in global VM, and used by 
these procedures.

The code dealing with this is in /lib/gs_cspace.ps and is well documented. 
The general form is an object-oriented implementation, colorspacedict 
contains a key (the space name) for every supported color space and a 
dictionary for each space. The color space specific dictionary contains a 
number of procedures (methods) such as cs_validate, cs_install etc. Spaces 
are added to the dictionary by gs_ciecs2.ps, gs_ciecs3.ps, gs_devcs.ps, 
gs_devn.ps, gs_devpxl.ps, gs_icc.ps, gs_indxd, gs_patrn.ps, gs_sepr.ps.

When a color space is set, the setcolorspace routine in gs_cspace.ps finds 
the name of the space either given directly as a base color name such as 
/DeviceGray or as the first entry from a color space array. The dictionary 
whose key in colorspacedict matches the color space name is extracted, and 
the methods in that dictionary used to actually install the color space via 
the custom operators.

One interesting wrinkle is that complex colour spaces are installed 
'backwards'. That is, each color space install method starts by setting the 
current color space to be the alternate for this space, and this is done 
recursively until we reach a color space which has no alternate. For 
example, the space:

[/Indexed [/Separation (Pink) /DeviceCMYK {0 exch 0 0}] 256 <..elided 
lookup..>>

Starts by setting the Indexed space, which sets its alternate space, 
Separation. The Separation space method then sets the base CMYK space. On 
return from setting the base space, the Separation method sets the current 
space to be /Separation. Finally, on return from the Separation space, we 
set the current space to be /Indexed. This recursion is all done in the 
PostScript code.

Now, Ghostscript also converts PostScript tint transform procedures into 
Functions, either type 4 (PostScript calculator) or type 0 (sampled). If we 
create a sampled function, the more common case, then we will (obviously) 
execute code which samples the color space. (We do this even if we will 
never actually use the alternate space, which potentially costs performance)

This is, at least partially, the reason why spaces are installed from the 
back forwards. In order to build a function dictionary the C code must know 
how many inputs and outputs are required. The number of inputs is given by 
the current space, but the number of outputs is given by the alternate 
space. By setting the current space to the alternate before we set the 
parent space, the current 'C' colorspace object gives us this information 
directly. Otherwise we would have to build a lot of common intelligence 
into each color space specific method in the kernel so that it could 
determine how many outputs it required.

There may be other reasons for this, if anyone knows what they are, I would 
very much appreciate the information, the comments in the PostScript and C 
both say that the current design means that spaces need to be installed 
this way, but give few details on why.


DeviceN Bug
============
While working on converting the color space handling so that the use of 
PostScript is minimised, I ran into the well-known bug with DeviceN and 
Photoshop 5+  I'll recap the bug details below, if you already understand 
i, skip ahead to 'Problem'.

Photoshop emits PostScript which, on level 3 devices, makes use of the 
DeviceN color space for multi-tone (duotone, tritone etc) images. To do 
this, it starts by setting a DeviceN space which contains all the required 
inks with an alternate of DeviceGray, the tint transform procedure is 
unusual in that it removes the ink tint percentages from the stack, and an 
additional extra operand. It then places a 'false' on the stack (replacing 
the extra operand) and a '0' for the tint. Eg:

[ /DeviceN [ /Black (PANTONE 541) ] /DeviceGray {3 {pop} repeat false 0 } ] 
setcolorspace

On the face of it this is odd behaviour. What it is actually intended to do 
is probe the interpreter to see whether the DeviceN space can be directly 
supported or not. After having set the space, the job executes:

true 0 0 setcolor

Now, if the DeviceN space is supported (ie all inks are present) then the 
tint transform procedure will not be executed during setcolor, and the 
stack will contain a 'true'. However if the space is not supported, then 
the tint transform will be executed and the stack will contain a 'false'.

In itself this violates part of the specification which states that 
"Because the tint Transform procedure is called by the setcolor and image 
operators at unpredictable times, it must operate as a pure function 
without side effects." Manipulating an extra object on the stack is pretty 
clearly a side-effect.

Arguably this is valid because the specification also states that if the 
space is supported then the alternative and tint transform are ignored.

In any event, the resulting Boolean is then used by the job to decide 
whether to load a DeviceN space, or a different color space (always some 
flavour of CIE) :

/PhotoshopDuotoneColorSpace [ /Indexed [ /DeviceN PhotoshopDuotoneList [ 
/DeviceGray ] { /NeverReached 4 {pop} repeat 0 } ] 255
<...
 > ] def
/PhotoshopDuotoneAltColorSpace [ /Indexed [ /CIEBasedABC <<
/MatrixLMN [0.9505 2E-5 0  0 1 0  0 2E-5 1.0890]
/DecodeLMN [{2.2 exp} dup dup]
/WhitePoint [0.9505 1 1.0890] >> ] 255
<...
 > ] def { PhotoshopDuotoneColorSpace } { PhotoshopDuotoneAltColorSpace } 
ifelse

It would, of course, be more sensible to simply set the CIE space as the 
alternate for the DeviceN space and allow the interpreter to make the decision.


Problems
===========
The first problem here is that the tint transform procedure for the DeviceN 
space has a bug. Although the space involves two inks, and the tint 
transform pushes a name, the procedure pops 4 values from the stack. There 
appears to be code in the sampled function routines to deal with this, but 
they do so by returning an 'undefinedresult' error. So if we were to sample 
this procedure, as required for the conversion to a PostScript function, we 
would have a problem.

Notice that none of these execute 'bind' so Idiom Recognition isn't really 
an option.


So why don't we have a problem ? This is the more serious issue, and I 
would welcome any extra information. At the moment I think we have a 
moderately serious bug which I'm surprised hasn't been reported by a customer.

setcolor is defined in gs_cspace as follows:

/setcolor
   {
       {
         currentcolorspace //.cs_prepare_color exec //setcolor
         currentcolorspace //.cs_complete_setcolor exec
       }
     stopped
       { //.cspace_util /setcolor get $error /errorname get signalerror }
     if
   }
bind odef

The '.cs_prepare_color' and '.cs_complete_setcolor' procedures extract the 
methods with the same name from the colorspacedict, using the current color 
space to select the correct instance, and then executes them.

Notice that we first execute the 'C' level setcolor routine, and then 
execute an additional 'complete setcolor' method. The comments for 
cs_complete_setcolor say:

% cs_complete_setcolor
%   This method is invoked immediately after a (successful) invocation
%   of setcolor. Ii is provided as a separate method for compatibility
%   with Adobe implementations. These implementations invoke the lookup
%   (Indexed) or tint procedure each time setcolor is invoked (only if
%   the alternative color space is used in the case of the tint
%   transform). Because Ghostscript may convert these procedures to
%   functions (or pre-sample them), the procedures may not always be
%   called when expected. There are applications that depend on this
%   behavior (e.g.: Adobe PhotoShop 5+), so this method provides a way
%   to emulate it.

So that looks good. If we are using the alternate space, then this will 
execute the tint transform for the benefit of this kind of Adobe code. The 
definition in gs_devn.ps also looks fine:

     /cs_complete_setcolor
       {
         .usealternate
           {
             pop
             currentcolor
             currentcolorspace 3 get exec
             currentcolorspace 2 get
             //clear_setcolor_operands exec
           }
           { pop }
         ifelse
       }
     bind

So if '.usealternate' is true we will execute the tint transform. Good 
enough. Checking the definition of .usealternate to see how it 'knows' 
whether the alternate is being used or not (zcolor2.c):

static int
zusealternate(i_ctx_t * i_ctx_p)
{
     os_ptr                  op = osp;
     const gs_color_space *  pcs = gs_currentcolorspace(igs);

     push(1);
     make_bool(op, pcs->base_space != 0);
     return 0;
}

So it is true if the current color space has a non-zero 'alternate' Still 
sounds OK, but....

In zcsdevn.c, zsetdevicenspace:

     /* The alternate color space has been selected as the current color 
space */
     pacs = gs_currentcolorspace(igs);

     code = gs_cspace_new_DeviceN(&pcs, num_components, pacs, imemory);

and then in gscdevn.c, gs_cspace_new_DeviceN :

     pcs->base_space = palt_cspace;


Bearing in mind that we set color space alternates backwards, by the time 
we reach zsetdevicenspace the current color space has already been set to 
the alternate, and the gs_color_space object base_space member will then be 
set to that. So when we execute .usealternate, base_space will always be 
non-zero.

This is why we don't get an error sampling the broken tint transform, the 
Photoshop job thinks we can't support the spot color, and instead uses the 
CIE color space.



To test this, I used two files, the first a simple hand crafted file:

%!
[/DeviceN [/Pink] /DeviceGray {}] setcolorspace
0.5 setcolor
10 10 100 100 rectfill
.usealternate ==            %% Are we using the alternate space ?
showpage

This always echoes true to sdout.

I also tried the file from bug 688584. Unfortunately this is rather a large 
file, but it does contain a genuine Adobe Photoshop duotone. I have reduced 
the file size, but it is still 11 Mb.

I tested these two files using the following command lines:

gswin32c -sDEVICE=tiffsep -sOutputFile=test.tif test.ps
gswin32c -sDEVICE=tiffsep -sOutputFile=test.tif -c "<< 
/SeparationColorNames [/Pink] >> setpagedevice" -f test.ps

gswin32c -sDEVICE=tiffsep -sOutputFile=test.tif Bug688584.ps
gswin32c -sDEVICE=tiffsep -sOutputFile=test.tif -c "<< 
/SeparationColorNames [(PANTONE 541) cvn] >> setpagedevice" -f Bug688584.ps

My own test file renders a rectangle to the spot plate as expected in both 
cases. The duotone renders the image to the Cyan, Magenta and Yellow plates 
in both cases, when it *should* be rendered to the Black and spot plates, 
both of which are blank.


This seems like a bit of a hole to me, can anyone tell me if I'm doing 
something wrong ? It appears to me that Photoshop duotones will never 
render on the correct plates.

Alex, I'm having some similar problems with PDF files, do you know if the 
PDF interpreter does any kind of similar operations to the Adobe Photoshop 
job ?



                             Ken

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TASK: Shoot yourself in the foot.
HTML 1 You shoot yourself in the foot, only to find out that no matter 
how gory the result looks, your foot keeps working. Your foot finally 
stops working when you stub your toe kicking the box the gun came in.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



More information about the gs-devel mailing list