/**
* Copyright 2013 JogAmp Community. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of JogAmp Community.
*/
package jogamp.opengl.awt;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;
import javax.imageio.ImageIO;
import com.jogamp.nativewindow.util.DimensionImmutable;
import com.jogamp.nativewindow.util.PixelFormat;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLCapabilitiesImmutable;
import com.jogamp.opengl.GLEventListener;
import jogamp.opengl.Debug;
import com.jogamp.opengl.util.TileRenderer;
import com.jogamp.opengl.util.TileRendererBase;
import com.jogamp.opengl.util.GLPixelBuffer.GLPixelAttributes;
import com.jogamp.opengl.util.awt.AWTGLPixelBuffer;
import com.jogamp.opengl.util.awt.AWTGLPixelBuffer.AWTGLPixelBufferProvider;
/**
* Implementing AWT {@link Graphics2D} based {@link TileRenderer} painter .
*
* Maybe utilized for AWT printing.
*
*/
public class AWTTilePainter {
private static final boolean DEBUG_TILES = Debug.debug("TileRenderer.PNG");
public final TileRenderer renderer;
public final int componentCount;
public final double scaleMatX, scaleMatY;
public final int customTileWidth, customTileHeight, customNumSamples;
public final boolean verbose;
/** Default for OpenGL: True */
public boolean flipVertical;
/** Default for OpenGL: True */
public boolean originBottomLeft;
private AWTGLPixelBuffer tBuffer = null;
private BufferedImage vFlipImage = null;
private Graphics2D g2d = null;
private AffineTransform saveAT = null;
public static void dumpHintsAndScale(final Graphics2D g2d) {
final RenderingHints rHints = g2d.getRenderingHints();
final Set> rEntries = rHints.entrySet();
int count = 0;
for(final Iterator> rEntryIter = rEntries.iterator(); rEntryIter.hasNext(); count++) {
final Entry rEntry = rEntryIter.next();
System.err.println("Hint["+count+"]: "+rEntry.getKey()+" -> "+rEntry.getValue());
}
final AffineTransform aTrans = g2d.getTransform();
if( null != aTrans ) {
System.err.println(" type "+aTrans.getType());
System.err.println(" scale "+aTrans.getScaleX()+" x "+aTrans.getScaleY());
System.err.println(" move "+aTrans.getTranslateX()+" x "+aTrans.getTranslateY());
System.err.println(" mat "+aTrans);
} else {
System.err.println(" null transform");
}
}
/**
* @return resulting number of samples by comparing w/ {@link #customNumSamples} and the caps-config, 0 if disabled
*/
public int getNumSamples(final GLCapabilitiesImmutable caps) {
if( 0 > customNumSamples ) {
return 0;
} else if( 0 < customNumSamples ) {
if ( !caps.getGLProfile().isGL2ES3() ) {
return 0;
}
return Math.max(caps.getNumSamples(), customNumSamples);
} else {
return caps.getNumSamples();
}
}
/**
* Assumes a configured {@link TileRenderer}, i.e.
* an {@link TileRenderer#attachAutoDrawable(GLAutoDrawable) attached}
* {@link GLAutoDrawable} with {@link TileRenderer#setTileSize(int, int, int) set tile size}.
*
* Sets the renderer to {@link TileRenderer#TR_TOP_TO_BOTTOM} row order.
*
*
* componentCount
reflects opaque, i.e. 4 if non opaque.
*
* @param renderer
* @param componentCount
* @param scaleMatX {@link Graphics2D} {@link Graphics2D#scale(double, double) scaling factor}, i.e. rendering 1/scaleMatX * width pixels
* @param scaleMatY {@link Graphics2D} {@link Graphics2D#scale(double, double) scaling factor}, i.e. rendering 1/scaleMatY * height pixels
* @param numSamples custom multisampling value: < 0 turns off, == 0 leaves as-is, > 0 enables using given num samples
* @param tileWidth custom tile width for {@link TileRenderer#setTileSize(int, int, int) tile renderer}, pass -1 for default.
* @param tileHeight custom tile height for {@link TileRenderer#setTileSize(int, int, int) tile renderer}, pass -1 for default.
* @param verbose
*/
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) {
this.renderer = renderer;
this.renderer.setGLEventListener(preTileGLEL, postTileGLEL);
this.componentCount = componentCount;
this.scaleMatX = scaleMatX;
this.scaleMatY = scaleMatY;
this.customNumSamples = numSamples;
this.customTileWidth= tileWidth;
this.customTileHeight = tileHeight;
this.verbose = verbose;
this.flipVertical = true;
}
@Override
public String toString() {
return "AWTTilePainter[flipVertical "+flipVertical+", startFromBottom "+originBottomLeft+", "+
renderer.toString()+"]";
}
/**
* @param flipVertical if true
, the image will be flipped vertically (Default for OpenGL).
* @param originBottomLeft if true
, the image's origin is on the bottom left (Default for OpenGL).
*/
public void setGLOrientation(final boolean flipVertical, final boolean originBottomLeft) {
this.flipVertical = flipVertical;
this.originBottomLeft = originBottomLeft;
}
private static Rectangle2D getClipBounds2D(final Graphics2D g) {
final Shape shape = g.getClip();
return null != shape ? shape.getBounds2D() : null;
}
private static Rectangle2D clipNegative(final Rectangle2D in) {
if( null == in ) { return null; }
double x=in.getX(), y=in.getY(), width=in.getWidth(), height=in.getHeight();
if( 0 > x ) {
width += x;
x = 0;
}
if( 0 > y ) {
height += y;
y = 0;
}
return new Rectangle2D.Double(x, y, width, height);
}
/**
* Caches the {@link Graphics2D} instance for rendering.
*
* Copies the current {@link Graphics2D} {@link AffineTransform}
* and scales {@link Graphics2D} w/ scaleMatX
x scaleMatY
.
* After rendering, the {@link AffineTransform} should be reset via {@link #resetGraphics2D()}.
*
*
* Sets the {@link TileRenderer}'s {@link TileRenderer#setImageSize(int, int) image size}
* and {@link TileRenderer#setTileOffset(int, int) tile offset} according the
* the {@link Graphics2D#getClipBounds() graphics clip bounds}.
*
* @param g2d Graphics2D instance used for transform and clipping
* @param width width of the AWT component in case clipping is null
* @param height height of the AWT component in case clipping is null
* @throws NoninvertibleTransformException if the {@link Graphics2D}'s {@link AffineTransform} {@link AffineTransform#invert() inversion} fails.
* Since inversion is tested before scaling the given {@link Graphics2D}, caller shall ignore the whole term .
*/
public void setupGraphics2DAndClipBounds(final Graphics2D g2d, final int width, final int height) throws NoninvertibleTransformException {
this.g2d = g2d;
saveAT = g2d.getTransform();
if( null == saveAT ) {
saveAT = new AffineTransform(); // use identity
}
// We use double precision for scaling
//
// Setup original rectangles
final Rectangle2D dClipOrigR = getClipBounds2D(g2d);
final Rectangle2D dClipOrig = clipNegative(dClipOrigR);
final Rectangle2D dImageSizeOrig = new Rectangle2D.Double(0, 0, width, height);
// Retrieve scaled image-size and clip-bounds
// Note: Clip bounds lie within image-size!
final Rectangle2D dImageSizeScaled, dClipScaled;
{
final AffineTransform scaledATI;
{
final AffineTransform scaledAT = new AffineTransform(saveAT);
scaledAT.scale(scaleMatX, scaleMatY);
scaledATI = scaledAT.createInverse(); // -> NoninvertibleTransformException
}
Shape s0 = saveAT.createTransformedShape(dImageSizeOrig); // user in
dImageSizeScaled = scaledATI.createTransformedShape(s0).getBounds2D(); // scaled out
if( null == dClipOrig ) {
dClipScaled = (Rectangle2D) dImageSizeScaled.clone();
} else {
s0 = saveAT.createTransformedShape(dClipOrig); // user in
dClipScaled = scaledATI.createTransformedShape(s0).getBounds2D(); // scaled out
}
}
final Rectangle iClipScaled = dClipScaled.getBounds();
final Rectangle iImageSizeScaled = dImageSizeScaled.getBounds();
renderer.setImageSize(iImageSizeScaled.width, iImageSizeScaled.height);
renderer.clipImageSize(iClipScaled.width, iClipScaled.height);
final int clipH = Math.min(iImageSizeScaled.height, iClipScaled.height);
// Clip bounds lie within image-size!
// GL y-offset is lower-left origin, AWT y-offset upper-left.
scaledYOffset = iClipScaled.y;
renderer.setTileOffset(iClipScaled.x, iImageSizeScaled.height - ( iClipScaled.y + clipH ));
// Scale actual Grahics2D matrix
g2d.scale(scaleMatX, scaleMatY);
if( verbose ) {
System.err.println("AWT print.0: image "+dImageSizeOrig + " -> " + dImageSizeScaled + " -> " + iImageSizeScaled);
System.err.println("AWT print.0: clip "+dClipOrigR + " -> " + dClipOrig + " -> " + dClipScaled + " -> " + iClipScaled);
System.err.println("AWT print.0: "+renderer);
}
}
private int scaledYOffset;
/** See {@ #setupGraphics2DAndClipBounds(Graphics2D)}. */
public void resetGraphics2D() {
g2d.setTransform(saveAT);
}
/**
* Disposes resources and {@link TileRenderer#detachAutoDrawable() detaches}
* the {@link TileRenderer}'s {@link GLAutoDrawable}.
*/
public void dispose() {
renderer.detachAutoDrawable(); // tile-renderer -> printGLAD
g2d = null;
if( null != tBuffer ) {
tBuffer.dispose();
tBuffer = null;
}
if( null != vFlipImage ) {
vFlipImage.flush();
vFlipImage = null;
}
}
final GLEventListener preTileGLEL = new GLEventListener() {
@Override
public void init(final GLAutoDrawable drawable) {}
@Override
public void dispose(final GLAutoDrawable drawable) {}
@Override
public void display(final GLAutoDrawable drawable) {
final GL gl = drawable.getGL();
if( null == tBuffer ) {
final int tWidth = renderer.getParam(TileRenderer.TR_TILE_WIDTH);
final int tHeight = renderer.getParam(TileRenderer.TR_TILE_HEIGHT);
final AWTGLPixelBufferProvider printBufferProvider = new AWTGLPixelBufferProvider( true /* allowRowStride */ );
final PixelFormat.Composition hostPixelComp = printBufferProvider.getHostPixelComp(gl.getGLProfile(), componentCount);
final GLPixelAttributes pixelAttribs = printBufferProvider.getAttributes(gl, componentCount, true);
tBuffer = printBufferProvider.allocate(gl, hostPixelComp, pixelAttribs, true, tWidth, tHeight, 1, 0);
renderer.setTileBuffer(tBuffer);
if( flipVertical ) {
vFlipImage = new BufferedImage(tBuffer.width, tBuffer.height, tBuffer.image.getType());
} else {
vFlipImage = null;
}
}
if( verbose ) {
System.err.println("XXX tile-pre "+renderer);
}
}
@Override
public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {}
};
static int _counter = 0;
final GLEventListener postTileGLEL = new GLEventListener() {
@Override
public void init(final GLAutoDrawable drawable) {
}
@Override
public void dispose(final GLAutoDrawable drawable) {}
@Override
public void display(final GLAutoDrawable drawable) {
final DimensionImmutable cis = renderer.getClippedImageSize();
final int tWidth = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_WIDTH);
final int tHeight = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_HEIGHT);
final int tY = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_Y_POS);
final int tYOff = renderer.getParam(TileRenderer.TR_TILE_Y_OFFSET);
final int imgYOff = originBottomLeft ? 0 : renderer.getParam(TileRenderer.TR_TILE_HEIGHT) - tHeight; // imgYOff will be cut-off via sub-image
final int pX = renderer.getParam(TileRendererBase.TR_CURRENT_TILE_X_POS); // tileX == pX
final int pY = cis.getHeight() - ( tY - tYOff + tHeight ) + scaledYOffset;
// Copy temporary data into raster of BufferedImage for faster
// blitting Note that we could avoid this copy in the cases
// where !offscreenDrawable.isGLOriented(),
// but that's the software rendering path which is very slow anyway.
final BufferedImage dstImage;
if( DEBUG_TILES ) {
final String fname = String.format("file_%03d_0_tile_[%02d][%02d]_sz_%03dx%03d_pos0_%03d_%03d_yOff_%03d_pos1_%03d_%03d.png",
_counter,
renderer.getParam(TileRenderer.TR_CURRENT_COLUMN), renderer.getParam(TileRenderer.TR_CURRENT_ROW),
tWidth, tHeight,
pX, tY, tYOff, pX, pY).replace(' ', '_');
System.err.println("XXX file "+fname);
final File fout = new File(fname);
try {
ImageIO.write(tBuffer.image, "png", fout);
} catch (final IOException e) {
e.printStackTrace();
}
}
if( flipVertical ) {
final BufferedImage srcImage = tBuffer.image;
dstImage = vFlipImage;
final int[] src = ((DataBufferInt) srcImage.getRaster().getDataBuffer()).getData();
final int[] dst = ((DataBufferInt) dstImage.getRaster().getDataBuffer()).getData();
if( DEBUG_TILES ) {
Arrays.fill(dst, 0x55);
}
final int incr = tBuffer.width;
int srcPos = 0;
int destPos = (tHeight - 1) * tBuffer.width;
for (; destPos >= 0; srcPos += incr, destPos -= incr) {
System.arraycopy(src, srcPos, dst, destPos, incr);
}
} else {
dstImage = tBuffer.image;
}
if( DEBUG_TILES ) {
final String fname = String.format("file_%03d_1_tile_[%02d][%02d]_sz_%03dx%03d_pos0_%03d_%03d_yOff_%03d_pos1_%03d_%03d.png",
_counter,
renderer.getParam(TileRenderer.TR_CURRENT_COLUMN), renderer.getParam(TileRenderer.TR_CURRENT_ROW),
tWidth, tHeight,
pX, tY, tYOff, pX, pY).replace(' ', '_');
System.err.println("XXX file "+fname);
final File fout = new File(fname);
try {
ImageIO.write(dstImage, "png", fout);
} catch (final IOException e) {
e.printStackTrace();
}
_counter++;
}
// Draw resulting image in one shot
final BufferedImage outImage = dstImage.getSubimage(0, imgYOff, tWidth, tHeight);
final boolean drawDone = g2d.drawImage(outImage, pX, pY, null); // Null ImageObserver since image data is ready.
if( verbose ) {
final Shape oClip = g2d.getClip();
System.err.println("XXX tile-post.X tile 0 / "+imgYOff+" "+tWidth+"x"+tHeight+", clippedImgSize "+cis);
System.err.println("XXX tile-post.X pYf "+cis.getHeight()+" - ( "+tY+" - "+tYOff+" + "+tHeight+" ) "+scaledYOffset+" = "+ pY);
System.err.println("XXX tile-post.X clip "+oClip+" + "+pX+" / [pY "+tY+", pYOff "+tYOff+", pYf "+pY+"] -> "+g2d.getClip());
g2d.setColor(Color.BLACK);
g2d.drawRect(pX, pY, tWidth, tHeight);
if( null != oClip ) {
final Rectangle r = oClip.getBounds();
g2d.setColor(Color.YELLOW);
g2d.drawRect(r.x, r.y, r.width, r.height);
}
System.err.println("XXX tile-post.X "+renderer);
System.err.println("XXX tile-post.X dst-img "+dstImage.getWidth()+"x"+dstImage.getHeight());
System.err.println("XXX tile-post.X out-img "+outImage.getWidth()+"x"+outImage.getHeight());
System.err.println("XXX tile-post.X y-flip "+flipVertical+", originBottomLeft "+originBottomLeft+" -> "+pX+"/"+pY+", drawDone "+drawDone);
}
}
@Override
public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {}
};
}