blob: 0be1e5fce9b81441a5415cbc8c0afca1ffc54465 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
#include "core/css/FontFaceSet.h"
#include "bindings/core/v8/Dictionary.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/core/v8/ScriptState.h"
#include "core/css/CSSFontSelector.h"
#include "core/css/CSSSegmentedFontFace.h"
#include "core/css/FontFaceCache.h"
#include "core/css/FontFaceSetLoadEvent.h"
#include "core/css/StylePropertySet.h"
#include "core/css/parser/CSSParser.h"
#include "core/css/resolver/StyleResolver.h"
#include "core/dom/Document.h"
#include "core/dom/StyleEngine.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/style/ComputedStyle.h"
#include "platform/Histogram.h"
namespace blink {
static const int defaultFontSize = 10;
static const char defaultFontFamily[] = "sans-serif";
class LoadFontPromiseResolver final
: public GarbageCollectedFinalized<LoadFontPromiseResolver>,
public FontFace::LoadFontCallback {
USING_GARBAGE_COLLECTED_MIXIN(LoadFontPromiseResolver);
public:
static LoadFontPromiseResolver* create(FontFaceArray faces,
ScriptState* scriptState) {
return new LoadFontPromiseResolver(faces, scriptState);
}
void loadFonts(ExecutionContext*);
ScriptPromise promise() { return m_resolver->promise(); }
void notifyLoaded(FontFace*) override;
void notifyError(FontFace*) override;
DECLARE_VIRTUAL_TRACE();
private:
LoadFontPromiseResolver(FontFaceArray faces, ScriptState* scriptState)
: m_numLoading(faces.size()),
m_errorOccured(false),
m_resolver(ScriptPromiseResolver::create(scriptState)) {
m_fontFaces.swap(faces);
}
HeapVector<Member<FontFace>> m_fontFaces;
int m_numLoading;
bool m_errorOccured;
Member<ScriptPromiseResolver> m_resolver;
};
void LoadFontPromiseResolver::loadFonts(ExecutionContext* context) {
if (!m_numLoading) {
m_resolver->resolve(m_fontFaces);
return;
}
for (size_t i = 0; i < m_fontFaces.size(); i++)
m_fontFaces[i]->loadWithCallback(this, context);
}
void LoadFontPromiseResolver::notifyLoaded(FontFace* fontFace) {
m_numLoading--;
if (m_numLoading || m_errorOccured)
return;
m_resolver->resolve(m_fontFaces);
}
void LoadFontPromiseResolver::notifyError(FontFace* fontFace) {
m_numLoading--;
if (!m_errorOccured) {
m_errorOccured = true;
m_resolver->reject(fontFace->error());
}
}
DEFINE_TRACE(LoadFontPromiseResolver) {
visitor->trace(m_fontFaces);
visitor->trace(m_resolver);
LoadFontCallback::trace(visitor);
}
FontFaceSet::FontFaceSet(Document& document)
: SuspendableObject(&document),
m_shouldFireLoadingEvent(false),
m_isLoading(false),
m_ready(
new ReadyProperty(getExecutionContext(), this, ReadyProperty::Ready)),
m_asyncRunner(AsyncMethodRunner<FontFaceSet>::create(
this,
&FontFaceSet::handlePendingEventsAndPromises)) {
suspendIfNeeded();
}
FontFaceSet::~FontFaceSet() {}
Document* FontFaceSet::document() const {
return toDocument(getExecutionContext());
}
bool FontFaceSet::inActiveDocumentContext() const {
ExecutionContext* context = getExecutionContext();
return context && toDocument(context)->isActive();
}
void FontFaceSet::addFontFacesToFontFaceCache(FontFaceCache* fontFaceCache,
CSSFontSelector* fontSelector) {
for (const auto& fontFace : m_nonCSSConnectedFaces)
fontFaceCache->addFontFace(fontSelector, fontFace, false);
}
const AtomicString& FontFaceSet::interfaceName() const {
return EventTargetNames::FontFaceSet;
}
ExecutionContext* FontFaceSet::getExecutionContext() const {
return SuspendableObject::getExecutionContext();
}
AtomicString FontFaceSet::status() const {
DEFINE_STATIC_LOCAL(AtomicString, loading, ("loading"));
DEFINE_STATIC_LOCAL(AtomicString, loaded, ("loaded"));
return m_isLoading ? loading : loaded;
}
void FontFaceSet::handlePendingEventsAndPromisesSoon() {
// m_asyncRunner will be automatically stopped on destruction.
m_asyncRunner->runAsync();
}
void FontFaceSet::didLayout() {
if (document()->frame()->isMainFrame() && m_loadingFonts.isEmpty())
m_histogram.record();
if (!shouldSignalReady())
return;
handlePendingEventsAndPromisesSoon();
}
bool FontFaceSet::shouldSignalReady() const {
if (!m_loadingFonts.isEmpty())
return false;
return m_isLoading || m_ready->getState() == ReadyProperty::Pending;
}
void FontFaceSet::handlePendingEventsAndPromises() {
fireLoadingEvent();
fireDoneEventIfPossible();
}
void FontFaceSet::fireLoadingEvent() {
if (m_shouldFireLoadingEvent) {
m_shouldFireLoadingEvent = false;
dispatchEvent(
FontFaceSetLoadEvent::createForFontFaces(EventTypeNames::loading));
}
}
void FontFaceSet::suspend() {
m_asyncRunner->suspend();
}
void FontFaceSet::resume() {
m_asyncRunner->resume();
}
void FontFaceSet::contextDestroyed() {
m_asyncRunner->stop();
}
void FontFaceSet::beginFontLoading(FontFace* fontFace) {
m_histogram.incrementCount();
addToLoadingFonts(fontFace);
}
void FontFaceSet::notifyLoaded(FontFace* fontFace) {
m_histogram.updateStatus(fontFace);
m_loadedFonts.push_back(fontFace);
removeFromLoadingFonts(fontFace);
}
void FontFaceSet::notifyError(FontFace* fontFace) {
m_histogram.updateStatus(fontFace);
m_failedFonts.push_back(fontFace);
removeFromLoadingFonts(fontFace);
}
size_t FontFaceSet::approximateBlankCharacterCount() const {
size_t count = 0;
for (auto& fontFace : m_loadingFonts)
count += fontFace->approximateBlankCharacterCount();
return count;
}
void FontFaceSet::addToLoadingFonts(FontFace* fontFace) {
if (!m_isLoading) {
m_isLoading = true;
m_shouldFireLoadingEvent = true;
if (m_ready->getState() != ReadyProperty::Pending)
m_ready->reset();
handlePendingEventsAndPromisesSoon();
}
m_loadingFonts.add(fontFace);
fontFace->addCallback(this);
}
void FontFaceSet::removeFromLoadingFonts(FontFace* fontFace) {
m_loadingFonts.remove(fontFace);
if (m_loadingFonts.isEmpty())
handlePendingEventsAndPromisesSoon();
}
ScriptPromise FontFaceSet::ready(ScriptState* scriptState) {
return m_ready->promise(scriptState->world());
}
FontFaceSet* FontFaceSet::addForBinding(ScriptState*,
FontFace* fontFace,
ExceptionState&) {
ASSERT(fontFace);
if (!inActiveDocumentContext())
return this;
if (m_nonCSSConnectedFaces.contains(fontFace))
return this;
if (isCSSConnectedFontFace(fontFace))
return this;
CSSFontSelector* fontSelector = document()->styleEngine().fontSelector();
m_nonCSSConnectedFaces.add(fontFace);
fontSelector->fontFaceCache()->addFontFace(fontSelector, fontFace, false);
if (fontFace->loadStatus() == FontFace::Loading)
addToLoadingFonts(fontFace);
fontSelector->fontFaceInvalidated();
return this;
}
void FontFaceSet::clearForBinding(ScriptState*, ExceptionState&) {
if (!inActiveDocumentContext() || m_nonCSSConnectedFaces.isEmpty())
return;
CSSFontSelector* fontSelector = document()->styleEngine().fontSelector();
FontFaceCache* fontFaceCache = fontSelector->fontFaceCache();
for (const auto& fontFace : m_nonCSSConnectedFaces) {
fontFaceCache->removeFontFace(fontFace.get(), false);
if (fontFace->loadStatus() == FontFace::Loading)
removeFromLoadingFonts(fontFace);
}
m_nonCSSConnectedFaces.clear();
fontSelector->fontFaceInvalidated();
}
bool FontFaceSet::deleteForBinding(ScriptState*,
FontFace* fontFace,
ExceptionState&) {
ASSERT(fontFace);
if (!inActiveDocumentContext())
return false;
HeapListHashSet<Member<FontFace>>::iterator it =
m_nonCSSConnectedFaces.find(fontFace);
if (it != m_nonCSSConnectedFaces.end()) {
m_nonCSSConnectedFaces.remove(it);
CSSFontSelector* fontSelector = document()->styleEngine().fontSelector();
fontSelector->fontFaceCache()->removeFontFace(fontFace, false);
if (fontFace->loadStatus() == FontFace::Loading)
removeFromLoadingFonts(fontFace);
fontSelector->fontFaceInvalidated();
return true;
}
return false;
}
bool FontFaceSet::hasForBinding(ScriptState*,
FontFace* fontFace,
ExceptionState&) const {
ASSERT(fontFace);
if (!inActiveDocumentContext())
return false;
return m_nonCSSConnectedFaces.contains(fontFace) ||
isCSSConnectedFontFace(fontFace);
}
const HeapListHashSet<Member<FontFace>>& FontFaceSet::cssConnectedFontFaceList()
const {
Document* document = this->document();
document->updateActiveStyle();
return document->styleEngine()
.fontSelector()
->fontFaceCache()
->cssConnectedFontFaces();
}
bool FontFaceSet::isCSSConnectedFontFace(FontFace* fontFace) const {
return cssConnectedFontFaceList().contains(fontFace);
}
size_t FontFaceSet::size() const {
if (!inActiveDocumentContext())
return m_nonCSSConnectedFaces.size();
return cssConnectedFontFaceList().size() + m_nonCSSConnectedFaces.size();
}
void FontFaceSet::fireDoneEventIfPossible() {
if (m_shouldFireLoadingEvent)
return;
if (!shouldSignalReady())
return;
Document* d = document();
if (!d)
return;
// If the layout was invalidated in between when we thought layout
// was updated and when we're ready to fire the event, just wait
// until after the next layout before firing events.
if (!d->view() || d->view()->needsLayout())
return;
if (m_isLoading) {
FontFaceSetLoadEvent* doneEvent = nullptr;
FontFaceSetLoadEvent* errorEvent = nullptr;
doneEvent = FontFaceSetLoadEvent::createForFontFaces(
EventTypeNames::loadingdone, m_loadedFonts);
m_loadedFonts.clear();
if (!m_failedFonts.isEmpty()) {
errorEvent = FontFaceSetLoadEvent::createForFontFaces(
EventTypeNames::loadingerror, m_failedFonts);
m_failedFonts.clear();
}
m_isLoading = false;
dispatchEvent(doneEvent);
if (errorEvent)
dispatchEvent(errorEvent);
}
if (m_ready->getState() == ReadyProperty::Pending)
m_ready->resolve(this);
}
ScriptPromise FontFaceSet::load(ScriptState* scriptState,
const String& fontString,
const String& text) {
if (!inActiveDocumentContext())
return ScriptPromise();
Font font;
if (!resolveFontStyle(fontString, font)) {
ScriptPromiseResolver* resolver =
ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
resolver->reject(DOMException::create(
SyntaxError, "Could not resolve '" + fontString + "' as a font."));
return promise;
}
FontFaceCache* fontFaceCache =
document()->styleEngine().fontSelector()->fontFaceCache();
FontFaceArray faces;
for (const FontFamily* f = &font.getFontDescription().family(); f;
f = f->next()) {
CSSSegmentedFontFace* segmentedFontFace =
fontFaceCache->get(font.getFontDescription(), f->family());
if (segmentedFontFace)
segmentedFontFace->match(text, faces);
}
LoadFontPromiseResolver* resolver =
LoadFontPromiseResolver::create(faces, scriptState);
ScriptPromise promise = resolver->promise();
// After this, resolver->promise() may return null.
resolver->loadFonts(getExecutionContext());
return promise;
}
bool FontFaceSet::check(const String& fontString,
const String& text,
ExceptionState& exceptionState) {
if (!inActiveDocumentContext())
return false;
Font font;
if (!resolveFontStyle(fontString, font)) {
exceptionState.throwDOMException(
SyntaxError, "Could not resolve '" + fontString + "' as a font.");
return false;
}
CSSFontSelector* fontSelector = document()->styleEngine().fontSelector();
FontFaceCache* fontFaceCache = fontSelector->fontFaceCache();
bool hasLoadedFaces = false;
for (const FontFamily* f = &font.getFontDescription().family(); f;
f = f->next()) {
CSSSegmentedFontFace* face =
fontFaceCache->get(font.getFontDescription(), f->family());
if (face) {
if (!face->checkFont(text))
return false;
hasLoadedFaces = true;
}
}
if (hasLoadedFaces)
return true;
for (const FontFamily* f = &font.getFontDescription().family(); f;
f = f->next()) {
if (fontSelector->isPlatformFontAvailable(font.getFontDescription(),
f->family()))
return true;
}
return false;
}
bool FontFaceSet::resolveFontStyle(const String& fontString, Font& font) {
if (fontString.isEmpty())
return false;
// Interpret fontString in the same way as the 'font' attribute of
// CanvasRenderingContext2D.
MutableStylePropertySet* parsedStyle =
MutableStylePropertySet::create(HTMLStandardMode);
CSSParser::parseValue(parsedStyle, CSSPropertyFont, fontString, true, 0);
if (parsedStyle->isEmpty())
return false;
String fontValue = parsedStyle->getPropertyValue(CSSPropertyFont);
if (fontValue == "inherit" || fontValue == "initial")
return false;
RefPtr<ComputedStyle> style = ComputedStyle::create();
FontFamily fontFamily;
fontFamily.setFamily(defaultFontFamily);
FontDescription defaultFontDescription;
defaultFontDescription.setFamily(fontFamily);
defaultFontDescription.setSpecifiedSize(defaultFontSize);
defaultFontDescription.setComputedSize(defaultFontSize);
style->setFontDescription(defaultFontDescription);
style->font().update(style->font().getFontSelector());
document()->updateActiveStyle();
document()->ensureStyleResolver().computeFont(style.get(), *parsedStyle);
font = style->font();
font.update(document()->styleEngine().fontSelector());
return true;
}
void FontFaceSet::FontLoadHistogram::updateStatus(FontFace* fontFace) {
if (m_status == Reported)
return;
if (fontFace->hadBlankText())
m_status = HadBlankText;
else if (m_status == NoWebFonts)
m_status = DidNotHaveBlankText;
}
void FontFaceSet::FontLoadHistogram::record() {
if (!m_recorded) {
m_recorded = true;
DEFINE_STATIC_LOCAL(CustomCountHistogram, webFontsInPageHistogram,
("WebFont.WebFontsInPage", 1, 100, 50));
webFontsInPageHistogram.count(m_count);
}
if (m_status == HadBlankText || m_status == DidNotHaveBlankText) {
DEFINE_STATIC_LOCAL(EnumerationHistogram, hadBlankTextHistogram,
("WebFont.HadBlankText", 2));
hadBlankTextHistogram.count(m_status == HadBlankText ? 1 : 0);
m_status = Reported;
}
}
FontFaceSet* FontFaceSet::from(Document& document) {
FontFaceSet* fonts = static_cast<FontFaceSet*>(
Supplement<Document>::from(document, supplementName()));
if (!fonts) {
fonts = FontFaceSet::create(document);
Supplement<Document>::provideTo(document, supplementName(), fonts);
}
return fonts;
}
void FontFaceSet::didLayout(Document& document) {
if (FontFaceSet* fonts = static_cast<FontFaceSet*>(
Supplement<Document>::from(document, supplementName())))
fonts->didLayout();
}
size_t FontFaceSet::approximateBlankCharacterCount(Document& document) {
if (FontFaceSet* fonts = static_cast<FontFaceSet*>(
Supplement<Document>::from(document, supplementName())))
return fonts->approximateBlankCharacterCount();
return 0;
}
FontFaceSetIterable::IterationSource* FontFaceSet::startIteration(
ScriptState*,
ExceptionState&) {
// Setlike should iterate each item in insertion order, and items should
// be keep on up to date. But since blink does not have a way to hook up CSS
// modification, take a snapshot here, and make it ordered as follows.
HeapVector<Member<FontFace>> fontFaces;
if (inActiveDocumentContext()) {
const HeapListHashSet<Member<FontFace>>& cssConnectedFaces =
cssConnectedFontFaceList();
fontFaces.reserveInitialCapacity(cssConnectedFaces.size() +
m_nonCSSConnectedFaces.size());
for (const auto& fontFace : cssConnectedFaces)
fontFaces.push_back(fontFace);
for (const auto& fontFace : m_nonCSSConnectedFaces)
fontFaces.push_back(fontFace);
}
return new IterationSource(fontFaces);
}
bool FontFaceSet::IterationSource::next(ScriptState*,
Member<FontFace>& key,
Member<FontFace>& value,
ExceptionState&) {
if (m_fontFaces.size() <= m_index)
return false;
key = value = m_fontFaces[m_index++];
return true;
}
DEFINE_TRACE(FontFaceSet) {
visitor->trace(m_ready);
visitor->trace(m_loadingFonts);
visitor->trace(m_loadedFonts);
visitor->trace(m_failedFonts);
visitor->trace(m_nonCSSConnectedFaces);
visitor->trace(m_asyncRunner);
EventTargetWithInlineData::trace(visitor);
Supplement<Document>::trace(visitor);
SuspendableObject::trace(visitor);
FontFace::LoadFontCallback::trace(visitor);
}
} // namespace blink