TEST_F(ImageQualityControllerTest, LowQualityFilterForLiveResize) { MockTimer* mockTimer = new MockTimer(controller(), &ImageQualityController::highQualityRepaintTimerFired); controller()->setTimer(mockTimer); setBodyInnerHTML("<img src='myimage'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); // Start a resize document().frame()->view()->willStartLiveResize(); EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(2, 2))); document().frame()->view()->willEndLiveResize(); // End of live resize, but timer has not fired. Therefore paint at non-low quality. EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(3, 3))); // Start another resize document().frame()->view()->willStartLiveResize(); EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(3, 3))); // While still in resize, expire the timer. document().frame()->view()->willEndLiveResize(); mockTimer->fire(); // End of live resize, and timer has fired. Therefore paint at non-low quality, even though the size has changed. EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(4, 4))); }
TEST_F(ImageQualityControllerTest, DontKickTheAnimationTimerWhenPaintingAtTheSameSize) { MockTimer* mockTimer = new MockTimer(controller(), &ImageQualityController::highQualityRepaintTimerFired); controller()->setTimer(mockTimer); setBodyInnerHTML("<img src='myimage'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); // Paint once. This will kick off a timer to see if we resize it during that timer's execution. EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(2, 2))); // Go into low-quality mode now that the size changed. EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(3, 3))); // Stay in low-quality mode since the size changed again. EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(4, 4))); mockTimer->stop(); EXPECT_FALSE(mockTimer->isActive()); // Painted at the same size, so even though timer is still executing, don't go to low quality. EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(4, 4))); // Check that the timer was not kicked. It should not have been, since the image was painted at the same size as last time. EXPECT_FALSE(mockTimer->isActive()); }
std::unique_ptr<Shape> ShapeOutsideInfo::createShapeForImage( StyleImage* styleImage, float shapeImageThreshold, WritingMode writingMode, float margin) const { const LayoutSize& imageSize = styleImage->imageSize(m_layoutBox, m_layoutBox.style()->effectiveZoom(), m_referenceBoxLogicalSize); const LayoutRect& marginRect = getShapeImageMarginRect(m_layoutBox, m_referenceBoxLogicalSize); const LayoutRect& imageRect = (m_layoutBox.isLayoutImage()) ? toLayoutImage(m_layoutBox).replacedContentRect() : LayoutRect(LayoutPoint(), imageSize); if (!isValidRasterShapeRect(marginRect) || !isValidRasterShapeRect(imageRect)) { m_layoutBox.document().addConsoleMessage( ConsoleMessage::create(RenderingMessageSource, ErrorMessageLevel, "The shape-outside image is too large.")); return Shape::createEmptyRasterShape(writingMode, margin); } ASSERT(!styleImage->isPendingImage()); RefPtr<Image> image = styleImage->image(m_layoutBox, flooredIntSize(imageSize), m_layoutBox.style()->effectiveZoom()); return Shape::createRasterShape(image.get(), shapeImageThreshold, imageRect, marginRect, writingMode, margin); }
void LayoutReplaced::computeIntrinsicSizingInfoForReplacedContent( LayoutReplaced* contentLayoutObject, IntrinsicSizingInfo& intrinsicSizingInfo) const { if (contentLayoutObject) { contentLayoutObject->computeIntrinsicSizingInfo(intrinsicSizingInfo); // Handle zoom & vertical writing modes here, as the embedded document // doesn't know about them. intrinsicSizingInfo.size.scale(style()->effectiveZoom()); if (isLayoutImage()) intrinsicSizingInfo.size.scale( toLayoutImage(this)->imageDevicePixelRatio()); // Update our intrinsic size to match what the content layoutObject has // computed, so that when we constrain the size below, the correct intrinsic // size will be obtained for comparison against min and max widths. if (!intrinsicSizingInfo.aspectRatio.isEmpty() && !intrinsicSizingInfo.size.isEmpty()) m_intrinsicSize = LayoutSize(intrinsicSizingInfo.size); if (!isHorizontalWritingMode()) intrinsicSizingInfo.transpose(); } else { computeIntrinsicSizingInfo(intrinsicSizingInfo); if (!intrinsicSizingInfo.aspectRatio.isEmpty() && !intrinsicSizingInfo.size.isEmpty()) m_intrinsicSize = LayoutSize(isHorizontalWritingMode() ? intrinsicSizingInfo.size : intrinsicSizingInfo.size.transposedSize()); } }
TEST_F(ImageQualityControllerTest, MediumQualityFilterForUnscaledImage) { setBodyInnerHTML("<img src='myimage'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(1, 1))); }
TEST_F(ImageQualityControllerTest, ImageMaybeAnimated) { setBodyInnerHTML("<img src='myimage'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageAnimated> testImage = adoptRef(new TestImageAnimated); EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), nullptr, LayoutSize())); }
TEST_F(ImageQualityControllerTest, LowQualityFilterForContrast) { setBodyInnerHTML("<img src='myimage' style='image-rendering: -webkit-optimize-contrast'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageWithContrast> testImage = adoptRef(new TestImageWithContrast); EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize())); }
LayoutSize LayoutImageResource::getImageSize(float multiplier, ImageResource::SizeType type) const { if (!m_cachedImage) return LayoutSize(); LayoutSize size = m_cachedImage->imageSizeForLayoutObject(m_renderer, multiplier, type); if (m_renderer && m_renderer->isLayoutImage()) size.scale(toLayoutImage(m_renderer)->imageDevicePixelRatio()); return size; }
LayoutSize LayoutImageResource::imageSize(float multiplier) const { if (!m_cachedImage) return LayoutSize(); LayoutSize size = m_cachedImage->imageSize(LayoutObject::shouldRespectImageOrientation(m_layoutObject), multiplier); if (m_layoutObject && m_layoutObject->isLayoutImage() && size.width() && size.height()) size.scale(toLayoutImage(m_layoutObject)->imageDevicePixelRatio()); return size; }
LayoutSize LayoutImageResource::getImageSize(float multiplier, ImageResource::SizeType type) const { if (!m_cachedImage) return LayoutSize(); LayoutSize size = m_cachedImage->imageSizeForLayoutObject(m_layoutObject, multiplier, type); if (m_layoutObject && m_layoutObject->isLayoutImage() && size.width() && size.height()) size.scale(toLayoutImage(m_layoutObject)->imageDevicePixelRatio()); return size; }
static ImageResourceContent* getImageResourceContent(Element* element) { // Attempt to pull ImageResourceContent from element ASSERT(element); LayoutObject* layoutObject = element->layoutObject(); if (!layoutObject || !layoutObject->isImage()) return 0; LayoutImage* image = toLayoutImage(layoutObject); if (image->cachedImage() && !image->cachedImage()->errorOccurred()) return image->cachedImage(); return 0; }
void HTMLAreaElement::setFocus(bool shouldBeFocused) { if (focused() == shouldBeFocused) return; HTMLAnchorElement::setFocus(shouldBeFocused); HTMLImageElement* imageElement = this->imageElement(); if (!imageElement) return; LayoutObject* layoutObject = imageElement->layoutObject(); if (!layoutObject || !layoutObject->isImage()) return; toLayoutImage(layoutObject)->areaElementFocusChanged(this); }
void LayoutReplaced::computeAspectRatioInformationForLayoutBox(LayoutBox* contentLayoutObject, FloatSize& constrainedSize, double& intrinsicRatio) const { FloatSize intrinsicSize; if (contentLayoutObject) { contentLayoutObject->computeIntrinsicRatioInformation(intrinsicSize, intrinsicRatio); // Handle zoom & vertical writing modes here, as the embedded document doesn't know about them. intrinsicSize.scale(style()->effectiveZoom()); if (isLayoutImage()) intrinsicSize.scale(toLayoutImage(this)->imageDevicePixelRatio()); // Update our intrinsic size to match what the content layoutObject has computed, so that when we // constrain the size below, the correct intrinsic size will be obtained for comparison against // min and max widths. if (intrinsicRatio && !intrinsicSize.isEmpty()) m_intrinsicSize = LayoutSize(intrinsicSize); if (!isHorizontalWritingMode()) { if (intrinsicRatio) intrinsicRatio = 1 / intrinsicRatio; intrinsicSize = intrinsicSize.transposedSize(); } } else { computeIntrinsicRatioInformation(intrinsicSize, intrinsicRatio); if (intrinsicRatio && !intrinsicSize.isEmpty()) m_intrinsicSize = LayoutSize(isHorizontalWritingMode() ? intrinsicSize : intrinsicSize.transposedSize()); } // Now constrain the intrinsic size along each axis according to minimum and maximum width/heights along the // opposite axis. So for example a maximum width that shrinks our width will result in the height we compute here // having to shrink in order to preserve the aspect ratio. Because we compute these values independently along // each axis, the final returned size may in fact not preserve the aspect ratio. // FIXME: In the long term, it might be better to just return this code more to the way it used to be before this // function was added, since all it has done is make the code more unclear. constrainedSize = intrinsicSize; if (intrinsicRatio && !intrinsicSize.isEmpty() && style()->logicalWidth().isAuto() && style()->logicalHeight().isAuto()) { // We can't multiply or divide by 'intrinsicRatio' here, it breaks tests, like fast/images/zoomed-img-size.html, which // can only be fixed once subpixel precision is available for things like intrinsicWidth/Height - which include zoom! constrainedSize.setWidth(LayoutBox::computeReplacedLogicalHeight() * intrinsicSize.width() / intrinsicSize.height()); constrainedSize.setHeight(LayoutBox::computeReplacedLogicalWidth() * intrinsicSize.height() / intrinsicSize.width()); } }
PassOwnPtr<Shape> ShapeOutsideInfo::createShapeForImage(StyleImage* styleImage, float shapeImageThreshold, WritingMode writingMode, float margin) const { const IntSize& imageSize = m_layoutBox.calculateImageIntrinsicDimensions(styleImage, roundedIntSize(m_referenceBoxLogicalSize), LayoutImage::ScaleByEffectiveZoom); styleImage->setContainerSizeForLayoutObject(&m_layoutBox, imageSize, m_layoutBox.style()->effectiveZoom()); const LayoutRect& marginRect = getShapeImageMarginRect(m_layoutBox, m_referenceBoxLogicalSize); const LayoutRect& imageRect = (m_layoutBox.isLayoutImage()) ? toLayoutImage(m_layoutBox).replacedContentRect() : LayoutRect(LayoutPoint(), LayoutSize(imageSize)); if (!isValidRasterShapeRect(marginRect) || !isValidRasterShapeRect(imageRect)) { m_layoutBox.document().addConsoleMessage(ConsoleMessage::create(RenderingMessageSource, ErrorMessageLevel, "The shape-outside image is too large.")); return Shape::createEmptyRasterShape(writingMode, margin); } ASSERT(!styleImage->isPendingImage()); RefPtr<Image> image = styleImage->image(const_cast<LayoutBox*>(&m_layoutBox), imageSize); return Shape::createRasterShape(image.get(), shapeImageThreshold, imageRect, marginRect, writingMode, margin); }
LayoutImageResource* ImageLoader::layoutImageResource() { LayoutObject* layoutObject = m_element->layoutObject(); if (!layoutObject) return 0; // We don't return style generated image because it doesn't belong to the // ImageLoader. See <https://bugs.webkit.org/show_bug.cgi?id=42840> if (layoutObject->isImage() && !static_cast<LayoutImage*>(layoutObject)->isGeneratedContent()) return toLayoutImage(layoutObject)->imageResource(); if (layoutObject->isSVGImage()) return toLayoutSVGImage(layoutObject)->imageResource(); if (layoutObject->isVideo()) return toLayoutVideo(layoutObject)->imageResource(); return 0; }
TEST_F(ImageQualityControllerTest, LowQualityFilterForResizingImage) { MockTimer* mockTimer = new MockTimer(controller(), &ImageQualityController::highQualityRepaintTimerFired); controller()->setTimer(mockTimer); setBodyInnerHTML("<img src='myimage'></img>"); LayoutImage* img = toLayoutImage(document().body()->firstChild()->layoutObject()); RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); OwnPtr<PaintController> paintController = PaintController::create(); GraphicsContext context(*paintController); // Paint once. This will kick off a timer to see if we resize it during that timer's execution. EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(2, 2))); // Go into low-quality mode now that the size changed. EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(3, 3))); // Stay in low-quality mode since the size changed again. EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(4, 4))); mockTimer->fire(); // The timer fired before painting at another size, so this doesn't count as animation. Therefore not painting at low quality. EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality(*img, testImage.get(), testImage.get(), LayoutSize(4, 4))); }
void ImageLoader::doUpdateFromElement(BypassMainWorldBehavior bypassBehavior, UpdateFromElementBehavior updateBehavior) { // FIXME: According to // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#the-img-element:the-img-element-55 // When "update image" is called due to environment changes and the load fails, onerror should not be called. // That is currently not the case. // // We don't need to call clearLoader here: Either we were called from the // task, or our caller updateFromElement cleared the task's loader (and set // m_pendingTask to null). m_pendingTask.clear(); // Make sure to only decrement the count when we exit this function OwnPtr<IncrementLoadEventDelayCount> loadDelayCounter; loadDelayCounter.swap(m_loadDelayCounter); Document& document = m_element->document(); if (!document.isActive()) return; AtomicString imageSourceURL = m_element->imageSourceURL(); KURL url = imageSourceToKURL(imageSourceURL); ResourcePtr<ImageResource> newImage = 0; RefPtrWillBeRawPtr<Element> protectElement(m_element.get()); if (!url.isNull()) { // Unlike raw <img>, we block mixed content inside of <picture> or <img srcset>. ResourceLoaderOptions resourceLoaderOptions = ResourceFetcher::defaultResourceOptions(); ResourceRequest resourceRequest(url); resourceRequest.setFetchCredentialsMode(WebURLRequest::FetchCredentialsModeSameOrigin); if (updateBehavior == UpdateForcedReload) { resourceRequest.setCachePolicy(ResourceRequestCachePolicy::ReloadBypassingCache); // ImageLoader defers the load of images when in an ImageDocument. Don't defer this load on a forced reload. m_loadingImageDocument = false; } if (isHTMLPictureElement(element()->parentNode()) || !element()->fastGetAttribute(HTMLNames::srcsetAttr).isNull()) resourceRequest.setRequestContext(WebURLRequest::RequestContextImageSet); FetchRequest request(resourceRequest, element()->localName(), resourceLoaderOptions); configureRequest(request, bypassBehavior, *m_element, document.clientHintsPreferences()); // Prevent the immediate creation of a ResourceLoader (and therefore a network // request) for ImageDocument loads. In this case, the image contents have already // been requested as a main resource and ImageDocumentParser will take care of // funneling the main resource bytes into the ImageResource. if (m_loadingImageDocument) { request.setDefer(FetchRequest::DeferredByClient); request.setContentSecurityCheck(DoNotCheckContentSecurityPolicy); } newImage = ImageResource::fetch(request, document.fetcher()); if (m_loadingImageDocument && newImage) newImage->setLoading(true); if (!newImage && !pageIsBeingDismissed(&document)) { crossSiteOrCSPViolationOccurred(imageSourceURL); dispatchErrorEvent(); } else { clearFailedLoadURL(); } } else { if (!imageSourceURL.isNull()) { // Fire an error event if the url string is not empty, but the KURL is. dispatchErrorEvent(); } noImageResourceToLoad(); } ImageResource* oldImage = m_image.get(); if (updateBehavior == UpdateSizeChanged && m_element->layoutObject() && m_element->layoutObject()->isImage() && newImage == oldImage) { toLayoutImage(m_element->layoutObject())->intrinsicSizeChanged(); } else { if (newImage != oldImage) sourceImageChanged(); if (m_hasPendingLoadEvent) { loadEventSender().cancelEvent(this); m_hasPendingLoadEvent = false; } // Cancel error events that belong to the previous load, which is now cancelled by changing the src attribute. // If newImage is null and m_hasPendingErrorEvent is true, we know the error event has been just posted by // this load and we should not cancel the event. // FIXME: If both previous load and this one got blocked with an error, we can receive one error event instead of two. if (m_hasPendingErrorEvent && newImage) { errorEventSender().cancelEvent(this); m_hasPendingErrorEvent = false; } m_image = newImage; m_hasPendingLoadEvent = newImage; m_imageComplete = !newImage; updateLayoutObject(); // If newImage exists and is cached, addClient() will result in the load event // being queued to fire. Ensure this happens after beforeload is dispatched. if (newImage) newImage->addClient(this); if (oldImage) oldImage->removeClient(this); } if (LayoutImageResource* imageResource = layoutImageResource()) imageResource->resetAnimation(); // Only consider updating the protection ref-count of the Element immediately before returning // from this function as doing so might result in the destruction of this ImageLoader. updatedHasPendingEvent(); }
void ImageLoader::doUpdateFromElement(BypassMainWorldBehavior bypassBehavior, UpdateFromElementBehavior updateBehavior, const KURL& url, ReferrerPolicy referrerPolicy) { // FIXME: According to // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#the-img-element:the-img-element-55 // When "update image" is called due to environment changes and the load // fails, onerror should not be called. That is currently not the case. // // We don't need to call clearLoader here: Either we were called from the // task, or our caller updateFromElement cleared the task's loader (and set // m_pendingTask to null). m_pendingTask.reset(); // Make sure to only decrement the count when we exit this function std::unique_ptr<IncrementLoadEventDelayCount> loadDelayCounter; loadDelayCounter.swap(m_loadDelayCounter); Document& document = m_element->document(); if (!document.isActive()) return; AtomicString imageSourceURL = m_element->imageSourceURL(); ImageResource* newImage = nullptr; if (!url.isNull()) { // Unlike raw <img>, we block mixed content inside of <picture> or // <img srcset>. ResourceLoaderOptions resourceLoaderOptions = ResourceFetcher::defaultResourceOptions(); ResourceRequest resourceRequest(url); if (updateBehavior == UpdateForcedReload) { resourceRequest.setCachePolicy(WebCachePolicy::BypassingCache); resourceRequest.setLoFiState(WebURLRequest::LoFiOff); } if (referrerPolicy != ReferrerPolicyDefault) { resourceRequest.setHTTPReferrer(SecurityPolicy::generateReferrer( referrerPolicy, url, document.outgoingReferrer())); } if (isHTMLPictureElement(element()->parentNode()) || !element()->fastGetAttribute(HTMLNames::srcsetAttr).isNull()) resourceRequest.setRequestContext(WebURLRequest::RequestContextImageSet); FetchRequest request(resourceRequest, element()->localName(), resourceLoaderOptions); configureRequest(request, bypassBehavior, *m_element, document.clientHintsPreferences()); newImage = ImageResource::fetch(request, document.fetcher()); if (!newImage && !pageIsBeingDismissed(&document)) { crossSiteOrCSPViolationOccurred(imageSourceURL); dispatchErrorEvent(); } else { clearFailedLoadURL(); } } else { if (!imageSourceURL.isNull()) { // Fire an error event if the url string is not empty, but the KURL is. dispatchErrorEvent(); } noImageResourceToLoad(); } ImageResource* oldImage = m_image.get(); if (updateBehavior == UpdateSizeChanged && m_element->layoutObject() && m_element->layoutObject()->isImage() && newImage == oldImage) { toLayoutImage(m_element->layoutObject())->intrinsicSizeChanged(); } else { if (m_hasPendingLoadEvent) { loadEventSender().cancelEvent(this); m_hasPendingLoadEvent = false; } // Cancel error events that belong to the previous load, which is now // cancelled by changing the src attribute. If newImage is null and // m_hasPendingErrorEvent is true, we know the error event has been just // posted by this load and we should not cancel the event. // FIXME: If both previous load and this one got blocked with an error, we // can receive one error event instead of two. if (m_hasPendingErrorEvent && newImage) { errorEventSender().cancelEvent(this); m_hasPendingErrorEvent = false; } m_image = newImage; m_hasPendingLoadEvent = newImage; m_imageComplete = !newImage; updateLayoutObject(); // If newImage exists and is cached, addObserver() will result in the load // event being queued to fire. Ensure this happens after beforeload is // dispatched. if (newImage) { newImage->addObserver(this); } if (oldImage) { oldImage->removeObserver(this); } } if (LayoutImageResource* imageResource = layoutImageResource()) imageResource->resetAnimation(); // Only consider updating the protection ref-count of the Element immediately // before returning from this function as doing so might result in the // destruction of this ImageLoader. updatedHasPendingEvent(); }