in java/org/cef/browser/CefBrowserOsr.java [515:670]
public CompletableFuture<BufferedImage> createScreenshot(boolean nativeResolution) {
int width = (int) Math.ceil(canvas_.getWidth() * scaleFactor_);
int height = (int) Math.ceil(canvas_.getHeight() * scaleFactor_);
// In order to grab a screenshot of the browser window, we need to get the OpenGL internals
// from the GLCanvas that displays the browser.
GL2 gl = canvas_.getGL().getGL2();
int textureId = renderer_.getTextureID();
// This mirrors the two ways in which CefRenderer may render images internally - either via
// an incrementally updated texture that is the same size as the window and simply rendered
// onto a textured quad by graphics hardware, in which case we capture the data directly
// from this texture, or by directly writing pixels into the OpenGL framebuffer, in which
// case we directly read those pixels back. The latter is the way chosen if there is no
// hardware rasterizer capability detected. We can simply distinguish both approaches by
// looking whether the textureId of the renderer is a valid (non-zero) one.
boolean useReadPixels = (textureId == 0);
// This Callable encapsulates the pixel-reading code. After running it, the screenshot
// BufferedImage contains the grabbed image.
final Callable<BufferedImage> pixelGrabberCallable = new Callable<BufferedImage>() {
@Override
public BufferedImage call() {
BufferedImage screenshot =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
ByteBuffer buffer = GLBuffers.newDirectByteBuffer(width * height * 4);
gl.getContext().makeCurrent();
try {
if (useReadPixels) {
// If pixels are copied directly to the framebuffer, we also directly read
// them back.
gl.glReadPixels(
0, 0, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);
} else {
// In this case, read the texture pixel data from the previously-retrieved
// texture ID
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glBindTexture(GL.GL_TEXTURE_2D, textureId);
gl.glGetTexImage(
GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);
gl.glDisable(GL.GL_TEXTURE_2D);
}
} finally {
gl.getContext().release();
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// The OpenGL functions only support RGBA, while Java BufferedImage uses
// ARGB. We must convert.
int r = (buffer.get() & 0xff);
int g = (buffer.get() & 0xff);
int b = (buffer.get() & 0xff);
int a = (buffer.get() & 0xff);
int argb = (a << 24) | (r << 16) | (g << 8) | (b << 0);
// If pixels were read from the framebuffer, we have to flip the resulting
// image on the Y axis, as the OpenGL framebuffer's y axis starts at the
// bottom of the image pointing "upwards", while BufferedImage has the
// origin in the upper left corner. This flipping is done when drawing into
// the BufferedImage.
screenshot.setRGB(x, useReadPixels ? (height - y - 1) : y, argb);
}
}
if (!nativeResolution && scaleFactor_ != 1.0) {
// HiDPI images should be resized down to "normal" levels
BufferedImage resized =
new BufferedImage((int) (screenshot.getWidth() / scaleFactor_),
(int) (screenshot.getHeight() / scaleFactor_),
BufferedImage.TYPE_INT_ARGB);
AffineTransform tempTransform = new AffineTransform();
tempTransform.scale(1.0 / scaleFactor_, 1.0 / scaleFactor_);
AffineTransformOp tempScaleOperation =
new AffineTransformOp(tempTransform, AffineTransformOp.TYPE_BILINEAR);
resized = tempScaleOperation.filter(screenshot, resized);
return resized;
} else {
return screenshot;
}
}
};
if (SwingUtilities.isEventDispatchThread()) {
// If called on the AWT event thread, just access the GL API
try {
BufferedImage screenshot = pixelGrabberCallable.call();
return CompletableFuture.completedFuture(screenshot);
} catch (Exception e) {
CompletableFuture<BufferedImage> future = new CompletableFuture<BufferedImage>();
future.completeExceptionally(e);
return future;
}
} else {
// If called from another thread, register a GLEventListener and trigger an async
// redraw, during which we use the GL API to grab the pixel data. An unresolved Future
// is returned, on which the caller can wait for a result (but not with the Event
// Thread, as we need that for pixel grabbing, which is why there's a safeguard in place
// to catch that situation if it accidentally happens).
CompletableFuture<BufferedImage> future = new CompletableFuture<BufferedImage>() {
private void safeguardGet() {
if (SwingUtilities.isEventDispatchThread()) {
throw new RuntimeException(
"Waiting on this Future using the AWT Event Thread is illegal, "
+ "because it can potentially deadlock the thread.");
}
}
@Override
public BufferedImage get() throws InterruptedException, ExecutionException {
safeguardGet();
return super.get();
}
@Override
public BufferedImage get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
safeguardGet();
return super.get(timeout, unit);
}
};
canvas_.addGLEventListener(new GLEventListener() {
@Override
public void reshape(
GLAutoDrawable aDrawable, int aArg1, int aArg2, int aArg3, int aArg4) {
// ignore
}
@Override
public void init(GLAutoDrawable aDrawable) {
// ignore
}
@Override
public void dispose(GLAutoDrawable aDrawable) {
// ignore
}
@Override
public void display(GLAutoDrawable aDrawable) {
canvas_.removeGLEventListener(this);
try {
future.complete(pixelGrabberCallable.call());
} catch (Exception e) {
future.completeExceptionally(e);
}
}
});
// This repaint triggers an indirect call to the listeners' display method above, which
// ultimately completes the future that we return immediately.
canvas_.repaint();
return future;
}
}