Auto merge of #17141 - MortimerGoro:android_fonts, r=mbrubeck

Ged rid of libfontconfig in Android

Libfontconfig dependency is causing huge startup times in Android (20 seconds on each page load!). On other platforms fontconfig caches are already available or can be prebuilt on installation scripts, but this can't be done on Android. Updating libfontconfig dependency doesn't fix the problem either.

This PR gets rid of libfontconfig in Android. It queries available fonts and variations from Android System font configuration files. Android doesn't provide an API to query system fonts until Android O (which is very far from the minimum API right now...)

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [ ] These changes fix #16195 (github issue number if applicable).

<!-- Either: -->
- [x] There are tests for these changes OR
- [ ] These changes do not require tests because _____

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/17141)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2017-06-06 13:29:05 -07:00 committed by GitHub
commit f388c0ab1e
7 changed files with 498 additions and 11 deletions

1
Cargo.lock generated
View file

@ -1065,6 +1065,7 @@ dependencies = [
"unicode-script 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"webrender_traits 0.40.0 (git+https://github.com/servo/webrender)",
"xi-unicode 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"xml5ever 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View file

@ -50,8 +50,13 @@ core-text = "4.0"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
freetype = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
servo-fontconfig = "0.2.1"
[target.'cfg(target_os = "android")'.dependencies]
xml5ever = {version = "0.7", features = ["unstable"]}
[target.'cfg(any(target_feature = "sse2", target_feature = "neon"))'.dependencies]
simd = "0.2.0"

View file

@ -33,7 +33,7 @@ extern crate bitflags;
extern crate euclid;
extern crate fnv;
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(target_os = "linux")]
extern crate fontconfig;
extern crate fontsan;
#[cfg(any(target_os = "linux", target_os = "android"))]
@ -73,6 +73,8 @@ extern crate unicode_bidi;
extern crate unicode_script;
extern crate webrender_traits;
extern crate xi_unicode;
#[cfg(target_os = "android")]
extern crate xml5ever;
#[deny(unsafe_code)]
pub mod display_list;

View file

@ -0,0 +1,481 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use xml5ever::Attribute;
use xml5ever::driver::parse_document;
use xml5ever::rcdom::*;
use xml5ever::rcdom::{Node, RcDom};
use xml5ever::tendril::TendrilSink;
lazy_static! {
static ref FONT_LIST: FontList = FontList::new();
}
// Android doesn't provide an API to query system fonts until Android O:
// https://developer.android.com/reference/android/text/FontConfig.html
// System font configuration files must be parsed until Android O version is set as the minimum target.
// Android uses XML files to handle font mapping configurations.
// On Android API 21+ font mappings are loaded from /etc/fonts.xml.
// Each entry consists of a family with various font names, or a font alias.
// Example:
// <familyset>
// <!-- first font is default -->
// <family name="sans-serif">
// <font weight="100" style="normal">Roboto-Thin.ttf</font>
// <font weight="100" style="italic">Roboto-ThinItalic.ttf</font>
// <font weight="300" style="normal">Roboto-Light.ttf</font>
// <font weight="300" style="italic">Roboto-LightItalic.ttf</font>
// <font weight="400" style="normal">Roboto-Regular.ttf</font>
// <font weight="400" style="italic">Roboto-Italic.ttf</font>
// <font weight="500" style="normal">Roboto-Medium.ttf</font>
// <font weight="500" style="italic">Roboto-MediumItalic.ttf</font>
// <font weight="900" style="normal">Roboto-Black.ttf</font>
// <font weight="900" style="italic">Roboto-BlackItalic.ttf</font>
// <font weight="700" style="normal">Roboto-Bold.ttf</font>
// <font weight="700" style="italic">Roboto-BoldItalic.ttf</font>
// </family>//
// <!-- Note that aliases must come after the fonts they reference. -->
// <alias name="sans-serif-thin" to="sans-serif" weight="100" />
// <alias name="sans-serif-light" to="sans-serif" weight="300" />
// <alias name="sans-serif-medium" to="sans-serif" weight="500" />
// <alias name="sans-serif-black" to="sans-serif" weight="900" />
// <alias name="arial" to="sans-serif" />
// <alias name="helvetica" to="sans-serif" />
// <alias name="tahoma" to="sans-serif" />
// <alias name="verdana" to="sans-serif" />
// <family name="sans-serif-condensed">
// <font weight="300" style="normal">RobotoCondensed-Light.ttf</font>
// <font weight="300" style="italic">RobotoCondensed-LightItalic.ttf</font>
// <font weight="400" style="normal">RobotoCondensed-Regular.ttf</font>
// <font weight="400" style="italic">RobotoCondensed-Italic.ttf</font>
// <font weight="700" style="normal">RobotoCondensed-Bold.ttf</font>
// <font weight="700" style="italic">RobotoCondensed-BoldItalic.ttf</font>
// </family>
// <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" />
// </familyset>
// On Android API 17-20 font mappings are loaded from /system/etc/system_fonts.xml
// Each entry consists of a family with a nameset and a fileset.
// Example:
// <familyset>
// <family>
// <nameset>
// <name>sans-serif</name>
// <name>arial</name>
// <name>helvetica</name>
// <name>tahoma</name>
// <name>verdana</name>
// </nameset>
// <fileset>
// <file>Roboto-Regular.ttf</file>
// <file>Roboto-Bold.ttf</file>
// <file>Roboto-Italic.ttf</file>
// <file>Roboto-BoldItalic.ttf</file>
// </fileset>
// </family>//
// <family>
// <nameset>
// <name>sans-serif-light</name>
// </nameset>
// <fileset>
// <file>Roboto-Light.ttf</file>
// <file>Roboto-LightItalic.ttf</file>
// </fileset>
// </family>//
// <family>
// <nameset>
// <name>sans-serif-thin</name>
// </nameset>
// <fileset>
// <file>Roboto-Thin.ttf</file>
// <file>Roboto-ThinItalic.ttf</file>
// </fileset>
// </family>
// </familyset>
struct Font {
filename: String,
weight: Option<i32>,
}
struct FontFamily {
name: String,
fonts: Vec<Font>,
}
struct FontAlias {
from: String,
to: String,
weight: Option<i32>
}
struct FontList {
families: Vec<FontFamily>,
aliases: Vec<FontAlias>
}
impl FontList {
fn new() -> FontList {
// Possible paths containing the font mapping xml file.
let paths = [
"/etc/fonts.xml",
"/system/etc/system_fonts.xml"
];
// Try to load and parse paths until one of them success.
let mut result = None;
paths.iter().all(|path| {
result = Self::from_path(path);
!result.is_some()
});
match result {
Some(result) => result,
// If no xml mapping file is found fallback to some default
// fonts expected to be on all Android devices.
None => FontList {
families: Self::fallback_font_families(),
aliases: Vec::new(),
}
}
}
// Creates a new FontList from a path to the font mapping xml file.
fn from_path(path: &str) -> Option<FontList> {
let xml = match Self::load_file(path) {
Ok(xml) => xml,
_=> { return None; },
};
let dom: RcDom = parse_document(RcDom::default(), Default::default())
.one(xml);
let doc = &dom.document;
// find familyset root node
let children = doc.children.borrow();
let familyset = children.iter().find(|child| {
match child.data {
NodeData::Element { ref name, .. } => &*name.local == "familyset",
_ => false,
}
});
let familyset = match familyset {
Some(node) => node,
_ => { return None; }
};
// Parse familyset node
let mut families = Vec::new();
let mut aliases = Vec::new();
for node in familyset.children.borrow().iter() {
match node.data {
NodeData::Element { ref name, ref attrs, .. } => {
if &*name.local == "family" {
Self::parse_family(&node, attrs, &mut families);
} else if &*name.local == "alias" {
// aliases come after the fonts they reference. -->
if !families.is_empty() {
Self::parse_alias(attrs, &mut aliases);
}
}
},
_=> {}
}
}
Some(FontList {
families: families,
aliases: aliases
})
}
// Fonts expected to exist in Android devices.
// Only used in the unlikely case where no font xml mapping files are found.
fn fallback_font_families() -> Vec<FontFamily> {
let alternatives = [
("san-serif", "Roboto-Regular.ttf"),
("Droid Sans", "DroidSans.ttf"),
];
alternatives.iter().filter(|item| {
Path::new(&Self::font_absolute_path(item.1)).exists()
}).map(|item| {
FontFamily {
name: item.0.into(),
fonts: vec![Font {
filename: item.1.into(),
weight: None,
}]
}
}). collect()
}
// All Android fonts are located in /system/fonts
fn font_absolute_path(filename: &str) -> String {
format!("/system/fonts/{}", filename)
}
fn find_family(&self, name: &str) -> Option<&FontFamily>{
self.families.iter().find(|f| f.name == name)
}
fn find_alias(&self, name: &str) -> Option<&FontAlias>{
self.aliases.iter().find(|f| f.from == name)
}
fn load_file(path: &str) -> Result<String, io::Error> {
let mut file = try!(File::open(path));
let mut content = String::new();
try!(file.read_to_string(&mut content));
Ok(content)
}
// Parse family and font file names
// Example:
// <family name="sans-serif">
// <font weight="100" style="normal">Roboto-Thin.ttf</font>
// <font weight="100" style="italic">Roboto-ThinItalic.ttf</font>
// <font weight="300" style="normal">Roboto-Light.ttf</font>
// <font weight="300" style="italic">Roboto-LightItalic.ttf</font>
// <font weight="400" style="normal">Roboto-Regular.ttf</font>
// </family>
fn parse_family(familyset: &Node, attrs: &RefCell<Vec<Attribute>>, out:&mut Vec<FontFamily>) {
// Fallback to old Android API v17 xml format if required
let using_api_17 = familyset.children.borrow().iter().any(|node| {
match node.data {
NodeData::Element { ref name, .. } => &*name.local == "nameset",
_=> false,
}
});
if using_api_17 {
Self::parse_family_v17(familyset, out);
return;
}
// Parse family name
let name = match Self::find_attrib("name", attrs) {
Some(name) => name,
_ => { return; },
};
let mut fonts = Vec::new();
// Parse font variants
for node in familyset.children.borrow().iter() {
match node.data {
NodeData::Element { ref name, ref attrs, .. } => {
if &*name.local == "font" {
FontList::parse_font(&node, attrs, &mut fonts);
}
},
_=> {}
}
}
out.push(FontFamily {
name: name,
fonts: fonts
});
}
// Parse family and font file names for Androi API < 21
// Example:
// <family>
// <nameset>
// <name>sans-serif</name>
// <name>arial</name>
// <name>helvetica</name>
// <name>tahoma</name>
// <name>verdana</name>
// </nameset>
// <fileset>
// <file>Roboto-Regular.ttf</file>
// <file>Roboto-Bold.ttf</file>
// <file>Roboto-Italic.ttf</file>
// <file>Roboto-BoldItalic.ttf</file>
// </fileset>
// </family>
fn parse_family_v17(familyset: &Node, out:&mut Vec<FontFamily>) {
let mut nameset = Vec::new();
let mut fileset = Vec::new();
for node in familyset.children.borrow().iter() {
match node.data {
NodeData::Element { ref name, .. } => {
if &*name.local == "nameset" {
Self::collect_contents_with_tag(node, "name", &mut nameset);
} else if &*name.local == "fileset" {
Self::collect_contents_with_tag(node, "file", &mut fileset);
}
},
_=> {}
}
}
// Create a families for each variation
for name in nameset {
let fonts: Vec<Font> = fileset.iter().map(|f| Font {
filename: f.clone(),
weight: None,
}).collect();
if !fonts.is_empty() {
out.push(FontFamily {
name: name,
fonts: fonts
})
}
}
}
// Example:
// <font weight="100" style="normal">Roboto-Thin.ttf</font>
fn parse_font(node: &Node, attrs: &RefCell<Vec<Attribute>>, out:&mut Vec<Font>) {
// Parse font filename
let filename = match Self::text_content(node) {
Some(filename) => filename,
_ => { return; }
};
// Parse font weight
let weight = Self::find_attrib("weight", attrs).and_then(|w| w.parse().ok());
out.push(Font {
filename: filename,
weight: weight,
})
}
// Example:
// <alias name="sans-serif-thin" to="sans-serif" weight="100" />
// <alias name="sans-serif-light" to="sans-serif" weight="300" />
// <alias name="sans-serif-medium" to="sans-serif" weight="500" />
// <alias name="sans-serif-black" to="sans-serif" weight="900" />
// <alias name="arial" to="sans-serif" />
// <alias name="helvetica" to="sans-serif" />
// <alias name="tahoma" to="sans-serif" />
// <alias name="verdana" to="sans-serif" />
fn parse_alias(attrs: &RefCell<Vec<Attribute>>, out:&mut Vec<FontAlias>) {
// Parse alias name and referenced font
let from = match Self::find_attrib("name", attrs) {
Some(from) => from,
_ => { return; },
};
// Parse referenced font
let to = match Self::find_attrib("to", attrs) {
Some(to) => to,
_ => { return; },
};
// Parse optional weight filter
let weight = Self::find_attrib("weight", attrs).and_then(|w| w.parse().ok());
out.push(FontAlias {
from: from,
to: to,
weight: weight,
})
}
fn find_attrib(name: &str, attrs: &RefCell<Vec<Attribute>>) -> Option<String> {
attrs.borrow().iter().find(|attr| &*attr.name.local == name).map(|s| String::from(&s.value))
}
fn text_content(node: &Node) -> Option<String> {
node.children.borrow().get(0).and_then(|child| {
match child.data {
NodeData::Text { ref contents } => {
let mut result = String::new();
result.push_str(&contents.borrow());
Some(result)
},
_ => None
}
})
}
fn collect_contents_with_tag(node: &Node, tag: &str, out:&mut Vec<String>) {
for child in node.children.borrow().iter() {
match child.data {
NodeData::Element { ref name, .. } => {
if &*name.local == tag {
if let Some(content) = Self::text_content(child) {
out.push(content);
}
}
},
_=> {}
}
}
}
}
// Functions used by FontCacheThread
pub fn for_each_available_family<F>(mut callback: F) where F: FnMut(String) {
for family in &FONT_LIST.families {
callback(family.name.clone());
}
for alias in &FONT_LIST.aliases {
callback(alias.from.clone());
}
}
pub fn for_each_variation<F>(family_name: &str, mut callback: F)
where F: FnMut(String)
{
println!("Variatioooon {:?}", family_name);
if let Some(family) = FONT_LIST.find_family(family_name) {
for font in &family.fonts {
callback(FontList::font_absolute_path(&font.filename));
}
return;
}
if let Some(alias) = FONT_LIST.find_alias(family_name) {
if let Some(family) = FONT_LIST.find_family(&alias.to) {
for font in &family.fonts {
match (alias.weight, font.weight) {
(None, _) => callback(FontList::font_absolute_path(&font.filename)),
(Some(w1), Some(w2)) => {
if w1 == w2 {
callback(FontList::font_absolute_path(&font.filename))
}
},
_ => {}
}
}
}
}
}
pub fn system_default_family(generic_name: &str) -> Option<String> {
if let Some(family) = FONT_LIST.find_family(&generic_name) {
Some(family.name.clone())
} else if let Some(alias) = FONT_LIST.find_alias(&generic_name) {
Some(alias.from.clone())
} else {
// First font defined in the fonts.xml is the default on Android.
FONT_LIST.families.get(0).map(|family| family.name.clone())
}
}
pub fn last_resort_font_families() -> Vec<String> {
vec!(
"sans-serif".to_owned(),
"Droid Sans".to_owned(),
"serif".to_owned(),
)
}
pub static SANS_SERIF_FONT_FAMILY: &'static str = "sans-serif";

View file

@ -141,11 +141,6 @@ pub fn last_resort_font_families() -> Vec<String> {
)
}
#[cfg(target_os = "android")]
pub fn last_resort_font_families() -> Vec<String> {
vec!("Roboto".to_owned())
}
#[cfg(target_os = "windows")]
pub fn last_resort_font_families() -> Vec<String> {
vec!(
@ -153,9 +148,6 @@ pub fn last_resort_font_families() -> Vec<String> {
)
}
#[cfg(target_os = "android")]
pub static SANS_SERIF_FONT_FAMILY: &'static str = "Roboto";
#[cfg(target_os = "linux")]
pub static SANS_SERIF_FONT_FAMILY: &'static str = "DejaVu Sans";

View file

@ -29,8 +29,14 @@ mod freetype {
pub mod font;
pub mod font_context;
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(target_os = "linux")]
pub mod font_list;
#[cfg(target_os = "android")]
mod android {
pub mod font_list;
}
#[cfg(target_os = "android")]
pub use self::android::font_list;
#[cfg(any(target_os = "linux", target_os = "android"))]
pub mod font_template;

View file

@ -204,7 +204,7 @@ impl app::NestedEventLoopListener for BrowserWrapper {
#[cfg(target_os = "android")]
fn setup_logging() {
// Piping logs from stdout/stderr to logcat happens in android_injected_glue.
::std::env::set_var("RUST_LOG", "debug");
::std::env::set_var("RUST_LOG", "error");
unsafe { android_injected_glue::ffi::app_dummy() };
}