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