Overprint Simulation
Overview
Sometimes it's useful to simulate overprint when rendering a document. This can be achieved by either:
Using the
IOverprintSimulationTransform
before rendering or outputtingRendering the content as separations.
The former option, using the IOverprintSimulationTransform
, is useful where the intent is to output to an intermediate format or another PDL. For example, in SVG. using the transform will render the content set for overprint, while retaining the other content that may be vector based. This means that the output retains as much vector content as possible, while supporting overprint simulation.
The second option is useful where the final destination is a raster. In this case, it will be quicker to use a method such as IJawsRenderer::renderSeparations(...)
, than using the transform and then rendering. Using the separation rendering call will naturally handle overprint.
The other benefit of this approach is that separations can be omitted when re-combining into a composite image, in order to have a preview with custom separations turned off.
Using the IOverprintSimulationTransform
This is the simplest option, which transforms a page or subsection of a page to simulate overprint. It can be applied with a few simple lines of code.
const auto transform = IOverprintSimulationTransform::create(jawsMako);
transform->transformPage(page);
var transform = IOverprintSimulationTransform.create(jawsMako);
transform.transformPage(page);
Rendering the content as separations
This option takes a little more code but provides more control. Moreover, it can be faster when rendering to a raster.
The approach requires two steps:
Render the content to separations
Recombine the separations
1) Rendering Content
The first step is to render the content as individual separations. Under the hood, we use the Jaws RIP to render the content. Naturally, this takes into account the overprint flags set on objects in the PDF.
When rendering separations, we want to first find the inks for the page we're rendering. Once we do so, we prune that list of inks down to the spot colors, which is then passed to the render call in order to tell Mako the spot colors we want to render.
The following code can be used:
// We will also need the Mako CMM to perform colour conversion
var cmm = IColorManager.get(jawsMako.getFactory());
cmm.setMapDeviceGrayToCMYKBlack(true);
using var page = assembly.getDocument().getPage(pageIndex);
// Ask the renderer transform for the list of inks on the page
var candidateInks = IRendererTransform.findInks(jawsMako, page.getContent());
// We are going to be using CMYK as our intermediate space, so prune any CMYK inks
// from this list. While we do this, convert all the spot ink colors to sRGB which
// we will be using as our final preview space.
var spotInks = new CEDLVectCInkInfo();
var sRGB = IDOMColorSpacesRGB.create(jawsMako.getFactory());
for (uint i = 0; i < candidateInks.size(); i++)
{
if (candidateInks[i].inkName == "Cyan" ||
candidateInks[i].inkName == "Magenta" ||
candidateInks[i].inkName == "Yellow" ||
candidateInks[i].inkName == "Black")
{
continue;
}
candidateInks[i].inkColor.setColorSpace(sRGB, eRenderingIntent.eRelativeColorimetric, eBlackPointCompensation.eBPCDefault, jawsMako.getFactory());
spotInks.append(candidateInks[i]);
}
// Build the list of spot colours that Jaws needs
var spotColours = new CEDLVectString();
foreach (var spotInk in spotInks.toVector())
spotColours.append(spotInk.inkName);
var deviceCMYK = IDOMColorSpaceDeviceCMYK.create(jawsMako.getFactory());
var bounds = new FRect(0, 0, page.getWidth(), page.getHeight());
var destWidth = (uint) bounds.dX;
var destHeight = (uint) bounds.dY;
// Then, render to CMYK + spot separations
var separations = renderer.renderSeparations(page.getContent(), 8, deviceCMYK, true,
bounds, destWidth, destHeight, spotColours);
The separations that are returned then need to be recombined.
2) Recombining Separations
Once the separations have been rendered, we work through them scanline by scanline.
For each scanline, we first recombine the CMYK channels into a composite. We then use the IColorManager to convert from CMYK into RGB.
Once our scanline is in RGB, we can then apply the spot color's RGB values on top, to give our final preview.
// So now we have a vector of images, CMYK + any spot colourants.
// Time to make a preview.
// The technique here is somewhat simplistic, but is likely useful
// and a starting point for more accurate preview generation.
// In particular, more accurate results may be obtained by using a
// linear colour space as an intermediate when combining spot colours.
// First allocate a buffer for dealing with the CMYK channels
var cmykScanline = new byte[destWidth * 4];
// And raw buffers for reading the separated images
var rawBuffers = new List<byte[]>();
for (var separationIndex = 0; separationIndex < separations.size(); separationIndex++)
rawBuffers.Add(new byte[destWidth]);
// Get the frames for all separations
var frames = new List<IImageFrame>();
for (var i = 0; i < separations.size(); i++)
frames.Add(separations[(uint) i].getImageFrame(jawsMako.getFactory()));
var frameBufferStride = destWidth * 3;
var frameBuffer = new byte[frameBufferStride * destHeight];
var frameBufferScanLine = new byte[frameBufferStride];
// With everything prepared, generate the preview, scanline by scanline.
for (var y = 0; y < destHeight; y++)
{
// Read a scanline from all the separations
for (var i = 0; i < frames.Count; i++)
{
frames[i].readScanLine(rawBuffers[i], (uint) rawBuffers[i].Length);
}
// Assemble the CMYK into a composite scanline
for (var x = 0; x < destWidth; x++)
{
cmykScanline[x * 4 + 0] = rawBuffers[0][x];
cmykScanline[x * 4 + 1] = rawBuffers[1][x];
cmykScanline[x * 4 + 2] = rawBuffers[2][x];
cmykScanline[x * 4 + 3] = rawBuffers[3][x];
}
// Convert that to RGB, relative colorimetric, default black point handling.
// We'll convert directly into the output frame buffer
cmm.convertColors((int) destWidth, false, deviceCMYK, sRGB,
eRenderingIntent.eRelativeColorimetric, eBlackPointCompensation.eBPCDefault, cmykScanline, frameBufferScanLine, 8, 8);
// Now, the spots
for (var i = 4; i < frames.Count; i++)
{
// Get the RGB for the colorant
var rgbColorant = new float[3];
rgbColorant[0] = spotInks[(uint) i - 4].inkColor.getComponentValue(0);
rgbColorant[1] = spotInks[(uint) i - 4].inkColor.getComponentValue(1);
rgbColorant[2] = spotInks[(uint) i - 4].inkColor.getComponentValue(2);
// Convert to pseudo-additive (CMY)
rgbColorant[0] = 1.0f - rgbColorant[0];
rgbColorant[1] = 1.0f - rgbColorant[1];
rgbColorant[2] = 1.0f - rgbColorant[2];
// Now apply this by essentially scaling the colorant according to the intensity
// of the spot channel, and then performing a multiple blend with the contents
// of the preview.
var rawSeparation = rawBuffers[i];
for (var x = 0; x < destWidth; x++)
{
var inkIntensity = rawSeparation[x] / 255.0f;
// An improvement here would be to use a linear color space or
// profile rather than sRGB for greater correctness, however this
// gives reasonable results.
var rgbMultiplier = 1.0f - inkIntensity * rgbColorant[0];
frameBufferScanLine[x * 3 + 0] = (byte) (frameBufferScanLine[x * 3 + 0] * rgbMultiplier);
rgbMultiplier = 1.0f - inkIntensity * rgbColorant[1];
frameBufferScanLine[x * 3 + 1] = (byte) (frameBufferScanLine[x * 3 + 1] * rgbMultiplier);
rgbMultiplier = 1.0f - inkIntensity * rgbColorant[2];
frameBufferScanLine[x * 3 + 2] = (byte) (frameBufferScanLine[x * 3 + 2] * rgbMultiplier);
}
}
// Note: Enabling or disabling a channel from the preview is a simple matter of
// ignoring that channel in the above calculations. In this way inks could be
// toggled without requiring re-rendering.
Array.Copy(frameBufferScanLine, 0, frameBuffer, y * frameBufferStride, frameBufferStride);
}
var outputStream = IOutputStream.createToFile(factory, fileName);
IDOMPNGImage.encode(jawsMako, sRGB, 96, 8, frameBuffer, destWidth, destHeight, (int) frameBufferStride, false, outputStream);