blob: 241ac7cd5075f7965115e0e04510165fc75d4512 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/shapedetection/ShapeDetector.h"
#include "core/dom/DOMException.h"
#include "core/dom/DOMRect.h"
#include "core/dom/Document.h"
#include "core/frame/ImageBitmap.h"
#include "core/frame/LocalFrame.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLVideoElement.h"
#include "core/html/ImageData.h"
#include "core/loader/resource/ImageResourceContent.h"
#include "platform/graphics/Image.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "wtf/CheckedNumeric.h"
namespace blink {
namespace {
mojo::ScopedSharedBufferHandle getSharedBufferOnData(
ScriptPromiseResolver* resolver,
uint8_t* data,
int size) {
DCHECK(data);
DCHECK(size);
ScriptPromise promise = resolver->promise();
mojo::ScopedSharedBufferHandle sharedBufferHandle =
mojo::SharedBufferHandle::Create(size);
if (!sharedBufferHandle->is_valid()) {
resolver->reject(
DOMException::create(InvalidStateError, "Internal allocation error"));
return sharedBufferHandle;
}
const mojo::ScopedSharedBufferMapping mappedBuffer =
sharedBufferHandle->Map(size);
DCHECK(mappedBuffer.get());
memcpy(mappedBuffer.get(), data, size);
return sharedBufferHandle;
}
} // anonymous namespace
ShapeDetector::ShapeDetector(LocalFrame& frame) {
DCHECK(frame.interfaceProvider());
}
ScriptPromise ShapeDetector::detect(ScriptState* scriptState,
const ImageBitmapSourceUnion& imageSource) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
// ImageDatas cannot be tainted by definition.
if (imageSource.isImageData())
return detectShapesOnImageData(resolver, imageSource.getAsImageData());
CanvasImageSource* canvasImageSource;
if (imageSource.isHTMLImageElement()) {
canvasImageSource = imageSource.getAsHTMLImageElement();
} else if (imageSource.isImageBitmap()) {
canvasImageSource = imageSource.getAsImageBitmap();
} else if (imageSource.isHTMLVideoElement()) {
canvasImageSource = imageSource.getAsHTMLVideoElement();
} else if (imageSource.isHTMLCanvasElement()) {
canvasImageSource = imageSource.getAsHTMLCanvasElement();
} else if (imageSource.isOffscreenCanvas()) {
canvasImageSource = imageSource.getAsOffscreenCanvas();
} else {
NOTREACHED() << "Unsupported CanvasImageSource";
resolver->reject(
DOMException::create(NotSupportedError, "Unsupported source."));
return promise;
}
if (canvasImageSource->wouldTaintOrigin(
scriptState->getExecutionContext()->getSecurityOrigin())) {
resolver->reject(
DOMException::create(SecurityError, "Source would taint origin."));
return promise;
}
if (imageSource.isHTMLImageElement()) {
return detectShapesOnImageElement(resolver,
imageSource.getAsHTMLImageElement());
}
// TODO(mcasas): Check if |video| is actually playing a MediaStream by using
// HTMLMediaElement::isMediaStreamURL(video->currentSrc().getString()); if
// there is a local WebCam associated, there might be sophisticated ways to
// detect faces on it. Until then, treat as a normal <video> element.
const FloatSize size(canvasImageSource->sourceWidth(),
canvasImageSource->sourceHeight());
SourceImageStatus sourceImageStatus = InvalidSourceImageStatus;
RefPtr<Image> image = canvasImageSource->getSourceImageForCanvas(
&sourceImageStatus, PreferNoAcceleration, SnapshotReasonDrawImage, size);
if (!image || sourceImageStatus != NormalSourceImageStatus) {
resolver->reject(
DOMException::create(InvalidStateError, "Invalid element or state."));
return promise;
}
SkPixmap pixmap;
RefPtr<Uint8Array> pixelData;
uint8_t* pixelDataPtr = nullptr;
WTF::CheckedNumeric<int> allocationSize = 0;
// TODO(ccameron): ShapeDetector can ignore color conversion.
sk_sp<SkImage> skImage =
image->imageForCurrentFrame(ColorBehavior::transformToGlobalTarget());
// Use |skImage|'s pixels if it has direct access to them.
if (skImage->peekPixels(&pixmap)) {
pixelDataPtr = static_cast<uint8_t*>(pixmap.writable_addr());
allocationSize = pixmap.getSafeSize();
} else if (imageSource.isImageBitmap()) {
ImageBitmap* imageBitmap = imageSource.getAsImageBitmap();
pixelData = imageBitmap->copyBitmapData(imageBitmap->isPremultiplied()
? PremultiplyAlpha
: DontPremultiplyAlpha,
N32ColorType);
pixelDataPtr = pixelData->data();
allocationSize = imageBitmap->size().area() * 4 /* bytes per pixel */;
} else {
// TODO(mcasas): retrieve the pixels from elsewhere.
NOTREACHED();
resolver->reject(DOMException::create(
InvalidStateError, "Failed to get pixels for current frame."));
return promise;
}
mojo::ScopedSharedBufferHandle sharedBufferHandle = getSharedBufferOnData(
resolver, pixelDataPtr, allocationSize.ValueOrDefault(0));
if (!sharedBufferHandle->is_valid())
return promise;
return doDetect(resolver, std::move(sharedBufferHandle), image->width(),
image->height());
}
ScriptPromise ShapeDetector::detectShapesOnImageData(
ScriptPromiseResolver* resolver,
ImageData* imageData) {
ScriptPromise promise = resolver->promise();
uint8_t* const data = imageData->data()->data();
WTF::CheckedNumeric<int> allocationSize = imageData->size().area() * 4;
mojo::ScopedSharedBufferHandle sharedBufferHandle =
getSharedBufferOnData(resolver, data, allocationSize.ValueOrDefault(0));
if (!sharedBufferHandle->is_valid())
return promise;
return doDetect(resolver, std::move(sharedBufferHandle), imageData->width(),
imageData->height());
}
ScriptPromise ShapeDetector::detectShapesOnImageElement(
ScriptPromiseResolver* resolver,
const HTMLImageElement* img) {
ScriptPromise promise = resolver->promise();
// TODO(mcasas): reconsider this resolve(), https://crbug.com/674306.
if (img->bitmapSourceSize().isZero()) {
resolver->resolve(HeapVector<Member<DOMRect>>());
return promise;
}
ImageResourceContent* const imageResource = img->cachedImage();
if (!imageResource || imageResource->errorOccurred()) {
resolver->reject(DOMException::create(
InvalidStateError, "Failed to load or decode HTMLImageElement."));
return promise;
}
Image* const blinkImage = imageResource->getImage();
if (!blinkImage) {
resolver->reject(DOMException::create(
InvalidStateError, "Failed to get image from resource."));
return promise;
}
// TODO(ccameron): ShapeDetector can ignore color conversion.
const sk_sp<SkImage> image = blinkImage->imageForCurrentFrame(
ColorBehavior::transformToGlobalTarget());
DCHECK_EQ(img->naturalWidth(), static_cast<unsigned>(image->width()));
DCHECK_EQ(img->naturalHeight(), static_cast<unsigned>(image->height()));
if (!image) {
resolver->reject(DOMException::create(
InvalidStateError, "Failed to get image from current frame."));
return promise;
}
const SkImageInfo skiaInfo =
SkImageInfo::MakeN32(image->width(), image->height(), image->alphaType());
const uint32_t allocationSize = skiaInfo.getSafeSize(skiaInfo.minRowBytes());
mojo::ScopedSharedBufferHandle sharedBufferHandle =
mojo::SharedBufferHandle::Create(allocationSize);
if (!sharedBufferHandle.is_valid()) {
DLOG(ERROR) << "Requested allocation : " << allocationSize
<< "B, larger than |mojo::edk::kMaxSharedBufferSize| == 16MB ";
// TODO(xianglu): For now we reject the promise if the image is too large.
// But consider resizing the image to remove restriction on the user side.
// Also, add LayoutTests for this case later.
resolver->reject(
DOMException::create(InvalidStateError, "Image exceeds size limit."));
return promise;
}
const mojo::ScopedSharedBufferMapping mappedBuffer =
sharedBufferHandle->Map(allocationSize);
const SkPixmap pixmap(skiaInfo, mappedBuffer.get(), skiaInfo.minRowBytes());
if (!image->readPixels(pixmap, 0, 0)) {
resolver->reject(DOMException::create(
InvalidStateError,
"Failed to read pixels: Unable to decompress or unsupported format."));
return promise;
}
return doDetect(resolver, std::move(sharedBufferHandle), img->naturalWidth(),
img->naturalHeight());
}
} // namespace blink