1 /* BufferedImageGraphics.java
2    Copyright (C) 2006 Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package gnu.java.awt.peer.gtk;
40 
41 import java.awt.AlphaComposite;
42 import java.awt.Color;
43 import java.awt.Composite;
44 import java.awt.Graphics;
45 import java.awt.Graphics2D;
46 import java.awt.GraphicsConfiguration;
47 import java.awt.Image;
48 import java.awt.Rectangle;
49 import java.awt.Shape;
50 import java.awt.Toolkit;
51 import java.awt.font.GlyphVector;
52 import java.awt.geom.AffineTransform;
53 import java.awt.geom.Rectangle2D;
54 import java.awt.image.BufferedImage;
55 import java.awt.image.ColorModel;
56 import java.awt.image.DataBufferInt;
57 import java.awt.image.ImageObserver;
58 import java.awt.image.ImageProducer;
59 import java.awt.image.Raster;
60 import java.awt.image.RenderedImage;
61 import java.awt.image.SinglePixelPackedSampleModel;
62 import java.util.WeakHashMap;
63 
64 /**
65  * Implementation of Graphics2D on a Cairo surface.
66  *
67  * Simutanously maintains a CairoSurface and updates the
68  * BufferedImage from that after each drawing operation.
69  */
70 public class BufferedImageGraphics extends CairoGraphics2D
71 {
72   /**
73    * the buffered Image.
74    */
75   private BufferedImage image, buffer;
76 
77   /**
78    * Image size.
79    */
80   private int imageWidth, imageHeight;
81 
82   /**
83    * The cairo surface that we actually draw on.
84    */
85   CairoSurface surface;
86 
87   /**
88    * Cache BufferedImageGraphics surfaces.
89    */
90   static WeakHashMap<BufferedImage, CairoSurface> bufferedImages
91     = new WeakHashMap<BufferedImage, CairoSurface>();
92 
93   /**
94    * Its corresponding cairo_t.
95    */
96   private long cairo_t;
97 
98   private boolean hasFastCM;
99   private boolean hasAlpha;
100 
101 
BufferedImageGraphics(BufferedImage bi)102   public BufferedImageGraphics(BufferedImage bi)
103   {
104     this.image = bi;
105     imageWidth = bi.getWidth();
106     imageHeight = bi.getHeight();
107 
108     if (!(image.getSampleModel() instanceof SinglePixelPackedSampleModel))
109       hasFastCM = false;
110     else if(bi.getColorModel().equals(CairoSurface.cairoCM_opaque))
111       {
112         hasFastCM = true;
113         hasAlpha = false;
114       }
115     else if(bi.getColorModel().equals(CairoSurface.cairoColorModel)
116         || bi.getColorModel().equals(CairoSurface.cairoCM_pre))
117       {
118         hasFastCM = true;
119         hasAlpha = true;
120       }
121     else
122       hasFastCM = false;
123 
124     // Cache surfaces.
125     if( bufferedImages.get( bi ) != null )
126       surface = bufferedImages.get( bi );
127     else
128       {
129         surface = new CairoSurface( imageWidth, imageHeight );
130         bufferedImages.put(bi, surface);
131       }
132 
133     cairo_t = surface.newCairoContext();
134 
135     // Get pixels out of buffered image and set in cairo surface
136     Raster raster = bi.getRaster();
137     int[] pixels;
138 
139     if (hasFastCM)
140       {
141         SinglePixelPackedSampleModel sm = (SinglePixelPackedSampleModel)image.getSampleModel();
142         int minX = image.getRaster().getSampleModelTranslateX();
143         int minY = image.getRaster().getSampleModelTranslateY();
144 
145         // Pull pixels directly out of data buffer
146         pixels = ((DataBufferInt)raster.getDataBuffer()).getData();
147 
148         // Discard pixels that fall outside of the image's bounds
149         // (ie, this image is actually a subimage of a different image)
150         if (!(sm.getScanlineStride() == imageWidth && minX == 0 && minY == 0))
151           {
152             int[] pixels2 = new int[imageWidth * imageHeight];
153             int scanline = sm.getScanlineStride();
154 
155             for (int i = 0; i < imageHeight; i++)
156               System.arraycopy(pixels, (i - minY) * scanline - minX, pixels2,
157                                i * imageWidth, imageWidth);
158 
159             pixels = pixels2;
160           }
161 
162         // Fill the alpha channel as opaque if image does not have alpha
163         if( !hasAlpha )
164           for(int i = 0; i < pixels.length; i++)
165             pixels[i] &= 0xFFFFFFFF;
166       }
167     else
168       {
169         pixels = CairoGraphics2D.findSimpleIntegerArray(image.getColorModel(),
170                                                         image.getData());
171         if (pixels != null)
172           System.arraycopy(pixels, 0, surface.getData(),
173                            0, pixels.length);
174       }
175 
176     setup( cairo_t );
177     setClip(0, 0, imageWidth, imageHeight);
178   }
179 
BufferedImageGraphics(BufferedImageGraphics copyFrom)180   BufferedImageGraphics(BufferedImageGraphics copyFrom)
181   {
182     image = copyFrom.image;
183     surface = copyFrom.surface;
184     cairo_t = surface.newCairoContext();
185     imageWidth = copyFrom.imageWidth;
186     imageHeight = copyFrom.imageHeight;
187 
188     hasFastCM = copyFrom.hasFastCM;
189     hasAlpha = copyFrom.hasAlpha;
190 
191     copy( copyFrom, cairo_t );
192   }
193 
194   /**
195    * Update a rectangle of the bufferedImage. This can be improved upon a lot.
196    */
updateBufferedImage(int x, int y, int width, int height)197   private void updateBufferedImage(int x, int y, int width, int height)
198   {
199     Rectangle bounds = new Rectangle(x, y, width, height);
200     bounds = getTransformedBounds(bounds, transform).getBounds();
201     x = bounds.x;
202     y = bounds.y;
203     width = bounds.width;
204     height = bounds.height;
205 
206     int[] pixels = surface.getData();
207 
208     if( x > imageWidth || y > imageHeight )
209       return;
210 
211     // Deal with negative width/height.
212     if (height < 0)
213       {
214         y += height;
215         height = -height;
216       }
217     if (width < 0)
218       {
219         x += width;
220         width = -width;
221       }
222 
223     // Clip edges.
224     if( x < 0 )
225       x = 0;
226     if( y < 0 )
227       y = 0;
228 
229     if( x + width > imageWidth )
230       width = imageWidth - x;
231     if( y + height > imageHeight )
232       height = imageHeight - y;
233 
234     if(!hasFastCM)
235       {
236         image.setRGB(x, y, width, height, pixels,
237                      x + y * imageWidth, imageWidth);
238         // The setRGB method assumes (or should assume) that pixels are NOT
239         // alpha-premultiplied, but Cairo stores data with premultiplication
240         // (thus the pixels returned in getPixels are premultiplied).
241         // This is ignored for consistency, however, since in
242         // CairoGrahpics2D.drawImage we also use non-premultiplied data
243 
244       }
245     else
246       {
247         int[] db = ((DataBufferInt)image.getRaster().getDataBuffer()).
248                   getData();
249 
250         // This should not fail, as we check the image sample model when we
251         // set the hasFastCM flag
252         SinglePixelPackedSampleModel sm = (SinglePixelPackedSampleModel)image.getSampleModel() ;
253 
254         int minX = image.getRaster().getSampleModelTranslateX();
255         int minY = image.getRaster().getSampleModelTranslateY();
256 
257         if (sm.getScanlineStride() == imageWidth && minX == 0)
258           {
259             System.arraycopy(pixels, y * imageWidth,
260                              db, (y - minY) * imageWidth,
261                              height * imageWidth);
262           }
263         else
264           {
265             int scanline = sm.getScanlineStride();
266             for (int i = y; i < (height + y); i++)
267               System.arraycopy(pixels, i * imageWidth + x, db,
268                                (i - minY) * scanline + x - minX, width);
269 
270           }
271       }
272   }
273 
274   /**
275    * Abstract methods.
276    */
create()277   public Graphics create()
278   {
279     return new BufferedImageGraphics(this);
280   }
281 
getDeviceConfiguration()282   public GraphicsConfiguration getDeviceConfiguration()
283   {
284     return null;
285   }
286 
getRealBounds()287   protected Rectangle2D getRealBounds()
288   {
289     return new Rectangle2D.Double(0.0, 0.0, imageWidth, imageHeight);
290   }
291 
copyAreaImpl(int x, int y, int width, int height, int dx, int dy)292   public void copyAreaImpl(int x, int y, int width, int height, int dx, int dy)
293   {
294     surface.copyAreaNative(x, y, width, height, dx, dy, surface.width);
295     updateBufferedImage(x + dx, y + dy, width, height);
296   }
297 
298   /**
299    * Overloaded methods that do actual drawing need to enter the gdk threads
300    * and also do certain things before and after.
301    */
draw(Shape s)302   public void draw(Shape s)
303   {
304     // Find total bounds of shape
305     Rectangle r = findStrokedBounds(s);
306     if (shiftDrawCalls)
307       {
308         r.width++;
309         r.height++;
310       }
311 
312     // Do the drawing
313     if (comp == null || comp instanceof AlphaComposite)
314       {
315         super.draw(s);
316         updateBufferedImage(r.x, r.y, r.width, r.height);
317       }
318     else
319       {
320         createBuffer();
321 
322         Graphics2D g2d = (Graphics2D)buffer.getGraphics();
323         g2d.setStroke(this.getStroke());
324         g2d.setColor(this.getColor());
325         g2d.setTransform(transform);
326         g2d.draw(s);
327 
328         drawComposite(r.getBounds2D(), null);
329       }
330   }
331 
fill(Shape s)332   public void fill(Shape s)
333   {
334     if (comp == null || comp instanceof AlphaComposite)
335       {
336         super.fill(s);
337         Rectangle r = s.getBounds();
338         updateBufferedImage(r.x, r.y, r.width, r.height);
339       }
340     else
341       {
342         createBuffer();
343 
344         Graphics2D g2d = (Graphics2D)buffer.getGraphics();
345         g2d.setPaint(this.getPaint());
346         g2d.setColor(this.getColor());
347         g2d.setTransform(transform);
348         g2d.fill(s);
349 
350         drawComposite(s.getBounds2D(), null);
351       }
352   }
353 
drawRenderedImage(RenderedImage image, AffineTransform xform)354   public void drawRenderedImage(RenderedImage image, AffineTransform xform)
355   {
356     if (comp == null || comp instanceof AlphaComposite)
357       {
358         super.drawRenderedImage(image, xform);
359         updateBufferedImage(0, 0, imageWidth, imageHeight);
360       }
361     else
362       {
363         createBuffer();
364 
365         Graphics2D g2d = (Graphics2D)buffer.getGraphics();
366         g2d.setRenderingHints(this.getRenderingHints());
367         g2d.setTransform(transform);
368         g2d.drawRenderedImage(image, xform);
369 
370         drawComposite(buffer.getRaster().getBounds(), null);
371       }
372 
373   }
374 
drawImage(Image img, AffineTransform xform, Color bgcolor, ImageObserver obs)375   protected boolean drawImage(Image img, AffineTransform xform,
376                               Color bgcolor, ImageObserver obs)
377   {
378     if (comp == null || comp instanceof AlphaComposite)
379       {
380         boolean rv = super.drawImage(img, xform, bgcolor, obs);
381         updateBufferedImage(0, 0, imageWidth, imageHeight);
382         return rv;
383       }
384     else
385       {
386         // Get buffered image of source
387         if( !(img instanceof BufferedImage) )
388           {
389             ImageProducer source = img.getSource();
390             if (source == null)
391               return false;
392             img = Toolkit.getDefaultToolkit().createImage(source);
393           }
394         BufferedImage bImg = (BufferedImage) img;
395 
396         // Find translated bounds
397         Rectangle2D bounds = new Rectangle(bImg.getMinX(), bImg.getMinY(),
398                                            bImg.getWidth(), bImg.getHeight());
399         if (xform != null)
400           bounds = getTransformedBounds(bounds, xform);
401 
402         // Create buffer and draw image
403         createBuffer();
404 
405         Graphics2D g2d = (Graphics2D)buffer.getGraphics();
406         g2d.setRenderingHints(this.getRenderingHints());
407         g2d.drawImage(img, xform, obs);
408 
409         // Perform compositing
410         return drawComposite(bounds, obs);
411       }
412   }
413 
drawGlyphVector(GlyphVector gv, float x, float y)414   public void drawGlyphVector(GlyphVector gv, float x, float y)
415   {
416     // Find absolute bounds, in user-space, of this glyph vector
417     Rectangle2D bounds = gv.getLogicalBounds();
418     bounds = new Rectangle2D.Double(x + bounds.getX(), y + bounds.getY(),
419                                     bounds.getWidth(), bounds.getHeight());
420 
421     // Perform draw operation
422     if (comp == null || comp instanceof AlphaComposite)
423       {
424         super.drawGlyphVector(gv, x, y);
425 
426         // this returns an integer-based Rectangle (rather than a
427         // Rectangle2D), which takes care of any necessary rounding for us.
428         bounds = bounds.getBounds();
429 
430         updateBufferedImage((int)bounds.getX(), (int)bounds.getY(),
431                             (int)bounds.getWidth(), (int)bounds.getHeight());
432       }
433     else
434       {
435         createBuffer();
436 
437         Graphics2D g2d = (Graphics2D)buffer.getGraphics();
438         g2d.setPaint(this.getPaint());
439         g2d.setStroke(this.getStroke());
440         g2d.setTransform(transform);
441         g2d.drawGlyphVector(gv, x, y);
442 
443         drawComposite(bounds, null);
444       }
445   }
446 
447   /**
448    * Perform composite drawing from the buffer onto the main image.
449    *
450    * The image to be composited should already be drawn into the buffer, in the
451    * proper place, after all necessary transforms have been applied.
452    *
453    * @param bounds The bounds to draw, in user-space.
454    * @param observer The image observer, if any (may be null).
455    * @return True on success, false on failure.
456    */
drawComposite(Rectangle2D bounds, ImageObserver observer)457   private boolean drawComposite(Rectangle2D bounds, ImageObserver observer)
458   {
459     // Find bounds in device space
460     bounds = getTransformedBounds(bounds, transform);
461 
462     // Clip bounds by the stored clip, and by the internal buffer
463     Rectangle2D devClip = this.getClipInDevSpace();
464     Rectangle2D.intersect(bounds, devClip, bounds);
465     devClip = new Rectangle(buffer.getMinX(), buffer.getMinY(),
466                             buffer.getWidth(), buffer.getHeight());
467     Rectangle2D.intersect(bounds, devClip, bounds);
468 
469     // Round bounds as needed, but be careful in our rounding
470     // (otherwise it may leave unpainted stripes)
471     double x = bounds.getX();
472     double y = bounds.getY();
473     double maxX = x + bounds.getWidth();
474     double maxY = y + bounds.getHeight();
475     x = Math.round(x);
476     y = Math.round(y);
477     bounds.setRect(x, y, Math.round(maxX - x), Math.round(maxY - y));
478 
479     // Find subimage of internal buffer for updating
480     BufferedImage buffer2 = buffer;
481     if (!bounds.equals(buffer2.getRaster().getBounds()))
482       buffer2 = buffer2.getSubimage((int)bounds.getX(), (int)bounds.getY(),
483                                     (int)bounds.getWidth(),
484                                     (int)bounds.getHeight());
485 
486     // Find subimage of main image for updating
487     BufferedImage current = image;
488     current = current.getSubimage((int)bounds.getX(), (int)bounds.getY(),
489                                   (int)bounds.getWidth(),
490                                   (int)bounds.getHeight());
491 
492     // Perform actual composite operation
493     compCtx.compose(buffer2.getRaster(), current.getRaster(),
494                     current.getRaster());
495 
496     // Set cairo's composite to direct SRC, since we've already done our own
497     // compositing
498     Composite oldcomp = comp;
499     setComposite(AlphaComposite.Src);
500 
501     // This MUST call directly into the "action" method in CairoGraphics2D,
502     // not one of the wrappers, to ensure that the composite isn't processed
503     // more than once!
504     boolean rv = super.drawImage(current,
505                                  AffineTransform.getTranslateInstance(bounds.getX(),
506                                                                       bounds.getY()),
507                                  null, null);
508     setComposite(oldcomp);
509     updateColor();
510     return rv;
511   }
512 
createBuffer()513   private void createBuffer()
514   {
515     if (buffer == null)
516       {
517         buffer = new BufferedImage(image.getWidth(), image.getHeight(),
518                                    BufferedImage.TYPE_INT_ARGB);
519       }
520     else
521       {
522         Graphics2D g2d = ((Graphics2D)buffer.getGraphics());
523 
524         g2d.setBackground(new Color(0,0,0,0));
525         g2d.clearRect(0, 0, buffer.getWidth(), buffer.getHeight());
526       }
527   }
528 
getNativeCM()529   protected ColorModel getNativeCM()
530   {
531     return image.getColorModel();
532   }
533 
getBufferCM()534   protected ColorModel getBufferCM()
535   {
536     return ColorModel.getRGBdefault();
537   }
538 }
539