Skip to main content
Skip table of contents

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 outputting

  • Rendering 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::renderSeparationsToFrameBuffers(...), 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.

CPP
const auto transform = IOverprintSimulationTransform::create(jawsMako);
transform->transformPage(page);
C#
var transform = IOverprintSimulationTransform.create(jawsMako);
transform.transformPage(page);
JAVA
var transform = IOverprintSimulationTransform.create(mako);
transform.transformPage(page);
PY
transform = IOverprintSimulationTransform.create(mako)
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:

  1. Render the content to separations

  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 need to specify the spots we want to render. Here we can also specify spots to ignore, see Render separations to frame buffers for more information on this. We must generate additional separations for each spot color we want to render so that we can then recombine them in step 2.

The following code can be used:

CPP
const auto cmyk = IDOMColorSpaceDeviceCMYK::create(mako); // colorspace must be cmyk for spot merging (we can convert back to rgb later if necessary)

// Get spot lists
const auto spots = IRendererTransform::inkInfoToColorantInfo(mako, IRendererTransform::findInks(mako, fixedPage), cmyk);
CSpotColorNames spotNames;
for (auto& spot : spots) 
    spotNames.append(spot.name);

// Prepare frame buffers
const auto numProcess = cmyk->getNumComponents();
const auto numSpots = spots.size();
const auto numBuffers = numProcess + numSpots;
std::vector<std::vector<uint8_t>> buffers(numBuffers);
auto frameBuffers = IJawsRenderer::CFrameBufferInfoVect(numBuffers);
for (uint8_t bufferIndex = 0; bufferIndex < numBuffers; ++bufferIndex)
{
    buffers[bufferIndex].resize(pixelHeight * pixelWidth);
    frameBuffers[bufferIndex].buffer = buffers[bufferIndex].data();
    frameBuffers[bufferIndex].rowStride = static_cast<int>(pixelWidth);
    frameBuffers[bufferIndex].pixelStride = 0;
}

// Render using renderSeparationsToFrameBuffers()
IJawsRendererPtr renderer = IJawsRenderer::create(mako);
renderer->renderSeparationsToFrameBuffers(fixedPage, 8, true, pixelWidth, pixelHeight, cmyk, frameBuffers, 0, bounds, spotNames, IOptionalContentPtr(), eOCEPrint); // Non printable layers will be off
C#
// Colorspace must be CMYK for spot merging (we can convert to RGB later)
var cmyk = IDOMColorSpaceDeviceCMYK.create(mako);

// ----- Find inks and build spot list (names + CMYK components) -----
var spots = IRendererTransform.inkInfoToColorantInfo(mako, IRendererTransform.findInks(mako, fixedPage), cmyk);
var spotNames = new CEDLVectWString();
foreach (var spot in spots.toVector())
    spotNames.append(spot.name);

// ----- Prepare frame buffers for renderSeparationsToFrameBuffers() -----
int numProcess = cmyk.getNumComponents(); // 4 (CMYK)
int numSpots = (int)spots.size();
int numBuffers = numProcess + numSpots;
var buffers = new byte[numBuffers][];
var fb = new CEDLVectCFrameBufferInfo();
for (int i = 0; i < numBuffers; i++)
{
    buffers[i] = new byte[pixelHeight *  pixelWidth];
    var info = new IJawsRenderer.CFrameBufferInfo
    {
        bufferOfs = 0,
        rowStride = (int)pixelWidth,   // pixels per row (1 byte per pixel)
        pixelStride = 0                // tightly packed
    };
    fb.append(info);
}

// ----- Render true separations (process first, then spots) -----
var renderer = IJawsRenderer.create(mako);
renderer.renderSeparationsToFrameBuffers(fixedPage, 8, true, pixelWidth, pixelHeight, cmyk, buffers, fb, 0, bounds, spotNames, IOptionalContent.Null(), eOptionalContentEvent.eOCEPrint); // Non printable layers will be off
JAVA
// Colorspace must be CMYK for spot merging
IDOMColorSpaceDeviceCMYK cmyk = IDOMColorSpaceDeviceCMYK.create(factory);

