Implement FontLoader interface

https://bugs.webkit.org/show_bug.cgi?id=98395

Patch by Kunihiko Sakamoto <ksakamoto@chromium.org> on 2013-03-13
Reviewed by Eric Seidel.

Source/WebCore:

This patch implements the FontLoader interface defined in CSS Font
Load Events Module Level 3.[1] It adds fontloader attribute to
Document, and adds two interfaces, FontLoader and
CSSFontFaceLoadEvent to WebCore.

[1]: http://dev.w3.org/csswg/css-font-load-events-3/

This feature is guarded by FONT_LOAD_EVENTS compiler flag and
RuntimeEnabledFeatures::fontLoadEventsEnabled() runtime flag.

Tests: fast/css/fontloader-download-error.html
       fast/css/fontloader-events.html
       fast/css/fontloader-loadingdone.html
       fast/css/fontloader-multiple-faces-download-error.html
       fast/css/fontloader-multiple-faces.html
       fast/css/fontloader-multiple-families.html
       http/tests/webfont/fontloader-loading-attribute.html

* CMakeLists.txt: Adding FontLoader/CSSFontFaceLoadEvent files.
* DerivedSources.cpp: Ditto.
* DerivedSources.make: Ditto.
* DerivedSources.pri: Ditto.
* GNUmakefile.list.am: Ditto.
* Target.pri: Ditto.
* WebCore.gypi: Ditto.
* WebCore.vcproj/WebCore.vcproj: Ditto.
* WebCore.vcxproj/WebCore.vcxproj: Ditto.
* WebCore.vcxproj/WebCore.vcxproj.filters: Ditto.
* WebCore.xcodeproj/project.pbxproj: Ditto.
* bindings/js/JSDictionary.cpp:
(WebCore::JSDictionary::convertValue): Add convertValue() for CSSFontFaceRule, DOMError, and VoidCallback.
* bindings/js/JSDictionary.h: Ditto.
* bindings/v8/Dictionary.cpp: Add get() for CSSFontFaceRule, DOMError, and VoidCallback.
(WebCore::Dictionary::get):
* bindings/v8/Dictionary.h: Ditto.
(Dictionary):
* css/CSSFontFace.cpp: Notifies FontLoader as load progresses.
(WebCore::CSSFontFace::fontLoaded):
(WebCore::CSSFontFace::getFontData):
(WebCore::CSSFontFace::notifyFontLoader):
(WebCore::CSSFontFace::notifyLoadingDone):
* css/CSSFontFace.h: Add two member variables. m_rule stores CSSFontFaceRule for the fontface. m_loadState holds current state of the fontface in terms of FontLoader.
(WebCore::CSSFontFace::create): Takes additional parameter of type CSSFontFaceRule.
(WebCore::CSSFontFace::loadState):
(WebCore::CSSFontFace::CSSFontFace):
* css/CSSFontFaceLoadEvent.cpp: Added.
(WebCore::CSSFontFaceLoadEvent::CSSFontFaceLoadEvent):
(WebCore::CSSFontFaceLoadEvent::~CSSFontFaceLoadEvent):
(WebCore::CSSFontFaceLoadEvent::interfaceName):
* css/CSSFontFaceLoadEvent.h: Added.
(CSSFontFaceLoadEventInit):
(CSSFontFaceLoadEvent):
(WebCore::CSSFontFaceLoadEvent::create):
(WebCore::CSSFontFaceLoadEvent::createForFontFaceRule):
(WebCore::CSSFontFaceLoadEvent::createForError):
(WebCore::CSSFontFaceLoadEvent::fontface):
(WebCore::CSSFontFaceLoadEvent::error):
* css/CSSFontFaceLoadEvent.idl: Added.
* css/CSSFontFaceRule.idl: Add JSGenerateToJSObject and JSGenerateToNativeObject as JSC binding needs them.
* css/CSSFontFaceSource.cpp:
(WebCore::CSSFontFaceSource::isDecodeError): Added.
(WebCore::CSSFontFaceSource::ensureFontData): Added.
* css/CSSFontFaceSource.h:
(CSSFontFaceSource):
* css/CSSFontSelector.cpp:
(WebCore::CSSFontSelector::addFontFaceRule): Pass a CSSFontFaceRule to CSSFontFace::create().
(WebCore::CSSFontSelector::getFontData): Moved logic for creating a CSSSegmentedFontFace to a helper function getFontFace().
(WebCore::CSSFontSelector::getFontFace): Added.
* css/CSSFontSelector.h:
* css/CSSSegmentedFontFace.cpp:
(WebCore::CSSSegmentedFontFace::fontLoaded): Fires callbacks when load is completed.
(WebCore::CSSSegmentedFontFace::isLoading): Returns true if any fonts are still loading.
(WebCore::CSSSegmentedFontFace::checkFont): Returns true if all fonts are loaded.
(WebCore::CSSSegmentedFontFace::loadFont): Starts load by calling getFontData().
* css/CSSSegmentedFontFace.h:
(CSSSegmentedFontFace): Declares new functions and a vector to store callbacks.
(LoadFontCallback):
(WebCore::CSSSegmentedFontFace::LoadFontCallback::~LoadFontCallback):
* css/FontLoader.cpp: Added.
(LoadFontCallback): Holds callback functions of FontLoader.loadFont().
(WebCore::LoadFontCallback::create):
(WebCore::LoadFontCallback::createFromParams):
(WebCore::LoadFontCallback::LoadFontCallback):
(WebCore::LoadFontCallback::notifyLoaded):
(WebCore::LoadFontCallback::notifyError):
(WebCore::FontLoader::FontLoader):
(WebCore::FontLoader::~FontLoader):
(WebCore::FontLoader::eventTargetData):
(WebCore::FontLoader::ensureEventTargetData):
(WebCore::FontLoader::interfaceName):
(WebCore::FontLoader::scriptExecutionContext):
(WebCore::FontLoader::didLayout): Fires pending events and callbacks. This is called when layout have completed.
(WebCore::FontLoader::scheduleEvent): Defers event dispatching until layout completes.
(WebCore::FontLoader::firePendingEvents): Dispatches pending events.
(WebCore::FontLoader::beginFontLoading): Schedules loading/loadstart events. This is called from CSSFontFace.
(WebCore::FontLoader::fontLoaded): Schedules load/loadingdone events. This is called from CSSFontFace.
(WebCore::FontLoader::loadError): Schedules error/loadingdone events. This is called from CSSFontFace.
(WebCore::FontLoader::notifyWhenFontsReady): Implements fontloader.notifyWhenFontsReady().
(WebCore::FontLoader::loadingDone): Fires callbacks of notifyWhenFontsReady.
(WebCore::FontLoader::loadFont): Implements fontloader.loadFont().
(WebCore::FontLoader::checkFont): Implements fontloader.checkFont().
(WebCore::FontLoader::resolveFontStyle): Parses the given font parameter using the syntax of CSS 'font' property and creates Font object. The logic is taken from CanvasRenderingContext2D::setFont().
* css/FontLoader.h: Added.
(FontLoader):
(WebCore::FontLoader::create):
(WebCore::FontLoader::loading):
(WebCore::FontLoader::document):
(WebCore::FontLoader::refEventTarget):
(WebCore::FontLoader::derefEventTarget):
* css/FontLoader.idl: Added.
* dom/Document.cpp:
(WebCore::Document::Document):
(WebCore::Document::fontloader): Added.
* dom/Document.h:
(Document): Add m_fontloader and fontloader().
* dom/Document.idl: Add fontloader attribute.
* dom/EventNames.h: Add loading and loadingdone events.
* dom/EventNames.in: Add CSSFontFaceLoadEvent.
* dom/EventTargetFactory.in: Add FontLoader.
* page/FrameView.cpp:
(WebCore::FrameView::performPostLayoutTasks): Calls FontLoader::didLayout().

