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:
committed by
csabou@mozilla.com
parent
afed2108b5
commit
aabe491b92
@@ -1444,6 +1444,22 @@ public class GeckoSession {
|
||||
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
|
||||
public Binder asBinder() {
|
||||
if (mBinder == null) {
|
||||
@@ -1527,6 +1543,20 @@ public class GeckoSession {
|
||||
@WrapForJNI(dispatchTo = "proxy")
|
||||
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")
|
||||
private synchronized void onReady(final @Nullable NativeQueue queue) {
|
||||
// onReady is called the first time the Gecko window is ready, with a null queue
|
||||
|
||||
@@ -116,6 +116,18 @@ class GeckoViewSupport final
|
||||
const java::GeckoSession::Window::LocalRef& inst,
|
||||
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.
|
||||
void PerformHapticFeedback(int32_t aEffect);
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#include "mozilla/dom/ContentChild.h"
|
||||
#include "mozilla/dom/ContentParent.h"
|
||||
#include "mozilla/dom/MouseEventBinding.h"
|
||||
#include "mozilla/dom/WindowGlobalParent.h"
|
||||
#include "mozilla/gfx/2D.h"
|
||||
#include "mozilla/gfx/DataSurfaceHelpers.h"
|
||||
#include "mozilla/gfx/Logging.h"
|
||||
@@ -2206,6 +2207,288 @@ void GeckoViewSupport::PerformHapticFeedback(int32_t 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 mozilla
|
||||
|
||||
|
||||
Reference in New Issue
Block a user