001: /**
002: * Copyright (c) 2006, www.pdfbox.org
003: * All rights reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions are met:
007: *
008: * 1. Redistributions of source code must retain the above copyright notice,
009: * this list of conditions and the following disclaimer.
010: * 2. Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: * 3. Neither the name of pdfbox; nor the names of its
014: * contributors may be used to endorse or promote products derived from this
015: * software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
018: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
019: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
020: * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
021: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
022: * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
023: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
024: * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
026: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027: *
028: * http://www.pdfbox.org
029: *
030: */package org.pdfbox;
031:
032: import java.io.IOException;
033: import java.util.ArrayList;
034: import java.util.Hashtable;
035: import java.util.List;
036: import java.util.regex.Matcher;
037: import java.util.regex.Pattern;
038:
039: import org.pdfbox.cos.COSFloat;
040: import org.pdfbox.cos.COSNumber;
041: import org.pdfbox.exceptions.InvalidPasswordException;
042: import org.pdfbox.pdfparser.PDFStreamParser;
043: import org.pdfbox.pdfwriter.ContentStreamWriter;
044: import org.pdfbox.pdmodel.PDDocument;
045: import org.pdfbox.pdmodel.PDPage;
046: import org.pdfbox.pdmodel.common.PDStream;
047: import org.pdfbox.util.PDFOperator;
048:
049: /**
050: * This is the main program that simply parses the pdf document and replace
051: * change a PDF to use a specific colorspace.
052: *
053: * @author <a href="ben@benlitchfield.com">Ben Litchfield</a>
054: * @author Pierre-Yves Landuré (pierre-yves@landure.org)
055: * @version $Revision: 1.5 $
056: */
057: public class ConvertColorspace {
058:
059: private static final String PASSWORD = "-password";
060: private static final String CONVERSION = "-equiv";
061: private static final String DEST_COLORSPACE = "-toColorspace";
062:
063: /**
064: * private constructor.
065: */
066: private ConvertColorspace() {
067: //static class
068: }
069:
070: /**
071: * The method that replace RGB colors by CMYK ones.
072: *
073: * @param inputFile input file name.
074: * @param colorEquivalents a dictionnary for the color equivalents.
075: * @param destColorspace The destination colorspace, currently CMYK is supported.
076: *
077: * @throws IOException If there is an error parsing the document.
078: */
079: private void replaceColors(PDDocument inputFile,
080: Hashtable colorEquivalents, String destColorspace)
081: throws IOException {
082: if (!destColorspace.equals("CMYK")) {
083: throw new IOException("Error: Unknown colorspace "
084: + destColorspace);
085: }
086: List pagesList = inputFile.getDocumentCatalog().getAllPages();
087:
088: PDPage currentPage = null;
089: PDFStreamParser parser = null;
090: List pageTokens = null;
091: List editedPageTokens = null;
092:
093: for (int pageCounter = 0; pageCounter < pagesList.size(); pageCounter++) // For each document page
094: {
095: currentPage = (PDPage) pagesList.get(pageCounter);
096:
097: parser = new PDFStreamParser(currentPage.getContents()
098: .getStream());
099: parser.parse();
100: pageTokens = parser.getTokens();
101: editedPageTokens = new ArrayList();
102:
103: for (int counter = 0; counter < pageTokens.size(); counter++) // For each page token
104: {
105: Object token = pageTokens.get(counter);
106: if (token instanceof PDFOperator) // Test if PDFOperator
107: {
108: PDFOperator tokenOperator = (PDFOperator) token;
109:
110: if (tokenOperator.getOperation().equals("rg")) // Test if "rg" Operator.
111: {
112: if (destColorspace.equals("CMYK")) {
113: replaceRGBTokensWithCMYKTokens(
114: editedPageTokens, pageTokens,
115: counter, colorEquivalents);
116: editedPageTokens.add(PDFOperator
117: .getOperator("k"));
118: }
119: } else if (tokenOperator.getOperation()
120: .equals("RG")) // Test if "rg" Operator.
121: {
122: if (destColorspace.equals("CMYK")) {
123: replaceRGBTokensWithCMYKTokens(
124: editedPageTokens, pageTokens,
125: counter, colorEquivalents);
126: editedPageTokens.add(PDFOperator
127: .getOperator("K"));
128: }
129: } else if (tokenOperator.getOperation().equals("g")) // Test if "rg" Operator.
130: {
131: if (destColorspace.equals("CMYK")) {
132: replaceGrayTokensWithCMYKTokens(
133: editedPageTokens, pageTokens,
134: counter, colorEquivalents);
135: editedPageTokens.add(PDFOperator
136: .getOperator("k"));
137: }
138: } else if (tokenOperator.getOperation().equals("G")) // Test if "rg" Operator.
139: {
140: if (destColorspace.equals("CMYK")) {
141: replaceGrayTokensWithCMYKTokens(
142: editedPageTokens, pageTokens,
143: counter, colorEquivalents);
144: editedPageTokens.add(PDFOperator
145: .getOperator("K"));
146: }
147: } else {
148: editedPageTokens.add(token);
149: }
150: } else // Test if PDFOperator
151: {
152: editedPageTokens.add(token);
153: }
154: } // For each page token
155:
156: // We replace original page content by the edited one.
157: PDStream updatedPageContents = new PDStream(inputFile);
158: ContentStreamWriter contentWriter = new ContentStreamWriter(
159: updatedPageContents.createOutputStream());
160: contentWriter.writeTokens(editedPageTokens);
161: currentPage.setContents(updatedPageContents);
162:
163: } // For each document page
164: }
165:
166: private void replaceRGBTokensWithCMYKTokens(List editedPageTokens,
167: List pageTokens, int counter, Hashtable colorEquivalents) {
168: // Get current RGB color.
169: float red = ((COSNumber) pageTokens.get(counter - 3))
170: .floatValue();
171: float green = ((COSNumber) pageTokens.get(counter - 2))
172: .floatValue();
173: float blue = ((COSNumber) pageTokens.get(counter - 1))
174: .floatValue();
175:
176: int intRed = Math.round(red * 255.0f);
177: int intGreen = Math.round(green * 255.0f);
178: int intBlue = Math.round(blue * 255.0f);
179:
180: ColorSpaceInstance rgbColor = new ColorSpaceInstance();
181: rgbColor.colorspace = "RGB";
182: rgbColor.colorspaceValues = new int[] { intRed, intGreen,
183: intBlue };
184: ColorSpaceInstance cmykColor = (ColorSpaceInstance) colorEquivalents
185: .get(rgbColor);
186: float[] cmyk = null;
187:
188: if (cmykColor != null) {
189: cmyk = new float[] {
190: cmykColor.colorspaceValues[0] / 100.0f,
191: cmykColor.colorspaceValues[1] / 100.0f,
192: cmykColor.colorspaceValues[2] / 100.0f,
193: cmykColor.colorspaceValues[3] / 100.0f };
194: } else {
195: cmyk = convertRGBToCMYK(red, green, blue);
196: }
197:
198: //remove the RGB components that are already part of the editedPageTokens list
199: editedPageTokens.remove(editedPageTokens.size() - 1);
200: editedPageTokens.remove(editedPageTokens.size() - 1);
201: editedPageTokens.remove(editedPageTokens.size() - 1);
202:
203: // Add the new CMYK color
204: editedPageTokens.add(new COSFloat(cmyk[0]));
205: editedPageTokens.add(new COSFloat(cmyk[1]));
206: editedPageTokens.add(new COSFloat(cmyk[2]));
207: editedPageTokens.add(new COSFloat(cmyk[3]));
208: }
209:
210: private void replaceGrayTokensWithCMYKTokens(List editedPageTokens,
211: List pageTokens, int counter, Hashtable colorEquivalents) {
212: // Get current RGB color.
213: float gray = ((COSNumber) pageTokens.get(counter - 1))
214: .floatValue();
215:
216: ColorSpaceInstance grayColor = new ColorSpaceInstance();
217: grayColor.colorspace = "Grayscale";
218: grayColor.colorspaceValues = new int[] { Math.round(gray * 100) };
219: ColorSpaceInstance cmykColor = (ColorSpaceInstance) colorEquivalents
220: .get(grayColor);
221: float[] cmyk = null;
222:
223: if (cmykColor != null) {
224: cmyk = new float[] {
225: cmykColor.colorspaceValues[0] / 100.0f,
226: cmykColor.colorspaceValues[1] / 100.0f,
227: cmykColor.colorspaceValues[2] / 100.0f,
228: cmykColor.colorspaceValues[3] / 100.0f };
229: } else {
230: cmyk = new float[] { 0, 0, 0, gray };
231: }
232:
233: //remove the Gray components that are already part of the editedPageTokens list
234: editedPageTokens.remove(editedPageTokens.size() - 1);
235:
236: // Add the new CMYK color
237: editedPageTokens.add(new COSFloat(cmyk[0]));
238: editedPageTokens.add(new COSFloat(cmyk[1]));
239: editedPageTokens.add(new COSFloat(cmyk[2]));
240: editedPageTokens.add(new COSFloat(cmyk[3]));
241: }
242:
243: private static float[] convertRGBToCMYK(float red, float green,
244: float blue) {
245: //
246: // RGB->CMYK from From
247: // http://en.wikipedia.org/wiki/Talk:CMYK_color_model
248: //
249: float c = 1.0f - red;
250: float m = 1.0f - green;
251: float y = 1.0f - blue;
252: float k = 1.0f;
253:
254: k = Math.min(Math.min(Math.min(c, k), m), y);
255:
256: c = (c - k) / (1 - k);
257: m = (m - k) / (1 - k);
258: y = (y - k) / (1 - k);
259: return new float[] { c, m, y, k };
260: }
261:
262: private static int[] stringToIntArray(String string) {
263: String[] ints = string.split(",");
264: int[] retval = new int[ints.length];
265: for (int i = 0; i < ints.length; i++) {
266: retval[i] = Integer.parseInt(ints[i]);
267: }
268: return retval;
269: }
270:
271: /**
272: * Infamous main method.
273: *
274: * @param args Command line arguments, should be one and a reference to a file.
275: *
276: * @throws Exception If there is an error parsing the document.
277: */
278: public static void main(String[] args) throws Exception {
279: String password = "";
280: String inputFile = null;
281: String outputFile = null;
282: String destColorspace = "CMYK";
283:
284: Pattern colorEquivalentPattern = Pattern
285: .compile("^(.*):\\((.*)\\)" + "=(.*):\\((.*)\\)$");
286: Matcher colorEquivalentMatcher = null;
287:
288: //key= value=java.awt.Color
289: Hashtable colorEquivalents = new Hashtable();
290:
291: for (int i = 0; i < args.length; i++) {
292: if (args[i].equals(PASSWORD)) {
293: i++;
294: if (i >= args.length) {
295: usage();
296: }
297: password = args[i];
298: }
299: if (args[i].equals(DEST_COLORSPACE)) {
300: i++;
301: if (i >= args.length) {
302: usage();
303: }
304: destColorspace = args[i];
305: }
306: if (args[i].equals(CONVERSION)) {
307: i++;
308: if (i >= args.length) {
309: usage();
310: }
311:
312: colorEquivalentMatcher = colorEquivalentPattern
313: .matcher(args[i]);
314: if (!colorEquivalentMatcher.matches()) {
315: usage();
316: }
317: String srcColorSpace = colorEquivalentMatcher.group(1);
318: String srcColorvalues = colorEquivalentMatcher.group(2);
319: String destColorSpace = colorEquivalentMatcher.group(3);
320: String destColorvalues = colorEquivalentMatcher
321: .group(4);
322:
323: ConvertColorspace.ColorSpaceInstance source = new ColorSpaceInstance();
324: source.colorspace = srcColorSpace;
325: source.colorspaceValues = stringToIntArray(srcColorvalues);
326:
327: ColorSpaceInstance dest = new ColorSpaceInstance();
328: dest.colorspace = destColorSpace;
329: dest.colorspaceValues = stringToIntArray(destColorvalues);
330:
331: colorEquivalents.put(source, dest);
332:
333: } else {
334: if (inputFile == null) {
335: inputFile = args[i];
336: } else {
337: outputFile = args[i];
338: }
339: }
340: }
341:
342: if (inputFile == null) {
343: usage();
344: }
345:
346: if (outputFile == null || outputFile.equals(inputFile)) {
347: usage();
348: }
349:
350: PDDocument doc = null;
351: try {
352: doc = PDDocument.load(inputFile);
353: if (doc.isEncrypted()) {
354: try {
355: doc.decrypt(password);
356: } catch (InvalidPasswordException e) {
357: if (!password.equals(""))//they supplied the wrong password
358: {
359: System.err
360: .println("Error: The supplied password is incorrect.");
361: System.exit(2);
362: } else {
363: //they didn't suppply a password and the default of "" was wrong.
364: System.err
365: .println("Error: The document is encrypted.");
366: usage();
367: }
368: }
369: }
370: ConvertColorspace converter = new ConvertColorspace();
371: converter.replaceColors(doc, colorEquivalents,
372: destColorspace);
373: doc.save(outputFile);
374: } finally {
375: if (doc != null) {
376: doc.close();
377: }
378: }
379:
380: }
381:
382: /**
383: * This will print the usage requirements and exit.
384: */
385: private static void usage() {
386: System.err
387: .println("Usage: java org.pdfbox.ConvertColorspace [OPTIONS] <PDF Input file> "
388: + "<PDF Output File>\n"
389: + " -password <password> Password to decrypt document\n"
390: + " -equiv <color equivalent> Color equivalent to use for conversion.\n"
391: + " -destColorspace <color equivalent> The destination colorspace, CMYK is the only '"
392: + "supported colorspace."
393: + " \n"
394: + " The equiv format is : <source colorspace>:(colorspace value)=<dest colorspace>:(colorspace value)"
395: + " This option can be used as many times as necessary\n"
396: + " The supported equiv colorspaces are RGB and CMYK.\n"
397: + " RGB color values are integers between 0 and 255"
398: + " CMYK color values are integer between 0 and 100.\n"
399: + " Example: java org.pdfbox.ConvertColorspace -equiv RGB:(255,0,0)=CMYK(0,99,100,0) input.pdf output.pdf\n"
400: + " <PDF Input file> The PDF document to use\n"
401: + " <PDF Output file> The PDF file to write the result to. Must be different of input file\n");
402: System.exit(1);
403: }
404:
405: /**
406: *
407: *
408: */
409: private static class ColorSpaceInstance {
410: private String colorspace = null;
411: private int[] colorspaceValues = null;
412:
413: /**
414: * {@inheritDoc}
415: */
416: public int hashCode() {
417: int code = colorspace.hashCode();
418: for (int i = 0; i < colorspaceValues.length; i++) {
419: code += colorspaceValues[i];
420: }
421: return code;
422: }
423:
424: /**
425: * {@inheritDoc}
426: */
427: public boolean equals(Object o) {
428: boolean retval = false;
429: if (o instanceof ColorSpaceInstance) {
430: ColorSpaceInstance other = (ColorSpaceInstance) o;
431: if (this .colorspace.equals(other.colorspace)
432: && colorspaceValues.length == other.colorspaceValues.length) {
433: retval = true;
434: for (int i = 0; i < colorspaceValues.length
435: && retval; i++) {
436: retval = retval
437: && colorspaceValues[i] == other.colorspaceValues[i];
438: }
439: }
440: }
441: return retval;
442: }
443: }
444: }
|