1 /**
2  * Copyright 2013 JogAmp Community. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without modification, are
5  * permitted provided that the following conditions are met:
6  *
7  *    1. Redistributions of source code must retain the above copyright notice, this list of
8  *       conditions and the following disclaimer.
9  *
10  *    2. Redistributions in binary form must reproduce the above copyright notice, this list
11  *       of conditions and the following disclaimer in the documentation and/or other materials
12  *       provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
15  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
21  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
22  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23  *
24  * The views and conclusions contained in the software and documentation are those of the
25  * authors and should not be interpreted as representing official policies, either expressed
26  * or implied, of JogAmp Community.
27  */
28 package jogamp.opengl.awt;
29 
30 import java.awt.Color;
31 import java.awt.Graphics2D;
32 import java.awt.Rectangle;
33 import java.awt.RenderingHints;
34 import java.awt.Shape;
35 import java.awt.geom.AffineTransform;
36 import java.awt.geom.NoninvertibleTransformException;
37 import java.awt.geom.Rectangle2D;
38 import java.awt.image.BufferedImage;
39 import java.awt.image.DataBufferInt;
40 import java.io.File;
41 import java.io.IOException;
42 import java.util.Arrays;
43 import java.util.Iterator;
44 import java.util.Set;
45 import java.util.Map.Entry;
46 
47 import javax.imageio.ImageIO;
48 import com.jogamp.nativewindow.util.DimensionImmutable;
49 import com.jogamp.nativewindow.util.PixelFormat;
50 import com.jogamp.opengl.GL;
51 import com.jogamp.opengl.GLAutoDrawable;
52 import com.jogamp.opengl.GLCapabilitiesImmutable;
53 import com.jogamp.opengl.GLEventListener;
54 
55 import jogamp.opengl.Debug;
56 
57 import com.jogamp.opengl.util.TileRenderer;
58 import com.jogamp.opengl.util.TileRendererBase;
59 import com.jogamp.opengl.util.GLPixelBuffer.GLPixelAttributes;
60 import com.jogamp.opengl.util.awt.AWTGLPixelBuffer;
61 import com.jogamp.opengl.util.awt.AWTGLPixelBuffer.AWTGLPixelBufferProvider;
62 
63 /**
64  * Implementing AWT {@link Graphics2D} based {@link TileRenderer} <i>painter</i>.
65  * <p>
66  * Maybe utilized for AWT printing.
67  * </p>
68  */
69 public class AWTTilePainter {
70     private static final boolean DEBUG_TILES = Debug.debug("TileRenderer.PNG");
71 
72     public final TileRenderer renderer;
73     public final int componentCount;
74     public final double scaleMatX, scaleMatY;
75     public final int customTileWidth, customTileHeight, customNumSamples;
76     public final boolean verbose;
77 
78     /** Default for OpenGL: True */
79     public boolean flipVertical;
80     /** Default for OpenGL: True */
81     public boolean originBottomLeft;
82     private AWTGLPixelBuffer tBuffer = null;
83     private BufferedImage vFlipImage = null;
84     private Graphics2D g2d = null;
85     private AffineTransform saveAT = null;
86 
dumpHintsAndScale(final Graphics2D g2d)87     public static void dumpHintsAndScale(final Graphics2D g2d) {
88           final RenderingHints rHints = g2d.getRenderingHints();
89           final Set<Entry<Object, Object>> rEntries = rHints.entrySet();
90           int count = 0;
91           for(final Iterator<Entry<Object, Object>> rEntryIter = rEntries.iterator(); rEntryIter.hasNext(); count++) {
92               final Entry<Object, Object> rEntry = rEntryIter.next();
93               System.err.println("Hint["+count+"]: "+rEntry.getKey()+" -> "+rEntry.getValue());
94           }
95           final AffineTransform aTrans = g2d.getTransform();
96           if( null != aTrans ) {
97               System.err.println(" type "+aTrans.getType());
98               System.err.println(" scale "+aTrans.getScaleX()+" x "+aTrans.getScaleY());
99               System.err.println(" move "+aTrans.getTranslateX()+" x "+aTrans.getTranslateY());
100               System.err.println(" mat  "+aTrans);
101           } else {
102               System.err.println(" null transform");
103           }
104     }
105 
106     /**
107      * @return resulting number of samples by comparing w/ {@link #customNumSamples} and the caps-config, 0 if disabled
108      */
getNumSamples(final GLCapabilitiesImmutable caps)109     public int getNumSamples(final GLCapabilitiesImmutable caps) {
110           if( 0 > customNumSamples ) {
111               return 0;
112           } else if( 0 < customNumSamples ) {
113               if ( !caps.getGLProfile().isGL2ES3() ) {
114                   return 0;
115               }
116               return Math.max(caps.getNumSamples(), customNumSamples);
117           } else {
118               return caps.getNumSamples();
119           }
120     }
121 
122     /**
123      * Assumes a configured {@link TileRenderer}, i.e.
124      * an {@link TileRenderer#attachAutoDrawable(GLAutoDrawable) attached}
125      * {@link GLAutoDrawable} with {@link TileRenderer#setTileSize(int, int, int) set tile size}.
126      * <p>
127      * Sets the renderer to {@link TileRenderer#TR_TOP_TO_BOTTOM} row order.
128      * </p>
129      * <p>
130      * <code>componentCount</code> reflects opaque, i.e. 4 if non opaque.
131      * </p>
132      * @param renderer
133      * @param componentCount
134      * @param scaleMatX {@link Graphics2D} {@link Graphics2D#scale(double, double) scaling factor}, i.e. rendering 1/scaleMatX * width pixels
135      * @param scaleMatY {@link Graphics2D} {@link Graphics2D#scale(double, double) scaling factor}, i.e. rendering 1/scaleMatY * height pixels
136      * @param numSamples custom multisampling value: < 0 turns off, == 0 leaves as-is, > 0 enables using given num samples
137      * @param tileWidth custom tile width for {@link TileRenderer#setTileSize(int, int, int) tile renderer}, pass -1 for default.
138      * @param tileHeight custom tile height for {@link TileRenderer#setTileSize(int, int, int) tile renderer}, pass -1 for default.
139      * @param verbose
140      */
AWTTilePainter(final TileRenderer renderer, final int componentCount, final double scaleMatX, final double scaleMatY, final int numSamples, final int tileWidth, final int tileHeight, final boolean verbose)141     public AWTTilePainter(final TileRenderer renderer, final int componentCount, final double scaleMatX, final double scaleMatY, final int numSamples, final int tileWidth, final int tileHeight, final boolean verbose) {
142         this.renderer = renderer;
143         this.renderer.setGLEventListener(preTileGLEL, postTileGLEL);
144         this.componentCount = componentCount;
145         this.scaleMatX = scaleMatX;
146         this.scaleMatY = scaleMatY;
147         this.customNumSamples = numSamples;
148         this.customTileWidth= tileWidth;
149         this.customTileHeight = tileHeight;
150         this.verbose = verbose;
151         this.flipVertical = true;
152     }
153 
154     @Override
toString()155     public String toString() {
156         return "AWTTilePainter[flipVertical "+flipVertical+", startFromBottom "+originBottomLeft+", "+
157                 renderer.toString()+"]";
158     }
159 
160     /**
161      * @param flipVertical if <code>true</code>, the image will be flipped vertically (Default for OpenGL).
162      * @param originBottomLeft if <code>true</code>, the image's origin is on the bottom left (Default for OpenGL).
163      */
setGLOrientation(final boolean flipVertical, final boolean originBottomLeft)164     public void setGLOrientation(final boolean flipVertical, final boolean originBottomLeft) {
165         this.flipVertical = flipVertical;
166         this.originBottomLeft = originBottomLeft;
167     }
168 
getClipBounds2D(final Graphics2D g)169     private static Rectangle2D getClipBounds2D(final Graphics2D g) {
170         final Shape shape = g.getClip();
171         return null != shape ? shape.getBounds2D() : null;
172     }
clipNegative(final Rectangle2D in)173     private static Rectangle2D clipNegative(final Rectangle2D in) {
174         if( null == in ) { return null; }
175         double x=in.getX(), y=in.getY(), width=in.getWidth(), height=in.getHeight();
176         if( 0 > x ) {
177             width += x;
178             x = 0;
179         }
180         if( 0 > y ) {
181             height += y;
182             y = 0;
183         }
184         return new Rectangle2D.Double(x, y, width, height);
185     }
186 
187     /**
188      * Caches the {@link Graphics2D} instance for rendering.
189      * <p>
190      * Copies the current {@link Graphics2D} {@link AffineTransform}
191      * and scales {@link Graphics2D} w/ <code>scaleMatX</code> x <code>scaleMatY</code>.<br>
192      * After rendering, the {@link AffineTransform} should be reset via {@link #resetGraphics2D()}.
193      * </p>
194      * <p>
195      * Sets the {@link TileRenderer}'s {@link TileRenderer#setImageSize(int, int) image size}
196      * and {@link TileRenderer#setTileOffset(int, int) tile offset} according the
197      * the {@link Graphics2D#getClipBounds() graphics clip bounds}.
198      * </p>
199      * @param g2d Graphics2D instance used for transform and clipping
200      * @param width width of the AWT component in case clipping is null
201      * @param height height of the AWT component in case clipping is null
202      * @throws NoninvertibleTransformException if the {@link Graphics2D}'s {@link AffineTransform} {@link AffineTransform#invert() inversion} fails.
203      *                                         Since inversion is tested before scaling the given {@link Graphics2D}, caller shall ignore the whole <i>term</i>.
204      */
setupGraphics2DAndClipBounds(final Graphics2D g2d, final int width, final int height)205     public void setupGraphics2DAndClipBounds(final Graphics2D g2d, final int width, final int height) throws NoninvertibleTransformException {
206         this.g2d = g2d;
207         saveAT = g2d.getTransform();
208         if( null == saveAT ) {
209             saveAT = new AffineTransform(); // use identity
210         }
211         // We use double precision for scaling
212         //
213         // Setup original rectangles
214         final Rectangle2D dClipOrigR = getClipBounds2D(g2d);
215         final Rectangle2D dClipOrig = clipNegative(dClipOrigR);
216         final Rectangle2D dImageSizeOrig = new Rectangle2D.Double(0, 0, width, height);
217 
218         // Retrieve scaled image-size and clip-bounds
219         // Note: Clip bounds lie within image-size!
220         final Rectangle2D dImageSizeScaled, dClipScaled;
221         {
222             final AffineTransform scaledATI;
223             {
224                 final AffineTransform scaledAT = new AffineTransform(saveAT);
225                 scaledAT.scale(scaleMatX, scaleMatY);
226                 scaledATI = scaledAT.createInverse(); // -> NoninvertibleTransformException
227             }
228             Shape s0 = saveAT.createTransformedShape(dImageSizeOrig); // user in
229             dImageSizeScaled = scaledATI.createTransformedShape(s0).getBounds2D(); // scaled out
230             if( null == dClipOrig ) {
231                 dClipScaled = (Rectangle2D) dImageSizeScaled.clone();
232             } else {
233                 s0 = saveAT.createTransformedShape(dClipOrig); // user in
234                 dClipScaled = scaledATI.createTransformedShape(s0).getBounds2D(); // scaled out
235             }
236         }
237         final Rectangle iClipScaled = dClipScaled.getBounds();
238         final Rectangle iImageSizeScaled = dImageSizeScaled.getBounds();
239         renderer.setImageSize(iImageSizeScaled.width, iImageSizeScaled.height);
240         renderer.clipImageSize(iClipScaled.width, iClipScaled.height);
241         final int clipH = Math.min(iImageSizeScaled.height, iClipScaled.height);
242         // Clip bounds lie within image-size!
243         // GL y-offset is lower-left origin, AWT y-offset upper-left.
244         scaledYOffset = iClipScaled.y;
245         renderer.setTileOffset(iClipScaled.x, iImageSizeScaled.height - ( iClipScaled.y + clipH ));
246 
247         // Scale actual Grahics2D matrix
248         g2d.scale(scaleMatX, scaleMatY);
249 
250         if( verbose ) {
251             System.err.println("AWT print.0: image "+dImageSizeOrig + " -> " + dImageSizeScaled + " -> " + iImageSizeScaled);
252             System.err.println("AWT print.0: clip  "+dClipOrigR + " -> " + dClipOrig + " -> " + dClipScaled + " -> " + iClipScaled);
253             System.err.println("AWT print.0: "+renderer);
254         }
255     }
256     private int scaledYOffset;
257 
258     /** See {@ #setupGraphics2DAndClipBounds(Graphics2D)}. */
resetGraphics2D()259     public void resetGraphics2D() {
260         g2d.setTransform(saveAT);
261     }
262 
263     /**
264      * Disposes resources and {@link TileRenderer#detachAutoDrawable() detaches}
265      * the {@link TileRenderer}'s {@link GLAutoDrawable}.
266      */
dispose()267     public void dispose() {
268         renderer.detachAutoDrawable(); // tile-renderer -> printGLAD
269         g2d = null;
270         if( null != tBuffer ) {
271             tBuffer.dispose();
272             tBuffer = null;
273         }
274         if( null != vFlipImage ) {
275             vFlipImage.flush();
276             vFlipImage = null;
277         }
278     }
279 
280     final GLEventListener preTileGLEL = new GLEventListener() {
281         @Override
282         public void init(final GLAutoDrawable drawable) {}
283         @Override
284         public void dispose(final GLAutoDrawable drawable) {}
285         @Override
286         public void display(final GLAutoDrawable drawable) {
287             final GL gl = drawable.getGL();
288             if( null == tBuffer ) {
289                 final int tWidth = renderer.getParam(TileRenderer.TR_TILE_WIDTH);
290                 final int tHeight = renderer.getParam(TileRenderer.TR_TILE_HEIGHT);
291                 final AWTGLPixelBufferProvider printBufferProvider = new AWTGLPixelBufferProvider( true /* allowRowStride */ );
292                 final PixelFormat.Composition hostPixelComp = printBufferProvider.getHostPixelComp(gl.getGLProfile(), componentCount);
293                 final GLPixelAttributes pixelAttribs = printBufferProvider.getAttributes(gl, componentCount, true);
294                 tBuffer = printBufferProvider.allocate(gl, hostPixelComp, pixelAttribs, true, tWidth, tHeight, 1, 0);
295                 renderer.setTileBuffer(tBuffer);
296                 if( flipVertical ) {
297                     vFlipImage = new BufferedImage(tBuffer.width, tBuffer.height, tBuffer.image.getType());
298                 } else {
299                     vFlipImage = null;
300                 }
301             }
302             if( verbose ) {
303                 System.err.println("XXX tile-pre "+renderer);
304             }
305         }
306         @Override
307         public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {}
308     };
309     static int _counter = 0;
310     final GLEventListener postTileGLEL = new GLEventListener() {
311         @Override
312         public void init(final GLAutoDrawable drawable) {
313         }
314         @Override
315         public void dispose(final GLAutoDrawable drawable) {}
316         @Override
317         public void display(final GLAutoDrawable drawable) {
318             final DimensionImmutable cis = renderer.getClippedImageSize();
319             final int tWidth = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_WIDTH);
320             final int tHeight = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_HEIGHT);
321             final int tY = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_Y_POS);
322             final int tYOff = renderer.getParam(TileRenderer.TR_TILE_Y_OFFSET);
323             final int imgYOff = originBottomLeft ? 0 : renderer.getParam(TileRenderer.TR_TILE_HEIGHT) - tHeight; // imgYOff will be cut-off via sub-image
324             final int pX = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_X_POS); // tileX == pX
325             final int pY = cis.getHeight() - ( tY - tYOff + tHeight ) + scaledYOffset;
326 
327             // Copy temporary data into raster of BufferedImage for faster
328             // blitting Note that we could avoid this copy in the cases
329             // where !offscreenDrawable.isGLOriented(),
330             // but that's the software rendering path which is very slow anyway.
331             final BufferedImage dstImage;
332             if( DEBUG_TILES ) {
333                 final String fname = String.format("file_%03d_0_tile_[%02d][%02d]_sz_%03dx%03d_pos0_%03d_%03d_yOff_%03d_pos1_%03d_%03d.png",
334                         _counter,
335                         renderer.getParam(TileRenderer.TR_CURRENT_COLUMN), renderer.getParam(TileRenderer.TR_CURRENT_ROW),
336                         tWidth, tHeight,
337                         pX, tY, tYOff, pX, pY).replace(' ', '_');
338                 System.err.println("XXX file "+fname);
339                 final File fout = new File(fname);
340                 try {
341                     ImageIO.write(tBuffer.image, "png", fout);
342                 } catch (final IOException e) {
343                     e.printStackTrace();
344                 }
345             }
346             if( flipVertical ) {
347                 final BufferedImage srcImage = tBuffer.image;
348                 dstImage = vFlipImage;
349                 final int[] src = ((DataBufferInt) srcImage.getRaster().getDataBuffer()).getData();
350                 final int[] dst = ((DataBufferInt) dstImage.getRaster().getDataBuffer()).getData();
351                 if( DEBUG_TILES ) {
352                     Arrays.fill(dst, 0x55);
353                 }
354                 final int incr = tBuffer.width;
355                 int srcPos = 0;
356                 int destPos = (tHeight - 1) * tBuffer.width;
357                 for (; destPos >= 0; srcPos += incr, destPos -= incr) {
358                     System.arraycopy(src, srcPos, dst, destPos, incr);
359                 }
360             } else {
361                 dstImage = tBuffer.image;
362             }
363             if( DEBUG_TILES ) {
364                 final String fname = String.format("file_%03d_1_tile_[%02d][%02d]_sz_%03dx%03d_pos0_%03d_%03d_yOff_%03d_pos1_%03d_%03d.png",
365                         _counter,
366                         renderer.getParam(TileRenderer.TR_CURRENT_COLUMN), renderer.getParam(TileRenderer.TR_CURRENT_ROW),
367                         tWidth, tHeight,
368                         pX, tY, tYOff, pX, pY).replace(' ', '_');
369                 System.err.println("XXX file "+fname);
370                 final File fout = new File(fname);
371                 try {
372                     ImageIO.write(dstImage, "png", fout);
373                 } catch (final IOException e) {
374                     e.printStackTrace();
375                 }
376                 _counter++;
377             }
378             // Draw resulting image in one shot
379             final BufferedImage outImage = dstImage.getSubimage(0, imgYOff, tWidth, tHeight);
380             final boolean drawDone = g2d.drawImage(outImage, pX, pY, null); // Null ImageObserver since image data is ready.
381             if( verbose ) {
382                 final Shape oClip = g2d.getClip();
383                 System.err.println("XXX tile-post.X tile 0 / "+imgYOff+" "+tWidth+"x"+tHeight+", clippedImgSize "+cis);
384                 System.err.println("XXX tile-post.X pYf "+cis.getHeight()+" - ( "+tY+" - "+tYOff+" + "+tHeight+" ) "+scaledYOffset+" = "+ pY);
385                 System.err.println("XXX tile-post.X clip "+oClip+" + "+pX+" / [pY "+tY+", pYOff "+tYOff+", pYf "+pY+"] -> "+g2d.getClip());
386                 g2d.setColor(Color.BLACK);
387                 g2d.drawRect(pX, pY, tWidth, tHeight);
388                 if( null != oClip ) {
389                     final Rectangle r = oClip.getBounds();
390                     g2d.setColor(Color.YELLOW);
391                     g2d.drawRect(r.x, r.y, r.width, r.height);
392                 }
393                 System.err.println("XXX tile-post.X "+renderer);
394                 System.err.println("XXX tile-post.X dst-img "+dstImage.getWidth()+"x"+dstImage.getHeight());
395                 System.err.println("XXX tile-post.X out-img "+outImage.getWidth()+"x"+outImage.getHeight());
396                 System.err.println("XXX tile-post.X y-flip "+flipVertical+", originBottomLeft "+originBottomLeft+" -> "+pX+"/"+pY+", drawDone "+drawDone);
397             }
398         }
399         @Override
400         public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {}
401     };
402 }
403