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;
|
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
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user