Reputation: 10131
I'm using iText in Java to select a few pages out of a big PDF document and save as a new, smaller PDF. At the same time I'd like to change their colours.
For example, suppose my pages all use shades of grey, and I'd like to make it green. All the colours used are shades of gray. I'd like to replace each of those colours with a corresponding colour in green.
Mark Storer asks:
What exactly are you trying to accomplish?
Turn this... into this:
I have some documents, on which I'm already using iText to select a smaller set of pages from the document based on user input - cutting more than 100 pages down to about 5. At the same time I wish to produce green, blue, yellow, pink etc versions of them. Not every page is in grayscale, but all the ones that matter are, so I can force their colour space if need be.
Update:
Following Mark Storer's suggestion of blending modes, here's what I have:
val reader = new PdfReader(file.toURL)
val document = new Document
val writer = PdfWriter.getInstance(document, outputStream)
document.open()
/* draw a white background behind the page, so the
blend always has something to transform, otherwise
it just fills. */
val canvas = writer.getDirectContent
canvas.setColorFill(new CMYKColor(0.0f, 0.0f, 0.0f, 0.0f))
canvas.rectangle(10f, 0f, 100f, 100f)
canvas.fill
/* Put the imported page on top of that */
val page = writer.getImportedPage(reader, 1)
canvas.addTemplate(page, 0, 0)
/* Fill a box with colour and a blending mode */
canvas.setColorFill(new CMYKColor(0.6f,0.1f,0.0f,0.5f))
val gstate = new PdfGState
gstate.setBlendMode(PdfGState.BM_SCREEN)
canvas.setGState(gstate)
canvas.rectangle(0f, 0f, 100f, 100f)
canvas.fill
document.close()
(It's in Scala, but the iText library is just the same as in Java)
The problem is, all the blending modes iText has available are "separable" modes: they operate on each colour channel independently. That means I can separately adjust the cyan, magenta, yellow or black values, but I can't turn gray into green.
To do that, I'd need to use the Color blending mode, which is "non-separable", ie the colour channels affect each other. As far as I can tell, iText doesn't offer that - none of the non-separale blending modes are listed among the constants in PdfGState
. I'm using iText 5.0.5, which is the latest version as of writing this.
Is there a way of accessing these blending modes in iText, or even of hacking them in? Is there another way of achieving the result?
Update:
Even setting the blend mode to Color didn't work. I did this in code to force it:
val gstate = new PdfGState
gstate.put(PdfName.BM, new PdfName("Color"))
canvas.setGState(gstate)
and I checked the resulting PDF in a text editor to make sure it said the right thing. Sadly the result on screen just didn't work. I've no idea why, according to the PDF specification that should be the right blend mode.
Mark Storer asks:
"Color" didn't work? Funky. Can we see the PDF?
Putting it on the web, I can now see that the "Color" mode works correctly in Chrome, but doesn't work in Acrobat 9 Pro (CS4). So the technique is correct, but Adobe fails at rendering!
I wonder if there isn't some way of "flattening" the effect of the blending mode, so the PDF contains the desired colour object directly rather than a blending intended to result in the right colour.
Idea: Turn this upside down. Use the existing page as an alpha channel on a page filled entirely with the desired color rather than the other way around.
How? I'm not sure the GState applies to adding a template?
Also, the imported page would need the white background adding first, or it will simply flood with colour wherever there isn't an object rather then blending.
I tried doing this:
val canvas = writer.getDirectContent
canvas.setColorFill(new CMYKColor(0.6f,0.1f,0.0f,0.0f))
canvas.rectangle(10f, 0f, 500f, 500f)
canvas.fill
val template = canvas.createTemplate(500f, 500f)
template.setColorFill(new CMYKColor(0f, 0f, 0f, 0f))
template.rectangle(0f, 0f, 500f, 500f)
template.fill
val page = writer.getImportedPage(reader, 1)
template.addTemplate(page, 0, 0)
val gstate = new PdfGState
gstate.put(PdfName.BM, new PdfName("Color"))
canvas.setGState(gstate)
canvas.addTemplate(template, 0, 0)
And here's the PDF it produced. Not quite right, either in Chrome or Acrobat :)
Edit: Silly me. I changed the mode to "Luminosity", producing this file. As before, this looks correct in Chrome but not in Acrobat.
I just checked, and even Adobe Reader X doesn't render it properly. Which probably means what I'm asking for is impossible. :(
Solution
Leonard Rosenthal from Adobe got back to me, and clarified the problem: the "Color" blend mode only works when the transformation space is RGB, not CMYK. My PDF wasn't specifying the space, so Adobe products default to CMYK, while others default to RGB.
The solution in iText was to add this line near the top:
writer.setRgbTransparencyBlending(true)
Of course, for the sake of colour accuracy you don't want any more colour space conversions than absolutely necessary, so only use this line if you really do need to use RGB blend modes.
The colour pages produced look a little strange from a Photoshop user's perspective: it looks like light greys have been made more saturate than dark greys. I'm investigating ways of combining filters to adjust that output.
Many thanks to Mark Storer for helping me reach this solution.
Upvotes: 6
Views: 7790
Reputation: 15868
If you consistently want to go from "shades of gray" to "shades of Color X", you might be able to use transparency with some funky blending mode.
If you want to go through all the content streams and edit the existing color commands, that's a pretty tall order. You have to take into account a Wide Variety of colo(u)r spaces. DeviceGray, DeviceRGB, DeviceCMYK, ICC profiles, calibrated RGB and CMYK, spot colors, and So Forth.
What exactly are you trying to accomplish?
"Color" didn't work? Funky. Can we see the PDF?
Idea: Turn this upside down. Use the existing page as an alpha channel on a page filled entirely with the desired color rather than the other way around.
One more try. Rather than blending, use a transfer function. You'd need to build a Function dictionary. You're sticking with CMYK, so cramming any and all inputs into a specific output should be fairly simple.
something like this:
C: [0 1] -> [0 0.6]
M: [0 1] -> [0 0.1]
Y: [0 1] -> [0 0]
K: [0 1] -> [0 0]
(I swiped the 0.6 0.1 0 0 from your PDF)
Urgh... only your existing page is all deviceGray, right? Nope... CMYK as well, only just K's. You'd need a transfer function that took the K values, and mapped them to CMYK based on the color output you wanted.
And then I looked at how you define a function in PDF. So much for simple. Domains and ranges and samples oh my! Not exactly trivial.
Still, this just might work.
(though I still think you should find a blended PDF that works in Acrobat and see what the differences are)
Last ditch effort: PM Leonard Rosenthol. He has an account here on SO. He's the Acrobat developer relations guy for Adobe. Tell him that Mark Storer is stumped. That should get his attention. ;)
Upvotes: 5