Rendering individual colors as plates in cairo
This is a developer scratch pad issue for discussing and designing what a cairo based CMYK renderer would look like driven through liblcms2
The problem
The rendering library used by Inkscape is cairo, it only has the ability to render 3 channels of various pixel formats and an alpha channel. None of the available color modes are CMYK so we've never been able to render icc based cmyk profiles correctly in Inkscape to screen.
This matters because converting the colors to sRGB and then rendering causes artefacts in non-solid portions of the drawing. Blending a gradient between converted-to-rgb-cyan and converted-to-rgb-magenta produces different intermediary results when converted back to cmyk for printing, usually including yellow and black inks right where you would just expect a gradual shift between two inks.
You can see below in the proof of concept that the sRGB rendering has a different gradient between the magenta and soft orange background than the cmyk rendering. This is the kind of result we expect.
Definitions
- an icc is a color profile file which defines how transformations from one type of color space to another can be achieved.
- lcms2 is LittleCMS library, the main color conversion library that uses icc profiles
- cairo is a vector image rendering engine which Inkscape has used since the beginning
- A color plate is a single gray scale image where each pixel value is the value of that pixel in that channel
Proof of Concept Method
We want to know if it's possible to use cairo to render out our cmyk channels with multiple passes and use lcms2 to combine them back into sRGB for display.
We used this patch render_cmyk_channels_as_red.patch on top of the Color refactoring branch (as of May 12th 2024) to convert the RGB colors in a sample svg into their icc profile CMYK versions with this icc profile cmyk.icc
Original SVG |
---|
The code was compiled 4 times with a different channel values from 0 to 4, each run the svg file was loaded and a png was exported producing these single channel plate files:
Cyan | Magenta | Yellow | Black |
---|---|---|---|
The results show that the red channel has been encoded with the information of each of the cmyk channels from our image.
Next we use image magic from the command line to turn these into grey images convert sep_test_{channel}.png -channel R -separate ${c}.png
where channel and c stand for each of their cmyk channels, the results were:
C | M | Y | K |
---|---|---|---|
These results appear to show inverted images, if you are used to Black being ink and White being no-ink. Instead think of these images as "white is positive numbers, means put more of this ink here" and black is "lower numbers, put less ink here". When combined into a true cmyk image, the values are correct.
Finally we used ImageMagic to convert each of the channels back into a cmyk image without the icc profile, so the results are not perfect but illustrative convert c.png m.png y.png k.png -set colorspace CMYK -combine +write info: out.tiff
CMYK Tiff |
---|
Lumping or Splitting
Because Cairo only supports three channels of rendering and we need 4, we must do multiple passes in order to render out all of the required channels.
It's possible to combine the cmy plates into one pass as rgb and then process the k channel as it's own rendering pass. Or we might choose to process each channel on it's own in 4 passes, depending on the results we get, the complexity and the data formats available in cairo.
We may also want to use floating point output using 128bits per pixel, or stick with 8 bits per channel. These pixel formats are all valid in lcms2 so it depends on how accurate we want the conversion to be. Cairo also has no gray channel format, but does have an alpha only format. It's unknown if this alpha only format would work to save memory use as having two or four RGBA Cairo surfaces might be wasteful if we've only picking 1/4 of the bytes out of the plates.
Using LCMS2
In the proof of concept we use ImageMagic to split and combine the channels, but we can use lcms2 to do two steps of conversion in code, one to combine the plates and the second to convert them into sRGB for display.
LCMS2 has the concept of Memory Management cmsPlugin
(see. LittleCMS2.9 Plugin API.pdf|section 12) which allows you to define a new pixel format. Each pixel has a getter and setter for the format which allows reading and writing out.
We stack all the cairo drawing surfaces in the same memory location (if possible) so they can be read by lcms2 as one memory block. This is conjecture that this is possible, but I believe we can define the memory block in advance and tell cairo to write to it at a certain location.
Each plate would be written in memory sequentially. Cyan, Magenta, Yellow, then Black. Note: It doesn't have to be CMYK, an icc profile might contain 6 channels and if we wanted to be flexible we could do as many rendering passes as needed to render out each of the channels available (see cmykogv for example). We use a similar concept of line stride which tells lcms2 how wide an image is and thus if it should skip memory when reading and writing, but instead it tells use how the channel data is split out.
When we pass the memory location of these plates, we tell lcms2 the offset between each of the plates and our custom cmsPlugin uses this information to grab the values for each of the channels. Once formatted into lcms2 data, the normal process of converting to sRGB continues and the output surface contains the red green and blue values. We can choose to give lcms2 a new section of memory to write to, or let it over-write the original memory since cmy is the same size as rgb and this might not matter.
The original CMY surface will now contain our converted sRGB image ready for display, or for further processing in the pipeline.
Just copying
Instead of developing a cmsPlugin we could just loop through all the pixels and re-order them in a fresh memory location. But this would take more processor time and more memory. Though this might be a fair proof of concept implimentation while developing the cmsPlugin or it might be the best way to do it, especially if we're only using the channel display for brief previews and not continuous editing.
Separations
Because of the way we are rendering out, we can give the use the ability to change their view to just show certain channels. This gives them the ability to view what inks would be used and how vibrant it is. We can also use this information to render over-inking warnings directly to screen, which is something not currently possible.
Raster Images
Not tested in this proof of concept is raster images. Raster images should be strait forward as we can use lcms2 to convert their data into the target icc profile (assuming it's not in that format already) and use the pixel values directly. We may have to be clever about how we blit them onto the cairo surface but being able to move forward and backwards with our plate based rendering would be useful.
Filters
Filters might be more tricky as a lot of them assume RGB (and the spec allows for linear-RGB, though we don't support it), which color space a filter performs it's task in is important. To maintain functionality, we might have to render a filter in sRGB and then convert back to CMYK/icc for further processing. This means whatever method we decide to use must be able to convert to and from color separated plates.
Not the problem
This issue does not detail the issues without outputting to printers, pdf files or other various forms that cairo is also used in. This is just about rendering the screen.