Created Date: 20 Jan, 2021 09:04
Last Modified Date: 04 Mar, 2024 12:05


Color management in Mako

Even for the simple example described in Rendering 101, Mako uses the CMS (color management system) to control how colors are interpreted and subsequently represented in the output.

That example defaults to creating RGB output, as no output color space is declared. For example, to create 300dpi, 8bpc, CMYK output, pass in a DPI value, and color space:

IDOMImagePtr renderedImage = renderer->render(pageContent, 300, 8, IDOMColorSpaceDeviceCMYK::create(jawsMako));
CPP
var renderedImage = renderer.render(pageContent, 300, 8, IDOMColorSpaceDeviceCMYK.create(jawsMako.getFactory())
C#

DeviceCMYK represents the default CMYK color space, by default a SWOP profile. See Intercept color spaces below. You can load an alternative profile from disk and use that. For example:

String iccProfile = L"WebCoatedFOGRA28.icc";
IDOMColorSpaceICCBasedPtr iccBasedColorSpace = IDOMColorSpaceICCBased::create(
                    jawsMako, IDOMICCProfile::create(jawsMako, IInputStream::createFromFile(jawsMako, iccProfile)),
                    IDOMColorSpaceDeviceCMYK::create(jawsMako));
CPP
string iccProfile = "WebCoatedFOGRA28.icc";
var iccBasedColorSpace = IDOMColorSpaceICCBased.create(
                    jawsMako, IDOMICCProfile.create(jawsMako, IInputStream.createFromFile(jawsMako, iccProfile)), 
                    IDOMColorSpaceDeviceCMYK.create(jawsMako));
C#

Then use this color space for rendering:

Call the renderer

IDOMImagePtr renderedImage = renderer->render(pageContent, 300, 8, iccBasedColorSpace);
CPP

Call the renderer

var renderedImage = renderer.render(pageContent, 300, 8, iccBasedColorSpace);
C#

Device color spaces

Device color spaces directly specify colors or shades of gray that the output device is to produce. They provide a variety of color specification methods, including grayscale, RGB (red-green-blue), and CMYK (cyan-magenta-yellow-black), corresponding to the color space families DeviceGray, DeviceRGB, and DeviceCMYK. Since each of these families consists of just a single color space with no parameters, they are often loosely referred to as the DeviceGray, DeviceRGB, and DeviceCMYK color spaces.

CIE-based color spaces

CIE-based color spaces are based on an international standard for color specification created by the Commission Internationale de l’Éclairage (International Commission on Illumination). These spaces specify colors in a way that is independent of the characteristics of any particular output device. Color space families in this category include CalGray, CalRGB, Lab, and ICCBased. Individual color spaces within these families are specified by means of dictionaries containing the parameter values needed to define the space.

Intercept color spaces

These are the color spaces that will be used for conversion whenever the color manager is asked to convert from or to DeviceGray, DeviceRGB, or DeviceCMYK, respectively.

Color model# colorantsColorspaceDefault profile in Mako
Grayscale1DeviceGraysGray*
RGB2DeviceRGBsRGB
CMYK4DeviceCMYKSWOP ICC

* sGray is a grayscale equivalent of sRGB, adopting the same gamma curve as sRGB

Overriding the intercept defaults

You may want to set a different profile to be used for the intercept. For example, a way to speed up processing is to set the DeviceCMYK intercept color space to the same space used for rendering, as there will be no need to carry out an intermediate conversion. Or you may want to specify a different profile for RGB or grayscale processing. Load the profile using the technique in the code snippet above (Creating a CMYK color space from an external profile) and use the color manager's setIntercept() method. How to do this is described in "Configuring the CMS", below.

Conversion of color spaces during rendering

A simplified diagram of the color conversion that occurs inside the renderer follows:


Configuring the CMS

Mako provides a class, IColorManager, to control the CMS (or CMM, Color Management Module). It offers various methods to control how colors are transformed between color spaces, create profiles, retrieve built-in profiles, or convert colors.

There is only one instance of the color manager. The singleton can be retrieved with the get() method.


Get the Color Manager

#include <edl/icolormanager.h>
...
IColorManagerPtr cmm = IColorManager::get(jawsMako);
CPP
var cmm = IColorManager.get(jawsMako.getFactory());
C#

Intercept spaces - The profile associated with one of these intercept spaces can be retrieved or set with these methods:

DeviceGray
IColorManager::getDeviceGrayIntercept()
CPP
IColorManager::setDeviceGrayIntercept()
CPP
DeviceRGB
IColorManager::getDeviceRGBIntercept()
CPP
IColorManager::setDeviceRGBIntercept()
CPP
DeviceCMYK
IColorManager::getDeviceCMYKIntercept()
CPP
IColorManager::setDeviceCMYKIntercept()
CPP
DeviceGray
IColorManager::getDeviceGrayIntercept()
CPP
IColorManager::setDeviceGrayIntercept()
CPP
DeviceRGB
IColorManager::getDeviceRGBIntercept()
CPP
IColorManager::setDeviceRGBIntercept()
CPP
DeviceCMYK
IColorManager::getDeviceCMYKIntercept()
CPP
IColorManager::setDeviceCMYKIntercept()
CPP

Configuring DeviceGray to DeviceCMYK

Another commonly used setting is to configure the CMM to map DeviceGray to DeviceCMYK without color conversion. This is required to obtain correct results for many cases.

Configure gray to black mapping

cmm->setMapDeviceGrayToCMYKBlack(true);
CPP
cmm.setMapDeviceGrayToCMYKBlack(true);
C#


Controlling rendering intent and black point compensation

In general there is no need to explicitly set these for rendering. PDF defines a default rendering intent of Relative Colorimetric, and a black point compensation setting of "default" (meaning it's left up to the CMS).

These settings are properties of every node in the page tree. You can get or set these values to influence how the object is rendered:

Get or set rendering intent

// Get example
PValue riValue;
pageContent->getFirstChild()->getProperty("RenderingIntent", riValue);

// Set example
pageContent->getFirstChild()->setProperty("RenderingIntent", eAbsoluteColorimetric);
CPP
// Get example
PValue riValue;
pageContent.getFirstChild().getProperty("RenderingIntent", ref riValue);

// Set example
pageContent.getFirstChild().setProperty("RenderingIntent", eAbsoluteColorimetric);
C#


The PValue returned is an integer. An enumeration is available for setting the value:

0ePerceptualPerceptual rendering intent
1eRelativeColorimetricRelative colorimetric rendering intent
2eSaturationSaturation rendering intent
3eAbsoluteColorimetricAbsolute colorimetric rendering intent


Get or set black point compensation

// Get
PValue bpcValue;
pageContent->getFirstChild()->getProperty("BlackPointCompensation", bpcValue);
pageContent->getFirstChild()->setProperty("BlackPointCompensation", eBPCOn);
CPP
PValue bpcValue;
pageContent->getFirstChild()->getProperty("BlackPointCompensation", bpcValue);
pageContent->getFirstChild()->setProperty("BlackPointCompensation", eBPCOn);
C#


The PValue returned is an integer. An enumeration is available for setting the value.

ValueSettingDescription
0eBPCDefault

Default behavior: Black point compensation will be applied when ICC Version 4 or greater profiles are used and the intent is perceptual or saturation.

1eBPCOnUse black point compensation if applicable during color conversion: Black point compensation will be applied for all but absolute rendering intents.
2eBPCOffDo not use black point compensation during color conversion: Black point compensation wll not be used.

Setting the BPC property influences rendering or other operations that require color management, but this property is not persisted (saved) to PDF 1.x. It is saved to PDF 2.0.

Visiting every node on the page

As already mentioned, to control rendering intent or black point compensation it is necessary to update every marking node on the page. This sounds daunting but actually Mako provides a way to do this, known as a custom transform. This C++ example shows how this is done, while custom transforms are described more generally elsewhere in this documentation. The advantage of this approach is that the code can make a decision based on the node type and its properties.

Accessing color spaces through the DOM

Sometimes, it's useful to inspect the color space that a DOM node is using.

For example, a path node will have a fill or stroke color. When we access the path's fill or stroke, there's a method that allows us to get its corresponding color space.

const auto brush = pathNode->getFill();
 
if (brush && brush->getBrushType() == IDOMBrush::eSolidColor)
{
    const auto solidColorBrush = edlobj2IDOMSolidColorBrush(brush);
 
    // If it has, we can get its color space. Similar accessors exist for other brush types.
    const auto colorSpace = solidColorBrush->getColor()->getColorSpace();
 
    // Do something with the color space...
}
CPP
var brush = pathNode.getFill();
 
if (brush && brush.getBrushType() == IDOMBrush.eSolidColor)
{
    var solidColorBrush = edlobj2IDOMSolidColorBrush(brush);
 
    // If it has, we can get its color space. Similar accessors exist for other brush types.
    var colorSpace = solidColorBrush.getColor().getColorSpace();
 
    // Do something with the color space...
}
C#