1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
42
43
44 public class PDFGraphics2D extends VectorGraphics2D {
45
46 protected static final String FONT_RESOURCE_PREFIX = "F";
47
48 protected static final String IMAGE_RESOURCE_PREFIX = "Im";
49
50 protected static final String TRANSPARENCY_RESOURCE_PREFIX = "T";
51
52
53 protected static final double MM_IN_UNITS = 72.0 / 25.4;
54
55
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
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
68 private int curObjId;
69
70 private final Map<Integer, Integer> objPositions;
71
72 private final Map<Double, String> transpResources;
73
74 private final Map<BufferedImage, String> imageResources;
75
76 private final Map<Font, String> fontResources;
77
78 private int contentStart;
79
80
81
82
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
101
102
103
104 str = str.replaceAll("\\\\", "\\\\\\\\")
105 .replaceAll("\t", "\\\\t").replaceAll("\b", "\\\\b").replaceAll("\f", "\\\\f")
106 .replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");
107
108 float fontSize = getFont().getSize2D();
109
110
111
112 writeln("q BT");
113
114 String fontResourceId = getFontResource(getFont());
115 writeln("/", fontResourceId, " ", fontSize, " Tf");
116
117
118
119
120 writeln("1 0 0 -1 ", x, " ", y, " cm");
121
122
123
124
125
126
127
128
129
130 str = str.replaceAll("[\r\n]", "");
131 writeln("(", str, ") Tj");
132
133
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
172 write("q ");
173
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
181 write(width, " 0 0 ", height, " ", x, " ", y, " cm ");
182
183 write("1 0 0 -1 0 1 cm ");
184
185 write("/", imageResourceId, " Do ");
186
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
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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
254 writeObj(
255 "Type", "/Catalog",
256 "Pages", "2 0 R"
257 );
258
259 writeObj(
260 "Type", "/Pages",
261 "Kids", "[3 0 R]",
262 "Count", "1"
263 );
264
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
273 writeln(nextObjId(size()), " 0 obj");
274 writeDict("Length", "5 0 R");
275 writeln("stream");
276 contentStart = size();
277 writeln("q");
278
279 writeln(MM_IN_UNITS, " 0 0 ", -MM_IN_UNITS, " 0 ", h, " cm");
280 }
281
282
283
284
285
286
287
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
299
300
301
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
313
314
315 protected int peekObjId() {
316 return curObjId + 1;
317 }
318
319
320
321
322
323
324 private int nextObjId(int position) {
325 objPositions.put(curObjId, position);
326 return curObjId++;
327 }
328
329
330
331
332
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
345
346
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
359
360
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
373
374 @Override
375 protected void writeClosingDraw() {
376 writeln(" S");
377 }
378
379
380
381
382 @Override
383 protected void writeClosingFill() {
384 writeln(" f");
385 }
386
387
388
389
390
391
392 @Override
393 protected void writeShape(Shape s) {
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
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
458
459
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
485
486
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
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
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
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
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
582
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
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 }