Tools:

Enable FontLoadEvents runtime flag for TestShell.

* DumpRenderTree/chromium/TestRunner/src/TestInterfaces.cpp:
(WebTestRunner::TestInterfaces::TestInterfaces):

LayoutTests:

Add tests for document.fontloader. Since the feature is currently enabled
only for chromium, the tests are expected to fail on the other ports.

* fast/css/fontloader-download-error-expected.txt: Added.
* fast/css/fontloader-download-error.html: Added.
* fast/css/fontloader-events-expected.txt: Added.
* fast/css/fontloader-events.html: Added.
* fast/css/fontloader-loadingdone-expected.txt: Added.
* fast/css/fontloader-loadingdone.html: Added.
* fast/css/fontloader-multiple-faces-download-error-expected.txt: Added.
* fast/css/fontloader-multiple-faces-download-error.html: Added.
* fast/css/fontloader-multiple-faces-expected.txt: Added.
* fast/css/fontloader-multiple-faces.html: Added.
* fast/css/fontloader-multiple-families-expected.txt: Added.
* fast/css/fontloader-multiple-families.html: Added.
* http/tests/webfont/fontloader-loading-attribute-expected.txt: Added.
* http/tests/webfont/fontloader-loading-attribute.html: Added.
* platform/chromium/fast/css/fontloader-download-error-expected.txt: Added.
* platform/chromium/fast/css/fontloader-events-expected.txt: Added.
* platform/chromium/fast/css/fontloader-loadingdone-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-faces-download-error-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-faces-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-families-expected.txt: Added.
* platform/chromium/http/tests/webfont/fontloader-loading-attribute-expected.txt: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@145787 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent d0af6015
2013-03-13 Kunihiko Sakamoto <ksakamoto@chromium.org>
Implement FontLoader interface
https://bugs.webkit.org/show_bug.cgi?id=98395
Reviewed by Eric Seidel.
Add tests for document.fontloader. Since the feature is currently enabled
only for chromium, the tests are expected to fail on the other ports.
* fast/css/fontloader-download-error-expected.txt: Added.
* fast/css/fontloader-download-error.html: Added.
* fast/css/fontloader-events-expected.txt: Added.
* fast/css/fontloader-events.html: Added.
* fast/css/fontloader-loadingdone-expected.txt: Added.
* fast/css/fontloader-loadingdone.html: Added.
* fast/css/fontloader-multiple-faces-download-error-expected.txt: Added.
* fast/css/fontloader-multiple-faces-download-error.html: Added.
* fast/css/fontloader-multiple-faces-expected.txt: Added.
* fast/css/fontloader-multiple-faces.html: Added.
* fast/css/fontloader-multiple-families-expected.txt: Added.
* fast/css/fontloader-multiple-families.html: Added.
* http/tests/webfont/fontloader-loading-attribute-expected.txt: Added.
* http/tests/webfont/fontloader-loading-attribute.html: Added.
* platform/chromium/fast/css/fontloader-download-error-expected.txt: Added.
* platform/chromium/fast/css/fontloader-events-expected.txt: Added.
* platform/chromium/fast/css/fontloader-loadingdone-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-faces-download-error-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-faces-expected.txt: Added.
* platform/chromium/fast/css/fontloader-multiple-families-expected.txt: Added.
* platform/chromium/http/tests/webfont/fontloader-loading-attribute-expected.txt: Added.
2013-03-13 Antti Koivisto <antti@apple.com>
Compute image background size when testing for background visibility
Test download error cases. "load" or "error" events should be fired exactly once for each @font-face rule.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
font1
font2
font3
font4
font5
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
/* Test 1: Invalid font data */
@font-face {
font-family: myfont1;
src: url('resources/invalidfont.png') format(opentype);
}
/* Test 2: Download error */
@font-face {
font-family: myfont2;
src: url('resources/DownLoadErrorAhem.otf');
}
/* Test 3: Empty data url */
@font-face {
font-family: myfont3;
src: url(data:application/x-truetype-font,) format(truetype);
}
/* Test 4: Download error followed by existing local font */
@font-face {
font-family: myfont4;
src: url('resources/DownLoadErrorAhem.otf'), local('Courier New');
}
/* Test 5: Multiple errors */
@font-face {
font-family: myfont5;
src: url('resources/DownLoadErrorAhem.otf'), url(data:application/x-truetype-font,) format(truetype);
}
</style>
<script>
description('Test download error cases. "load" or "error" events should be fired exactly once for each @font-face rule.');
window.jsTestIsAsync = true;
var events = {};
function logEvent(e) {
var family = e.fontface.style.getPropertyValue('font-family');
if (!events[family])
events[family] = [];
events[family].push(e.type);
}
function runTests() {
document.fontloader.addEventListener('loadstart', logEvent);
document.fontloader.addEventListener('load', logEvent);
document.fontloader.addEventListener('error', logEvent);
document.fontloader.notifyWhenFontsReady(function() {
shouldBe("events['myfont1']", "['loadstart', 'error']");
shouldBe("events['myfont2']", "['loadstart', 'error']");
shouldBe("events['myfont3']", "['loadstart', 'error']");
shouldBe("events['myfont4']", "['loadstart', 'load']");
shouldBe("events['myfont5']", "['loadstart', 'error']");
finishJSTest();
});
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<div style="font-family: myfont1">font1</div>
<div style="font-family: myfont2">font2</div>
<div style="font-family: myfont3">font3</div>
<div style="font-family: myfont4">font4</div>
<div style="font-family: myfont5">font5</div>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test that the event handlers of FontLoader are called in the correct order.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
Hello, world!
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
@font-face {
font-family: 'ahem';
src: url(../../resources/Ahem.ttf);
}
</style>
<script>
description('Test that the event handlers of FontLoader are called in the correct order.');
window.jsTestIsAsync = true;
var event;
var numberOfEvents = 0;
function handleEvent(e) {
event = e;
shouldBeEqualToString('event.type', ['loading', 'loadstart', 'load', 'loadingdone'][numberOfEvents]);
numberOfEvents++;
}
function runTests() {
document.fontloader.onloading = handleEvent;
document.fontloader.onloadingdone = handleEvent;
document.fontloader.onloadstart = handleEvent;
document.fontloader.onload = handleEvent;
document.fontloader.onerror = handleEvent;
document.fontloader.notifyWhenFontsReady(function() {
shouldBe('numberOfEvents', '4');
finishJSTest();
});
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<div style="font-family: ahem">Hello, world!</div>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test that onloadingdone is fired after style recalculation.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
aaa
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
body {
font-family: serif;
}
@font-face {
font-family: 'ahem';
src: url(../../resources/Ahem.ttf);
}
#test {
position: absolute
}
#test span {
font-family: ahem;
}
</style>
<script>
description('Test that onloadingdone is fired after style recalculation.');
window.jsTestIsAsync = true;
function runTests() {
document.fontloader.onloadingdone = function() {
debug('loadingdone');
testDiv = document.getElementById('test');
shouldBeEqualToString('window.getComputedStyle(testDiv, 0).width', '48px');
};
document.fontloader.notifyWhenFontsReady(function() {
debug('notifyWhenFontsReady() callback');
shouldBeEqualToString('window.getComputedStyle(testDiv, 0).width', '48px');
finishJSTest();
});
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<div id=test>
<a href="#">
<span>aaa</span>
</a>
</div>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test load events for a font family consists of multiple @font-faces, including download error.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
@font-face {
font-family: TestFont;
src: url(../../resources/Ahem.ttf);
}
@font-face {
font-family: TestFont;
src: url('resources/DownLoadErrorAhem.otf');
unicode-range: u+61-7a; /* 'a'-'z' */
}
</style>
<script>
description('Test load events for a font family consists of multiple @font-faces, including download error.');
window.jsTestIsAsync = true;
var events = {};
function countEvent(e) {
if (!events[e.type])
events[e.type] = 0;
events[e.type] += 1;
}
function runTests() {
document.fontloader.addEventListener('loading', countEvent);
document.fontloader.addEventListener('loadstart', countEvent);
document.fontloader.addEventListener('load', countEvent);
document.fontloader.addEventListener('error', countEvent);
document.fontloader.addEventListener('loadingdone', countEvent);
document.fontloader.loadFont({ font: '10px TestFont', onsuccess: onsuccess, onerror: onerror });
document.fontloader.notifyWhenFontsReady(verify);
}
function onsuccess() {
testFailed("Expected error, but onsuccess() called");
onerror();
}
function onerror() {
shouldBe("events['loading']", "1");
shouldBe("events['loadstart']", "2");
shouldBe("events['load']", "1");
shouldBe("events['error']", "1");
shouldBe("events['loadingdone']", "undefined");
shouldBe("document.fontloader.checkFont('10px TestFont')", "false");
}
function verify() {
shouldBe("events['loadingdone']", "1");
finishJSTest();
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test load events for a font family consists of multiple @font-faces.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
@font-face {
font-family: TestFont;
src: local('Courier New');
}
@font-face {
font-family: TestFont;
src: url(../../resources/Ahem.ttf);
unicode-range: u+61-7a; /* 'a'-'z' */
}
</style>
<script>
description('Test load events for a font family consists of multiple @font-faces.');
window.jsTestIsAsync = true;
var events = {};
function countEvent(e) {
if (!events[e.type])
events[e.type] = 0;
events[e.type] += 1;
}
function runTests() {
document.fontloader.addEventListener('loading', countEvent);
document.fontloader.addEventListener('loadstart', countEvent);
document.fontloader.addEventListener('load', countEvent);
document.fontloader.addEventListener('error', countEvent);
document.fontloader.addEventListener('loadingdone', countEvent);
document.fontloader.loadFont({ font: '10px TestFont', onsuccess: onsuccess, onerror: onerror });
document.fontloader.notifyWhenFontsReady(verify);
}
function onsuccess() {
shouldBe("events['loading']", "1");
shouldBe("events['loadstart']", "2");
shouldBe("events['load']", "2");
shouldBe("events['error']", "undefined");
shouldBe("events['loadingdone']", "undefined");
shouldBe("document.fontloader.checkFont('10px TestFont')", "true");
}
function onerror() {
testFailed("Expected success, but onerror() called");
onsuccess();
}
function verify() {
shouldBe("events['loadingdone']", "1");
finishJSTest();
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test load events for fontloader.loadFont() with multiple font families.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
<html>
<head>
<script src="../js/resources/js-test-pre.js"></script>
<style>
@font-face {
font-family: TestFont1;
src: local('Courier New');
}
@font-face {
font-family: TestFont2;
src: url(../../resources/Ahem.ttf);
unicode-range: u+61-7a; /* 'a'-'z' */
}
</style>
<script>
description('Test load events for fontloader.loadFont() with multiple font families.');
window.jsTestIsAsync = true;
var events = {};
function countEvent(e) {
if (!events[e.type])
events[e.type] = 0;
events[e.type] += 1;
}
function runTests() {
document.fontloader.addEventListener('loading', countEvent);
document.fontloader.addEventListener('loadstart', countEvent);
document.fontloader.addEventListener('load', countEvent);
document.fontloader.addEventListener('error', countEvent);
document.fontloader.addEventListener('loadingdone', countEvent);
document.fontloader.loadFont({ font: '10px TestFont1, TestFont2', onsuccess: onsuccess, onerror: onerror });
document.fontloader.notifyWhenFontsReady(verify);
}
function onsuccess() {
shouldBe("events['loading']", "1");
shouldBe("events['loadstart']", "2");
shouldBe("events['load']", "2");
shouldBe("events['error']", "undefined");
shouldBe("events['loadingdone']", "undefined");
shouldBe("document.fontloader.checkFont('10px TestFont1, TestFont2')", "true");
}
function onerror() {
testFailed("Expected success, but onerror() called");
}
function verify() {
shouldBe("events['loadingdone']", "1");
finishJSTest();
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<script src="../js/resources/js-test-post.js"></script>
</body>
</html>
Test for FontLoader.loading attribute
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
FAIL document.fontloader does not exist
PASS successfullyParsed is true
TEST COMPLETE
<html>
<head>
<script src="../../js-test-resources/js-test-pre.js"></script>
<style>
@font-face {
font-family: 'TestFont';
src: url(slow-ahem-loading.cgi);
}
</style>
<script>
description('Test for FontLoader.loading attribute');
window.jsTestIsAsync = true;
function runTests() {
shouldBe("document.fontloader.checkFont('10px TestFont')", "false");
shouldBe("document.fontloader.loading", "false");
document.fontloader.loadFont({ font: '10px TestFont', onsuccess: onsuccess });
shouldBe("document.fontloader.loading", "true");
}
function onsuccess() {
shouldBe("document.fontloader.checkFont('10px TestFont')", "true");
shouldBe("document.fontloader.loading", "false");
finishJSTest();
}
if (document.fontloader)
runTests();
else {
testFailed('document.fontloader does not exist');
finishJSTest();
}
</script>
</head>
<body>
<script src="../../js-test-resources/js-test-post.js"></script>
</body>
</html>
Test download error cases. "load" or "error" events should be fired exactly once for each @font-face rule.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS events['myfont1'] is ['loadstart', 'error']
PASS events['myfont2'] is ['loadstart', 'error']
PASS events['myfont3'] is ['loadstart', 'error']
PASS events['myfont4'] is ['loadstart', 'load']
PASS events['myfont5'] is ['loadstart', 'error']
PASS successfullyParsed is true
TEST COMPLETE
font1
font2
font3
font4
font5
Test that the event handlers of FontLoader are called in the correct order.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS event.type is "loading"
PASS event.type is "loadstart"
PASS event.type is "load"
PASS event.type is "loadingdone"
PASS numberOfEvents is 4
PASS successfullyParsed is true
TEST COMPLETE
Hello, world!
Test that onloadingdone is fired after style recalculation.