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