1 /* 2 * Jalview - A Sequence Alignment Editor and Viewer (2.11.1.4) 3 * Copyright (C) 2021 The Jalview Authors 4 * 5 * This file is part of Jalview. 6 * 7 * Jalview is free software: you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License 9 * as published by the Free Software Foundation, either version 3 10 * of the License, or (at your option) any later version. 11 * 12 * Jalview is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty 14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 15 * PURPOSE. See the GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>. 19 * The Jalview Authors are detailed in the 'AUTHORS' file. 20 */ 21 package org.jibble.epsgraphics; 22 23 import jalview.util.MessageManager; 24 25 import java.awt.AlphaComposite; 26 import java.awt.BasicStroke; 27 import java.awt.Color; 28 import java.awt.Composite; 29 import java.awt.Font; 30 import java.awt.FontMetrics; 31 import java.awt.Graphics; 32 import java.awt.GraphicsConfiguration; 33 import java.awt.GraphicsDevice; 34 import java.awt.GraphicsEnvironment; 35 import java.awt.Image; 36 import java.awt.Paint; 37 import java.awt.Polygon; 38 import java.awt.Rectangle; 39 import java.awt.RenderingHints; 40 import java.awt.Shape; 41 import java.awt.Stroke; 42 import java.awt.font.FontRenderContext; 43 import java.awt.font.GlyphVector; 44 import java.awt.font.TextAttribute; 45 import java.awt.font.TextLayout; 46 import java.awt.geom.AffineTransform; 47 import java.awt.geom.Arc2D; 48 import java.awt.geom.Area; 49 import java.awt.geom.Ellipse2D; 50 import java.awt.geom.GeneralPath; 51 import java.awt.geom.Line2D; 52 import java.awt.geom.PathIterator; 53 import java.awt.geom.Point2D; 54 import java.awt.geom.Rectangle2D; 55 import java.awt.geom.RoundRectangle2D; 56 import java.awt.image.BufferedImage; 57 import java.awt.image.BufferedImageOp; 58 import java.awt.image.ColorModel; 59 import java.awt.image.ImageObserver; 60 import java.awt.image.PixelGrabber; 61 import java.awt.image.RenderedImage; 62 import java.awt.image.WritableRaster; 63 import java.awt.image.renderable.RenderableImage; 64 import java.io.File; 65 import java.io.FileOutputStream; 66 import java.io.IOException; 67 import java.io.OutputStream; 68 import java.io.StringWriter; 69 import java.text.AttributedCharacterIterator; 70 import java.text.AttributedString; 71 import java.text.CharacterIterator; 72 import java.util.Hashtable; 73 import java.util.Map; 74 75 /** 76 * EpsGraphics2D is suitable for creating high quality EPS graphics for use in 77 * documents and papers, and can be used just like a standard Graphics2D object. 78 * <p> 79 * Many Java programs use Graphics2D to draw stuff on the screen, and while it 80 * is easy to save the output as a png or jpeg file, it is a little harder to 81 * export it as an EPS for including in a document or paper. 82 * <p> 83 * This class makes the whole process extremely easy, because you can use it as 84 * if it's a Graphics2D object. The only difference is that all of the 85 * implemented methods create EPS output, which means the diagrams you draw can 86 * be resized without leading to any of the jagged edges you may see when 87 * resizing pixel-based images, such as jpeg and png files. 88 * <p> 89 * Example usage: 90 * <p> 91 * 92 * <pre> 93 * Graphics2D g = new EpsGraphics2D(); 94 * g.setColor(Color.black); 95 * 96 * // Line thickness 2. 97 * g.setStroke(new BasicStroke(2.0f)); 98 * 99 * // Draw a line. 100 * g.drawLine(10, 10, 50, 10); 101 * 102 * // Fill a rectangle in blue 103 * g.setColor(Color.blue); 104 * g.fillRect(10, 0, 20, 20); 105 * 106 * // Get the EPS output. 107 * String output = g.toString(); 108 * </pre> 109 * 110 * <p> 111 * You do not need to worry about the size of the canvas when drawing on a 112 * EpsGraphics2D object. The bounding box of the EPS document will automatically 113 * resize to accomodate new items that you draw. 114 * <p> 115 * Not all methods are implemented yet. Those that are not are clearly labelled. 116 * <p> 117 * Copyright Paul Mutton, <a 118 * href="http://www.jibble.org/">http://www.jibble.org/</a> 119 * 120 */ 121 public class EpsGraphics2D extends java.awt.Graphics2D 122 implements AutoCloseable 123 { 124 125 public static final String VERSION = "0.8.8"; 126 127 /** 128 * Constructs a new EPS document that is initially empty and can be drawn on 129 * like a Graphics2D object. The EPS document is stored in memory. 130 */ EpsGraphics2D()131 public EpsGraphics2D() 132 { 133 this("Untitled"); 134 } 135 136 /** 137 * Constructs a new EPS document that is initially empty and can be drawn on 138 * like a Graphics2D object. The EPS document is stored in memory. 139 */ EpsGraphics2D(String title)140 public EpsGraphics2D(String title) 141 { 142 _document = new EpsDocument(title); 143 _backgroundColor = Color.white; 144 _clip = null; 145 _transform = new AffineTransform(); 146 _clipTransform = new AffineTransform(); 147 _accurateTextMode = true; 148 setColor(Color.black); 149 setPaint(Color.black); 150 setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR)); 151 setFont(Font.decode(null)); 152 setStroke(new BasicStroke()); 153 } 154 155 /** 156 * Constructs a new EPS document that is initially empty and can be drawn on 157 * like a Graphics2D object. The EPS document is written to the file as it 158 * goes, which reduces memory usage. The bounding box of the document is fixed 159 * and specified at construction time by minX,minY,maxX,maxY. The file is 160 * flushed and closed when the close() method is called. 161 */ EpsGraphics2D(String title, File file, int minX, int minY, int maxX, int maxY)162 public EpsGraphics2D(String title, File file, int minX, int minY, 163 int maxX, int maxY) throws IOException 164 { 165 this(title, new FileOutputStream(file), minX, minY, maxX, maxY); 166 } 167 168 /** 169 * Constructs a new EPS document that is initially empty and can be drawn on 170 * like a Graphics2D object. The EPS document is written to the output stream 171 * as it goes, which reduces memory usage. The bounding box of the document is 172 * fixed and specified at construction time by minX,minY,maxX,maxY. The output 173 * stream is flushed and closed when the close() method is called. 174 */ EpsGraphics2D(String title, OutputStream outputStream, int minX, int minY, int maxX, int maxY)175 public EpsGraphics2D(String title, OutputStream outputStream, int minX, 176 int minY, int maxX, int maxY) throws IOException 177 { 178 this(title); 179 _document = new EpsDocument(title, outputStream, minX, minY, maxX, maxY); 180 } 181 182 /** 183 * Constructs a new EpsGraphics2D instance that is a copy of the supplied 184 * argument and points at the same EpsDocument. 185 */ EpsGraphics2D(EpsGraphics2D g)186 protected EpsGraphics2D(EpsGraphics2D g) 187 { 188 _document = g._document; 189 _backgroundColor = g._backgroundColor; 190 _clip = g._clip; 191 _clipTransform = (AffineTransform) g._clipTransform.clone(); 192 _transform = (AffineTransform) g._transform.clone(); 193 _color = g._color; 194 _paint = g._paint; 195 _composite = g._composite; 196 _font = g._font; 197 _stroke = g._stroke; 198 _accurateTextMode = g._accurateTextMode; 199 } 200 201 /** 202 * This method is called to indicate that a particular method is not supported 203 * yet. The stack trace is printed to the standard output. 204 */ methodNotSupported()205 private void methodNotSupported() 206 { 207 EpsException e = new EpsException(MessageManager.formatMessage( 208 "exception.eps_method_not_supported", new String[] { VERSION })); 209 e.printStackTrace(System.err); 210 } 211 212 // ///////////// Specialist methods /////////////////////// 213 214 /** 215 * Sets whether to use accurate text mode when rendering text in EPS. This is 216 * enabled (true) by default. When accurate text mode is used, all text will 217 * be rendered in EPS to appear exactly the same as it would do when drawn 218 * with a Graphics2D context. With accurate text mode enabled, it is not 219 * necessary for the EPS viewer to have the required font installed. 220 * <p> 221 * Turning off accurate text mode will require the EPS viewer to have the 222 * necessary fonts installed. If you are using a lot of text, you will find 223 * that this significantly reduces the file size of your EPS documents. 224 * AffineTransforms can only affect the starting point of text using this 225 * simpler text mode - all text will be horizontal. 226 */ setAccurateTextMode(boolean b)227 public void setAccurateTextMode(boolean b) 228 { 229 _accurateTextMode = b; 230 } 231 232 /** 233 * Returns whether accurate text mode is being used. 234 */ getAccurateTextMode()235 public boolean getAccurateTextMode() 236 { 237 return _accurateTextMode; 238 } 239 240 /** 241 * Flushes the buffered contents of this EPS document to the underlying 242 * OutputStream it is being written to. 243 */ flush()244 public void flush() throws IOException 245 { 246 _document.flush(); 247 } 248 249 /** 250 * Closes the EPS file being output to the underlying OutputStream. The 251 * OutputStream is automatically flushed before being closed. If you forget to 252 * do this, the file may be incomplete. 253 */ 254 @Override close()255 public void close() throws IOException 256 { 257 flush(); 258 _document.close(); 259 } 260 261 /** 262 * Appends a line to the EpsDocument. 263 */ append(String line)264 private void append(String line) 265 { 266 _document.append(this, line); 267 } 268 269 /** 270 * Returns the point after it has been transformed by the transformation. 271 */ transform(float x, float y)272 private Point2D transform(float x, float y) 273 { 274 Point2D result = new Point2D.Float(x, y); 275 result = _transform.transform(result, result); 276 result.setLocation(result.getX(), -result.getY()); 277 return result; 278 } 279 280 /** 281 * Appends the commands required to draw a shape on the EPS document. 282 */ draw(Shape s, String action)283 private void draw(Shape s, String action) 284 { 285 286 if (s != null) 287 { 288 289 // Rectangle2D userBounds = s.getBounds2D(); 290 if (!_transform.isIdentity()) 291 { 292 s = _transform.createTransformedShape(s); 293 } 294 295 // Update the bounds. 296 if (!action.equals("clip")) 297 { 298 Rectangle2D shapeBounds = s.getBounds2D(); 299 Rectangle2D visibleBounds = shapeBounds; 300 if (_clip != null) 301 { 302 Rectangle2D clipBounds = _clip.getBounds2D(); 303 visibleBounds = shapeBounds.createIntersection(clipBounds); 304 } 305 float lineRadius = _stroke.getLineWidth() / 2; 306 float minX = (float) visibleBounds.getMinX() - lineRadius; 307 float minY = (float) visibleBounds.getMinY() - lineRadius; 308 float maxX = (float) visibleBounds.getMaxX() + lineRadius; 309 float maxY = (float) visibleBounds.getMaxY() + lineRadius; 310 _document.updateBounds(minX, -minY); 311 _document.updateBounds(maxX, -maxY); 312 } 313 314 append("newpath"); 315 int type = 0; 316 float[] coords = new float[6]; 317 PathIterator it = s.getPathIterator(null); 318 float x0 = 0; 319 float y0 = 0; 320 int count = 0; 321 while (!it.isDone()) 322 { 323 type = it.currentSegment(coords); 324 float x1 = coords[0]; 325 float y1 = -coords[1]; 326 float x2 = coords[2]; 327 float y2 = -coords[3]; 328 float x3 = coords[4]; 329 float y3 = -coords[5]; 330 331 if (type == PathIterator.SEG_CLOSE) 332 { 333 append("closepath"); 334 count++; 335 } 336 else if (type == PathIterator.SEG_CUBICTO) 337 { 338 append(x1 + " " + y1 + " " + x2 + " " + y2 + " " + x3 + " " + y3 339 + " curveto"); 340 count++; 341 x0 = x3; 342 y0 = y3; 343 } 344 else if (type == PathIterator.SEG_LINETO) 345 { 346 append(x1 + " " + y1 + " lineto"); 347 count++; 348 x0 = x1; 349 y0 = y1; 350 } 351 else if (type == PathIterator.SEG_MOVETO) 352 { 353 append(x1 + " " + y1 + " moveto"); 354 count++; 355 x0 = x1; 356 y0 = y1; 357 } 358 else if (type == PathIterator.SEG_QUADTO) 359 { 360 // Convert the quad curve into a cubic. 361 float _x1 = x0 + 2 / 3f * (x1 - x0); 362 float _y1 = y0 + 2 / 3f * (y1 - y0); 363 float _x2 = x1 + 1 / 3f * (x2 - x1); 364 float _y2 = y1 + 1 / 3f * (y2 - y1); 365 float _x3 = x2; 366 float _y3 = y2; 367 append(_x1 + " " + _y1 + " " + _x2 + " " + _y2 + " " + _x3 + " " 368 + _y3 + " curveto"); 369 count++; 370 x0 = _x3; 371 y0 = _y3; 372 } 373 else if (type == PathIterator.WIND_EVEN_ODD) 374 { 375 // Ignore. 376 } 377 else if (type == PathIterator.WIND_NON_ZERO) 378 { 379 // Ignore. 380 } 381 it.next(); 382 } 383 append(action); 384 append("newpath"); 385 } 386 } 387 388 /** 389 * Returns a hex string that always contains two characters. 390 */ toHexString(int n)391 private String toHexString(int n) 392 { 393 String result = Integer.toString(n, 16); 394 while (result.length() < 2) 395 { 396 result = "0" + result; 397 } 398 return result; 399 } 400 401 // ///////////// Graphics2D methods /////////////////////// 402 403 /** 404 * Draws a 3D rectangle outline. If it is raised, light appears to come from 405 * the top left. 406 */ 407 @Override draw3DRect(int x, int y, int width, int height, boolean raised)408 public void draw3DRect(int x, int y, int width, int height, boolean raised) 409 { 410 Color originalColor = getColor(); 411 Stroke originalStroke = getStroke(); 412 413 setStroke(new BasicStroke(1.0f)); 414 415 if (raised) 416 { 417 setColor(originalColor.brighter()); 418 } 419 else 420 { 421 setColor(originalColor.darker()); 422 } 423 424 drawLine(x, y, x + width, y); 425 drawLine(x, y, x, y + height); 426 427 if (raised) 428 { 429 setColor(originalColor.darker()); 430 } 431 else 432 { 433 setColor(originalColor.brighter()); 434 } 435 436 drawLine(x + width, y + height, x, y + height); 437 drawLine(x + width, y + height, x + width, y); 438 439 setColor(originalColor); 440 setStroke(originalStroke); 441 } 442 443 /** 444 * Fills a 3D rectangle. If raised, it has bright fill and light appears to 445 * come from the top left. 446 */ 447 @Override fill3DRect(int x, int y, int width, int height, boolean raised)448 public void fill3DRect(int x, int y, int width, int height, boolean raised) 449 { 450 Color originalColor = getColor(); 451 452 if (raised) 453 { 454 setColor(originalColor.brighter()); 455 } 456 else 457 { 458 setColor(originalColor.darker()); 459 } 460 draw(new Rectangle(x, y, width, height), "fill"); 461 setColor(originalColor); 462 draw3DRect(x, y, width, height, raised); 463 } 464 465 /** 466 * Draws a Shape on the EPS document. 467 */ 468 @Override draw(Shape s)469 public void draw(Shape s) 470 { 471 draw(s, "stroke"); 472 } 473 474 /** 475 * Draws an Image on the EPS document. 476 */ 477 @Override drawImage(Image img, AffineTransform xform, ImageObserver obs)478 public boolean drawImage(Image img, AffineTransform xform, 479 ImageObserver obs) 480 { 481 AffineTransform at = getTransform(); 482 transform(xform); 483 boolean st = drawImage(img, 0, 0, obs); 484 setTransform(at); 485 return st; 486 } 487 488 /** 489 * Draws a BufferedImage on the EPS document. 490 */ 491 @Override drawImage(BufferedImage img, BufferedImageOp op, int x, int y)492 public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) 493 { 494 BufferedImage img1 = op.filter(img, null); 495 drawImage(img1, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); 496 } 497 498 /** 499 * Draws a RenderedImage on the EPS document. 500 */ 501 @Override drawRenderedImage(RenderedImage img, AffineTransform xform)502 public void drawRenderedImage(RenderedImage img, AffineTransform xform) 503 { 504 Hashtable properties = new Hashtable(); 505 String[] names = img.getPropertyNames(); 506 for (int i = 0; i < names.length; i++) 507 { 508 properties.put(names[i], img.getProperty(names[i])); 509 } 510 511 ColorModel cm = img.getColorModel(); 512 WritableRaster wr = img.copyData(null); 513 BufferedImage img1 = new BufferedImage(cm, wr, 514 cm.isAlphaPremultiplied(), properties); 515 AffineTransform at = AffineTransform.getTranslateInstance( 516 img.getMinX(), img.getMinY()); 517 at.preConcatenate(xform); 518 drawImage(img1, at, null); 519 } 520 521 /** 522 * Draws a RenderableImage by invoking its createDefaultRendering method. 523 */ 524 @Override drawRenderableImage(RenderableImage img, AffineTransform xform)525 public void drawRenderableImage(RenderableImage img, AffineTransform xform) 526 { 527 drawRenderedImage(img.createDefaultRendering(), xform); 528 } 529 530 /** 531 * Draws a string at (x,y) 532 */ 533 @Override drawString(String str, int x, int y)534 public void drawString(String str, int x, int y) 535 { 536 drawString(str, (float) x, (float) y); 537 } 538 539 /** 540 * Draws a string at (x,y) 541 */ 542 @Override drawString(String s, float x, float y)543 public void drawString(String s, float x, float y) 544 { 545 if (s != null && s.length() > 0) 546 { 547 AttributedString as = new AttributedString(s); 548 as.addAttribute(TextAttribute.FONT, getFont()); 549 drawString(as.getIterator(), x, y); 550 } 551 } 552 553 /** 554 * Draws the characters of an AttributedCharacterIterator, starting from 555 * (x,y). 556 */ 557 @Override drawString(AttributedCharacterIterator iterator, int x, int y)558 public void drawString(AttributedCharacterIterator iterator, int x, int y) 559 { 560 drawString(iterator, (float) x, (float) y); 561 } 562 563 /** 564 * Draws the characters of an AttributedCharacterIterator, starting from 565 * (x,y). 566 */ 567 @Override drawString(AttributedCharacterIterator iterator, float x, float y)568 public void drawString(AttributedCharacterIterator iterator, float x, 569 float y) 570 { 571 if (getAccurateTextMode()) 572 { 573 TextLayout layout = new TextLayout(iterator, getFontRenderContext()); 574 Shape shape = layout.getOutline(AffineTransform.getTranslateInstance( 575 x, y)); 576 draw(shape, "fill"); 577 } 578 else 579 { 580 append("newpath"); 581 Point2D location = transform(x, y); 582 append(location.getX() + " " + location.getY() + " moveto"); 583 StringBuffer buffer = new StringBuffer(); 584 for (char ch = iterator.first(); ch != CharacterIterator.DONE; ch = iterator 585 .next()) 586 { 587 if (ch == '(' || ch == ')') 588 { 589 buffer.append('\\'); 590 } 591 buffer.append(ch); 592 } 593 append("(" + buffer.toString() + ") show"); 594 } 595 } 596 597 /** 598 * Draws a GlyphVector at (x,y) 599 */ 600 @Override drawGlyphVector(GlyphVector g, float x, float y)601 public void drawGlyphVector(GlyphVector g, float x, float y) 602 { 603 Shape shape = g.getOutline(x, y); 604 draw(shape, "fill"); 605 } 606 607 /** 608 * Fills a Shape on the EPS document. 609 */ 610 @Override fill(Shape s)611 public void fill(Shape s) 612 { 613 draw(s, "fill"); 614 } 615 616 /** 617 * Checks whether or not the specified Shape intersects the specified 618 * Rectangle, which is in device space. 619 */ 620 @Override hit(Rectangle rect, Shape s, boolean onStroke)621 public boolean hit(Rectangle rect, Shape s, boolean onStroke) 622 { 623 return s.intersects(rect); 624 } 625 626 /** 627 * Returns the device configuration associated with this EpsGraphics2D object. 628 */ 629 @Override getDeviceConfiguration()630 public GraphicsConfiguration getDeviceConfiguration() 631 { 632 GraphicsConfiguration gc = null; 633 GraphicsEnvironment ge = GraphicsEnvironment 634 .getLocalGraphicsEnvironment(); 635 GraphicsDevice[] gds = ge.getScreenDevices(); 636 for (int i = 0; i < gds.length; i++) 637 { 638 GraphicsDevice gd = gds[i]; 639 GraphicsConfiguration[] gcs = gd.getConfigurations(); 640 if (gcs.length > 0) 641 { 642 return gcs[0]; 643 } 644 } 645 return gc; 646 } 647 648 /** 649 * Sets the Composite to be used by this EpsGraphics2D. EpsGraphics2D does not 650 * make use of these. 651 */ 652 @Override setComposite(Composite comp)653 public void setComposite(Composite comp) 654 { 655 _composite = comp; 656 } 657 658 /** 659 * Sets the Paint attribute for the EpsGraphics2D object. Only Paint objects 660 * of type Color are respected by EpsGraphics2D. 661 */ 662 @Override setPaint(Paint paint)663 public void setPaint(Paint paint) 664 { 665 _paint = paint; 666 if (paint instanceof Color) 667 { 668 setColor((Color) paint); 669 } 670 } 671 672 /** 673 * Sets the stroke. Only accepts BasicStroke objects (or subclasses of 674 * BasicStroke). 675 */ 676 @Override setStroke(Stroke s)677 public void setStroke(Stroke s) 678 { 679 if (s instanceof BasicStroke) 680 { 681 _stroke = (BasicStroke) s; 682 683 append(_stroke.getLineWidth() + " setlinewidth"); 684 float miterLimit = _stroke.getMiterLimit(); 685 if (miterLimit < 1.0f) 686 { 687 miterLimit = 1; 688 } 689 append(miterLimit + " setmiterlimit"); 690 append(_stroke.getLineJoin() + " setlinejoin"); 691 append(_stroke.getEndCap() + " setlinecap"); 692 693 StringBuffer dashes = new StringBuffer(); 694 dashes.append("[ "); 695 float[] dashArray = _stroke.getDashArray(); 696 if (dashArray != null) 697 { 698 for (int i = 0; i < dashArray.length; i++) 699 { 700 dashes.append((dashArray[i]) + " "); 701 } 702 } 703 dashes.append("]"); 704 append(dashes.toString() + " 0 setdash"); 705 } 706 } 707 708 /** 709 * Sets a rendering hint. These are not used by EpsGraphics2D. 710 */ 711 @Override setRenderingHint(RenderingHints.Key hintKey, Object hintValue)712 public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) 713 { 714 // Do nothing. 715 } 716 717 /** 718 * Returns the value of a single preference for the rendering algorithms. 719 * Rendering hints are not used by EpsGraphics2D. 720 */ 721 @Override getRenderingHint(RenderingHints.Key hintKey)722 public Object getRenderingHint(RenderingHints.Key hintKey) 723 { 724 return null; 725 } 726 727 /** 728 * Sets the rendering hints. These are ignored by EpsGraphics2D. 729 */ 730 @Override setRenderingHints(Map hints)731 public void setRenderingHints(Map hints) 732 { 733 // Do nothing. 734 } 735 736 /** 737 * Adds rendering hints. These are ignored by EpsGraphics2D. 738 */ 739 @Override addRenderingHints(Map hints)740 public void addRenderingHints(Map hints) 741 { 742 // Do nothing. 743 } 744 745 /** 746 * Returns the preferences for the rendering algorithms. 747 */ 748 @Override getRenderingHints()749 public RenderingHints getRenderingHints() 750 { 751 return new RenderingHints(null); 752 } 753 754 /** 755 * Translates the origin of the EpsGraphics2D context to the point (x,y) in 756 * the current coordinate system. 757 */ 758 @Override translate(int x, int y)759 public void translate(int x, int y) 760 { 761 translate((double) x, (double) y); 762 } 763 764 /** 765 * Concatenates the current EpsGraphics2D Transformation with a translation 766 * transform. 767 */ 768 @Override translate(double tx, double ty)769 public void translate(double tx, double ty) 770 { 771 transform(AffineTransform.getTranslateInstance(tx, ty)); 772 } 773 774 /** 775 * Concatenates the current EpsGraphics2D Transform with a rotation transform. 776 */ 777 @Override rotate(double theta)778 public void rotate(double theta) 779 { 780 rotate(theta, 0, 0); 781 } 782 783 /** 784 * Concatenates the current EpsGraphics2D Transform with a translated rotation 785 * transform. 786 */ 787 @Override rotate(double theta, double x, double y)788 public void rotate(double theta, double x, double y) 789 { 790 transform(AffineTransform.getRotateInstance(theta, x, y)); 791 } 792 793 /** 794 * Concatenates the current EpsGraphics2D Transform with a scaling 795 * transformation. 796 */ 797 @Override scale(double sx, double sy)798 public void scale(double sx, double sy) 799 { 800 transform(AffineTransform.getScaleInstance(sx, sy)); 801 } 802 803 /** 804 * Concatenates the current EpsGraphics2D Transform with a shearing transform. 805 */ 806 @Override shear(double shx, double shy)807 public void shear(double shx, double shy) 808 { 809 transform(AffineTransform.getShearInstance(shx, shy)); 810 } 811 812 /** 813 * Composes an AffineTransform object with the Transform in this EpsGraphics2D 814 * according to the rule last-specified-first-applied. 815 */ 816 @Override transform(AffineTransform Tx)817 public void transform(AffineTransform Tx) 818 { 819 _transform.concatenate(Tx); 820 setTransform(getTransform()); 821 } 822 823 /** 824 * Sets the AffineTransform to be used by this EpsGraphics2D. 825 */ 826 @Override setTransform(AffineTransform Tx)827 public void setTransform(AffineTransform Tx) 828 { 829 if (Tx == null) 830 { 831 _transform = new AffineTransform(); 832 } 833 else 834 { 835 _transform = new AffineTransform(Tx); 836 } 837 // Need to update the stroke and font so they know the scale changed 838 setStroke(getStroke()); 839 setFont(getFont()); 840 } 841 842 /** 843 * Gets the AffineTransform used by this EpsGraphics2D. 844 */ 845 @Override getTransform()846 public AffineTransform getTransform() 847 { 848 return new AffineTransform(_transform); 849 } 850 851 /** 852 * Returns the current Paint of the EpsGraphics2D object. 853 */ 854 @Override getPaint()855 public Paint getPaint() 856 { 857 return _paint; 858 } 859 860 /** 861 * returns the current Composite of the EpsGraphics2D object. 862 */ 863 @Override getComposite()864 public Composite getComposite() 865 { 866 return _composite; 867 } 868 869 /** 870 * Sets the background color to be used by the clearRect method. 871 */ 872 @Override setBackground(Color color)873 public void setBackground(Color color) 874 { 875 if (color == null) 876 { 877 color = Color.black; 878 } 879 _backgroundColor = color; 880 } 881 882 /** 883 * Gets the background color that is used by the clearRect method. 884 */ 885 @Override getBackground()886 public Color getBackground() 887 { 888 return _backgroundColor; 889 } 890 891 /** 892 * Returns the Stroke currently used. Guaranteed to be an instance of 893 * BasicStroke. 894 */ 895 @Override getStroke()896 public Stroke getStroke() 897 { 898 return _stroke; 899 } 900 901 /** 902 * Intersects the current clip with the interior of the specified Shape and 903 * sets the clip to the resulting intersection. 904 */ 905 @Override clip(Shape s)906 public void clip(Shape s) 907 { 908 if (_clip == null) 909 { 910 setClip(s); 911 } 912 else 913 { 914 Area area = new Area(_clip); 915 area.intersect(new Area(s)); 916 setClip(area); 917 } 918 } 919 920 /** 921 * Returns the FontRenderContext. 922 */ 923 @Override getFontRenderContext()924 public FontRenderContext getFontRenderContext() 925 { 926 return _fontRenderContext; 927 } 928 929 // ///////////// Graphics methods /////////////////////// 930 931 /** 932 * Returns a new Graphics object that is identical to this EpsGraphics2D. 933 */ 934 @Override create()935 public Graphics create() 936 { 937 return new EpsGraphics2D(this); 938 } 939 940 /** 941 * Returns an EpsGraphics2D object based on this Graphics object, but with a 942 * new translation and clip area. 943 */ 944 @Override create(int x, int y, int width, int height)945 public Graphics create(int x, int y, int width, int height) 946 { 947 Graphics g = create(); 948 g.translate(x, y); 949 g.clipRect(0, 0, width, height); 950 return g; 951 } 952 953 /** 954 * Returns the current Color. This will be a default value (black) until it is 955 * changed using the setColor method. 956 */ 957 @Override getColor()958 public Color getColor() 959 { 960 return _color; 961 } 962 963 /** 964 * Sets the Color to be used when drawing all future shapes, text, etc. 965 */ 966 @Override setColor(Color c)967 public void setColor(Color c) 968 { 969 if (c == null) 970 { 971 c = Color.black; 972 } 973 _color = c; 974 append((c.getRed() / 255f) + " " + (c.getGreen() / 255f) + " " 975 + (c.getBlue() / 255f) + " setrgbcolor"); 976 } 977 978 /** 979 * Sets the paint mode of this EpsGraphics2D object to overwrite the 980 * destination EpsDocument with the current color. 981 */ 982 @Override setPaintMode()983 public void setPaintMode() 984 { 985 // Do nothing - paint mode is the only method supported anyway. 986 } 987 988 /** 989 * <b><i><font color="red">Not implemented</font></i></b> - performs no 990 * action. 991 */ 992 @Override setXORMode(Color c1)993 public void setXORMode(Color c1) 994 { 995 methodNotSupported(); 996 } 997 998 /** 999 * Returns the Font currently being used. 1000 */ 1001 @Override getFont()1002 public Font getFont() 1003 { 1004 return _font; 1005 } 1006 1007 /** 1008 * Sets the Font to be used in future text. 1009 */ 1010 @Override setFont(Font font)1011 public void setFont(Font font) 1012 { 1013 if (font == null) 1014 { 1015 font = Font.decode(null); 1016 } 1017 _font = font; 1018 append("/" + _font.getPSName() + " findfont " + (_font.getSize()) 1019 + " scalefont setfont"); 1020 } 1021 1022 /** 1023 * Gets the font metrics of the current font. 1024 */ 1025 @Override getFontMetrics()1026 public FontMetrics getFontMetrics() 1027 { 1028 return getFontMetrics(getFont()); 1029 } 1030 1031 /** 1032 * Gets the font metrics for the specified font. 1033 */ 1034 @Override getFontMetrics(Font f)1035 public FontMetrics getFontMetrics(Font f) 1036 { 1037 BufferedImage image = new BufferedImage(1, 1, 1038 BufferedImage.TYPE_INT_RGB); 1039 Graphics g = image.getGraphics(); 1040 return g.getFontMetrics(f); 1041 } 1042 1043 /** 1044 * Returns the bounding rectangle of the current clipping area. 1045 */ 1046 @Override getClipBounds()1047 public Rectangle getClipBounds() 1048 { 1049 if (_clip == null) 1050 { 1051 return null; 1052 } 1053 Rectangle rect = getClip().getBounds(); 1054 return rect; 1055 } 1056 1057 /** 1058 * Intersects the current clip with the specified rectangle. 1059 */ 1060 @Override clipRect(int x, int y, int width, int height)1061 public void clipRect(int x, int y, int width, int height) 1062 { 1063 clip(new Rectangle(x, y, width, height)); 1064 } 1065 1066 /** 1067 * Sets the current clip to the rectangle specified by the given coordinates. 1068 */ 1069 @Override setClip(int x, int y, int width, int height)1070 public void setClip(int x, int y, int width, int height) 1071 { 1072 setClip(new Rectangle(x, y, width, height)); 1073 } 1074 1075 /** 1076 * Gets the current clipping area. 1077 */ 1078 @Override getClip()1079 public Shape getClip() 1080 { 1081 if (_clip == null) 1082 { 1083 return null; 1084 } 1085 else 1086 { 1087 try 1088 { 1089 AffineTransform t = _transform.createInverse(); 1090 t.concatenate(_clipTransform); 1091 return t.createTransformedShape(_clip); 1092 } catch (Exception e) 1093 { 1094 throw new EpsException(MessageManager.formatMessage( 1095 "exception.eps_unable_to_get_inverse_matrix", 1096 new String[] { _transform.toString() })); 1097 } 1098 } 1099 } 1100 1101 /** 1102 * Sets the current clipping area to an arbitrary clip shape. 1103 */ 1104 @Override setClip(Shape clip)1105 public void setClip(Shape clip) 1106 { 1107 if (clip != null) 1108 { 1109 if (_document.isClipSet()) 1110 { 1111 append("grestore"); 1112 append("gsave"); 1113 } 1114 else 1115 { 1116 _document.setClipSet(true); 1117 append("gsave"); 1118 } 1119 draw(clip, "clip"); 1120 _clip = clip; 1121 _clipTransform = (AffineTransform) _transform.clone(); 1122 } 1123 else 1124 { 1125 if (_document.isClipSet()) 1126 { 1127 append("grestore"); 1128 _document.setClipSet(false); 1129 } 1130 _clip = null; 1131 } 1132 } 1133 1134 /** 1135 * <b><i><font color="red">Not implemented</font></i></b> - performs no 1136 * action. 1137 */ 1138 @Override copyArea(int x, int y, int width, int height, int dx, int dy)1139 public void copyArea(int x, int y, int width, int height, int dx, int dy) 1140 { 1141 methodNotSupported(); 1142 } 1143 1144 /** 1145 * Draws a straight line from (x1,y1) to (x2,y2). 1146 */ 1147 @Override drawLine(int x1, int y1, int x2, int y2)1148 public void drawLine(int x1, int y1, int x2, int y2) 1149 { 1150 Shape shape = new Line2D.Float(x1, y1, x2, y2); 1151 draw(shape); 1152 } 1153 1154 /** 1155 * Fills a rectangle with top-left corner placed at (x,y). 1156 */ 1157 @Override fillRect(int x, int y, int width, int height)1158 public void fillRect(int x, int y, int width, int height) 1159 { 1160 Shape shape = new Rectangle(x, y, width, height); 1161 draw(shape, "fill"); 1162 } 1163 1164 /** 1165 * Draws a rectangle with top-left corner placed at (x,y). 1166 */ 1167 @Override drawRect(int x, int y, int width, int height)1168 public void drawRect(int x, int y, int width, int height) 1169 { 1170 Shape shape = new Rectangle(x, y, width, height); 1171 draw(shape); 1172 } 1173 1174 /** 1175 * Clears a rectangle with top-left corner placed at (x,y) using the current 1176 * background color. 1177 */ 1178 @Override clearRect(int x, int y, int width, int height)1179 public void clearRect(int x, int y, int width, int height) 1180 { 1181 Color originalColor = getColor(); 1182 1183 setColor(getBackground()); 1184 Shape shape = new Rectangle(x, y, width, height); 1185 draw(shape, "fill"); 1186 1187 setColor(originalColor); 1188 } 1189 1190 /** 1191 * Draws a rounded rectangle. 1192 */ 1193 @Override drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)1194 public void drawRoundRect(int x, int y, int width, int height, 1195 int arcWidth, int arcHeight) 1196 { 1197 Shape shape = new RoundRectangle2D.Float(x, y, width, height, arcWidth, 1198 arcHeight); 1199 draw(shape); 1200 } 1201 1202 /** 1203 * Fills a rounded rectangle. 1204 */ 1205 @Override fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)1206 public void fillRoundRect(int x, int y, int width, int height, 1207 int arcWidth, int arcHeight) 1208 { 1209 Shape shape = new RoundRectangle2D.Float(x, y, width, height, arcWidth, 1210 arcHeight); 1211 draw(shape, "fill"); 1212 } 1213 1214 /** 1215 * Draws an oval. 1216 */ 1217 @Override drawOval(int x, int y, int width, int height)1218 public void drawOval(int x, int y, int width, int height) 1219 { 1220 Shape shape = new Ellipse2D.Float(x, y, width, height); 1221 draw(shape); 1222 } 1223 1224 /** 1225 * Fills an oval. 1226 */ 1227 @Override fillOval(int x, int y, int width, int height)1228 public void fillOval(int x, int y, int width, int height) 1229 { 1230 Shape shape = new Ellipse2D.Float(x, y, width, height); 1231 draw(shape, "fill"); 1232 } 1233 1234 /** 1235 * Draws an arc. 1236 */ 1237 @Override drawArc(int x, int y, int width, int height, int startAngle, int arcAngle)1238 public void drawArc(int x, int y, int width, int height, int startAngle, 1239 int arcAngle) 1240 { 1241 Shape shape = new Arc2D.Float(x, y, width, height, startAngle, 1242 arcAngle, Arc2D.OPEN); 1243 draw(shape); 1244 } 1245 1246 /** 1247 * Fills an arc. 1248 */ 1249 @Override fillArc(int x, int y, int width, int height, int startAngle, int arcAngle)1250 public void fillArc(int x, int y, int width, int height, int startAngle, 1251 int arcAngle) 1252 { 1253 Shape shape = new Arc2D.Float(x, y, width, height, startAngle, 1254 arcAngle, Arc2D.PIE); 1255 draw(shape, "fill"); 1256 } 1257 1258 /** 1259 * Draws a polyline. 1260 */ 1261 @Override drawPolyline(int[] xPoints, int[] yPoints, int nPoints)1262 public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) 1263 { 1264 if (nPoints > 0) 1265 { 1266 GeneralPath path = new GeneralPath(); 1267 path.moveTo(xPoints[0], yPoints[0]); 1268 for (int i = 1; i < nPoints; i++) 1269 { 1270 path.lineTo(xPoints[i], yPoints[i]); 1271 } 1272 draw(path); 1273 } 1274 } 1275 1276 /** 1277 * Draws a polygon made with the specified points. 1278 */ 1279 @Override drawPolygon(int[] xPoints, int[] yPoints, int nPoints)1280 public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) 1281 { 1282 Shape shape = new Polygon(xPoints, yPoints, nPoints); 1283 draw(shape); 1284 } 1285 1286 /** 1287 * Draws a polygon. 1288 */ 1289 @Override drawPolygon(Polygon p)1290 public void drawPolygon(Polygon p) 1291 { 1292 draw(p); 1293 } 1294 1295 /** 1296 * Fills a polygon made with the specified points. 1297 */ 1298 @Override fillPolygon(int[] xPoints, int[] yPoints, int nPoints)1299 public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) 1300 { 1301 Shape shape = new Polygon(xPoints, yPoints, nPoints); 1302 draw(shape, "fill"); 1303 } 1304 1305 /** 1306 * Fills a polygon. 1307 */ 1308 @Override fillPolygon(Polygon p)1309 public void fillPolygon(Polygon p) 1310 { 1311 draw(p, "fill"); 1312 } 1313 1314 /** 1315 * Draws the specified characters, starting from (x,y) 1316 */ 1317 @Override drawChars(char[] data, int offset, int length, int x, int y)1318 public void drawChars(char[] data, int offset, int length, int x, int y) 1319 { 1320 String string = new String(data, offset, length); 1321 drawString(string, x, y); 1322 } 1323 1324 /** 1325 * Draws the specified bytes, starting from (x,y) 1326 */ 1327 @Override drawBytes(byte[] data, int offset, int length, int x, int y)1328 public void drawBytes(byte[] data, int offset, int length, int x, int y) 1329 { 1330 String string = new String(data, offset, length); 1331 drawString(string, x, y); 1332 } 1333 1334 /** 1335 * Draws an image. 1336 */ 1337 @Override drawImage(Image img, int x, int y, ImageObserver observer)1338 public boolean drawImage(Image img, int x, int y, ImageObserver observer) 1339 { 1340 return drawImage(img, x, y, Color.white, observer); 1341 } 1342 1343 /** 1344 * Draws an image. 1345 */ 1346 @Override drawImage(Image img, int x, int y, int width, int height, ImageObserver observer)1347 public boolean drawImage(Image img, int x, int y, int width, int height, 1348 ImageObserver observer) 1349 { 1350 return drawImage(img, x, y, width, height, Color.white, observer); 1351 } 1352 1353 /** 1354 * Draws an image. 1355 */ 1356 @Override drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer)1357 public boolean drawImage(Image img, int x, int y, Color bgcolor, 1358 ImageObserver observer) 1359 { 1360 int width = img.getWidth(null); 1361 int height = img.getHeight(null); 1362 return drawImage(img, x, y, width, height, bgcolor, observer); 1363 } 1364 1365 /** 1366 * Draws an image. 1367 */ 1368 @Override drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer)1369 public boolean drawImage(Image img, int x, int y, int width, int height, 1370 Color bgcolor, ImageObserver observer) 1371 { 1372 return drawImage(img, x, y, x + width, y + height, 0, 0, width, height, 1373 bgcolor, observer); 1374 } 1375 1376 /** 1377 * Draws an image. 1378 */ 1379 @Override drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer)1380 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 1381 int sx1, int sy1, int sx2, int sy2, ImageObserver observer) 1382 { 1383 return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, 1384 Color.white, observer); 1385 } 1386 1387 /** 1388 * Draws an image. 1389 */ 1390 @Override drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer)1391 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 1392 int sx1, int sy1, int sx2, int sy2, Color bgcolor, 1393 ImageObserver observer) 1394 { 1395 if (dx1 >= dx2) 1396 { 1397 throw new IllegalArgumentException("dx1 >= dx2"); 1398 } 1399 if (sx1 >= sx2) 1400 { 1401 throw new IllegalArgumentException("sx1 >= sx2"); 1402 } 1403 if (dy1 >= dy2) 1404 { 1405 throw new IllegalArgumentException("dy1 >= dy2"); 1406 } 1407 if (sy1 >= sy2) 1408 { 1409 throw new IllegalArgumentException("sy1 >= sy2"); 1410 } 1411 1412 append("gsave"); 1413 1414 int width = sx2 - sx1; 1415 int height = sy2 - sy1; 1416 int destWidth = dx2 - dx1; 1417 int destHeight = dy2 - dy1; 1418 1419 int[] pixels = new int[width * height]; 1420 PixelGrabber pg = new PixelGrabber(img, sx1, sy1, sx2 - sx1, sy2 - sy1, 1421 pixels, 0, width); 1422 try 1423 { 1424 pg.grabPixels(); 1425 } catch (InterruptedException e) 1426 { 1427 return false; 1428 } 1429 1430 AffineTransform matrix = new AffineTransform(_transform); 1431 matrix.translate(dx1, dy1); 1432 matrix.scale(destWidth / (double) width, destHeight / (double) height); 1433 double[] m = new double[6]; 1434 try 1435 { 1436 matrix = matrix.createInverse(); 1437 } catch (Exception e) 1438 { 1439 throw new EpsException(MessageManager.formatMessage( 1440 "exception.eps_unable_to_get_inverse_matrix", 1441 new String[] { matrix.toString() })); 1442 } 1443 matrix.scale(1, -1); 1444 matrix.getMatrix(m); 1445 append(width + " " + height + " 8 [" + m[0] + " " + m[1] + " " + m[2] 1446 + " " + m[3] + " " + m[4] + " " + m[5] + "]"); 1447 // Fill the background to update the bounding box. 1448 Color oldColor = getColor(); 1449 setColor(getBackground()); 1450 fillRect(dx1, dy1, destWidth, destHeight); 1451 setColor(oldColor); 1452 append("{currentfile 3 " + width 1453 + " mul string readhexstring pop} bind"); 1454 append("false 3 colorimage"); 1455 StringBuffer line = new StringBuffer(); 1456 for (int y = 0; y < height; y++) 1457 { 1458 for (int x = 0; x < width; x++) 1459 { 1460 Color color = new Color(pixels[x + width * y]); 1461 line.append(toHexString(color.getRed()) 1462 + toHexString(color.getGreen()) 1463 + toHexString(color.getBlue())); 1464 if (line.length() > 64) 1465 { 1466 append(line.toString()); 1467 line = new StringBuffer(); 1468 } 1469 } 1470 } 1471 if (line.length() > 0) 1472 { 1473 append(line.toString()); 1474 } 1475 1476 append("grestore"); 1477 1478 return true; 1479 } 1480 1481 /** 1482 * Disposes of all resources used by this EpsGraphics2D object. If this is the 1483 * only remaining EpsGraphics2D instance pointing at a EpsDocument object, 1484 * then the EpsDocument object shall become eligible for garbage collection. 1485 */ 1486 @Override dispose()1487 public void dispose() 1488 { 1489 _document = null; 1490 } 1491 1492 /* bsoares 2019-03-20 1493 * finalize is now deprecated. Implementing AutoCloseable instead 1494 /** 1495 * Finalizes the object. 1496 @Override 1497 public void finalize() 1498 { 1499 super.finalize(); 1500 } 1501 */ 1502 1503 /** 1504 * Returns the entire contents of the EPS document, complete with headers and 1505 * bounding box. The returned String is suitable for being written directly to 1506 * disk as an EPS file. 1507 */ 1508 @Override toString()1509 public String toString() 1510 { 1511 StringWriter writer = new StringWriter(); 1512 try 1513 { 1514 _document.write(writer); 1515 _document.flush(); 1516 _document.close(); 1517 } catch (IOException e) 1518 { 1519 throw new EpsException(e.toString()); 1520 } 1521 return writer.toString(); 1522 } 1523 1524 /** 1525 * Returns true if the specified rectangular area might intersect the current 1526 * clipping area. 1527 */ 1528 @Override hitClip(int x, int y, int width, int height)1529 public boolean hitClip(int x, int y, int width, int height) 1530 { 1531 if (_clip == null) 1532 { 1533 return true; 1534 } 1535 Rectangle rect = new Rectangle(x, y, width, height); 1536 return hit(rect, _clip, true); 1537 } 1538 1539 /** 1540 * Returns the bounding rectangle of the current clipping area. 1541 */ 1542 @Override getClipBounds(Rectangle r)1543 public Rectangle getClipBounds(Rectangle r) 1544 { 1545 if (_clip == null) 1546 { 1547 return r; 1548 } 1549 Rectangle rect = getClipBounds(); 1550 r.setLocation((int) rect.getX(), (int) rect.getY()); 1551 r.setSize((int) rect.getWidth(), (int) rect.getHeight()); 1552 return r; 1553 } 1554 1555 private Color _color; 1556 1557 private Color _backgroundColor; 1558 1559 private Paint _paint; 1560 1561 private Composite _composite; 1562 1563 private BasicStroke _stroke; 1564 1565 private Font _font; 1566 1567 private Shape _clip; 1568 1569 private AffineTransform _clipTransform; 1570 1571 private AffineTransform _transform; 1572 1573 private boolean _accurateTextMode; 1574 1575 private EpsDocument _document; 1576 1577 private static FontRenderContext _fontRenderContext = new FontRenderContext( 1578 null, false, true); 1579 } 1580