// Find inks and build spot list
CEDLVectColorantInfo spots = IRendererTransform.inkInfoToColorantInfo(mako, IRendererTransform.findInks(mako, fixedPage), cmyk);
CEDLVectWString spotNames = new CEDLVectWString();
for (int i = 0; i < spots.size(); i++)
    spotNames.append(spots.getitem(i).getName());

// Prepare frame buffers
int numProcess = cmyk.getNumComponents(); // 4
int numSpots = (int) spots.size();
int numBuffers = numProcess + numSpots;
ByteBuffer[] buffers = new ByteBuffer[numBuffers];
CEDLVectCFrameBufferInfo fb = new CEDLVectCFrameBufferInfo();

for (int i = 0; i < numBuffers; i++) {
    ByteBuffer buf = ByteBuffer.allocateDirect(pixelWidth * pixelHeight);
    buf.order(ByteOrder.nativeOrder());
    buffers[i] = buf;
    IJawsRenderer.CFrameBufferInfo info = new IJawsRenderer.CFrameBufferInfo();
    info.setBufferOfs(0);
    info.setRowStride(pixelWidth);  // bytes per row
    info.setPixelStride(0);         // tightly packed
    fb.append(info);
}

// Render true separations
IJawsRenderer renderer = IJawsRenderer.create(mako);
renderer.renderSeparationsToFrameBuffers(fixedPage, (short) 8, true, pixelWidth, pixelHeight, cmyk, buffers, fb, (short) 0, bounds, spotNames, IOptionalContent.Null(), eOptionalContentEvent.eOCEPrint);
PY
# Colorspace must be CMYK for spot merging
cmyk = IDOMColorSpaceDeviceCMYK.create(factory)

# Find inks and build spot list
inks = IRendererTransform.findInks(mako, fixed_page)
spots = IRendererTransform.inkInfoToColorantInfo(mako, inks, cmyk)
spot_names = CEDLVectWString()
for i in range(spots.size()):
    spot_names.append(spots.getitem(i).name)

# Prepare frame buffers
num_process = cmyk.getNumComponents()  # 4
num_spots = int(spots.size())
num_buffers = num_process + num_spots
buffers = []
fb = CEDLVectCFrameBufferInfo()

for i in range(num_buffers):
    # allocate pixel buffer for this plane
    buf = bytearray(pixel_width * pixel_height)
    buffers.append(buf)

    info = IJawsRenderer.CFrameBufferInfo()
    info.bufferOfs = 0
    info.rowStride = pixel_width
    info.pixelStride = 0
    fb.append(info)

# Render true separations
renderer = IJawsRenderer.create(mako)
renderer.renderSeparationsToFrameBuffers(fixed_page, 8, True, pixel_width, pixel_height, cmyk, buffers, fb, 0, bounds, spot_names, IOptionalContent.Null(), eOCEPrint)

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 get the CMYK process color values at each pixel.

  • Then we multiply each process color value by the corresponding CMYK component of each spot color in turn.

  • Once we have written each scanline, we can convert the image to RGB if necessary.

CPP
// write the buffers to an image
IImageFrameWriterPtr frameWriter;
auto image = IDOMRawImage::createWriterAndImage(mako, frameWriter, cmyk, pixelWidth, pixelHeight, 8, resolution, resolution);

// Get spot components 
CEDLVector<CFloatVect> components(numSpots);
for (uint32_t i = 0; i < numSpots; i++)
    components[i] = CFloatVect({spots[i].components[0], spots[i].components[1], spots[i].components[2], spots[i].components[3]});

// Merge spots into process
CEDLVector<uint8_t*> outPtrs(numProcess);
CEDLVector<uint8_t*> inPtrs(numSpots);
constexpr float inv255 = 1.0f / 255.0f;

