Page tree
Skip to end of metadata
Go to start of metadata



Sometimes it's useful to simulate overprint when rendering a document.

This can be achieved by either:

  • Using the IOverprintSimulationTransform before rendering or outputting.
  • Rendering the Content as Separations

The former option, using the IOverprintSimulationTransform, is useful where the intent it to output to an intermediate format or another PDL. For example, SVG. In this case, using the transform will render the content set for overprint, while retaining the other content, which 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'll be quicker to use a method such as IJawsRenderer::renderSeparations(...), than using the transform, 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, 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, in order to simulate overprint.

It can be used with a few simple lines of code:

const auto transform = IOverprintSimulationTransform::create(jawsMako);

Rendering the Content as Separations

This option takes a little more code, but it gives more control, and it can be a better solution, performance wise, when rendering to a raster.

This requires two steps:

  1. Render the content
  2. 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 just the spot colors. This is then passed to the render call in order to tell Mako which 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());

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")

    candidateInks[i].inkColor.setColorSpace(sRGB, eRenderingIntent.eRelativeColorimetric, eBlackPointCompensation.eBPCDefault, jawsMako.getFactory());

// Build the list of spot colours that Jaws needs
var spotColours = new CEDLVectString();
foreach (var spotInk in spotInks.toVector()) 

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 we get back, 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);
  • No labels