Bug 2057685 - Part 4: Add GeckoView backend support for full page screenshot API r=geckoview-reviewers,ohall

Differential Revision: https://phabricator.services.mozilla.com/D318616
This commit is contained in:
owlishDeveloper
2026-09-12 20:28:11 +00:00
committed by csabou@mozilla.com
parent afed2108b5
commit aabe491b92
3 changed files with 325 additions and 0 deletions
@@ -1444,6 +1444,22 @@ public class GeckoSession {
mNativeQueue = nativeQueue; mNativeQueue = nativeQueue;
} }
@WrapForJNI
/* package */ static class ContentMetrics {
int width;
int height;
float devicePixelRatio;
/* package */ ContentMetrics() {}
@WrapForJNI
/* package */ void set(final int width, final int height, final float devicePixelRatio) {
this.width = width;
this.height = height;
this.devicePixelRatio = devicePixelRatio;
}
}
@Override // IInterface @Override // IInterface
public Binder asBinder() { public Binder asBinder() {
if (mBinder == null) { if (mBinder == null) {
@@ -1527,6 +1543,20 @@ public class GeckoSession {
@WrapForJNI(dispatchTo = "proxy") @WrapForJNI(dispatchTo = "proxy")
private native void printToPdf(GeckoResult<InputStream> geckoResult, long browserContextId); private native void printToPdf(GeckoResult<InputStream> geckoResult, long browserContextId);
@WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
public native void requestFullScreenshot(
GeckoResult<Bitmap> result,
final Bitmap target,
final int x,
final int y,
final int width,
final int height,
final float renderingScale);
@WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
public native void requestContentMetrics(
GeckoResult<ContentMetrics> result, ContentMetrics metrics);
@WrapForJNI(calledFrom = "gecko") @WrapForJNI(calledFrom = "gecko")
private synchronized void onReady(final @Nullable NativeQueue queue) { private synchronized void onReady(final @Nullable NativeQueue queue) {
// onReady is called the first time the Gecko window is ready, with a null queue // onReady is called the first time the Gecko window is ready, with a null queue
+12
View File
@@ -116,6 +116,18 @@ class GeckoViewSupport final
const java::GeckoSession::Window::LocalRef& inst, const java::GeckoSession::Window::LocalRef& inst,
jni::Object::Param aStream); jni::Object::Param aStream);
void RequestFullScreenshot(const java::GeckoSession::Window::LocalRef& inst,
jni::Object::Param aResult,
jni::Object::Param aTarget, int32_t aX, int32_t aY,
int32_t aWidth, int32_t aHeight,
float aRenderingScale);
// Query content for the top document's scroll size (in CSS pixels)
// and device pixel ratio (for calculating the size in device pixels)
void RequestContentMetrics(const java::GeckoSession::Window::LocalRef& inst,
jni::Object::Param aResult,
jni::Object::Param aMetrics);
// See nsIHapticFeedback::HapticFeedbackType for available effects. // See nsIHapticFeedback::HapticFeedbackType for available effects.
void PerformHapticFeedback(int32_t aEffect); void PerformHapticFeedback(int32_t aEffect);
}; };
+283
View File
@@ -60,6 +60,7 @@
#include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/gfx/2D.h" #include "mozilla/gfx/2D.h"
#include "mozilla/gfx/DataSurfaceHelpers.h" #include "mozilla/gfx/DataSurfaceHelpers.h"
#include "mozilla/gfx/Logging.h" #include "mozilla/gfx/Logging.h"
@@ -2206,6 +2207,288 @@ void GeckoViewSupport::PerformHapticFeedback(int32_t aEffect) {
window->PerformHapticFeedback(aEffect); window->PerformHapticFeedback(aEffect);
} }
class FullPageScreenshot final {
public:
NS_INLINE_DECL_REFCOUNTING(FullPageScreenshot)
// Tile size in CSS pixels.
// The number initially is taken from desktop
// (where it was chosen empirically as
// the most performant, see Bug 1854953),
// and then confirmed empirically (in a similar fashion)
// to also be the sweet spot between speed and
// memory pressure on Android.
static constexpr int32_t kTileSize = 1024;
FullPageScreenshot(java::GeckoResult::GlobalRef&& aResult,
RefPtr<dom::WindowGlobalParent>&& aWgp,
java::sdk::Bitmap::GlobalRef&& aBitmap,
const gfx::IntRect& aFullRect, float aScale)
: mResult(std::move(aResult)),
mWgp(std::move(aWgp)),
mBitmap(std::move(aBitmap)),
mFullRect(aFullRect),
mScale(aScale) {}
void Start() {
MOZ_ASSERT(NS_IsMainThread());
if (!mBitmap) {
Reject("Failed to start screenshot capture - no target bitmap");
return;
}
mSnapshotSize = int32_t(kTileSize * mScale);
if (mSnapshotSize <= 0) {
Reject("Failed to start screenshot capture - invalid tile size");
return;
}
/**
* Note: It is up to the caller to pre-fill the bitmap,
* so the device pixels left uncovered by a tile
* show the background rather than remain transparent.
*/
// (CSS pixels)
for (int32_t y = mFullRect.Y(); y < mFullRect.YMost(); y += kTileSize) {
for (int32_t x = mFullRect.X(); x < mFullRect.XMost(); x += kTileSize) {
mTiles.AppendElement(
gfx::IntRect(x, y, std::min(kTileSize, mFullRect.XMost() - x),
std::min(kTileSize, mFullRect.YMost() - y)));
}
}
CaptureTile(0);
}
private:
~FullPageScreenshot() = default;
void Reject(const char* aMsg) {
mResult->CompleteExceptionally(
java::sdk::IllegalStateException::New(aMsg).Cast<jni::Throwable>());
GVS_LOG("%s", aMsg);
}
void CaptureTile(size_t aIndex) {
if (aIndex >= mTiles.Length()) {
mResult->Complete(mBitmap);
return;
}
const gfx::IntRect tile = mTiles[aIndex];
gfx::CrossProcessPaint::Start(
mWgp, Some(tile), mScale, NS_RGB(255, 255, 255),
gfx::CrossProcessPaintFlags::UseHighQualityScaling)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self = RefPtr{this}, tile,
aIndex](RefPtr<gfx::SourceSurface>&& aSurface) {
if (!self->BlitTile(tile, aSurface)) {
self->Reject("Full screenshot failure: failed to copy a tile");
return;
}
self->CaptureTile(aIndex + 1);
},
[self = RefPtr{this}](const nsresult&) {
self->Reject("Full screenshot failure: failed to capture a tile");
});
}
bool BlitTile(const gfx::IntRect& aTileCss, gfx::SourceSurface* aSurface) {
// The tile surface uses R8G8B8A8 to match the ARGB_8888 destination bitmap.
const int32_t bpp = gfx::BytesPerPixel(gfx::SurfaceFormat::R8G8B8A8);
RefPtr<gfx::DataSourceSurface> data =
AndroidWidgetUtils::GetDataSourceSurfaceForAndroidBitmap(
aSurface, nullptr, aSurface->GetSize().width * bpp);
if (!data) {
return false;
}
gfx::DataSourceSurface::ScopedMap srcMap(data,
gfx::DataSourceSurface::READ);
if (!srcMap.IsMapped()) {
return false;
}
JNIEnv* const env = jni::GetEnvForThread();
AndroidBitmapInfo info;
if (AndroidBitmap_getInfo(env, mBitmap.Get(), &info) < 0) {
return false;
}
MOZ_RELEASE_ASSERT(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888);
uint8_t* destBuf = nullptr;
if (AndroidBitmap_lockPixels(env, mBitmap.Get(),
reinterpret_cast<void**>(&destBuf)) < 0) {
return false;
}
auto unlock = MakeScopeExit(
[&]() { AndroidBitmap_unlockPixels(env, mBitmap.Get()); });
const gfx::IntSize tilePx = data->GetSize();
// Place each tile avoiding seams from fractional-scale rounding.
const int32_t col = (aTileCss.X() - mFullRect.X()) / kTileSize;
const int32_t row = (aTileCss.Y() - mFullRect.Y()) / kTileSize;
const int32_t destX = col * mSnapshotSize;
const int32_t destY = row * mSnapshotSize;
if (destX < 0 || destY < 0) {
return false;
}
const int32_t copyW = std::min(tilePx.width, int32_t(info.width) - destX);
const int32_t copyH = std::min(tilePx.height, int32_t(info.height) - destY);
if (copyW <= 0 || copyH <= 0) {
return false;
}
const uint8_t* srcRow = srcMap.GetData();
uint8_t* dstRow =
destBuf + size_t(destY) * info.stride + size_t(destX) * bpp;
for (int32_t y = 0; y < copyH; y++) {
memcpy(dstRow, srcRow, size_t(copyW) * bpp);
srcRow += srcMap.GetStride();
dstRow += info.stride;
}
return true;
}
java::GeckoResult::GlobalRef mResult;
RefPtr<dom::WindowGlobalParent> mWgp;
java::sdk::Bitmap::GlobalRef mBitmap;
gfx::IntRect mFullRect;
const float mScale;
int32_t mSnapshotSize = 0;
nsTArray<gfx::IntRect> mTiles;
};
void GeckoViewSupport::RequestFullScreenshot(
const java::GeckoSession::Window::LocalRef& inst,
jni::Object::Param aResult, jni::Object::Param aTarget, int32_t aX,
int32_t aY, int32_t aWidth, int32_t aHeight, float aRenderingScale) {
MOZ_ASSERT(NS_IsMainThread());
auto result =
java::GeckoResult::GlobalRef(java::GeckoResult::LocalRef(aResult));
auto target =
java::sdk::Bitmap::GlobalRef(java::sdk::Bitmap::LocalRef(aTarget));
if (!target) {
result->CompleteExceptionally(
java::sdk::IllegalArgumentException::New(
"Error requesting full screenshot - no target bitmap")
.Cast<jni::Throwable>());
GVS_LOG("Error requesting full screenshot - no target bitmap");
return;
}
if (aWidth <= 0 || aHeight <= 0) {
result->CompleteExceptionally(
java::sdk::IllegalArgumentException::New(
"Error requesting full screenshot - invalid dimensions")
.Cast<jni::Throwable>());
GVS_LOG("Error requesting full screenshot - invalid dimensions");
return;
}
RefPtr<CanonicalBrowsingContext> cbc = GetContentCanonicalBrowsingContext();
if (!cbc) {
result->CompleteExceptionally(
java::sdk::IllegalStateException::New(
"Error requesting full screenshot - could not retrieve canonical "
"browsing context")
.Cast<jni::Throwable>());
GVS_LOG(
"Error requesting full screenshot - could not retrieve canonical "
"browsing context");
return;
}
RefPtr<dom::WindowGlobalParent> wgp = cbc->GetCurrentWindowGlobal();
if (!wgp) {
result->CompleteExceptionally(
java::sdk::IllegalStateException::New(
"Error requesting full screenshot - could not retrieve current "
"window global")
.Cast<jni::Throwable>());
GVS_LOG(
"Error requesting full screenshot - could not retrieve current window "
"global");
return;
}
const gfx::IntRect srcRect(aX, aY, aWidth, aHeight);
MakeRefPtr<FullPageScreenshot>(std::move(result), std::move(wgp),
std::move(target), srcRect, aRenderingScale)
->Start();
}
void GeckoViewSupport::RequestContentMetrics(
const java::GeckoSession::Window::LocalRef& inst,
jni::Object::Param aResult, jni::Object::Param aMetrics) {
MOZ_ASSERT(NS_IsMainThread());
auto result =
java::GeckoResult::GlobalRef(java::GeckoResult::LocalRef(aResult));
auto metrics = java::GeckoSession::Window::ContentMetrics::GlobalRef(
java::GeckoSession::Window::ContentMetrics::LocalRef(aMetrics));
if (!metrics) {
result->CompleteExceptionally(
java::sdk::IllegalArgumentException::New(
"Error requesting content metrics - no metrics object")
.Cast<jni::Throwable>());
GVS_LOG("Error requesting content metrics - no metrics object");
return;
}
RefPtr<CanonicalBrowsingContext> cbc = GetContentCanonicalBrowsingContext();
if (!cbc) {
result->CompleteExceptionally(
java::sdk::IllegalStateException::New(
"Error requesting content metrics - could not retrieve canonical "
"browsing context")
.Cast<jni::Throwable>());
GVS_LOG(
"Error requesting content metrics - Could not retrieve canonical "
"browsing context");
return;
}
RefPtr<dom::WindowGlobalParent> wgp = cbc->GetCurrentWindowGlobal();
if (!wgp) {
result->CompleteExceptionally(
java::sdk::IllegalStateException::New(
"Error requesting content metrics - could not retrieve current "
"window global")
.Cast<jni::Throwable>());
GVS_LOG(
"Error requesting content metrics - Could not retrieve current window "
"global");
return;
}
wgp->SendGetContentMetrics()->Then(
GetMainThreadSerialEventTarget(), __func__,
[result, metrics](
const PWindowGlobalParent::GetContentMetricsPromise::ResolveValueType&
aResolved) {
const auto& [size, dpr] = aResolved;
metrics->Set(size.width, size.height, dpr);
result->Complete(metrics);
},
[result](mozilla::ipc::ResponseRejectReason) {
result->CompleteExceptionally(
java::sdk::IllegalStateException::New(
"Error requesting content metrics - failed to query content")
.Cast<jni::Throwable>());
GVS_LOG("Error requesting content metrics - failed to query content");
});
}
} // namespace widget } // namespace widget
} // namespace mozilla } // namespace mozilla