for (uint32_t y = 0; y < pixelHeight; ++y)
{
    for (uint32_t i = 0; i < numProcess; ++i)
        outPtrs[i] = static_cast<uint8_t*>(frameBuffers[i].buffer) + y * frameBuffers[i].rowStride;

    for (uint32_t i = 0; i < numSpots; ++i)
        inPtrs[i] = static_cast<uint8_t*>(frameBuffers[numProcess + i].buffer) + y * frameBuffers[numProcess + i].rowStride;

    auto scanline = std::vector<uint8_t>(pixelWidth * numProcess);

    for (uint32_t x = 0; x < pixelWidth; ++x)
        for (uint32_t c = 0; c < 4; ++c)
        {
            scanline[x * numProcess + c] = outPtrs[c][x];
            for (uint32_t i = 0; i < numSpots; ++i)
            {
                float spotVal = inPtrs[i][x] * inv255;
                float currentVal = scanline[x * numProcess + c] * inv255;
                float newVal = 1.0f - ((1.0f - components[i][c] * spotVal) * (1.0f - currentVal));
                scanline[x * numProcess + c] = static_cast<uint8_t>(newVal * 255.0f + 0.5f);
            }
        }
    frameWriter->writeScanLine(scanline.data());
}
frameWriter->flushData();

//now we can convert back to RGB if needed
const auto filteredImage = IDOMFilteredImage::create(mako, image, IDOMImageColorConverterFilter::create(mako, IDOMColorSpaceDeviceRGB::create(mako), eRelativeColorimetric, eBPCDefault));

// save the output to jpeg file
std::string outputJPEG = "output_" + std::to_string(i) + ".jpg";
IDOMJPEGImage::encode(mako, (IDOMImagePtr)filteredImage, IOutputStream::createToFile(mako, outputJPEG.c_str()));
C#
// ----- Create writer and image -----
var pair = IDOMRawImage.createWriterAndImage(mako, cmyk, pixelWidth, pixelHeight, 8, resolution, resolution);
IImageFrameWriter frameWriter = pair.frameWriter;

// ----- Get spot components of spots to merge -----
var components = new CEDLVectVectFloat();
for (uint i = 0; i < numSpots; i++)
    components.append(new CEDLVectFloat(new StdVectFloat(){ spots[i].components[0], spots[i].components[1], spots[i].components[2], spots[i].components[3] }));

// ----- Merge each spot component with the process buffer values and write the result to the scanline -----
const float inv255 = 1.0f / 255.0f;
for (uint y = 0; y < pixelHeight; y++)
{
    var rowStart = (int)(y * pixelWidth);
    var scanline = new byte[pixelWidth * numProcess];

    for (uint x = 0; x < pixelWidth; ++x)
    {
        for (uint c = 0; c < numProcess; ++c)
        {
            scanline[x * numProcess + c] = buffers[c][rowStart + x];
            for (uint i = 0; i < numSpots; ++i)
            {
                float spotVal = buffers[numProcess + i][rowStart + x] * inv255;
                float currentVal = scanline[x * numProcess + c] * inv255;
                float newVal = 1.0f - (1.0f - components[i][c] * spotVal) * (1.0f - currentVal);
                scanline[x * numProcess + c] = (byte)(newVal * 255.0f + 0.5f);
            }
        }
    }
    frameWriter.writeScanLine(scanline);
}
frameWriter.flushData();

// ----- Convert to RGB (optional) and save as a JPEG (or other image type) -----
var rgb = IDOMColorSpaceDeviceRGB.create(mako);
var cc = IDOMImageColorConverterFilter.create(mako, rgb, eRenderingIntent.eRelativeColorimetric, eBlackPointCompensation.eBPCDefault);
var filtered = IDOMFilteredImage.create(mako, pair.domImage, cc);

string outJpeg = $"output_{pageIndex}.jpg";
IDOMJPEGImage.encode(mako, filtered, IOutputStream.createToFile(mako, outJpeg));
Console.WriteLine($"Wrote: {outJpeg}");
JAVA
// Create writer and image
var pair = IDOMRawImage.createWriterAndImage(mako, cmyk, pixelWidth, pixelHeight, (short) 8, resolution, resolution);
IImageFrameWriter frameWriter = pair.getFrameWriter();

