View Javadoc

1   /*
2    * VectorGraphics2D: Vector export for Java(R) Graphics2D
3    *
4    * (C) Copyright 2010 Erich Seifert <dev[at]erichseifert.de>
5    *
6    * This file is part of VectorGraphics2D.
7    *
8    * VectorGraphics2D is free software: you can redistribute it and/or modify
9    * it under the terms of the GNU Lesser General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or
11   * (at your option) any later version.
12   *
13   * VectorGraphics2D is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU Lesser General Public License for more details.
17   *
18   * You should have received a copy of the GNU Lesser General Public License
19   * along with VectorGraphics2D.  If not, see <http://www.gnu.org/licenses/>.
20   */
21  
22  package de.erichseifert.vectorgraphics2d;
23  
24  import java.awt.BasicStroke;
25  import java.awt.Color;
26  import java.awt.Font;
27  import java.awt.Image;
28  import java.awt.Shape;
29  import java.awt.Stroke;
30  import java.awt.geom.AffineTransform;
31  import java.awt.geom.PathIterator;
32  import java.awt.geom.Rectangle2D;
33  import java.awt.image.BufferedImage;
34  import java.io.UnsupportedEncodingException;
35  import java.util.Arrays;
36  import java.util.LinkedHashMap;
37  import java.util.Map;
38  import java.util.TreeMap;
39  
40  /**
41   * <code>Graphics2D</code> implementation that saves all operations to a string
42   * in the <i>Portable Document Format</i> (PDF).
43   */
44  public class PDFGraphics2D extends VectorGraphics2D {
45  	/** Prefix string for PDF font resource ids. */
46  	protected static final String FONT_RESOURCE_PREFIX = "F";
47  	/** Prefix string for PDF image resource ids. */
48  	protected static final String IMAGE_RESOURCE_PREFIX = "Im";
49  	/** Prefix string for PDF transparency resource ids. */
50  	protected static final String TRANSPARENCY_RESOURCE_PREFIX = "T";
51  
52  	/** Constant to convert values from millimeters to PostScript®/PDF units (1/72th inch). */
53  	protected static final double MM_IN_UNITS = 72.0 / 25.4;
54  
55  	/** Mapping of stroke endcap values from Java to PDF. */
56  	private static final Map<Integer, Integer> STROKE_ENDCAPS = DataUtils.map(
57  		new Integer[] { BasicStroke.CAP_BUTT, BasicStroke.CAP_ROUND, BasicStroke.CAP_SQUARE },
58  		new Integer[] { 0, 1, 2 }
59  	);
60  
61  	/** Mapping of line join values for path drawing from Java to PDF. */
62  	private static final Map<Integer, Integer> STROKE_LINEJOIN = DataUtils.map(
63  		new Integer[] { BasicStroke.JOIN_MITER, BasicStroke.JOIN_ROUND, BasicStroke.JOIN_BEVEL },
64  		new Integer[] { 0, 1, 2 }
65  	);
66  
67  	/** Id of the current PDF object. */
68  	private int curObjId;
69  	/** Mapping from objects to file positions. */
70  	private final Map<Integer, Integer> objPositions;
71  	/** Mapping from transparency levels to transparency resource ids. */
72  	private final Map<Double, String> transpResources;
73  	/** Mapping from image data to image resource ids. */
74  	private final Map<BufferedImage, String> imageResources;
75  	/** Mapping from font objects to font resource ids. */
76  	private final Map<Font, String> fontResources;
77  	/** File position of the actual content. */
78  	private int contentStart;
79  
80  	/**
81  	 * Constructor that initializes a new <code>PDFGraphics2D</code> instance.
82  	 * The document dimension must be specified as parameters.
83  	 */
84  	public PDFGraphics2D(double x, double y, double width, double height) {
85  		super(x, y, width, height);
86  		curObjId = 1;
87  		objPositions = new TreeMap<Integer, Integer>();
88  		transpResources = new TreeMap<Double, String>();
89  		imageResources = new LinkedHashMap<BufferedImage, String>();
90  		fontResources = new LinkedHashMap<Font, String>();
91  		writeHeader();
92  	}
93  
94  	@Override
95  	protected void writeString(String str, double x, double y) {
96  		if (str == null || str.isEmpty()) {
97  			return;
98  		}
99  
100 		// TODO Encode string
101 		//byte[] bytes = str.getBytes("ISO-8859-1");
102 
103 		// Escape string
104 		str = str.replaceAll("\\\\", "\\\\\\\\")
105 			.replaceAll("\t", "\\\\t").replaceAll("\b", "\\\\b").replaceAll("\f", "\\\\f")
106 			.replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");
107 
108 		float fontSize = getFont().getSize2D();
109 		//float leading = getFont().getLineMetrics("", getFontRenderContext()).getLeading();
110 
111 		// Start text and save current graphics state
112 		writeln("q BT");
113 
114 		String fontResourceId = getFontResource(getFont());
115 		writeln("/", fontResourceId, " ", fontSize, " Tf");
116 		// Set leading
117 		//writeln(fontSize + leading, " TL");
118 
119 		// Undo swapping of y axis for text
120 		writeln("1 0 0 -1 ", x, " ", y, " cm");
121 
122 		/*
123 		// Extract lines
124 		String[] lines = str.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
125 		// Paint lines
126 		for (int i = 0; i < lines.length; i++) {
127 			writeln("(", lines[i], ") ", (i == 0) ? "Tj" : "'");
128 		}*/
129 
130 		str = str.replaceAll("[\r\n]", "");
131 		writeln("(", str, ") Tj");
132 
133 		// End text and restore previous graphics state
134 		writeln("ET Q");
135 	}
136 
137 	@Override
138 	public void setStroke(Stroke s) {
139 		BasicStroke bsPrev;
140 		if (getStroke() instanceof BasicStroke) {
141 			bsPrev = (BasicStroke) getStroke();
142 		} else {
143 			bsPrev = new BasicStroke();
144 		}
145 
146 		super.setStroke(s);
147 
148 		if (s instanceof BasicStroke) {
149 			BasicStroke bs = (BasicStroke) s;
150 			if (bs.getLineWidth() != bsPrev.getLineWidth()) {
151 				writeln(bs.getLineWidth(), " w");
152 			}
153 			if (bs.getLineJoin() != bsPrev.getLineJoin()) {
154 				writeln(STROKE_LINEJOIN.get(bs.getLineJoin()), " j");
155 			}
156 			if (bs.getEndCap() != bsPrev.getEndCap()) {
157 				writeln(STROKE_ENDCAPS.get(bs.getEndCap()), " J");
158 			}
159 			if ((!Arrays.equals(bs.getDashArray(), bsPrev.getDashArray())) ||
160 				(bs.getDashPhase() != bsPrev.getDashPhase())) {
161 				writeln("[", DataUtils.join(" ", bs.getDashArray()), "] ",
162 						bs.getDashPhase(), " d");
163 			}
164 		}
165 	}
166 
167 	@Override
168 	protected void writeImage(Image img, int imgWidth, int imgHeight, double x, double y, double width, double height) {
169 		BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
170 		String imageResourceId = getImageResource(bufferedImg);
171 		// Save graphics state
172 		write("q ");
173 		// Take current transformations into account
174 		AffineTransform txCurrent = getTransform();
175 		if (!txCurrent.isIdentity()) {
176 			double[] matrix = new double[6];
177 			txCurrent.getMatrix(matrix);
178 			write(DataUtils.join(" ", matrix), " cm ");
179 		}
180 		// Move image to correct position and scale it to (width, height)
181 		write(width, " 0 0 ", height, " ", x, " ", y, " cm ");
182 		// Swap y axis
183 		write("1 0 0 -1 0 1 cm ");
184 		// Draw image
185 		write("/", imageResourceId, " Do ");
186 		// Restore old graphics state
187 		writeln("Q");
188 	}
189 
190 	@Override
191 	public void setColor(Color c) {
192 		Color color = getColor();
193 		if (c != null) {
194 			super.setColor(c);
195 			if (color.getAlpha() != c.getAlpha()) {
196 				// Add a new graphics state to resources
197 				double a = c.getAlpha()/255.0;
198 				String transpResourceId = getTransparencyResource(a);
199 				writeln("/", transpResourceId, " gs");
200 			}
201 			if (color.getRed() != c.getRed() || color.getGreen() != c.getGreen() || color.getBlue() != c.getBlue()) {
202 				double r = c.getRed()/255.0;
203 				double g = c.getGreen()/255.0;
204 				double b = c.getBlue()/255.0;
205 				write(r, " ", g, " ", b, " rg ");
206 				writeln(r, " ", g, " ", b, " RG");
207 			}
208 		}
209 	}
210 
211 	@Override
212 	public void setClip(Shape clip) {
213 		if (getClip() != null) {
214 			writeln("Q");
215 		}
216 		super.setClip(clip);
217 		if (getClip() != null) {
218 			writeln("q");
219 			writeShape(getClip());
220 			writeln(" W n");
221 		}
222 	}
223 
224 	// TODO Correct transformations
225 	/*
226 	@Override
227 	protected void setAffineTransform(AffineTransform tx) {
228 		// Undo previous transforms
229 		if (isTransformed()) {
230 			writeln("Q");
231 		}
232 		// Set new transform
233 		super.setAffineTransform(tx);
234 		// Write transform to document
235 		if (isTransformed()) {
236 			writeln("q");
237 			double[] matrix = new double[6];
238 			getTransform().getMatrix(matrix);
239 			writeln(DataUtils.join(" ", matrix), " cm");
240 		}
241 	}
242 	//*/
243 
244 	@Override
245 	protected void writeHeader() {
246 		Rectangle2D bounds = getBounds();
247 		int x = (int)Math.floor(bounds.getX() * MM_IN_UNITS);
248 		int y = (int)Math.floor(bounds.getY() * MM_IN_UNITS);
249 		int w = (int)Math.ceil(bounds.getWidth() * MM_IN_UNITS);
250 		int h = (int)Math.ceil(bounds.getHeight() * MM_IN_UNITS);
251 
252 		writeln("%PDF-1.4");
253 		// Object 1
254 		writeObj(
255 			"Type", "/Catalog",
256 			"Pages", "2 0 R"
257 		);
258 		// Object 2
259 		writeObj(
260 			"Type", "/Pages",
261 			"Kids", "[3 0 R]",
262 			"Count", "1"
263 		);
264 		// Object 3
265 		writeObj(
266 			"Type", "/Page",
267 			"Parent", "2 0 R",
268 			"MediaBox", String.format("[%d %d %d %d]", x, y, w, h),
269 			"Contents", "4 0 R",
270 			"Resources", "6 0 R"
271 		);
272 		// Object 5
273 		writeln(nextObjId(size()), " 0 obj");
274 		writeDict("Length", "5 0 R");
275 		writeln("stream");
276 		contentStart = size();
277 		writeln("q");
278 		// Adjust page size and page origin
279 		writeln(MM_IN_UNITS, " 0 0 ", -MM_IN_UNITS, " 0 ", h, " cm");
280 	}
281 
282 	/**
283 	 * Write a PDF dictionary from the specified collection of objects.
284 	 * The passed objects are converted to strings. Every object with odd
285 	 * position is used as key, every object with even position is used
286 	 * as value.
287 	 * @param strs Objects to be written to dictionary
288 	 */
289 	protected void writeDict(Object... strs) {
290 		writeln("<<");
291 		for (int i = 0; i < strs.length; i += 2) {
292 			writeln("/", strs[i], " ", strs[i + 1]);
293 		}
294 		writeln(">>");
295 	}
296 
297 	/**
298 	 * Write a collection of elements to the document stream as PDF object.
299 	 * The passed objects are converted to strings.
300 	 * @param strs Objects to be written to the document stream.
301 	 * @return Id of the PDF object that was written.
302 	 */
303 	protected int writeObj(Object... strs) {
304 		int objId = nextObjId(size());
305 		writeln(objId, " 0 obj");
306 		writeDict(strs);
307 		writeln("endobj");
308 		return objId;
309 	}
310 
311 	/**
312 	 * Returns the next PDF object id without incrementing.
313 	 * @return Next PDF object id.
314 	 */
315 	protected int peekObjId() {
316 		return curObjId + 1;
317 	}
318 
319 	/**
320 	 * Returns a new PDF object id with every call.
321 	 * @param position File position of the object.
322 	 * @return A new PDF object id.
323 	 */
324 	private int nextObjId(int position) {
325 		objPositions.put(curObjId, position);
326 		return curObjId++;
327 	}
328 
329 	/**
330 	 * Returns the resource for the specified transparency level.
331 	 * @param a Transparency level.
332 	 * @return A new PDF object id.
333 	 */
334 	protected String getTransparencyResource(double a) {
335 		String name = transpResources.get(a);
336 		if (name == null) {
337 			name = String.format("%s%d", TRANSPARENCY_RESOURCE_PREFIX, transpResources.size() + 1);
338 			transpResources.put(a, name);
339 		}
340 		return name;
341 	}
342 
343 	/**
344 	 * Returns the resource for the specified image data.
345 	 * @param bufferedImg Image object with data.
346 	 * @return A new PDF object id.
347 	 */
348 	protected String getImageResource(BufferedImage bufferedImg) {
349 		String name = imageResources.get(bufferedImg);
350 		if (name == null) {
351 			name = String.format("%s%d", IMAGE_RESOURCE_PREFIX, imageResources.size() + 1);
352 			imageResources.put(bufferedImg, name);
353 		}
354 		return name;
355 	}
356 
357 	/**
358 	 * Returns the resource describing the specified font.
359 	 * @param font Font to be described.
360 	 * @return A new PDF object id.
361 	 */
362 	protected String getFontResource(Font font) {
363 		String name = fontResources.get(font);
364 		if (name == null) {
365 			name = String.format("%s%d", FONT_RESOURCE_PREFIX, fontResources.size() + 1);
366 			fontResources.put(font, name);
367 		}
368 		return name;
369 	}
370 
371 	/**
372 	 * Utility method for writing a tag closing fragment for drawing operations.
373 	 */
374 	@Override
375 	protected void writeClosingDraw() {
376 		writeln(" S");
377 	}
378 
379 	/**
380 	 * Utility method for writing a tag closing fragment for filling operations.
381 	 */
382 	@Override
383 	protected void writeClosingFill() {
384 		writeln(" f");
385 	}
386 
387 	/**
388 	 * Utility method for writing an arbitrary shape to.
389 	 * It tries to translate Java2D shapes to the corresponding PDF shape
390 	 * commands.
391 	 */
392 	@Override
393 	protected void writeShape(Shape s) {
394 		// TODO Correct transformations
395 		/*
396 		if (s instanceof Line2D) {
397 			Line2D l = (Line2D) s;
398 			double x1 = l.getX1();
399 			double y1 = l.getY1();
400 			double x2 = l.getX2();
401 			double y2 = l.getY2();
402 			write(x1, " ", y1, " m ", x2, " ", y2, " l");
403 		} else if (s instanceof Rectangle2D) {
404 			Rectangle2D r = (Rectangle2D) s;
405 			double x = r.getX();
406 			double y = r.getY();
407 			double width = r.getWidth();
408 			double height = r.getHeight();
409 			write(x, " ", y, " ", width, " ", height, " re");
410 		} else //*/
411 		{
412 			s = getTransform().createTransformedShape(s);
413 			PathIterator segments = s.getPathIterator(null);
414 			double[] coordsCur = new double[6];
415 			double[] pointPrev = new double[2];
416 			for (int i = 0; !segments.isDone(); i++, segments.next()) {
417 				if (i > 0) {
418 					write(" ");
419 				}
420 				int segmentType = segments.currentSegment(coordsCur);
421 				switch (segmentType) {
422 				case PathIterator.SEG_MOVETO:
423 					write(coordsCur[0], " ", coordsCur[1], " m");
424 					pointPrev[0] = coordsCur[0];
425 					pointPrev[1] = coordsCur[1];
426 					break;
427 				case PathIterator.SEG_LINETO:
428 					write(coordsCur[0], " ", coordsCur[1], " l");
429 					pointPrev[0] = coordsCur[0];
430 					pointPrev[1] = coordsCur[1];
431 					break;
432 				case PathIterator.SEG_CUBICTO:
433 					write(coordsCur[0], " ", coordsCur[1], " ", coordsCur[2], " ", coordsCur[3], " ", coordsCur[4], " ", coordsCur[5], " c");
434 					pointPrev[0] = coordsCur[4];
435 					pointPrev[1] = coordsCur[5];
436 					break;
437 				case PathIterator.SEG_QUADTO:
438 					double x1 = pointPrev[0] + 2.0/3.0*(coordsCur[0] - pointPrev[0]);
439 					double y1 = pointPrev[1] + 2.0/3.0*(coordsCur[1] - pointPrev[1]);
440 					double x2 = coordsCur[0] + 1.0/3.0*(coordsCur[2] - coordsCur[0]);
441 					double y2 = coordsCur[1] + 1.0/3.0*(coordsCur[3] - coordsCur[1]);
442 					double x3 = coordsCur[2];
443 					double y3 = coordsCur[3];
444 					write(x1, " ", y1, " ", x2, " ", y2, " ", x3, " ", y3, " c");
445 					pointPrev[0] = x3;
446 					pointPrev[1] = y3;
447 					break;
448 				case PathIterator.SEG_CLOSE:
449 					write("h");
450 					break;
451 				}
452 			}
453 		}
454 	}
455 
456 	/**
457 	 * Returns a string which represents the data of the specified image.
458 	 * @param bufferedImg Image to convert.
459 	 * @return String with image data.
460 	 */
461 	private String getPdf(BufferedImage bufferedImg) {
462 		int width = bufferedImg.getWidth();
463 		int height = bufferedImg.getHeight();
464 		int bands = bufferedImg.getSampleModel().getNumBands();
465 		StringBuffer str = new StringBuffer(width*height*bands*2);
466 		for (int y = 0; y < height; y++) {
467 			for (int x = 0; x < width; x++) {
468 				int pixel = bufferedImg.getRGB(x, y) & 0xffffff;
469 				if (bands >= 3) {
470 					String hex = String.format("%06x", pixel);
471 					str.append(hex);
472 				} else if (bands == 1) {
473 					str.append(String.format("%02x", pixel));
474 				}
475 			}
476 			str.append('\n');
477 		}
478 		return str.append('>').toString();
479 	}
480 
481 	@Override
482 	protected String getFooter() {
483 		StringBuffer footer = new StringBuffer();
484 		// TODO Correct transformations
485 		/*if (isTransformed()) {
486 			footer.append("Q\n");
487 		}*/
488 		if (getClip() != null) {
489 			footer.append("Q\n");
490 		}
491 		footer.append("Q");
492 		int contentEnd = size() + footer.length();
493 		footer.append('\n');
494 		footer.append("endstream\n");
495 		footer.append("endobj\n");
496 
497 		int lenObjId = nextObjId(size() + footer.length());
498 		footer.append(lenObjId).append(" 0 obj\n");
499 		footer.append(contentEnd - contentStart).append('\n');
500 		footer.append("endobj\n");
501 
502 		int resourcesObjId = nextObjId(size() + footer.length());
503 		footer.append(resourcesObjId).append(" 0 obj\n");
504 		footer.append("<<\n");
505 		footer.append(" /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n");
506 
507 
508 		// Add resources for fonts
509 		if (!fontResources.isEmpty()) {
510 			footer.append(" /Font <<\n");
511 			for (Map.Entry<Font, String> entry : fontResources.entrySet()) {
512 				Font font = entry.getKey();
513 				String resourceId = entry.getValue();
514 				footer.append("  /").append(resourceId)
515 					.append(" << /Type /Font")
516 					.append(" /Subtype /").append("TrueType").append(" /BaseFont /").append(font.getPSName())
517 					.append(" >>\n");
518 			}
519 			footer.append(" >>\n");
520 		}
521 
522 		// Add resources for images
523 		if (!imageResources.isEmpty()) {
524 			footer.append(" /XObject <<\n");
525 
526 			int objIdOffset = 0;
527 			for (Map.Entry<BufferedImage, String> entry : imageResources.entrySet()) {
528 				BufferedImage image = entry.getKey();
529 				String resourceId = entry.getValue();
530 
531 				footer.append("  /").append(resourceId).append(' ')
532 					.append(curObjId + objIdOffset).append(" 0 R\n");
533 				objIdOffset++;
534 			}
535 			footer.append(" >>\n");
536 		}
537 
538 		// Add resources for transparency levels
539 		if (!transpResources.isEmpty()) {
540 			footer.append(" /ExtGState <<\n");
541 			for (Map.Entry<Double, String> entry : transpResources.entrySet()) {
542 				Double alpha = entry.getKey();
543 				String resourceId = entry.getValue();
544 				footer.append("  /").append(resourceId)
545 					.append(" << /Type /ExtGState")
546 					.append(" /ca ").append(alpha).append(" /CA ").append(alpha)
547 					.append(" >>\n");
548 			}
549 			footer.append(" >>\n");
550 		}
551 
552 		footer.append(">>\n");
553 		footer.append("endobj\n");
554 
555 		// Add data of images
556 		for (BufferedImage image : imageResources.keySet()) {
557 			int imageObjId = nextObjId(size() + footer.length());
558 			footer.append(imageObjId).append(" 0 obj\n");
559 			footer.append("<<\n");
560 			String imageData = getPdf(image);
561 			footer.append("/Type /XObject\n")
562 				.append("/Subtype /Image\n")
563 				.append("/Width ").append(image.getWidth()).append('\n')
564 				.append("/Height ").append(image.getHeight()).append('\n')
565 				.append("/ColorSpace /DeviceRGB\n")
566 				.append("/BitsPerComponent 8\n")
567 				.append("/Length ").append(imageData.length()).append('\n')
568 				.append("/Filter /ASCIIHexDecode\n")
569 				.append(">>\n")
570 				.append("stream\n")
571 				.append(imageData)
572 				.append("\nendstream\n")
573 				.append("endobj\n");
574 		}
575 
576 		int objs = objPositions.size() + 1;
577 
578 		int xrefPos = size() + footer.length();
579 		footer.append("xref\n");
580 		footer.append("0 ").append(objs).append('\n');
581 		// lines of xref entries must must be exactly 20 bytes long
582 		// (including line break) and thus end with <SPACE NEWLINE>
583 		footer.append(String.format("%010d %05d", 0, 65535)).append(" f \n");
584 		for (int pos : objPositions.values()) {
585 			footer.append(String.format("%010d %05d", pos, 0)).append(" n \n");
586 		}
587 
588 		footer.append("trailer\n");
589 		footer.append("<<\n");
590 		footer.append("/Size ").append(objs).append('\n');
591 		footer.append("/Root 1 0 R\n");
592 		footer.append(">>\n");
593 		footer.append("startxref\n");
594 		footer.append(xrefPos).append('\n');
595 
596 		footer.append("%%EOF\n");
597 
598 		return footer.toString();
599 	}
600 
601 	@Override
602 	public String toString() {
603 		String doc = super.toString();
604 		//doc = doc.replaceAll("q\n[0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* cm\nQ\n", "");
605 		return doc;
606 	}
607 
608 	@Override
609 	public byte[] getBytes() {
610 		try {
611 			return toString().getBytes("ISO-8859-1");
612 		} catch (UnsupportedEncodingException e) {
613 			return super.getBytes();
614 		}
615 	}
616 }