Overprint Simulation
π Overview
Sometimes it's useful to simulate overprint when rendering a document. This can be achieved by either:
Using the
IOverprintSimulationTransformbefore 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::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.
const auto transform = IOverprintSimulationTransform::create(jawsMako);
transform->transformPage(page);
var transform = IOverprintSimulationTransform.create(jawsMako);
transform.transformPage(page);
var transform = IOverprintSimulationTransform.create(mako);
transform.transformPage(page);
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:
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 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:
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
// 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
// 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);
# 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.
// 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()));
// ----- 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}");
// 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));
# 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.