// Spot components to merge
CEDLVectVectFloat components = new CEDLVectVectFloat();
for (int i = 0; i < numSpots; i++) {
    CEDLVectFloat comps = new CEDLVectFloat();
    CEDLVectFloat vals = spots.getitem(i).getComponents();
    for (int c = 0; c < 4; c++)
        comps.append(vals.getitem(c));
    components.append(comps);
}

// Merge each spot buffer with process values
final float inv255 = 1.0f / 255.0f;
byte[] scanline = new byte[pixelWidth * numProcess];
for (int y = 0; y < pixelHeight; y++) {
    int rowStart = y * pixelWidth;
    for (int x = 0; x < pixelWidth; x++) {
        for (int c = 0; c < numProcess; c++) {
            int idx = x * numProcess + c;
            scanline[idx] = buffers[c].get(rowStart + x);
            for (int i = 0; i < numSpots; i++) {
                float spotVal = (buffers[numProcess + i].get(rowStart + x) & 0xFF) * inv255;
                float currentVal = (scanline[idx] & 0xFF) * inv255;
                float newVal = 1.0f - (1.0f - components.getitem(i).getitem(c) * spotVal) * (1.0f - currentVal);
                scanline[idx] = (byte) (newVal * 255.0f + 0.5f);
            }
        }
    }
    frameWriter.writeScanLine(scanline);
}
frameWriter.flushData();

// Convert to RGB and save as JPEG
IDOMColorSpaceDeviceRGB rgb = IDOMColorSpaceDeviceRGB.create(factory);
IDOMImageColorConverterFilter cc = IDOMImageColorConverterFilter.create(factory, rgb, eRenderingIntent.eRelativeColorimetric, eBlackPointCompensation.eBPCDefault);
IDOMFilteredImage filtered = IDOMFilteredImage.create(factory, pair.getDomImage(), cc);

String outJpeg = String.format("output_%d.jpg", pageIndex);
IDOMJPEGImage.encode(mako, filtered, IOutputStream.createToFile(factory, outJpeg));
PY
# Create writer and image
pair = IDOMRawImage.createWriterAndImage(mako, cmyk, pixel_width, pixel_height, 8, resolution, resolution)
frame_writer = pair.frameWriter

# Spot components to merge
components = CEDLVectVectFloat()
for i in range(num_spots):
    comps = CEDLVectFloat()
    vals = spots.getitem(i).components()
    for c in range(4):
        comps.append(vals.getitem(c))
    components.append(comps)

# Merge each spot buffer with process values
inv255 = 1.0 / 255.0
scanline = bytearray(pixel_width * num_process)

for y in range(pixel_height):
    row_start = y * pixel_width
    for x in range(pixel_width):
        for c in range(num_process):
            idx = x * num_process + c
            scanline[idx] = buffers[c][row_start + x]
            for i in range(num_spots):
                spot_val = buffers[num_process + i][row_start + x] * inv255
                current_val = scanline[idx] * inv255
                new_val = 1.0 - (1.0 - components.getitem(i).getitem(c) * spot_val) * (1.0 - current_val)
                scanline[idx] = int(new_val * 255.0 + 0.5)
    frame_writer.writeScanLine(scanline)

frame_writer.flushData()

# Convert to RGB and save as JPEG
rgb = IDOMColorSpaceDeviceRGB.create(factory)
cc = IDOMImageColorConverterFilter.create(factory, rgb, eRelativeColorimetric, eBPCDefault)
filtered = IDOMFilteredImage.create(factory, pair.getDomImage(), cc)

out_jpeg = f"output_{page_index}.jpg"
IDOMJPEGImage.encode(mako, filtered, IOutputStream.createToFile(factory, out_jpeg))

Full implementation

For the full implementation see Visual Studio Projects/OverprintSimulation, Java Projects/OverprintSimulation or Python Projects/OverprintSimulation.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.