Overview
Introduction
Sometimes it's useful to simulate overprint when rendering a document. This can be achieved by either:
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.
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);
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:
C++ | C# |
|---|
CPP
// Page bounds & raster geometry
const auto fixedPage = document->getPage(i)->getContent();
const auto bounds = FRect(0, 0, fixedPage->getWidth(), fixedPage->getHeight());
const double resolution = 150;
const auto pixelWidth = static_cast<uint32_t>(lround(bounds.dX / 96.0 * resolution));
const auto pixelHeight = static_cast<uint32_t>(lround(bounds.dY / 96.0 * resolution));
// Colorspace must be CMYK for spot merging (we can convert back to rgb later)
const auto cmyk = IDOMColorSpaceDeviceCMYK::create(mako);
// ----- Get spot lists -----
const auto spots = IRendererTransform::inkInfoToColorantInfo(mako, IRendererTransform::findInks(mako, fixedPage), cmyk);
CSpotColorNames spotNames;
for (auto& spot : spots)
spotNames.append(spot.name);
const auto numProcess = cmyk->getNumComponents();
const auto numSpots = spots.size();
// ----- Prepare frame buffers -----
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
);
|
C#
// Page bounds & raster geometry
var fixedPage = doc.getPage(pageIndex).getContent();
var bounds = new FRect(0, 0, fixedPage.getWidth(), fixedPage.getHeight());
double resolution = 150.0;
uint pixelWidth = (uint)Math.Round(bounds.dX / 96.0 * resolution);
uint pixelHeight = (uint)Math.Round(bounds.dY / 96.0 * resolution);
// Colorspace must be CMYK for spot merging (we can convert to RGB later)
var cmyk = IDOMColorSpaceDeviceCMYK.create(mako);
// ----- Find inks and build spot list -----
var spots = IRendererTransform.inkInfoToColorantInfo(mako, IRendererTransform.findInks(mako, fixedPage), cmyk);
var spotNames = new CEDLVectWString();
foreach (var spot in spots.toVector())
spotNames.append(spot.name);
int numProcess = cmyk.getNumComponents(); // 4 (CMYK)
int numSpots = (int)spots.size();
int numBuffers = numProcess + numSpots;
// ----- Prepare frame buffers -----
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 using renderSeparationsToFrameBuffers() -----
var renderer = IJawsRenderer.create(mako);
renderer.renderSeparationsToFrameBuffers(
fixedPage,
8, // bits per component per plate
true, // antialiased
pixelWidth,
pixelHeight,
cmyk, // process space
buffers,
fb,
0, // bandMemorySize (0 = no banding)
bounds,
spotNames // which spots to keep (all found)
);
|
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.
C++ | C# |
|---|
CPP
// ----- Create writer and image -----
IImageFrameWriterPtr frameWriter;
auto image = IDOMRawImage::createWriterAndImage(mako, frameWriter, cmyk,
pixelWidth, pixelHeight,
8, resolution, resolution);
// ----- Get CMYK components of each spot -----
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 spot and process separations, line by line, and write to image -----
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)
{
// for each process color, start by getting the value (out of 255) at the current pixel
scanline[x * numProcess + c] = outPtrs[c][x];
// then merge in spot colors by multiplying by each spot value in turn
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();
// ----- Convert to RGB (optional) and save as a JPEG (or other image type) -----
const auto rgb = IDOMColorSpaceDeviceRGB::create(mako);
const auto cc = IDOMImageColorConverterFilter::create(mako, rgb, eRelativeColorimetric, eBPCDefault)
const auto filteredImage = IDOMFilteredImage::create(mako, image, cc);
IDOMJPEGImage::encode(mako, (IDOMImagePtr)filteredImage, IOutputStream::createToFile(mako, "output.jpg"));
|
C#
// ----- Create writer and image -----
var pair = IDOMRawImage.createWriterAndImage(mako, cmyk, pixelWidth, pixelHeight, 8, resolution, resolution);
IImageFrameWriter frameWriter = pair.frameWriter;
// ----- Get CMYK components of each spot -----
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 spot and process separations, line by line, and write to image -----
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)
{
// for each process color, start by getting the value (out of 255) at the current pixel
scanline[x * numProcess + c] = buffers[c][rowStart + x];
// then merge in spot colors by multiplying by each spot value in turn
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);
IDOMJPEGImage.encode(mako, filtered, IOutputStream.createToFile(mako, "output.jpg"));
|