[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